Skip to content

Commit 18f606e

Browse files
authored
[EDR Workflows] Add runscript from Microsoft Defender for Endpoint support to Response Console (#222377)
1 parent fe80f76 commit 18f606e

29 files changed

Lines changed: 1862 additions & 256 deletions

File tree

x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap

Lines changed: 12 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/schema.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,7 @@ export const GetActionsParamsSchema = schema.object({
219219
});
220220

221221
export const GetActionResultsParamsSchema = schema.object({
222-
id: schema.maybe(
223-
schema.oneOf([
224-
schema.string({ minLength: 1 }),
225-
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
226-
])
227-
),
222+
id: schema.string({ minLength: 1 }),
228223
});
229224

230225
export const MSDefenderLibraryFileSchema = schema.object(
@@ -249,6 +244,8 @@ export const GetLibraryFilesResponse = schema.object(
249244
{ unknowns: 'allow' }
250245
);
251246

247+
export const DownloadActionResultsResponseSchema = schema.stream();
248+
252249
// ----------------------------------
253250
// Connector Sub-Actions
254251
// ----------------------------------

x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export interface MicrosoftDefenderEndpointGetActionsResponse {
6464

6565
export interface MicrosoftDefenderEndpointGetActionResultsResponse {
6666
'@odata.context': string;
67-
value: string[]; // Downloadable link
67+
value: string; // Downloadable link
6868
}
6969

7070
/**

x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,121 @@ describe('Microsoft Defender for Endpoint Connector', () => {
211211
);
212212
});
213213
});
214+
215+
describe('#getActionResults()', () => {
216+
it('should call Microsoft Defender API to retrieve action results download link', async () => {
217+
const actionId = 'test-action-123';
218+
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';
219+
220+
// Mock only the external download URL (Microsoft Defender API is mocked in mocks.ts)
221+
connectorMock.apiMock[mockDownloadUrl] = () =>
222+
microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({
223+
pipe: jest.fn(),
224+
on: jest.fn(),
225+
read: jest.fn(),
226+
});
227+
228+
await connectorMock.instanceMock.getActionResults(
229+
{ id: actionId },
230+
connectorMock.usageCollector
231+
);
232+
233+
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
234+
expect.objectContaining({
235+
url: `https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`,
236+
method: 'GET',
237+
}),
238+
connectorMock.usageCollector
239+
);
240+
241+
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
242+
expect.objectContaining({
243+
url: mockDownloadUrl,
244+
method: 'get',
245+
responseType: 'stream',
246+
}),
247+
connectorMock.usageCollector
248+
);
249+
});
250+
251+
it('should return a Stream for downloading the file', async () => {
252+
const actionId = 'test-action-123';
253+
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';
254+
255+
// Mock external download URL to return a stream (Microsoft Defender API uses default mock)
256+
const mockStream = { pipe: jest.fn(), on: jest.fn(), read: jest.fn() };
257+
connectorMock.apiMock[mockDownloadUrl] = () =>
258+
microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock(mockStream);
259+
260+
const result = await connectorMock.instanceMock.getActionResults(
261+
{ id: actionId },
262+
connectorMock.usageCollector
263+
);
264+
265+
expect(result).toEqual(mockStream);
266+
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
267+
expect.objectContaining({
268+
url: mockDownloadUrl,
269+
method: 'get',
270+
responseType: 'stream',
271+
}),
272+
connectorMock.usageCollector
273+
);
274+
});
275+
276+
it('should error if download URL is not found in API response', async () => {
277+
const actionId = 'test-action-123';
278+
279+
// Override the default mock to return null
280+
connectorMock.apiMock[
281+
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
282+
] = () => microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({ value: null });
283+
284+
await expect(
285+
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
286+
).rejects.toThrow(`Download URL for script results of machineId [${actionId}] not found`);
287+
});
288+
289+
it('should error if download URL is empty string in API response', async () => {
290+
const actionId = 'test-action-123';
291+
292+
// Override the default mock to return empty string
293+
connectorMock.apiMock[
294+
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
295+
] = () => microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({ value: '' });
296+
297+
await expect(
298+
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
299+
).rejects.toThrow(`Download URL for script results of machineId [${actionId}] not found`);
300+
});
301+
302+
it('should handle Microsoft Defender API errors for download link retrieval', async () => {
303+
const actionId = 'test-action-123';
304+
305+
// Override the default mock to throw an error
306+
connectorMock.apiMock[
307+
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
308+
] = () => {
309+
throw new Error('Microsoft Defender API error');
310+
};
311+
312+
await expect(
313+
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
314+
).rejects.toThrow('Microsoft Defender API error');
315+
});
316+
317+
it('should handle file download errors', async () => {
318+
const actionId = 'test-action-123';
319+
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';
320+
321+
// Mock external download URL to throw an error (Microsoft Defender API uses default mock)
322+
connectorMock.apiMock[mockDownloadUrl] = () => {
323+
throw new Error('File download failed');
324+
};
325+
326+
await expect(
327+
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
328+
).rejects.toThrow('File download failed');
329+
});
330+
});
214331
});

x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SubActionConnector } from '@kbn/actions-plugin/server';
1010
import type { AxiosError, AxiosResponse } from 'axios';
1111
import type { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
1212
import type { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
13+
import type { Stream } from 'stream';
1314
import { OAuthTokenManager } from './o_auth_token_manager';
1415
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '../../../common/microsoft_defender_endpoint/constants';
1516
import {
@@ -23,6 +24,7 @@ import {
2324
RunScriptParamsSchema,
2425
MicrosoftDefenderEndpointEmptyParamsSchema,
2526
GetActionResultsParamsSchema,
27+
DownloadActionResultsResponseSchema,
2628
} from '../../../common/microsoft_defender_endpoint/schema';
2729
import type {
2830
MicrosoftDefenderEndpointAgentDetailsParams,
@@ -450,16 +452,36 @@ export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
450452
public async getActionResults(
451453
{ id }: MicrosoftDefenderEndpointGetActionsParams,
452454
connectorUsageCollector: ConnectorUsageCollector
453-
): Promise<MicrosoftDefenderEndpointGetActionResultsResponse> {
455+
): Promise<Stream> {
454456
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/get-live-response-result
455457

456-
return this.fetchFromMicrosoft<MicrosoftDefenderEndpointGetActionResultsResponse>(
458+
const resultDownloadLink =
459+
await this.fetchFromMicrosoft<MicrosoftDefenderEndpointGetActionResultsResponse>(
460+
{
461+
url: `${this.urls.machineActions}/${id}/GetLiveResponseResultDownloadLink(index=0)`, // We want to download the first result
462+
method: 'GET',
463+
},
464+
connectorUsageCollector
465+
);
466+
this.logger.debug(
467+
() => `script results for machineId [${id}]:\n${JSON.stringify(resultDownloadLink)}`
468+
);
469+
470+
const fileUrl = resultDownloadLink.value;
471+
472+
if (!fileUrl) {
473+
throw new Error(`Download URL for script results of machineId [${id}] not found`);
474+
}
475+
const downloadConnection = await this.request(
457476
{
458-
url: `${this.urls.machineActions}/${id}/GetLiveResponseResultDownloadLink(index=0)`, // We want to download the first result
459-
method: 'GET',
477+
url: fileUrl,
478+
method: 'get',
479+
responseType: 'stream',
480+
responseSchema: DownloadActionResultsResponseSchema,
460481
},
461482
connectorUsageCollector
462483
);
484+
return downloadConnection.data;
463485
}
464486

465487
public async getLibraryFiles(

x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/mocks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ const createMicrosoftDefenderConnectorMock = (): CreateMicrosoftDefenderConnecto
154154
'@odata.context': 'https://api-us3.securitycenter.microsoft.com/api/$metadata#Machines',
155155
value: [createMicrosoftMachineMock()],
156156
}),
157+
158+
// GetActionResults - GetLiveResponseResultDownloadLink (default for test action IDs)
159+
[`${apiUrl}/api/machineactions/test-action-123/GetLiveResponseResultDownloadLink(index=0)`]:
160+
() =>
161+
createAxiosResponseMock({
162+
value: 'https://download.microsoft.com/mock-download-url/results.json',
163+
}),
157164
};
158165

159166
instanceMock.request.mockImplementation(

0 commit comments

Comments
 (0)