[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:
Ash 2024-09-27 10:06:31 +02:00 committed by GitHub
parent 2d9f13c41f
commit a80335e378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 668 additions and 109 deletions

View file

@ -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
*/

View file

@ -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,

View file

@ -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,

View file

@ -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',
});

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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":[]}

View file

@ -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;
}
}

View file

@ -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(),
};
};

View file

@ -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<

View file

@ -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()', () => {

View file

@ -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

View file

@ -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()', () => {

View file

@ -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;
}
}

View file

@ -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,
];

View file

@ -238,6 +238,7 @@ export class Plugin implements ISecuritySolutionPlugin {
securitySolutionRequestContextFactory: requestContextFactory,
cloud: plugins.cloud,
loggerFactory: this.pluginContext.logger,
telemetry: core.analytics,
});
initUsageCollectors({