Skip to content

Commit

Permalink
[Security Solution][Endpoint] Upload response action create API (re-c…
Browse files Browse the repository at this point in the history
…ommit) (#157182)

## Summary

This is a re-commit of PR #156303 which was reverted due to a type error
that slipped passed the last PR.
  • Loading branch information
paul-tavares authored May 9, 2023
1 parent b16af48 commit a5ac5b6
Show file tree
Hide file tree
Showing 45 changed files with 964 additions and 212 deletions.
5 changes: 4 additions & 1 deletion src/plugins/files/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { KibanaRequest } from '@kbn/core/server';
import { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import * as stream from 'stream';
import { clone } from 'lodash';
import { File } from '../common';
import { FileClient, FileServiceFactory, FileServiceStart, FilesSetup } from '.';

Expand Down Expand Up @@ -56,7 +57,9 @@ export const createFileMock = (): DeeplyMockedKeys<File> => {
share: jest.fn(),
listShares: jest.fn(),
unshare: jest.fn(),
toJSON: jest.fn(),
toJSON: jest.fn(() => {
return clone(fileMock.data);
}),
};

fileMock.update.mockResolvedValue(fileMock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const KILL_PROCESS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/kill_process`;
export const SUSPEND_PROCESS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/suspend_process`;
export const GET_FILE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/get_file`;
export const EXECUTE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/execute`;
export const UPLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/upload`;

/** Endpoint Actions Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_log/{agent_id}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
ResponseActionGetFileParameters,
ResponseActionsExecuteParameters,
ResponseActionExecuteOutputContent,
ResponseActionUploadOutputContent,
ResponseActionUploadParameters,
} from '../types';
import { ActivityLogItemTypes } from '../types';
import {
Expand Down Expand Up @@ -239,6 +241,24 @@ export class EndpointActionGenerator extends BaseDataGenerator {
}
}

if (command === 'upload') {
if (!details.parameters) {
(
details as ActionDetails<
ResponseActionUploadOutputContent,
ResponseActionUploadParameters
>
).parameters = {
file: {
file_id: 'file-x-y-z',
file_name: 'foo.txt',
size: 1234,
sha256: 'file-hash-sha-256',
},
};
}
}

return merge(details, overrides as ActionDetails) as unknown as ActionDetails<
TOutputType,
TParameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export class EndpointMetadataGenerator extends BaseDataGenerator {
capabilities.push('execute');
}

// v8.9 introduced `upload` capability
if (gte(agentVersion, '8.9.0')) {
capabilities.push('upload_file');
}

const hostMetadataDoc: HostMetadataInterface = {
'@timestamp': ts,
event: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
NoParametersRequestSchema,
KillOrSuspendProcessRequestSchema,
ExecuteActionRequestSchema,
UploadActionRequestSchema,
} from './actions';
import { createHapiReadableStreamMock } from '../../../server/endpoint/services/actions/mocks';
import type { HapiReadableStream } from '../../../server/types';

describe('actions schemas', () => {
describe('Endpoint action list API Schema', () => {
Expand Down Expand Up @@ -639,4 +642,56 @@ describe('actions schemas', () => {
}).not.toThrow();
});
});

describe(`UploadActionRequestSchema`, () => {
let fileStream: HapiReadableStream;

beforeEach(() => {
fileStream = createHapiReadableStreamMock();
});

it('should not error if `override` parameter is not defined', () => {
expect(() => {
UploadActionRequestSchema.body.validate({
endpoint_ids: ['endpoint_id'],
file: fileStream,
});
}).not.toThrow();
});

it('should allow `override` parameter', () => {
expect(() => {
UploadActionRequestSchema.body.validate({
endpoint_ids: ['endpoint_id'],
parameters: {
overwrite: true,
},
file: fileStream,
});
}).not.toThrow();
});

it('should error if `file` is not defined', () => {
expect(() => {
UploadActionRequestSchema.body.validate({
endpoint_ids: ['endpoint_id'],
parameters: {
override: true,
},
});
}).toThrow();
});

it('should error if `file` is not a Stream', () => {
expect(() => {
UploadActionRequestSchema.body.validate({
endpoint_ids: ['endpoint_id'],
parameters: {
overwrite: true,
},
file: {},
});
}).toThrow();
});
});
});
14 changes: 14 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 @@ -239,3 +239,17 @@ export const ResponseActionBodySchema = schema.oneOf([
EndpointActionGetFileSchema.body,
ExecuteActionRequestSchema.body,
]);

export const UploadActionRequestSchema = {
body: schema.object({
...BaseActionRequestSchema,

parameters: schema.object({
overwrite: schema.maybe(schema.boolean()),
}),

file: schema.stream(),
}),
};

export type UploadActionRequestBody = TypeOf<typeof UploadActionRequestSchema.body>;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const RESPONSE_ACTION_API_COMMANDS_NAMES = [
'running-processes',
'get-file',
'execute',
'upload',
] as const;

export type ResponseActionsApiCommandNames = typeof RESPONSE_ACTION_API_COMMANDS_NAMES[number];
Expand All @@ -36,6 +37,7 @@ export const ENDPOINT_CAPABILITIES = [
'running_processes',
'get_file',
'execute',
'upload_file',
] as const;

export type EndpointCapabilities = typeof ENDPOINT_CAPABILITIES[number];
Expand All @@ -52,6 +54,7 @@ export const CONSOLE_RESPONSE_ACTION_COMMANDS = [
'processes',
'get-file',
'execute',
'upload',
] as const;

export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMANDS[number];
Expand All @@ -74,6 +77,7 @@ export const commandToRBACMap: Record<ConsoleResponseActionCommands, ResponseCon
processes: 'writeProcessOperations',
'get-file': 'writeFileOperations',
execute: 'writeExecuteOperations',
upload: 'writeFileOperations',
});

export const RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP = Object.freeze<
Expand All @@ -86,6 +90,7 @@ export const RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP = Object.freeze
'running-processes': 'processes',
'kill-process': 'kill-process',
'suspend-process': 'suspend-process',
upload: 'upload',
});

// 4 hrs in seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
NoParametersRequestSchema,
ResponseActionBodySchema,
KillOrSuspendProcessRequestSchema,
UploadActionRequestBody,
} from '../schema/actions';
import type {
ResponseActionStatus,
Expand Down Expand Up @@ -176,7 +177,8 @@ export type EndpointActionDataParameterTypes =
| undefined
| ResponseActionParametersWithPidOrEntityId
| ResponseActionsExecuteParameters
| ResponseActionGetFileParameters;
| ResponseActionGetFileParameters
| ResponseActionUploadParameters;

export interface EndpointActionData<
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
Expand Down Expand Up @@ -468,3 +470,25 @@ export type UploadedFileInfo = Pick<
export interface ActionFileInfoApiResponse {
data: UploadedFileInfo;
}

/**
* The parameters that are sent to the Endpoint.
*
* NOTE: Most of the parameters below are NOT accepted via the API. They are inserted into
* the action's parameters via the API route handler
*/
export type ResponseActionUploadParameters = UploadActionRequestBody['parameters'] & {
file: {
sha256: string;
size: number;
file_name: string;
file_id: string;
};
};

export interface ResponseActionUploadOutputContent {
/** Full path to the file on the host machine where it was saved */
path: string;
/** The free space available (after saving the file) of the drive where the file was saved to, In Bytes */
disk_free_space: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
responseActionExecuteEnabled: true,

/**
* Enables the `upload` endpoint response action
*/
responseActionUploadEnabled: false,

/**
* Enables top charts on Alerts Page
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,20 +234,21 @@ export const useActionsLogFilter = ({
: isHostsFilter
? []
: RESPONSE_ACTION_API_COMMANDS_NAMES.filter((commandName) => {
const featureFlags = ExperimentalFeaturesService.get();

// `get-file` is currently behind FF
if (
commandName === 'get-file' &&
!ExperimentalFeaturesService.get().responseActionGetFileEnabled
) {
if (commandName === 'get-file' && !featureFlags.responseActionGetFileEnabled) {
return false;
}

// TODO: remove this when `execute` is no longer behind FF
// planned for 8.8
if (
commandName === 'execute' &&
!ExperimentalFeaturesService.get().responseActionExecuteEnabled
) {
if (commandName === 'execute' && !featureFlags.responseActionExecuteEnabled) {
return false;
}

// upload - v8.9
if (commandName === 'upload' && !featureFlags.responseActionUploadEnabled) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,6 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {

jest.mock('../../../hooks/endpoint/use_get_endpoints_list');

jest.mock('../../../../common/experimental_features_service');

jest.mock('../../../../common/components/user_privileges');
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;

Expand Down Expand Up @@ -917,6 +915,7 @@ describe('Response actions history', () => {
});

it('should show a list of actions when opened', () => {
mockedContext.setExperimentalFlag({ responseActionUploadEnabled: true });
render();
const { getByTestId, getAllByTestId } = renderResult;

Expand All @@ -934,6 +933,7 @@ describe('Response actions history', () => {
'processes',
'get-file',
'execute',
'upload',
]);
});

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/server/config.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const createMockConfig = (): ConfigType => {
'responseActionGetFileEnabled',
// remove property below once `execute` FF is enabled or removed
'responseActionExecuteEnabled',
'responseActionUploadEnabled',
];

return {
Expand All @@ -29,6 +30,7 @@ export const createMockConfig = (): ConfigType => {
prebuiltRulesPackageVersion: '',
alertMergeStrategy: 'missingFields',
alertIgnoreFields: [],
maxUploadResponseActionFileBytes: 26214400,

experimentalFeatures: parseExperimentalConfigValue(enableExperimental),
enabled: true,
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/security_solution/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ export const configSchema = schema.object({
*/
prebuiltRulesPackageVersion: schema.maybe(schema.string()),
enabled: schema.boolean({ defaultValue: true }),

/**
* The Max number of Bytes allowed for the `upload` endpoint response action
*/
maxUploadResponseActionFileBytes: schema.number({
defaultValue: 26214400, // 25MB,
max: 104857600, // 100MB,
}),
});

export type ConfigSchema = TypeOf<typeof configSchema>;
Expand Down
Loading

0 comments on commit a5ac5b6

Please sign in to comment.