[Security Solution][Endpoint] update CLI tool that responds to actions to also send metadata updates for Isolate and Release actions (#135601)

* endpoint metadata service to retrieve individual endpoint metadata

* Fleet service with method to check in an agent into fleet

* Endpoint metadata service method to send a metadata update

* update action responder to also send endpoint metadata updates for isolate and release actions

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Paul Tavares 2022-07-05 11:33:58 -04:00 committed by GitHub
parent 5460e38361
commit 4d3cab8983
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 2 deletions

View file

@ -8,6 +8,7 @@
import { KbnClient } from '@kbn/test';
import { Client } from '@elastic/elasticsearch';
import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
import { sendEndpointMetadataUpdate } from '../common/endpoint_metadata_services';
import { FleetActionGenerator } from '../../../common/endpoint/data_generators/fleet_action_generator';
import {
ENDPOINT_ACTION_RESPONSES_INDEX,
@ -87,8 +88,6 @@ export const sendEndpointActionResponse = async (
action: ActionDetails,
{ state }: { state?: 'success' | 'failure' } = {}
): Promise<LogsEndpointActionResponse> => {
// FIXME:PT Generate command specific responses
const endpointResponse = endpointActionGenerator.generateResponse({
agent: { id: action.agents[0] },
EndpointActions: {
@ -115,6 +114,36 @@ export const sendEndpointActionResponse = async (
refresh: 'wait_for',
});
// ------------------------------------------
// Post Action Response tasks
// ------------------------------------------
// For isolate, If the response is not an error, then also send a metadata update
if (action.command === 'isolate' && !endpointResponse.error) {
for (const agentId of action.agents) {
await sendEndpointMetadataUpdate(esClient, agentId, {
Endpoint: {
state: {
isolation: true,
},
},
});
}
}
// For UnIsolate, if response is not an Error, then also send metadata update
if (action.command === 'unisolate' && !endpointResponse.error) {
for (const agentId of action.agents) {
await sendEndpointMetadataUpdate(esClient, agentId, {
Endpoint: {
state: {
isolation: false,
},
},
});
}
}
return endpointResponse;
};

View file

@ -0,0 +1,116 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import { KbnClient } from '@kbn/test';
import { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types';
import { clone, merge } from 'lodash';
import { DeepPartial } from 'utility-types';
import { resolvePathVariables } from '../../../public/common/utils/resolve_path_variables';
import { HOST_METADATA_GET_ROUTE, METADATA_DATASTREAM } from '../../../common/endpoint/constants';
import { HostInfo, HostMetadata } from '../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import { checkInFleetAgent } from './fleet_services';
const endpointGenerator = new EndpointDocGenerator();
export const fetchEndpointMetadata = async (
kbnClient: KbnClient,
agentId: string
): Promise<HostInfo> => {
return (
await kbnClient.request<HostInfo>({
method: 'GET',
path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }),
})
).data;
};
export const sendEndpointMetadataUpdate = async (
esClient: Client,
agentId: string,
overrides: DeepPartial<HostMetadata> = {},
{ checkInAgent = true }: Partial<{ checkInAgent: boolean }> = {}
): Promise<WriteResponseBase> => {
const lastStreamedDoc = await fetchLastStreamedEndpointUpdate(esClient, agentId);
if (!lastStreamedDoc) {
throw new Error(`An endpoint with agent.id of [${agentId}] not found!`);
}
if (checkInAgent) {
// Trigger an agent checkin and just let it run
checkInFleetAgent(esClient, agentId);
}
const generatedHostMetadataDoc = clone(endpointGenerator.generateHostMetadata());
const newUpdate: HostMetadata = merge(
lastStreamedDoc,
{
event: generatedHostMetadataDoc.event, // Make sure to use a new event object
'@timestamp': generatedHostMetadataDoc['@timestamp'],
},
overrides
);
return esClient.index({
index: METADATA_DATASTREAM,
body: newUpdate,
op_type: 'create',
});
};
const fetchLastStreamedEndpointUpdate = async (
esClient: Client,
agentId: string
): Promise<HostMetadata | undefined> => {
const queryResult = await esClient.search<HostMetadata>(
{
index: METADATA_DATASTREAM,
size: 1,
body: {
query: {
bool: {
filter: [
{
bool: {
should: [{ term: { 'elastic.agent.id': agentId } }],
},
},
],
},
},
// Am I doing this right? I want only the last document for the host.id that was sent
collapse: {
field: 'host.id',
inner_hits: {
name: 'most_recent',
size: 1,
sort: [{ 'event.created': 'desc' }],
},
},
aggs: {
total: {
cardinality: {
field: 'host.id',
},
},
},
sort: [
{
'event.created': {
order: 'desc',
},
},
],
},
},
{ ignore: [404] }
);
return queryResult.hits?.hits[0]?._source;
};

View file

@ -0,0 +1,27 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import { AGENTS_INDEX } from '@kbn/fleet-plugin/common';
export const checkInFleetAgent = async (esClient: Client, agentId: string) => {
const checkinNow = new Date().toISOString();
await esClient.update({
index: AGENTS_INDEX,
id: agentId,
refresh: 'wait_for',
retry_on_conflict: 5,
body: {
doc: {
active: true,
last_checkin: checkinNow,
updated_at: checkinNow,
},
},
});
};