mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [OLM] Endpoint pending actions API (#101269)
This commit is contained in:
parent
e4f74471ec
commit
747b80b58f
10 changed files with 592 additions and 21 deletions
|
@ -36,3 +36,4 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`;
|
|||
|
||||
/** Endpoint Actions Log Routes */
|
||||
export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`;
|
||||
export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`;
|
||||
|
|
|
@ -28,3 +28,12 @@ export const EndpointActionLogRequestSchema = {
|
|||
agent_id: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const ActionStatusRequestSchema = {
|
||||
query: schema.object({
|
||||
agent_ids: schema.oneOf([
|
||||
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1, maxSize: 50 }),
|
||||
schema.string({ minLength: 1 }),
|
||||
]),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -10,6 +10,11 @@ import { HostIsolationRequestSchema } from '../schema/actions';
|
|||
|
||||
export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
|
||||
|
||||
export interface EndpointActionData {
|
||||
command: ISOLATION_ACTIONS;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface EndpointAction {
|
||||
action_id: string;
|
||||
'@timestamp': string;
|
||||
|
@ -18,10 +23,7 @@ export interface EndpointAction {
|
|||
input_type: 'endpoint';
|
||||
agents: string[];
|
||||
user_id: string;
|
||||
data: {
|
||||
command: ISOLATION_ACTIONS;
|
||||
comment?: string;
|
||||
};
|
||||
data: EndpointActionData;
|
||||
}
|
||||
|
||||
export interface EndpointActionResponse {
|
||||
|
@ -32,11 +34,8 @@ export interface EndpointActionResponse {
|
|||
agent_id: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
error: string;
|
||||
action_data: {
|
||||
command: ISOLATION_ACTIONS;
|
||||
comment?: string;
|
||||
};
|
||||
error?: string;
|
||||
action_data: EndpointActionData;
|
||||
}
|
||||
|
||||
export type HostIsolationRequestBody = TypeOf<typeof HostIsolationRequestSchema.body>;
|
||||
|
@ -44,3 +43,10 @@ export type HostIsolationRequestBody = TypeOf<typeof HostIsolationRequestSchema.
|
|||
export interface HostIsolationResponse {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface PendingActionsResponse {
|
||||
agent_id: string;
|
||||
pending_actions: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,5 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
import { registerHostIsolationRoutes } from './isolation';
|
||||
import { registerActionStatusRoutes } from './status';
|
||||
import { registerActionAuditLogRoutes } from './audit_log';
|
||||
|
||||
export * from './isolation';
|
||||
export * from './audit_log';
|
||||
|
||||
// wrap route registration
|
||||
|
||||
export function registerActionRoutes(
|
||||
router: SecuritySolutionPluginRouter,
|
||||
endpointContext: EndpointAppContext
|
||||
) {
|
||||
registerHostIsolationRoutes(router, endpointContext);
|
||||
registerActionStatusRoutes(router, endpointContext);
|
||||
registerActionAuditLogRoutes(router, endpointContext);
|
||||
}
|
||||
|
|
|
@ -94,7 +94,6 @@ describe('Host Isolation', () => {
|
|||
},
|
||||
])
|
||||
);
|
||||
endpointAppContextService.start({ ...startContract, packageService: mockPackageService });
|
||||
licenseEmitter = new Subject();
|
||||
licenseService = new LicenseService();
|
||||
licenseService.start(licenseEmitter);
|
||||
|
|
|
@ -117,7 +117,7 @@ export const isolationRequestHandler = function (
|
|||
const actionID = uuid.v4();
|
||||
let result;
|
||||
try {
|
||||
result = await esClient.index({
|
||||
result = await esClient.index<EndpointAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: {
|
||||
action_id: actionID,
|
||||
|
@ -126,12 +126,12 @@ export const isolationRequestHandler = function (
|
|||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: agentIDs,
|
||||
user_id: user?.username,
|
||||
user_id: user!.username,
|
||||
data: {
|
||||
command: isolate ? 'isolate' : 'unisolate',
|
||||
comment: req.body.comment,
|
||||
comment: req.body.comment ?? undefined,
|
||||
},
|
||||
} as EndpointAction,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return res.customError({
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable @typescript-eslint/no-useless-constructor */
|
||||
|
||||
import { ApiResponse } from '@elastic/elasticsearch';
|
||||
import moment from 'moment';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
EndpointAction,
|
||||
EndpointActionResponse,
|
||||
ISOLATION_ACTIONS,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
||||
export const mockSearchResult = (results: any = []): ApiResponse<any> => {
|
||||
return {
|
||||
body: {
|
||||
hits: {
|
||||
hits: results.map((a: any) => ({
|
||||
_source: a,
|
||||
})),
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {} as any,
|
||||
};
|
||||
};
|
||||
|
||||
export class MockAction {
|
||||
private actionID: string = uuid.v4();
|
||||
private ts: moment.Moment = moment();
|
||||
private user: string = '';
|
||||
private agents: string[] = [];
|
||||
private command: ISOLATION_ACTIONS = 'isolate';
|
||||
private comment?: string;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public build(): EndpointAction {
|
||||
return {
|
||||
action_id: this.actionID,
|
||||
'@timestamp': this.ts.toISOString(),
|
||||
expiration: this.ts.add(2, 'weeks').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: this.agents,
|
||||
user_id: this.user,
|
||||
data: {
|
||||
command: this.command,
|
||||
comment: this.comment,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public fromUser(u: string) {
|
||||
this.user = u;
|
||||
return this;
|
||||
}
|
||||
public withAgents(a: string[]) {
|
||||
this.agents = a;
|
||||
return this;
|
||||
}
|
||||
public withAgent(a: string) {
|
||||
this.agents = [a];
|
||||
return this;
|
||||
}
|
||||
public withComment(c: string) {
|
||||
this.comment = c;
|
||||
return this;
|
||||
}
|
||||
public withAction(a: ISOLATION_ACTIONS) {
|
||||
this.command = a;
|
||||
return this;
|
||||
}
|
||||
public atTime(m: moment.Moment | Date) {
|
||||
if (m instanceof Date) {
|
||||
this.ts = moment(m);
|
||||
} else {
|
||||
this.ts = m;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public withID(id: string) {
|
||||
this.actionID = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const aMockAction = (): MockAction => {
|
||||
return new MockAction();
|
||||
};
|
||||
|
||||
export class MockResponse {
|
||||
private actionID: string = uuid.v4();
|
||||
private ts: moment.Moment = moment();
|
||||
private started: moment.Moment = moment();
|
||||
private completed: moment.Moment = moment();
|
||||
private agent: string = '';
|
||||
private command: ISOLATION_ACTIONS = 'isolate';
|
||||
private comment?: string;
|
||||
private error?: string;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public build(): EndpointActionResponse {
|
||||
return {
|
||||
'@timestamp': this.ts.toISOString(),
|
||||
action_id: this.actionID,
|
||||
agent_id: this.agent,
|
||||
started_at: this.started.toISOString(),
|
||||
completed_at: this.completed.toISOString(),
|
||||
error: this.error,
|
||||
action_data: {
|
||||
command: this.command,
|
||||
comment: this.comment,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public forAction(id: string) {
|
||||
this.actionID = id;
|
||||
return this;
|
||||
}
|
||||
public forAgent(id: string) {
|
||||
this.agent = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const aMockResponse = (actionID: string, agentID: string): MockResponse => {
|
||||
return new MockResponse().forAction(actionID).forAgent(agentID);
|
||||
};
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { KibanaResponseFactory, RequestHandler, RouteConfig } from 'kibana/server';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
httpServiceMock,
|
||||
loggingSystemMock,
|
||||
savedObjectsClientMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { ActionStatusRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import {
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
createRouteHandlerContext,
|
||||
} from '../../mocks';
|
||||
import { registerActionStatusRoutes } from './status';
|
||||
import uuid from 'uuid';
|
||||
import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks';
|
||||
|
||||
describe('Endpoint Action Status', () => {
|
||||
describe('schema', () => {
|
||||
it('should require at least 1 agent ID', () => {
|
||||
expect(() => {
|
||||
ActionStatusRequestSchema.query.validate({}); // no agent_ids provided
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should accept a single agent ID', () => {
|
||||
expect(() => {
|
||||
ActionStatusRequestSchema.query.validate({ agent_ids: uuid.v4() });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept multiple agent IDs', () => {
|
||||
expect(() => {
|
||||
ActionStatusRequestSchema.query.validate({ agent_ids: [uuid.v4(), uuid.v4()] });
|
||||
}).not.toThrow();
|
||||
});
|
||||
it('should limit the maximum number of agent IDs', () => {
|
||||
const tooManyCooks = new Array(200).fill(uuid.v4()); // all the same ID string
|
||||
expect(() => {
|
||||
ActionStatusRequestSchema.query.validate({ agent_ids: tooManyCooks });
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('response', () => {
|
||||
let endpointAppContextService: EndpointAppContextService;
|
||||
|
||||
// convenience for calling the route and handler for action status
|
||||
let getPendingStatus: (reqParams?: any) => Promise<jest.Mocked<KibanaResponseFactory>>;
|
||||
// convenience for injecting mock responses for actions index and responses
|
||||
let havingActionsAndResponses: (actions: MockAction[], responses: any[]) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
const esClientMock = elasticsearchServiceMock.createScopedClusterClient();
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
endpointAppContextService = new EndpointAppContextService();
|
||||
endpointAppContextService.start(createMockEndpointAppContextServiceStartContract());
|
||||
|
||||
registerActionStatusRoutes(routerMock, {
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
getPendingStatus = async (reqParams?: any): Promise<jest.Mocked<KibanaResponseFactory>> => {
|
||||
const req = httpServerMock.createKibanaRequest(reqParams);
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
const [, routeHandler]: [
|
||||
RouteConfig<any, any, any, any>,
|
||||
RequestHandler<any, any, any, any>
|
||||
] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(ACTION_STATUS_ROUTE))!;
|
||||
await routeHandler(
|
||||
createRouteHandlerContext(esClientMock, savedObjectsClientMock.create()),
|
||||
req,
|
||||
mockResponse
|
||||
);
|
||||
|
||||
return mockResponse;
|
||||
};
|
||||
|
||||
havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => {
|
||||
esClientMock.asCurrentUser.search = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(mockSearchResult(actions.map((a) => a.build())))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(mockSearchResult(responses.map((r) => r.build())))
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
endpointAppContextService.stop();
|
||||
});
|
||||
|
||||
it('should include agent IDs in the output, even if they have no actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses([], []);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
});
|
||||
|
||||
it('should respond with a valid pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses([aMockAction().withAgent(mockID)], []);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
});
|
||||
it('should include a total count of a pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
aMockAction().withAgent(mockID).withAction('isolate'),
|
||||
aMockAction().withAgent(mockID).withAction('isolate'),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
2
|
||||
);
|
||||
});
|
||||
it('should show multiple pending actions, and their counts', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
aMockAction().withAgent(mockID).withAction('isolate'),
|
||||
aMockAction().withAgent(mockID).withAction('isolate'),
|
||||
aMockAction().withAgent(mockID).withAction('isolate'),
|
||||
aMockAction().withAgent(mockID).withAction('unisolate'),
|
||||
aMockAction().withAgent(mockID).withAction('unisolate'),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
3
|
||||
);
|
||||
expect(
|
||||
(response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.unisolate
|
||||
).toEqual(2);
|
||||
});
|
||||
it('should calculate correct pending counts from grouped/bulked actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
aMockAction()
|
||||
.withAgents([mockID, 'IRRELEVANT-OTHER-AGENT', 'ANOTHER-POSSIBLE-AGENT'])
|
||||
.withAction('isolate'),
|
||||
aMockAction().withAgents([mockID, 'YET-ANOTHER-AGENT-ID']).withAction('isolate'),
|
||||
aMockAction().withAgents(['YET-ANOTHER-AGENT-ID']).withAction('isolate'), // one WITHOUT our agent-under-test
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude actions that have responses from the pending count', async () => {
|
||||
const mockAgentID = 'XYZABC-000';
|
||||
const actionID = 'some-known-actionid';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
aMockAction().withAgent(mockAgentID).withAction('isolate'),
|
||||
aMockAction().withAgent(mockAgentID).withAction('isolate').withID(actionID),
|
||||
],
|
||||
[aMockResponse(actionID, mockAgentID)]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockAgentID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockAgentID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it('should have accurate counts for multiple agents, bulk actions, and responses', async () => {
|
||||
const agentOne = 'XYZABC-000';
|
||||
const agentTwo = 'DEADBEEF';
|
||||
const agentThree = 'IDIDIDID';
|
||||
|
||||
const actionTwoID = 'ID-TWO';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
aMockAction().withAgents([agentOne, agentTwo, agentThree]).withAction('isolate'),
|
||||
aMockAction()
|
||||
.withAgents([agentTwo, agentThree])
|
||||
.withAction('isolate')
|
||||
.withID(actionTwoID),
|
||||
aMockAction().withAgents([agentThree]).withAction('isolate'),
|
||||
],
|
||||
[aMockResponse(actionTwoID, agentThree)]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [agentOne, agentTwo, agentThree],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(3);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentOne,
|
||||
pending_actions: {
|
||||
isolate: 1,
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentTwo,
|
||||
pending_actions: {
|
||||
isolate: 2,
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentThree,
|
||||
pending_actions: {
|
||||
isolate: 2, // present in all three actions, but second one has a response, therefore not pending
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { RequestHandler } from 'kibana/server';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
EndpointAction,
|
||||
EndpointActionResponse,
|
||||
PendingActionsResponse,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
|
||||
import { ActionStatusRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import {
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
} from '../../../types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
/**
|
||||
* Registers routes for checking status of endpoints based on pending actions
|
||||
*/
|
||||
export function registerActionStatusRoutes(
|
||||
router: SecuritySolutionPluginRouter,
|
||||
endpointContext: EndpointAppContext
|
||||
) {
|
||||
router.get(
|
||||
{
|
||||
path: ACTION_STATUS_ROUTE,
|
||||
validate: ActionStatusRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
actionStatusRequestHandler(endpointContext)
|
||||
);
|
||||
}
|
||||
|
||||
export const actionStatusRequestHandler = function (
|
||||
endpointContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
unknown,
|
||||
TypeOf<typeof ActionStatusRequestSchema.query>,
|
||||
unknown,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> {
|
||||
return async (context, req, res) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const agentIDs: string[] = Array.isArray(req.query.agent_ids)
|
||||
? [...new Set(req.query.agent_ids)]
|
||||
: [req.query.agent_ids];
|
||||
|
||||
// retrieve the unexpired actions for the given hosts
|
||||
const recentActionResults = await esClient.search<EndpointAction>(
|
||||
{
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { type: 'INPUT_ACTION' } }, // actions that are directed at agent children
|
||||
{ term: { input_type: 'endpoint' } }, // filter for agent->endpoint actions
|
||||
{ range: { expiration: { gte: 'now' } } }, // that have not expired yet
|
||||
{ terms: { agents: agentIDs } }, // for the requested agent IDs
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
}
|
||||
);
|
||||
const pendingActions =
|
||||
recentActionResults.body?.hits?.hits?.map((a): EndpointAction => a._source!) || [];
|
||||
|
||||
// retrieve any responses to those action IDs from these agents
|
||||
const actionIDs = pendingActions.map((a) => a.action_id);
|
||||
const responseResults = await esClient.search<EndpointActionResponse>(
|
||||
{
|
||||
index: '.fleet-actions-results',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ terms: { action_id: actionIDs } }, // get results for these actions
|
||||
{ terms: { agent_id: agentIDs } }, // ignoring responses from agents we're not looking for
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
}
|
||||
);
|
||||
const actionResponses = responseResults.body?.hits?.hits?.map((a) => a._source!) || [];
|
||||
|
||||
// respond with action-count per agent
|
||||
const response = agentIDs.map((aid) => {
|
||||
const responseIDsFromAgent = actionResponses
|
||||
.filter((r) => r.agent_id === aid)
|
||||
.map((r) => r.action_id);
|
||||
return {
|
||||
agent_id: aid,
|
||||
pending_actions: pendingActions
|
||||
.filter((a) => a.agents.includes(aid) && !responseIDsFromAgent.includes(a.action_id))
|
||||
.map((a) => a.data.command)
|
||||
.reduce((acc, cur) => {
|
||||
if (cur in acc) {
|
||||
acc[cur] += 1;
|
||||
} else {
|
||||
acc[cur] = 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as PendingActionsResponse['pending_actions']),
|
||||
} as PendingActionsResponse;
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
data: response,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
|
@ -75,10 +75,7 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
|||
import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency';
|
||||
import { registerResolverRoutes } from './endpoint/routes/resolver';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import {
|
||||
registerHostIsolationRoutes,
|
||||
registerActionAuditLogRoutes,
|
||||
} from './endpoint/routes/actions';
|
||||
import { registerActionRoutes } from './endpoint/routes/actions';
|
||||
import { EndpointArtifactClient, ManifestManager } from './endpoint/services';
|
||||
import { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
|
||||
import { EndpointAppContext } from './endpoint/types';
|
||||
|
@ -293,8 +290,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
registerResolverRoutes(router);
|
||||
registerPolicyRoutes(router, endpointContext);
|
||||
registerTrustedAppsRoutes(router, endpointContext);
|
||||
registerHostIsolationRoutes(router, endpointContext);
|
||||
registerActionAuditLogRoutes(router, endpointContext);
|
||||
registerActionRoutes(router, endpointContext);
|
||||
|
||||
const referenceRuleTypes = [
|
||||
REFERENCE_RULE_ALERT_TYPE_ID,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue