Skip to content

Commit

Permalink
[EDR Workflows] Crowdstrike - add support to responder (elastic#184217)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsonpl authored and rohanxz committed Jun 4, 2024
1 parent 8cb54ba commit 975e482
Show file tree
Hide file tree
Showing 12 changed files with 521 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import { getCrowdstrikeAgentId } from '../../../common/utils/crowdstrike_alert_check';
import { getExternalEdrAgentInfo } from './get_external_edr_agent_info';

jest.mock('../../../common/utils/sentinelone_alert_check');
jest.mock('../../../common/utils/crowdstrike_alert_check');

describe('getExternalEdrAgentInfo', () => {
const mockEventData = [
{
category: 'event',
field: 'event.module',
values: ['sentinel_one'],
isObjectArray: false,
},
{
category: 'host',
field: 'host.name',
values: ['test-host'],
isObjectArray: false,
},
{
category: 'host',
field: 'host.os.name',
values: ['Windows'],
isObjectArray: false,
},
{
category: 'host',
field: 'host.os.family',
values: ['windows'],
isObjectArray: false,
},
{
category: 'host',
field: 'host.os.version',
values: ['10'],
isObjectArray: false,
},
{
category: 'kibana',
field: 'kibana.alert.last_detected',
values: ['2023-05-01T12:34:56Z'],
isObjectArray: false,
},
{
category: 'crowdstrike',
field: 'crowdstrike.event.HostName',
values: ['test-crowdstrike-host'],
isObjectArray: false,
},
{
category: 'crowdstrike',
field: 'crowdstrike.event.Platform',
values: ['linux'],
isObjectArray: false,
},
];

beforeEach(() => {
(getSentinelOneAgentId as jest.Mock).mockReturnValue('sentinel-one-agent-id');
(getCrowdstrikeAgentId as jest.Mock).mockReturnValue('crowdstrike-agent-id');
});

afterEach(() => {
jest.clearAllMocks();
});

it('should return correct info for sentinel_one agent type', () => {
const result = getExternalEdrAgentInfo(mockEventData, 'sentinel_one');
expect(result).toEqual({
agent: {
id: 'sentinel-one-agent-id',
type: 'sentinel_one',
},
host: {
name: 'test-host',
os: {
name: 'Windows',
family: 'windows',
version: '10',
},
},
lastCheckin: '2023-05-01T12:34:56Z',
});
});

it('should return correct info for crowdstrike agent type', () => {
const result = getExternalEdrAgentInfo(mockEventData, 'crowdstrike');
expect(result).toEqual({
agent: {
id: 'crowdstrike-agent-id',
type: 'crowdstrike',
},
host: {
name: 'test-crowdstrike-host',
os: {
name: '',
family: 'linux',
version: '',
},
},
lastCheckin: '2023-05-01T12:34:56Z',
});
});

it('should throw an error for unsupported agent type', () => {
expect(() => {
// @ts-expect-error testing purpose
getExternalEdrAgentInfo(mockEventData, 'unsupported_agent_type');
}).toThrow('Unsupported agent type: unsupported_agent_type');
});
});
Original file line number Diff line number Diff line change
@@ -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 type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants';
import type { ThirdPartyAgentInfo } from '../../../../common/types';
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import { getFieldValue } from '../host_isolation/helpers';
import { getCrowdstrikeAgentId } from '../../../common/utils/crowdstrike_alert_check';

export const getExternalEdrAgentInfo = (
eventData: TimelineEventsDetailsItem[],
agentType: ResponseActionAgentType
): ThirdPartyAgentInfo => {
switch (agentType) {
case 'sentinel_one':
return {
agent: {
id: getSentinelOneAgentId(eventData) || '',
type: getFieldValue(
{ category: 'event', field: 'event.module' },
eventData
) as ResponseActionAgentType,
},
host: {
name: getFieldValue({ category: 'host', field: 'host.name' }, eventData),
os: {
name: getFieldValue({ category: 'host', field: 'host.os.name' }, eventData),
family: getFieldValue({ category: 'host', field: 'host.os.family' }, eventData),
version: getFieldValue({ category: 'host', field: 'host.os.version' }, eventData),
},
},
lastCheckin: getFieldValue(
{ category: 'kibana', field: 'kibana.alert.last_detected' },
eventData
),
};
case 'crowdstrike':
return {
agent: {
id: getCrowdstrikeAgentId(eventData) || '',
type: agentType,
},
host: {
name: getFieldValue(
{ category: 'crowdstrike', field: 'crowdstrike.event.HostName' },
eventData
),
os: {
name: '',
family: getFieldValue(
{ category: 'crowdstrike', field: 'crowdstrike.event.Platform' },
eventData
),
version: '',
},
},
lastCheckin: getFieldValue(
{ category: 'kibana', field: 'kibana.alert.last_detected' },
eventData
),
};
default:
throw new Error(`Unsupported agent type: ${agentType}`);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
import { CROWDSTRIKE_AGENT_ID_FIELD } from '../../../common/utils/crowdstrike_alert_check';
import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../common/utils/sentinelone_alert_check';

export const NOT_FROM_ENDPOINT_HOST_TOOLTIP = i18n.translate(
Expand Down Expand Up @@ -36,3 +37,12 @@ export const SENTINEL_ONE_AGENT_ID_PROPERTY_MISSING = i18n.translate(
},
}
);
export const CROWDSTRIKE_AGENT_ID_PROPERTY_MISSING = i18n.translate(
'xpack.securitySolution.endpoint.detections.takeAction.responseActionConsole.missingCrowdstrikeAgentId',
{
defaultMessage: 'Event data missing Crowdstrike agent identifier ({field})',
values: {
field: CROWDSTRIKE_AGENT_ID_FIELD,
},
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,57 @@ describe('#useResponderActionData', () => {
expect(result.current.isDisabled).toEqual(false);
});
});
describe('when agentType is `crowdstrike`', () => {
const createEventDataMock = (): TimelineEventsDetailsItem[] => {
return [
{
category: 'crowdstrike',
field: 'crowdstrike.event.DeviceId',
values: ['mockedAgentId'],
originalValue: ['mockedAgentId'],
isObjectArray: false,
},
];
};

it('should return `responder` menu item as `disabled` if agentType is `crowdstrike` and feature flag is disabled', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);

const { result } = renderHook(() =>
useResponderActionData({
endpointId: 'crowdstrike-id',
agentType: 'crowdstrike',
eventData: createEventDataMock(),
})
);
expect(result.current.isDisabled).toEqual(true);
});

it('should return responder menu item as disabled with tooltip if agent id property is missing from event data', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
const { result } = renderHook(() =>
useResponderActionData({
endpointId: 'crowdstrike-id',
agentType: 'crowdstrike',
eventData: [],
})
);
expect(result.current.isDisabled).toEqual(true);
expect(result.current.tooltip).toEqual(
'Event data missing Crowdstrike agent identifier (crowdstrike.event.DeviceId)'
);
});

it('should return `responder` menu item as `enabled `if agentType is `crowdstrike` and feature flag is enabled', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
const { result } = renderHook(() =>
useResponderActionData({
endpointId: 'crowdstrike-id',
agentType: 'crowdstrike',
eventData: createEventDataMock(),
})
);
expect(result.current.isDisabled).toEqual(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { getExternalEdrAgentInfo } from './get_external_edr_agent_info';
import { getCrowdstrikeAgentId } from '../../../common/utils/crowdstrike_alert_check';
import type { Platform } from '../../../management/components/endpoint_responder/components/header_info/platforms';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check';
import type { ThirdPartyAgentInfo } from '../../../../common/types';
import type {
ResponseActionAgentType,
EndpointCapabilities,
} from '../../../../common/endpoint/service/response_actions/constants';
import { useGetEndpointDetails, useWithShowResponder } from '../../../management/hooks';
import { HostStatus } from '../../../../common/endpoint/types';
import {
CROWDSTRIKE_AGENT_ID_PROPERTY_MISSING,
HOST_ENDPOINT_UNENROLLED_TOOLTIP,
LOADING_ENDPOINT_DATA_TOOLTIP,
METADATA_API_ERROR_TOOLTIP,
NOT_FROM_ENDPOINT_HOST_TOOLTIP,
SENTINEL_ONE_AGENT_ID_PROPERTY_MISSING,
} from './translations';
import { getFieldValue } from '../host_isolation/helpers';

export interface ResponderContextMenuItemProps {
endpointId: string;
Expand All @@ -33,32 +34,6 @@ export interface ResponderContextMenuItemProps {
eventData?: TimelineEventsDetailsItem[] | null;
}

const getThirdPartyAgentInfo = (
eventData: TimelineEventsDetailsItem[] | null
): ThirdPartyAgentInfo => {
return {
agent: {
id: getSentinelOneAgentId(eventData) || '',
type: getFieldValue(
{ category: 'event', field: 'event.module' },
eventData
) as ResponseActionAgentType,
},
host: {
name: getFieldValue({ category: 'host', field: 'host.name' }, eventData),
os: {
name: getFieldValue({ category: 'host', field: 'host.os.name' }, eventData),
family: getFieldValue({ category: 'host', field: 'host.os.family' }, eventData),
version: getFieldValue({ category: 'host', field: 'host.os.version' }, eventData),
},
},
lastCheckin: getFieldValue(
{ category: 'kibana', field: 'kibana.alert.last_detected' },
eventData
),
};
};

/**
* This hook is used to get the data needed to show the context menu items for the responder
* actions.
Expand All @@ -85,6 +60,9 @@ export const useResponderActionData = ({
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
'responseActionsSentinelOneV1Enabled'
);
const responseActionsCrowdstrikeManualHostIsolationEnabled = useIsExperimentalFeatureEnabled(
'responseActionsCrowdstrikeManualHostIsolationEnabled'
);
const {
data: hostInfo,
isFetching,
Expand All @@ -105,6 +83,17 @@ export const useResponderActionData = ({
return [true, SENTINEL_ONE_AGENT_ID_PROPERTY_MISSING];
}

return [false, undefined];
case 'crowdstrike':
// Disable it if feature flag is disabled
if (!responseActionsCrowdstrikeManualHostIsolationEnabled) {
return [true, undefined];
}
// Event must have the property that identifies the agent id
if (!getCrowdstrikeAgentId(eventData ?? null)) {
return [true, CROWDSTRIKE_AGENT_ID_PROPERTY_MISSING];
}

return [false, undefined];

default:
Expand Down Expand Up @@ -152,11 +141,12 @@ export const useResponderActionData = ({
agentType,
isSentinelOneV1Enabled,
eventData,
responseActionsCrowdstrikeManualHostIsolationEnabled,
]);

const handleResponseActionsClick = useCallback(() => {
if (!isEndpointHost) {
const agentInfoFromAlert = getThirdPartyAgentInfo(eventData || null);
if (!isEndpointHost && eventData != null) {
const agentInfoFromAlert = getExternalEdrAgentInfo(eventData, agentType);
showResponseActionsConsole({
agentId: agentInfoFromAlert.agent.id,
agentType,
Expand Down
Loading

0 comments on commit 975e482

Please sign in to comment.