mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SecuritySolution][Endpoint][ResponseActions] Response action telemetry (endpoint/third party) (#192685)
## Summary Adds server-side telemetry collection for response action creation and responses. part of elastic/security-team/issues/7466 <details><summary>Events from telemetry staging</summary> <img src="https://github.com/user-attachments/assets/2e9f37f1-c5b5-46e9-be34-c3bdcff4015b" /> <img src="https://github.com/user-attachments/assets/85a5a75d-f9f1-4d76-a782-272d9d7da0cb" /> </details> <details><summary>Dashboard on staging</summary> <img src="https://github.com/user-attachments/assets/9faa96a2-a553-4def-b5da-6b66b5728ca4"> </details> This PR adds Server Side EBTs (event-based telemetry) for: ### Action creation event ```json5 "event_type": [ "endpoint_response_action_sent" ], "properties": [ { "responseActions": { "actionId": "696608a5-1908-457d-9072-5f555c740ffc", "agentType": "sentinel_one", "command": "unisolate", "isAutomated": false } } ], ``` ### Action response event ```json5 { "event_type": [ "endpoint_response_action_status_change_event" ], "properties": [ { "responseActions": { "actionId": "696608a5-1908-457d-9072-5f555c740ffc", "agentType": "sentinel_one", "actionStatus": "successful", "command": "unisolate", } } ], } ``` ### Action creation error event ```json5 "event_type": [ "endpoint_response_action_sent_error" ], "properties": [ { "responseActions": { "command": "execute", "error": "error message", "agentType": "endpoint" } } ], ``` **Note:** This PR does not add response completion telemetry for `endpoint` agent type. There would be follow up PRs to add that and some usage/snapshot telemetry. ### Checklist - [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] If a plugin configuration key changed, check if it needs to be allow-listed 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 commit is contained in:
parent
2d9f13c41f
commit
a80335e378
19 changed files with 668 additions and 109 deletions
|
@ -182,6 +182,12 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
crowdstrikeDataInAnalyzerEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables Response actions telemetry collection
|
||||
* Should be enabled in 8.17.0
|
||||
*/
|
||||
responseActionsTelemetryEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables experimental JAMF integration data to be available in Analyzer
|
||||
*/
|
||||
|
|
|
@ -32,12 +32,13 @@ export const ExecuteActionResult = memo<
|
|||
>(({ command, setStore, store, status, setStatus, ResultComponent }) => {
|
||||
const actionCreator = useSendExecuteEndpoint();
|
||||
const actionRequestBody = useMemo<undefined | ExecuteActionRequestBody>(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
|
||||
|
||||
if (!endpointId) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
agent_type: agentType,
|
||||
endpoint_ids: [endpointId],
|
||||
parameters: {
|
||||
command: command.args.args.command[0],
|
||||
|
@ -46,7 +47,7 @@ export const ExecuteActionResult = memo<
|
|||
comment: command.args.args?.comment?.[0],
|
||||
};
|
||||
}, [
|
||||
command.commandDefinition?.meta?.endpointId,
|
||||
command.commandDefinition?.meta,
|
||||
command.args.args.command,
|
||||
command.args.args.timeout,
|
||||
command.args.args?.comment,
|
||||
|
|
|
@ -23,9 +23,8 @@ export const GetFileActionResult = memo<
|
|||
const actionCreator = useSendGetFileRequest();
|
||||
|
||||
const actionRequestBody = useMemo<undefined | ResponseActionGetFileRequestBody>(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { agentType, endpointId } = command.commandDefinition?.meta ?? {};
|
||||
const { path, comment } = command.args.args;
|
||||
const agentType = command.commandDefinition?.meta?.agentType;
|
||||
|
||||
return endpointId
|
||||
? {
|
||||
|
@ -37,11 +36,7 @@ export const GetFileActionResult = memo<
|
|||
},
|
||||
}
|
||||
: undefined;
|
||||
}, [
|
||||
command.args.args,
|
||||
command.commandDefinition?.meta?.agentType,
|
||||
command.commandDefinition?.meta?.endpointId,
|
||||
]);
|
||||
}, [command.args.args, command.commandDefinition?.meta]);
|
||||
|
||||
const { result, actionDetails } = useConsoleActionSubmitter<ResponseActionGetFileRequestBody>({
|
||||
ResultComponent,
|
||||
|
|
|
@ -153,7 +153,7 @@ describe('When using execute action from response actions console', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.execute).toHaveBeenCalledWith({
|
||||
body: '{"endpoint_ids":["a.b.c"],"parameters":{"command":"ls -al"}}',
|
||||
body: '{"agent_type":"endpoint","endpoint_ids":["a.b.c"],"parameters":{"command":"ls -al"}}',
|
||||
path: EXECUTE_ROUTE,
|
||||
version: '2023-10-31',
|
||||
});
|
||||
|
|
|
@ -19,9 +19,8 @@ export const IsolateActionResult = memo<ActionRequestComponentProps>(
|
|||
const isolateHostApi = useSendIsolateEndpointRequest();
|
||||
|
||||
const actionRequestBody = useMemo(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { agentType, endpointId } = command.commandDefinition?.meta ?? {};
|
||||
const comment = command.args.args?.comment?.[0];
|
||||
const agentType = command.commandDefinition?.meta?.agentType;
|
||||
|
||||
return endpointId
|
||||
? {
|
||||
|
@ -30,12 +29,7 @@ export const IsolateActionResult = memo<ActionRequestComponentProps>(
|
|||
comment,
|
||||
}
|
||||
: undefined;
|
||||
}, [
|
||||
command.args.args?.comment,
|
||||
command.commandDefinition?.meta?.agentType,
|
||||
command.commandDefinition?.meta?.endpointId,
|
||||
isSentinelOneV1Enabled,
|
||||
]);
|
||||
}, [command.args.args?.comment, command.commandDefinition?.meta, isSentinelOneV1Enabled]);
|
||||
|
||||
return useConsoleActionSubmitter({
|
||||
ResultComponent,
|
||||
|
|
|
@ -18,8 +18,7 @@ export const KillProcessActionResult = memo<
|
|||
const actionCreator = useSendKillProcessRequest();
|
||||
|
||||
const actionRequestBody = useMemo<undefined | KillProcessRequestBody>(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const agentType = command.commandDefinition?.meta?.agentType;
|
||||
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
|
||||
const parameters = parsedKillOrSuspendParameter(command.args.args);
|
||||
|
||||
return endpointId
|
||||
|
@ -30,11 +29,7 @@ export const KillProcessActionResult = memo<
|
|||
parameters,
|
||||
}
|
||||
: undefined;
|
||||
}, [
|
||||
command.args.args,
|
||||
command.commandDefinition?.meta?.agentType,
|
||||
command.commandDefinition?.meta?.endpointId,
|
||||
]);
|
||||
}, [command.args.args, command.commandDefinition?.meta]);
|
||||
|
||||
return useConsoleActionSubmitter<KillProcessRequestBody>({
|
||||
ResultComponent,
|
||||
|
|
|
@ -19,9 +19,8 @@ export const ReleaseActionResult = memo<ActionRequestComponentProps>(
|
|||
const releaseHostApi = useSendReleaseEndpointRequest();
|
||||
|
||||
const actionRequestBody = useMemo(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { endpointId, agentType } = command.commandDefinition?.meta ?? {};
|
||||
const comment = command.args.args?.comment?.[0];
|
||||
const agentType = command.commandDefinition?.meta?.agentType;
|
||||
|
||||
return endpointId
|
||||
? {
|
||||
|
@ -30,12 +29,7 @@ export const ReleaseActionResult = memo<ActionRequestComponentProps>(
|
|||
comment,
|
||||
}
|
||||
: undefined;
|
||||
}, [
|
||||
command.args.args?.comment,
|
||||
command.commandDefinition?.meta?.agentType,
|
||||
command.commandDefinition?.meta?.endpointId,
|
||||
isSentinelOneV1Enabled,
|
||||
]);
|
||||
}, [command.args.args?.comment, command.commandDefinition?.meta, isSentinelOneV1Enabled]);
|
||||
|
||||
return useConsoleActionSubmitter({
|
||||
ResultComponent,
|
||||
|
|
|
@ -23,19 +23,20 @@ export const SuspendProcessActionResult = memo<
|
|||
const actionCreator = useSendSuspendProcessRequest();
|
||||
|
||||
const actionRequestBody = useMemo<undefined | SuspendProcessRequestBody>(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { agentType, endpointId } = command.commandDefinition?.meta ?? {};
|
||||
const parameters = parsedKillOrSuspendParameter(command.args.args) as
|
||||
| ResponseActionParametersWithPid
|
||||
| ResponseActionParametersWithEntityId;
|
||||
|
||||
return endpointId
|
||||
? {
|
||||
agent_type: agentType,
|
||||
endpoint_ids: [endpointId],
|
||||
comment: command.args.args?.comment?.[0],
|
||||
parameters,
|
||||
}
|
||||
: undefined;
|
||||
}, [command.args.args, command.commandDefinition?.meta?.endpointId]);
|
||||
}, [command.args.args, command.commandDefinition?.meta]);
|
||||
|
||||
return useConsoleActionSubmitter<SuspendProcessRequestBody, SuspendProcessActionOutputContent>({
|
||||
ResultComponent,
|
||||
|
|
|
@ -29,7 +29,7 @@ export const UploadActionResult = memo<
|
|||
const actionCreator = useSendUploadEndpointRequest();
|
||||
|
||||
const actionRequestBody = useMemo<undefined | UploadActionUIRequestBody>(() => {
|
||||
const endpointId = command.commandDefinition?.meta?.endpointId;
|
||||
const { agentType, endpointId } = command.commandDefinition?.meta ?? {};
|
||||
const { comment, overwrite, file } = command.args.args;
|
||||
|
||||
if (!endpointId) {
|
||||
|
@ -37,6 +37,7 @@ export const UploadActionResult = memo<
|
|||
}
|
||||
|
||||
const reqBody: UploadActionUIRequestBody = {
|
||||
agent_type: agentType,
|
||||
endpoint_ids: [endpointId],
|
||||
...(comment?.[0] ? { comment: comment?.[0] } : {}),
|
||||
parameters:
|
||||
|
@ -49,7 +50,7 @@ export const UploadActionResult = memo<
|
|||
};
|
||||
|
||||
return reqBody;
|
||||
}, [command.args.args, command.commandDefinition?.meta?.endpointId]);
|
||||
}, [command.args.args, command.commandDefinition?.meta]);
|
||||
|
||||
const { result, actionDetails } = useConsoleActionSubmitter<
|
||||
UploadActionUIRequestBody,
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.model\":{},\"properties.resourceAccessed\":{},\"properties.resultCount\":{},\"properties.responseTime\":{},\"properties.errorMessage\":{},\"properties.isEnabledKnowledgeBase\":{},\"properties.isEnabledRAGAlerts\":{},\"properties.assistantStreamingEnabled\":{},\"properties.actionTypeId\":{},\"properties.message\":{},\"properties.productTier\":{},\"properties.failedToDeleteCount\":{},\"properties.totalInstalledCount\":{},\"properties.scoresWritten\":{},\"properties.taskDurationInSeconds\":{},\"properties.interval\":{},\"properties.alertSampleSizePerShard\":{},\"properties.status\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.result.successful\":{},\"properties.result.failed\":{},\"properties.result.total\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.discoveriesGenerated\":{},\"properties.durationMs\":{},\"properties.provider\":{},\"properties.total_tokens\":{},\"properties.prompt_tokens\":{},\"properties.completion_tokens\":{},\"properties.suppressionRuleType\":{},\"properties.suppressionMissingFields\":{},\"properties.suppressionAlertsCreated\":{},\"properties.suppressionAlertsSuppressed\":{},\"properties.suppressionRuleName\":{},\"properties.suppressionDuration\":{},\"properties.suppressionFieldsNumber\":{},\"properties.suppressionGroupByFieldsNumber\":{},\"properties.suppressionGroupByFields\":{},\"properties.suppressionRuleId\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-server","runtimeFieldMap":"{\"properties.message\":{\"type\":\"keyword\"},\"properties.productTier\":{\"type\":\"keyword\"},\"properties.failedToDeleteCount\":{\"type\":\"long\"},\"properties.totalInstalledCount\":{\"type\":\"long\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.discoveriesGenerated\":{\"type\":\"long\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.scoresWritten\":{\"type\":\"long\"},\"properties.taskDurationInSeconds\":{\"type\":\"long\"},\"properties.interval\":{\"type\":\"keyword\"},\"properties.alertSampleSizePerShard\":{\"type\":\"long\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.result.successful\":{\"type\":\"long\"},\"properties.result.failed\":{\"type\":\"long\"},\"properties.result.total\":{\"type\":\"long\"},\"properties.total_tokens\":{\"type\":\"long\"},\"properties.prompt_tokens\":{\"type\":\"long\"},\"properties.completion_tokens\":{\"type\":\"keyword\"},\"properties.suppressionMissingFields\":{\"type\":\"boolean\"},\"properties.suppressionAlertsCreated\":{\"type\":\"long\"},\"properties.suppressionAlertsSuppressed\":{\"type\":\"long\"},\"properties.suppressionRuleName\":{\"type\":\"keyword\"},\"properties.suppressionDuration\":{\"type\":\"long\"},\"properties.suppressionRuleType\":{\"type\":\"keyword\"},\"properties.suppressionGroupByFieldsNumber\":{\"type\":\"long\"},\"properties.suppressionGroupByFields\":{\"type\":\"keyword\"},\"properties.suppressionRuleId\":{\"type\":\"keyword\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-server"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:44.874Z","id":"security-solution-ebt-kibana-server","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-07-30T11:12:43.928Z","version":"WzM4ODczLDVd"}
|
||||
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]}
|
||||
{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.model\":{},\"properties.resourceAccessed\":{},\"properties.resultCount\":{},\"properties.responseTime\":{},\"properties.errorMessage\":{},\"properties.isEnabledKnowledgeBase\":{},\"properties.isEnabledRAGAlerts\":{},\"properties.assistantStreamingEnabled\":{},\"properties.actionTypeId\":{},\"properties.message\":{},\"properties.productTier\":{},\"properties.failedToDeleteCount\":{},\"properties.totalInstalledCount\":{},\"properties.scoresWritten\":{},\"properties.taskDurationInSeconds\":{},\"properties.interval\":{},\"properties.alertSampleSizePerShard\":{},\"properties.status\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.result.successful\":{},\"properties.result.failed\":{},\"properties.result.total\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.discoveriesGenerated\":{},\"properties.durationMs\":{},\"properties.provider\":{},\"properties.total_tokens\":{},\"properties.prompt_tokens\":{},\"properties.completion_tokens\":{},\"properties.suppressionRuleType\":{},\"properties.suppressionMissingFields\":{},\"properties.suppressionAlertsCreated\":{},\"properties.suppressionAlertsSuppressed\":{},\"properties.suppressionRuleName\":{},\"properties.suppressionDuration\":{},\"properties.suppressionFieldsNumber\":{},\"properties.suppressionGroupByFieldsNumber\":{},\"properties.suppressionGroupByFields\":{},\"properties.suppressionRuleId\":{},\"properties.responseActions.actionId\":{},\"properties.responseActions.agentType\":{},\"properties.responseActions.command\":{},\"properties.responseActions.endpointIds\":{},\"properties.responseActions.isAutomated\":{},\"properties.responseActions.actionStatus\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-server","runtimeFieldMap":"{\"properties.message\":{\"type\":\"keyword\"},\"properties.productTier\":{\"type\":\"keyword\"},\"properties.failedToDeleteCount\":{\"type\":\"long\"},\"properties.totalInstalledCount\":{\"type\":\"long\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.total_tokens\":{\"type\":\"long\"},\"properties.prompt_tokens\":{\"type\":\"long\"},\"properties.completion_tokens\":{\"type\":\"keyword\"},\"properties.suppressionGroupByFields\":{\"type\":\"keyword\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.discoveriesGenerated\":{\"type\":\"long\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.scoresWritten\":{\"type\":\"long\"},\"properties.taskDurationInSeconds\":{\"type\":\"long\"},\"properties.interval\":{\"type\":\"keyword\"},\"properties.alertSampleSizePerShard\":{\"type\":\"long\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.result.successful\":{\"type\":\"long\"},\"properties.result.failed\":{\"type\":\"long\"},\"properties.result.total\":{\"type\":\"long\"},\"properties.suppressionAlertsCreated\":{\"type\":\"long\"},\"properties.suppressionAlertsSuppressed\":{\"type\":\"long\"},\"properties.suppressionRuleName\":{\"type\":\"keyword\"},\"properties.suppressionDuration\":{\"type\":\"long\"},\"properties.suppressionGroupByFieldsNumber\":{\"type\":\"long\"},\"properties.suppressionRuleType\":{\"type\":\"keyword\"},\"properties.suppressionMissingFields\":{\"type\":\"boolean\"},\"properties.suppressionRuleId\":{\"type\":\"keyword\"},\"properties.responseActions.actionId\":{\"type\":\"keyword\"},\"properties.responseActions.agentType\":{\"type\":\"keyword\"},\"properties.responseActions.command\":{\"type\":\"keyword\"},\"properties.responseActions.endpointIds\":{\"type\":\"keyword\"},\"properties.responseActions.isAutomated\":{\"type\":\"boolean\"},\"properties.responseActions.actionStatus\":{\"type\":\"keyword\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-server"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:44.874Z","id":"security-solution-ebt-kibana-server","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-09-16T11:22:09.683Z","version":"WzQ2MDU0LDdd"}
|
||||
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
AnalyticsServiceSetup,
|
||||
ElasticsearchClient,
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
|
@ -58,6 +59,7 @@ export interface EndpointAppContextServiceSetupContract {
|
|||
securitySolutionRequestContextFactory: IRequestContextFactory;
|
||||
cloud: CloudSetup;
|
||||
loggerFactory: LoggerFactory;
|
||||
telemetry: AnalyticsServiceSetup;
|
||||
}
|
||||
|
||||
export interface EndpointAppContextServiceStartContract {
|
||||
|
@ -339,4 +341,11 @@ export class EndpointAppContextService {
|
|||
|
||||
return this.startDependencies.createFleetActionsClient('endpoint');
|
||||
}
|
||||
|
||||
public getTelemetryService(): AnalyticsServiceSetup {
|
||||
if (!this.setupDependencies?.telemetry) {
|
||||
throw new EndpointAppContentServicesNotSetUpError();
|
||||
}
|
||||
return this.setupDependencies.telemetry;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import type { ScopedClusterClientMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
analyticsServiceMock,
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
httpServiceMock,
|
||||
|
@ -128,6 +129,7 @@ export const createMockEndpointAppContextService = (
|
|||
getExceptionListsClient: jest.fn(),
|
||||
getMessageSigningService: jest.fn().mockReturnValue(messageSigningService),
|
||||
getFleetActionsClient: jest.fn(async (_) => fleetActionsClientMock),
|
||||
getTelemetryService: jest.fn(),
|
||||
getInternalResponseActionsClient: jest.fn(() => {
|
||||
return responseActionsClientMock.create();
|
||||
}),
|
||||
|
@ -143,6 +145,7 @@ export const createMockEndpointAppContextServiceSetupContract =
|
|||
securitySolutionRequestContextFactory: requestContextFactoryMock.create(),
|
||||
cloud: cloudMock.createSetup(),
|
||||
loggerFactory: loggingSystemMock.create(),
|
||||
telemetry: analyticsServiceMock.createAnalyticsServiceSetup(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -55,7 +55,10 @@ import type {
|
|||
ResponseActionsExecuteParameters,
|
||||
ResponseActionScanParameters,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type {
|
||||
ResponseActionAgentType,
|
||||
ResponseActionsApiCommandNames,
|
||||
} from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type {
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
|
@ -321,15 +324,12 @@ function responseActionRequestHandler<T extends EndpointActionDataParameterTypes
|
|||
return async (context, req, res) => {
|
||||
logger.debug(() => `response action [${command}]:\n${stringify(req.body)}`);
|
||||
|
||||
const experimentalFeatures = endpointContext.experimentalFeatures;
|
||||
|
||||
// Note: because our API schemas are defined as module static variables (as opposed to a
|
||||
// `getter` function), we need to include this additional validation here, since
|
||||
// `agent_type` is included in the schema independent of the feature flag
|
||||
if (
|
||||
(req.body.agent_type === 'sentinel_one' &&
|
||||
!endpointContext.experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
|
||||
(req.body.agent_type === 'crowdstrike' &&
|
||||
!endpointContext.experimentalFeatures.responseActionsCrowdstrikeManualHostIsolationEnabled)
|
||||
) {
|
||||
if (isThirdPartyFeatureDisabled(req.body.agent_type, experimentalFeatures)) {
|
||||
return errorHandler(
|
||||
logger,
|
||||
res,
|
||||
|
@ -354,59 +354,12 @@ function responseActionRequestHandler<T extends EndpointActionDataParameterTypes
|
|||
);
|
||||
|
||||
try {
|
||||
let action: ActionDetails;
|
||||
|
||||
switch (command) {
|
||||
case 'isolate':
|
||||
action = await responseActionsClient.isolate(req.body);
|
||||
break;
|
||||
|
||||
case 'unisolate':
|
||||
action = await responseActionsClient.release(req.body);
|
||||
break;
|
||||
|
||||
case 'running-processes':
|
||||
action = await responseActionsClient.runningProcesses(req.body);
|
||||
break;
|
||||
|
||||
case 'execute':
|
||||
action = await responseActionsClient.execute(req.body as ExecuteActionRequestBody);
|
||||
break;
|
||||
|
||||
case 'suspend-process':
|
||||
action = await responseActionsClient.suspendProcess(
|
||||
req.body as SuspendProcessRequestBody
|
||||
);
|
||||
break;
|
||||
|
||||
case 'kill-process':
|
||||
action = await responseActionsClient.killProcess(req.body as KillProcessRequestBody);
|
||||
break;
|
||||
|
||||
case 'get-file':
|
||||
action = await responseActionsClient.getFile(
|
||||
req.body as ResponseActionGetFileRequestBody
|
||||
);
|
||||
break;
|
||||
|
||||
case 'upload':
|
||||
action = await responseActionsClient.upload(req.body as UploadActionApiRequestBody);
|
||||
break;
|
||||
|
||||
case 'scan':
|
||||
action = await responseActionsClient.scan(req.body as ScanActionRequestBody);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new CustomHttpRequestError(
|
||||
`No handler found for response action command: [${command}]`,
|
||||
501
|
||||
);
|
||||
}
|
||||
|
||||
const action: ActionDetails = await handleActionCreation(
|
||||
command,
|
||||
req.body,
|
||||
responseActionsClient
|
||||
);
|
||||
const { action: actionId, ...data } = action;
|
||||
|
||||
// `action` is deprecated, but still returned in order to ensure backwards compatibility
|
||||
const legacyResponseData = responseActionsWithLegacyActionProperty.includes(command)
|
||||
? {
|
||||
action: actionId ?? data.id ?? '',
|
||||
|
@ -425,6 +378,49 @@ function responseActionRequestHandler<T extends EndpointActionDataParameterTypes
|
|||
};
|
||||
}
|
||||
|
||||
function isThirdPartyFeatureDisabled(
|
||||
agentType: ResponseActionAgentType | undefined,
|
||||
experimentalFeatures: EndpointAppContext['experimentalFeatures']
|
||||
): boolean {
|
||||
return (
|
||||
(agentType === 'sentinel_one' && !experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
|
||||
(agentType === 'crowdstrike' &&
|
||||
!experimentalFeatures.responseActionsCrowdstrikeManualHostIsolationEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleActionCreation(
|
||||
command: ResponseActionsApiCommandNames,
|
||||
body: ResponseActionsRequestBody,
|
||||
responseActionsClient: ResponseActionsClient
|
||||
): Promise<ActionDetails> {
|
||||
switch (command) {
|
||||
case 'isolate':
|
||||
return responseActionsClient.isolate(body);
|
||||
case 'unisolate':
|
||||
return responseActionsClient.release(body);
|
||||
case 'running-processes':
|
||||
return responseActionsClient.runningProcesses(body);
|
||||
case 'execute':
|
||||
return responseActionsClient.execute(body as ExecuteActionRequestBody);
|
||||
case 'suspend-process':
|
||||
return responseActionsClient.suspendProcess(body as SuspendProcessRequestBody);
|
||||
case 'kill-process':
|
||||
return responseActionsClient.killProcess(body as KillProcessRequestBody);
|
||||
case 'get-file':
|
||||
return responseActionsClient.getFile(body as ResponseActionGetFileRequestBody);
|
||||
case 'upload':
|
||||
return responseActionsClient.upload(body as UploadActionApiRequestBody);
|
||||
case 'scan':
|
||||
return responseActionsClient.scan(body as ScanActionRequestBody);
|
||||
default:
|
||||
throw new CustomHttpRequestError(
|
||||
`No handler found for response action command: [${command}]`,
|
||||
501
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function redirectHandler(
|
||||
location: string
|
||||
): RequestHandler<
|
||||
|
|
|
@ -43,6 +43,10 @@ import { getResponseActionFeatureKey } from '../../../feature_usage/feature_keys
|
|||
import { isActionSupportedByAgentType as _isActionSupportedByAgentType } from '../../../../../../common/endpoint/service/response_actions/is_response_action_supported';
|
||||
import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_EVENT,
|
||||
} from '../../../../../lib/telemetry/event_based/events';
|
||||
|
||||
jest.mock('../../action_details_by_id', () => {
|
||||
const original = jest.requireActual('../../action_details_by_id');
|
||||
|
@ -535,6 +539,100 @@ describe('ResponseActionsClientImpl base class', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Telemetry', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error
|
||||
endpointAppContextService.experimentalFeatures.responseActionsTelemetryEnabled = true;
|
||||
});
|
||||
|
||||
it('should send action creation success telemetry for manual actions', async () => {
|
||||
await baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).toHaveBeenCalledWith(
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType,
|
||||
{
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
agentType: indexDocOptions.agent_type,
|
||||
command: indexDocOptions.command,
|
||||
isAutomated: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should send action creation success telemetry for automated actions', async () => {
|
||||
constructorOptions.isAutomated = true;
|
||||
baseClassMock = new MockClassWithExposedProtectedMembers(constructorOptions);
|
||||
|
||||
await baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).toHaveBeenCalledWith(
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType,
|
||||
{
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
agentType: indexDocOptions.agent_type,
|
||||
command: indexDocOptions.command,
|
||||
isAutomated: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should send error telemetry if action creation fails', async () => {
|
||||
esClient.index.mockImplementation(async () => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
const responsePromise = baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
await expect(responsePromise).rejects.toBeInstanceOf(ResponseActionsClientError);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).toHaveBeenCalledWith(
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT.eventType,
|
||||
{
|
||||
responseActions: {
|
||||
agentType: indexDocOptions.agent_type,
|
||||
command: indexDocOptions.command,
|
||||
error: 'test error',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Telemetry (with feature disabled)', () => {
|
||||
// although this is redundant, it is here to make sure that it works as expected wit the feature disabled
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error
|
||||
endpointAppContextService.experimentalFeatures.responseActionsTelemetryEnabled = false;
|
||||
});
|
||||
|
||||
it('should not send action creation success telemetry for manual actions', async () => {
|
||||
await baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not send action creation success telemetry for automated actions', async () => {
|
||||
constructorOptions.isAutomated = true;
|
||||
baseClassMock = new MockClassWithExposedProtectedMembers(constructorOptions);
|
||||
|
||||
await baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not send error telemetry if action creation fails', async () => {
|
||||
esClient.index.mockImplementation(async () => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
const responsePromise = baseClassMock.writeActionRequestToEndpointIndex(indexDocOptions);
|
||||
await expect(responsePromise).rejects.toBeInstanceOf(ResponseActionsClientError);
|
||||
|
||||
expect(endpointAppContextService.getTelemetryService().reportEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#writeActionResponseToEndpointIndex()', () => {
|
||||
|
|
|
@ -13,6 +13,11 @@ import { AttachmentType, ExternalReferenceStorageType } from '@kbn/cases-plugin/
|
|||
import type { CaseAttachments } from '@kbn/cases-plugin/public/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT,
|
||||
} from '../../../../../lib/telemetry/event_based/events';
|
||||
import { NotFoundError } from '../../../../errors';
|
||||
import { fetchActionRequestById } from '../../utils/fetch_action_request_by_id';
|
||||
import { SimpleMemCache } from './simple_mem_cache';
|
||||
|
@ -513,8 +518,12 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
);
|
||||
}
|
||||
|
||||
this.sendActionCreationTelemetry(doc);
|
||||
|
||||
return doc;
|
||||
} catch (err) {
|
||||
this.sendActionCreationErrorTelemetry(actionRequest.command, err);
|
||||
|
||||
if (!(err instanceof ResponseActionsClientError)) {
|
||||
throw new ResponseActionsClientError(
|
||||
`Failed to create action request document: ${err.message}`,
|
||||
|
@ -709,6 +718,58 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
});
|
||||
}
|
||||
|
||||
protected sendActionCreationTelemetry(actionRequest: LogsEndpointAction): void {
|
||||
if (!this.options.endpointService.experimentalFeatures.responseActionsTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
this.options.endpointService
|
||||
.getTelemetryService()
|
||||
.reportEvent(ENDPOINT_RESPONSE_ACTION_SENT_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: actionRequest.EndpointActions.action_id,
|
||||
agentType: this.agentType,
|
||||
command: actionRequest.EndpointActions.data.command,
|
||||
isAutomated: this.options.isAutomated ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected sendActionCreationErrorTelemetry(
|
||||
command: ResponseActionsApiCommandNames,
|
||||
error: Error
|
||||
): void {
|
||||
if (!this.options.endpointService.experimentalFeatures.responseActionsTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
this.options.endpointService
|
||||
.getTelemetryService()
|
||||
.reportEvent(ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT.eventType, {
|
||||
responseActions: {
|
||||
agentType: this.agentType,
|
||||
command,
|
||||
error: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected sendActionResponseTelemetry(responseList: LogsEndpointActionResponse[]): void {
|
||||
if (!this.options.endpointService.experimentalFeatures.responseActionsTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
for (const response of responseList) {
|
||||
this.options.endpointService
|
||||
.getTelemetryService()
|
||||
.reportEvent(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: response.EndpointActions.action_id,
|
||||
agentType: this.agentType,
|
||||
actionStatus: response.error ? 'failed' : 'successful',
|
||||
command: response.EndpointActions.data.command,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async isolate(
|
||||
actionRequest: IsolationRouteRequestBody,
|
||||
options?: CommonResponseActionMethodOptions
|
||||
|
|
|
@ -51,6 +51,7 @@ 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';
|
||||
|
||||
jest.mock('../../action_details_by_id', () => {
|
||||
const originalMod = jest.requireActual('../../action_details_by_id');
|
||||
|
@ -803,7 +804,7 @@ describe('SentinelOneActionsClient class', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should create response at error if request has no parentTaskId', async () => {
|
||||
it('should create response as error if request has no parentTaskId', async () => {
|
||||
// @ts-expect-error
|
||||
actionRequestsSearchResponse.hits.hits[0]!._source!.meta!.parentTaskId = '';
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
@ -904,6 +905,278 @@ describe('SentinelOneActionsClient class', () => {
|
|||
expect(processPendingActionsOptions.addToQueue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Telemetry', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error
|
||||
classConstructorOptions.endpointService.experimentalFeatures.responseActionsTelemetryEnabled =
|
||||
true;
|
||||
});
|
||||
describe('for Isolate and Release', () => {
|
||||
let s1ActivityHits: Array<SearchHit<SentinelOneActivityEsDoc>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const s1DataGenerator = new SentinelOneDataGenerator('seed');
|
||||
const actionRequestsSearchResponse = s1DataGenerator.toEsSearchResponse([
|
||||
s1DataGenerator.generateActionEsHit<undefined, {}, SentinelOneIsolationRequestMeta>({
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
meta: {
|
||||
agentId: 's1-agent-a',
|
||||
agentUUID: 'agent-uuid-1',
|
||||
hostName: 's1-host-name',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const actionResponsesSearchResponse = s1DataGenerator.toEsSearchResponse<
|
||||
LogsEndpointActionResponse | EndpointActionResponse
|
||||
>([]);
|
||||
const s1ActivitySearchResponse = s1DataGenerator.generateActivityEsSearchResponse([
|
||||
s1DataGenerator.generateActivityEsSearchHit({
|
||||
sentinel_one: {
|
||||
activity: {
|
||||
agent: {
|
||||
id: 's1-agent-a',
|
||||
},
|
||||
type: 1001,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
s1ActivityHits = s1ActivitySearchResponse.hits.hits;
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
response: actionRequestsSearchResponse,
|
||||
pitUsage: true,
|
||||
});
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN,
|
||||
response: actionResponsesSearchResponse,
|
||||
});
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: SENTINEL_ONE_ACTIVITY_INDEX_PATTERN,
|
||||
response: s1ActivitySearchResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/failed action', async () => {
|
||||
s1ActivityHits[0]._source!.sentinel_one.activity.type = 2010;
|
||||
s1ActivityHits[0]._source!.sentinel_one.activity.description.primary =
|
||||
'Agent SOME_HOST_NAME was unable to disconnect from network.';
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'failed',
|
||||
agentType: 'sentinel_one',
|
||||
command: 'isolate',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/successful action', async () => {
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'successful',
|
||||
agentType: 'sentinel_one',
|
||||
command: 'isolate',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for get-file response action', () => {
|
||||
let actionRequestsSearchResponse: SearchResponse<
|
||||
LogsEndpointAction<ResponseActionGetFileParameters, ResponseActionGetFileOutputContent>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
const s1DataGenerator = new SentinelOneDataGenerator('seed');
|
||||
actionRequestsSearchResponse = s1DataGenerator.toEsSearchResponse([
|
||||
s1DataGenerator.generateActionEsHit<
|
||||
ResponseActionGetFileParameters,
|
||||
ResponseActionGetFileOutputContent,
|
||||
SentinelOneGetFileRequestMeta
|
||||
>({
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
EndpointActions: { data: { command: 'get-file' } },
|
||||
meta: {
|
||||
agentId: 's1-agent-a',
|
||||
agentUUID: 'agent-uuid-1',
|
||||
hostName: 's1-host-name',
|
||||
commandBatchUuid: 'batch-111',
|
||||
activityId: 'activity-222',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const actionResponsesSearchResponse = s1DataGenerator.toEsSearchResponse<
|
||||
LogsEndpointActionResponse | EndpointActionResponse
|
||||
>([]);
|
||||
const s1ActivitySearchResponse = s1DataGenerator.generateActivityEsSearchResponse([
|
||||
s1DataGenerator.generateActivityEsSearchHit<SentinelOneActivityDataForType80>({
|
||||
sentinel_one: {
|
||||
activity: {
|
||||
id: 'activity-222',
|
||||
data: s1DataGenerator.generateActivityFetchFileResponseData({
|
||||
flattened: {
|
||||
commandBatchUuid: 'batch-111',
|
||||
},
|
||||
}),
|
||||
agent: {
|
||||
id: 's1-agent-a',
|
||||
},
|
||||
type: 80,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
response: actionRequestsSearchResponse,
|
||||
pitUsage: true,
|
||||
});
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN,
|
||||
response: actionResponsesSearchResponse,
|
||||
});
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: SENTINEL_ONE_ACTIVITY_INDEX_PATTERN,
|
||||
response: s1ActivitySearchResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/failed action', async () => {
|
||||
actionRequestsSearchResponse.hits.hits[0]!._source!.meta = {
|
||||
agentId: 's1-agent-a',
|
||||
agentUUID: 'agent-uuid-1',
|
||||
hostName: 's1-host-name',
|
||||
};
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'failed',
|
||||
agentType: 'sentinel_one',
|
||||
command: 'get-file',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/successful action', async () => {
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'successful',
|
||||
agentType: 'sentinel_one',
|
||||
command: 'get-file',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
actionName | requestData
|
||||
${'kill-process'} | ${{ command: 'kill-process', parameters: { process_name: 'foo' } }}
|
||||
${'running-processes'} | ${{ command: 'running-processes', parameters: undefined }}
|
||||
`('for $actionName response action', ({ actionName, requestData }) => {
|
||||
let actionRequestsSearchResponse: SearchResponse<LogsEndpointAction>;
|
||||
|
||||
beforeEach(() => {
|
||||
const s1DataGenerator = new SentinelOneDataGenerator('seed');
|
||||
|
||||
actionRequestsSearchResponse = s1DataGenerator.toEsSearchResponse([
|
||||
s1DataGenerator.generateActionEsHit({
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
EndpointActions: {
|
||||
data: requestData,
|
||||
},
|
||||
meta: {
|
||||
agentId: 's1-agent-a',
|
||||
agentUUID: 'agent-uuid-1',
|
||||
hostName: 's1-host-name',
|
||||
parentTaskId: 's1-parent-task-123',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const actionResponsesSearchResponse = s1DataGenerator.toEsSearchResponse<
|
||||
LogsEndpointActionResponse | EndpointActionResponse
|
||||
>([]);
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
response: actionRequestsSearchResponse,
|
||||
pitUsage: true,
|
||||
});
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: classConstructorOptions.esClient,
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX_PATTERN,
|
||||
response: actionResponsesSearchResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/failed action', async () => {
|
||||
// @ts-expect-error
|
||||
actionRequestsSearchResponse.hits.hits[0]!._source!.meta!.parentTaskId = '';
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'failed',
|
||||
agentType: 'sentinel_one',
|
||||
command: actionName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send action response telemetry for completed/successful action', async () => {
|
||||
await s1ActionsClient.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(
|
||||
classConstructorOptions.endpointService.getTelemetryService().reportEvent
|
||||
).toHaveBeenCalledWith(ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT.eventType, {
|
||||
responseActions: {
|
||||
actionId: expect.any(String),
|
||||
actionStatus: 'successful',
|
||||
agentType: 'sentinel_one',
|
||||
command: actionName,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFile()', () => {
|
||||
|
|
|
@ -824,7 +824,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
return actionDetails;
|
||||
}
|
||||
|
||||
public async runningProcesses(
|
||||
async runningProcesses(
|
||||
actionRequest: GetProcessesRequestBody,
|
||||
options?: CommonResponseActionMethodOptions
|
||||
): Promise<ActionDetails<GetProcessesActionOutputContent>> {
|
||||
|
@ -908,6 +908,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
const addResponsesToQueueIfAny = (responseList: LogsEndpointActionResponse[]): void => {
|
||||
if (responseList.length > 0) {
|
||||
addToQueue(...responseList);
|
||||
|
||||
this.sendActionResponseTelemetry(responseList);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -950,8 +952,6 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
);
|
||||
break;
|
||||
|
||||
// FIXME:PT refactor kill-process entry here when that PR is merged
|
||||
|
||||
case 'get-file':
|
||||
addResponsesToQueueIfAny(
|
||||
await this.checkPendingGetFileActions(
|
||||
|
@ -967,8 +967,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
break;
|
||||
|
||||
case 'kill-process':
|
||||
{
|
||||
const responseDocsForKillProcess = await this.checkPendingKillProcessActions(
|
||||
addResponsesToQueueIfAny(
|
||||
await this.checkPendingKillProcessActions(
|
||||
typePendingActions as Array<
|
||||
ResponseActionsClientPendingAction<
|
||||
ResponseActionParametersWithProcessName,
|
||||
|
@ -976,11 +976,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
|
|||
SentinelOneKillProcessRequestMeta
|
||||
>
|
||||
>
|
||||
);
|
||||
if (responseDocsForKillProcess.length) {
|
||||
addToQueue(...responseDocsForKillProcess);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { EventTypeOpts } from '@kbn/core/server';
|
||||
import type {
|
||||
ResponseActionAgentType,
|
||||
ResponseActionStatus,
|
||||
ResponseActionsApiCommandNames,
|
||||
} from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { BulkUpsertAssetCriticalityRecordsResponse } from '../../../../common/api/entity_analytics';
|
||||
|
||||
export const RISK_SCORE_EXECUTION_SUCCESS_EVENT: EventTypeOpts<{
|
||||
|
@ -250,10 +255,139 @@ const getUploadStatus = (stats?: BulkUpsertAssetCriticalityRecordsResponse['stat
|
|||
return 'fail';
|
||||
};
|
||||
|
||||
export const ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT: EventTypeOpts<{
|
||||
responseActions: {
|
||||
agentType: ResponseActionAgentType;
|
||||
command: ResponseActionsApiCommandNames;
|
||||
error: string;
|
||||
};
|
||||
}> = {
|
||||
eventType: 'endpoint_response_action_sent_error',
|
||||
schema: {
|
||||
responseActions: {
|
||||
properties: {
|
||||
agentType: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The type of agent that the action was sent to',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
command: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The command that was sent to the endpoint',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
error: {
|
||||
type: 'text',
|
||||
_meta: {
|
||||
description: 'The error message for the response action',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ENDPOINT_RESPONSE_ACTION_SENT_EVENT: EventTypeOpts<{
|
||||
responseActions: {
|
||||
actionId: string;
|
||||
agentType: ResponseActionAgentType;
|
||||
command: ResponseActionsApiCommandNames;
|
||||
isAutomated: boolean;
|
||||
};
|
||||
}> = {
|
||||
eventType: 'endpoint_response_action_sent',
|
||||
schema: {
|
||||
responseActions: {
|
||||
properties: {
|
||||
actionId: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The ID of the action that was sent to the endpoint',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
agentType: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The type of agent that the action was sent to',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
command: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The command that was sent to the endpoint',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
isAutomated: {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'Whether the action was auto-initiated by a pre-configured rule',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT: EventTypeOpts<{
|
||||
responseActions: {
|
||||
actionId: string;
|
||||
agentType: ResponseActionAgentType;
|
||||
actionStatus: ResponseActionStatus;
|
||||
command: ResponseActionsApiCommandNames;
|
||||
};
|
||||
}> = {
|
||||
eventType: 'endpoint_response_action_status_change_event',
|
||||
schema: {
|
||||
responseActions: {
|
||||
properties: {
|
||||
actionId: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The ID of the action that was sent to the endpoint',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
agentType: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The type of agent that the action was sent to',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
actionStatus: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The status of the action',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
command: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The command that was sent to the endpoint',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const events = [
|
||||
RISK_SCORE_EXECUTION_SUCCESS_EVENT,
|
||||
RISK_SCORE_EXECUTION_ERROR_EVENT,
|
||||
RISK_SCORE_EXECUTION_CANCELLATION_EVENT,
|
||||
ASSET_CRITICALITY_SYSTEM_PROCESSED_ASSIGNMENT_FILE_EVENT,
|
||||
ALERT_SUPPRESSION_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT,
|
||||
ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT,
|
||||
];
|
||||
|
|
|
@ -238,6 +238,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
securitySolutionRequestContextFactory: requestContextFactory,
|
||||
cloud: plugins.cloud,
|
||||
loggerFactory: this.pluginContext.logger,
|
||||
telemetry: core.analytics,
|
||||
});
|
||||
|
||||
initUsageCollectors({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue