mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SecuritySolutions][Endpoint] Microsoft defender for Endpoint response actions API (#205097)
## Summary - Adds response actions client/APIs for isolate and release actions for Microsoft Defender for Endpoint - The feature is behind a feature flag `responseActionsMSDefenderEndpointEnabled`
This commit is contained in:
parent
737cf96809
commit
b25c9984bb
30 changed files with 1290 additions and 57 deletions
|
@ -46558,10 +46558,12 @@ components:
|
|||
- minLength: 1
|
||||
type: string
|
||||
Security_Endpoint_Management_API_AgentTypes:
|
||||
description: The host agent type (optional). Defaults to endpoint.
|
||||
enum:
|
||||
- endpoint
|
||||
- sentinel_one
|
||||
- crowdstrike
|
||||
- microsoft_defender_endpoint
|
||||
type: string
|
||||
Security_Endpoint_Management_API_AlertIds:
|
||||
description: A list of alerts ids.
|
||||
|
|
|
@ -53434,10 +53434,12 @@ components:
|
|||
- minLength: 1
|
||||
type: string
|
||||
Security_Endpoint_Management_API_AgentTypes:
|
||||
description: The host agent type (optional). Defaults to endpoint.
|
||||
enum:
|
||||
- endpoint
|
||||
- sentinel_one
|
||||
- crowdstrike
|
||||
- microsoft_defender_endpoint
|
||||
type: string
|
||||
Security_Endpoint_Management_API_AlertIds:
|
||||
description: A list of alerts ids.
|
||||
|
|
|
@ -142,8 +142,16 @@ export const Comment = z.string();
|
|||
export type Parameters = z.infer<typeof Parameters>;
|
||||
export const Parameters = z.object({});
|
||||
|
||||
/**
|
||||
* The host agent type (optional). Defaults to endpoint.
|
||||
*/
|
||||
export type AgentTypes = z.infer<typeof AgentTypes>;
|
||||
export const AgentTypes = z.enum(['endpoint', 'sentinel_one', 'crowdstrike']);
|
||||
export const AgentTypes = z.enum([
|
||||
'endpoint',
|
||||
'sentinel_one',
|
||||
'crowdstrike',
|
||||
'microsoft_defender_endpoint',
|
||||
]);
|
||||
export type AgentTypesEnum = typeof AgentTypes.enum;
|
||||
export const AgentTypesEnum = AgentTypes.enum;
|
||||
|
||||
|
|
|
@ -141,10 +141,12 @@ components:
|
|||
description: Optional parameters object
|
||||
AgentTypes:
|
||||
type: string
|
||||
description: The host agent type (optional). Defaults to endpoint.
|
||||
enum:
|
||||
- endpoint
|
||||
- sentinel_one
|
||||
- crowdstrike
|
||||
- microsoft_defender_endpoint
|
||||
|
||||
BaseActionSchema:
|
||||
x-inline: true
|
||||
|
|
|
@ -12,7 +12,12 @@ export type ResponseActionStatus = (typeof RESPONSE_ACTION_STATUS)[number];
|
|||
export const RESPONSE_ACTION_TYPE = ['automated', 'manual'] as const;
|
||||
export type ResponseActionType = (typeof RESPONSE_ACTION_TYPE)[number];
|
||||
|
||||
export const RESPONSE_ACTION_AGENT_TYPE = ['endpoint', 'sentinel_one', 'crowdstrike'] as const;
|
||||
export const RESPONSE_ACTION_AGENT_TYPE = [
|
||||
'endpoint',
|
||||
'sentinel_one',
|
||||
'crowdstrike',
|
||||
'microsoft_defender_endpoint',
|
||||
] as const;
|
||||
export type ResponseActionAgentType = (typeof RESPONSE_ACTION_AGENT_TYPE)[number];
|
||||
|
||||
/**
|
||||
|
@ -181,6 +186,7 @@ export const RESPONSE_ACTIONS_ZIP_PASSCODE: Readonly<Record<ResponseActionAgentT
|
|||
endpoint: 'elastic',
|
||||
sentinel_one: 'Elastic@123',
|
||||
crowdstrike: 'tbd..',
|
||||
microsoft_defender_endpoint: 'tbd..',
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -206,6 +212,8 @@ export const RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS: Readonly<
|
|||
'sentinel_one.agent.agent.id',
|
||||
],
|
||||
crowdstrike: ['device.id'],
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
microsoft_defender_endpoint: ['some.id'],
|
||||
});
|
||||
|
||||
export const SUPPORTED_AGENT_ID_ALERT_FIELDS: Readonly<string[]> = Object.values(
|
||||
|
|
|
@ -23,11 +23,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: true,
|
||||
microsoft_defender_endpoint: true,
|
||||
},
|
||||
},
|
||||
unisolate: {
|
||||
|
@ -35,11 +37,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: true,
|
||||
microsoft_defender_endpoint: true,
|
||||
},
|
||||
},
|
||||
upload: {
|
||||
|
@ -47,11 +51,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
'get-file': {
|
||||
|
@ -59,11 +65,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
'kill-process': {
|
||||
|
@ -71,11 +79,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
execute: {
|
||||
|
@ -83,11 +93,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
'suspend-process': {
|
||||
|
@ -95,11 +107,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
'running-processes': {
|
||||
|
@ -107,11 +121,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
scan: {
|
||||
|
@ -119,11 +135,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
runscript: {
|
||||
|
@ -131,11 +149,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
|||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
manual: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: true,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ export * from './trusted_apps';
|
|||
export * from './utility_types';
|
||||
export * from './agents';
|
||||
export * from './sentinel_one';
|
||||
export * from './microsoft_defender_endpoint';
|
||||
export type { ConditionEntriesMap, ConditionEntry } from './exception_list_items';
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface MicrosoftDefenderEndpointActionRequestCommonMeta {
|
||||
/** The ID of the action in Microsoft Defender's system */
|
||||
machineActionId: string;
|
||||
}
|
|
@ -272,6 +272,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables the Asset Inventory feature
|
||||
*/
|
||||
assetInventoryUXEnabled: false,
|
||||
|
||||
/**
|
||||
* Enabled Microsoft Defender for Endpoint actions client
|
||||
*/
|
||||
responseActionsMSDefenderEndpointEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -480,10 +480,12 @@ components:
|
|||
- minLength: 1
|
||||
type: string
|
||||
AgentTypes:
|
||||
description: The host agent type (optional). Defaults to endpoint.
|
||||
enum:
|
||||
- endpoint
|
||||
- sentinel_one
|
||||
- crowdstrike
|
||||
- microsoft_defender_endpoint
|
||||
type: string
|
||||
AlertIds:
|
||||
description: A list of alerts ids.
|
||||
|
|
|
@ -480,10 +480,12 @@ components:
|
|||
- minLength: 1
|
||||
type: string
|
||||
AgentTypes:
|
||||
description: The host agent type (optional). Defaults to endpoint.
|
||||
enum:
|
||||
- endpoint
|
||||
- sentinel_one
|
||||
- crowdstrike
|
||||
- microsoft_defender_endpoint
|
||||
type: string
|
||||
AlertIds:
|
||||
description: A list of alerts ids.
|
||||
|
|
|
@ -28,7 +28,10 @@ describe('AgentTypeVendorLogo component', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)('should display logo for: %s', async (agentType) => {
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
it.each(
|
||||
RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => agentType !== 'microsoft_defender_endpoint')
|
||||
)('should display logo for: %s', async (agentType) => {
|
||||
props.agentType = agentType;
|
||||
const { getByTitle } = render();
|
||||
|
||||
|
|
|
@ -110,7 +110,12 @@ describe('use responder action data hooks', () => {
|
|||
expect(onClickMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([...RESPONSE_ACTION_AGENT_TYPE])(
|
||||
it.each([
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
...RESPONSE_ACTION_AGENT_TYPE.filter(
|
||||
(agentType) => agentType !== 'microsoft_defender_endpoint'
|
||||
),
|
||||
])(
|
||||
'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];
|
||||
|
|
|
@ -76,16 +76,15 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
|
|||
appContextMock.renderHook(() => useAlertResponseActionsSupport(alertDetailItemData));
|
||||
});
|
||||
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
||||
'should return expected response for agentType: `%s`',
|
||||
(agentType) => {
|
||||
alertDetailItemData =
|
||||
endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
||||
const { result } = renderHook();
|
||||
it.each(
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => agentType !== 'microsoft_defender_endpoint')
|
||||
)('should return expected response for agentType: `%s`', (agentType) => {
|
||||
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(getExpectedResult({ details: { agentType } }));
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual(getExpectedResult({ details: { agentType } }));
|
||||
});
|
||||
|
||||
it('should set `isSupported` to `false` if no alert details item data is provided', () => {
|
||||
alertDetailItemData = [];
|
||||
|
@ -178,7 +177,9 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
|
|||
});
|
||||
|
||||
it.each(
|
||||
RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => agentType !== 'endpoint') as Array<
|
||||
RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => agentType !== 'endpoint')
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
.filter((agentType) => agentType !== 'microsoft_defender_endpoint') as Array<
|
||||
Exclude<ResponseActionAgentType, 'endpoint'>
|
||||
>
|
||||
)('should set `isSupported` to `false` for [%s] if feature flag is disabled', (agentType) => {
|
||||
|
|
|
@ -13,7 +13,11 @@ import {
|
|||
import { getEventDetailsAgentIdField, parseEcsFieldPath } from '..';
|
||||
|
||||
describe('getEventDetailsAgentIdField()', () => {
|
||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(`should return agent id info for %s`, (agentType) => {
|
||||
it.each(
|
||||
RESPONSE_ACTION_AGENT_TYPE
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
.filter((agentType) => agentType !== 'microsoft_defender_endpoint')
|
||||
)(`should return agent id info for %s`, (agentType) => {
|
||||
const field = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
|
||||
const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
||||
|
||||
|
@ -25,26 +29,24 @@ describe('getEventDetailsAgentIdField()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
it.each(
|
||||
RESPONSE_ACTION_AGENT_TYPE
|
||||
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||
.filter((agentType) => agentType !== 'microsoft_defender_endpoint')
|
||||
)('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: '',
|
||||
});
|
||||
}
|
||||
);
|
||||
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',
|
||||
|
|
|
@ -177,6 +177,9 @@ const useTypesFilterInitialState = ({
|
|||
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsSentinelOneV1Enabled'
|
||||
);
|
||||
const isMicrosoftDefenderEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsMSDefenderEndpointEnabled'
|
||||
);
|
||||
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||
);
|
||||
|
@ -207,14 +210,21 @@ const useTypesFilterInitialState = ({
|
|||
|
||||
// v8.13 onwards
|
||||
// for showing agent types and action types in the same filter
|
||||
if (isSentinelOneV1Enabled || isCrowdstrikeEnabled) {
|
||||
if (isSentinelOneV1Enabled || isCrowdstrikeEnabled || isMicrosoftDefenderEnabled) {
|
||||
if (!isFlyout) {
|
||||
return [
|
||||
{
|
||||
label: FILTER_NAMES.agentTypes,
|
||||
isGroupLabel: true,
|
||||
},
|
||||
...RESPONSE_ACTION_AGENT_TYPE.map((type) =>
|
||||
...RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => {
|
||||
switch (agentType) {
|
||||
case 'microsoft_defender_endpoint':
|
||||
return isMicrosoftDefenderEnabled;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).map((type) =>
|
||||
getFilterOptions({
|
||||
key: type,
|
||||
label: getAgentTypeName(type),
|
||||
|
|
|
@ -1877,6 +1877,7 @@ describe('Response actions history', () => {
|
|||
mockedContext.setExperimentalFlag({
|
||||
responseActionsSentinelOneV1Enabled: true,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
||||
responseActionsMSDefenderEndpointEnabled: true,
|
||||
});
|
||||
render({ isFlyout: false });
|
||||
const { getByTestId, getAllByTestId } = renderResult;
|
||||
|
@ -1891,6 +1892,7 @@ describe('Response actions history', () => {
|
|||
'Elastic Defend',
|
||||
'SentinelOne',
|
||||
'Crowdstrike',
|
||||
'microsoft_defender_endpoint',
|
||||
'Triggered by rule',
|
||||
'Triggered manually',
|
||||
]);
|
||||
|
|
|
@ -70,6 +70,9 @@ export const ResponseActionsLog = memo<
|
|||
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||
);
|
||||
const isMicrosoftDefenderEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsMSDefenderEndpointEnabled'
|
||||
);
|
||||
|
||||
// Used to decide if display global loader or not (only the fist time tha page loads)
|
||||
const [isFirstAttempt, setIsFirstAttempt] = useState(true);
|
||||
|
@ -92,7 +95,7 @@ export const ResponseActionsLog = memo<
|
|||
setQueryParams((prevState) => ({
|
||||
...prevState,
|
||||
agentTypes:
|
||||
isSentinelOneV1Enabled || isCrowdstrikeEnabled
|
||||
isSentinelOneV1Enabled || isCrowdstrikeEnabled || isMicrosoftDefenderEnabled
|
||||
? agentTypesFromUrl?.length
|
||||
? agentTypesFromUrl
|
||||
: prevState.agentTypes
|
||||
|
@ -121,6 +124,7 @@ export const ResponseActionsLog = memo<
|
|||
isFlyout,
|
||||
isCrowdstrikeEnabled,
|
||||
isSentinelOneV1Enabled,
|
||||
isMicrosoftDefenderEnabled,
|
||||
statusesFromUrl,
|
||||
setQueryParams,
|
||||
usersFromUrl,
|
||||
|
|
|
@ -115,13 +115,26 @@ describe('CompleteExternalTaskRunner class', () => {
|
|||
|
||||
expect(esClientMock.bulk).toHaveBeenCalledWith({
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX,
|
||||
// Array below will have records for each type of external EDR, so as new ones are
|
||||
// added, a new response should be added to the array below
|
||||
operations: [
|
||||
// for SentinelOne
|
||||
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
||||
expect.objectContaining({
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: expect.any(Object),
|
||||
agent: expect.any(Object),
|
||||
}),
|
||||
|
||||
// for crowdstrike
|
||||
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
||||
expect.objectContaining({
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: expect.any(Object),
|
||||
agent: expect.any(Object),
|
||||
}),
|
||||
|
||||
// for Microsoft Defender
|
||||
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
||||
expect.objectContaining({
|
||||
'@timestamp': expect.any(String),
|
||||
|
|
|
@ -1277,4 +1277,102 @@ describe('Response actions', () => {
|
|||
expect(httpResponseMock.ok).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and agent type if Microsoft Defender', () => {
|
||||
let testSetup: HttpApiTestSetupMock;
|
||||
let httpRequestMock: ReturnType<HttpApiTestSetupMock['createRequestMock']>;
|
||||
let httpHandlerContextMock: HttpApiTestSetupMock['httpHandlerContextMock'];
|
||||
let httpResponseMock: HttpApiTestSetupMock['httpResponseMock'];
|
||||
let callHandler: () => ReturnType<RequestHandler>;
|
||||
|
||||
beforeEach(async () => {
|
||||
testSetup = createHttpApiTestSetupMock();
|
||||
|
||||
({ httpHandlerContextMock, httpResponseMock } = testSetup);
|
||||
httpRequestMock = testSetup.createRequestMock();
|
||||
|
||||
testSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...testSetup.endpointAppContextMock.experimentalFeatures,
|
||||
responseActionsMSDefenderEndpointEnabled: true,
|
||||
};
|
||||
|
||||
httpHandlerContextMock.actions = Promise.resolve({
|
||||
getActionsClient: () => sentinelOneMock.createConnectorActionsClient(),
|
||||
} as unknown as jest.Mocked<ActionsApiRequestHandlerContext>);
|
||||
|
||||
// Set the esClient to be used in the handler context
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
httpHandlerContextMock.core = Promise.resolve(
|
||||
set(
|
||||
await httpHandlerContextMock.core,
|
||||
'elasticsearch.client.asInternalUser',
|
||||
responseActionsClientMock.createConstructorOptions().esClient
|
||||
)
|
||||
);
|
||||
|
||||
httpRequestMock = testSetup.createRequestMock({
|
||||
body: {
|
||||
endpoint_ids: ['123-456'],
|
||||
agent_type: 'microsoft_defender_endpoint',
|
||||
},
|
||||
});
|
||||
registerResponseActionRoutes(testSetup.routerMock, testSetup.endpointAppContextMock);
|
||||
|
||||
(testSetup.endpointAppContextMock.service.getEndpointMetadataService as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
getMetadataForEndpoints: jest.fn().mockResolvedValue([
|
||||
{
|
||||
elastic: {
|
||||
agent: {
|
||||
id: '123-456',
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
id: '123-456',
|
||||
},
|
||||
host: {
|
||||
hostname: 'test-host',
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const handler = testSetup.getRegisteredVersionedRoute(
|
||||
'post',
|
||||
ISOLATE_HOST_ROUTE_V2,
|
||||
'2023-10-31'
|
||||
).routeHandler as RequestHandler;
|
||||
|
||||
callHandler = () => handler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should use the Microsoft Defender response actions client', async () => {
|
||||
await callHandler();
|
||||
expect(getResponseActionsClientMock).toHaveBeenCalledWith(
|
||||
'microsoft_defender_endpoint',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should error if feature is disabled', async () => {
|
||||
testSetup.endpointAppContextMock.experimentalFeatures = {
|
||||
...testSetup.endpointAppContextMock.experimentalFeatures,
|
||||
responseActionsMSDefenderEndpointEnabled: false,
|
||||
};
|
||||
|
||||
await callHandler();
|
||||
|
||||
expect(httpResponseMock.customError).toHaveBeenCalledWith({
|
||||
body: expect.objectContaining({
|
||||
message: '[request body.agent_type]: feature is disabled',
|
||||
}),
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -412,7 +412,9 @@ function isThirdPartyFeatureDisabled(
|
|||
return (
|
||||
(agentType === 'sentinel_one' && !experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
|
||||
(agentType === 'crowdstrike' &&
|
||||
!experimentalFeatures.responseActionsCrowdstrikeManualHostIsolationEnabled)
|
||||
!experimentalFeatures.responseActionsCrowdstrikeManualHostIsolationEnabled) ||
|
||||
(agentType === 'microsoft_defender_endpoint' &&
|
||||
!experimentalFeatures.responseActionsMSDefenderEndpointEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,51 +29,61 @@ const COMMANDS_WITH_ACCESS_TO_FILES: CommandsWithFileAccess = deepFreeze<Command
|
|||
endpoint: true,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
execute: {
|
||||
endpoint: true,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
'running-processes': {
|
||||
endpoint: false,
|
||||
sentinel_one: true,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
upload: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
scan: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
isolate: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
unisolate: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
'kill-process': {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
'suspend-process': {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
runscript: {
|
||||
endpoint: false,
|
||||
sentinel_one: false,
|
||||
crowdstrike: false,
|
||||
microsoft_defender_endpoint: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import type { ResponseActionsClient } from '../..';
|
||||
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error';
|
||||
|
@ -31,7 +32,7 @@ export class ResponseActionsClientError extends CustomHttpRequestError {
|
|||
|
||||
export class ResponseActionsNotSupportedError extends ResponseActionsClientError {
|
||||
constructor(
|
||||
responseAction?: ResponseActionsApiCommandNames,
|
||||
responseAction?: ResponseActionsApiCommandNames | keyof ResponseActionsClient,
|
||||
statusCode: number = 405,
|
||||
meta?: unknown
|
||||
) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { UnsupportedResponseActionsAgentTypeError } from './errors';
|
|||
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { CrowdstrikeActionsClientOptions } from './crowdstrike/crowdstrike_actions_client';
|
||||
import { CrowdstrikeActionsClient } from './crowdstrike/crowdstrike_actions_client';
|
||||
import { MicrosoftDefenderEndpointActionsClient } from './microsoft/defender/endpoint/ms_defender_endpoint_actions_client';
|
||||
|
||||
export type GetResponseActionsClientConstructorOptions = ResponseActionsClientOptions &
|
||||
SentinelOneActionsClientOptions &
|
||||
|
@ -37,6 +38,8 @@ export const getResponseActionsClient = (
|
|||
return new SentinelOneActionsClient(constructorOptions);
|
||||
case 'crowdstrike':
|
||||
return new CrowdstrikeActionsClient(constructorOptions);
|
||||
case 'microsoft_defender_endpoint':
|
||||
return new MicrosoftDefenderEndpointActionsClient(constructorOptions);
|
||||
default:
|
||||
throw new UnsupportedResponseActionsAgentTypeError(
|
||||
`Agent type [${agentType}] does not support response actions`
|
||||
|
|
|
@ -112,6 +112,8 @@ describe('ResponseActionsClientImpl base class', () => {
|
|||
'getFile',
|
||||
'execute',
|
||||
'upload',
|
||||
'getFileDownload',
|
||||
'getFileInfo',
|
||||
];
|
||||
|
||||
it.each(methods)('should throw Not Supported error for %s()', async (method) => {
|
||||
|
@ -121,17 +123,6 @@ describe('ResponseActionsClientImpl base class', () => {
|
|||
await expect(responsePromise).rejects.toBeInstanceOf(ResponseActionsNotSupportedError);
|
||||
await expect(responsePromise).rejects.toHaveProperty('statusCode', 405);
|
||||
});
|
||||
|
||||
it.each(['getFileDownload', 'getFileInfo'])(
|
||||
'should throw not implemented error for %s()',
|
||||
async (method) => {
|
||||
// @ts-expect-error ignoring input type to method since they all should throw
|
||||
const responsePromise = baseClassMock[method]({});
|
||||
|
||||
await expect(responsePromise).rejects.toThrow(`Method ${method}() not implemented`);
|
||||
await expect(responsePromise).rejects.toHaveProperty('statusCode', 501);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('#updateCases()', () => {
|
||||
|
@ -836,7 +827,13 @@ class MockClassWithExposedProtectedMembers extends ResponseActionsClientImpl {
|
|||
return super.writeActionResponseToEndpointIndex<TOutputContent>(options);
|
||||
}
|
||||
|
||||
public fetchAllPendingActions(): AsyncIterable<ResponseActionsClientPendingAction[]> {
|
||||
return super.fetchAllPendingActions();
|
||||
public fetchAllPendingActions<
|
||||
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
|
||||
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
|
||||
TMeta extends {} = {}
|
||||
>(): AsyncIterable<
|
||||
Array<ResponseActionsClientPendingAction<TParameters, TOutputContent, TMeta>>
|
||||
> {
|
||||
return super.fetchAllPendingActions<TParameters, TOutputContent, TMeta>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -654,7 +654,13 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
}
|
||||
}
|
||||
|
||||
protected fetchAllPendingActions(): AsyncIterable<ResponseActionsClientPendingAction[]> {
|
||||
protected fetchAllPendingActions<
|
||||
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
|
||||
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
|
||||
TMeta extends {} = {}
|
||||
>(): AsyncIterable<
|
||||
Array<ResponseActionsClientPendingAction<TParameters, TOutputContent, TMeta>>
|
||||
> {
|
||||
const esClient = this.options.esClient;
|
||||
const query: QueryDslQueryContainer = {
|
||||
bool: {
|
||||
|
@ -861,10 +867,10 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
actionId: string,
|
||||
fileId: string
|
||||
): Promise<GetFileDownloadMethodResponse> {
|
||||
throw new ResponseActionsClientError(`Method getFileDownload() not implemented`, 501);
|
||||
throw new ResponseActionsNotSupportedError('getFileDownload');
|
||||
}
|
||||
|
||||
public async getFileInfo(actionId: string, fileId: string): Promise<UploadedFileInfo> {
|
||||
throw new ResponseActionsClientError(`Method getFileInfo() not implemented`, 501);
|
||||
throw new ResponseActionsNotSupportedError('getFileInfo');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { ActionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
||||
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
|
||||
import {
|
||||
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
|
||||
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
|
||||
import type {
|
||||
MicrosoftDefenderEndpointGetActionsResponse,
|
||||
MicrosoftDefenderEndpointMachine,
|
||||
MicrosoftDefenderEndpointMachineAction,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types';
|
||||
import type { NormalizedExternalConnectorClient } from '../../../../..';
|
||||
import { responseActionsClientMock, type ResponseActionsClientOptionsMock } from '../../../mocks';
|
||||
|
||||
export interface MicrosoftDefenderActionsClientOptionsMock
|
||||
extends ResponseActionsClientOptionsMock {
|
||||
connectorActions: NormalizedExternalConnectorClient;
|
||||
}
|
||||
|
||||
const createMsDefenderClientConstructorOptionsMock = () => {
|
||||
return {
|
||||
...responseActionsClientMock.createConstructorOptions(),
|
||||
connectorActions: responseActionsClientMock.createNormalizedExternalConnectorClient(
|
||||
createMsConnectorActionsClientMock()
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const createMsConnectorActionsClientMock = (): ActionsClientMock => {
|
||||
const client = responseActionsClientMock.createConnectorActionsClient();
|
||||
|
||||
(client.getAll as jest.Mock).mockImplementation(async () => {
|
||||
const result: ConnectorWithExtraFindData[] = [
|
||||
// return a MS connector
|
||||
responseActionsClientMock.createConnector({
|
||||
actionTypeId: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
|
||||
id: 'ms-connector-instance-id',
|
||||
}),
|
||||
];
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
(client.execute as jest.Mock).mockImplementation(
|
||||
async (options: Parameters<typeof client.execute>[0]) => {
|
||||
const subAction = options.params.subAction;
|
||||
|
||||
// Mocks for the different connector methods
|
||||
switch (subAction) {
|
||||
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_DETAILS:
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: createMicrosoftMachineMock(),
|
||||
});
|
||||
|
||||
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST:
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: createMicrosoftMachineActionMock({ type: 'Isolate' }),
|
||||
});
|
||||
|
||||
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RELEASE_HOST:
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: createMicrosoftMachineActionMock({ type: 'Unisolate' }),
|
||||
});
|
||||
|
||||
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS:
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: {
|
||||
'@odata.context': 'some-context',
|
||||
'@odata.count': 1,
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 0,
|
||||
value: [createMicrosoftMachineActionMock()],
|
||||
},
|
||||
});
|
||||
|
||||
default:
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const createMicrosoftMachineMock = (
|
||||
overrides: Partial<MicrosoftDefenderEndpointMachine> = {}
|
||||
): MicrosoftDefenderEndpointMachine => {
|
||||
return {
|
||||
id: '1-2-3',
|
||||
computerDnsName: 'mymachine1.contoso.com',
|
||||
firstSeen: '2018-08-02T14:55:03.7791856Z',
|
||||
lastSeen: '2018-08-02T14:55:03.7791856Z',
|
||||
osPlatform: 'Windows1',
|
||||
version: '1709',
|
||||
osProcessor: 'x64',
|
||||
lastIpAddress: '172.17.230.209',
|
||||
lastExternalIpAddress: '167.220.196.71',
|
||||
osBuild: 18209,
|
||||
healthStatus: 'Active',
|
||||
rbacGroupId: '140',
|
||||
rbacGroupName: 'The-A-Team',
|
||||
riskScore: 'Low',
|
||||
exposureLevel: 'Medium',
|
||||
aadDeviceId: '80fe8ff8-2624-418e-9591-41f0491218f9',
|
||||
machineTags: ['test tag 1', 'test tag 2'],
|
||||
onboardingstatus: 'foo',
|
||||
ipAddresses: [
|
||||
{ ipAddress: '1.1.1.1', macAddress: '23:a2:5t', type: '', operationalStatus: '' },
|
||||
],
|
||||
osArchitecture: '',
|
||||
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const createMicrosoftMachineActionMock = (
|
||||
overrides: Partial<MicrosoftDefenderEndpointMachineAction> = {}
|
||||
): MicrosoftDefenderEndpointMachineAction => {
|
||||
return {
|
||||
id: '5382f7ea-7557-4ab7-9782-d50480024a4e',
|
||||
type: 'Isolate',
|
||||
scope: 'Selective',
|
||||
requestor: 'Analyst@TestPrd.onmicrosoft.com',
|
||||
requestorComment: 'test for docs',
|
||||
requestSource: '',
|
||||
status: 'Succeeded',
|
||||
machineId: '1-2-3',
|
||||
computerDnsName: 'desktop-test',
|
||||
creationDateTimeUtc: '2019-01-02T14:39:38.2262283Z',
|
||||
lastUpdateDateTimeUtc: '2019-01-02T14:40:44.6596267Z',
|
||||
externalID: 'abc',
|
||||
commands: ['RunScript'],
|
||||
cancellationRequestor: '',
|
||||
cancellationComment: '',
|
||||
cancellationDateTimeUtc: '',
|
||||
title: '',
|
||||
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const createMicrosoftGetActionsApiResponseMock =
|
||||
(): MicrosoftDefenderEndpointGetActionsResponse => {
|
||||
return {
|
||||
'@odata.context': 'some-context',
|
||||
'@odata.count': 1,
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 0,
|
||||
value: [createMicrosoftMachineActionMock()],
|
||||
};
|
||||
};
|
||||
|
||||
export const microsoftDefenderMock = {
|
||||
createConstructorOptions: createMsDefenderClientConstructorOptionsMock,
|
||||
createMachineAction: createMicrosoftMachineActionMock,
|
||||
createMachine: createMicrosoftMachineMock,
|
||||
createGetActionsApiResponse: createMicrosoftGetActionsApiResponseMock,
|
||||
};
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* 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 { MicrosoftDefenderEndpointActionsClient } from './ms_defender_endpoint_actions_client';
|
||||
import type { ProcessPendingActionsMethodOptions, ResponseActionsClient } from '../../../../..';
|
||||
import { getActionDetailsById as _getActionDetailsById } from '../../../../action_details_by_id';
|
||||
import type { MicrosoftDefenderActionsClientOptionsMock } from './mocks';
|
||||
import { microsoftDefenderMock } from './mocks';
|
||||
import { ResponseActionsNotSupportedError } from '../../../errors';
|
||||
import type { NormalizedExternalConnectorClientMock } from '../../../mocks';
|
||||
import { responseActionsClientMock } from '../../../mocks';
|
||||
import type {
|
||||
LogsEndpointActionResponse,
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta,
|
||||
} from '../../../../../../../../common/endpoint/types';
|
||||
import { EndpointActionGenerator } from '../../../../../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import { applyEsClientSearchMock } from '../../../../../../mocks/utils.mock';
|
||||
import { ENDPOINT_ACTIONS_INDEX } from '../../../../../../../../common/endpoint/constants';
|
||||
import type {
|
||||
MicrosoftDefenderEndpointGetActionsResponse,
|
||||
MicrosoftDefenderEndpointMachineAction,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types';
|
||||
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
|
||||
|
||||
jest.mock('../../../../action_details_by_id', () => {
|
||||
const originalMod = jest.requireActual('../../../../action_details_by_id');
|
||||
|
||||
return {
|
||||
...originalMod,
|
||||
getActionDetailsById: jest.fn(originalMod.getActionDetailsById),
|
||||
};
|
||||
});
|
||||
|
||||
const getActionDetailsByIdMock = _getActionDetailsById as jest.Mock;
|
||||
|
||||
describe('MS Defender response actions client', () => {
|
||||
let clientConstructorOptionsMock: MicrosoftDefenderActionsClientOptionsMock;
|
||||
let connectorActionsMock: NormalizedExternalConnectorClientMock;
|
||||
let msClientMock: ResponseActionsClient;
|
||||
|
||||
beforeEach(() => {
|
||||
clientConstructorOptionsMock = microsoftDefenderMock.createConstructorOptions();
|
||||
connectorActionsMock =
|
||||
clientConstructorOptionsMock.connectorActions as NormalizedExternalConnectorClientMock;
|
||||
msClientMock = new MicrosoftDefenderEndpointActionsClient(clientConstructorOptionsMock);
|
||||
});
|
||||
|
||||
const supporteResponseActionClassMethods: Record<keyof ResponseActionsClient, boolean> = {
|
||||
upload: false,
|
||||
scan: false,
|
||||
execute: false,
|
||||
getFile: false,
|
||||
getFileDownload: false,
|
||||
getFileInfo: false,
|
||||
killProcess: false,
|
||||
runningProcesses: false,
|
||||
runscript: false,
|
||||
suspendProcess: false,
|
||||
isolate: true,
|
||||
release: true,
|
||||
processPendingActions: true,
|
||||
};
|
||||
|
||||
it.each(
|
||||
Object.entries(supporteResponseActionClassMethods).reduce((acc, [key, value]) => {
|
||||
if (!value) {
|
||||
acc.push(key as keyof ResponseActionsClient);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<keyof ResponseActionsClient>)
|
||||
)('should throw error for %s', async (methodName) => {
|
||||
// @ts-expect-error Purposely passing in empty object for options
|
||||
await expect(msClientMock[methodName]({})).rejects.toBeInstanceOf(
|
||||
ResponseActionsNotSupportedError
|
||||
);
|
||||
});
|
||||
|
||||
it('should error if multiple agent ids are received', async () => {
|
||||
await expect(msClientMock.isolate({ endpoint_ids: ['a', 'b'] })).rejects.toMatchObject({
|
||||
message: `[body.endpoint_ids]: Multiple agents IDs not currently supported for Microsoft Defender for Endpoint`,
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update cases', async () => {
|
||||
await msClientMock.isolate(
|
||||
responseActionsClientMock.createIsolateOptions({ case_ids: ['case-1'] })
|
||||
);
|
||||
|
||||
expect(clientConstructorOptionsMock.casesClient?.attachments.bulkCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe.each<Extract<keyof ResponseActionsClient, 'isolate' | 'release'>>([
|
||||
'isolate',
|
||||
'release',
|
||||
])('#%s()', (responseActionMethod) => {
|
||||
it(`should send ${responseActionMethod} request to Microsoft with expected comment`, async () => {
|
||||
await msClientMock[responseActionMethod](responseActionsClientMock.createIsolateOptions());
|
||||
|
||||
expect(connectorActionsMock.execute).toHaveBeenCalledWith({
|
||||
params: {
|
||||
subAction: responseActionMethod === 'isolate' ? 'isolateHost' : 'releaseHost',
|
||||
subActionParams: {
|
||||
comment: expect.stringMatching(
|
||||
/Action triggered from Elastic Security by user \[foo\] for action \[.* \(action id: .*\)\]: test comment/
|
||||
),
|
||||
id: '1-2-3',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should write action request doc. to endpoint index', async () => {
|
||||
await msClientMock[responseActionMethod](responseActionsClientMock.createIsolateOptions());
|
||||
|
||||
expect(clientConstructorOptionsMock.esClient.index).toHaveBeenCalledWith(
|
||||
{
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: responseActionMethod === 'isolate' ? 'isolate' : 'unisolate',
|
||||
comment: 'test comment',
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'mymachine1.contoso.com',
|
||||
},
|
||||
},
|
||||
parameters: undefined,
|
||||
},
|
||||
expiration: expect.any(String),
|
||||
input_type: 'microsoft_defender_endpoint',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
agent: {
|
||||
id: ['1-2-3'],
|
||||
},
|
||||
meta: {
|
||||
machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e',
|
||||
},
|
||||
user: {
|
||||
id: 'foo',
|
||||
},
|
||||
},
|
||||
index: '.logs-endpoint.actions-default',
|
||||
refresh: 'wait_for',
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should return action details', async () => {
|
||||
await expect(
|
||||
msClientMock[responseActionMethod](responseActionsClientMock.createIsolateOptions())
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
command: expect.any(String),
|
||||
isCompleted: false,
|
||||
})
|
||||
);
|
||||
expect(getActionDetailsByIdMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update cases', async () => {
|
||||
await msClientMock[responseActionMethod](
|
||||
responseActionsClientMock.createIsolateOptions({
|
||||
case_ids: ['case-1'],
|
||||
})
|
||||
);
|
||||
|
||||
expect(clientConstructorOptionsMock.casesClient?.attachments.bulkCreate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#processPendingActions()', () => {
|
||||
let abortController: AbortController;
|
||||
let processPendingActionsOptions: ProcessPendingActionsMethodOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
abortController = new AbortController();
|
||||
processPendingActionsOptions = {
|
||||
abortSignal: abortController.signal,
|
||||
addToQueue: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('for Isolate and Release', () => {
|
||||
let msMachineActionsApiResponse: MicrosoftDefenderEndpointGetActionsResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
const generator = new EndpointActionGenerator('seed');
|
||||
|
||||
const actionRequestsSearchResponse = generator.toEsSearchResponse([
|
||||
generator.generateActionEsHit<
|
||||
undefined,
|
||||
{},
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
>({
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
EndpointActions: {
|
||||
data: { command: 'isolate' },
|
||||
input_type: 'microsoft_defender_endpoint',
|
||||
},
|
||||
meta: { machineActionId: '5382f7ea-7557-4ab7-9782-d50480024a4e' },
|
||||
}),
|
||||
]);
|
||||
|
||||
applyEsClientSearchMock({
|
||||
esClientMock: clientConstructorOptionsMock.esClient,
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
response: actionRequestsSearchResponse,
|
||||
pitUsage: true,
|
||||
});
|
||||
|
||||
msMachineActionsApiResponse = microsoftDefenderMock.createGetActionsApiResponse();
|
||||
responseActionsClientMock.setConnectorActionsClientExecuteResponse(
|
||||
connectorActionsMock,
|
||||
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS,
|
||||
msMachineActionsApiResponse
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate action response docs for completed actions', async () => {
|
||||
await msClientMock.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(processPendingActionsOptions.addToQueue).toHaveBeenCalledWith({
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: '1d6e6796-b0af-496f-92b0-25fcb06db499',
|
||||
completed_at: expect.any(String),
|
||||
data: { command: 'isolate' },
|
||||
input_type: 'microsoft_defender_endpoint',
|
||||
started_at: expect.any(String),
|
||||
},
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
error: undefined,
|
||||
meta: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it.each<MicrosoftDefenderEndpointMachineAction['status']>(['Pending', 'InProgress'])(
|
||||
'should NOT generate action responses if action in MS Defender as a status of %s',
|
||||
async (machineActionStatus) => {
|
||||
msMachineActionsApiResponse.value[0].status = machineActionStatus;
|
||||
await msClientMock.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(processPendingActionsOptions.addToQueue).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
msStatusValue | responseState
|
||||
${'Failed'} | ${'failure'}
|
||||
${'TimeOut'} | ${'failure'}
|
||||
${'Cancelled'} | ${'failure'}
|
||||
${'Succeeded'} | ${'success'}
|
||||
`(
|
||||
'should generate $responseState action response if MS machine action status is $msStatusValue',
|
||||
async ({ msStatusValue, responseState }) => {
|
||||
msMachineActionsApiResponse.value[0].status = msStatusValue;
|
||||
const expectedResult: LogsEndpointActionResponse = {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: '1d6e6796-b0af-496f-92b0-25fcb06db499',
|
||||
completed_at: expect.any(String),
|
||||
data: { command: 'isolate' },
|
||||
input_type: 'microsoft_defender_endpoint',
|
||||
started_at: expect.any(String),
|
||||
},
|
||||
agent: { id: 'agent-uuid-1' },
|
||||
error: undefined,
|
||||
meta: undefined,
|
||||
};
|
||||
if (responseState === 'failure') {
|
||||
expectedResult.error = {
|
||||
message: expect.any(String),
|
||||
};
|
||||
}
|
||||
await msClientMock.processPendingActions(processPendingActionsOptions);
|
||||
|
||||
expect(processPendingActionsOptions.addToQueue).toHaveBeenCalledWith(expectedResult);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,524 @@
|
|||
/*
|
||||
* 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 { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
|
||||
import {
|
||||
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
|
||||
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
|
||||
import type {
|
||||
MicrosoftDefenderEndpointGetActionsParams,
|
||||
MicrosoftDefenderEndpointGetActionsResponse,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types';
|
||||
import {
|
||||
type MicrosoftDefenderEndpointAgentDetailsParams,
|
||||
type MicrosoftDefenderEndpointIsolateHostParams,
|
||||
type MicrosoftDefenderEndpointMachine,
|
||||
type MicrosoftDefenderEndpointMachineAction,
|
||||
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types';
|
||||
import { groupBy } from 'lodash';
|
||||
import type {
|
||||
IsolationRouteRequestBody,
|
||||
UnisolationRouteRequestBody,
|
||||
} from '../../../../../../../../common/api/endpoint';
|
||||
import type {
|
||||
ActionDetails,
|
||||
EndpointActionDataParameterTypes,
|
||||
EndpointActionResponseDataOutput,
|
||||
LogsEndpointAction,
|
||||
LogsEndpointActionResponse,
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta,
|
||||
} from '../../../../../../../../common/endpoint/types';
|
||||
import type {
|
||||
ResponseActionAgentType,
|
||||
ResponseActionsApiCommandNames,
|
||||
} from '../../../../../../../../common/endpoint/service/response_actions/constants';
|
||||
import type { NormalizedExternalConnectorClient } from '../../../lib/normalized_external_connector_client';
|
||||
import type {
|
||||
ResponseActionsClientPendingAction,
|
||||
ResponseActionsClientValidateRequestResponse,
|
||||
ResponseActionsClientWriteActionRequestToEndpointIndexOptions,
|
||||
} from '../../../lib/base_response_actions_client';
|
||||
import {
|
||||
ResponseActionsClientImpl,
|
||||
type ResponseActionsClientOptions,
|
||||
} from '../../../lib/base_response_actions_client';
|
||||
import { stringify } from '../../../../../../utils/stringify';
|
||||
import { ResponseActionsClientError } from '../../../errors';
|
||||
import type {
|
||||
CommonResponseActionMethodOptions,
|
||||
ProcessPendingActionsMethodOptions,
|
||||
} from '../../../lib/types';
|
||||
|
||||
export type MicrosoftDefenderActionsClientOptions = ResponseActionsClientOptions & {
|
||||
connectorActions: NormalizedExternalConnectorClient;
|
||||
};
|
||||
|
||||
export class MicrosoftDefenderEndpointActionsClient extends ResponseActionsClientImpl {
|
||||
protected readonly agentType: ResponseActionAgentType = 'microsoft_defender_endpoint';
|
||||
private readonly connectorActionsClient: NormalizedExternalConnectorClient;
|
||||
|
||||
constructor({ connectorActions, ...options }: MicrosoftDefenderActionsClientOptions) {
|
||||
super(options);
|
||||
this.connectorActionsClient = connectorActions;
|
||||
connectorActions.setup(MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID);
|
||||
}
|
||||
|
||||
protected async handleResponseActionCreation<
|
||||
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
|
||||
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
|
||||
TMeta extends {} = {}
|
||||
>(
|
||||
actionRequestOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
|
||||
TParameters,
|
||||
TOutputContent,
|
||||
TMeta
|
||||
>
|
||||
): Promise<{
|
||||
actionEsDoc: LogsEndpointAction<TParameters, TOutputContent, TMeta>;
|
||||
actionDetails: ActionDetails<TOutputContent, TParameters>;
|
||||
}> {
|
||||
const actionRequestDoc = await this.writeActionRequestToEndpointIndex<
|
||||
TParameters,
|
||||
TOutputContent,
|
||||
TMeta
|
||||
>(actionRequestOptions);
|
||||
|
||||
await this.updateCases({
|
||||
command: actionRequestOptions.command,
|
||||
caseIds: actionRequestOptions.case_ids,
|
||||
alertIds: actionRequestOptions.alert_ids,
|
||||
actionId: actionRequestDoc.EndpointActions.action_id,
|
||||
hosts: actionRequestOptions.endpoint_ids.map((agentId) => {
|
||||
return {
|
||||
hostId: agentId,
|
||||
hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '',
|
||||
};
|
||||
}),
|
||||
comment: actionRequestOptions.comment,
|
||||
});
|
||||
|
||||
return {
|
||||
actionEsDoc: actionRequestDoc,
|
||||
actionDetails: await this.fetchActionDetails<ActionDetails<TOutputContent, TParameters>>(
|
||||
actionRequestDoc.EndpointActions.action_id
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends actions to Ms Defender for Endpoint directly (via Connector)
|
||||
* @private
|
||||
*/
|
||||
private async sendAction<
|
||||
TResponse = unknown,
|
||||
TParams extends Record<string, unknown> = Record<string, unknown>
|
||||
>(
|
||||
actionType: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
|
||||
actionParams: TParams
|
||||
): Promise<ActionTypeExecutorResult<TResponse>> {
|
||||
const executeOptions: Parameters<typeof this.connectorActionsClient.execute>[0] = {
|
||||
params: {
|
||||
subAction: actionType,
|
||||
subActionParams: actionParams,
|
||||
},
|
||||
};
|
||||
|
||||
this.log.debug(
|
||||
() =>
|
||||
`calling connector actions 'execute()' for Microsoft Defender for Endpoint with:\n${stringify(
|
||||
executeOptions
|
||||
)}`
|
||||
);
|
||||
|
||||
const actionSendResponse = await this.connectorActionsClient.execute(executeOptions);
|
||||
|
||||
if (actionSendResponse.status === 'error') {
|
||||
this.log.error(stringify(actionSendResponse));
|
||||
|
||||
throw new ResponseActionsClientError(
|
||||
`Attempt to send [${actionType}] to Microsoft Defender for Endpoint failed: ${
|
||||
actionSendResponse.serviceMessage || actionSendResponse.message
|
||||
}`,
|
||||
500,
|
||||
actionSendResponse
|
||||
);
|
||||
}
|
||||
|
||||
this.log.debug(() => `Response:\n${stringify(actionSendResponse)}`);
|
||||
|
||||
return actionSendResponse as ActionTypeExecutorResult<TResponse>;
|
||||
}
|
||||
|
||||
protected async writeActionRequestToEndpointIndex<
|
||||
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
|
||||
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
|
||||
TMeta extends {} = MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
>(
|
||||
actionRequest: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
|
||||
TParameters,
|
||||
TOutputContent,
|
||||
TMeta
|
||||
>
|
||||
): Promise<LogsEndpointAction<TParameters, TOutputContent, TMeta>> {
|
||||
const agentId = actionRequest.endpoint_ids[0];
|
||||
const agentDetails = await this.getAgentDetails(agentId);
|
||||
|
||||
const doc = await super.writeActionRequestToEndpointIndex<TParameters, TOutputContent, TMeta>({
|
||||
...actionRequest,
|
||||
hosts: {
|
||||
[agentId]: { name: agentDetails.computerDnsName },
|
||||
},
|
||||
});
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Gets agent details directly from MS Defender for Endpoint */
|
||||
private async getAgentDetails(agentId: string): Promise<MicrosoftDefenderEndpointMachine> {
|
||||
const cachedEntry = this.cache.get<MicrosoftDefenderEndpointMachine>(agentId);
|
||||
|
||||
if (cachedEntry) {
|
||||
this.log.debug(
|
||||
`Found cached agent details for UUID [${agentId}]:\n${stringify(cachedEntry)}`
|
||||
);
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
let msDefenderEndpointGetMachineDetailsApiResponse:
|
||||
| MicrosoftDefenderEndpointMachine
|
||||
| undefined;
|
||||
|
||||
try {
|
||||
const agentDetailsResponse = await this.sendAction<
|
||||
MicrosoftDefenderEndpointMachine,
|
||||
MicrosoftDefenderEndpointAgentDetailsParams
|
||||
>(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_DETAILS, { id: agentId });
|
||||
|
||||
msDefenderEndpointGetMachineDetailsApiResponse = agentDetailsResponse.data;
|
||||
} catch (err) {
|
||||
throw new ResponseActionsClientError(
|
||||
`Error while attempting to retrieve Microsoft Defender for Endpoint host with agent id [${agentId}]: ${err.message}`,
|
||||
500,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
if (!msDefenderEndpointGetMachineDetailsApiResponse) {
|
||||
throw new ResponseActionsClientError(
|
||||
`Microsoft Defender for Endpoint agent id [${agentId}] not found`,
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
this.cache.set(agentId, msDefenderEndpointGetMachineDetailsApiResponse);
|
||||
|
||||
return msDefenderEndpointGetMachineDetailsApiResponse;
|
||||
}
|
||||
|
||||
protected async validateRequest(
|
||||
payload: ResponseActionsClientWriteActionRequestToEndpointIndexOptions
|
||||
): Promise<ResponseActionsClientValidateRequestResponse> {
|
||||
// TODO: support multiple agents
|
||||
if (payload.endpoint_ids.length > 1) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: new ResponseActionsClientError(
|
||||
`[body.endpoint_ids]: Multiple agents IDs not currently supported for Microsoft Defender for Endpoint`,
|
||||
400
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return super.validateRequest(payload);
|
||||
}
|
||||
|
||||
async isolate(
|
||||
actionRequest: IsolationRouteRequestBody,
|
||||
options: CommonResponseActionMethodOptions = {}
|
||||
): Promise<ActionDetails> {
|
||||
const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
|
||||
undefined,
|
||||
{},
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
> = {
|
||||
...actionRequest,
|
||||
...this.getMethodOptions(options),
|
||||
command: 'isolate',
|
||||
};
|
||||
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
const msActionResponse = await this.sendAction<
|
||||
MicrosoftDefenderEndpointMachineAction,
|
||||
MicrosoftDefenderEndpointIsolateHostParams
|
||||
>(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST, {
|
||||
id: actionRequest.endpoint_ids[0],
|
||||
comment: this.buildExternalComment(reqIndexOptions),
|
||||
});
|
||||
|
||||
if (msActionResponse?.data?.id) {
|
||||
reqIndexOptions.meta = { machineActionId: msActionResponse.data.id };
|
||||
} else {
|
||||
throw new ResponseActionsClientError(
|
||||
`Isolate request was sent to Microsoft Defender, but Machine Action Id was not provided!`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
|
||||
reqIndexOptions.error = error?.message;
|
||||
|
||||
if (!this.options.isAutomated && error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { actionDetails } = await this.handleResponseActionCreation(reqIndexOptions);
|
||||
|
||||
return actionDetails;
|
||||
}
|
||||
|
||||
async release(
|
||||
actionRequest: UnisolationRouteRequestBody,
|
||||
options: CommonResponseActionMethodOptions = {}
|
||||
): Promise<ActionDetails> {
|
||||
const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
|
||||
undefined,
|
||||
{},
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
> = {
|
||||
...actionRequest,
|
||||
...this.getMethodOptions(options),
|
||||
command: 'unisolate',
|
||||
};
|
||||
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
const msActionResponse = await this.sendAction<
|
||||
MicrosoftDefenderEndpointMachineAction,
|
||||
MicrosoftDefenderEndpointIsolateHostParams
|
||||
>(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RELEASE_HOST, {
|
||||
id: actionRequest.endpoint_ids[0],
|
||||
comment: this.buildExternalComment(reqIndexOptions),
|
||||
});
|
||||
|
||||
if (msActionResponse?.data?.id) {
|
||||
reqIndexOptions.meta = { machineActionId: msActionResponse.data.id };
|
||||
} else {
|
||||
throw new ResponseActionsClientError(
|
||||
`Un-Isolate request was sent to Microsoft Defender, but Machine Action Id was not provided!`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
|
||||
reqIndexOptions.error = error?.message;
|
||||
|
||||
if (!this.options.isAutomated && error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { actionDetails } = await this.handleResponseActionCreation(reqIndexOptions);
|
||||
|
||||
return actionDetails;
|
||||
}
|
||||
|
||||
async processPendingActions({
|
||||
abortSignal,
|
||||
addToQueue,
|
||||
}: ProcessPendingActionsMethodOptions): Promise<void> {
|
||||
if (abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addResponsesToQueueIfAny = (responseList: LogsEndpointActionResponse[]): void => {
|
||||
if (responseList.length > 0) {
|
||||
addToQueue(...responseList);
|
||||
|
||||
this.sendActionResponseTelemetry(responseList);
|
||||
}
|
||||
};
|
||||
|
||||
for await (const pendingActions of this.fetchAllPendingActions<
|
||||
EndpointActionDataParameterTypes,
|
||||
EndpointActionResponseDataOutput,
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
>()) {
|
||||
if (abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingActionsByType = groupBy(pendingActions, 'action.EndpointActions.data.command');
|
||||
|
||||
for (const [actionType, typePendingActions] of Object.entries(pendingActionsByType)) {
|
||||
if (abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (actionType as ResponseActionsApiCommandNames) {
|
||||
case 'isolate':
|
||||
case 'unisolate':
|
||||
addResponsesToQueueIfAny(
|
||||
await this.checkPendingIsolateReleaseActions(
|
||||
typePendingActions as Array<
|
||||
ResponseActionsClientPendingAction<
|
||||
undefined,
|
||||
{},
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
>
|
||||
>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async checkPendingIsolateReleaseActions(
|
||||
actionRequests: Array<
|
||||
ResponseActionsClientPendingAction<
|
||||
undefined,
|
||||
{},
|
||||
MicrosoftDefenderEndpointActionRequestCommonMeta
|
||||
>
|
||||
>
|
||||
): Promise<LogsEndpointActionResponse[]> {
|
||||
const completedResponses: LogsEndpointActionResponse[] = [];
|
||||
const warnings: string[] = [];
|
||||
const actionsByMachineId: Record<
|
||||
string,
|
||||
Array<LogsEndpointAction<undefined, {}, MicrosoftDefenderEndpointActionRequestCommonMeta>>
|
||||
> = {};
|
||||
const machineActionIds: string[] = [];
|
||||
const msApiOptions: MicrosoftDefenderEndpointGetActionsParams = {
|
||||
id: machineActionIds,
|
||||
pageSize: 1000,
|
||||
};
|
||||
|
||||
for (const { action } of actionRequests) {
|
||||
const command = action.EndpointActions.data.command;
|
||||
const machineActionId = action.meta?.machineActionId;
|
||||
|
||||
if (!machineActionId) {
|
||||
warnings.push(
|
||||
`${command} response action ID [${action.EndpointActions.action_id}] is missing Microsoft Defender for Endpoint machine action id, thus unable to check on it's status. Forcing it to complete as failure.`
|
||||
);
|
||||
|
||||
completedResponses.push(
|
||||
this.buildActionResponseEsDoc({
|
||||
actionId: action.EndpointActions.action_id,
|
||||
agentId: Array.isArray(action.agent.id) ? action.agent.id[0] : action.agent.id,
|
||||
data: { command },
|
||||
error: {
|
||||
message: `Unable to very if action completed. Microsoft Defender machine action id ('meta.machineActionId') missing on action request document!`,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (!actionsByMachineId[machineActionId]) {
|
||||
actionsByMachineId[machineActionId] = [];
|
||||
}
|
||||
|
||||
actionsByMachineId[machineActionId].push(action);
|
||||
machineActionIds.push(machineActionId);
|
||||
}
|
||||
}
|
||||
|
||||
const { data: machineActions } =
|
||||
await this.sendAction<MicrosoftDefenderEndpointGetActionsResponse>(
|
||||
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS,
|
||||
msApiOptions
|
||||
);
|
||||
|
||||
if (machineActions?.value) {
|
||||
for (const machineAction of machineActions.value) {
|
||||
const { isPending, isError, message } = this.calculateMachineActionState(machineAction);
|
||||
|
||||
if (!isPending) {
|
||||
const pendingActionRequests = actionsByMachineId[machineAction.id] ?? [];
|
||||
|
||||
for (const actionRequest of pendingActionRequests) {
|
||||
completedResponses.push(
|
||||
this.buildActionResponseEsDoc({
|
||||
actionId: actionRequest.EndpointActions.action_id,
|
||||
agentId: Array.isArray(actionRequest.agent.id)
|
||||
? actionRequest.agent.id[0]
|
||||
: actionRequest.agent.id,
|
||||
data: { command: actionRequest.EndpointActions.data.command },
|
||||
error: isError ? { message } : undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log.debug(
|
||||
() =>
|
||||
`${completedResponses.length} action responses generated:\n${stringify(completedResponses)}`
|
||||
);
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.log.warn(warnings.join('\n'));
|
||||
}
|
||||
|
||||
return completedResponses;
|
||||
}
|
||||
|
||||
private calculateMachineActionState(machineAction: MicrosoftDefenderEndpointMachineAction): {
|
||||
isPending: boolean;
|
||||
isError: boolean;
|
||||
message: string;
|
||||
} {
|
||||
let isPending = true;
|
||||
let isError = false;
|
||||
let message = '';
|
||||
|
||||
switch (machineAction.status) {
|
||||
case 'Failed':
|
||||
case 'TimeOut':
|
||||
isPending = false;
|
||||
isError = true;
|
||||
message = `Response action ${machineAction.status} (Microsoft Defender for Endpoint machine action ID: ${machineAction.id})`;
|
||||
break;
|
||||
|
||||
case 'Cancelled':
|
||||
isPending = false;
|
||||
isError = true;
|
||||
message = `Response action was canceled by [${
|
||||
machineAction.cancellationRequestor
|
||||
}] (Microsoft Defender for Endpoint machine action ID: ${machineAction.id})${
|
||||
machineAction.cancellationComment ? `: ${machineAction.cancellationComment}` : ''
|
||||
}`;
|
||||
break;
|
||||
|
||||
case 'Succeeded':
|
||||
isPending = false;
|
||||
isError = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
// covers 'Pending' | 'InProgress'
|
||||
isPending = true;
|
||||
isError = false;
|
||||
}
|
||||
|
||||
return { isPending, isError, message };
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import type { ActionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
||||
import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
||||
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
|
||||
|
@ -57,6 +59,9 @@ export interface ResponseActionsClientOptionsMock extends ResponseActionsClientO
|
|||
casesClient?: CasesClientMock;
|
||||
}
|
||||
|
||||
export type NormalizedExternalConnectorClientMock =
|
||||
DeeplyMockedKeys<NormalizedExternalConnectorClient>;
|
||||
|
||||
const createResponseActionClientMock = (): jest.Mocked<ResponseActionsClient> => {
|
||||
return {
|
||||
suspendProcess: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
|
@ -305,7 +310,7 @@ const createConnectorActionsClientMock = ({
|
|||
|
||||
const createNormalizedExternalConnectorClientMock = (
|
||||
connectorActionsClientMock: ActionsClientMock = createConnectorActionsClientMock()
|
||||
): DeeplyMockedKeys<NormalizedExternalConnectorClient> => {
|
||||
): NormalizedExternalConnectorClientMock => {
|
||||
const normalizedClient = new NormalizedExternalConnectorClient(
|
||||
connectorActionsClientMock,
|
||||
loggingSystemMock.createLogger()
|
||||
|
@ -314,7 +319,31 @@ const createNormalizedExternalConnectorClientMock = (
|
|||
jest.spyOn(normalizedClient, 'execute');
|
||||
jest.spyOn(normalizedClient, 'setup');
|
||||
|
||||
return normalizedClient as DeeplyMockedKeys<NormalizedExternalConnectorClient>;
|
||||
return normalizedClient as NormalizedExternalConnectorClientMock;
|
||||
};
|
||||
|
||||
const setConnectorActionsClientExecuteResponseMock = (
|
||||
connectorActionsClient: ActionsClientMock | NormalizedExternalConnectorClientMock,
|
||||
subAction: string,
|
||||
response: any
|
||||
): void => {
|
||||
const executeMockFn = (connectorActionsClient.execute as jest.Mock).getMockImplementation();
|
||||
|
||||
(connectorActionsClient.execute as jest.Mock).mockImplementation(async (options) => {
|
||||
if (options.params.subAction === subAction) {
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: response,
|
||||
});
|
||||
}
|
||||
|
||||
if (executeMockFn) {
|
||||
return executeMockFn(options);
|
||||
}
|
||||
|
||||
return responseActionsClientMock.createConnectorActionExecuteResponse({
|
||||
data: {},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const responseActionsClientMock = Object.freeze({
|
||||
|
@ -341,4 +370,5 @@ export const responseActionsClientMock = Object.freeze({
|
|||
/** Create a mock connector instance */
|
||||
createConnector: createConnectorMock,
|
||||
createConnectorActionExecuteResponse: createConnectorActionExecuteResponseMock,
|
||||
setConnectorActionsClientExecuteResponse: setConnectorActionsClientExecuteResponseMock,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue