Skip to content

Commit

Permalink
[Security Solution][Endpoint] Response Action get-file api for retr…
Browse files Browse the repository at this point in the history
…ieving file information (#143911)

* service for getting file info

* Deleted UploadFile type (not used)

* Create new `UploadedFileInfo` type

* new API for returning file information

* add todo for ilm

* Add file status checking to the `ResponseActionFileDownloadLink` component

* Deleted unnecessary services for building the API urls for download and file info

* Added static methods to `BaseDataGenerator` (`toEsSearchHit()` + `toEsSearchResponse()`)

* Jest tests for `getFileInfo()` service

* Jest tests for the File Info API

* Jest test for `useGetFileInfo()` api

* update file storage indexes

* test outline for ResponseActionFileDownloadLink component

* updated get-file action response code

* Fix response acton http mock for file info api

* Tests for ResponseActionFileDownloadLink

* endpoint emulator: Update get-file upload metadata based on real endpoint upload

* Fix get-file console test

* Adjustments from code reviews

* Fix `queryKey` for `useGetFileInfo()` hook
  • Loading branch information
paul-tavares committed Oct 26, 2022
1 parent 5d2b2e5 commit d0e78ce
Show file tree
Hide file tree
Showing 20 changed files with 914 additions and 145 deletions.
5 changes: 3 additions & 2 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ 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';
export const FILE_STORAGE_METADATA_INDEX = '.fleet-endpoint-files';
export const FILE_STORAGE_DATA_INDEX = '.fleet-endpoint-file-data';

// Endpoint API routes
export const BASE_ENDPOINT_ROUTE = '/api/endpoint';
Expand Down Expand Up @@ -73,6 +73,7 @@ export const GET_FILE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/get_file`;
export const ENDPOINT_ACTION_LOG_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_log/{agent_id}`;
export const ACTION_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_status`;
export const ACTION_DETAILS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/{action_id}`;
export const ACTION_AGENT_FILE_INFO_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/{action_id}/{agent_id}/file`;
export const ACTION_AGENT_FILE_DOWNLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/{action_id}/{agent_id}/file/download`;
export const ENDPOINTS_ACTION_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/action`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,41 @@ const USERS = [
'Genevieve',
];

const toEsSearchHit = <T extends object = object>(
hitSource: T,
index: string = 'some-index'
): estypes.SearchHit<T> => {
return {
_index: index,
_id: '123',
_score: 1.0,
_source: hitSource,
};
};

const 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,
},
};
};

/**
* A generic base class to assist in creating domain specific data generators. It includes
* several general purpose random data generators for use within the class and exposes one
Expand Down Expand Up @@ -193,12 +228,17 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> {
hitSource: T,
index: string = 'some-index'
): estypes.SearchHit<T> {
return {
_index: index,
_id: this.seededUUIDv4(),
_score: 1.0,
_source: hitSource,
};
const hit = toEsSearchHit<T>(hitSource, index);
hit._id = this.seededUUIDv4();

return hit;
}

static toEsSearchHit<T extends object = object>(
hitSource: T,
index: string = 'some-index'
): estypes.SearchHit<T> {
return toEsSearchHit<T>(hitSource, index);
}

/**
Expand All @@ -209,23 +249,12 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> {
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,
},
};
return toEsSearchResponse<T>(hitsSource);
}

static toEsSearchResponse<T extends object = object>(
hitsSource: Array<estypes.SearchHit<T>>
): estypes.SearchResponse<T> {
return toEsSearchResponse<T>(hitsSource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
output = {
type: 'json',
content: {
code: 'ra_get-file_success',
code: 'ra_get-file_success_done',
path: '/some/path/bad_file.txt',
size: 1234,
zip_size: 123,
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/schema/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,13 @@ export const EndpointActionFileDownloadSchema = {
export type EndpointActionFileDownloadParams = TypeOf<
typeof EndpointActionFileDownloadSchema.params
>;

/** Schema that validates the file info API */
export const EndpointActionFileInfoSchema = {
params: schema.object({
action_id: schema.string({ minLength: 1 }),
agent_id: schema.string({ minLength: 1 }),
}),
};

export type EndpointActionFileInfoParams = TypeOf<typeof EndpointActionFileInfoSchema.params>;
10 changes: 10 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 @@ -6,6 +6,7 @@
*/

import type { TypeOf } from '@kbn/config-schema';
import type { FileJSON } from '@kbn/files-plugin/common';
import type {
ActionStatusRequestSchema,
NoParametersRequestSchema,
Expand Down Expand Up @@ -360,3 +361,12 @@ export interface ActionListApiResponse {
statuses: ResponseActionStatus[] | undefined;
total: number;
}

export type UploadedFileInfo = Pick<
FileJSON,
'name' | 'id' | 'mimeType' | 'size' | 'status' | 'created'
>;

export interface ActionFileInfoApiResponse {
data: UploadedFileInfo;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { GET_FILE_ROUTE } from '../../../../../common/endpoint/constants';
import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks';
import type { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { INSUFFICIENT_PRIVILEGES_FOR_COMMAND } from '../../../../common/translations';
import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser';

jest.mock('../../../../common/components/user_privileges');

Expand Down Expand Up @@ -128,15 +129,30 @@ describe('When using get-file action from response actions console', () => {
});

it('should display download link once action completes', async () => {
const actionDetailsApiResponseMock: ReturnType<typeof apiMocks.responseProvider.actionDetails> =
{
data: {
...apiMocks.responseProvider.actionDetails({
path: '/1',
} as HttpFetchOptionsWithPath).data,

completedAt: new Date().toISOString(),
command: 'get-file',
},
};
apiMocks.responseProvider.actionDetails.mockReturnValue(actionDetailsApiResponseMock);

await render();
enterConsoleCommand(renderResult, 'get-file --path="one/two"');

await waitFor(() => {
expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled();
});

expect(renderResult.getByTestId('getFileSuccess').textContent).toEqual(
'File retrieved from the host.Click here to download(ZIP file passcode: elastic)'
);
await waitFor(() => {
expect(renderResult.getByTestId('getFileSuccess').textContent).toEqual(
'File retrieved from the host.Click here to download(ZIP file passcode: elastic)'
);
});
});
});
Loading

0 comments on commit d0e78ce

Please sign in to comment.