Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add support to Action Responder script …
Browse files Browse the repository at this point in the history
…for `get-file` response action (#142663)

* Add `const`'s for file storage indexes

* Add file storage TS type

* Add support to Action Responder script for `get-file` response action

* Add support to action generator to output `get-file` responses

* correct `last` property of the upload chunk

* Fix type

* fix UI jest test failures

* Fix server jest tests and Generator bug
  • Loading branch information
paul-tavares authored Oct 5, 2022
1 parent 3469d64 commit 3fa8a87
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 17 deletions.
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const METADATA_UNITED_INDEX = '.metrics-endpoint.metadata_united_default'
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';

// File storage indexes supporting endpoint Upload/download
export const FILE_STORAGE_METADATA_INDEX = '.fleet-files';
export const FILE_STORAGE_DATA_INDEX = '.fleet-file_data';

// Endpoint API routes
export const BASE_ENDPOINT_ROUTE = '/api/endpoint';
export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`;
export const HOST_METADATA_GET_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/{id}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {
LogsEndpointAction,
LogsEndpointActionResponse,
ProcessesEntry,
EndpointActionDataParameterTypes,
ActionResponseOutput,
} from '../types';
import { ActivityLogItemTypes } from '../types';
import { RESPONSE_ACTION_COMMANDS } from '../service/response_actions/constants';
Expand Down Expand Up @@ -75,6 +77,32 @@ export class EndpointActionGenerator extends BaseDataGenerator {
);
});

const command = overrides?.EndpointActions?.data?.command ?? this.randomResponseActionCommand();
let parameters: EndpointActionDataParameterTypes = overrides?.EndpointActions?.data?.parameters;
let output: ActionResponseOutput = overrides?.EndpointActions?.data
?.output as ActionResponseOutput;

if (command === 'get-file') {
if (!parameters) {
parameters = {
file: '/some/path/bad_file.txt',
};
}

if (!output) {
output = {
type: 'json',
content: {
file: {
name: 'bad_file.txt',
path: '/some/path/bad_file.txt',
size: 221,
},
},
};
}
}

return merge(
{
'@timestamp': timeStamp.toISOString(),
Expand All @@ -84,14 +112,14 @@ export class EndpointActionGenerator extends BaseDataGenerator {
EndpointActions: {
action_id: this.seededUUIDv4(),
completed_at: timeStamp.toISOString(),
// randomly before a few hours/minutes/seconds later
started_at: new Date(startedAtTimes[this.randomN(startedAtTimes.length)]).toISOString(),
data: {
command: this.randomResponseActionCommand(),
command,
comment: '',
parameters: undefined,
parameters,
output,
},
// randomly before a few hours/minutes/seconds later
started_at: new Date(startedAtTimes[this.randomN(startedAtTimes.length)]).toISOString(),
output: undefined,
},
error: undefined,
},
Expand Down Expand Up @@ -160,7 +188,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
type: ActivityLogItemTypes.RESPONSE,
item: {
id: this.seededUUIDv4(),
data: this.generateResponse(),
data: this.generateResponse({ ...(overrides?.item?.data ?? {}) }),
},
},
overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export const RESPONSE_ACTION_COMMANDS = [
'kill-process',
'suspend-process',
'running-processes',
'get-file',
] as const;
export type ResponseActions = typeof RESPONSE_ACTION_COMMANDS[number];
21 changes: 16 additions & 5 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ interface EcsError {
type?: string;
}

interface EndpointActionFields<TOutputContent extends object = object> {
interface EndpointActionFields<
TParameters extends EndpointActionDataParameterTypes = never,
TOutputContent extends object = object
> {
action_id: string;
data: EndpointActionData<undefined, TOutputContent>;
data: EndpointActionData<TParameters, TOutputContent>;
}

interface ActionRequestFields {
Expand Down Expand Up @@ -98,12 +101,15 @@ export interface LogsEndpointAction {
* An Action response written by the endpoint to the Endpoint `.logs-endpoint.action.responses` datastream
* @since v7.16
*/
export interface LogsEndpointActionResponse<TOutputContent extends object = object> {
export interface LogsEndpointActionResponse<
TParameters extends EndpointActionDataParameterTypes = never,
TOutputContent extends object = object
> {
'@timestamp': string;
agent: {
id: string | string[];
};
EndpointActions: EndpointActionFields<TOutputContent> & ActionResponseFields;
EndpointActions: EndpointActionFields<TParameters, TOutputContent> & ActionResponseFields;
error?: EcsError;
}

Expand All @@ -121,9 +127,14 @@ export type ResponseActionParametersWithPidOrEntityId =
| ResponseActionParametersWithPid
| ResponseActionParametersWithEntityId;

export interface ResponseActionGetFileParameters {
file: string;
}

export type EndpointActionDataParameterTypes =
| undefined
| ResponseActionParametersWithPidOrEntityId;
| ResponseActionParametersWithPidOrEntityId
| ResponseActionGetFileParameters;

export interface EndpointActionData<
T extends EndpointActionDataParameterTypes = never,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.
*/

/**
* The Metadata information about a file that was uploaded by Endpoint
* as a result of a `get-file` response action
*/
export interface UploadedFile {
file: {
/** The chunk size used for each chunk in this file */
ChunkSize?: number;
/**
* - `AWAITING_UPLOAD`: file metadata has been created. File is ready to be uploaded.
* - `UPLOADING`: file contents are being uploaded.
* - `READY`: file has been uploaded, successfully, without errors.
* - `UPLOAD_ERROR`: an error happened while the file was being uploaded, file contents
* are most likely corrupted.
* - `DELETED`: file is deleted. Files can be marked as deleted before the actual deletion
* of the contents and metadata happens. Deleted files should be treated as if they don’t
* exist. Only files in READY state can transition into DELETED state.
*/
Status: 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED';
/** File extension (if any) */
extension?: string;
hash?: {
md5?: string;
sha1?: string;
sha256?: string;
sha384?: string;
sha512?: string;
ssdeep?: string;
tlsh?: string;
};
mime_type?: string;
mode?: string;
/** File name */
name: string;
/** The full path to the file on the host machine */
path: string;
/** The total size in bytes */
size: number;
created?: string;
type: string;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MANAGEMENT_PATH } from '../../../../common/constants';
import { getActionListMock } from './mocks';
import { useGetEndpointsList } from '../../hooks/endpoint/use_get_endpoints_list';
import uuid from 'uuid';
import { RESPONSE_ACTION_COMMANDS } from '../../../../common/endpoint/service/response_actions/constants';

let mockUseGetEndpointActionList: {
isFetched?: boolean;
Expand Down Expand Up @@ -556,10 +557,10 @@ describe('Response actions history', () => {
userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
expect(filterList).toBeTruthy();
expect(filterList.querySelectorAll('ul>li').length).toEqual(5);
expect(filterList.querySelectorAll('ul>li').length).toEqual(RESPONSE_ACTION_COMMANDS.length);
expect(
Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent)
).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes']);
).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes', 'get-file']);
});

it('should have `clear all` button `disabled` when no selected values', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ describe('Response actions history page', () => {
});

expect(history.location.search).toEqual(
'?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses'
'?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses%2Cget-file'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import type { KbnClient } from '@kbn/test';
import type { Client } from '@elastic/elasticsearch';
import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
import type { UploadedFile } from '../../../common/endpoint/types/file_storage';
import { sendEndpointMetadataUpdate } from '../common/endpoint_metadata_services';
import { FleetActionGenerator } from '../../../common/endpoint/data_generators/fleet_action_generator';
import {
ENDPOINT_ACTION_RESPONSES_INDEX,
ENDPOINTS_ACTION_LIST_ROUTE,
FILE_STORAGE_DATA_INDEX,
FILE_STORAGE_METADATA_INDEX,
} from '../../../common/endpoint/constants';
import type {
ActionDetails,
Expand Down Expand Up @@ -144,6 +147,40 @@ export const sendEndpointActionResponse = async (
}
}

// For `get-file`, upload a file to ES
if (action.command === 'get-file' && !endpointResponse.error) {
// Add the file's metadata
const fileMeta = await esClient.index<UploadedFile>({
index: FILE_STORAGE_METADATA_INDEX,
id: `${action.id}.${action.hosts[0]}`,
body: {
file: {
created: new Date().toISOString(),
extension: 'zip',
path: '/some/path/bad_file.txt',
type: 'file',
size: 221,
name: 'bad_file.txt.zip',
mime_type: 'application/zip',
Status: 'READY',
ChunkSize: 4194304,
},
},
refresh: 'wait_for',
});

await esClient.index({
index: FILE_STORAGE_DATA_INDEX,
id: `${fileMeta._id}.0`,
body: {
bid: fileMeta._id,
last: true,
data: 'UEsDBBQACAAIAFVeRFUAAAAAAAAAABMAAAAMACAAYmFkX2ZpbGUudHh0VVQNAAdTVjxjU1Y8Y1NWPGN1eAsAAQT1AQAABBQAAAArycgsVgCiRIWkxBSFtMycVC4AUEsHCKkCwMsTAAAAEwAAAFBLAQIUAxQACAAIAFVeRFWpAsDLEwAAABMAAAAMACAAAAAAAAAAAACkgQAAAABiYWRfZmlsZS50eHRVVA0AB1NWPGNTVjxjU1Y8Y3V4CwABBPUBAAAEFAAAAFBLBQYAAAAAAQABAFoAAABtAAAAAAA=',
},
refresh: 'wait_for',
});
}

return endpointResponse;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export const createActionResponsesEsSearchResultsMock = (
const fleetActionGenerator = new FleetActionGenerator('seed');

let hitSource: Array<
| estypes.SearchHit<EndpointActionResponse>
| estypes.SearchHit<LogsEndpointActionResponse<object>>
estypes.SearchHit<EndpointActionResponse> | estypes.SearchHit<LogsEndpointActionResponse>
> = [
fleetActionGenerator.generateResponseEsHit({
action_id: '123',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,18 @@ describe('When using Actions service utilities', () => {
completedAt: COMPLETED_AT,
wasSuccessful: true,
errors: undefined,
outputs: {},
outputs: {
'456': {
content: {
file: {
name: 'bad_file.txt',
path: '/some/path/bad_file.txt',
size: 221,
},
},
type: 'json',
},
},
agentState: {
'123': {
completedAt: '2022-01-05T19:27:23.816Z',
Expand Down

0 comments on commit 3fa8a87

Please sign in to comment.