diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 83496fa37b85b..a7c22bc8dcaee 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -10652,89 +10652,28 @@ Object { "keys": Object { "id": Object { "flags": Object { - "default": [Function], "error": [Function], - "presence": "optional", }, - "matches": Array [ + "metas": Array [ Object { - "schema": Object { - "flags": Object { - "error": [Function], - }, - "metas": Array [ - Object { - "x-oas-min-length": 1, - }, - ], - "rules": Array [ - Object { - "args": Object { - "method": [Function], - }, - "name": "custom", - }, - Object { - "args": Object { - "method": [Function], - }, - "name": "custom", - }, - ], - "type": "string", - }, + "x-oas-min-length": 1, }, + ], + "rules": Array [ Object { - "schema": Object { - "flags": Object { - "error": [Function], - }, - "items": Array [ - Object { - "flags": Object { - "error": [Function], - "presence": "optional", - }, - "metas": Array [ - Object { - "x-oas-min-length": 1, - }, - ], - "rules": Array [ - Object { - "args": Object { - "method": [Function], - }, - "name": "custom", - }, - Object { - "args": Object { - "method": [Function], - }, - "name": "custom", - }, - ], - "type": "string", - }, - ], - "rules": Array [ - Object { - "args": Object { - "limit": 1, - }, - "name": "min", - }, - ], - "type": "array", + "args": Object { + "method": [Function], }, + "name": "custom", }, - ], - "metas": Array [ Object { - "x-oas-optional": true, + "args": Object { + "method": [Function], + }, + "name": "custom", }, ], - "type": "alternatives", + "type": "string", }, }, "type": "object", diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/schema.ts b/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/schema.ts index 7ffab000ad413..051357a5f9943 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/schema.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/schema.ts @@ -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( @@ -249,6 +244,8 @@ export const GetLibraryFilesResponse = schema.object( { unknowns: 'allow' } ); +export const DownloadActionResultsResponseSchema = schema.stream(); + // ---------------------------------- // Connector Sub-Actions // ---------------------------------- diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/types.ts b/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/types.ts index 32c83c13b3731..adee61f85ea25 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/types.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint/types.ts @@ -64,7 +64,7 @@ export interface MicrosoftDefenderEndpointGetActionsResponse { export interface MicrosoftDefenderEndpointGetActionResultsResponse { '@odata.context': string; - value: string[]; // Downloadable link + value: string; // Downloadable link } /** diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.test.ts index 07fb0b11ac6d8..ee6687a18d67c 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.test.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.test.ts @@ -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'); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts index bea295b024c56..6cefb7ce2f698 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts @@ -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 { @@ -23,6 +24,7 @@ import { RunScriptParamsSchema, MicrosoftDefenderEndpointEmptyParamsSchema, GetActionResultsParamsSchema, + DownloadActionResultsResponseSchema, } from '../../../common/microsoft_defender_endpoint/schema'; import type { MicrosoftDefenderEndpointAgentDetailsParams, @@ -450,16 +452,36 @@ export class MicrosoftDefenderEndpointConnector extends SubActionConnector< public async getActionResults( { id }: MicrosoftDefenderEndpointGetActionsParams, connectorUsageCollector: ConnectorUsageCollector - ): Promise { + ): Promise { // API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/get-live-response-result - return this.fetchFromMicrosoft( + const resultDownloadLink = + await this.fetchFromMicrosoft( + { + 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( diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/mocks.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/mocks.ts index 7b5039b997a49..d71a40f9a7fdd 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/mocks.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint/mocks.ts @@ -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( diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts index 95c035e866884..9ed2d1fd9b705 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts @@ -10,49 +10,83 @@ import { schema } from '@kbn/config-schema'; import { BaseActionRequestSchema } from '../../common/base'; const { parameters, ...restBaseSchema } = BaseActionRequestSchema; -const NonEmptyString = schema.string({ - minLength: 1, - validate: (value) => { - if (!value.trim().length) { - return 'Raw cannot be an empty string'; - } +const getNonEmptyString = (fieldName: string) => + schema.string({ + minLength: 1, + validate: (value) => { + if (!value.trim().length) { + return `${fieldName} cannot be an empty string`; + } + }, + }); + +// CrowdStrike schemas +const CrowdStrikeRunScriptActionRequestParamsSchema = schema.object( + { + /** + * The script to run + */ + raw: schema.maybe(getNonEmptyString('Raw')), + /** + * The path to the script on the host to run + */ + hostPath: schema.maybe(getNonEmptyString('HostPath')), + /** + * The path to the script in the cloud to run + */ + cloudFile: schema.maybe(getNonEmptyString('CloudFile')), + /** + * The command line to run + */ + commandLine: schema.maybe(getNonEmptyString('CommandLine')), + /** + * The max timeout value before the command is killed. Number represents milliseconds + */ + timeout: schema.maybe(schema.number({ min: 1 })), }, + { + validate: (params) => { + if (!params.raw && !params.hostPath && !params.cloudFile) { + return 'At least one of Raw, HostPath, or CloudFile must be provided'; + } + }, + } +); + +// Microsoft Defender Endpoint schemas +export const MSDefenderEndpointRunScriptActionRequestParamsSchema = schema.object({ + /** + * The path to the script in the cloud to run + */ + scriptName: getNonEmptyString('ScriptName'), + args: schema.maybe(getNonEmptyString('Args')), }); + +export const MSDefenderEndpointRunScriptActionRequestSchema = { + body: schema.object({ + ...restBaseSchema, + parameters: MSDefenderEndpointRunScriptActionRequestParamsSchema, + }), +}; + export const RunScriptActionRequestSchema = { body: schema.object({ ...restBaseSchema, - parameters: schema.object( - { - /** - * The script to run - */ - raw: schema.maybe(NonEmptyString), - /** - * The path to the script on the host to run - */ - hostPath: schema.maybe(NonEmptyString), - /** - * The path to the script in the cloud to run - */ - cloudFile: schema.maybe(NonEmptyString), - /** - * The command line to run - */ - commandLine: schema.maybe(NonEmptyString), - /** - * The max timeout value before the command is killed. Number represents milliseconds - */ - timeout: schema.maybe(schema.number({ min: 1 })), - }, - { - validate: (params) => { - if (!params.raw && !params.hostPath && !params.cloudFile) { - return 'At least one of Raw, HostPath, or CloudFile must be provided'; - } - }, - } + parameters: schema.conditional( + schema.siblingRef('agent_type'), + 'crowdstrike', + CrowdStrikeRunScriptActionRequestParamsSchema, + schema.conditional( + schema.siblingRef('agent_type'), + 'microsoft_defender_endpoint', + MSDefenderEndpointRunScriptActionRequestParamsSchema, + schema.never() + ) ), }), }; +export type MSDefenderRunScriptActionRequestParams = TypeOf< + typeof MSDefenderEndpointRunScriptActionRequestParamsSchema +>; export type RunScriptActionRequestBody = TypeOf; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/schema/actions.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/schema/actions.test.ts index c7ce775bb2129..0a740f67f084e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/schema/actions.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/schema/actions.test.ts @@ -22,6 +22,7 @@ import { ExecuteActionRequestSchema, ScanActionRequestSchema, NoParametersRequestSchema, + RunScriptActionRequestSchema, } from '../../api/endpoint'; // NOTE: Even though schemas are kept in common/api/endpoint - we keep tests here, because common/api should import from outside @@ -830,4 +831,232 @@ describe('actions schemas', () => { }).not.toThrow(); }); }); + + describe('RunScriptActionRequestSchema', () => { + describe('CrowdStrike agent type', () => { + const validCrowdStrikeBase = { + endpoint_ids: ['endpoint_id'], + agent_type: 'crowdstrike' as const, + }; + it('should accept valid raw parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + }, + }); + }).not.toThrow(); + }); + + it('should accept valid hostPath parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + hostPath: '/path/to/script.ps1', + }, + }); + }).not.toThrow(); + }); + + it('should accept valid cloudFile parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + cloudFile: 'cloud-script-id', + }, + }); + }).not.toThrow(); + }); + + it('should accept multiple parameters together', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + commandLine: '-ProcessName explorer', + timeout: 30000, + }, + }); + }).not.toThrow(); + }); + + it('should accept valid timeout parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + timeout: 60000, + }, + }); + }).not.toThrow(); + }); + + it('should accept valid commandLine parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + commandLine: '-ProcessName explorer', + }, + }); + }).not.toThrow(); + }); + + it('should reject empty raw parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: ' ', + }, + }); + }).toThrow('Raw cannot be an empty string'); + }); + + it('should reject empty hostPath parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + hostPath: ' ', + }, + }); + }).toThrow('HostPath cannot be an empty string'); + }); + + it('should reject empty cloudFile parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + cloudFile: ' ', + }, + }); + }).toThrow('CloudFile cannot be an empty string'); + }); + + it('should reject when no required parameters are provided', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + commandLine: '-ProcessName explorer', + }, + }); + }).toThrow('At least one of Raw, HostPath, or CloudFile must be provided'); + }); + + it('should reject when parameters object is empty', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: {}, + }); + }).toThrow('At least one of Raw, HostPath, or CloudFile must be provided'); + }); + + it('should reject negative timeout values', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + timeout: -1, + }, + }); + }).toThrow(); + }); + + it('should reject zero timeout values', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validCrowdStrikeBase, + parameters: { + raw: 'Get-Process', + timeout: 0, + }, + }); + }).toThrow(); + }); + }); + + describe('Microsoft Defender Endpoint agent type', () => { + const validMdeBase = { + endpoint_ids: ['endpoint_id'], + agent_type: 'microsoft_defender_endpoint' as const, + }; + + it('should accept valid scriptName parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: { + scriptName: 'MyScript.ps1', + }, + }); + }).not.toThrow(); + }); + + it('should accept scriptName with args parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: { + scriptName: 'MyScript.ps1', + args: '-Parameter Value', + }, + }); + }).not.toThrow(); + }); + + it('should reject empty scriptName parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: { + scriptName: '', + }, + }); + }).toThrow(); + }); + + it('should reject when scriptName is missing', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: { + args: '-Parameter Value', + }, + }); + }).toThrow('[parameters.scriptName]: expected value of type [string] but got [undefined]'); + }); + + it('should reject when parameters object is empty', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: {}, + }); + }).toThrow('[parameters.scriptName]: expected value of type [string] but got [undefined]'); + }); + + it('should reject empty args parameter', () => { + expect(() => { + RunScriptActionRequestSchema.body.validate({ + ...validMdeBase, + parameters: { + scriptName: 'MyScript.ps1', + args: '', + }, + }); + }).toThrow(); + }); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts index 5776ff0dd3432..18aecd8073b74 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts @@ -155,7 +155,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { endpoint: false, sentinel_one: false, crowdstrike: true, - microsoft_defender_endpoint: false, + microsoft_defender_endpoint: true, }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts index aefa215529faa..9f2ed9b38b41e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts @@ -13,6 +13,7 @@ import type { ActionStatusRequestSchema, KillProcessRequestBody, SuspendProcessRequestBody, + RunScriptActionRequestBody, } from '../../api/endpoint'; import type { @@ -250,20 +251,7 @@ export interface ResponseActionScanParameters { path: string; } -// Currently reflecting CrowdStrike's RunScript parameters -interface ActionsRunScriptParametersBase { - Raw?: string; - HostPath?: string; - CloudFile?: string; - CommandLine?: string; - Timeout?: number; -} - -// Enforce at least one of the script parameters is required -export type ResponseActionRunScriptParameters = AtLeastOne< - ActionsRunScriptParametersBase, - 'Raw' | 'HostPath' | 'CloudFile' ->; +export type ResponseActionRunScriptParameters = RunScriptActionRequestBody['parameters']; export type EndpointActionDataParameterTypes = | undefined @@ -622,7 +610,3 @@ export interface ResponseActionUploadOutputContent { /** The free space available (after saving the file) of the drive where the file was saved to, In Bytes */ disk_free_space: number; } - -type AtLeastOne = K extends keyof T - ? Required> & Partial> - : never; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/microsoft_defender_endpoint.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/microsoft_defender_endpoint.ts index fe2eb92573169..4534471b46812 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/microsoft_defender_endpoint.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/microsoft_defender_endpoint.ts @@ -31,3 +31,11 @@ export interface MicrosoftDefenderEndpointLogEsDoc { defender_endpoint: Record; }; } + +export interface MicrosoftDefenderEndpointActionRequestFileMeta + extends MicrosoftDefenderEndpointActionRequestCommonMeta { + // Timestamp of when the file was created + createdAt: string; + // Name of the file + filename: string; +} diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index b277edb66ff7b..8318f0a507d45 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -264,6 +264,12 @@ export const allowedExperimentalValues = Object.freeze({ */ securityAIPromptsEnabled: false, + /** + * Enables Microsoft Defender for Endpoint's RunScript RTR command + * Release: 8.19/9.1 + */ + microsoftDefenderEndpointRunScriptEnabled: false, + /** * Enables advanced mode for Trusted Apps creation and update */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts index e9d8c9acbb704..b4977fb403afb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts @@ -212,6 +212,11 @@ export const CONSOLE_COMMANDS = { }, }, }, + runscript: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.runscript.about', { + defaultMessage: 'Run a script on the host', + }), + }, }; export const CROWDSTRIKE_CONSOLE_COMMANDS = { @@ -259,10 +264,7 @@ export const CROWDSTRIKE_CONSOLE_COMMANDS = { }, }, title: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.title', { - defaultMessage: 'Isolate', - }), - about: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.about', { - defaultMessage: 'Run a script on the host', + defaultMessage: 'Run Script', }), helpUsage: i18n.translate('xpack.securitySolution.crowdStrikeConsoleCommands.runscript.about', { defaultMessage: `Command Examples for Running Scripts: @@ -295,6 +297,45 @@ export const CROWDSTRIKE_CONSOLE_COMMANDS = { }, }; +export const MS_DEFENDER_ENDPOINT_CONSOLE_COMMANDS = { + runscript: { + args: { + scriptName: { + about: i18n.translate( + 'xpack.securitySolution.msDefenderEndpointConsoleCommands.runscript.args.scriptName.about', + { + defaultMessage: 'Script name in Files Library', + } + ), + }, + args: { + about: i18n.translate( + 'xpack.securitySolution.msDefenderEndpointConsoleCommands.runscript.args.args.about', + { + defaultMessage: 'Command line arguments', + } + ), + }, + }, + + helpUsage: i18n.translate( + 'xpack.securitySolution.msDefenderEndpointConsoleCommands.runscript.about', + { + defaultMessage: `Command Examples for Running Scripts: + runscript --ScriptName="CloudScript1.ps1" --Args="--Verbose true" +`, + } + ), + privileges: i18n.translate( + 'xpack.securitySolution.msDefenderEndpointConsoleCommands.runscript.privileges', + { + defaultMessage: + 'Insufficient privileges to run script. Contact your Kibana administrator if you think you should have this permission.', + } + ), + }, +}; + export const CONFIRM_WARNING_MODAL_LABELS = (entryType: string) => { return { title: i18n.translate('xpack.securitySolution.artifacts.confirmWarningModal.title', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx index 09d1e45f24c45..d300022e9db0d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx @@ -29,6 +29,7 @@ export interface ExecuteActionHostResponseProps { textSize?: 'xs' | 's'; hideFile?: boolean; hideContext?: boolean; + showPasscode?: boolean; } // Note: also used for RunScript command @@ -41,6 +42,7 @@ export const ExecuteActionHostResponse = memo( 'data-test-subj': dataTestSubj, hideFile, hideContext, + showPasscode, }) => { const outputContent = useMemo( () => @@ -60,6 +62,7 @@ export const ExecuteActionHostResponse = memo( canAccessFileDownloadLink={canAccessFileDownloadLink} data-test-subj={`${dataTestSubj}-getExecuteLink`} textSize={textSize} + showPasscode={showPasscode} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx index e2ed358455027..1a67b25cf667b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx @@ -17,17 +17,22 @@ import type { ResponseActionRunScriptParameters, } from '../../../../../common/endpoint/types'; import type { ActionRequestComponentProps } from '../types'; +export interface CrowdStrikeRunScriptActionParameters { + Raw?: string[]; + HostPath?: string[]; + CloudFile?: string[]; + CommandLine?: string[]; + Timeout?: number[]; +} + +export interface MicrosoftDefenderEndpointRunScriptActionParameters { + ScriptName: string[]; + Args?: string[]; +} export const RunScriptActionResult = memo< ActionRequestComponentProps< - { - Raw?: string; - HostPath?: string; - CloudFile?: string; - CommandLine?: string; - Timeout?: number; - comment?: string; - }, + CrowdStrikeRunScriptActionParameters | MicrosoftDefenderEndpointRunScriptActionParameters, ResponseActionRunScriptOutputContent, ResponseActionRunScriptParameters > @@ -39,16 +44,44 @@ export const RunScriptActionResult = memo< if (!endpointId) { return; } + // Note TC: I had much issues moving this outside of useMemo - caused by command type. If you think this is a problem - please try to move it out. + const getParams = () => { + const args = command.args.args; + + if (agentType === 'microsoft_defender_endpoint') { + const msDefenderArgs = args as MicrosoftDefenderEndpointRunScriptActionParameters; + + return { + scriptName: msDefenderArgs.ScriptName?.[0], + args: msDefenderArgs.Args?.[0], + }; + } + + if (agentType === 'crowdstrike') { + const csArgs = args as CrowdStrikeRunScriptActionParameters; + + return { + raw: csArgs.Raw?.[0], + hostPath: csArgs.HostPath?.[0], + cloudFile: csArgs.CloudFile?.[0], + commandLine: csArgs.CommandLine?.[0], + timeout: csArgs.Timeout?.[0], + }; + } + + return {}; + }; + + const parameters = getParams(); + // Early return if we have no parameters + if (Object.keys(parameters).length === 0) { + return; + } + return { agent_type: agentType, endpoint_ids: [endpointId], - parameters: { - raw: command.args.args.Raw?.[0], - hostPath: command.args.args.HostPath?.[0], - cloudFile: command.args.args.CloudFile?.[0], - commandLine: command.args.args.CommandLine?.[0], - timeout: command.args.args.Timeout?.[0], - }, + parameters, comment: command.args.args?.comment?.[0], }; }, [command]); @@ -87,7 +120,9 @@ export const RunScriptActionResult = memo< agentId={command.commandDefinition?.meta?.endpointId} textSize="s" data-test-subj="console" - hideFile={true} + // Currently file is not supported for CrowdStrike + hideFile={command.commandDefinition?.meta?.agentType === 'crowdstrike'} + showPasscode={false} hideContext={true} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 32c3dca06c0ef..c54f5136456bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -44,7 +44,11 @@ import { import { getCommandAboutInfo } from './get_command_about_info'; import { validateUnitOfTime } from './utils'; -import { CONSOLE_COMMANDS, CROWDSTRIKE_CONSOLE_COMMANDS } from '../../../common/translations'; +import { + CONSOLE_COMMANDS, + CROWDSTRIKE_CONSOLE_COMMANDS, + MS_DEFENDER_ENDPOINT_CONSOLE_COMMANDS, +} from '../../../common/translations'; import { ScanActionResult } from '../command_render_components/scan_action'; const emptyArgumentValidator = (argData: ParsedArgData): true | string => { @@ -167,9 +171,11 @@ export const getEndpointConsoleCommands = ({ platform, }: GetEndpointConsoleCommandsOptions): CommandDefinition[] => { const featureFlags = ExperimentalFeaturesService.get(); - - const isUploadEnabled = featureFlags.responseActionUploadEnabled; - const crowdstrikeRunScriptEnabled = featureFlags.crowdstrikeRunScriptEnabled; + const { + responseActionUploadEnabled: isUploadEnabled, + crowdstrikeRunScriptEnabled, + microsoftDefenderEndpointRunScriptEnabled, + } = featureFlags; const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => { // Agent capabilities are only validated for Endpoint agent types @@ -526,81 +532,49 @@ export const getEndpointConsoleCommands = ({ privileges: endpointPrivileges, }), }); - if (crowdstrikeRunScriptEnabled) { - consoleCommands.push({ - name: 'runscript', - about: getCommandAboutInfo({ - aboutInfo: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, - isSupported: agentType === 'crowdstrike' && doesEndpointSupportCommand('runscript'), - }), - RenderComponent: RunScriptActionResult, - meta: { - agentType, - endpointId: endpointAgentId, - capabilities: endpointCapabilities, + consoleCommands.push({ + name: 'runscript', + about: getCommandAboutInfo({ + aboutInfo: CONSOLE_COMMANDS.runscript.about, + isSupported: doesEndpointSupportCommand('runscript'), + }), + RenderComponent: RunScriptActionResult, + meta: { + agentType, + endpointId: endpointAgentId, + capabilities: endpointCapabilities, + privileges: endpointPrivileges, + }, + exampleInstruction: CONSOLE_COMMANDS.runscript.about, + validate: capabilitiesAndPrivilegesValidator(agentType), + mustHaveArgs: true, + helpGroupLabel: HELP_GROUPS.responseActions.label, + helpGroupPosition: HELP_GROUPS.responseActions.position, + helpCommandPosition: 9, + helpDisabled: + !doesEndpointSupportCommand('runscript') || + (agentType !== 'crowdstrike' && agentType !== 'microsoft_defender_endpoint'), + helpHidden: + !getRbacControl({ + commandName: 'runscript', privileges: endpointPrivileges, - }, - exampleUsage: `runscript --Raw=\`\`\`Get-ChildItem .\`\`\` --CommandLine=""`, - helpUsage: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.helpUsage, - exampleInstruction: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, - validate: capabilitiesAndPrivilegesValidator(agentType), - mustHaveArgs: true, - args: { - Raw: { - required: false, - allowMultiples: false, - about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.raw.about, - mustHaveValue: 'non-empty-string', - exclusiveOr: true, - }, - CloudFile: { - required: false, - allowMultiples: false, - about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.cloudFile.about, - mustHaveValue: 'truthy', - exclusiveOr: true, - SelectorComponent: CustomScriptSelector(agentType), - }, - CommandLine: { - required: false, - allowMultiples: false, - about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.commandLine.about, - mustHaveValue: 'non-empty-string', - }, - HostPath: { - required: false, - allowMultiples: false, - about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.hostPath.about, - mustHaveValue: 'non-empty-string', - exclusiveOr: true, - }, - Timeout: { - required: false, - allowMultiples: false, - about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.timeout.about, - mustHaveValue: 'number-greater-than-zero', - }, - ...commandCommentArgument(), - }, - helpGroupLabel: HELP_GROUPS.responseActions.label, - helpGroupPosition: HELP_GROUPS.responseActions.position, - helpCommandPosition: 9, - helpDisabled: !doesEndpointSupportCommand('runscript') || agentType !== 'crowdstrike', - helpHidden: - !getRbacControl({ - commandName: 'runscript', - privileges: endpointPrivileges, - }) || agentType !== 'crowdstrike', - }); - } + }) || + (agentType !== 'crowdstrike' && agentType !== 'microsoft_defender_endpoint'), + }); switch (agentType) { case 'sentinel_one': return adjustCommandsForSentinelOne({ commandList: consoleCommands, platform }); case 'crowdstrike': - return adjustCommandsForCrowdstrike({ commandList: consoleCommands }); + return adjustCommandsForCrowdstrike({ + commandList: consoleCommands, + crowdstrikeRunScriptEnabled, + }); case 'microsoft_defender_endpoint': - return adjustCommandsForMicrosoftDefenderEndpoint({ commandList: consoleCommands }); + return adjustCommandsForMicrosoftDefenderEndpoint({ + commandList: consoleCommands, + microsoftDefenderEndpointRunScriptEnabled, + }); default: // agentType === endpoint: just returns the defined command list return consoleCommands; @@ -688,8 +662,10 @@ const adjustCommandsForSentinelOne = ({ /** @private */ const adjustCommandsForCrowdstrike = ({ commandList, + crowdstrikeRunScriptEnabled, }: { commandList: CommandDefinition[]; + crowdstrikeRunScriptEnabled: boolean; }): CommandDefinition[] => { return commandList.map((command) => { if ( @@ -702,6 +678,54 @@ const adjustCommandsForCrowdstrike = ({ ) { disableCommand(command, 'crowdstrike'); } + if (command.name === 'runscript') { + if (!crowdstrikeRunScriptEnabled) { + disableCommand(command, 'crowdstrike'); + } else { + return { + ...command, + exampleUsage: `runscript --Raw=\`\`\`Get-ChildItem .\`\`\` --CommandLine=""`, + helpUsage: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.helpUsage, + args: { + Raw: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.raw.about, + mustHaveValue: 'non-empty-string', + exclusiveOr: true, + }, + CloudFile: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.cloudFile.about, + mustHaveValue: 'truthy', + exclusiveOr: true, + SelectorComponent: CustomScriptSelector('crowdstrike'), + }, + CommandLine: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.commandLine.about, + mustHaveValue: 'non-empty-string', + }, + HostPath: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.hostPath.about, + mustHaveValue: 'non-empty-string', + exclusiveOr: true, + }, + Timeout: { + required: false, + allowMultiples: false, + about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.timeout.about, + mustHaveValue: 'number-greater-than-zero', + }, + ...commandCommentArgument(), + }, + }; + } + } return command; }); @@ -709,8 +733,10 @@ const adjustCommandsForCrowdstrike = ({ const adjustCommandsForMicrosoftDefenderEndpoint = ({ commandList, + microsoftDefenderEndpointRunScriptEnabled, }: { commandList: CommandDefinition[]; + microsoftDefenderEndpointRunScriptEnabled: boolean; }): CommandDefinition[] => { const featureFlags = ExperimentalFeaturesService.get(); const isMicrosoftDefenderEndpointEnabled = featureFlags.responseActionsMSDefenderEndpointEnabled; @@ -728,6 +754,33 @@ const adjustCommandsForMicrosoftDefenderEndpoint = ({ disableCommand(command, 'microsoft_defender_endpoint'); } + if (command.name === 'runscript') { + if (!microsoftDefenderEndpointRunScriptEnabled) { + disableCommand(command, 'microsoft_defender_endpoint'); + } else { + return { + ...command, + exampleUsage: `runscript --ScriptName='test.ps1'`, + args: { + ScriptName: { + required: true, + allowMultiples: false, + about: MS_DEFENDER_ENDPOINT_CONSOLE_COMMANDS.runscript.args.scriptName.about, + mustHaveValue: 'truthy', + SelectorComponent: CustomScriptSelector('microsoft_defender_endpoint'), + }, + Args: { + required: false, + allowMultiples: false, + about: MS_DEFENDER_ENDPOINT_CONSOLE_COMMANDS.runscript.args.args.about, + mustHaveValue: 'non-empty-string', + }, + ...commandCommentArgument(), + }, + }; + } + } + return command; }); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx index 182266e934c40..cce0e9f0b29e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx @@ -224,7 +224,7 @@ const OutputContent = memo<{ } textSize="xs" data-test-subj={getTestId('actionsLogTray')} - hideFile={true} + hideFile={action.agentType === 'crowdstrike'} hideContext={true} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index bf2d40a350390..565341914fedd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -344,7 +344,11 @@ export const useActionsLogFilter = ({ return false; } - if (commandName === 'runscript' && !featureFlags.crowdstrikeRunScriptEnabled) { + if ( + commandName === 'runscript' && + !featureFlags.microsoftDefenderEndpointRunScriptEnabled && + !featureFlags.crowdstrikeRunScriptEnabled + ) { return false; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.ts index 15bb2260e9f0b..c996ddf1f784c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/actions/utils.ts @@ -83,7 +83,7 @@ const COMMANDS_WITH_ACCESS_TO_FILES: CommandsWithFileAccess = deepFreeze > { - const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions = { + const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions< + RunScriptActionRequestBody['parameters'] + > = { ...actionRequest, ...this.getMethodOptions(options), command: 'runscript', diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/mocks.ts index bdab2050743c9..3aa195759bd66 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/mocks.ts @@ -11,6 +11,7 @@ import { } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants'; import type { ActionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types'; +import { merge } from 'lodash'; import { BaseDataGenerator } from '../../../../../../common/endpoint/data_generators/base_data_generator'; import { createCrowdstrikeAgentDetailsMock, @@ -22,6 +23,7 @@ import { responseActionsClientMock } from '../mocks'; import type { NormalizedExternalConnectorClient } from '../../..'; import { applyEsClientSearchMock } from '../../../../mocks/utils.mock'; import { CROWDSTRIKE_INDEX_PATTERNS_BY_INTEGRATION } from '../../../../../../common/endpoint/service/response_actions/crowdstrike'; +import type { RunScriptActionRequestBody } from '../../../../../../common/api/endpoint'; export interface CrowdstrikeActionsClientOptionsMock extends ResponseActionsClientOptionsMock { connectorActions: NormalizedExternalConnectorClient; @@ -130,6 +132,21 @@ const createEventSearchResponseMock = (): CrowdstrikeEventSearchResponseMock => timed_out: false, }); +const createCrowdstrikeRunScriptOptionsMock = ( + overrides: Partial = {} +): RunScriptActionRequestBody => { + const options: RunScriptActionRequestBody = { + endpoint_ids: ['1-2-3'], + comment: 'test comment', + agent_type: 'crowdstrike', + parameters: { + raw: 'Write-Output "Hello from CrowdStrike script"', + timeout: 60000, + }, + }; + return merge(options, overrides); +}; + export const CrowdstrikeMock = { createGetAgentsResponse: createCrowdstrikeGetAgentsApiResponseMock, createGetAgentOnlineStatusDetails: createCrowdstrikeGetAgentOnlineStatusDetailsMock, @@ -137,4 +154,5 @@ export const CrowdstrikeMock = { createConnectorActionsClient: createConnectorActionsClientMock, createConstructorOptions: createConstructorOptionsMock, createEventSearchResponse: createEventSearchResponseMock, + createCrowdstrikeRunScriptOptions: createCrowdstrikeRunScriptOptionsMock, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts index 2dd9f43d9b9c0..793763752af73 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts @@ -55,6 +55,7 @@ import type { CommonResponseActionMethodOptions, CustomScriptsResponse, GetFileDownloadMethodResponse, + OmitUnsupportedAttributes, ProcessPendingActionsMethodOptions, ResponseActionsClient, } from './types'; @@ -70,6 +71,8 @@ import type { ResponseActionGetFileOutputContent, ResponseActionGetFileParameters, ResponseActionParametersWithProcessData, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, ResponseActionScanOutputContent, ResponseActionsExecuteParameters, ResponseActionScanParameters, @@ -78,8 +81,6 @@ import type { SuspendProcessActionOutputContent, UploadedFileInfo, WithAllKeys, - ResponseActionRunScriptOutputContent, - ResponseActionRunScriptParameters, } from '../../../../../../common/endpoint/types'; import type { ExecuteActionRequestBody, @@ -1002,7 +1003,7 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient } public async runscript( - actionRequest: RunScriptActionRequestBody, + actionRequest: OmitUnsupportedAttributes, options?: CommonResponseActionMethodOptions ): Promise< ActionDetails diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts index a8cadd2623527..545c1a9b0de0f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/lib/types.ts @@ -33,14 +33,14 @@ import type { ResponseActionGetFileRequestBody, ExecuteActionRequestBody, UploadActionApiRequestBody, - BaseActionRequestBody, ScanActionRequestBody, KillProcessRequestBody, SuspendProcessRequestBody, RunScriptActionRequestBody, + BaseActionRequestBody, } from '../../../../../../common/api/endpoint'; -type OmitUnsupportedAttributes = Omit< +export type OmitUnsupportedAttributes = Omit< T, // We don't need agent type in the Response Action client because each client is initialized for only 1 agent type 'agent_type' diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/mocks.ts index f6668d9234b02..ea6f8f61243ba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/mocks.ts @@ -13,15 +13,19 @@ import { } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants'; import type { MicrosoftDefenderEndpointAgentListResponse, + MicrosoftDefenderEndpointGetActionResultsResponse, MicrosoftDefenderEndpointGetActionsResponse, MicrosoftDefenderEndpointMachine, MicrosoftDefenderEndpointMachineAction, + MicrosoftDefenderGetLibraryFilesResponse, } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types'; +import { merge } from 'lodash'; import { applyEsClientSearchMock } from '../../../../../../mocks/utils.mock'; import { MICROSOFT_DEFENDER_ENDPOINT_LOG_INDEX_PATTERN } from '../../../../../../../../common/endpoint/service/response_actions/microsoft_defender'; import { MicrosoftDefenderDataGenerator } from '../../../../../../../../common/endpoint/data_generators/microsoft_defender_data_generator'; import { responseActionsClientMock, type ResponseActionsClientOptionsMock } from '../../../mocks'; import type { NormalizedExternalConnectorClient } from '../../../../..'; +import type { RunScriptActionRequestBody } from '../../../../../../../../common/api/endpoint'; export interface MicrosoftDefenderActionsClientOptionsMock extends ResponseActionsClientOptionsMock { @@ -123,6 +127,16 @@ const createMsConnectorActionsClientMock = (): ActionsClientMock => { }, }); + case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RUN_SCRIPT: + return responseActionsClientMock.createConnectorActionExecuteResponse({ + data: createMicrosoftMachineActionMock({ type: 'LiveResponse' }), + }); + + case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES: + return responseActionsClientMock.createConnectorActionExecuteResponse({ + data: createMicrosoftGetLibraryFilesApiResponseMock(), + }); + default: return responseActionsClientMock.createConnectorActionExecuteResponse(); } @@ -201,6 +215,13 @@ const createMicrosoftGetActionsApiResponseMock = ( value: [createMicrosoftMachineActionMock(overrides)], }; }; +const createMicrosoftGetActionResultsApiResponseMock = + (): MicrosoftDefenderEndpointGetActionResultsResponse => { + return { + '@odata.context': 'some-context', + value: 'http://example.com', + }; + }; const createMicrosoftGetMachineListApiResponseMock = ( /** Any overrides to the 1 machine action that is included in the mock response */ @@ -216,11 +237,50 @@ const createMicrosoftGetMachineListApiResponseMock = ( }; }; +const createMicrosoftGetLibraryFilesApiResponseMock = + (): MicrosoftDefenderGetLibraryFilesResponse => { + return { + '@odata.context': 'some-context', + value: [ + { + fileName: 'test-script-1.ps1', + description: 'Test PowerShell script for demonstration', + creationTime: '2023-01-01T10:00:00Z', + createdBy: 'user@example.com', + }, + { + fileName: 'test-script-2.py', + description: 'Test Python script for automation', + creationTime: '2023-01-02T10:00:00Z', + createdBy: 'admin@example.com', + }, + ], + }; + }; + +const createMicrosoftRunScriptOptionsMock = ( + overrides: Partial = {} +): RunScriptActionRequestBody => { + const options: RunScriptActionRequestBody = { + endpoint_ids: ['1-2-3'], + comment: 'test comment', + agent_type: 'microsoft_defender_endpoint', + parameters: { + scriptName: 'test-script.ps1', + args: 'test-args', + }, + }; + return merge(options, overrides); +}; + export const microsoftDefenderMock = { createConstructorOptions: createMsDefenderClientConstructorOptionsMock, createMsConnectorActionsClient: createMsConnectorActionsClientMock, createMachineAction: createMicrosoftMachineActionMock, createMachine: createMicrosoftMachineMock, createGetActionsApiResponse: createMicrosoftGetActionsApiResponseMock, + createGetActionResultsApiResponse: createMicrosoftGetActionResultsApiResponseMock, createMicrosoftGetMachineListApiResponse: createMicrosoftGetMachineListApiResponseMock, + createGetLibraryFilesApiResponse: createMicrosoftGetLibraryFilesApiResponseMock, + createRunScriptOptions: createMicrosoftRunScriptOptionsMock, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts index b6df073cd8dde..f413fb7d69393 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Readable } from 'stream'; import { MicrosoftDefenderEndpointActionsClient } from './ms_defender_endpoint_actions_client'; import type { ProcessPendingActionsMethodOptions, ResponseActionsClient } from '../../../../..'; import { getActionDetailsById as _getActionDetailsById } from '../../../../action_details_by_id'; @@ -19,7 +20,10 @@ import type { } from '../../../../../../../../common/endpoint/types'; import { EndpointActionGenerator } from '../../../../../../../../common/endpoint/data_generators/endpoint_action_generator'; import { applyEsClientSearchMock } from '../../../../../../mocks/utils.mock'; -import { ENDPOINT_ACTIONS_INDEX } from '../../../../../../../../common/endpoint/constants'; +import { + ENDPOINT_ACTIONS_INDEX, + ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, +} from '../../../../../../../../common/endpoint/constants'; import type { MicrosoftDefenderEndpointGetActionsResponse, MicrosoftDefenderEndpointMachineAction, @@ -57,16 +61,16 @@ describe('MS Defender response actions client', () => { scan: false, execute: false, getFile: false, - getFileDownload: false, - getFileInfo: false, + getFileDownload: true, + getFileInfo: true, killProcess: false, runningProcesses: false, - runscript: false, + runscript: true, suspendProcess: false, isolate: true, release: true, processPendingActions: true, - getCustomScripts: false, + getCustomScripts: true, }; it.each( @@ -182,6 +186,629 @@ describe('MS Defender response actions client', () => { }); }); + describe('#runscript()', () => { + beforeEach(() => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + true; + }); + it('should send runscript request to Microsoft with expected parameters', async () => { + await msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1', args: 'arg1 arg2' }, + }) + ); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ + params: { + subAction: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RUN_SCRIPT, + subActionParams: { + comment: expect.stringMatching( + /Action triggered from Elastic Security by user \[foo\] for action \[.* \(action id: .*\)\]: test comment/ + ), + id: '1-2-3', + parameters: { + scriptName: 'test-script.ps1', + args: 'arg1 arg2', + }, + }, + }, + }); + }); + + it('should write action request doc. to endpoint index', async () => { + await msClientMock.runscript( + microsoftDefenderMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1' }, + }) + ); + + expect(clientConstructorOptionsMock.esClient.index).toHaveBeenCalledWith( + { + document: { + '@timestamp': expect.any(String), + EndpointActions: { + action_id: expect.any(String), + data: { + command: 'runscript', + comment: 'test comment', + hosts: { + '1-2-3': { + name: 'mymachine1.contoso.com', + }, + }, + parameters: { + args: 'test-args', + scriptName: 'test-script.ps1', + }, + }, + expiration: expect.any(String), + input_type: 'microsoft_defender_endpoint', + type: 'INPUT_ACTION', + }, + agent: { + id: ['1-2-3'], + }, + meta: { + machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e', + }, + user: { + id: 'foo', + }, + }, + index: '.logs-endpoint.actions-default', + refresh: 'wait_for', + }, + { meta: true } + ); + }); + + it('should return action details', async () => { + getActionDetailsByIdMock.mockResolvedValue({ + id: expect.any(String), + command: 'runscript', + agentType: 'microsoft_defender_endpoint', + isCompleted: false, + }); + + await expect( + msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1' }, + }) + ) + ).resolves.toEqual( + expect.objectContaining({ + command: 'runscript', + id: expect.any(String), + }) + ); + }); + + it('should handle Microsoft Defender API errors gracefully', async () => { + const apiError = new Error('Microsoft Defender API error'); + connectorActionsMock.execute.mockRejectedValueOnce(apiError); + + await expect( + msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1' }, + }) + ) + ).rejects.toThrow('Microsoft Defender API error'); + }); + + it('should handle missing machine action ID in response', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RUN_SCRIPT, + responseActionsClientMock.createConnectorActionExecuteResponse({ + data: { + /* missing id */ + }, + }) + ); + + await expect( + msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1' }, + }) + ) + ).rejects.toThrow( + 'Run Script request was sent to Microsoft Defender, but Machine Action Id was not provided!' + ); + }); + + it('should include args parameter when provided', async () => { + await msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1', args: 'param1 param2' }, + }) + ); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + subActionParams: expect.objectContaining({ + parameters: { + scriptName: 'test-script.ps1', + args: 'param1 param2', + }, + }), + }), + }) + ); + }); + + it('should omit args parameter when not provided', async () => { + await msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1' }, + }) + ); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + subActionParams: expect.objectContaining({ + parameters: { + scriptName: 'test-script.ps1', + args: undefined, + }, + }), + }), + }) + ); + }); + }); + + describe('#getFileInfo()', () => { + beforeEach(() => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + true; + + const generator = new EndpointActionGenerator('seed'); + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'runscript', comment: 'test comment' }, + input_type: 'microsoft_defender_endpoint', + }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: actionRequestsSearchResponse, + }); + }); + + it('should throw error if feature flag is disabled', async () => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + false; + + await expect(msClientMock.getFileInfo('abc', '123')).rejects.toThrow( + 'File downloads are not supported for microsoft_defender_endpoint agent type. Feature disabled' + ); + }); + + it('should return file info with status AWAITING_UPLOAD when no response document exists', async () => { + const generator = new EndpointActionGenerator('seed'); + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, + response: generator.toEsSearchResponse([]), + }); + + await expect(msClientMock.getFileInfo('abc', '123')).resolves.toEqual({ + actionId: 'abc', + agentId: '123', + id: '123', + agentType: 'microsoft_defender_endpoint', + status: 'AWAITING_UPLOAD', + created: '', + name: '', + size: 0, + mimeType: '', + }); + }); + + it('should return file info with status READY when response document exists', async () => { + const generator = new EndpointActionGenerator('seed'); + const responseEsHit = generator.generateResponseEsHit({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'runscript' }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { + createdAt: '2024-05-09T10:30:00Z', + filename: 'script_output.txt', + machineActionId: 'machine-action-123', + }, + }); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, + response: generator.toEsSearchResponse([responseEsHit]), + }); + + await expect(msClientMock.getFileInfo('abc', '123')).resolves.toEqual({ + actionId: 'abc', + agentId: '123', + id: '123', + agentType: 'microsoft_defender_endpoint', + status: 'READY', + created: '2024-05-09T10:30:00Z', + name: 'script_output.txt', + size: 0, + mimeType: 'application/octet-stream', + }); + }); + + it('should throw error for unsupported command types', async () => { + const generator = new EndpointActionGenerator('seed'); + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'isolate', comment: 'test comment' }, + input_type: 'microsoft_defender_endpoint', + }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: actionRequestsSearchResponse, + }); + + const responseEsHit = generator.generateResponseEsHit({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'isolate' }, + input_type: 'microsoft_defender_endpoint', + }, + }); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, + response: generator.toEsSearchResponse([responseEsHit]), + }); + + await expect(msClientMock.getFileInfo('abc', '123')).rejects.toThrow( + 'isolate does not support file downloads' + ); + }); + + it('should handle ES client errors properly', async () => { + // Mock the ES client to throw an error + (clientConstructorOptionsMock.esClient.search as unknown as jest.Mock).mockRejectedValueOnce( + new Error('ES client error') + ); + + // The method should catch the error and return AWAITING_UPLOAD status only for ResponseActionAgentResponseEsDocNotFound + // Other errors should be propagated + await expect(msClientMock.getFileInfo('abc', '123')).rejects.toThrow('ES client error'); + }); + }); + + describe('#getFileDownload()', () => { + beforeEach(() => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + true; + + const generator = new EndpointActionGenerator('seed'); + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'runscript', comment: 'test comment' }, + input_type: 'microsoft_defender_endpoint', + }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: actionRequestsSearchResponse, + }); + + const responseEsHit = generator.generateResponseEsHit({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'runscript' }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { + createdAt: '2024-05-09T10:30:00Z', + filename: 'script_output.txt', + machineActionId: 'machine-action-123', + }, + }); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, + response: generator.toEsSearchResponse([responseEsHit]), + }); + }); + + it('should throw error if feature flag is disabled', async () => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + false; + + await expect(msClientMock.getFileDownload('abc', '123')).rejects.toThrow( + 'File downloads are not supported for microsoft_defender_endpoint agent type. Feature disabled' + ); + }); + + it('should successfully download file and verify API call to Microsoft Defender GET_ACTION_RESULTS', async () => { + const mockStream = new Readable({ + read() { + this.push('test file content'); + this.push(null); + }, + }); + + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + { data: mockStream } + ); + + const result = await msClientMock.getFileDownload('abc', '123'); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ + params: { + subAction: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + subActionParams: { + id: 'machine-action-123', + }, + }, + }); + + expect(result).toEqual({ + stream: { + data: expect.any(Readable), + }, + fileName: 'script_output.txt', + mimeType: undefined, + }); + }); + + it('should throw error when Microsoft Defender GET_ACTION_RESULTS API returns no data', async () => { + // Clear any previous mocks first + (connectorActionsMock.execute as jest.Mock).mockReset(); + + // Mock the connector to return undefined data + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce({ + data: undefined, + }); + + await expect(msClientMock.getFileDownload('abc', '123')).rejects.toThrow( + 'Unable to establish a file download Readable stream with Microsoft Defender for Endpoint for response action [runscript] [abc]' + ); + }); + + it('should throw error when machine action ID is missing in response document', async () => { + const generator = new EndpointActionGenerator('seed'); + const responseEsHit = generator.generateResponseEsHit({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'runscript' }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { + createdAt: '2024-05-09T10:30:00Z', + filename: 'script_output.txt', + // machineActionId is missing + }, + }); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN, + response: generator.toEsSearchResponse([responseEsHit]), + }); + + await expect(msClientMock.getFileDownload('abc', '123')).rejects.toThrow( + 'Unable to retrieve file from Microsoft Defender for Endpoint. Response ES document is missing [meta.machineActionId]' + ); + }); + + it('should throw error for unsupported command types', async () => { + const generator = new EndpointActionGenerator('seed'); + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: '123' }, + EndpointActions: { + data: { command: 'isolate', comment: 'test comment' }, + input_type: 'microsoft_defender_endpoint', + }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: actionRequestsSearchResponse, + }); + + // Note: For unsupported commands, the method will not enter the switch case + // and downloadStream will remain undefined, causing the method to throw + await expect(msClientMock.getFileDownload('abc', '123')).rejects.toThrow( + 'Unable to establish a file download Readable stream with Microsoft Defender for Endpoint for response action [isolate] [abc]' + ); + }); + + it('should handle Microsoft Defender API errors and propagate them properly', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + { data: undefined } + ); + + // Mock the connector to throw an error + (connectorActionsMock.execute as jest.Mock).mockRejectedValueOnce( + new Error('Microsoft Defender API error') + ); + + await expect(msClientMock.getFileDownload('abc', '123')).rejects.toThrow( + 'Microsoft Defender API error' + ); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ + params: { + subAction: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + subActionParams: { + id: 'machine-action-123', + }, + }, + }); + }); + }); + + describe('#getCustomScripts()', () => { + it('should retrieve custom scripts from Microsoft Defender', async () => { + const result = await msClientMock.getCustomScripts(); + + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ + params: { + subAction: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + subActionParams: {}, + }, + }); + + expect(result).toEqual({ + data: [ + { + id: 'test-script-1.ps1', + name: 'test-script-1.ps1', + description: 'Test PowerShell script for demonstration', + }, + { + id: 'test-script-2.py', + name: 'test-script-2.py', + description: 'Test Python script for automation', + }, + ], + }); + }); + + it('should handle empty library files response', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + responseActionsClientMock.createConnectorActionExecuteResponse({ + data: { '@odata.context': 'some-context', value: [] }, + }) + ); + + const result = await msClientMock.getCustomScripts(); + + expect(result).toEqual({ data: [] }); + }); + + it('should handle missing data in library files response', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + responseActionsClientMock.createConnectorActionExecuteResponse({ + data: {}, + }) + ); + + const result = await msClientMock.getCustomScripts(); + + expect(result).toEqual({ data: [] }); + }); + + it('should handle Microsoft Defender API errors gracefully', async () => { + const apiError = new Error('Microsoft Defender API error'); + connectorActionsMock.execute.mockImplementation(async (options) => { + if (options.params.subAction === MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES) { + throw apiError; + } + return responseActionsClientMock.createConnectorActionExecuteResponse(); + }); + + await expect(msClientMock.getCustomScripts()).rejects.toThrow( + 'Failed to fetch Microsoft Defender for Endpoint scripts, failed with: Microsoft Defender API error' + ); + }); + + it('should handle null response data', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + responseActionsClientMock.createConnectorActionExecuteResponse({ + data: null, + }) + ); + + const result = await msClientMock.getCustomScripts(); + + expect(result).toEqual({ data: [] }); + }); + + it('should handle undefined response data', async () => { + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + responseActionsClientMock.createConnectorActionExecuteResponse({ + data: undefined, + }) + ); + + const result = await msClientMock.getCustomScripts(); + + expect(result).toEqual({ data: [] }); + }); + + it('should throw ResponseActionsClientError on API failure', async () => { + const apiError = new Error('Microsoft Defender API error'); + + connectorActionsMock.execute.mockImplementation(async (options) => { + if (options.params.subAction === MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES) { + throw apiError; + } + return responseActionsClientMock.createConnectorActionExecuteResponse(); + }); + + await expect(msClientMock.getCustomScripts()).rejects.toThrow( + 'Failed to fetch Microsoft Defender for Endpoint scripts, failed with: Microsoft Defender API error' + ); + }); + }); + describe('#processPendingActions()', () => { let abortController: AbortController; let processPendingActionsOptions: ProcessPendingActionsMethodOptions; @@ -230,6 +857,16 @@ describe('MS Defender response actions client', () => { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS, msMachineActionsApiResponse ); + + const msGetActionResultsApiResponse = + microsoftDefenderMock.createGetActionResultsApiResponse(); + + // Set the mock response for GET_ACTION_RESULTS + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + msGetActionResultsApiResponse + ); }); it('should generate action response docs for completed actions', async () => { @@ -294,6 +931,136 @@ describe('MS Defender response actions client', () => { } ); }); + + describe('for Runscript', () => { + let msMachineActionsApiResponse: MicrosoftDefenderEndpointGetActionsResponse; + + beforeEach(() => { + // @ts-expect-error assign to readonly property + clientConstructorOptionsMock.endpointService.experimentalFeatures.microsoftDefenderEndpointRunScriptEnabled = + true; + + const generator = new EndpointActionGenerator('seed'); + + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + { scriptName: string }, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: 'agent-uuid-1' }, + EndpointActions: { + data: { command: 'runscript', parameters: { scriptName: 'test-script.ps1' } }, + input_type: 'microsoft_defender_endpoint', + }, + meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e' }, + }), + ]); + + applyEsClientSearchMock({ + esClientMock: clientConstructorOptionsMock.esClient, + index: ENDPOINT_ACTIONS_INDEX, + response: jest + .fn(() => generator.toEsSearchResponse([])) + .mockReturnValueOnce(actionRequestsSearchResponse), + pitUsage: true, + }); + + msMachineActionsApiResponse = microsoftDefenderMock.createGetActionsApiResponse(); + // Override the default machine action to be runscript-specific + msMachineActionsApiResponse.value[0] = { + ...msMachineActionsApiResponse.value[0], + type: 'LiveResponse', + commands: ['RunScript'], + }; + + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS, + msMachineActionsApiResponse + ); + + const msGetActionResultsApiResponse = + microsoftDefenderMock.createGetActionResultsApiResponse(); + + // Set the mock response for GET_ACTION_RESULTS + responseActionsClientMock.setConnectorActionsClientExecuteResponse( + connectorActionsMock, + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + msGetActionResultsApiResponse + ); + }); + + it('should generate action response docs for completed runscript actions', async () => { + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect(processPendingActionsOptions.addToQueue).toHaveBeenCalledWith({ + '@timestamp': expect.any(String), + EndpointActions: { + action_id: '90d62689-f72d-4a05-b5e3-500cad0dc366', + completed_at: expect.any(String), + data: { command: 'runscript' }, + input_type: 'microsoft_defender_endpoint', + started_at: expect.any(String), + }, + agent: { id: 'agent-uuid-1' }, + error: undefined, + meta: expect.objectContaining({ + machineActionId: expect.any(String), + createdAt: expect.any(String), + filename: expect.any(String), + }), + }); + }); + + it.each(['Pending', 'InProgress'])( + 'should NOT generate action responses if runscript action in MS Defender has a status of %s', + async (machineActionStatus) => { + msMachineActionsApiResponse.value[0].status = machineActionStatus; + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect(processPendingActionsOptions.addToQueue).not.toHaveBeenCalled(); + } + ); + + it.each` + msStatusValue | responseState + ${'Failed'} | ${'failure'} + ${'TimeOut'} | ${'failure'} + ${'Cancelled'} | ${'failure'} + ${'Succeeded'} | ${'success'} + `( + 'should generate $responseState action response if MS runscript machine action status is $msStatusValue', + async ({ msStatusValue, responseState }) => { + msMachineActionsApiResponse.value[0].status = msStatusValue; + const expectedResult: LogsEndpointActionResponse = { + '@timestamp': expect.any(String), + EndpointActions: { + action_id: '90d62689-f72d-4a05-b5e3-500cad0dc366', + completed_at: expect.any(String), + data: { command: 'runscript' }, + input_type: 'microsoft_defender_endpoint', + started_at: expect.any(String), + }, + agent: { id: 'agent-uuid-1' }, + error: undefined, + meta: expect.objectContaining({ + machineActionId: expect.any(String), + createdAt: expect.any(String), + filename: expect.any(String), + }), + }; + if (responseState === 'failure') { + expectedResult.error = { + message: expect.any(String), + }; + } + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect(processPendingActionsOptions.addToQueue).toHaveBeenCalledWith(expectedResult); + } + ); + }); }); describe('and space awareness is enabled', () => { @@ -304,7 +1071,6 @@ describe('MS Defender response actions client', () => { getActionDetailsByIdMock.mockResolvedValue({}); }); - afterEach(() => { getActionDetailsByIdMock.mockReset(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.ts index f44386906f587..ed5942c13fb6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/microsoft/defender/endpoint/ms_defender_endpoint_actions_client.ts @@ -13,6 +13,8 @@ import { import type { MicrosoftDefenderEndpointGetActionsParams, MicrosoftDefenderEndpointGetActionsResponse, + MicrosoftDefenderEndpointRunScriptParams, + MicrosoftDefenderGetLibraryFilesResponse, } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types'; import { type MicrosoftDefenderEndpointAgentDetailsParams, @@ -21,12 +23,15 @@ import { type MicrosoftDefenderEndpointMachineAction, } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types'; import { groupBy } from 'lodash'; +import type { Readable } from 'stream'; import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; import { buildIndexNameWithNamespace } from '../../../../../../../../common/endpoint/utils/index_name_utilities'; import { MICROSOFT_DEFENDER_INDEX_PATTERNS_BY_INTEGRATION } from '../../../../../../../../common/endpoint/service/response_actions/microsoft_defender'; import type { IsolationRouteRequestBody, + RunScriptActionRequestBody, UnisolationRouteRequestBody, + MSDefenderRunScriptActionRequestParams, } from '../../../../../../../../common/api/endpoint'; import type { ActionDetails, @@ -35,7 +40,11 @@ import type { LogsEndpointAction, LogsEndpointActionResponse, MicrosoftDefenderEndpointActionRequestCommonMeta, + MicrosoftDefenderEndpointActionRequestFileMeta, MicrosoftDefenderEndpointLogEsDoc, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, + UploadedFileInfo, } from '../../../../../../../../common/endpoint/types'; import type { ResponseActionAgentType, @@ -52,9 +61,14 @@ import { type ResponseActionsClientOptions, } from '../../../lib/base_response_actions_client'; import { stringify } from '../../../../../../utils/stringify'; -import { ResponseActionsClientError } from '../../../errors'; +import { + ResponseActionAgentResponseEsDocNotFound, + ResponseActionsClientError, +} from '../../../errors'; import type { CommonResponseActionMethodOptions, + CustomScriptsResponse, + GetFileDownloadMethodResponse, ProcessPendingActionsMethodOptions, } from '../../../lib/types'; import { catchAndWrapError } from '../../../../../../utils'; @@ -483,6 +497,67 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien return actionDetails; } + public async runscript( + actionRequest: RunScriptActionRequestBody, + options?: CommonResponseActionMethodOptions + ): Promise< + ActionDetails + > { + const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions< + RunScriptActionRequestBody['parameters'], + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + > = { + ...actionRequest, + ...this.getMethodOptions(options), + command: 'runscript', + }; + + if (!reqIndexOptions.error) { + let error = (await this.validateRequest(reqIndexOptions)).error; + + if (!error) { + try { + const msActionResponse = await this.sendAction< + MicrosoftDefenderEndpointMachineAction, + MicrosoftDefenderEndpointRunScriptParams + >(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RUN_SCRIPT, { + id: reqIndexOptions.endpoint_ids[0], + comment: this.buildExternalComment(reqIndexOptions), + parameters: { + scriptName: (reqIndexOptions.parameters as MSDefenderRunScriptActionRequestParams) + .scriptName, + args: (reqIndexOptions.parameters as MSDefenderRunScriptActionRequestParams).args, + }, + }); + + if (msActionResponse?.data?.id) { + reqIndexOptions.meta = { machineActionId: msActionResponse.data.id }; + } else { + throw new ResponseActionsClientError( + `Run Script request was sent to Microsoft Defender, but Machine Action Id was not provided!` + ); + } + } catch (err) { + error = err; + } + } + + reqIndexOptions.error = error?.message; + + if (!this.options.isAutomated && error) { + throw error; + } + } + + const { actionDetails } = await this.handleResponseActionCreation(reqIndexOptions); + + return actionDetails as ActionDetails< + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + >; + } + async processPendingActions({ abortSignal, addToQueue, @@ -519,7 +594,7 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien case 'isolate': case 'unisolate': addResponsesToQueueIfAny( - await this.checkPendingIsolateReleaseActions( + await this.checkPendingActions( typePendingActions as Array< ResponseActionsClientPendingAction< undefined, @@ -529,19 +604,33 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien > ) ); + case 'runscript': + addResponsesToQueueIfAny( + await this.checkPendingActions( + typePendingActions as Array< + ResponseActionsClientPendingAction< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + > + >, + { downloadResult: true } + ) + ); } } } } - private async checkPendingIsolateReleaseActions( + private async checkPendingActions( actionRequests: Array< ResponseActionsClientPendingAction< undefined, {}, MicrosoftDefenderEndpointActionRequestCommonMeta > - > + >, + options: { downloadResult?: boolean } = { downloadResult: false } ): Promise { const completedResponses: LogsEndpointActionResponse[] = []; const warnings: string[] = []; @@ -570,7 +659,7 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien agentId: Array.isArray(action.agent.id) ? action.agent.id[0] : action.agent.id, data: { command }, error: { - message: `Unable to very if action completed. Microsoft Defender machine action id ('meta.machineActionId') missing on action request document!`, + message: `Unable to verify if action completed. Microsoft Defender machine action id ('meta.machineActionId') missing from the action request document!`, }, }) ); @@ -598,6 +687,17 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien const pendingActionRequests = actionsByMachineId[machineAction.id] ?? []; for (const actionRequest of pendingActionRequests) { + let additionalData = {}; + // In order to not copy paste most of the logic, I decided to add this additional check here to support `runscript` action and it's result that comes back as a link to download the file + if (options.downloadResult) { + additionalData = { + meta: { + machineActionId: machineAction.id, + filename: `runscript-output-${machineAction.id}.json`, + createdAt: new Date().toISOString(), + }, + }; + } completedResponses.push( this.buildActionResponseEsDoc({ actionId: actionRequest.EndpointActions.action_id, @@ -606,6 +706,7 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien : actionRequest.agent.id, data: { command: actionRequest.EndpointActions.data.command }, error: isError ? { message } : undefined, + ...additionalData, }) ); } @@ -665,4 +766,180 @@ export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClien return { isPending, isError, message }; } + + async getCustomScripts(): Promise { + try { + const customScriptsResponse = (await this.sendAction( + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_LIBRARY_FILES, + {} + )) as ActionTypeExecutorResult; + + const scripts = customScriptsResponse.data?.value || []; + + // Transform MS Defender scripts to CustomScriptsResponse format + const data = scripts.map((script) => ({ + // due to External EDR's schema nature - we expect a maybe() everywhere - empty strings are needed + id: script.fileName || '', + name: script.fileName || '', + description: script.description || '', + })); + + return { data } as CustomScriptsResponse; + } catch (err) { + const error = new ResponseActionsClientError( + `Failed to fetch Microsoft Defender for Endpoint scripts, failed with: ${err.message}`, + 500, + err + ); + this.log.error(error); + throw error; + } + } + + async getFileInfo(actionId: string, agentId: string): Promise { + await this.ensureValidActionId(actionId); + const { + EndpointActions: { + data: { command }, + }, + } = await this.fetchActionRequestEsDoc(actionId); + + const { microsoftDefenderEndpointRunScriptEnabled } = + this.options.endpointService.experimentalFeatures; + if (command === 'runscript' && !microsoftDefenderEndpointRunScriptEnabled) { + throw new ResponseActionsClientError( + `File downloads are not supported for ${this.agentType} agent type. Feature disabled.`, + 400 + ); + } + + const fileInfo: UploadedFileInfo = { + actionId, + agentId, + id: agentId, + agentType: this.agentType, + status: 'AWAITING_UPLOAD', + created: '', + name: '', + size: 0, + mimeType: '', + }; + + try { + switch (command) { + case 'runscript': + { + const agentResponse = await this.fetchEsResponseDocForAgentId< + {}, + MicrosoftDefenderEndpointActionRequestFileMeta + >(actionId, agentId); + + fileInfo.status = 'READY'; + fileInfo.created = agentResponse.meta?.createdAt ?? ''; + fileInfo.name = agentResponse.meta?.filename ?? ''; + fileInfo.mimeType = 'application/octet-stream'; + } + break; + + default: + throw new ResponseActionsClientError(`${command} does not support file downloads`, 400); + } + } catch (e) { + // Ignore "no response doc" error for the agent and just return the file info with the status of 'AWAITING_UPLOAD' + if (!(e instanceof ResponseActionAgentResponseEsDocNotFound)) { + throw e; + } + } + + return fileInfo; + } + + async getFileDownload(actionId: string, agentId: string): Promise { + await this.ensureValidActionId(actionId); + const { + EndpointActions: { + data: { command }, + }, + } = await this.fetchActionRequestEsDoc(actionId); + + const { microsoftDefenderEndpointRunScriptEnabled } = + this.options.endpointService.experimentalFeatures; + if (command === 'runscript' && !microsoftDefenderEndpointRunScriptEnabled) { + throw new ResponseActionsClientError( + `File downloads are not supported for ${this.agentType} agent type. Feature disabled.`, + 400 + ); + } + + let downloadStream: Readable | undefined; + let fileName: string = 'download.json'; + + try { + switch (command) { + case 'runscript': + { + const runscriptAgentResponse = await this.fetchEsResponseDocForAgentId< + {}, + MicrosoftDefenderEndpointActionRequestFileMeta + >(actionId, agentId); + + if (!runscriptAgentResponse.meta?.machineActionId) { + throw new ResponseActionsClientError( + `Unable to retrieve file from Microsoft Defender for Endpoint. Response ES document is missing [meta.machineActionId]` + ); + } + + const { data } = await this.sendAction( + MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTION_RESULTS, + { id: runscriptAgentResponse.meta?.machineActionId } + ); + + if (data) { + downloadStream = data; + fileName = runscriptAgentResponse.meta.filename; + } + } + break; + } + + if (!downloadStream) { + throw new ResponseActionsClientError( + `Unable to establish a file download Readable stream with Microsoft Defender for Endpoint for response action [${command}] [${actionId}]` + ); + } + } catch (e) { + this.log.debug( + () => + `Attempt to get file download stream from Microsoft Defender for Endpoint for response action failed with:\n${stringify( + e + )}` + ); + + throw e; + } + + return { + stream: downloadStream, + mimeType: undefined, + fileName, + }; + } + + private async fetchEsResponseDocForAgentId< + TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput, + TMeta extends {} = {} + >(actionId: string, agentId: string): Promise> { + const agentResponse = ( + await this.fetchActionResponseEsDocs(actionId, [agentId]) + )[agentId]; + + if (!agentResponse) { + throw new ResponseActionAgentResponseEsDocNotFound( + `Action ID [${actionId}] for agent ID [${agentId}] is still pending`, + 404 + ); + } + + return agentResponse; + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts index c861c1db51005..60910b0c6ae3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts @@ -1585,7 +1585,7 @@ describe('SentinelOneActionsClient class', () => { response: s1DataGenerator.toEsSearchResponse([]), }); await expect(s1ActionsClient.getFileDownload('abc', '123')).rejects.toThrow( - 'Action ID [abc] for agent ID [abc] is still pending' + 'Action ID [abc] for agent ID [123] is still pending' ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts index 119cb07d3f516..5b13c0c224afc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts @@ -914,8 +914,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { requiresApproval: false, outputDestination: 'SentinelCloud', inputParams: terminateScriptInfo.buildScriptArgs({ - // @ts-expect-error TS2339: Property 'process_name' does not exist (`.validateRequest()` has already validated that `process_name` exists) - processName: reqIndexOptions.parameters.process_name, + processName: (reqIndexOptions.parameters as ResponseActionParametersWithProcessName) + .process_name, }), }, }); @@ -1221,7 +1221,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { if (!agentResponse) { throw new ResponseActionAgentResponseEsDocNotFound( - `Action ID [${actionId}] for agent ID [${actionId}] is still pending`, + `Action ID [${actionId}] for agent ID [${agentId}] is still pending`, 404 ); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts index a488f0576cbbf..978c8439f0a58 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts @@ -157,7 +157,6 @@ export class CrowdstrikeAgentStatusClient extends AgentStatusClient { pendingActions: pendingActions?.pending_actions ?? {}, }; - // console.log({ acc }); return acc; }, {}); } catch (err) {