From 4b8f002907e579501da06f7574b0a3e49876a62c Mon Sep 17 00:00:00 2001 From: Ash <1849116+ashokaditya@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:44:39 +0200 Subject: [PATCH 1/2] [SecuritySolution][Endpoint][ResponseActions] Response actions telemetry tests for third party agents (#225916) This is a follow up of https://github.com/elastic/kibana/pull/221518. - [x] Adds missing tests for action creation and action response events for third party agents. - [x] Adds telemetry events for completed action responses for crowdstrike agents. **Note:** The action creation tests in each agent type is redundant as the action creation telemetry event is sent from a method in the base action client class, but tests are added here just to ensure that telemetry events are not broken by any changes in the future for third party agents. Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 11bb9235d6afb98bbd4147a7d2225fc6af1b2acc) --- .../crowdstrike_actions_client.test.ts | 376 ++++++++++++++++++ .../crowdstrike/crowdstrike_actions_client.ts | 25 +- ...s_defender_endpoint_actions_client.test.ts | 217 ++++++++++ .../sentinel_one_actions_client.test.ts | 92 ++++- 4 files changed, 704 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts index cba00e7645e9f..0e45db597fa22 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.test.ts @@ -19,6 +19,10 @@ import { } from '../../../../../../common/endpoint/constants'; import { SUB_ACTION } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants'; import type { NormalizedExternalConnectorClient } from '../../..'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../lib/telemetry/event_based/events'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -43,6 +47,14 @@ describe('CrowdstrikeActionsClient class', () => { > = {} ) => responseActionsClientMock.createIsolateOptions({ ...overrides, agent_type: 'crowdstrike' }); + const createCrowdstrikeRunscrtiptOptions = ( + overrides: Omit< + Parameters[0], + 'agent_type' + > = {} + ) => + responseActionsClientMock.createRunScriptOptions({ ...overrides, agent_type: 'crowdstrike' }); + beforeEach(() => { classConstructorOptions = CrowdstrikeMock.createConstructorOptions(); connectorActionsMock = classConstructorOptions.connectorActions; @@ -217,6 +229,84 @@ describe('CrowdstrikeActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `isolate` action creation telemetry event', async () => { + await crowdstrikeActionsClient.isolate(createCrowdstrikeIsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'isolate', + isAutomated: false, + }, + }); + }); + + it('should send `isolate` action response telemetry event for successful action', async () => { + const actionResponse = { + data: { + errors: [], + action_id: '123-345-456', + action_status: 'successful', + command: 'isolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + await crowdstrikeActionsClient.isolate( + createCrowdstrikeIsolationOptions({ actionId: '123-345-456' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-345-456', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'isolate', + }, + }); + }); + + it('should send `isolate` action response telemetry event for failed action', async () => { + const actionResponse = { + data: { + errors: [ + { + message: 'Failed to isolate host', + }, + ], + action_id: '123-456-678', + action_status: 'failed', + command: 'isolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.isolate( + createCrowdstrikeIsolationOptions({ actionId: '123-456-678' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-456-678', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'isolate', + }, + }); + }); + }); }); describe('#release()', () => { @@ -313,5 +403,291 @@ describe('CrowdstrikeActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `release` action creation telemetry event', async () => { + await crowdstrikeActionsClient.release(createCrowdstrikeIsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'unisolate', + isAutomated: false, + }, + }); + }); + + it('should send `release` action response telemetry event for successful action', async () => { + const actionResponse = { + data: { + errors: [], + action_id: '123-345-456', + action_status: 'successful', + command: 'unisolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + await crowdstrikeActionsClient.release( + createCrowdstrikeIsolationOptions({ actionId: '123-345-456' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-345-456', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'unisolate', + }, + }); + }); + + it('should send `release` action response telemetry event for failed action', async () => { + const actionResponse = { + data: { + errors: [ + { + message: 'Failed to release host', + }, + ], + action_id: '123-456-678', + action_status: 'failed', + command: 'unisolate', + agent_type: 'crowdstrike', + agent_id: '1-2-3', + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.release( + createCrowdstrikeIsolationOptions({ actionId: '123-456-678' }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-456-678', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'unisolate', + }, + }); + }); + }); + }); + + describe('#runscript()', () => { + it('should send action to Crowdstrike', async () => { + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '123-456-789', + endpoint_ids: ['1-2-3-cs-agent'], + comment: 'test runscript comment', + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect(connectorActionsMock.execute as jest.Mock).toHaveBeenCalledWith({ + params: { + subAction: SUB_ACTION.EXECUTE_ADMIN_RTR, + subActionParams: { + command: 'runscript --Raw=```echo "Hello World"```', + endpoint_ids: ['1-2-3-cs-agent'], + actionParameters: { + comment: + 'Action triggered from Elastic Security by user [foo] for action [runscript (action id: 123-456-789)]: test runscript comment', + }, + }, + }, + }); + }); + + it('should write action request to endpoint indexes', async () => { + await crowdstrikeActionsClient.runscript(responseActionsClientMock.createRunScriptOptions()); + + expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2); + expect(classConstructorOptions.esClient.index.mock.calls[0][0]).toEqual({ + document: { + '@timestamp': expect.any(String), + EndpointActions: { + action_id: expect.any(String), + data: { + command: 'runscript', + comment: 'test comment', + parameters: { raw: 'ls' }, + hosts: { + '1-2-3': { + name: 'Crowdstrike-1460', + }, + }, + }, + expiration: expect.any(String), + input_type: 'crowdstrike', + type: 'INPUT_ACTION', + }, + agent: { id: ['1-2-3'] }, + meta: { + hostName: 'Crowdstrike-1460', + }, + user: { id: 'foo' }, + }, + index: ENDPOINT_ACTIONS_INDEX, + refresh: 'wait_for', + }); + expect(classConstructorOptions.esClient.index.mock.calls[1][0]).toEqual({ + document: { + '@timestamp': expect.any(String), + agent: { id: ['1-2-3'] }, + EndpointActions: { + action_id: expect.any(String), + completed_at: expect.any(String), + started_at: expect.any(String), + data: { + command: 'runscript', + comment: 'test comment', + hosts: { + '1-2-3': { + name: 'Crowdstrike-1460', + }, + }, + output: { + content: { + code: '200', + stderr: '', + stdout: '', + }, + type: 'text', + }, + parameters: { raw: 'ls' }, + }, + input_type: 'crowdstrike', + }, + error: undefined, + meta: undefined, + }, + index: ENDPOINT_ACTION_RESPONSES_INDEX, + refresh: 'wait_for', + }); + }); + + it('should return action details', async () => { + await crowdstrikeActionsClient.runscript(responseActionsClientMock.createRunScriptOptions()); + + expect(getActionDetailsByIdMock).toHaveBeenCalled(); + }); + + it('should update cases', async () => { + await crowdstrikeActionsClient.runscript( + responseActionsClientMock.createRunScriptOptions({ + case_ids: ['case-1'], + }) + ); + + expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); + }); + + describe('telemetry events', () => { + it('should send `runscript` action creation telemetry event', async () => { + await crowdstrikeActionsClient.runscript( + responseActionsClientMock.createRunScriptOptions() + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'crowdstrike', + command: 'runscript', + isAutomated: false, + }, + }); + }); + + it('should send `runscript` action response telemetry event for successful action', async () => { + const actionResponse = { + actionId: '123-abc-678', + data: undefined, + status: 'ok', + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '123-abc-678', + endpoint_ids: ['1-2-3-cs-agent'], + comment: 'test runscript comment', + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '123-abc-678', + actionStatus: 'successful', + agentType: 'crowdstrike', + command: 'runscript', + }, + }); + }); + + it('should send `runscript` action response telemetry event for failed action', async () => { + const actionResponse = { + actionId: '456-pqr-789', + status: 'ok', + data: { + combined: { + resources: { + '1-2-3-cs-agent': { + stdout: '', + stderr: '', + errors: [ + { + code: '500', + message: 'Failed to run script on host', + }, + ], + }, + }, + }, + }, + }; + (connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse); + + await crowdstrikeActionsClient.runscript( + createCrowdstrikeRunscrtiptOptions({ + actionId: '456-pqr-789', + endpoint_ids: ['1-2-3-cs-agent'], + parameters: { + raw: 'echo "Hello World"', + }, + }) + ); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenNthCalledWith(2, ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: '456-pqr-789', + actionStatus: 'failed', + agentType: 'crowdstrike', + command: 'runscript', + }, + }); + }); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index ace7efa3e5388..8316917fa6f29 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -30,6 +30,7 @@ import { stringify } from '../../../../utils/stringify'; import { ResponseActionsClientError } from '../errors'; import type { ActionDetails, + EndpointActionData, EndpointActionDataParameterTypes, EndpointActionResponseDataOutput, LogsEndpointAction, @@ -56,6 +57,18 @@ export type CrowdstrikeActionsClientOptions = ResponseActionsClientOptions & { connectorActions: NormalizedExternalConnectorClient; }; +interface CrowdstrikeResponseOptions { + error?: + | { + code: string; + message: string; + } + | undefined; + actionId: string; + agentId: string | string[]; + data: EndpointActionData; +} + export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { protected readonly agentType: ResponseActionAgentType = 'crowdstrike'; private readonly connectorActionsClient: NormalizedExternalConnectorClient; @@ -378,7 +391,7 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { const stdout = actionResponse.data?.combined.resources[agentId].stdout || ''; const stderr = actionResponse.data?.combined.resources[agentId].stderr || ''; const error = actionResponse.data?.combined.resources[agentId].errors?.[0]; - const options = { + const options: CrowdstrikeResponseOptions = { actionId: doc.EndpointActions.action_id, agentId, data: { @@ -402,14 +415,16 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { : {}), }; - await this.writeActionResponseToEndpointIndex(options); + const responseDoc = await this.writeActionResponseToEndpointIndex(options); + // telemetry event for completed action + await this.sendActionResponseTelemetry([responseDoc]); } private async completeCrowdstrikeAction( actionResponse: ActionTypeExecutorResult, doc: LogsEndpointAction ): Promise { - const options = { + const options: CrowdstrikeResponseOptions = { actionId: doc.EndpointActions.action_id, agentId: doc.agent.id, data: doc.EndpointActions.data, @@ -423,7 +438,9 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { : {}), }; - await this.writeActionResponseToEndpointIndex(options); + const responseDoc = await this.writeActionResponseToEndpointIndex(options); + // telemetry event for completed action + await this.sendActionResponseTelemetry([responseDoc]); } async getCustomScripts(): Promise { 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 3448cff50d5da..6ecee6a5aadba 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 @@ -29,6 +29,10 @@ import type { MicrosoftDefenderEndpointMachineAction, } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types'; import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../../../lib/telemetry/event_based/events'; jest.mock('../../../../action_details_by_id', () => { const originalMod = jest.requireActual('../../../../action_details_by_id'); @@ -181,6 +185,22 @@ describe('MS Defender response actions client', () => { expect(clientConstructorOptionsMock.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it(`should send ${responseActionMethod} action creation telemetry event`, async () => { + await msClientMock[responseActionMethod](responseActionsClientMock.createIsolateOptions()); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'microsoft_defender_endpoint', + command: responseActionMethod === 'release' ? 'unisolate' : responseActionMethod, + isAutomated: false, + }, + }); + }); + }); }); describe('#runscript()', () => { @@ -358,6 +378,26 @@ describe('MS Defender response actions client', () => { }) ); }); + + describe('telemetry event', () => { + it('should send runscript action creation telemetry event', async () => { + await msClientMock.runscript( + responseActionsClientMock.createRunScriptOptions({ + parameters: { scriptName: 'test-script.ps1', args: 'arg1 arg2' }, + }) + ); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + isAutomated: false, + }, + }); + }); + }); }); describe('#getFileInfo()', () => { @@ -1054,5 +1094,182 @@ describe('MS Defender response actions client', () => { } ); }); + + describe('telemetry events', () => { + describe('for Isolate and Release', () => { + let msMachineActionsApiResponse: MicrosoftDefenderEndpointGetActionsResponse; + + beforeEach(() => { + const generator = new EndpointActionGenerator('seed'); + + const actionRequestsSearchResponse = generator.toEsSearchResponse([ + generator.generateActionEsHit< + undefined, + {}, + MicrosoftDefenderEndpointActionRequestCommonMeta + >({ + agent: { id: 'agent-uuid-1' }, + EndpointActions: { + data: { command: 'isolate' }, + 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(); + 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.each` + msStatusValue | responseState + ${'Failed'} | ${'failed'} + ${'TimeOut'} | ${'failed'} + ${'Cancelled'} | ${'failed'} + ${'Succeeded'} | ${'successful'} + `( + 'should send telemetry for $responseState action response if MS machine action status is $msStatusValue', + async ({ msStatusValue, responseState }) => { + msMachineActionsApiResponse.value[0].status = msStatusValue; + + await msClientMock.processPendingActions(processPendingActionsOptions); + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: responseState, + agentType: 'microsoft_defender_endpoint', + command: 'isolate', + }, + }); + } + ); + }); + + 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 send telemetry for completed runscript actions', async () => { + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: 'successful', + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + }, + }); + }); + + it.each` + msStatusValue | responseState + ${'Failed'} | ${'failed'} + ${'TimeOut'} | ${'failed'} + ${'Cancelled'} | ${'failed'} + ${'Succeeded'} | ${'successful'} + `( + 'should generate $responseState action response if MS runscript machine action status is $msStatusValue', + async ({ msStatusValue, responseState }) => { + msMachineActionsApiResponse.value[0].status = msStatusValue; + + await msClientMock.processPendingActions(processPendingActionsOptions); + + expect( + clientConstructorOptionsMock.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + actionStatus: responseState, + agentType: 'microsoft_defender_endpoint', + command: 'runscript', + }, + }); + } + ); + }); + }); }); }); 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 d27120545edcc..fbe4149273707 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 @@ -51,7 +51,10 @@ import type { SentinelOneGetRemoteScriptStatusApiResponse, SentinelOneRemoteScriptExecutionStatus, } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; -import { ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT } from '../../../../../lib/telemetry/event_based/events'; +import { + ENDPOINT_RESPONSE_ACTION_SENT_EVENT, + ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT, +} from '../../../../../lib/telemetry/event_based/events'; jest.mock('../../action_details_by_id', () => { const originalMod = jest.requireActual('../../action_details_by_id'); @@ -235,6 +238,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `isolate` action creation telemetry event', async () => { + await s1ActionsClient.isolate(createS1IsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'isolate', + isAutomated: false, + }, + }); + }); + }); }); describe('#release()', () => { @@ -368,6 +388,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `release` action creation telemetry event', async () => { + await s1ActionsClient.release(createS1IsolationOptions()); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'unisolate', + isAutomated: false, + }, + }); + }); + }); }); describe('#processPendingActions()', () => { @@ -906,7 +943,7 @@ describe('SentinelOneActionsClient class', () => { }); }); - describe('Telemetry', () => { + describe('telemetry events', () => { describe('for Isolate and Release', () => { let s1ActivityHits: Array>; @@ -1387,6 +1424,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `get-file` action creation telemetry event', async () => { + await s1ActionsClient.getFile(getFileReqOptions); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'get-file', + isAutomated: false, + }, + }); + }); + }); }); describe('#getFileInfo()', () => { @@ -1781,6 +1835,23 @@ describe('SentinelOneActionsClient class', () => { expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled(); }); + + describe('telemetry events', () => { + it('should send `kill-process` action creation telemetry event', async () => { + await s1ActionsClient.killProcess(killProcessActionRequest); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'kill-process', + isAutomated: false, + }, + }); + }); + }); }); describe('#runningProcesses()', () => { @@ -1940,5 +2011,22 @@ describe('SentinelOneActionsClient class', () => { { meta: true } ); }); + + describe('telemetry events', () => { + it('should send `kill-process` action creation telemetry event', async () => { + await s1ActionsClient.runningProcesses(processesActionRequest); + + expect( + classConstructorOptions.endpointService.getTelemetryService().reportEvent + ).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, { + responseActions: { + actionId: expect.any(String), + agentType: 'sentinel_one', + command: 'running-processes', + isAutomated: false, + }, + }); + }); + }); }); }); From b9e06f742a82f91778e4432c8f16cd85b2a9a8bc Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 3 Jul 2025 21:27:39 +0200 Subject: [PATCH 2/2] fix type error --- .../endpoint/ms_defender_endpoint_actions_client.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 6ecee6a5aadba..42b5fe6baf4d9 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 @@ -1120,9 +1120,7 @@ describe('MS Defender response actions client', () => { applyEsClientSearchMock({ esClientMock: clientConstructorOptionsMock.esClient, index: ENDPOINT_ACTIONS_INDEX, - response: jest - .fn(() => generator.toEsSearchResponse([])) - .mockReturnValueOnce(actionRequestsSearchResponse), + response: actionRequestsSearchResponse, pitUsage: true, }); @@ -1198,9 +1196,7 @@ describe('MS Defender response actions client', () => { applyEsClientSearchMock({ esClientMock: clientConstructorOptionsMock.esClient, index: ENDPOINT_ACTIONS_INDEX, - response: jest - .fn(() => generator.toEsSearchResponse([])) - .mockReturnValueOnce(actionRequestsSearchResponse), + response: actionRequestsSearchResponse, pitUsage: true, });