[Security Solution][Endpoint] Host Isolation API changes (#113621)
* Use the new data stream (if exists) to write action request to and then the fleet index. Else do as usual. fixes elastic/security-team/issues/1704 * fix legacy tests * add relevant additional tests * remove duplicate test * update tests * cleanup review changes refs elastic/security-team/issues/1704 * fix lint * Use correct mapping keys when writing to index * write record on new index when action request fails to write to `.fleet-actions` review comments * better error message review comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3d75154368
commit
1d71d42a7b
6 changed files with 326 additions and 101 deletions
|
@ -5,8 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ENDPOINT_ACTIONS_INDEX = '.logs-endpoint.actions-default';
|
||||
export const ENDPOINT_ACTION_RESPONSES_INDEX = '.logs-endpoint.action.responses-default';
|
||||
/** endpoint data streams that are used for host isolation */
|
||||
/** for index patterns `.logs-endpoint.actions-* and .logs-endpoint.action.responses-*`*/
|
||||
export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions';
|
||||
export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
|
||||
export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses';
|
||||
export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
|
||||
|
||||
export const eventsIndexPattern = 'logs-endpoint.events.*';
|
||||
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
|
||||
|
|
|
@ -8,51 +8,7 @@
|
|||
import { DeepPartial } from 'utility-types';
|
||||
import { merge } from 'lodash';
|
||||
import { BaseDataGenerator } from './base_data_generator';
|
||||
import { EndpointActionData, ISOLATION_ACTIONS } from '../types';
|
||||
|
||||
interface EcsError {
|
||||
code: string;
|
||||
id: string;
|
||||
message: string;
|
||||
stack_trace: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface EndpointActionFields {
|
||||
action_id: string;
|
||||
data: EndpointActionData;
|
||||
}
|
||||
|
||||
interface ActionRequestFields {
|
||||
expiration: string;
|
||||
type: 'INPUT_ACTION';
|
||||
input_type: 'endpoint';
|
||||
}
|
||||
|
||||
interface ActionResponseFields {
|
||||
completed_at: string;
|
||||
started_at: string;
|
||||
}
|
||||
export interface LogsEndpointAction {
|
||||
'@timestamp': string;
|
||||
agent: {
|
||||
id: string | string[];
|
||||
};
|
||||
EndpointAction: EndpointActionFields & ActionRequestFields;
|
||||
error?: EcsError;
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LogsEndpointActionResponse {
|
||||
'@timestamp': string;
|
||||
agent: {
|
||||
id: string | string[];
|
||||
};
|
||||
EndpointAction: EndpointActionFields & ActionResponseFields;
|
||||
error?: EcsError;
|
||||
}
|
||||
import { ISOLATION_ACTIONS, LogsEndpointAction, LogsEndpointActionResponse } from '../types';
|
||||
|
||||
const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate'];
|
||||
|
||||
|
@ -66,7 +22,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
agent: {
|
||||
id: [this.randomUUID()],
|
||||
},
|
||||
EndpointAction: {
|
||||
EndpointActions: {
|
||||
action_id: this.randomUUID(),
|
||||
expiration: this.randomFutureDate(timeStamp),
|
||||
type: 'INPUT_ACTION',
|
||||
|
@ -86,11 +42,11 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
}
|
||||
|
||||
generateIsolateAction(overrides: DeepPartial<LogsEndpointAction> = {}): LogsEndpointAction {
|
||||
return merge(this.generate({ EndpointAction: { data: { command: 'isolate' } } }), overrides);
|
||||
return merge(this.generate({ EndpointActions: { data: { command: 'isolate' } } }), overrides);
|
||||
}
|
||||
|
||||
generateUnIsolateAction(overrides: DeepPartial<LogsEndpointAction> = {}): LogsEndpointAction {
|
||||
return merge(this.generate({ EndpointAction: { data: { command: 'unisolate' } } }), overrides);
|
||||
return merge(this.generate({ EndpointActions: { data: { command: 'unisolate' } } }), overrides);
|
||||
}
|
||||
|
||||
/** Generates an endpoint action response */
|
||||
|
@ -105,7 +61,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
agent: {
|
||||
id: this.randomUUID(),
|
||||
},
|
||||
EndpointAction: {
|
||||
EndpointActions: {
|
||||
action_id: this.randomUUID(),
|
||||
completed_at: timeStamp.toISOString(),
|
||||
data: {
|
||||
|
|
|
@ -7,12 +7,8 @@
|
|||
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { DeleteByQueryResponse } from '@elastic/elasticsearch/api/types';
|
||||
import { HostMetadata } from '../types';
|
||||
import {
|
||||
EndpointActionGenerator,
|
||||
LogsEndpointAction,
|
||||
LogsEndpointActionResponse,
|
||||
} from '../data_generators/endpoint_action_generator';
|
||||
import { HostMetadata, LogsEndpointAction, LogsEndpointActionResponse } from '../types';
|
||||
import { EndpointActionGenerator } from '../data_generators/endpoint_action_generator';
|
||||
import { wrapErrorAndRejectPromise } from './utils';
|
||||
import { ENDPOINT_ACTIONS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX } from '../constants';
|
||||
|
||||
|
@ -49,7 +45,7 @@ export const indexEndpointActionsForHost = async (
|
|||
for (let i = 0; i < total; i++) {
|
||||
// create an action
|
||||
const action = endpointActionGenerator.generate({
|
||||
EndpointAction: {
|
||||
EndpointActions: {
|
||||
data: { comment: 'data generator: this host is same as bad' },
|
||||
},
|
||||
});
|
||||
|
@ -66,9 +62,9 @@ export const indexEndpointActionsForHost = async (
|
|||
// Create an action response for the above
|
||||
const actionResponse = endpointActionGenerator.generateResponse({
|
||||
agent: { id: agentId },
|
||||
EndpointAction: {
|
||||
action_id: action.EndpointAction.action_id,
|
||||
data: action.EndpointAction.data,
|
||||
EndpointActions: {
|
||||
action_id: action.EndpointActions.action_id,
|
||||
data: action.EndpointActions.data,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -174,7 +170,7 @@ export const deleteIndexedEndpointActions = async (
|
|||
{
|
||||
terms: {
|
||||
action_id: indexedData.endpointActions.map(
|
||||
(action) => action.EndpointAction.action_id
|
||||
(action) => action.EndpointActions.action_id
|
||||
),
|
||||
},
|
||||
},
|
||||
|
@ -200,7 +196,7 @@ export const deleteIndexedEndpointActions = async (
|
|||
{
|
||||
terms: {
|
||||
action_id: indexedData.endpointActionResponses.map(
|
||||
(action) => action.EndpointAction.action_id
|
||||
(action) => action.EndpointActions.action_id
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -10,6 +10,50 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema
|
|||
|
||||
export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
|
||||
|
||||
interface EcsError {
|
||||
code?: string;
|
||||
id?: string;
|
||||
message: string;
|
||||
stack_trace?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface EndpointActionFields {
|
||||
action_id: string;
|
||||
data: EndpointActionData;
|
||||
}
|
||||
|
||||
interface ActionRequestFields {
|
||||
expiration: string;
|
||||
type: 'INPUT_ACTION';
|
||||
input_type: 'endpoint';
|
||||
}
|
||||
|
||||
interface ActionResponseFields {
|
||||
completed_at: string;
|
||||
started_at: string;
|
||||
}
|
||||
export interface LogsEndpointAction {
|
||||
'@timestamp': string;
|
||||
agent: {
|
||||
id: string | string[];
|
||||
};
|
||||
EndpointActions: EndpointActionFields & ActionRequestFields;
|
||||
error?: EcsError;
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LogsEndpointActionResponse {
|
||||
'@timestamp': string;
|
||||
agent: {
|
||||
id: string | string[];
|
||||
};
|
||||
EndpointActions: EndpointActionFields & ActionResponseFields;
|
||||
error?: EcsError;
|
||||
}
|
||||
|
||||
export interface EndpointActionData {
|
||||
command: ISOLATION_ACTIONS;
|
||||
comment?: string;
|
||||
|
|
|
@ -34,16 +34,18 @@ import {
|
|||
ISOLATE_HOST_ROUTE,
|
||||
UNISOLATE_HOST_ROUTE,
|
||||
metadataTransformPrefix,
|
||||
ENDPOINT_ACTIONS_INDEX,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
import {
|
||||
EndpointAction,
|
||||
HostIsolationRequestBody,
|
||||
HostIsolationResponse,
|
||||
HostMetadata,
|
||||
LogsEndpointAction,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { legacyMetadataSearchResponse } from '../metadata/support/test_support';
|
||||
import { ElasticsearchAssetType } from '../../../../../fleet/common';
|
||||
import { AGENT_ACTIONS_INDEX, ElasticsearchAssetType } from '../../../../../fleet/common';
|
||||
import { CasesClientMock } from '../../../../../cases/server/client/mocks';
|
||||
|
||||
interface CallRouteInterface {
|
||||
|
@ -109,7 +111,8 @@ describe('Host Isolation', () => {
|
|||
|
||||
let callRoute: (
|
||||
routePrefix: string,
|
||||
opts: CallRouteInterface
|
||||
opts: CallRouteInterface,
|
||||
indexExists?: { endpointDsExists: boolean }
|
||||
) => Promise<jest.Mocked<SecuritySolutionRequestHandlerContext>>;
|
||||
const superUser = {
|
||||
username: 'superuser',
|
||||
|
@ -175,22 +178,42 @@ describe('Host Isolation', () => {
|
|||
// it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document)
|
||||
callRoute = async (
|
||||
routePrefix: string,
|
||||
{ body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface
|
||||
{ body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface,
|
||||
indexExists?: { endpointDsExists: boolean }
|
||||
): Promise<jest.Mocked<SecuritySolutionRequestHandlerContext>> => {
|
||||
const asUser = mockUser ? mockUser : superUser;
|
||||
(startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce(
|
||||
() => asUser
|
||||
);
|
||||
|
||||
const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient);
|
||||
const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 };
|
||||
ctx.core.elasticsearch.client.asCurrentUser.index = jest
|
||||
// mock _index_template
|
||||
ctx.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => Promise.resolve(withIdxResp));
|
||||
ctx.core.elasticsearch.client.asCurrentUser.search = jest
|
||||
.mockImplementationOnce(() => {
|
||||
if (indexExists) {
|
||||
return Promise.resolve({
|
||||
body: true,
|
||||
statusCode: 200,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
body: false,
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 };
|
||||
const mockIndexResponse = jest.fn().mockImplementation(() => Promise.resolve(withIdxResp));
|
||||
const mockSearchResponse = jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ body: legacyMetadataSearchResponse(searchResponse) })
|
||||
);
|
||||
if (indexExists) {
|
||||
ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse;
|
||||
}
|
||||
ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse;
|
||||
ctx.core.elasticsearch.client.asCurrentUser.search = mockSearchResponse;
|
||||
const withLicense = license ? license : Platinum;
|
||||
licenseEmitter.next(withLicense);
|
||||
const mockRequest = httpServerMock.createKibanaRequest({ body });
|
||||
|
@ -288,11 +311,6 @@ describe('Host Isolation', () => {
|
|||
).mock.calls[0][0].body;
|
||||
expect(actionDoc.timeout).toEqual(300);
|
||||
});
|
||||
|
||||
it('succeeds when just an endpoint ID is provided', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } });
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('sends the action to the correct agent when endpoint ID is given', async () => {
|
||||
const doc = docGen.generateHostMetadata();
|
||||
const AgentID = doc.elastic.agent.id;
|
||||
|
@ -326,6 +344,74 @@ describe('Host Isolation', () => {
|
|||
expect(actionDoc.data.command).toEqual('unisolate');
|
||||
});
|
||||
|
||||
describe('With endpoint data streams', () => {
|
||||
it('handles unisolation', async () => {
|
||||
const ctx = await callRoute(
|
||||
UNISOLATE_HOST_ROUTE,
|
||||
{
|
||||
body: { endpoint_ids: ['XYZ'] },
|
||||
},
|
||||
{ endpointDsExists: true }
|
||||
);
|
||||
const actionDocs: [
|
||||
{ index: string; body: LogsEndpointAction },
|
||||
{ index: string; body: EndpointAction }
|
||||
] = [
|
||||
(ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock).mock.calls[0][0],
|
||||
(ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock).mock.calls[1][0],
|
||||
];
|
||||
|
||||
expect(actionDocs[0].index).toEqual(ENDPOINT_ACTIONS_INDEX);
|
||||
expect(actionDocs[1].index).toEqual(AGENT_ACTIONS_INDEX);
|
||||
expect(actionDocs[0].body.EndpointActions.data.command).toEqual('unisolate');
|
||||
expect(actionDocs[1].body.data.command).toEqual('unisolate');
|
||||
});
|
||||
|
||||
it('handles isolation', async () => {
|
||||
const ctx = await callRoute(
|
||||
ISOLATE_HOST_ROUTE,
|
||||
{
|
||||
body: { endpoint_ids: ['XYZ'] },
|
||||
},
|
||||
{ endpointDsExists: true }
|
||||
);
|
||||
const actionDocs: [
|
||||
{ index: string; body: LogsEndpointAction },
|
||||
{ index: string; body: EndpointAction }
|
||||
] = [
|
||||
(ctx.core.elasticsearch.client.asCurrentUser.index as jest.Mock).mock.calls[0][0],
|
||||
(ctx.core.elasticsearch.client.asInternalUser.index as jest.Mock).mock.calls[1][0],
|
||||
];
|
||||
|
||||
expect(actionDocs[0].index).toEqual(ENDPOINT_ACTIONS_INDEX);
|
||||
expect(actionDocs[1].index).toEqual(AGENT_ACTIONS_INDEX);
|
||||
expect(actionDocs[0].body.EndpointActions.data.command).toEqual('isolate');
|
||||
expect(actionDocs[1].body.data.command).toEqual('isolate');
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
const ErrMessage = 'Uh oh!';
|
||||
await callRoute(
|
||||
UNISOLATE_HOST_ROUTE,
|
||||
{
|
||||
body: { endpoint_ids: ['XYZ'] },
|
||||
idxResponse: {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
result: ErrMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ endpointDsExists: true }
|
||||
);
|
||||
|
||||
expect(mockResponse.ok).not.toBeCalled();
|
||||
const response = mockResponse.customError.mock.calls[0][0];
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect((response.body as Error).message).toEqual(ErrMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('License Level', () => {
|
||||
it('allows platinum license levels to isolate hosts', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
|
|
|
@ -6,15 +6,25 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { RequestHandler } from 'src/core/server';
|
||||
import { RequestHandler, Logger } from 'src/core/server';
|
||||
import uuid from 'uuid';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { CommentType } from '../../../../../cases/common';
|
||||
import { CasesByAlertId } from '../../../../../cases/common/api/cases/case';
|
||||
import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import {
|
||||
ENDPOINT_ACTIONS_DS,
|
||||
ENDPOINT_ACTION_RESPONSES_DS,
|
||||
ISOLATE_HOST_ROUTE,
|
||||
UNISOLATE_HOST_ROUTE,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
|
||||
import { EndpointAction, HostMetadata } from '../../../../common/endpoint/types';
|
||||
import {
|
||||
EndpointAction,
|
||||
HostMetadata,
|
||||
LogsEndpointAction,
|
||||
LogsEndpointActionResponse,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import {
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
|
@ -52,6 +62,57 @@ export function registerHostIsolationRoutes(
|
|||
);
|
||||
}
|
||||
|
||||
const createFailedActionResponseEntry = async ({
|
||||
context,
|
||||
doc,
|
||||
logger,
|
||||
}: {
|
||||
context: SecuritySolutionRequestHandlerContext;
|
||||
doc: LogsEndpointActionResponse;
|
||||
logger: Logger;
|
||||
}): Promise<void> => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
try {
|
||||
await esClient.index<LogsEndpointActionResponse>({
|
||||
index: `${ENDPOINT_ACTION_RESPONSES_DS}-default`,
|
||||
body: {
|
||||
...doc,
|
||||
error: {
|
||||
code: '424',
|
||||
message: 'Failed to deliver action request to fleet',
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const doLogsEndpointActionDsExists = async ({
|
||||
context,
|
||||
logger,
|
||||
dataStreamName,
|
||||
}: {
|
||||
context: SecuritySolutionRequestHandlerContext;
|
||||
logger: Logger;
|
||||
dataStreamName: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
const esClient = context.core.elasticsearch.client.asInternalUser;
|
||||
const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({
|
||||
name: dataStreamName,
|
||||
});
|
||||
return doesIndexTemplateExist.statusCode === 404 ? false : true;
|
||||
} catch (error) {
|
||||
const errorType = error?.type ?? '';
|
||||
if (errorType !== 'resource_not_found_exception') {
|
||||
logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isolationRequestHandler = function (
|
||||
endpointContext: EndpointAppContext,
|
||||
isolate: boolean
|
||||
|
@ -106,43 +167,121 @@ export const isolationRequestHandler = function (
|
|||
caseIDs = [...new Set(caseIDs)];
|
||||
|
||||
// create an Action ID and dispatch it to ES & Fleet Server
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const actionID = uuid.v4();
|
||||
let result;
|
||||
|
||||
let fleetActionIndexResult;
|
||||
let logsEndpointActionsResult;
|
||||
|
||||
const agents = endpointData.map((endpoint: HostMetadata) => endpoint.elastic.agent.id);
|
||||
const doc = {
|
||||
'@timestamp': moment().toISOString(),
|
||||
agent: {
|
||||
id: agents,
|
||||
},
|
||||
EndpointActions: {
|
||||
action_id: actionID,
|
||||
expiration: moment().add(2, 'weeks').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
data: {
|
||||
command: isolate ? 'isolate' : 'unisolate',
|
||||
comment: req.body.comment ?? undefined,
|
||||
},
|
||||
} as Omit<EndpointAction, 'agents' | 'user_id'>,
|
||||
user: {
|
||||
id: user!.username,
|
||||
},
|
||||
};
|
||||
|
||||
// if .logs-endpoint.actions data stream exists
|
||||
// try to create action request record in .logs-endpoint.actions DS as the current user
|
||||
// (from >= v7.16, use this check to ensure the current user has privileges to write to the new index)
|
||||
// and allow only users with superuser privileges to write to fleet indices
|
||||
const logger = endpointContext.logFactory.get('host-isolation');
|
||||
const doesLogsEndpointActionsDsExist = await doLogsEndpointActionDsExists({
|
||||
context,
|
||||
logger,
|
||||
dataStreamName: ENDPOINT_ACTIONS_DS,
|
||||
});
|
||||
// if the new endpoint indices/data streams exists
|
||||
// write the action request to the new index as the current user
|
||||
if (doesLogsEndpointActionsDsExist) {
|
||||
try {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
logsEndpointActionsResult = await esClient.index<LogsEndpointAction>({
|
||||
index: `${ENDPOINT_ACTIONS_DS}-default`,
|
||||
body: {
|
||||
...doc,
|
||||
},
|
||||
});
|
||||
if (logsEndpointActionsResult.statusCode !== 201) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: logsEndpointActionsResult.body.result,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
result = await esClient.index<EndpointAction>({
|
||||
let esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
if (doesLogsEndpointActionsDsExist) {
|
||||
// create action request record as system user with user in .fleet-actions
|
||||
esClient = context.core.elasticsearch.client.asInternalUser;
|
||||
}
|
||||
// write as the current user if the new indices do not exist
|
||||
// <v7.16 requires the current user to be super user
|
||||
fleetActionIndexResult = await esClient.index<EndpointAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: {
|
||||
action_id: actionID,
|
||||
'@timestamp': moment().toISOString(),
|
||||
expiration: moment().add(2, 'weeks').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: endpointData.map((endpt: HostMetadata) => endpt.elastic.agent.id),
|
||||
user_id: user!.username,
|
||||
...doc.EndpointActions,
|
||||
'@timestamp': doc['@timestamp'],
|
||||
agents,
|
||||
timeout: 300, // 5 minutes
|
||||
data: {
|
||||
command: isolate ? 'isolate' : 'unisolate',
|
||||
comment: req.body.comment ?? undefined,
|
||||
},
|
||||
user_id: doc.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (fleetActionIndexResult.statusCode !== 201) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: fleetActionIndexResult.body.result,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// create entry in .logs-endpoint.action.responses-default data stream
|
||||
// when writing to .fleet-actions fails
|
||||
if (doesLogsEndpointActionsDsExist) {
|
||||
await createFailedActionResponseEntry({
|
||||
context,
|
||||
doc: {
|
||||
'@timestamp': moment().toISOString(),
|
||||
agent: doc.agent,
|
||||
EndpointActions: {
|
||||
action_id: doc.EndpointActions.action_id,
|
||||
completed_at: moment().toISOString(),
|
||||
started_at: moment().toISOString(),
|
||||
data: doc.EndpointActions.data,
|
||||
},
|
||||
},
|
||||
logger,
|
||||
});
|
||||
}
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.statusCode !== 201) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: result.body.result,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Update all cases with a comment
|
||||
if (caseIDs.length > 0) {
|
||||
const targets = endpointData.map((endpt: HostMetadata) => ({
|
||||
|
|
Loading…
Reference in a new issue