Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2120f49
add custom scripts
tomsonpl Jun 3, 2025
f4ac160
add runscript
tomsonpl Jun 3, 2025
a6589fc
fix types
tomsonpl Jun 4, 2025
b0ed7ae
fix types
tomsonpl Jun 4, 2025
a512883
fix types
tomsonpl Jun 4, 2025
1e5bd18
fix
tomsonpl Jun 4, 2025
c539e6e
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 4, 2025
e9ea856
fix tests
tomsonpl Jun 4, 2025
15ef33d
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 4, 2025
00a3c11
fix
tomsonpl Jun 5, 2025
fb2a520
fix
tomsonpl Jun 5, 2025
cc33315
feature flag
tomsonpl Jun 6, 2025
6b9efc2
fix
tomsonpl Jun 6, 2025
5d2f3f4
add tests
tomsonpl Jun 6, 2025
62e4191
more tests
tomsonpl Jun 6, 2025
3bbeede
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 6, 2025
a23c7c6
clean up
tomsonpl Jun 6, 2025
e7ff67d
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 6, 2025
bb6dcfe
fix
tomsonpl Jun 6, 2025
e69f780
fix
tomsonpl Jun 6, 2025
47f65a7
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 6, 2025
6dc08ae
fix
tomsonpl Jun 6, 2025
1ee8e71
update snapshot
tomsonpl Jun 6, 2025
4627a16
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 9, 2025
a9ba3b2
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 9, 2025
0f7344d
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 10, 2025
9575d8e
download file support
tomsonpl Jun 11, 2025
5b44314
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 11, 2025
ca34db9
Update x-pack/solutions/security/plugins/security_solution/public/man…
tomsonpl Jun 11, 2025
f38934e
add some logging
tomsonpl Jun 11, 2025
8b935cf
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 11, 2025
f385f85
fix
tomsonpl Jun 11, 2025
a79a358
fix tests
tomsonpl Jun 11, 2025
f3607b1
add tests
tomsonpl Jun 11, 2025
bf7abf3
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 11, 2025
bfd7900
add tests
tomsonpl Jun 11, 2025
daf4b04
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 11, 2025
0d6c838
add tests and rename checkPendingIsolateReleaseActions
tomsonpl Jun 11, 2025
8e79496
fix indentation
tomsonpl Jun 11, 2025
a8d4e29
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 11, 2025
f3d9fc6
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 12, 2025
9755961
turn off feature flag
tomsonpl Jun 12, 2025
acc07b8
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 12, 2025
e7395a2
remove redundant validation
tomsonpl Jun 16, 2025
f38f073
change schema and add tests
tomsonpl Jun 16, 2025
354b480
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 16, 2025
79646a1
fix file download
tomsonpl Jun 16, 2025
8ac0377
fix types
tomsonpl Jun 16, 2025
7f10728
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 16, 2025
af5a749
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 16, 2025
12bdbe2
fix test
tomsonpl Jun 16, 2025
d2eb09b
Merge remote-tracking branch 'origin/mde-custom-scripts' into mde-cus…
tomsonpl Jun 16, 2025
85874b6
apply cr comments
tomsonpl Jun 17, 2025
9c43e68
remove tests
tomsonpl Jun 17, 2025
4ac616e
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 17, 2025
7554043
Merge branch 'main' into mde-custom-scripts
tomsonpl Jun 17, 2025
a115a5f
fix test
tomsonpl Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,7 @@ export const GetActionsParamsSchema = schema.object({
});

export const GetActionResultsParamsSchema = schema.object({
id: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
id: schema.string({ minLength: 1 }),
});

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

export const DownloadActionResultsResponseSchema = schema.stream();

// ----------------------------------
// Connector Sub-Actions
// ----------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface MicrosoftDefenderEndpointGetActionsResponse {

export interface MicrosoftDefenderEndpointGetActionResultsResponse {
'@odata.context': string;
value: string[]; // Downloadable link
value: string; // Downloadable link
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,121 @@ describe('Microsoft Defender for Endpoint Connector', () => {
);
});
});

describe('#getActionResults()', () => {
it('should call Microsoft Defender API to retrieve action results download link', async () => {
const actionId = 'test-action-123';
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';

// Mock only the external download URL (Microsoft Defender API is mocked in mocks.ts)
connectorMock.apiMock[mockDownloadUrl] = () =>
microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({
pipe: jest.fn(),
on: jest.fn(),
read: jest.fn(),
});

await connectorMock.instanceMock.getActionResults(
{ id: actionId },
connectorMock.usageCollector
);

expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: `https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`,
method: 'GET',
}),
connectorMock.usageCollector
);

expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: mockDownloadUrl,
method: 'get',
responseType: 'stream',
}),
connectorMock.usageCollector
);
});

it('should return a Stream for downloading the file', async () => {
const actionId = 'test-action-123';
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';

// Mock external download URL to return a stream (Microsoft Defender API uses default mock)
const mockStream = { pipe: jest.fn(), on: jest.fn(), read: jest.fn() };
connectorMock.apiMock[mockDownloadUrl] = () =>
microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock(mockStream);

const result = await connectorMock.instanceMock.getActionResults(
{ id: actionId },
connectorMock.usageCollector
);

expect(result).toEqual(mockStream);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: mockDownloadUrl,
method: 'get',
responseType: 'stream',
}),
connectorMock.usageCollector
);
});

it('should error if download URL is not found in API response', async () => {
const actionId = 'test-action-123';

// Override the default mock to return null
connectorMock.apiMock[
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
] = () => microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({ value: null });

await expect(
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
).rejects.toThrow(`Download URL for script results of machineId [${actionId}] not found`);
});

it('should error if download URL is empty string in API response', async () => {
const actionId = 'test-action-123';

// Override the default mock to return empty string
connectorMock.apiMock[
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
] = () => microsoftDefenderEndpointConnectorMocks.createAxiosResponseMock({ value: '' });

await expect(
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
).rejects.toThrow(`Download URL for script results of machineId [${actionId}] not found`);
});

it('should handle Microsoft Defender API errors for download link retrieval', async () => {
const actionId = 'test-action-123';

// Override the default mock to throw an error
connectorMock.apiMock[
`https://api.mock__microsoft.com/api/machineactions/${actionId}/GetLiveResponseResultDownloadLink(index=0)`
] = () => {
throw new Error('Microsoft Defender API error');
};

await expect(
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
).rejects.toThrow('Microsoft Defender API error');
});

it('should handle file download errors', async () => {
const actionId = 'test-action-123';
const mockDownloadUrl = 'https://download.microsoft.com/mock-download-url/results.json';

// Mock external download URL to throw an error (Microsoft Defender API uses default mock)
connectorMock.apiMock[mockDownloadUrl] = () => {
throw new Error('File download failed');
};

await expect(
connectorMock.instanceMock.getActionResults({ id: actionId }, connectorMock.usageCollector)
).rejects.toThrow('File download failed');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SubActionConnector } from '@kbn/actions-plugin/server';
import type { AxiosError, AxiosResponse } from 'axios';
import type { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import type { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
import type { Stream } from 'stream';
import { OAuthTokenManager } from './o_auth_token_manager';
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '../../../common/microsoft_defender_endpoint/constants';
import {
Expand All @@ -23,6 +24,7 @@ import {
RunScriptParamsSchema,
MicrosoftDefenderEndpointEmptyParamsSchema,
GetActionResultsParamsSchema,
DownloadActionResultsResponseSchema,
} from '../../../common/microsoft_defender_endpoint/schema';
import type {
MicrosoftDefenderEndpointAgentDetailsParams,
Expand Down Expand Up @@ -450,16 +452,36 @@ export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
public async getActionResults(
{ id }: MicrosoftDefenderEndpointGetActionsParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointGetActionResultsResponse> {
): Promise<Stream> {
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/get-live-response-result

return this.fetchFromMicrosoft<MicrosoftDefenderEndpointGetActionResultsResponse>(
const resultDownloadLink =
await this.fetchFromMicrosoft<MicrosoftDefenderEndpointGetActionResultsResponse>(
{
url: `${this.urls.machineActions}/${id}/GetLiveResponseResultDownloadLink(index=0)`, // We want to download the first result
method: 'GET',
},
connectorUsageCollector
);
this.logger.debug(
() => `script results for machineId [${id}]:\n${JSON.stringify(resultDownloadLink)}`
);

const fileUrl = resultDownloadLink.value;

if (!fileUrl) {
throw new Error(`Download URL for script results of machineId [${id}] not found`);
}
const downloadConnection = await this.request(
{
url: `${this.urls.machineActions}/${id}/GetLiveResponseResultDownloadLink(index=0)`, // We want to download the first result
method: 'GET',
url: fileUrl,
method: 'get',
responseType: 'stream',
responseSchema: DownloadActionResultsResponseSchema,
},
connectorUsageCollector
);
return downloadConnection.data;
}

public async getLibraryFiles(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ const createMicrosoftDefenderConnectorMock = (): CreateMicrosoftDefenderConnecto
'@odata.context': 'https://api-us3.securitycenter.microsoft.com/api/$metadata#Machines',
value: [createMicrosoftMachineMock()],
}),

// GetActionResults - GetLiveResponseResultDownloadLink (default for test action IDs)
[`${apiUrl}/api/machineactions/test-action-123/GetLiveResponseResultDownloadLink(index=0)`]:
() =>
createAxiosResponseMock({
value: 'https://download.microsoft.com/mock-download-url/results.json',
}),
};

instanceMock.request.mockImplementation(
Expand Down
Loading