Skip to content

Commit

Permalink
[Security Solution][Endpoint] New action details API (#131632)
Browse files Browse the repository at this point in the history
* Action details API
* FleetActionGenerator: change `generate()` method to use "seeded" UUIDs
* EndopintActionGenerator: change `generate()` method to use "seeded" UUIDs
* Added generation of Activity log entries for both Fleet and Endpoint action generators
* Add public `toEsSearchHit()` and `toEsSearchResponse()` methods to base generator class
* Add es search result generator to Endpoint and fleet action generators
  • Loading branch information
paul-tavares committed May 10, 2022
1 parent 9f992c2 commit cfefccd
Show file tree
Hide file tree
Showing 22 changed files with 1,296 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`;
export const ISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/isolate`;
export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`;

/** Endpoint Actions Log Routes */
/** Endpoint Actions Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`;
export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`;
export const ACTION_DETAILS_ROUTE = `/api/endpoint/action/{action_id}`;

export const failedFleetActionErrorCode = '424';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import seedrandom from 'seedrandom';
import uuid from 'uuid';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

const OS_FAMILY = ['windows', 'macos', 'linux'];
/** Array of 14 day offsets */
Expand Down Expand Up @@ -180,4 +181,45 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> {
protected randomHostname(): string {
return `Host-${this.randomString(10)}`;
}

/**
* Returns an single search hit (normally found in a `SearchResponse`) for the given document source.
* @param hitSource
*/
toEsSearchHit<T extends object = object>(hitSource: T): estypes.SearchHit<T> {
return {
_index: 'some-index',
_id: this.seededUUIDv4(),
_score: 1.0,
_source: hitSource,
};
}

/**
* Returns an ES Search Response for the give set of records. Each record will be wrapped with
* the `toEsSearchHit()`
* @param hitsSource
*/
toEsSearchResponse<T extends object = object>(
hitsSource: Array<estypes.SearchHit<T>>
): estypes.SearchResponse<T> {
return {
took: 3,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: hitsSource.length,
relation: 'eq',
},
max_score: 0,
hits: hitsSource,
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,34 @@

import { DeepPartial } from 'utility-types';
import { merge } from 'lodash';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ENDPOINT_ACTION_RESPONSES_DS, ENDPOINT_ACTIONS_INDEX } from '../constants';
import { BaseDataGenerator } from './base_data_generator';
import { ISOLATION_ACTIONS, LogsEndpointAction, LogsEndpointActionResponse } from '../types';
import {
ActivityLogItemTypes,
EndpointActivityLogActionResponse,
ISOLATION_ACTIONS,
LogsEndpointAction,
LogsEndpointActionResponse,
} from '../types';

const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate'];

export class EndpointActionGenerator extends BaseDataGenerator {
/** Generate a random endpoint Action request (isolate or unisolate) */
generate(overrides: DeepPartial<LogsEndpointAction> = {}): LogsEndpointAction {
const timeStamp = new Date(this.randomPastDate());
const timeStamp = overrides['@timestamp']
? new Date(overrides['@timestamp'])
: new Date(this.randomPastDate());

return merge(
{
'@timestamp': timeStamp.toISOString(),
agent: {
id: [this.randomUUID()],
id: [this.seededUUIDv4()],
},
EndpointActions: {
action_id: this.randomUUID(),
action_id: this.seededUUIDv4(),
expiration: this.randomFutureDate(timeStamp),
type: 'INPUT_ACTION',
input_type: 'endpoint',
Expand All @@ -41,6 +52,14 @@ export class EndpointActionGenerator extends BaseDataGenerator {
);
}

generateActionEsHit(
overrides: DeepPartial<LogsEndpointAction> = {}
): estypes.SearchHit<LogsEndpointAction> {
return Object.assign(this.toEsSearchHit(this.generate(overrides)), {
_index: `.ds-${ENDPOINT_ACTIONS_INDEX}-some_namespace`,
});
}

generateIsolateAction(overrides: DeepPartial<LogsEndpointAction> = {}): LogsEndpointAction {
return merge(this.generate({ EndpointActions: { data: { command: 'isolate' } } }), overrides);
}
Expand All @@ -53,16 +72,16 @@ export class EndpointActionGenerator extends BaseDataGenerator {
generateResponse(
overrides: DeepPartial<LogsEndpointActionResponse> = {}
): LogsEndpointActionResponse {
const timeStamp = new Date();
const timeStamp = overrides['@timestamp'] ? new Date(overrides['@timestamp']) : new Date();

return merge(
{
'@timestamp': timeStamp.toISOString(),
agent: {
id: this.randomUUID(),
id: this.seededUUIDv4(),
},
EndpointActions: {
action_id: this.randomUUID(),
action_id: this.seededUUIDv4(),
completed_at: timeStamp.toISOString(),
data: {
command: this.randomIsolateCommand(),
Expand All @@ -76,6 +95,29 @@ export class EndpointActionGenerator extends BaseDataGenerator {
);
}

generateResponseEsHit(
overrides: DeepPartial<LogsEndpointActionResponse> = {}
): estypes.SearchHit<LogsEndpointActionResponse> {
return Object.assign(this.toEsSearchHit(this.generateResponse(overrides)), {
_index: `.ds-${ENDPOINT_ACTION_RESPONSES_DS}-some_namespace-something`,
});
}

generateActivityLogActionResponse(
overrides: DeepPartial<EndpointActivityLogActionResponse>
): EndpointActivityLogActionResponse {
return merge(
{
type: ActivityLogItemTypes.RESPONSE,
item: {
id: this.seededUUIDv4(),
data: this.generateResponse(),
},
},
overrides
);
}

randomFloat(): number {
return this.random();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,34 @@

import { DeepPartial } from 'utility-types';
import { merge } from 'lodash';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
import { BaseDataGenerator } from './base_data_generator';
import { EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS } from '../types';
import {
ActivityLogActionResponse,
ActivityLogItemTypes,
EndpointAction,
EndpointActionResponse,
ISOLATION_ACTIONS,
} from '../types';

const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate'];

export class FleetActionGenerator extends BaseDataGenerator {
/** Generate a random endpoint Action (isolate or unisolate) */
generate(overrides: DeepPartial<EndpointAction> = {}): EndpointAction {
const timeStamp = new Date(this.randomPastDate());
const timeStamp = overrides['@timestamp']
? new Date(overrides['@timestamp'])
: new Date(this.randomPastDate());

return merge(
{
action_id: this.randomUUID(),
action_id: this.seededUUIDv4(),
'@timestamp': timeStamp.toISOString(),
expiration: this.randomFutureDate(timeStamp),
type: 'INPUT_ACTION',
input_type: 'endpoint',
agents: [this.randomUUID()],
agents: [this.seededUUIDv4()],
user_id: 'elastic',
data: {
command: this.randomIsolateCommand(),
Expand All @@ -35,6 +45,14 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}

generateActionEsHit(
overrides: DeepPartial<EndpointAction> = {}
): estypes.SearchHit<EndpointAction> {
return Object.assign(this.toEsSearchHit(this.generate(overrides)), {
_index: AGENT_ACTIONS_INDEX,
});
}

generateIsolateAction(overrides: DeepPartial<EndpointAction> = {}): EndpointAction {
return merge(this.generate({ data: { command: 'isolate' } }), overrides);
}
Expand All @@ -45,16 +63,16 @@ export class FleetActionGenerator extends BaseDataGenerator {

/** Generates an endpoint action response */
generateResponse(overrides: DeepPartial<EndpointActionResponse> = {}): EndpointActionResponse {
const timeStamp = new Date();
const timeStamp = overrides['@timestamp'] ? new Date(overrides['@timestamp']) : new Date();

return merge(
{
action_data: {
command: this.randomIsolateCommand(),
comment: '',
},
action_id: this.randomUUID(),
agent_id: this.randomUUID(),
action_id: this.seededUUIDv4(),
agent_id: this.seededUUIDv4(),
started_at: this.randomPastDate(),
completed_at: timeStamp.toISOString(),
error: 'some error happened',
Expand All @@ -64,6 +82,33 @@ export class FleetActionGenerator extends BaseDataGenerator {
);
}

generateResponseEsHit(
overrides: DeepPartial<EndpointActionResponse> = {}
): estypes.SearchHit<EndpointActionResponse> {
return Object.assign(this.toEsSearchHit(this.generateResponse(overrides)), {
_index: AGENT_ACTIONS_RESULTS_INDEX,
});
}

/**
* An Activity Log entry as returned by the Activity log API
* @param overrides
*/
generateActivityLogActionResponse(
overrides: DeepPartial<ActivityLogActionResponse> = {}
): ActivityLogActionResponse {
return merge(
{
type: ActivityLogItemTypes.FLEET_RESPONSE,
item: {
id: this.seededUUIDv4(),
data: this.generateResponse(),
},
},
overrides
);
}

randomFloat(): number {
return this.random();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ export const ActionStatusRequestSchema = {
]),
}),
};

export const ActionDetailsRequestSchema = {
params: schema.object({
action_id: schema.string(),
}),
};
47 changes: 47 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ interface ActionResponseFields {
completed_at: string;
started_at: string;
}

/**
* An endpoint Action created in the Endpoint's `.logs-endpoint.actions-default` index.
* @since v7.16
*/
export interface LogsEndpointAction {
'@timestamp': string;
agent: {
Expand All @@ -52,6 +57,10 @@ export interface LogsEndpointAction {
};
}

/**
* An Action response written by the endpoint to the Endpoint `.logs-endpoint.action.responses` datastream
* @since v7.16
*/
export interface LogsEndpointActionResponse {
'@timestamp': string;
agent: {
Expand All @@ -72,6 +81,9 @@ export interface FleetActionResponseData {
};
}

/**
* And endpoint action created in Fleet's `.fleet-actions`
*/
export interface EndpointAction {
action_id: string;
'@timestamp': string;
Expand Down Expand Up @@ -136,11 +148,17 @@ export interface ActivityLogActionResponse {
data: EndpointActionResponse;
};
}

/**
* One of the possible Response Action Log entry - Either a Fleet Action request, Fleet action response,
* Endpoint action request and/or endpoint action response.
*/
export type ActivityLogEntry =
| ActivityLogAction
| ActivityLogActionResponse
| EndpointActivityLogAction
| EndpointActivityLogActionResponse;

export interface ActivityLog {
page: number;
pageSize: number;
Expand Down Expand Up @@ -168,3 +186,32 @@ export interface PendingActionsResponse {
}

export type PendingActionsRequestQuery = TypeOf<typeof ActionStatusRequestSchema.query>;

export interface ActionDetails {
/** The action id */
id: string;
/**
* The Endpoint ID (and fleet agent ID - they are the same) for which the action was created for.
* This is an Array because the action could have been sent to multiple endpoints.
*/
agents: string[];
/**
* The Endpoint type of action (ex. `isolate`, `release`) that is being requested to be
* performed on the endpoint
*/
command: string;
isExpired: boolean;
isCompleted: boolean;
/** The date when the initial action request was submitted */
startedAt: string;
/** The date when the action was completed (a response by the endpoint (not fleet) was received) */
completedAt: string | undefined;
/**
* The list of action log items (actions and responses) received thus far for the action.
*/
logEntries: ActivityLogEntry[];
}

export interface ActionDetailsApiResponse {
data: ActionDetails;
}
Loading

0 comments on commit cfefccd

Please sign in to comment.