[Security Solution][Endpoint] Update host isolation pending status API to work with new endpoint (#115441)

* get the whole response when fetching responses by agentIds

fixes elastic/security-team/issues/1705

* search new response index with actionId

fixes elastic/security-team/issues/1705

* Find matching responses in new response index if fleet action response has an `ack`

* review changes

* hasIndexedDoc fix fetch logic

* remove file

* simplify code

* add some acks to generator responses

* meaningful names

review changes

Co-authored-by: Esteban Beltran <esteban.beltran@elastic.co>
This commit is contained in:
Ashokaditya 2021-10-19 23:29:03 +02:00 committed by GitHub
parent ab16b485cd
commit b1dd89e173
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 14 deletions

View file

@ -66,7 +66,11 @@ export const indexFleetActionsForHost = async (
const actionResponse = fleetActionGenerator.generateResponse({
action_id: action.action_id,
agent_id: agentId,
action_data: action.data,
action_data: {
...action.data,
// add ack to 4/5th of fleet response
ack: fleetActionGenerator.randomFloat() < 0.8 ? true : undefined,
},
});
esClient

View file

@ -64,6 +64,7 @@ export interface LogsEndpointActionResponse {
export interface EndpointActionData {
command: ISOLATION_ACTIONS;
comment?: string;
ack?: boolean;
}
export interface EndpointAction {

View file

@ -9,6 +9,7 @@ import { ElasticsearchClient, Logger } from 'kibana/server';
import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types';
import { ApiResponse } from '@elastic/elasticsearch';
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common';
import { ENDPOINT_ACTION_RESPONSES_INDEX } from '../../../common/endpoint/constants';
import { SecuritySolutionRequestHandlerContext } from '../../types';
import {
ActivityLog,
@ -146,6 +147,41 @@ const getActivityLog = async ({
return sortedData;
};
const hasAckInResponse = (response: EndpointActionResponse): boolean => {
return typeof response.action_data.ack !== 'undefined';
};
// return TRUE if for given action_id/agent_id
// there is no doc in .logs-endpoint.action.response-default
const hasNoEndpointResponse = ({
action,
agentId,
indexedActionIds,
}: {
action: EndpointAction;
agentId: string;
indexedActionIds: string[];
}): boolean => {
return action.agents.includes(agentId) && !indexedActionIds.includes(action.action_id);
};
// return TRUE if for given action_id/agent_id
// there is no doc in .fleet-actions-results
const hasNoFleetResponse = ({
action,
agentId,
agentResponses,
}: {
action: EndpointAction;
agentId: string;
agentResponses: EndpointActionResponse[];
}): boolean => {
return (
action.agents.includes(agentId) &&
!agentResponses.map((e) => e.action_id).includes(action.action_id)
);
};
export const getPendingActionCounts = async (
esClient: ElasticsearchClient,
metadataService: EndpointMetadataService,
@ -179,21 +215,45 @@ export const getPendingActionCounts = async (
.catch(catchAndWrapError);
// retrieve any responses to those action IDs from these agents
const responses = await fetchActionResponseIds(
const responses = await fetchActionResponses(
esClient,
metadataService,
recentActions.map((a) => a.action_id),
agentIDs
);
const pending: EndpointPendingActions[] = [];
//
const pending: EndpointPendingActions[] = [];
for (const agentId of agentIDs) {
const responseIDsFromAgent = responses[agentId];
const agentResponses = responses[agentId];
// get response actionIds for responses with ACKs
const ackResponseActionIdList: string[] = agentResponses
.filter(hasAckInResponse)
.map((response) => response.action_id);
// actions Ids that are indexed in new response index
const indexedActionIds = await hasEndpointResponseDoc({
agentId,
actionIds: ackResponseActionIdList,
esClient,
});
const pendingActions: EndpointAction[] = recentActions.filter((action) => {
return ackResponseActionIdList.includes(action.action_id) // if has ack
? hasNoEndpointResponse({ action, agentId, indexedActionIds }) // then find responses in new index
: hasNoFleetResponse({
// else use the legacy way
action,
agentId,
agentResponses,
});
});
pending.push({
agent_id: agentId,
pending_actions: recentActions
.filter((a) => a.agents.includes(agentId) && !responseIDsFromAgent.includes(a.action_id))
pending_actions: pendingActions
.map((a) => a.data.command)
.reduce((acc, cur) => {
if (cur in acc) {
@ -209,6 +269,43 @@ export const getPendingActionCounts = async (
return pending;
};
/**
* Returns a boolean for search result
*
* @param esClient
* @param actionIds
* @param agentIds
*/
const hasEndpointResponseDoc = async ({
actionIds,
agentId,
esClient,
}: {
actionIds: string[];
agentId: string;
esClient: ElasticsearchClient;
}): Promise<string[]> => {
const response = await esClient
.search<LogsEndpointActionResponse>(
{
index: ENDPOINT_ACTION_RESPONSES_INDEX,
body: {
query: {
bool: {
filter: [{ terms: { action_id: actionIds } }, { term: { agent_id: agentId } }],
},
},
},
},
{ ignore: [404] }
)
.then(
(result) => result.body?.hits?.hits?.map((a) => a._source?.EndpointActions.action_id) || []
)
.catch(catchAndWrapError);
return response.filter((action): action is string => action !== undefined);
};
/**
* Returns back a map of elastic Agent IDs to array of Action IDs that have received a response.
*
@ -217,16 +314,19 @@ export const getPendingActionCounts = async (
* @param actionIds
* @param agentIds
*/
const fetchActionResponseIds = async (
const fetchActionResponses = async (
esClient: ElasticsearchClient,
metadataService: EndpointMetadataService,
actionIds: string[],
agentIds: string[]
): Promise<Record<string, string[]>> => {
const actionResponsesByAgentId: Record<string, string[]> = agentIds.reduce((acc, agentId) => {
acc[agentId] = [];
return acc;
}, {} as Record<string, string[]>);
): Promise<Record<string, EndpointActionResponse[]>> => {
const actionResponsesByAgentId: Record<string, EndpointActionResponse[]> = agentIds.reduce(
(acc, agentId) => {
acc[agentId] = [];
return acc;
},
{} as Record<string, EndpointActionResponse[]>
);
const actionResponses = await esClient
.search<EndpointActionResponse>(
@ -255,7 +355,7 @@ const fetchActionResponseIds = async (
return actionResponsesByAgentId;
}
// Get the latest docs from the metadata datastream for the Elastic Agent IDs in the action responses
// Get the latest docs from the metadata data-stream for the Elastic Agent IDs in the action responses
// This will be used determine if we should withhold the action id from the returned list in cases where
// the Endpoint might not yet have sent an updated metadata document (which would be representative of
// the state of the endpoint post-action)
@ -288,7 +388,7 @@ const fetchActionResponseIds = async (
enoughTimeHasLapsed ||
lastEndpointMetadataEventTimestamp > actionCompletedAtTimestamp
) {
actionResponsesByAgentId[actionResponse.agent_id].push(actionResponse.action_id);
actionResponsesByAgentId[actionResponse.agent_id].push(actionResponse);
}
}