mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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
|
- minLength: 1
|
||||||
type: string
|
type: string
|
||||||
Security_Endpoint_Management_API_AgentTypes:
|
Security_Endpoint_Management_API_AgentTypes:
|
||||||
|
description: The host agent type (optional). Defaults to endpoint.
|
||||||
enum:
|
enum:
|
||||||
- endpoint
|
- endpoint
|
||||||
- sentinel_one
|
- sentinel_one
|
||||||
- crowdstrike
|
- crowdstrike
|
||||||
|
- microsoft_defender_endpoint
|
||||||
type: string
|
type: string
|
||||||
Security_Endpoint_Management_API_AlertIds:
|
Security_Endpoint_Management_API_AlertIds:
|
||||||
description: A list of alerts ids.
|
description: A list of alerts ids.
|
||||||
|
|
|
@ -53434,10 +53434,12 @@ components:
|
||||||
- minLength: 1
|
- minLength: 1
|
||||||
type: string
|
type: string
|
||||||
Security_Endpoint_Management_API_AgentTypes:
|
Security_Endpoint_Management_API_AgentTypes:
|
||||||
|
description: The host agent type (optional). Defaults to endpoint.
|
||||||
enum:
|
enum:
|
||||||
- endpoint
|
- endpoint
|
||||||
- sentinel_one
|
- sentinel_one
|
||||||
- crowdstrike
|
- crowdstrike
|
||||||
|
- microsoft_defender_endpoint
|
||||||
type: string
|
type: string
|
||||||
Security_Endpoint_Management_API_AlertIds:
|
Security_Endpoint_Management_API_AlertIds:
|
||||||
description: A list of alerts ids.
|
description: A list of alerts ids.
|
||||||
|
|
|
@ -142,8 +142,16 @@ export const Comment = z.string();
|
||||||
export type Parameters = z.infer<typeof Parameters>;
|
export type Parameters = z.infer<typeof Parameters>;
|
||||||
export const Parameters = z.object({});
|
export const Parameters = z.object({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host agent type (optional). Defaults to endpoint.
|
||||||
|
*/
|
||||||
export type AgentTypes = z.infer<typeof AgentTypes>;
|
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 type AgentTypesEnum = typeof AgentTypes.enum;
|
||||||
export const AgentTypesEnum = AgentTypes.enum;
|
export const AgentTypesEnum = AgentTypes.enum;
|
||||||
|
|
||||||
|
|
|
@ -141,10 +141,12 @@ components:
|
||||||
description: Optional parameters object
|
description: Optional parameters object
|
||||||
AgentTypes:
|
AgentTypes:
|
||||||
type: string
|
type: string
|
||||||
|
description: The host agent type (optional). Defaults to endpoint.
|
||||||
enum:
|
enum:
|
||||||
- endpoint
|
- endpoint
|
||||||
- sentinel_one
|
- sentinel_one
|
||||||
- crowdstrike
|
- crowdstrike
|
||||||
|
- microsoft_defender_endpoint
|
||||||
|
|
||||||
BaseActionSchema:
|
BaseActionSchema:
|
||||||
x-inline: true
|
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 const RESPONSE_ACTION_TYPE = ['automated', 'manual'] as const;
|
||||||
export type ResponseActionType = (typeof RESPONSE_ACTION_TYPE)[number];
|
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];
|
export type ResponseActionAgentType = (typeof RESPONSE_ACTION_AGENT_TYPE)[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -181,6 +186,7 @@ export const RESPONSE_ACTIONS_ZIP_PASSCODE: Readonly<Record<ResponseActionAgentT
|
||||||
endpoint: 'elastic',
|
endpoint: 'elastic',
|
||||||
sentinel_one: 'Elastic@123',
|
sentinel_one: 'Elastic@123',
|
||||||
crowdstrike: 'tbd..',
|
crowdstrike: 'tbd..',
|
||||||
|
microsoft_defender_endpoint: 'tbd..',
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -206,6 +212,8 @@ export const RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS: Readonly<
|
||||||
'sentinel_one.agent.agent.id',
|
'sentinel_one.agent.agent.id',
|
||||||
],
|
],
|
||||||
crowdstrike: ['device.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(
|
export const SUPPORTED_AGENT_ID_ALERT_FIELDS: Readonly<string[]> = Object.values(
|
||||||
|
|
|
@ -23,11 +23,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: true,
|
crowdstrike: true,
|
||||||
|
microsoft_defender_endpoint: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unisolate: {
|
unisolate: {
|
||||||
|
@ -35,11 +37,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: true,
|
crowdstrike: true,
|
||||||
|
microsoft_defender_endpoint: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -47,11 +51,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'get-file': {
|
'get-file': {
|
||||||
|
@ -59,11 +65,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'kill-process': {
|
'kill-process': {
|
||||||
|
@ -71,11 +79,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute: {
|
execute: {
|
||||||
|
@ -83,11 +93,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'suspend-process': {
|
'suspend-process': {
|
||||||
|
@ -95,11 +107,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'running-processes': {
|
'running-processes': {
|
||||||
|
@ -107,11 +121,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scan: {
|
scan: {
|
||||||
|
@ -119,11 +135,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runscript: {
|
runscript: {
|
||||||
|
@ -131,11 +149,13 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: true,
|
crowdstrike: true,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ export * from './trusted_apps';
|
||||||
export * from './utility_types';
|
export * from './utility_types';
|
||||||
export * from './agents';
|
export * from './agents';
|
||||||
export * from './sentinel_one';
|
export * from './sentinel_one';
|
||||||
|
export * from './microsoft_defender_endpoint';
|
||||||
export type { ConditionEntriesMap, ConditionEntry } from './exception_list_items';
|
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
|
* Enables the Asset Inventory feature
|
||||||
*/
|
*/
|
||||||
assetInventoryUXEnabled: false,
|
assetInventoryUXEnabled: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled Microsoft Defender for Endpoint actions client
|
||||||
|
*/
|
||||||
|
responseActionsMSDefenderEndpointEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||||
|
|
|
@ -480,10 +480,12 @@ components:
|
||||||
- minLength: 1
|
- minLength: 1
|
||||||
type: string
|
type: string
|
||||||
AgentTypes:
|
AgentTypes:
|
||||||
|
description: The host agent type (optional). Defaults to endpoint.
|
||||||
enum:
|
enum:
|
||||||
- endpoint
|
- endpoint
|
||||||
- sentinel_one
|
- sentinel_one
|
||||||
- crowdstrike
|
- crowdstrike
|
||||||
|
- microsoft_defender_endpoint
|
||||||
type: string
|
type: string
|
||||||
AlertIds:
|
AlertIds:
|
||||||
description: A list of alerts ids.
|
description: A list of alerts ids.
|
||||||
|
|
|
@ -480,10 +480,12 @@ components:
|
||||||
- minLength: 1
|
- minLength: 1
|
||||||
type: string
|
type: string
|
||||||
AgentTypes:
|
AgentTypes:
|
||||||
|
description: The host agent type (optional). Defaults to endpoint.
|
||||||
enum:
|
enum:
|
||||||
- endpoint
|
- endpoint
|
||||||
- sentinel_one
|
- sentinel_one
|
||||||
- crowdstrike
|
- crowdstrike
|
||||||
|
- microsoft_defender_endpoint
|
||||||
type: string
|
type: string
|
||||||
AlertIds:
|
AlertIds:
|
||||||
description: A list of alerts ids.
|
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;
|
props.agentType = agentType;
|
||||||
const { getByTitle } = render();
|
const { getByTitle } = render();
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,12 @@ describe('use responder action data hooks', () => {
|
||||||
expect(onClickMock).not.toHaveBeenCalled();
|
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',
|
'should show action disabled with tooltip for %s if agent id field is missing',
|
||||||
(agentType) => {
|
(agentType) => {
|
||||||
const agentTypeField = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
|
const agentTypeField = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
|
||||||
|
|
|
@ -76,16 +76,15 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
|
||||||
appContextMock.renderHook(() => useAlertResponseActionsSupport(alertDetailItemData));
|
appContextMock.renderHook(() => useAlertResponseActionsSupport(alertDetailItemData));
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
it.each(
|
||||||
'should return expected response for agentType: `%s`',
|
// FIXME:PT temporary change. Tests for MS defender will be in PR https://github.com/elastic/kibana/pull/205012
|
||||||
(agentType) => {
|
RESPONSE_ACTION_AGENT_TYPE.filter((agentType) => agentType !== 'microsoft_defender_endpoint')
|
||||||
alertDetailItemData =
|
)('should return expected response for agentType: `%s`', (agentType) => {
|
||||||
endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
||||||
const { result } = renderHook();
|
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', () => {
|
it('should set `isSupported` to `false` if no alert details item data is provided', () => {
|
||||||
alertDetailItemData = [];
|
alertDetailItemData = [];
|
||||||
|
@ -178,7 +177,9 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(
|
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'>
|
Exclude<ResponseActionAgentType, 'endpoint'>
|
||||||
>
|
>
|
||||||
)('should set `isSupported` to `false` for [%s] if feature flag is disabled', (agentType) => {
|
)('should set `isSupported` to `false` for [%s] if feature flag is disabled', (agentType) => {
|
||||||
|
|
|
@ -13,7 +13,11 @@ import {
|
||||||
import { getEventDetailsAgentIdField, parseEcsFieldPath } from '..';
|
import { getEventDetailsAgentIdField, parseEcsFieldPath } from '..';
|
||||||
|
|
||||||
describe('getEventDetailsAgentIdField()', () => {
|
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 field = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
|
||||||
const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType);
|
||||||
|
|
||||||
|
@ -25,17 +29,16 @@ describe('getEventDetailsAgentIdField()', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(RESPONSE_ACTION_AGENT_TYPE)(
|
it.each(
|
||||||
'should include a field when agent id is not found: %s',
|
RESPONSE_ACTION_AGENT_TYPE
|
||||||
(agentType) => {
|
// 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 field = RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS[agentType][0];
|
||||||
const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(
|
const eventDetails = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(agentType, {
|
||||||
agentType,
|
|
||||||
{
|
|
||||||
'event.dataset': { values: ['foo'], originalValue: ['foo'] },
|
'event.dataset': { values: ['foo'], originalValue: ['foo'] },
|
||||||
[field]: undefined,
|
[field]: undefined,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(getEventDetailsAgentIdField(agentType, eventDetails)).toEqual({
|
expect(getEventDetailsAgentIdField(agentType, eventDetails)).toEqual({
|
||||||
found: false,
|
found: false,
|
||||||
|
@ -43,8 +46,7 @@ describe('getEventDetailsAgentIdField()', () => {
|
||||||
field,
|
field,
|
||||||
agentId: '',
|
agentId: '',
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it.each(RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one)(
|
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',
|
'should return field [%s] for sentinelone when agent is not found and event.dataset matches',
|
||||||
|
|
|
@ -177,6 +177,9 @@ const useTypesFilterInitialState = ({
|
||||||
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
|
const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled(
|
||||||
'responseActionsSentinelOneV1Enabled'
|
'responseActionsSentinelOneV1Enabled'
|
||||||
);
|
);
|
||||||
|
const isMicrosoftDefenderEnabled = useIsExperimentalFeatureEnabled(
|
||||||
|
'responseActionsMSDefenderEndpointEnabled'
|
||||||
|
);
|
||||||
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
||||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||||
);
|
);
|
||||||
|
@ -207,14 +210,21 @@ const useTypesFilterInitialState = ({
|
||||||
|
|
||||||
// v8.13 onwards
|
// v8.13 onwards
|
||||||
// for showing agent types and action types in the same filter
|
// for showing agent types and action types in the same filter
|
||||||
if (isSentinelOneV1Enabled || isCrowdstrikeEnabled) {
|
if (isSentinelOneV1Enabled || isCrowdstrikeEnabled || isMicrosoftDefenderEnabled) {
|
||||||
if (!isFlyout) {
|
if (!isFlyout) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: FILTER_NAMES.agentTypes,
|
label: FILTER_NAMES.agentTypes,
|
||||||
isGroupLabel: true,
|
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({
|
getFilterOptions({
|
||||||
key: type,
|
key: type,
|
||||||
label: getAgentTypeName(type),
|
label: getAgentTypeName(type),
|
||||||
|
|
|
@ -1877,6 +1877,7 @@ describe('Response actions history', () => {
|
||||||
mockedContext.setExperimentalFlag({
|
mockedContext.setExperimentalFlag({
|
||||||
responseActionsSentinelOneV1Enabled: true,
|
responseActionsSentinelOneV1Enabled: true,
|
||||||
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
||||||
|
responseActionsMSDefenderEndpointEnabled: true,
|
||||||
});
|
});
|
||||||
render({ isFlyout: false });
|
render({ isFlyout: false });
|
||||||
const { getByTestId, getAllByTestId } = renderResult;
|
const { getByTestId, getAllByTestId } = renderResult;
|
||||||
|
@ -1891,6 +1892,7 @@ describe('Response actions history', () => {
|
||||||
'Elastic Defend',
|
'Elastic Defend',
|
||||||
'SentinelOne',
|
'SentinelOne',
|
||||||
'Crowdstrike',
|
'Crowdstrike',
|
||||||
|
'microsoft_defender_endpoint',
|
||||||
'Triggered by rule',
|
'Triggered by rule',
|
||||||
'Triggered manually',
|
'Triggered manually',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -70,6 +70,9 @@ export const ResponseActionsLog = memo<
|
||||||
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
const isCrowdstrikeEnabled = useIsExperimentalFeatureEnabled(
|
||||||
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
'responseActionsCrowdstrikeManualHostIsolationEnabled'
|
||||||
);
|
);
|
||||||
|
const isMicrosoftDefenderEnabled = useIsExperimentalFeatureEnabled(
|
||||||
|
'responseActionsMSDefenderEndpointEnabled'
|
||||||
|
);
|
||||||
|
|
||||||
// Used to decide if display global loader or not (only the fist time tha page loads)
|
// Used to decide if display global loader or not (only the fist time tha page loads)
|
||||||
const [isFirstAttempt, setIsFirstAttempt] = useState(true);
|
const [isFirstAttempt, setIsFirstAttempt] = useState(true);
|
||||||
|
@ -92,7 +95,7 @@ export const ResponseActionsLog = memo<
|
||||||
setQueryParams((prevState) => ({
|
setQueryParams((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
agentTypes:
|
agentTypes:
|
||||||
isSentinelOneV1Enabled || isCrowdstrikeEnabled
|
isSentinelOneV1Enabled || isCrowdstrikeEnabled || isMicrosoftDefenderEnabled
|
||||||
? agentTypesFromUrl?.length
|
? agentTypesFromUrl?.length
|
||||||
? agentTypesFromUrl
|
? agentTypesFromUrl
|
||||||
: prevState.agentTypes
|
: prevState.agentTypes
|
||||||
|
@ -121,6 +124,7 @@ export const ResponseActionsLog = memo<
|
||||||
isFlyout,
|
isFlyout,
|
||||||
isCrowdstrikeEnabled,
|
isCrowdstrikeEnabled,
|
||||||
isSentinelOneV1Enabled,
|
isSentinelOneV1Enabled,
|
||||||
|
isMicrosoftDefenderEnabled,
|
||||||
statusesFromUrl,
|
statusesFromUrl,
|
||||||
setQueryParams,
|
setQueryParams,
|
||||||
usersFromUrl,
|
usersFromUrl,
|
||||||
|
|
|
@ -115,13 +115,26 @@ describe('CompleteExternalTaskRunner class', () => {
|
||||||
|
|
||||||
expect(esClientMock.bulk).toHaveBeenCalledWith({
|
expect(esClientMock.bulk).toHaveBeenCalledWith({
|
||||||
index: ENDPOINT_ACTION_RESPONSES_INDEX,
|
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: [
|
operations: [
|
||||||
|
// for SentinelOne
|
||||||
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
'@timestamp': expect.any(String),
|
'@timestamp': expect.any(String),
|
||||||
EndpointActions: expect.any(Object),
|
EndpointActions: expect.any(Object),
|
||||||
agent: 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 } },
|
{ create: { _index: ENDPOINT_ACTION_RESPONSES_INDEX } },
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
'@timestamp': expect.any(String),
|
'@timestamp': expect.any(String),
|
||||||
|
|
|
@ -1277,4 +1277,102 @@ describe('Response actions', () => {
|
||||||
expect(httpResponseMock.ok).toHaveBeenCalled();
|
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 (
|
return (
|
||||||
(agentType === 'sentinel_one' && !experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
|
(agentType === 'sentinel_one' && !experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
|
||||||
(agentType === 'crowdstrike' &&
|
(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,
|
endpoint: true,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
execute: {
|
execute: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
'running-processes': {
|
'running-processes': {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: true,
|
sentinel_one: true,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
scan: {
|
scan: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
isolate: {
|
isolate: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
unisolate: {
|
unisolate: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
'kill-process': {
|
'kill-process': {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
'suspend-process': {
|
'suspend-process': {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
runscript: {
|
runscript: {
|
||||||
endpoint: false,
|
endpoint: false,
|
||||||
sentinel_one: false,
|
sentinel_one: false,
|
||||||
crowdstrike: false,
|
crowdstrike: false,
|
||||||
|
microsoft_defender_endpoint: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import type { ResponseActionsClient } from '../..';
|
||||||
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
|
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||||
import { stringify } from '../../../utils/stringify';
|
import { stringify } from '../../../utils/stringify';
|
||||||
import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error';
|
import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error';
|
||||||
|
@ -31,7 +32,7 @@ export class ResponseActionsClientError extends CustomHttpRequestError {
|
||||||
|
|
||||||
export class ResponseActionsNotSupportedError extends ResponseActionsClientError {
|
export class ResponseActionsNotSupportedError extends ResponseActionsClientError {
|
||||||
constructor(
|
constructor(
|
||||||
responseAction?: ResponseActionsApiCommandNames,
|
responseAction?: ResponseActionsApiCommandNames | keyof ResponseActionsClient,
|
||||||
statusCode: number = 405,
|
statusCode: number = 405,
|
||||||
meta?: unknown
|
meta?: unknown
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { UnsupportedResponseActionsAgentTypeError } from './errors';
|
||||||
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||||
import type { CrowdstrikeActionsClientOptions } from './crowdstrike/crowdstrike_actions_client';
|
import type { CrowdstrikeActionsClientOptions } from './crowdstrike/crowdstrike_actions_client';
|
||||||
import { CrowdstrikeActionsClient } 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 &
|
export type GetResponseActionsClientConstructorOptions = ResponseActionsClientOptions &
|
||||||
SentinelOneActionsClientOptions &
|
SentinelOneActionsClientOptions &
|
||||||
|
@ -37,6 +38,8 @@ export const getResponseActionsClient = (
|
||||||
return new SentinelOneActionsClient(constructorOptions);
|
return new SentinelOneActionsClient(constructorOptions);
|
||||||
case 'crowdstrike':
|
case 'crowdstrike':
|
||||||
return new CrowdstrikeActionsClient(constructorOptions);
|
return new CrowdstrikeActionsClient(constructorOptions);
|
||||||
|
case 'microsoft_defender_endpoint':
|
||||||
|
return new MicrosoftDefenderEndpointActionsClient(constructorOptions);
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedResponseActionsAgentTypeError(
|
throw new UnsupportedResponseActionsAgentTypeError(
|
||||||
`Agent type [${agentType}] does not support response actions`
|
`Agent type [${agentType}] does not support response actions`
|
||||||
|
|
|
@ -112,6 +112,8 @@ describe('ResponseActionsClientImpl base class', () => {
|
||||||
'getFile',
|
'getFile',
|
||||||
'execute',
|
'execute',
|
||||||
'upload',
|
'upload',
|
||||||
|
'getFileDownload',
|
||||||
|
'getFileInfo',
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(methods)('should throw Not Supported error for %s()', async (method) => {
|
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.toBeInstanceOf(ResponseActionsNotSupportedError);
|
||||||
await expect(responsePromise).rejects.toHaveProperty('statusCode', 405);
|
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()', () => {
|
describe('#updateCases()', () => {
|
||||||
|
@ -836,7 +827,13 @@ class MockClassWithExposedProtectedMembers extends ResponseActionsClientImpl {
|
||||||
return super.writeActionResponseToEndpointIndex<TOutputContent>(options);
|
return super.writeActionResponseToEndpointIndex<TOutputContent>(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAllPendingActions(): AsyncIterable<ResponseActionsClientPendingAction[]> {
|
public fetchAllPendingActions<
|
||||||
return super.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 esClient = this.options.esClient;
|
||||||
const query: QueryDslQueryContainer = {
|
const query: QueryDslQueryContainer = {
|
||||||
bool: {
|
bool: {
|
||||||
|
@ -861,10 +867,10 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
||||||
actionId: string,
|
actionId: string,
|
||||||
fileId: string
|
fileId: string
|
||||||
): Promise<GetFileDownloadMethodResponse> {
|
): Promise<GetFileDownloadMethodResponse> {
|
||||||
throw new ResponseActionsClientError(`Method getFileDownload() not implemented`, 501);
|
throw new ResponseActionsNotSupportedError('getFileDownload');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileInfo(actionId: string, fileId: string): Promise<UploadedFileInfo> {
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import type { ActionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
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 { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
||||||
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
|
import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
|
||||||
|
@ -57,6 +59,9 @@ export interface ResponseActionsClientOptionsMock extends ResponseActionsClientO
|
||||||
casesClient?: CasesClientMock;
|
casesClient?: CasesClientMock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NormalizedExternalConnectorClientMock =
|
||||||
|
DeeplyMockedKeys<NormalizedExternalConnectorClient>;
|
||||||
|
|
||||||
const createResponseActionClientMock = (): jest.Mocked<ResponseActionsClient> => {
|
const createResponseActionClientMock = (): jest.Mocked<ResponseActionsClient> => {
|
||||||
return {
|
return {
|
||||||
suspendProcess: jest.fn().mockReturnValue(Promise.resolve()),
|
suspendProcess: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
@ -305,7 +310,7 @@ const createConnectorActionsClientMock = ({
|
||||||
|
|
||||||
const createNormalizedExternalConnectorClientMock = (
|
const createNormalizedExternalConnectorClientMock = (
|
||||||
connectorActionsClientMock: ActionsClientMock = createConnectorActionsClientMock()
|
connectorActionsClientMock: ActionsClientMock = createConnectorActionsClientMock()
|
||||||
): DeeplyMockedKeys<NormalizedExternalConnectorClient> => {
|
): NormalizedExternalConnectorClientMock => {
|
||||||
const normalizedClient = new NormalizedExternalConnectorClient(
|
const normalizedClient = new NormalizedExternalConnectorClient(
|
||||||
connectorActionsClientMock,
|
connectorActionsClientMock,
|
||||||
loggingSystemMock.createLogger()
|
loggingSystemMock.createLogger()
|
||||||
|
@ -314,7 +319,31 @@ const createNormalizedExternalConnectorClientMock = (
|
||||||
jest.spyOn(normalizedClient, 'execute');
|
jest.spyOn(normalizedClient, 'execute');
|
||||||
jest.spyOn(normalizedClient, 'setup');
|
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({
|
export const responseActionsClientMock = Object.freeze({
|
||||||
|
@ -341,4 +370,5 @@ export const responseActionsClientMock = Object.freeze({
|
||||||
/** Create a mock connector instance */
|
/** Create a mock connector instance */
|
||||||
createConnector: createConnectorMock,
|
createConnector: createConnectorMock,
|
||||||
createConnectorActionExecuteResponse: createConnectorActionExecuteResponseMock,
|
createConnectorActionExecuteResponse: createConnectorActionExecuteResponseMock,
|
||||||
|
setConnectorActionsClientExecuteResponse: setConnectorActionsClientExecuteResponseMock,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue