mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[EDR Workflows] Complete Crowdstrike actions (#186522)
This commit is contained in:
parent
606c695866
commit
7ce69fc52c
3 changed files with 186 additions and 105 deletions
|
@ -13,9 +13,13 @@ import { ResponseActionsNotSupportedError } from '../errors';
|
|||
import type { CrowdstrikeActionsClientOptionsMock } from './mocks';
|
||||
import { CrowdstrikeMock } from './mocks';
|
||||
|
||||
import { ENDPOINT_ACTIONS_INDEX } from '../../../../../../common/endpoint/constants';
|
||||
import {
|
||||
ENDPOINT_ACTION_RESPONSES_INDEX,
|
||||
ENDPOINT_ACTIONS_INDEX,
|
||||
} from '../../../../../../common/endpoint/constants';
|
||||
import { SUB_ACTION } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
|
||||
import type { NormalizedExternalConnectorClient } from '../../..';
|
||||
|
||||
jest.mock('../../action_details_by_id', () => {
|
||||
const originalMod = jest.requireActual('../../action_details_by_id');
|
||||
|
||||
|
@ -75,6 +79,48 @@ describe('CrowdstrikeActionsClient class', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should save response with error in case of actionResponse containing errors', async () => {
|
||||
// mock execute of CS action to return error
|
||||
const actionResponse = {
|
||||
data: {
|
||||
errors: [{ message: 'error message' }],
|
||||
},
|
||||
};
|
||||
(connectorActionsMock.execute as jest.Mock).mockResolvedValueOnce(actionResponse);
|
||||
|
||||
await crowdstrikeActionsClient.isolate(
|
||||
createCrowdstrikeIsolationOptions({ actionId: '123-345-567' })
|
||||
);
|
||||
expect(classConstructorOptions.esClient.index.mock.calls[1][0]).toEqual({
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
agent: { id: ['1-2-3'] },
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
completed_at: expect.any(String),
|
||||
started_at: expect.any(String),
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'test comment',
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
},
|
||||
},
|
||||
input_type: 'crowdstrike',
|
||||
},
|
||||
error: {
|
||||
code: '500',
|
||||
message: 'Crowdstrike action failed: error message',
|
||||
},
|
||||
meta: undefined,
|
||||
},
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#isolate()`, () => {
|
||||
it('should send action to Crowdstrike', async () => {
|
||||
await crowdstrikeActionsClient.isolate(
|
||||
|
@ -99,40 +145,61 @@ describe('CrowdstrikeActionsClient class', () => {
|
|||
it('should write action request to endpoint indexes', async () => {
|
||||
await crowdstrikeActionsClient.isolate(createCrowdstrikeIsolationOptions());
|
||||
|
||||
// we do not write response to es yet
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(1);
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'test comment',
|
||||
parameters: undefined,
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2);
|
||||
expect(classConstructorOptions.esClient.index.mock.calls[0][0]).toEqual({
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'test comment',
|
||||
parameters: undefined,
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
},
|
||||
expiration: expect.any(String),
|
||||
input_type: 'crowdstrike',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
agent: { id: ['1-2-3'] },
|
||||
meta: {
|
||||
hostName: 'Crowdstrike-1460',
|
||||
},
|
||||
user: { id: 'foo' },
|
||||
expiration: expect.any(String),
|
||||
input_type: 'crowdstrike',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
refresh: 'wait_for',
|
||||
agent: { id: ['1-2-3'] },
|
||||
meta: {
|
||||
hostName: 'Crowdstrike-1460',
|
||||
},
|
||||
user: { id: 'foo' },
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
expect(classConstructorOptions.esClient.index.mock.calls[1][0]).toEqual({
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
agent: { id: ['1-2-3'] },
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
completed_at: expect.any(String),
|
||||
started_at: expect.any(String),
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'test comment',
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
parameters: undefined,
|
||||
},
|
||||
},
|
||||
input_type: 'crowdstrike',
|
||||
error: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
},
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return action details', async () => {
|
||||
|
@ -174,40 +241,61 @@ describe('CrowdstrikeActionsClient class', () => {
|
|||
it('should write action request to endpoint indexes', async () => {
|
||||
await crowdstrikeActionsClient.release(createCrowdstrikeIsolationOptions());
|
||||
|
||||
// we do not write response to es yet
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(1);
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: 'unisolate',
|
||||
comment: 'test comment',
|
||||
parameters: undefined,
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
expect(classConstructorOptions.esClient.index).toHaveBeenCalledTimes(2);
|
||||
expect(classConstructorOptions.esClient.index.mock.calls[0][0]).toEqual({
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
data: {
|
||||
command: 'unisolate',
|
||||
comment: 'test comment',
|
||||
parameters: undefined,
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
},
|
||||
expiration: expect.any(String),
|
||||
input_type: 'crowdstrike',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
agent: { id: ['1-2-3'] },
|
||||
meta: {
|
||||
hostName: 'Crowdstrike-1460',
|
||||
},
|
||||
user: { id: 'foo' },
|
||||
expiration: expect.any(String),
|
||||
input_type: 'crowdstrike',
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
refresh: 'wait_for',
|
||||
agent: { id: ['1-2-3'] },
|
||||
meta: {
|
||||
hostName: 'Crowdstrike-1460',
|
||||
},
|
||||
user: { id: 'foo' },
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
index: ENDPOINT_ACTIONS_INDEX,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
expect(classConstructorOptions.esClient.index.mock.calls[1][0]).toEqual({
|
||||
document: {
|
||||
'@timestamp': expect.any(String),
|
||||
agent: { id: ['1-2-3'] },
|
||||
EndpointActions: {
|
||||
action_id: expect.any(String),
|
||||
completed_at: expect.any(String),
|
||||
started_at: expect.any(String),
|
||||
data: {
|
||||
command: 'unisolate',
|
||||
comment: 'test comment',
|
||||
hosts: {
|
||||
'1-2-3': {
|
||||
name: 'Crowdstrike-1460',
|
||||
},
|
||||
},
|
||||
parameters: undefined,
|
||||
},
|
||||
input_type: 'crowdstrike',
|
||||
},
|
||||
error: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
index: ENDPOINT_ACTION_RESPONSES_INDEX,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return action details', async () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
} from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { CrowdstrikeBaseApiResponse } from '@kbn/stack-connectors-plugin/common/crowdstrike/types';
|
||||
import type { CrowdstrikeActionRequestCommonMeta } from '../../../../../../common/endpoint/types/crowdstrike';
|
||||
import type {
|
||||
CommonResponseActionMethodOptions,
|
||||
|
@ -159,45 +160,6 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO TC: uncomment when working on agent status support
|
||||
// private async getAgentDetails(
|
||||
// id: string
|
||||
// ): Promise<CrowdstrikeGetAgentsResponse['resources'][number]> {
|
||||
// const executeOptions: NormalizedExternalConnectorClientExecuteOptions<
|
||||
// CrowdstrikeGetAgentsParams,
|
||||
// SUB_ACTION
|
||||
// > = {
|
||||
// params: {
|
||||
// subAction: SUB_ACTION.GET_AGENT_DETAILS,
|
||||
// subActionParams: {
|
||||
// ids: [id],
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
// let crowdstrikeApiResponse: CrowdstrikeGetAgentsResponse | undefined;
|
||||
|
||||
// try {
|
||||
// const response = await this.connectorActionsClient.execute(executeOptions);
|
||||
|
||||
// this.log.debug(`Response for Crowdstrike agent id [${id}] returned:\n${stringify(response)}`);
|
||||
|
||||
// crowdstrikeApiResponse = response.data;
|
||||
// } catch (err) {
|
||||
// throw new ResponseActionsClientError(
|
||||
// `Error while attempting to retrieve Crowdstrike host with agent id [${id}]`,
|
||||
// 500,
|
||||
// err
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (!crowdstrikeApiResponse || !crowdstrikeApiResponse.resources[0]) {
|
||||
// throw new ResponseActionsClientError(`Crowdstrike agent id [${id}] not found`, 404);
|
||||
// }
|
||||
|
||||
// return crowdstrikeApiResponse.resources[0];
|
||||
// }
|
||||
|
||||
protected async validateRequest(
|
||||
payload: ResponseActionsClientWriteActionRequestToEndpointIndexOptions
|
||||
): Promise<ResponseActionsClientValidateRequestResponse> {
|
||||
|
@ -224,17 +186,16 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
...this.getMethodOptions(options),
|
||||
command: 'isolate',
|
||||
};
|
||||
|
||||
let actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse> | undefined;
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
const actionCommentMessage = ELASTIC_RESPONSE_ACTION_MESSAGE(
|
||||
this.options.username,
|
||||
reqIndexOptions.actionId
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
await this.sendAction(SUB_ACTION.HOST_ACTIONS, {
|
||||
actionResponse = (await this.sendAction(SUB_ACTION.HOST_ACTIONS, {
|
||||
ids: actionRequest.endpoint_ids,
|
||||
actionParameters: {
|
||||
comment: reqIndexOptions.comment
|
||||
|
@ -242,7 +203,7 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
: actionCommentMessage,
|
||||
},
|
||||
command: 'contain',
|
||||
});
|
||||
})) as ActionTypeExecutorResult<CrowdstrikeBaseApiResponse>;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
@ -257,6 +218,11 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
|
||||
const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions);
|
||||
|
||||
// Ensure actionResponse is assigned before using it
|
||||
if (actionResponse) {
|
||||
await this.completeCrowdstrikeAction(actionResponse, actionRequestDoc);
|
||||
}
|
||||
|
||||
await this.updateCases({
|
||||
command: reqIndexOptions.command,
|
||||
caseIds: reqIndexOptions.case_ids,
|
||||
|
@ -284,6 +250,7 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
command: 'unisolate',
|
||||
};
|
||||
|
||||
let actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse> | undefined;
|
||||
if (!reqIndexOptions.error) {
|
||||
let error = (await this.validateRequest(reqIndexOptions)).error;
|
||||
const actionCommentMessage = ELASTIC_RESPONSE_ACTION_MESSAGE(
|
||||
|
@ -292,13 +259,13 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
);
|
||||
if (!error) {
|
||||
try {
|
||||
await this.sendAction(SUB_ACTION.HOST_ACTIONS, {
|
||||
actionResponse = (await this.sendAction(SUB_ACTION.HOST_ACTIONS, {
|
||||
ids: actionRequest.endpoint_ids,
|
||||
command: 'lift_containment',
|
||||
comment: reqIndexOptions.comment
|
||||
? `${actionCommentMessage}: ${reqIndexOptions.comment}`
|
||||
: actionCommentMessage,
|
||||
});
|
||||
})) as ActionTypeExecutorResult<CrowdstrikeBaseApiResponse>;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
@ -313,6 +280,11 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
|
||||
const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions);
|
||||
|
||||
// Ensure actionResponse is assigned before using it
|
||||
if (actionResponse) {
|
||||
await this.completeCrowdstrikeAction(actionResponse, actionRequestDoc);
|
||||
}
|
||||
|
||||
await this.updateCases({
|
||||
command: reqIndexOptions.command,
|
||||
caseIds: reqIndexOptions.case_ids,
|
||||
|
@ -330,6 +302,27 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl {
|
|||
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
|
||||
}
|
||||
|
||||
private async completeCrowdstrikeAction(
|
||||
actionResponse: ActionTypeExecutorResult<CrowdstrikeBaseApiResponse> | undefined,
|
||||
doc: LogsEndpointAction
|
||||
): Promise<void> {
|
||||
const options = {
|
||||
actionId: doc.EndpointActions.action_id,
|
||||
agentId: doc.agent.id,
|
||||
data: doc.EndpointActions.data,
|
||||
...(actionResponse?.data?.errors?.length
|
||||
? {
|
||||
error: {
|
||||
code: '500',
|
||||
message: `Crowdstrike action failed: ${actionResponse.data.errors[0].message}`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
await this.writeActionResponseToEndpointIndex(options);
|
||||
}
|
||||
|
||||
async processPendingActions({
|
||||
abortSignal,
|
||||
addToQueue,
|
||||
|
|
|
@ -135,7 +135,7 @@ export class CrowdstrikeAgentStatusClient extends AgentStatusClient {
|
|||
const agentStatuses = await this.getAgentStatusFromConnectorAction(agentIds);
|
||||
|
||||
return agentIds.reduce<AgentStatusRecords>((acc, agentId) => {
|
||||
const agentInfo = mostRecentAgentInfosByAgentId[agentId].crowdstrike;
|
||||
const agentInfo = mostRecentAgentInfosByAgentId[agentId]?.crowdstrike;
|
||||
|
||||
const agentStatus = agentStatuses[agentId];
|
||||
const pendingActions = allPendingActions.find(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue