diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 53d79f80d697b..d534170f74d0f 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -8326,7 +8326,7 @@ Object { "presence": "optional", }, "keys": Object { - "agentUUID": Object { + "agentId": Object { "flags": Object { "error": [Function], }, @@ -8450,7 +8450,7 @@ Object { ], "type": "string", }, - "agentUUID": Object { + "agentId": Object { "flags": Object { "error": [Function], }, diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 3a10093b20a5b..276d2fa32da76 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -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]; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts index ae28eca848403..3f06fbd4e4ffc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/constants.ts @@ -176,12 +176,30 @@ export const RESPONSE_ACTIONS_ZIP_PASSCODE: Readonly +export const RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS: Readonly< + Record > = 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 = Object.values( + RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS +).flat(); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts index 88383ecb7eff4..3b68c0efdf9e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts @@ -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'; @@ -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, } ); @@ -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 ), }) ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index f632e48301b67..357e1230ebee5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -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'; @@ -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' }, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 86bdbb3297530..6b3bd565f0eef 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -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 @@ -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, }; /** diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/__mocks__/use_alert_response_actions_support.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/__mocks__/use_alert_response_actions_support.ts index dcced934f3bc9..ac0c212dc8ca0 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/__mocks__/use_alert_response_actions_support.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/__mocks__/use_alert_response_actions_support.ts @@ -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 => { @@ -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< diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.test.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.test.ts index b5a07b34c65bb..451e1454f1ed0 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.test.ts @@ -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 { @@ -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 @@ -121,7 +121,7 @@ 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], }, }) ); @@ -129,7 +129,7 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => { 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( diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts index a483a5c465b3f..dc7835191428d 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_alert_response_actions_support.ts @@ -9,6 +9,7 @@ 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'; @@ -16,10 +17,7 @@ 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 = ( @@ -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( (acc, responseActionName) => { diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.test.ts new file mode 100644 index 0000000000000..4e33bed9ee789 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.ts new file mode 100644 index 0000000000000..7d1e77fb50180 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_agent_type_for_agent_id_field.ts @@ -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'; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.test.ts new file mode 100644 index 0000000000000..bd34bc529b202 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { endpointAlertDataMock } from '../../../mock/endpoint'; +import { + RESPONSE_ACTION_AGENT_TYPE, + RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS, +} from '../../../../../common/endpoint/service/response_actions/constants'; +import { getEventDetailsAgentIdField, parseEcsFieldPath } from '..'; + +describe('getEventDetailsAgentIdField()', () => { + it.each(RESPONSE_ACTION_AGENT_TYPE)(`should return agent id info for %s`, (agentType) => { + const field = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0]; + const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType); + + expect(getEventDetailsAgentIdField(agentType, eventDetails)).toEqual({ + found: true, + category: parseEcsFieldPath(field).category, + field, + agentId: 'abfe4a35-d5b4-42a0-a539-bd054c791769', + }); + }); + + it.each(RESPONSE_ACTION_AGENT_TYPE)( + 'should include a field when agent id is not found: %s', + (agentType) => { + const field = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0]; + const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType( + agentType, + { + 'event.dataset': { values: ['foo'], originalValue: ['foo'] }, + [field]: undefined, + } + ); + + expect(getEventDetailsAgentIdField(agentType, eventDetails)).toEqual({ + found: false, + category: parseEcsFieldPath(field).category, + field, + agentId: '', + }); + } + ); + + it.each(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one)( + 'should return field [%s] for sentinelone when agent is not found and event.dataset matches', + (field) => { + const dataset = field.split('.').slice(0, 2).join('.'); + const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType( + 'sentinel_one', + { + 'event.dataset': { values: [dataset], originalValue: [dataset] }, + // Make sure we remove the default agentId field from the mock data + [RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0]]: undefined, + } + ); + + expect(getEventDetailsAgentIdField('sentinel_one', eventDetails)).toEqual({ + found: false, + category: parseEcsFieldPath(field).category, + agentId: '', + field, + }); + } + ); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.ts new file mode 100644 index 0000000000000..094a8d267f131 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_event_details_agent_id_field.ts @@ -0,0 +1,85 @@ +/* + * 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 { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { parseEcsFieldPath } from './parse_ecs_field_path'; +import { getAlertDetailsFieldValue } from './get_event_details_field_values'; +import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS } from '../../../../../common/endpoint/service/response_actions/constants'; + +export interface EventDetailsAgentIdField { + found: boolean; + category: string; + field: string; + agentId: string; +} + +/** + * Returns the Agent ID and associated field from an alert Event Details data that should be used + * for executing response actions + */ +export const getEventDetailsAgentIdField = ( + agentType: ResponseActionAgentType, + eventData: TimelineEventsDetailsItem[] | null = [] +): EventDetailsAgentIdField => { + const result: EventDetailsAgentIdField = { + found: false, + category: '', + field: '', + agentId: '', + }; + + const fieldList: string[] = [...RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType]]; + + fieldList.some((fieldPath) => { + const { field, category } = parseEcsFieldPath(fieldPath); + const agentId = getAlertDetailsFieldValue({ category, field }, eventData); + + if (agentId) { + result.found = true; + result.category = category; + result.field = field; + result.agentId = agentId; + + return true; + } + + return false; + }); + + // ensure a `field` is always returned since we know the `agentType`. The field is sometime used + // to show the user what might have been missing in the data that prevented it from displaying + // response actions options. + if (!result.found) { + const eventDataset = getAlertDetailsFieldValue( + { category: 'event', field: 'event.dataset' }, + eventData + ).toLowerCase(); + + // Let's try to get the event field that might be used for the given source of the alert. + // The `event.dataset` seems to contain the same pattern as the one used to store data + // by the integrations in the ES document - example: `sentinel_one.alert` or `sentinel_one.threat`. + // So we'll use this to see if the field is defined for this datasource. + if (eventDataset) { + for (const field of fieldList) { + if (field.toLowerCase().startsWith(eventDataset)) { + result.field = field; + result.category = parseEcsFieldPath(field).category; + break; + } + } + } + + // Fallback: just set it to the first field defined for the agentType + if (!result.field) { + result.field = fieldList[0]; + result.category = parseEcsFieldPath(result.field).category; + } + } + + return result; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts index 61cc2053eb8fc..1459c690068b4 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts @@ -37,15 +37,15 @@ describe('getHostPlatform() util', () => { }; it.each` - title | setupData | expectedResult - ${'ECS data with host.os.platform info'} | ${buildEcsData({ platform: 'windows' })} | ${'windows'} - ${'ECS data with host.os.type info'} | ${buildEcsData({ type: 'Linux' })} | ${'linux'} - ${'ECS data with host.os.name info'} | ${buildEcsData({ name: 'MACOS' })} | ${'macos'} - ${'ECS data with all os info'} | ${buildEcsData({ platform: 'macos', type: 'windows', name: 'linux' })} | ${'macos'} - ${'Event Details data with host.os.platform info'} | ${buildEventDetails({ platform: 'windows' })} | ${'windows'} - ${'Event Details data with host.os.type info'} | ${buildEventDetails({ type: 'Linux' })} | ${'linux'} - ${'Event Details data with host.os.name info'} | ${buildEventDetails({ name: 'MACOS' })} | ${'macos'} - ${'Event Details data with all os info'} | ${buildEventDetails({ platform: 'macos', type: 'windows', name: 'linux' })} | ${'macos'} + title | setupData | expectedResult + ${'ECS data with host.os.platform info'} | ${buildEcsData({ platform: 'windows' })} | ${'windows'} + ${'ECS data with host.os.type info'} | ${buildEcsData({ type: 'Linux' })} | ${'linux'} + ${'ECS data with host.os.name info'} | ${buildEcsData({ name: 'MACOS' })} | ${'macos'} + ${'ECS data with all os info'} | ${buildEcsData({ platform: 'macos', type: 'windows', name: 'linux' })} | ${'windows'} + ${'Event Details data with host.os.platform info'} | ${buildEventDetails({ platform: 'windows' })} | ${'windows'} + ${'Event Details data with host.os.type info'} | ${buildEventDetails({ type: 'Linux' })} | ${'linux'} + ${'Event Details data with host.os.name info'} | ${buildEventDetails({ name: 'MACOS' })} | ${'macos'} + ${'Event Details data with all os info'} | ${buildEventDetails({ platform: 'macos', type: 'win2', name: 'linux' })} | ${'linux'} `(`should handle $title`, ({ setupData, expectedResult }) => { expect(getHostPlatform(setupData)).toEqual(expectedResult); }); diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts index 52df785cabff0..fa17162939c0e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.ts @@ -7,9 +7,13 @@ import type { Ecs } from '@elastic/ecs'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { get } from 'lodash'; +import { parseEcsFieldPath } from '..'; +import type { SupportedHostOsType } from '../../../../../common/endpoint/constants'; import type { MaybeImmutable } from '../../../../../common/endpoint/types'; import { getAlertDetailsFieldValue } from './get_event_details_field_values'; import type { Platform } from '../../../../management/components/endpoint_responder/components/header_info/platforms'; +import { SUPPORTED_HOST_OS_TYPE } from '../../../../../common/endpoint/constants'; type EcsHostData = MaybeImmutable>; @@ -19,21 +23,35 @@ const isTimelineEventDetailsItems = ( return Array.isArray(data); }; +// The list of ECS fields we check to try and determine the OS type +const ECS_OS_TYPE_FIELDS = Object.freeze(['host.os.type', 'host.os.name', 'host.os.platform']); + /** * Retrieve a host's platform type from either ECS data or Event Details list of items * @param data */ -export const getHostPlatform = (data: EcsHostData | TimelineEventsDetailsItem[]): Platform => { +export const getHostPlatform = ( + data: EcsHostData | TimelineEventsDetailsItem[] +): SupportedHostOsType => { let platform = ''; - if (isTimelineEventDetailsItems(data)) { - platform = (getAlertDetailsFieldValue({ category: 'host', field: 'host.os.platform' }, data) || - getAlertDetailsFieldValue({ category: 'host', field: 'host.os.type' }, data) || - getAlertDetailsFieldValue({ category: 'host', field: 'host.os.name' }, data)) as Platform; - } else { - platform = - ((data.host?.os?.platform || data.host?.os?.type || data.host?.os?.name) as Platform) || ''; + for (const field of ECS_OS_TYPE_FIELDS) { + let fieldValue = ''; + + if (isTimelineEventDetailsItems(data)) { + fieldValue = getAlertDetailsFieldValue( + { category: parseEcsFieldPath(field).category, field }, + data + ).toLowerCase(); + } else { + fieldValue = get(data, field, '').toLowerCase(); + } + + if (SUPPORTED_HOST_OS_TYPE.includes(fieldValue as Platform)) { + platform = fieldValue; + break; + } } - return platform.toLowerCase() as Platform; + return platform as Platform; }; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/index.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/index.ts index b5e173e2c360f..8854a4833b355 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/index.ts @@ -7,3 +7,7 @@ export * from './is_agent_type_and_action_supported'; export * from './is_response_actions_alert_agent_id_field'; +export * from './parse_ecs_field_path'; +export * from './get_event_details_agent_id_field'; +export * from './get_event_details_field_values'; +export * from './get_host_platform'; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/is_response_actions_alert_agent_id_field.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/is_response_actions_alert_agent_id_field.ts index 88b940ba3b748..48eda055d2fa2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/is_response_actions_alert_agent_id_field.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/is_response_actions_alert_agent_id_field.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../../common/endpoint/service/response_actions/constants'; - -const SUPPORTED_ALERT_FIELDS: Readonly = Object.values( - RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD -); +import { SUPPORTED_AGENT_ID_ALERT_FIELDS } from '../../../../../common/endpoint/service/response_actions/constants'; /** * Checks to see if a given alert field (ex. `agent.id`) is used by Agents that have support for response actions. */ export const isResponseActionsAlertAgentIdField = (field: string): boolean => { - return SUPPORTED_ALERT_FIELDS.includes(field); + return SUPPORTED_AGENT_ID_ALERT_FIELDS.includes(field); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/parse_ecs_field_path.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/parse_ecs_field_path.ts new file mode 100644 index 0000000000000..f83c6a5ce10bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/parse_ecs_field_path.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Parses a ECS field path (ex. `host.os.name`) into an object that contains the `category` and + * `field` value. Good for using when wanting to search for items in `TimelineEventsDetailsItem` + * @param field + */ +export const parseEcsFieldPath = (field: string): { category: string; field: string } => { + const result = { category: '', field }; + + if (field.includes('.')) { + result.category = field.substring(0, field.indexOf('.')); + } + + return result; +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/endpoint_alert_data_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/endpoint_alert_data_mock.ts index 98b94d05353ca..0ae9777c398c7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/endpoint_alert_data_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/endpoint_alert_data_mock.ts @@ -6,8 +6,9 @@ */ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { parseEcsFieldPath } from '../../lib/endpoint'; import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; -import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS } from '../../../../common/endpoint/service/response_actions/constants'; /** * Provide overrides for data `fields`. If a field is set to `undefined`, then it will be removed @@ -89,8 +90,8 @@ const generateEndpointAlertDetailsItemDataMock = ( isObjectArray: false, }, { - category: 'agent', - field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.endpoint, + category: parseEcsFieldPath(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.endpoint[0]).category, + field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.endpoint[0], values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], isObjectArray: false, @@ -102,6 +103,13 @@ const generateEndpointAlertDetailsItemDataMock = ( originalValue: ['endpoint'], isObjectArray: false, }, + { + category: 'event', + field: 'event.dataset', + values: ['endpoint'], + originalValue: ['endpoint'], + isObjectArray: false, + }, { category: 'event', field: 'event.category', @@ -150,6 +158,11 @@ const generateSentinelOneAlertDetailsItemDataMock = ( itemData.originalValue = ['sentinel_one']; break; + case 'event.dataset': + itemData.values = ['sentinel_one.alert']; + itemData.originalValue = ['sentinel_one.alert']; + break; + case 'agent.type': itemData.values = ['filebeat']; itemData.originalValue = ['filebeat']; @@ -158,8 +171,8 @@ const generateSentinelOneAlertDetailsItemDataMock = ( }); data.push({ - category: 'observer', - field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one, + category: parseEcsFieldPath(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0]).category, + field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0], values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], isObjectArray: false, @@ -183,6 +196,11 @@ const generateCrowdStrikeAlertDetailsItemDataMock = ( itemData.originalValue = ['crowdstrike']; break; + case 'event.dataset': + itemData.values = ['crowdstrike.alert']; + itemData.originalValue = ['crowdstrike.alert']; + break; + case 'agent.type': itemData.values = ['filebeat']; itemData.originalValue = ['filebeat']; @@ -192,8 +210,8 @@ const generateCrowdStrikeAlertDetailsItemDataMock = ( data.push( { - category: 'device', - field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike, + category: parseEcsFieldPath(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.crowdstrike[0]).category, + field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.crowdstrike[0], values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], isObjectArray: false, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx index 574aa6a6c1970..ab549388be214 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx @@ -49,7 +49,7 @@ import { ALERT_ORIGINAL_EVENT_MODULE, } from '../../../../common/field_maps/field_names'; import { AGENT_ID } from './highlighted_fields_config'; -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'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), @@ -1797,21 +1797,14 @@ describe('Exception helpers', () => { { id: 'host.name', }, - { - id: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.endpoint, - label: 'Agent status', - overrideField: 'agent.status', - }, - { - id: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one, - overrideField: 'agent.status', - label: 'Agent status', - }, - { - id: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike, - overrideField: 'agent.status', - label: 'Agent status', - }, + // Fields used in support of Response Actions + ...SUPPORTED_AGENT_ID_ALERT_FIELDS.map((fieldPath) => { + return { + id: fieldPath, + overrideField: 'agent.status', + label: 'Agent status', + }; + }), { id: 'user.name', }, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx index 8ff457ba6ee22..c23ebe3b89c3d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx @@ -9,6 +9,7 @@ import type { VFC } from 'react'; import React, { useCallback, useMemo } from 'react'; import { EuiFlexItem, EuiLink } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { getAgentTypeForAgentIdField } from '../../../../common/lib/endpoint/utils/get_agent_type_for_agent_id_field'; import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status'; import { useDocumentDetailsContext } from '../../shared/context'; @@ -28,7 +29,6 @@ import { HIGHLIGHTED_FIELDS_CELL_TEST_ID, HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID, } from './test_ids'; -import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../../common/endpoint/service/response_actions/constants'; import { HostPreviewPanelKey } from '../../../entity_details/host_right'; import { HOST_PREVIEW_BANNER } from './host_entity_overview'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; @@ -135,25 +135,11 @@ export interface HighlightedFieldsCellProps { export const HighlightedFieldsCell: VFC = ({ values, field, - originalField, + originalField = '', }) => { - const isSentinelOneAgentIdField = useMemo( - () => originalField === RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one, - [originalField] - ); - const isCrowdstrikeAgentIdField = useMemo( - () => originalField === RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike, - [originalField] - ); const agentType: ResponseActionAgentType = useMemo(() => { - if (isSentinelOneAgentIdField) { - return 'sentinel_one'; - } - if (isCrowdstrikeAgentIdField) { - return 'crowdstrike'; - } - return 'endpoint'; - }, [isCrowdstrikeAgentIdField, isSentinelOneAgentIdField]); + return getAgentTypeForAgentIdField(originalField); + }, [originalField]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx index 28a7277fa5318..2c56b3d67d82f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx @@ -12,7 +12,8 @@ import { mockDataFormattedForFieldBrowserWithOverridenField, } from '../mocks/mock_data_formatted_for_field_browser'; import { useHighlightedFields } from './use_highlighted_fields'; -import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS } from '../../../../../common/endpoint/service/response_actions/constants'; +import { parseEcsFieldPath } from '../../../../common/lib/endpoint'; jest.mock('../../../../common/experimental_features_service'); @@ -105,13 +106,17 @@ describe('useHighlightedFields', () => { const hookResult = renderHook(() => useHighlightedFields({ dataFormattedForFieldBrowser: dataFormattedForFieldBrowser.concat({ - category: 'observer', - field: `observer.${RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one}`, + category: parseEcsFieldPath(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0]) + .category, + field: `observer.${RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0]}`, values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], originalValue: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], isObjectArray: false, }), - investigationFields: ['agent.status', 'observer.serial_number'], + investigationFields: [ + 'agent.status', + RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0], + ], }) ); @@ -126,8 +131,9 @@ describe('useHighlightedFields', () => { const hookResult = renderHook(() => useHighlightedFields({ dataFormattedForFieldBrowser: dataFormattedForFieldBrowser.concat({ - category: 'device', - field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.crowdstrike, + category: parseEcsFieldPath(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.crowdstrike[0]) + .category, + field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.crowdstrike[0], values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], isObjectArray: false, @@ -143,38 +149,41 @@ describe('useHighlightedFields', () => { }); }); - it('should return sentinelone agent id field if data is s1 alert', () => { - const hookResult = renderHook(() => - useHighlightedFields({ - dataFormattedForFieldBrowser: dataFormattedForFieldBrowser.concat([ - { - category: 'event', - field: 'event.module', - values: ['sentinel_one'], - originalValue: ['sentinel_one'], - isObjectArray: false, - }, - { - category: 'observer', - field: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one, - values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], - originalValue: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], - isObjectArray: false, - }, - ]), - investigationFields: ['agent.status', 'observer.serial_number'], - }) - ); - - expect(hookResult.result.current).toEqual({ - 'kibana.alert.rule.type': { - values: ['query'], - }, - 'observer.serial_number': { - values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], - }, - }); - }); + it.each(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one)( + 'should return sentinelone agent id field: %s', + (agentIdField) => { + const hookResult = renderHook(() => + useHighlightedFields({ + dataFormattedForFieldBrowser: dataFormattedForFieldBrowser.concat([ + { + category: 'event', + field: 'event.module', + values: ['sentinel_one'], + originalValue: ['sentinel_one'], + isObjectArray: false, + }, + { + category: parseEcsFieldPath(agentIdField).category, + field: agentIdField, + values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], + originalValue: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], + isObjectArray: false, + }, + ]), + investigationFields: ['agent.status', agentIdField], + }) + ); + + expect(hookResult.result.current).toEqual({ + 'kibana.alert.rule.type': { + values: ['query'], + }, + [agentIdField]: { + values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], + }, + }); + } + ); it('should return crowdstrike agent id field if data is crowdstrike alert', () => { const hookResult = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_processes_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_processes_action.test.tsx index 9897319a24900..f14b1e2110811 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_processes_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_processes_action.test.tsx @@ -314,7 +314,7 @@ describe('When using processes action from response actions console', () => { await waitFor(() => { expect(renderResult.getByTestId('getProcessesSuccessCallout').textContent).toEqual( - 'Click here to download(ZIP file passcode: Elastic@123).' + + 'Click here to download' + 'Files are periodically deleted to clear storage space. Download and save file locally if needed.' ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/platforms/platform_icon.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/platforms/platform_icon.tsx index 6aa9c0853f206..3997d2107596c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/platforms/platform_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_info/platforms/platform_icon.tsx @@ -7,11 +7,13 @@ import { EuiIcon, type EuiIconProps } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; +import type { SupportedHostOsType } from '../../../../../../../common/endpoint/constants'; import linuxSvg from './logos/linux.svg'; import windowsSvg from './logos/windows.svg'; import macosSvg from './logos/macos.svg'; -export type Platform = 'macos' | 'linux' | 'windows'; +export type Platform = SupportedHostOsType; + const getPlatformIcon = (platform: Platform): string | null => { switch (platform) { case 'macos': diff --git a/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.test.tsx b/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.test.tsx index 873e816e12dce..59701ae926076 100644 --- a/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.test.tsx @@ -199,4 +199,14 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => { expect(apiMocks.responseProvider.fileInfo).not.toHaveBeenCalled(); expect(renderResult.container.children.length).toBe(0); }); + + it('should not display the passcode text if `showPasscode` prop is `false`', async () => { + renderProps.showPasscode = false; + render(); + await waitFor(() => { + expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled(); + }); + + expect(renderResult.queryByTestId('test-passcodeMessage')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.tsx b/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.tsx index 0a552f3fe6cbc..140cabf935f31 100644 --- a/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.tsx +++ b/x-pack/plugins/security_solution/public/management/components/response_action_file_download_link/response_action_file_download_link.tsx @@ -105,6 +105,11 @@ export interface ResponseActionFileDownloadLinkProps { isTruncatedFile?: boolean; 'data-test-subj'?: string; textSize?: 's' | 'xs'; + /** + * If zip file needs a passcode to be opened. If `false`, then the passcode text will not be displayed. + * Default is `true` + */ + showPasscode?: boolean; } /** @@ -120,6 +125,7 @@ export const ResponseActionFileDownloadLink = memo { @@ -181,13 +187,15 @@ export const ResponseActionFileDownloadLink = memo {buttonTitle} - - {FILE_PASSCODE_INFO_MESSAGE(RESPONSE_ACTIONS_ZIP_PASSCODE[action.agentType])} - + {showPasscode && ( + + {FILE_PASSCODE_INFO_MESSAGE(RESPONSE_ACTIONS_ZIP_PASSCODE[action.agentType])} + + )} {FILE_DELETED_MESSAGE} diff --git a/x-pack/plugins/security_solution/public/management/components/running_processes_action_results/running_processes_action_results.tsx b/x-pack/plugins/security_solution/public/management/components/running_processes_action_results/running_processes_action_results.tsx index e35fe1fff1a08..425c674f75d59 100644 --- a/x-pack/plugins/security_solution/public/management/components/running_processes_action_results/running_processes_action_results.tsx +++ b/x-pack/plugins/security_solution/public/management/components/running_processes_action_results/running_processes_action_results.tsx @@ -140,7 +140,7 @@ const EndpointRunningProcessesResults = memo { return css({ '.accordion-host-name-button-content': { - 'font-size': 'inherit', + fontSize: 'inherit', }, }); }, []); @@ -211,6 +211,7 @@ const SentinelOneRunningProcessesResults = memo ) : ( @@ -227,6 +228,7 @@ const SentinelOneRunningProcessesResults = memo diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 8b6242fbdb205..299bb736ec4f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -13,7 +13,7 @@ import { isEmpty, isNumber } from 'lodash/fp'; import React from 'react'; import { css } from '@emotion/css'; import type { FieldSpec } from '@kbn/data-plugin/common'; - +import { getAgentTypeForAgentIdField } from '../../../../../common/lib/endpoint/utils/get_agent_type_for_agent_id_field'; import { ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, @@ -49,7 +49,6 @@ import { RuleStatus } from './rule_status'; import { HostName } from './host_name'; import { UserName } from './user_name'; import { AssetCriticalityLevel } from './asset_criticality_level'; -import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../../../common/endpoint/service/response_actions/constants'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -268,11 +267,6 @@ const FormattedFieldValueComponent: React.FC<{ iconSide={isButton ? 'right' : undefined} /> ); - } else if ( - fieldName === AGENT_STATUS_FIELD_NAME && - fieldFromBrowserField?.name === RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD.sentinel_one - ) { - return ; } else if (fieldName === ALERT_HOST_CRITICALITY || fieldName === ALERT_USER_CRITICALITY) { return ( ); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts index d508f9bc605bf..6e829256e3c1c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/common.ts @@ -274,11 +274,12 @@ export const createDetectionEngineSentinelOneRuleIfNeeded = async ( log: ToolingLog ): Promise => { const ruleName = 'Promote SentinelOne alerts'; - const sentinelOneAlertsIndexPattern = 'logs-sentinel_one.alert*'; - const ruleQueryValue = 'observer.serial_number:*'; + const tag = 'dev-script-run-sentinelone-host'; + const index = ['logs-sentinel_one.alert*', 'logs-sentinel_one.threat*']; + const ruleQueryValue = 'sentinel_one.alert.agent.id:* OR sentinel_one.threat.agent.id:*'; const { data } = await findRules(kbnClient, { - filter: `(alert.attributes.params.query: "${ruleQueryValue}" AND alert.attributes.params.index: ${sentinelOneAlertsIndexPattern})`, + filter: `alert.attributes.tags:("${tag}")`, }); if (data.length) { @@ -292,9 +293,12 @@ export const createDetectionEngineSentinelOneRuleIfNeeded = async ( log.info(`Creating new detection engine rule named [${ruleName}] for SentinelOne`); const createdRule = await createRule(kbnClient, { - index: [sentinelOneAlertsIndexPattern], + index, query: ruleQueryValue, from: 'now-3660s', + name: ruleName, + description: `Created by dev script located at: ${__filename}`, + tags: [tag], }); log.verbose(dump(createdRule)); 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 834267b5627f3..c3adf944bc024 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 @@ -110,7 +110,7 @@ describe('SentinelOneActionsClient class', () => { params: { subAction: 'isolateHost', subActionParams: { - uuid: '1-2-3', + ids: '1-2-3', }, }, }); @@ -244,7 +244,7 @@ describe('SentinelOneActionsClient class', () => { params: { subAction: 'releaseHost', subActionParams: { - uuid: '1-2-3', + ids: '1-2-3', }, }, }); @@ -935,7 +935,7 @@ describe('SentinelOneActionsClient class', () => { params: { subAction: SUB_ACTION.FETCH_AGENT_FILES, subActionParams: { - agentUUID: '1-2-3', + agentId: '1-2-3', files: [getFileReqOptions.parameters.path], zipPassCode: RESPONSE_ACTIONS_ZIP_PASSCODE.sentinel_one, }, @@ -1030,7 +1030,7 @@ describe('SentinelOneActionsClient class', () => { it('should query for the activity log entry record after successful submit of action', async () => { await s1ActionsClient.getFile(getFileReqOptions); - expect(connectorActionsMock.execute).toHaveBeenNthCalledWith(3, { + expect(connectorActionsMock.execute).toHaveBeenCalledWith({ params: { subAction: SUB_ACTION.GET_ACTIVITIES, subActionParams: { @@ -1040,7 +1040,7 @@ describe('SentinelOneActionsClient class', () => { sortOrder: 'asc', // eslint-disable-next-line @typescript-eslint/naming-convention createdAt__gte: expect.any(String), - agentIds: '1845174760470303882', + agentIds: '1-2-3', }, }, }); @@ -1327,7 +1327,7 @@ describe('SentinelOneActionsClient class', () => { subAction: 'downloadAgentFile', subActionParams: { activityId: 'activity-1', - agentUUID: '123', + agentId: '123', }, }, }); @@ -1444,7 +1444,7 @@ describe('SentinelOneActionsClient class', () => { params: { subAction: 'executeScript', subActionParams: { - filter: { uuids: '1-2-3' }, + filter: { ids: '1-2-3' }, script: { inputParams: '--terminate --processes "foo" --force', outputDestination: 'SentinelCloud', @@ -1588,7 +1588,7 @@ describe('SentinelOneActionsClient class', () => { params: { subAction: 'executeScript', subActionParams: { - filter: { uuids: '1-2-3' }, + filter: { ids: '1-2-3' }, script: { inputParams: '', outputDestination: 'SentinelCloud', 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 2933f25cfd0ad..ba017ea9db1c9 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 @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { SENTINELONE_CONNECTOR_ID, SUB_ACTION, @@ -17,11 +19,15 @@ import type { SentinelOneGetActivitiesParams, SentinelOneGetActivitiesResponse, SentinelOneGetAgentsResponse, + SentinelOneGetAgentsParams, SentinelOneGetRemoteScriptResultsApiResponse, SentinelOneGetRemoteScriptsParams, SentinelOneGetRemoteScriptsResponse, SentinelOneGetRemoteScriptStatusApiResponse, SentinelOneRemoteScriptExecutionStatus, + SentinelOneIsolateHostParams, + SentinelOneFetchAgentFilesParams, + SentinelOneExecuteScriptParams, } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; import type { QueryDslQueryContainer, @@ -173,8 +179,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { ): Promise< LogsEndpointAction > { - const agentUUID = actionRequest.endpoint_ids[0]; - const agentDetails = await this.getAgentDetails(agentUUID); + const agentId = actionRequest.endpoint_ids[0]; + const agentDetails = await this.getAgentDetails(agentId); const doc = await super.writeActionRequestToEndpointIndex< TParameters, @@ -183,11 +189,11 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { >({ ...actionRequest, hosts: { - [agentUUID]: { name: agentDetails.computerName }, + [agentId]: { name: agentDetails.computerName }, }, meta: { // Add common meta data - agentUUID, + agentUUID: agentId, agentId: agentDetails.id, hostName: agentDetails.computerName, ...(actionRequest.meta ?? {}), @@ -201,10 +207,10 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { * Sends actions to SentinelOne directly (via Connector) * @private */ - private async sendAction( - actionType: SUB_ACTION, - actionParams: object - ): Promise> { + private async sendAction< + TResponse = unknown, + TParams extends Record = Record + >(actionType: SUB_ACTION, actionParams: TParams): Promise> { const executeOptions: Parameters[0] = { params: { subAction: actionType, @@ -233,18 +239,18 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { this.log.debug(() => `Response:\n${stringify(actionSendResponse)}`); - return actionSendResponse as ActionTypeExecutorResult; + return actionSendResponse as ActionTypeExecutorResult; } /** Gets agent details directly from SentinelOne */ private async getAgentDetails( - agentUUID: string + agentId: string ): Promise { - const cachedEntry = this.cache.get(agentUUID); + const cachedEntry = this.cache.get(agentId); if (cachedEntry) { this.log.debug( - `Found cached agent details for UUID [${agentUUID}]:\n${stringify(cachedEntry)}` + `Found cached agent details for UUID [${agentId}]:\n${stringify(cachedEntry)}` ); return cachedEntry; } @@ -252,24 +258,25 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { let s1ApiResponse: SentinelOneGetAgentsResponse | undefined; try { - const response = await this.sendAction(SUB_ACTION.GET_AGENTS, { - uuid: agentUUID, - }); + const response = await this.sendAction< + SentinelOneGetAgentsResponse, + SentinelOneGetAgentsParams + >(SUB_ACTION.GET_AGENTS, { ids: agentId }); s1ApiResponse = response.data; } catch (err) { throw new ResponseActionsClientError( - `Error while attempting to retrieve SentinelOne host with agent id [${agentUUID}]: ${err.message}`, + `Error while attempting to retrieve SentinelOne host with agent id [${agentId}]: ${err.message}`, 500, err ); } if (!s1ApiResponse || !s1ApiResponse.data[0]) { - throw new ResponseActionsClientError(`SentinelOne agent id [${agentUUID}] not found`, 404); + throw new ResponseActionsClientError(`SentinelOne agent id [${agentId}] not found`, 404); } - this.cache.set(agentUUID, s1ApiResponse.data[0]); + this.cache.set(agentId, s1ApiResponse.data[0]); return s1ApiResponse.data[0]; } @@ -288,6 +295,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { }; } + // KILL-PROCESS: // validate that we have a `process_name`. We need this here because the schema for this command // specifically because `KillProcessRequestBody` allows 3 types of parameters. if (payload.command === 'kill-process') { @@ -327,7 +335,9 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { if (!error) { try { - await this.sendAction(SUB_ACTION.ISOLATE_HOST, { uuid: actionRequest.endpoint_ids[0] }); + await this.sendAction(SUB_ACTION.ISOLATE_HOST, { + ids: actionRequest.endpoint_ids[0], + }); } catch (err) { error = err; } @@ -380,7 +390,9 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { if (!error) { try { - await this.sendAction(SUB_ACTION.RELEASE_HOST, { uuid: actionRequest.endpoint_ids[0] }); + await this.sendAction(SUB_ACTION.RELEASE_HOST, { + ids: actionRequest.endpoint_ids[0], + }); } catch (err) { error = err; } @@ -437,17 +449,22 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { command: 'get-file', }; + const agentId = actionRequest.endpoint_ids[0]; + if (!reqIndexOptions.error) { let error = (await this.validateRequest(reqIndexOptions)).error; const timestamp = new Date().toISOString(); if (!error) { try { - await this.sendAction(SUB_ACTION.FETCH_AGENT_FILES, { - agentUUID: actionRequest.endpoint_ids[0], - files: [actionRequest.parameters.path], - zipPassCode: RESPONSE_ACTIONS_ZIP_PASSCODE.sentinel_one, - }); + await this.sendAction( + SUB_ACTION.FETCH_AGENT_FILES, + { + agentId: actionRequest.endpoint_ids[0], + files: [actionRequest.parameters.path], + zipPassCode: RESPONSE_ACTIONS_ZIP_PASSCODE.sentinel_one, + } + ); } catch (err) { error = err; } @@ -460,8 +477,6 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { } if (!error) { - const { id: agentId } = await this.getAgentDetails(actionRequest.endpoint_ids[0]); - const activitySearchCriteria: SentinelOneGetActivitiesParams = { // Activity type for fetching a file from a host machine in SentinelOne: // { @@ -492,7 +507,9 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { }; } else { this.log.warn( - `Unable to find a fetch file command entry in SentinelOne activity log. May be unable to complete response action` + `Unable to find a fetch file command entry in SentinelOne activity log. May be unable to complete response action. Search criteria used:\n${stringify( + activitySearchCriteria + )}` ); } } @@ -658,7 +675,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { } const downloadAgentFileMethodOptions: SentinelOneDownloadAgentFileParams = { - agentUUID: agentId, + agentId, activityId: getFileAgentResponse.meta?.activityLogEntryId, }; const { data } = await this.sendAction( @@ -765,24 +782,24 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { ); try { - const s1Response = await this.sendAction( - SUB_ACTION.EXECUTE_SCRIPT, - { - filter: { - uuids: actionRequest.endpoint_ids[0], - }, - script: { - scriptId: terminateScriptInfo.scriptId, - taskDescription: this.buildExternalComment(reqIndexOptions), - requiresApproval: false, - outputDestination: 'SentinelCloud', - inputParams: terminateScriptInfo.buildScriptArgs({ - // @ts-expect-error TS2339: Property 'process_name' does not exist (`.validateRequest()` has already validated that `process_name` exists) - processName: reqIndexOptions.parameters.process_name, - }), - }, - } - ); + const s1Response = await this.sendAction< + SentinelOneExecuteScriptResponse, + SentinelOneExecuteScriptParams + >(SUB_ACTION.EXECUTE_SCRIPT, { + filter: { + ids: actionRequest.endpoint_ids[0], + }, + script: { + scriptId: terminateScriptInfo.scriptId, + taskDescription: this.buildExternalComment(reqIndexOptions), + requiresApproval: false, + outputDestination: 'SentinelCloud', + inputParams: terminateScriptInfo.buildScriptArgs({ + // @ts-expect-error TS2339: Property 'process_name' does not exist (`.validateRequest()` has already validated that `process_name` exists) + processName: reqIndexOptions.parameters.process_name, + }), + }, + }); reqIndexOptions.meta = { parentTaskId: s1Response.data?.data?.parentTaskId ?? '', @@ -842,21 +859,21 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl { ); try { - const s1Response = await this.sendAction( - SUB_ACTION.EXECUTE_SCRIPT, - { - filter: { - uuids: actionRequest.endpoint_ids[0], - }, - script: { - scriptId: processesScriptInfo.scriptId, - taskDescription: this.buildExternalComment(reqIndexOptions), - requiresApproval: false, - outputDestination: 'SentinelCloud', - inputParams: processesScriptInfo.buildScriptArgs({}), - }, - } - ); + const s1Response = await this.sendAction< + SentinelOneExecuteScriptResponse, + SentinelOneExecuteScriptParams + >(SUB_ACTION.EXECUTE_SCRIPT, { + filter: { + ids: actionRequest.endpoint_ids[0], + }, + script: { + scriptId: processesScriptInfo.scriptId, + taskDescription: this.buildExternalComment(reqIndexOptions), + requiresApproval: false, + outputDestination: 'SentinelCloud', + inputParams: processesScriptInfo.buildScriptArgs({}), + }, + }); reqIndexOptions.meta = { parentTaskId: s1Response.data?.data?.parentTaskId ?? '', diff --git a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/sentinel_one_agent_status_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/sentinel_one_agent_status_client.ts index 3509cee521fc0..cc5222114366e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/sentinel_one_agent_status_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/sentinel_one_agent_status_client.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { stringify } from '../../../../utils/stringify'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { catchAndWrapError } from '../../../../utils'; import { getPendingActionsSummary } from '../../..'; @@ -30,75 +32,74 @@ export class SentinelOneAgentStatusClient extends AgentStatusClient { const esClient = this.options.esClient; const metadataService = this.options.endpointService.getEndpointMetadataService(); const sortField = 'sentinel_one.agent.last_active_date'; - - const query = { - bool: { - must: [ - { - bool: { - filter: [ - { - terms: { - 'sentinel_one.agent.uuid': agentIds, - }, - }, - ], - }, - }, - ], + const searchRequest: SearchRequest = { + index: SENTINEL_ONE_AGENT_INDEX_PATTERN, + from: 0, + size: DEFAULT_MAX_TABLE_QUERY_SIZE, + query: { + bool: { + should: [ + { bool: { filter: [{ terms: { 'sentinel_one.agent.agent.id': agentIds } }] } }, + { bool: { filter: [{ terms: { 'sentinel_one.agent.uuid': agentIds } }] } }, + ], + minimum_should_match: 1, + }, + }, + collapse: { + field: 'sentinel_one.agent.agent.id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ [sortField]: { order: 'desc' } }], + _source: [ + 'sentinel_one.agent.agent.id', + 'sentinel_one.agent.uuid', + 'sentinel_one.agent.network_status', + 'sentinel_one.agent.last_active_date', + 'sentinel_one.agent.is_active', + 'sentinel_one.agent.is_pending_uninstall', + 'sentinel_one.agent.is_uninstalled', + ], + }, }, + sort: [ + { + [sortField]: { + order: 'desc', + }, + }, + ], + _source: false, }; try { const [searchResponse, allPendingActions] = await Promise.all([ - esClient.search( - { - index: SENTINEL_ONE_AGENT_INDEX_PATTERN, - from: 0, - size: DEFAULT_MAX_TABLE_QUERY_SIZE, - query, - collapse: { - field: 'sentinel_one.agent.uuid', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [ - { - [sortField]: { - order: 'desc', - }, - }, - ], - }, - }, - sort: [ - { - [sortField]: { - order: 'desc', - }, - }, - ], - _source: false, - }, - { ignore: [404] } - ), + esClient.search(searchRequest, { ignore: [404] }), getPendingActionsSummary(esClient, metadataService, this.log, agentIds), ]).catch(catchAndWrapError); + this.log.debug( + () => + `Searching SentinelOne agent data index [${SENTINEL_ONE_AGENT_INDEX_PATTERN}] with:\n${stringify( + searchRequest, + 15 + )}\n\nReturned:\n${stringify(searchResponse, 15)}` + ); + const mostRecentAgentInfosByAgentId = searchResponse?.hits?.hits?.reduce< Record >((acc, hit) => { - if (hit.fields?.['sentinel_one.agent.uuid'][0]) { - acc[hit.fields?.['sentinel_one.agent.uuid'][0]] = + if (hit.fields?.['sentinel_one.agent.agent.id']?.[0]) { + acc[hit.fields['sentinel_one.agent.agent.id'][0]] = hit.inner_hits?.most_recent.hits.hits[0]._source; } return acc; }, {}); - return agentIds.reduce((acc, agentId) => { - const agentInfo = mostRecentAgentInfosByAgentId[agentId].sentinel_one.agent; + const response = agentIds.reduce((acc, agentId) => { + const agentInfo = mostRecentAgentInfosByAgentId[agentId]?.sentinel_one?.agent; const pendingActions = allPendingActions.find( (agentPendingActions) => agentPendingActions.agent_id === agentId @@ -107,7 +108,7 @@ export class SentinelOneAgentStatusClient extends AgentStatusClient { acc[agentId] = { agentId, agentType: this.agentType, - found: agentInfo?.uuid === agentId, + found: agentInfo?.uuid === agentId || agentInfo.agent.id === agentId, isolated: agentInfo?.network_status === SENTINEL_ONE_NETWORK_STATUS.DISCONNECTED, lastSeen: agentInfo?.last_active_date || '', status: agentInfo?.is_active @@ -121,9 +122,13 @@ export class SentinelOneAgentStatusClient extends AgentStatusClient { return acc; }, {}); + + this.log.debug(() => `Agent status response:\n${stringify(response)}`); + + return response; } catch (err) { const error = new AgentStatusClientError( - `Failed to fetch sentinel one agent status for agentIds: [${agentIds}], failed with: ${err.message}`, + `Failed to fetch SentinelOne agent status for agentIds: [${agentIds}], failed with: ${err.message}`, 500, err ); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/types.ts b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/types.ts index 002a561e7db47..d39d2377fc2a2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/sentinel_one/types.ts @@ -13,6 +13,9 @@ import { export interface RawSentinelOneInfo { sentinel_one: { agent: { + agent: { + id: string; + }; uuid: string; last_active_date: string; network_status: string; diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts index 431d3be6d059b..d78ba776944f7 100644 --- a/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts +++ b/x-pack/plugins/stack_connectors/common/sentinelone/schema.ts @@ -169,7 +169,7 @@ export const SentinelOneGetRemoteScriptsParamsSchema = schema.object({ }); export const SentinelOneFetchAgentFilesParamsSchema = schema.object({ - agentUUID: schema.string({ minLength: 1 }), + agentId: schema.string({ minLength: 1 }), zipPassCode: schema.string({ minLength: 10 }), files: schema.arrayOf(schema.string({ minLength: 1 })), }); @@ -187,7 +187,7 @@ export const SentinelOneFetchAgentFilesResponseSchema = schema.object({ }); export const SentinelOneDownloadAgentFileParamsSchema = schema.object({ - agentUUID: schema.string({ minLength: 1 }), + agentId: schema.string({ minLength: 1 }), activityId: schema.string({ minLength: 1 }), }); diff --git a/x-pack/plugins/stack_connectors/common/sentinelone/types.ts b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts index bb5705d30f7c5..3290784a430f7 100644 --- a/x-pack/plugins/stack_connectors/common/sentinelone/types.ts +++ b/x-pack/plugins/stack_connectors/common/sentinelone/types.ts @@ -6,6 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { Mutable } from 'utility-types'; import { SentinelOneBaseApiResponseSchema, SentinelOneConfigSchema, @@ -137,15 +138,15 @@ export type SentinelOneGetRemoteScriptsResponse = TypeOf< typeof SentinelOneGetRemoteScriptsResponseSchema >; -export type SentinelOneFetchAgentFilesParams = TypeOf< - typeof SentinelOneFetchAgentFilesParamsSchema +export type SentinelOneFetchAgentFilesParams = Mutable< + TypeOf >; export type SentinelOneFetchAgentFilesResponse = TypeOf< typeof SentinelOneFetchAgentFilesResponseSchema >; -export type SentinelOneDownloadAgentFileParams = TypeOf< - typeof SentinelOneDownloadAgentFileParamsSchema +export type SentinelOneDownloadAgentFileParams = Mutable< + TypeOf >; export type SentinelOneActivityRecord = Omit< @@ -162,6 +163,8 @@ export type SentinelOneGetActivitiesResponse = Omit< 'data' > & { data: Array> }; -export type SentinelOneIsolateHostParams = TypeOf; +export type SentinelOneIsolateHostParams = Partial< + Mutable> +>; export type SentinelOneActionParams = TypeOf; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.test.ts index 675cf13f18516..ced2784f057a6 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.test.ts @@ -27,22 +27,22 @@ describe('SentinelOne Connector', () => { beforeEach(() => { fetchAgentFilesParams = { files: ['/tmp/one'], - agentUUID: 'uuid-1', + agentId: 'uuid-1', zipPassCode: 'foo', }; }); - it('should error if agent UUID is invalid', async () => { - connectorInstance.mockResponses.getAgentsApiResponse.data.length = 0; + it('should error if no agent id provided', async () => { + fetchAgentFilesParams.agentId = ''; await expect(connectorInstance.fetchAgentFiles(fetchAgentFilesParams)).rejects.toHaveProperty( 'message', - 'No agent found in SentinelOne for UUID [uuid-1]' + "'agentId' parameter is required" ); }); it('should call SentinelOne fetch-files API with expected data', async () => { - const fetchFilesUrl = `${connectorInstance.constructorParams.config.url}${API_PATH}/agents/1913920934584665209/actions/fetch-files`; + const fetchFilesUrl = `${connectorInstance.constructorParams.config.url}${API_PATH}/agents/${fetchAgentFilesParams.agentId}/actions/fetch-files`; const response = await connectorInstance.fetchAgentFiles(fetchAgentFilesParams); expect(response).toEqual({ data: { success: true }, errors: null }); @@ -68,17 +68,16 @@ describe('SentinelOne Connector', () => { beforeEach(() => { downloadAgentFileParams = { - agentUUID: 'uuid-1', + agentId: 'uuid-1', activityId: '11111', }; }); - it('should error if called with invalid agent UUID', async () => { - connectorInstance.mockResponses.getAgentsApiResponse.data.length = 0; - + it('should error if called with invalid agent id', async () => { + downloadAgentFileParams.agentId = ''; await expect( connectorInstance.downloadAgentFile(downloadAgentFileParams) - ).rejects.toHaveProperty('message', 'No agent found in SentinelOne for UUID [uuid-1]'); + ).rejects.toHaveProperty('message', "'agentId' parameter is required"); }); it('should call SentinelOne api with expected url', async () => { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts index 803a735ca55af..99f486b44a087 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts @@ -157,16 +157,9 @@ export class SentinelOneConnector extends SubActionConnector< }); } - public async fetchAgentFiles({ - files, - agentUUID, - zipPassCode, - }: SentinelOneFetchAgentFilesParams) { - const agent = await this.getAgents({ uuid: agentUUID }); - const agentId = agent.data[0]?.id; - + public async fetchAgentFiles({ files, agentId, zipPassCode }: SentinelOneFetchAgentFilesParams) { if (!agentId) { - throw new Error(`No agent found in SentinelOne for UUID [${agentUUID}]`); + throw new Error(`'agentId' parameter is required`); } return this.sentinelOneApiRequest({ @@ -182,12 +175,9 @@ export class SentinelOneConnector extends SubActionConnector< }); } - public async downloadAgentFile({ agentUUID, activityId }: SentinelOneDownloadAgentFileParams) { - const agent = await this.getAgents({ uuid: agentUUID }); - const agentId = agent.data[0]?.id; - + public async downloadAgentFile({ agentId, activityId }: SentinelOneDownloadAgentFileParams) { if (!agentId) { - throw new Error(`No agent found in SentinelOne for UUID [${agentUUID}]`); + throw new Error(`'agentId' parameter is required`); } return this.sentinelOneApiRequest({