Skip to content

Commit

Permalink
[Security Solution][Endpoint] Change SentinelOne response actions to …
Browse files Browse the repository at this point in the history
…use `agent.id` instead of `observer.serial_number` (elastic#189535)

## Summary

### Security Solution impacts

PR updates the SentinelOne response actions to:

- use `sentinel_one.[data_type].agent.id` field to identify the host ID
- With this change, our uses are no longer restricted to creating SIEM
alerts only from the `logs-sentinel_one.alert*` index
    - Indexes that currently include the `*.agent.id` field:
        - `logs-sentinel_one.alert*`
        - `logs-sentinel_one.threat*`
        - `logs-sentinel_one.activity*`
        - `logs-sentinel_one.agent*`
        - ❗  IMPORTANT ❗ : 
- Environments with a SIEM rule that looks for
`observable.serial_number` field _(the field used prior to this PR to
identify the agent id in the SentinelOne document)_ should update the
rule to use one of the new fields (see screen capture below)
- The following impacts were identified during testing for existing
deployments that may already be using the SentinelOne bi-directional
response actions (currently in Tech. Preview):
1. User will no longer be able to download the output from a previous
`get-file` command (this was just release 2 weeks ago to serverless).
2. After an upgrade, if a user opens the console and clicks on the
"Response actions history" button to display the host's response
actions, they will **not** see the response actions in the list that
were submitted prior to the upgrade. Those, however, will still be
displayed in the (global) Response Actions History Log page.
- Dev script was updated to create a SIEM rule that looks at both
`*.alert*` and `*.threat*` indexes
- Fixed the output for `processes` for SentinelOne to NOT display a Zip
file passcode for the download (not needed)
- Fixed bug that prevented the Host's OS platform icon (linux, windows,
macos) from being displayed in the console.



### Connector impacts

- SentinelOne connector sub-actions were updated to take in `agentId` as
an argument instead of `agentUUID`
  • Loading branch information
paul-tavares authored Aug 12, 2024
1 parent 017a9fd commit 9aa3910
Show file tree
Hide file tree
Showing 37 changed files with 630 additions and 345 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,7 @@ export const ENDPOINT_SEARCH_STRATEGY = 'endpointSearchStrategy';

/** Search strategy keys */
export const ENDPOINT_PACKAGE_POLICIES_STATS_STRATEGY = 'endpointPackagePoliciesStatsStrategy';

/** The list of OS types that support. Value usually found in ECS `host.os.type` */
export const SUPPORTED_HOST_OS_TYPE = Object.freeze(['macos', 'windows', 'linux'] as const);
export type SupportedHostOsType = (typeof SUPPORTED_HOST_OS_TYPE)[number];
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,30 @@ export const RESPONSE_ACTIONS_ZIP_PASSCODE: Readonly<Record<ResponseActionAgentT
});

/**
* Map of Agent Type to alert field that holds the Agent ID for that agent type
* Map of Agent Type to alert fields that holds the Agent ID for that agent type.
* Multiple alert fields are supported since different data sources define the agent
* id in different paths.
*
* NOTE: there are utilities in `x-pack/plugins/security_solution/public/common/lib/endpoint/utils`
* that facilitate working with alert (ECS) fields to determine if the give event/alert supports
* response actions, including:
* - `getAgentTypeForAgentIdField()`
* - `getEventDetailsAgentIdField()`
* - `isResponseActionsAlertAgentIdField()`
*/
export const RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD: Readonly<
Record<ResponseActionAgentType, string>
export const RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS: Readonly<
Record<ResponseActionAgentType, string[]>
> = Object.freeze({
endpoint: 'agent.id',
sentinel_one: 'observer.serial_number',
crowdstrike: 'device.id',
endpoint: ['agent.id'],
sentinel_one: [
'sentinel_one.alert.agent.id',
'sentinel_one.threat.agent.id',
'sentinel_one.activity.agent.id',
'sentinel_one.agent.agent.id',
],
crowdstrike: ['device.id'],
});

export const SUPPORTED_AGENT_ID_ALERT_FIELDS: Readonly<string[]> = Object.values(
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS
).flat();
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { createHttpFetchError } from '@kbn/core-http-browser-mocks';
import { HostStatus } from '../../../../../../common/endpoint/types';
import {
RESPONSE_ACTION_AGENT_TYPE,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS,
} from '../../../../../../common/endpoint/service/response_actions/constants';
import { getAgentTypeName } from '../../../../translations';
import { ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD } from '../../../../hooks/endpoint/use_alert_response_actions_support';
Expand Down Expand Up @@ -111,10 +111,11 @@ describe('use responder action data hooks', () => {
it.each([...RESPONSE_ACTION_AGENT_TYPE])(
'should show action disabled with tooltip for %s if agent id field is missing',
(agentType) => {
const agentTypeField = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(
agentType,
{
[RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD[agentType]]: undefined,
[agentTypeField]: undefined,
}
);

Expand All @@ -123,7 +124,7 @@ describe('use responder action data hooks', () => {
isDisabled: true,
tooltip: ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD(
getAgentTypeName(agentType),
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD[agentType]
agentTypeField
),
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { find, isEmpty, uniqBy } from 'lodash/fp';
import { ALERT_RULE_PARAMETERS, ALERT_RULE_TYPE } from '@kbn/rule-data-utils';

import { EventCode, EventCategory } from '@kbn/securitysolution-ecs';
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../common/endpoint/service/response_actions/constants';
import { SUPPORTED_AGENT_ID_ALERT_FIELDS } from '../../../../common/endpoint/service/response_actions/constants';
import { isResponseActionsAlertAgentIdField } from '../../lib/endpoint';
import { useAlertResponseActionsSupport } from '../../hooks/endpoint/use_alert_response_actions_support';
import * as i18n from './translations';
Expand Down Expand Up @@ -45,18 +45,17 @@ const THRESHOLD_COUNT = `${ALERT_THRESHOLD_RESULT}.count`;
/** Always show these fields */
const alwaysDisplayedFields: EventSummaryField[] = [
{ id: 'host.name' },
// ENDPOINT-related field //
{ id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS },
{
id: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one,
overrideField: AGENT_STATUS_FIELD_NAME,
label: i18n.AGENT_STATUS,
},
{
id: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike,
overrideField: AGENT_STATUS_FIELD_NAME,
label: i18n.AGENT_STATUS,
},

// Add all fields used to identify the agent ID in alert events and override them to
// show the `agent.status` field name/value
...SUPPORTED_AGENT_ID_ALERT_FIELDS.map((fieldPath) => {
return {
id: fieldPath,
overrideField: AGENT_STATUS_FIELD_NAME,
label: i18n.AGENT_STATUS,
};
}),

// ** //
{ id: 'user.name' },
{ id: 'rule.name' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
AGENT_STATUS_FIELD_NAME,
QUARANTINED_PATH_FIELD_NAME,
} from '../../../timelines/components/timeline/body/renderers/constants';
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../common/endpoint/service/response_actions/constants';

/**
* An item rendered in the table
Expand Down Expand Up @@ -169,8 +168,6 @@ export function getEnrichedFieldInfo({
export const FIELDS_WITHOUT_ACTIONS: { [field: string]: boolean } = {
[AGENT_STATUS_FIELD_NAME]: true,
[QUARANTINED_PATH_FIELD_NAME]: true,
[RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one]: true,
[RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike]: true,
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { AlertResponseActionsSupport } from '../use_alert_response_actions_support';
import {
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS,
} from '../../../../../common/endpoint/service/response_actions/constants';

const useAlertResponseActionsSupportMock = (): AlertResponseActionsSupport => {
Expand All @@ -19,7 +19,7 @@ const useAlertResponseActionsSupportMock = (): AlertResponseActionsSupport => {
details: {
agentId: '123',
agentType: 'endpoint',
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.endpoint,
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.endpoint[0],
hostName: 'host-a',
platform: 'linux',
agentSupport: RESPONSE_ACTION_API_COMMANDS_NAMES.reduce<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ResponseActionAgentType } from '../../../../common/endpoint/servic
import {
RESPONSE_ACTION_AGENT_TYPE,
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS,
} from '../../../../common/endpoint/service/response_actions/constants';
import type { AlertResponseActionsSupport } from './use_alert_response_actions_support';
import {
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
unsupportedReason: undefined,
details: {
agentId: 'abfe4a35-d5b4-42a0-a539-bd054c791769',
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD[agentType],
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0],
agentSupport: RESPONSE_ACTION_API_COMMANDS_NAMES.reduce((acc, commandName) => {
acc[commandName] = options.noAgentSupport
? false
Expand Down Expand Up @@ -121,15 +121,15 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
unsupportedReason: RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS,
details: {
agentType: 'sentinel_one',
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one,
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0],
},
})
);
});

it('should set `isSupported` to `false` if unable to get agent id', () => {
alertDetailItemData = endpointAlertDataMock.generateEndpointAlertDetailsItemData({
[RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.endpoint]: undefined,
[RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.endpoint[0]]: undefined,
});

expect(renderHook().result.current).toEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { useMemo } from 'react';
import { find, some } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { getEventDetailsAgentIdField } from '../../lib/endpoint/utils/get_event_details_agent_id_field';
import { getHostPlatform } from '../../lib/endpoint/utils/get_host_platform';
import { getAlertDetailsFieldValue } from '../../lib/endpoint/utils/get_event_details_field_values';
import { isAgentTypeAndActionSupported } from '../../lib/endpoint';
import type {
ResponseActionAgentType,
ResponseActionsApiCommandNames,
} from '../../../../common/endpoint/service/response_actions/constants';
import {
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD,
} from '../../../../common/endpoint/service/response_actions/constants';
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../common/endpoint/service/response_actions/constants';
import { getAgentTypeName } from '../../translations';

export const ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD = (
Expand Down Expand Up @@ -115,44 +113,23 @@ export const useAlertResponseActionsSupport = (
return agentType ? isAgentTypeAndActionSupported(agentType) : false;
}, [agentType]);

const agentId: string = useMemo(() => {
if (!agentType) {
return '';
}

if (agentType === 'endpoint') {
return getAlertDetailsFieldValue({ category: 'agent', field: 'agent.id' }, eventData);
}

if (agentType === 'sentinel_one') {
return getAlertDetailsFieldValue(
{ category: 'observer', field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one },
eventData
);
}
const { agentIdField, agentId } = useMemo<{ agentIdField: string; agentId: string }>(() => {
let field = '';
let id = '';

if (agentType === 'crowdstrike') {
return getAlertDetailsFieldValue(
{ category: 'device', field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike },
eventData
);
if (agentType) {
const eventAgentIdInfo = getEventDetailsAgentIdField(agentType, eventData);
field = eventAgentIdInfo.field;
id = eventAgentIdInfo.agentId;
}

return '';
return { agentId: id, agentIdField: field };
}, [agentType, eventData]);

const doesHostSupportResponseActions = useMemo(() => {
return Boolean(isFeatureEnabled && isAlert && agentId && agentType);
}, [agentId, agentType, isAlert, isFeatureEnabled]);

const agentIdField = useMemo(() => {
if (agentType) {
return RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD[agentType];
}

return '';
}, [agentType]);

const supportedActions = useMemo(() => {
return RESPONSE_ACTION_API_COMMANDS_NAMES.reduce<AlertAgentActionsSupported>(
(acc, responseActionName) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { getAgentTypeForAgentIdField } from './get_agent_type_for_agent_id_field';
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS } from '../../../../../common/endpoint/service/response_actions/constants';

describe('getAgentTypeForAgentIdField()', () => {
it('should return default agent type (endpoint) when field is unknown', () => {
expect(getAgentTypeForAgentIdField('foo.bar')).toEqual('endpoint');
});

// A flat map of `Array<[agentType, field]>`
const testConditions = Object.entries(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS)
.map(([agentType, fields]) => {
return fields.map((field) => [agentType, field]);
})
.flat();

it.each(testConditions)('should return `%s` for field `%s`', (agentType, field) => {
expect(getAgentTypeForAgentIdField(field)).toEqual(agentType);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS } from '../../../../../common/endpoint/service/response_actions/constants';

/**
* Checks the provided `agentIdEcsField` path provided to see if it is being used by one
* of the agent types that supports response actions and returns that agent type.
* Defaults to `endpoint` if no match is found
* @param agentIdEcsField
*/
export const getAgentTypeForAgentIdField = (agentIdEcsField: string): ResponseActionAgentType => {
for (const [fieldAgentType, fieldValues] of Object.entries(
RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS
)) {
if (fieldValues.includes(agentIdEcsField)) {
return fieldAgentType as ResponseActionAgentType;
}
}
return 'endpoint';
};
Loading

0 comments on commit 9aa3910

Please sign in to comment.