From d3cd29666972a22d9786331ae3c08ec4811485d8 Mon Sep 17 00:00:00 2001
From: Ash <1849116+ashokaditya@users.noreply.github.com>
Date: Fri, 27 Sep 2024 10:06:31 +0200
Subject: [PATCH] [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
Events from telemetry staging
Dashboard on staging
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)
(cherry picked from commit a80335e378da3a063bb79a191c550a88578afd07)
---
.../common/experimental_features.ts | 6 +
.../execute_action.tsx | 5 +-
.../get_file_action.tsx | 9 +-
.../integration_tests/execute_action.test.tsx | 2 +-
.../isolate_action.tsx | 10 +-
.../kill_process_action.tsx | 9 +-
.../release_action.tsx | 10 +-
.../suspend_process_action.tsx | 5 +-
.../upload_action.tsx | 5 +-
...security_solution_ebt_kibana_server.ndjson | 4 +-
.../endpoint/endpoint_app_context_services.ts | 9 +
.../server/endpoint/mocks/mocks.ts | 3 +
.../routes/actions/response_actions.ts | 114 ++++----
.../lib/base_response_actions_client.test.ts | 98 +++++++
.../lib/base_response_actions_client.ts | 61 ++++
.../sentinel_one_actions_client.test.ts | 275 +++++++++++++++++-
.../sentinel_one_actions_client.ts | 17 +-
.../lib/telemetry/event_based/events.ts | 134 +++++++++
.../security_solution/server/plugin.ts | 1 +
19 files changed, 668 insertions(+), 109 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 0122eccb2e7ce9..88ca097e09b18a 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -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
*/
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/execute_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/execute_action.tsx
index 7e19a03860013a..ba93729c68d2c6 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/execute_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/execute_action.tsx
@@ -32,12 +32,13 @@ export const ExecuteActionResult = memo<
>(({ command, setStore, store, status, setStatus, ResultComponent }) => {
const actionCreator = useSendExecuteEndpoint();
const actionRequestBody = useMemo(() => {
- 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,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx
index 90e44c4a56ee2a..8b45e1dee9dac1 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx
@@ -23,9 +23,8 @@ export const GetFileActionResult = memo<
const actionCreator = useSendGetFileRequest();
const actionRequestBody = useMemo(() => {
- 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({
ResultComponent,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx
index 52aaf0675e2df5..7e2a5bf6222338 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx
@@ -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',
});
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/isolate_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/isolate_action.tsx
index d4755a204482b6..325a84bc70bf0f 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/isolate_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/isolate_action.tsx
@@ -19,9 +19,8 @@ export const IsolateActionResult = memo(
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(
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,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx
index e96f1f0028b7d6..3f781d54240ffa 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx
@@ -18,8 +18,7 @@ export const KillProcessActionResult = memo<
const actionCreator = useSendKillProcessRequest();
const actionRequestBody = useMemo(() => {
- 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({
ResultComponent,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/release_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/release_action.tsx
index 5f250ea6564140..52caba57af8623 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/release_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/release_action.tsx
@@ -19,9 +19,8 @@ export const ReleaseActionResult = memo(
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(
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,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx
index b2a73b426aa27d..d685e306e7b0df 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx
@@ -23,19 +23,20 @@ export const SuspendProcessActionResult = memo<
const actionCreator = useSendSuspendProcessRequest();
const actionRequestBody = useMemo(() => {
- 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({
ResultComponent,
diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/upload_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/upload_action.tsx
index 5f27cb285ed17c..b656f7aade6eb5 100644
--- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/upload_action.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/upload_action.tsx
@@ -29,7 +29,7 @@ export const UploadActionResult = memo<
const actionCreator = useSendUploadEndpointRequest();
const actionRequestBody = useMemo(() => {
- 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,
diff --git a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_server.ndjson b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_server.ndjson
index 9b69cdc5a8493d..2d89364a0449be 100644
--- a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_server.ndjson
+++ b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_server.ndjson
@@ -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":[]}
\ No newline at end of file
+{"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":[]}
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
index d7181b3ce49c60..c18fce53146066 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
@@ -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;
+ }
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts
index 141a5ebb440f6d..7807dd44bddbde 100644
--- a/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/mocks/mocks.ts
@@ -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(),
};
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts
index 16c3ea8ffd427f..b6eb2376bd1cba 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts
@@ -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 {
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 {
+ 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<
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts
index 67633e3badcc97..20389d41f39564 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts
@@ -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()', () => {
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts
index 07ab63b77a312f..0411e4a9c8f653 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.ts
@@ -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
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
index c3adf944bc024a..b31e70b37e5607 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.test.ts
@@ -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>;
+
+ beforeEach(() => {
+ const s1DataGenerator = new SentinelOneDataGenerator('seed');
+ const actionRequestsSearchResponse = s1DataGenerator.toEsSearchResponse([
+ s1DataGenerator.generateActionEsHit({
+ 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
+ >;
+
+ 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({
+ 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;
+
+ 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()', () => {
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
index ba017ea9db1c9b..b35fa0fa455e92 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/sentinelone/sentinel_one_actions_client.ts
@@ -824,7 +824,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
return actionDetails;
}
- public async runningProcesses(
+ async runningProcesses(
actionRequest: GetProcessesRequestBody,
options?: CommonResponseActionMethodOptions
): Promise> {
@@ -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;
}
}
diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts b/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts
index e367f012ad0bca..b8a2df85f10adf 100644
--- a/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts
+++ b/x-pack/plugins/security_solution/server/lib/telemetry/event_based/events.ts
@@ -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,
];
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 0f442564d44873..e189f1c71a7837 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -238,6 +238,7 @@ export class Plugin implements ISecuritySolutionPlugin {
securitySolutionRequestContextFactory: requestContextFactory,
cloud: plugins.cloud,
loggerFactory: this.pluginContext.logger,
+ telemetry: core.analytics,
});
initUsageCollectors({