[Security Solution][Endpoint] New API for creating a get-file response action request (#142671)

* Register API for creating a `get-file` response action request
* Change command parameter to `path` instead of `file`
* add tests for new `get-file` request route
* Moved some `const` from `common/endpoint/constants` to `common/endpoint/service/response_actions/constants`
* Renames Response Action const for clarity
* Changed `useActionHistoryUrlParams()` test to include all response actions
* Added `get_file` to endpoint capabilities and added support into the generator
* Adjusted types to use `ConsoleResponseActionCommands`
This commit is contained in:
Paul Tavares 2022-10-11 14:41:02 -04:00 committed by GitHub
parent db6dacaa7c
commit b5bacc3cbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 201 additions and 105 deletions

View file

@ -7,11 +7,10 @@
import './feature_table.scss';
import type { EuiAccordionProps, EuiButtonGroupOptionProps } from '@elastic/eui';
import {
EuiAccordion,
EuiAccordionProps,
EuiButtonGroup,
EuiButtonGroupOptionProps,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,

View file

@ -67,6 +67,7 @@ export const UNISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/unisolate`
export const GET_PROCESSES_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/running_procs`;
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`;
/** Endpoint Actions Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_log/{agent_id}`;
@ -78,26 +79,3 @@ export const failedFleetActionErrorCode = '424';
export const ENDPOINT_DEFAULT_PAGE = 0;
export const ENDPOINT_DEFAULT_PAGE_SIZE = 10;
/**
* The list of possible capabilities, reported by the endpoint in the metadata document
*/
export const RESPONDER_CAPABILITIES = [
'isolation',
'kill_process',
'suspend_process',
'running_processes',
] as const;
export type ResponderCapabilities = typeof RESPONDER_CAPABILITIES[number];
/** The list of possible responder command names **/
export const RESPONDER_COMMANDS = [
'isolate',
'release',
'kill-process',
'suspend-process',
'processes',
] as const;
export type ResponderCommands = typeof RESPONDER_COMMANDS[number];

View file

@ -22,7 +22,7 @@ import type {
ActionResponseOutput,
} from '../types';
import { ActivityLogItemTypes } from '../types';
import { RESPONSE_ACTION_COMMANDS } from '../service/response_actions/constants';
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../service/response_actions/constants';
export class EndpointActionGenerator extends BaseDataGenerator {
/** Generate a random endpoint Action request (isolate or unisolate) */
@ -245,6 +245,6 @@ export class EndpointActionGenerator extends BaseDataGenerator {
}
protected randomResponseActionCommand() {
return this.randomChoice(RESPONSE_ACTION_COMMANDS);
return this.randomChoice(RESPONSE_ACTION_API_COMMANDS_NAMES);
}
}

View file

@ -8,6 +8,7 @@
import type { DeepPartial } from 'utility-types';
import { merge } from 'lodash';
import { gte } from 'semver';
import type { EndpointCapabilities } from '../service/response_actions/constants';
import { BaseDataGenerator } from './base_data_generator';
import type { HostMetadataInterface, OSFields } from '../types';
import { EndpointStatus, HostPolicyResponseActionStatus } from '../types';
@ -23,13 +24,17 @@ export class EndpointMetadataGenerator extends BaseDataGenerator {
const agentVersion = overrides?.agent?.version ?? this.randomVersion();
const agentId = this.seededUUIDv4();
const isIsolated = this.randomBoolean(0.3);
const capabilities = ['isolation'];
const capabilities: EndpointCapabilities[] = ['isolation'];
// v8.4 introduced additional endpoint capabilities
if (gte(agentVersion, '8.4.0')) {
capabilities.push('kill_process', 'suspend_process', 'running_processes');
}
if (gte(agentVersion, '8.6.0')) {
capabilities.push('get_file');
}
const hostMetadataDoc: HostMetadataInterface = {
'@timestamp': ts,
event: {

View file

@ -17,7 +17,7 @@ import type {
EndpointActionResponse,
} from '../types';
import { ActivityLogItemTypes } from '../types';
import { RESPONSE_ACTION_COMMANDS } from '../service/response_actions/constants';
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../service/response_actions/constants';
export class FleetActionGenerator extends BaseDataGenerator {
/** Generate a random endpoint Action (isolate or unisolate) */
@ -143,6 +143,6 @@ export class FleetActionGenerator extends BaseDataGenerator {
}
protected randomResponseActionCommand() {
return this.randomChoice(RESPONSE_ACTION_COMMANDS);
return this.randomChoice(RESPONSE_ACTION_API_COMMANDS_NAMES);
}
}

View file

@ -9,7 +9,7 @@ import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { ENDPOINT_DEFAULT_PAGE_SIZE } from '../constants';
import {
RESPONSE_ACTION_COMMANDS,
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTION_STATUS,
} from '../service/response_actions/constants';
@ -78,7 +78,7 @@ export const ActionDetailsRequestSchema = {
// TODO: fix the odd TS error
const commandsSchema = schema.oneOf(
// @ts-expect-error TS2769: No overload matches this call
RESPONSE_ACTION_COMMANDS.map((command) => schema.literal(command))
RESPONSE_ACTION_API_COMMANDS_NAMES.map((command) => schema.literal(command))
);
// TODO: fix the odd TS error
@ -115,3 +115,13 @@ export const EndpointActionListRequestSchema = {
};
export type EndpointActionListRequestQuery = TypeOf<typeof EndpointActionListRequestSchema.query>;
export const EndpointActionGetFileSchema = {
body: schema.object({
...BaseActionRequestSchema,
parameters: schema.object({
path: schema.string({ minLength: 1 }),
}),
}),
};

View file

@ -7,7 +7,10 @@
export const RESPONSE_ACTION_STATUS = ['failed', 'pending', 'successful'] as const;
export type ResponseActionStatus = typeof RESPONSE_ACTION_STATUS[number];
export const RESPONSE_ACTION_COMMANDS = [
/**
* The Command names that are used in the API payload for the `{ command: '' }` attribute
*/
export const RESPONSE_ACTION_API_COMMANDS_NAMES = [
'isolate',
'unisolate',
'kill-process',
@ -15,4 +18,33 @@ export const RESPONSE_ACTION_COMMANDS = [
'running-processes',
'get-file',
] as const;
export type ResponseActions = typeof RESPONSE_ACTION_COMMANDS[number];
export type ResponseActionsApiCommandNames = typeof RESPONSE_ACTION_API_COMMANDS_NAMES[number];
/**
* The list of possible capabilities, reported by the endpoint in the metadata document
*/
export const ENDPOINT_CAPABILITIES = [
'isolation',
'kill_process',
'suspend_process',
'running_processes',
'get_file',
] as const;
export type EndpointCapabilities = typeof ENDPOINT_CAPABILITIES[number];
/**
* The list of possible console command names that generate a Response Action to be dispatched
* to the Endpoint. (FYI: not all console commands are response actions)
*/
export const CONSOLE_RESPONSE_ACTION_COMMANDS = [
'isolate',
'release',
'kill-process',
'suspend-process',
'processes',
'get-file',
] as const;
export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMANDS[number];

View file

@ -12,7 +12,10 @@ import type {
ResponseActionBodySchema,
KillOrSuspendProcessRequestSchema,
} from '../schema/actions';
import type { ResponseActionStatus, ResponseActions } from '../service/response_actions/constants';
import type {
ResponseActionStatus,
ResponseActionsApiCommandNames,
} from '../service/response_actions/constants';
export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
@ -140,7 +143,7 @@ export interface EndpointActionData<
T extends EndpointActionDataParameterTypes = never,
TOutputContent extends object = object
> {
command: ResponseActions;
command: ResponseActionsApiCommandNames;
comment?: string;
parameters?: T;
output?: ActionResponseOutput<TOutputContent>;
@ -282,7 +285,7 @@ export interface ActionDetails<TOutputContent extends object = object> {
* The Endpoint type of action (ex. `isolate`, `release`) that is being requested to be
* performed on the endpoint
*/
command: ResponseActions;
command: ResponseActionsApiCommandNames;
/**
* Will be set to true only if action is not yet completed and elapsed time has exceeded
* the request's expiration date

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../common/endpoint/service/response_actions/constants';
import type { HostMetadata, MaybeImmutable } from '../../../../common/endpoint/types';
export const useDoesEndpointSupportResponder = (
endpointMetadata: MaybeImmutable<HostMetadata> | undefined
): boolean => {
if (endpointMetadata) {
return RESPONDER_CAPABILITIES.every((capability) =>
return ENDPOINT_CAPABILITIES.every((capability) =>
endpointMetadata?.Endpoint.capabilities?.includes(capability)
);
}

View file

@ -36,7 +36,7 @@ import {
import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__';
import { allCasesPermissions } from '../../../cases_test_utils';
import { HostStatus } from '../../../../common/endpoint/types';
import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../common/endpoint/service/response_actions/constants';
jest.mock('../../../common/components/user_privileges');
@ -470,7 +470,7 @@ describe('take action dropdown', () => {
...getApiResponse().metadata,
Endpoint: {
...getApiResponse().metadata.Endpoint,
capabilities: [...RESPONDER_CAPABILITIES],
capabilities: [...ENDPOINT_CAPABILITIES],
},
},
host_status: HostStatus.UNENROLLED,

View file

@ -6,6 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
import type {
EndpointCapabilities,
ConsoleResponseActionCommands,
} from '../../../../common/endpoint/service/response_actions/constants';
import type { Command, CommandDefinition } from '../console';
import { IsolateActionResult } from './isolate_action';
import { ReleaseActionResult } from './release_action';
@ -16,10 +20,6 @@ import { GetProcessesActionResult } from './get_processes_action';
import type { ParsedArgData } from '../console/service/parsed_command_input';
import type { ImmutableArray } from '../../../../common/endpoint/types';
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../common/translations';
import type {
ResponderCapabilities,
ResponderCommands,
} from '../../../../common/endpoint/constants';
import { getCommandAboutInfo } from './get_command_about_info';
const emptyArgumentValidator = (argData: ParsedArgData): true | string => {
@ -45,7 +45,7 @@ const pidValidator = (argData: ParsedArgData): true | string => {
}
};
const commandToCapabilitiesMap = new Map<ResponderCommands, ResponderCapabilities>([
const commandToCapabilitiesMap = new Map<ConsoleResponseActionCommands, EndpointCapabilities>([
['isolate', 'isolation'],
['release', 'isolation'],
['kill-process', 'kill_process'],
@ -54,9 +54,9 @@ const commandToCapabilitiesMap = new Map<ResponderCommands, ResponderCapabilitie
]);
const capabilitiesValidator = (command: Command): true | string => {
const endpointCapabilities: ResponderCapabilities[] = command.commandDefinition.meta.capabilities;
const endpointCapabilities: EndpointCapabilities[] = command.commandDefinition.meta.capabilities;
const responderCapability = commandToCapabilitiesMap.get(
command.commandDefinition.name as ResponderCommands
command.commandDefinition.name as ConsoleResponseActionCommands
);
if (responderCapability) {
if (endpointCapabilities.includes(responderCapability)) {
@ -97,7 +97,7 @@ export const getEndpointResponseActionsConsoleCommands = ({
endpointAgentId: string;
endpointCapabilities: ImmutableArray<string>;
}): CommandDefinition[] => {
const doesEndpointSupportCommand = (commandName: ResponderCommands) => {
const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => {
const responderCapability = commandToCapabilitiesMap.get(commandName);
if (responderCapability) {
return endpointCapabilities.includes(responderCapability);

View file

@ -16,12 +16,12 @@ import { getEndpointResponseActionsConsoleCommands } from '../endpoint_response_
import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks';
import { enterConsoleCommand } from '../../console/mocks';
import { waitFor } from '@testing-library/react';
import type { ResponderCapabilities } from '../../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../../common/endpoint/constants';
import type { EndpointCapabilities } from '../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
describe('When using processes action from response actions console', () => {
let render: (
capabilities?: ResponderCapabilities[]
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
@ -34,7 +34,7 @@ describe('When using processes action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {

View file

@ -16,13 +16,13 @@ import { getEndpointResponseActionsConsoleCommands } from '../endpoint_response_
import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks';
import { enterConsoleCommand } from '../../console/mocks';
import { waitFor } from '@testing-library/react';
import type { ResponderCapabilities } from '../../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../../common/endpoint/constants';
import { getDeferred } from '../../../mocks/utils';
import type { EndpointCapabilities } from '../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
describe('When using isolate action from response actions console', () => {
let render: (
capabilities?: ResponderCapabilities[]
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
@ -35,7 +35,7 @@ describe('When using isolate action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {

View file

@ -16,12 +16,12 @@ import { getEndpointResponseActionsConsoleCommands } from '../endpoint_response_
import { enterConsoleCommand } from '../../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks';
import type { ResponderCapabilities } from '../../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../../common/endpoint/constants';
import type { EndpointCapabilities } from '../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
describe('When using the kill-process action from response actions console', () => {
let render: (
capabilities?: ResponderCapabilities[]
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
@ -34,7 +34,7 @@ describe('When using the kill-process action from response actions console', ()
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {

View file

@ -16,13 +16,13 @@ import { getEndpointResponseActionsConsoleCommands } from '../endpoint_response_
import { enterConsoleCommand } from '../../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks';
import type { ResponderCapabilities } from '../../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../../common/endpoint/constants';
import { getDeferred } from '../../../mocks/utils';
import type { EndpointCapabilities } from '../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
describe('When using the release action from response actions console', () => {
let render: (
capabilities?: ResponderCapabilities[]
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
@ -35,7 +35,7 @@ describe('When using the release action from response actions console', () => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {

View file

@ -16,12 +16,12 @@ import { getEndpointResponseActionsConsoleCommands } from '../endpoint_response_
import { enterConsoleCommand } from '../../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks';
import type { ResponderCapabilities } from '../../../../../common/endpoint/constants';
import { RESPONDER_CAPABILITIES } from '../../../../../common/endpoint/constants';
import type { EndpointCapabilities } from '../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
describe('When using the suspend-process action from response actions console', () => {
let render: (
capabilities?: ResponderCapabilities[]
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
@ -34,7 +34,7 @@ describe('When using the suspend-process action from response actions console',
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
render = async (capabilities: ResponderCapabilities[] = [...RESPONDER_CAPABILITIES]) => {
render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {

View file

@ -8,7 +8,7 @@
import { orderBy } from 'lodash/fp';
import React, { memo, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiPopoverTitle } from '@elastic/eui';
import type { ResponseActions } from '../../../../../common/endpoint/service/response_actions/constants';
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
import { ActionsLogFilterPopover } from './actions_log_filter_popover';
import { type FilterItems, type FilterName, useActionsLogFilter, getUiCommand } from './hooks';
import { ClearAllButton } from './clear_all_button';
@ -105,7 +105,9 @@ export const ActionsLogFilter = memo(
// update URL params
if (filterName === 'actions') {
setUrlActionsFilters(
selectedItems.map((item) => getUiCommand(item as ResponseActions)).join()
selectedItems
.map((item) => getUiCommand(item as ResponseActionsApiCommandNames))
.join()
);
} else if (filterName === 'hosts') {
setUrlHostsFilters(selectedItems.join());

View file

@ -11,11 +11,12 @@ import type {
OnRefreshChangeProps,
} from '@elastic/eui/src/components/date_picker/types';
import type {
ResponseActions,
ConsoleResponseActionCommands,
ResponseActionsApiCommandNames,
ResponseActionStatus,
} from '../../../../../common/endpoint/service/response_actions/constants';
import {
RESPONSE_ACTION_COMMANDS,
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTION_STATUS,
} from '../../../../../common/endpoint/service/response_actions/constants';
import type { DateRangePickerValues } from './actions_log_date_range_picker';
@ -131,8 +132,8 @@ export const getActionStatus = (status: ResponseActionStatus): string => {
* running-processes -> processes
*/
export const getUiCommand = (
command: ResponseActions
): Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes' => {
command: ResponseActionsApiCommandNames
): ConsoleResponseActionCommands => {
if (command === 'unisolate') {
return 'release';
} else if (command === 'running-processes') {
@ -148,8 +149,8 @@ export const getUiCommand = (
* processes -> running-processes
*/
export const getCommandKey = (
uiCommand: Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes'
): ResponseActions => {
uiCommand: ConsoleResponseActionCommands
): ResponseActionsApiCommandNames => {
if (uiCommand === 'release') {
return 'unisolate';
} else if (uiCommand === 'processes') {
@ -231,7 +232,7 @@ export const useActionsLogFilter = ({
}))
: isHostsFilter
? []
: RESPONSE_ACTION_COMMANDS.map((commandName) => ({
: RESPONSE_ACTION_API_COMMANDS_NAMES.map((commandName) => ({
key: commandName,
label: getUiCommand(commandName),
checked:

View file

@ -5,8 +5,18 @@
* 2.0.
*/
import { actionsLogFiltersFromUrlParams } from './use_action_history_url_params';
import type { ConsoleResponseActionCommands } from '../../../../../common/endpoint/service/response_actions/constants';
import { CONSOLE_RESPONSE_ACTION_COMMANDS } from '../../../../../common/endpoint/service/response_actions/constants';
describe('#actionsLogFiltersFromUrlParams', () => {
const getConsoleCommandsAsString = (): string => {
return [...CONSOLE_RESPONSE_ACTION_COMMANDS].sort().join(',');
};
const getConsoleCommandsAsArray = (): ConsoleResponseActionCommands[] => {
return [...CONSOLE_RESPONSE_ACTION_COMMANDS].sort();
};
it('should not use invalid command values from URL params', () => {
expect(actionsLogFiltersFromUrlParams({ commands: 'asa,was' })).toEqual({
commands: undefined,
@ -21,10 +31,10 @@ describe('#actionsLogFiltersFromUrlParams', () => {
it('should use valid command values from URL params', () => {
expect(
actionsLogFiltersFromUrlParams({
commands: 'kill-process,isolate,processes,release,suspend-process',
commands: getConsoleCommandsAsString(),
})
).toEqual({
commands: ['isolate', 'kill-process', 'processes', 'release', 'suspend-process'],
commands: getConsoleCommandsAsArray(),
endDate: undefined,
hosts: undefined,
startDate: undefined,
@ -62,7 +72,7 @@ describe('#actionsLogFiltersFromUrlParams', () => {
it('should use valid command and status along with given host, user and date values from URL params', () => {
expect(
actionsLogFiltersFromUrlParams({
commands: 'release,kill-process,isolate,processes,suspend-process',
commands: getConsoleCommandsAsString(),
statuses: 'successful,pending,failed',
hosts: 'host-1,host-2',
users: 'user-1,user-2',
@ -70,7 +80,7 @@ describe('#actionsLogFiltersFromUrlParams', () => {
endDate: '2022-09-12T08:30:33.140Z',
})
).toEqual({
commands: ['isolate', 'kill-process', 'processes', 'release', 'suspend-process'],
commands: getConsoleCommandsAsArray(),
endDate: '2022-09-12T08:30:33.140Z',
hosts: ['host-1', 'host-2'],
startDate: '2022-09-12T08:00:00.000Z',

View file

@ -6,10 +6,11 @@
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import type { ConsoleResponseActionCommands } from '../../../../../common/endpoint/service/response_actions/constants';
import {
RESPONSE_ACTION_COMMANDS,
RESPONSE_ACTION_API_COMMANDS_NAMES,
RESPONSE_ACTION_STATUS,
type ResponseActions,
type ResponseActionsApiCommandNames,
type ResponseActionStatus,
} from '../../../../../common/endpoint/service/response_actions/constants';
import { useUrlParams } from '../../../hooks/use_url_params';
@ -24,9 +25,7 @@ interface UrlParamsActionsLogFilters {
}
interface ActionsLogFiltersFromUrlParams {
commands?: Array<
Exclude<ResponseActions, 'unisolate' | 'running-processes'> | 'release' | 'processes'
>;
commands?: ConsoleResponseActionCommands[];
hosts?: string[];
statuses?: ResponseActionStatus[];
startDate?: string;
@ -61,7 +60,7 @@ export const actionsLogFiltersFromUrlParams = (
.split(',')
.reduce<Required<ActionsLogFiltersFromUrlParams>['commands']>((acc, curr) => {
if (
RESPONSE_ACTION_COMMANDS.includes(curr as ResponseActions) ||
RESPONSE_ACTION_API_COMMANDS_NAMES.includes(curr as ResponseActionsApiCommandNames) ||
curr === 'release' ||
curr === 'processes'
) {

View file

@ -20,7 +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';
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../common/endpoint/service/response_actions/constants';
let mockUseGetEndpointActionList: {
isFetched?: boolean;
@ -557,7 +557,9 @@ 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(RESPONSE_ACTION_COMMANDS.length);
expect(filterList.querySelectorAll('ul>li').length).toEqual(
RESPONSE_ACTION_API_COMMANDS_NAMES.length
);
expect(
Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent)
).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes', 'get-file']);

View file

@ -11,7 +11,7 @@ import type { CriteriaWithPagination } from '@elastic/eui';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type {
ResponseActions,
ResponseActionsApiCommandNames,
ResponseActionStatus,
} from '../../../../common/endpoint/service/response_actions/constants';
@ -142,7 +142,7 @@ export const ResponseActionsLog = memo<
(selectedCommands: string[]) => {
setQueryParams((prevState) => ({
...prevState,
commands: selectedCommands as ResponseActions[],
commands: selectedCommands as ResponseActionsApiCommandNames[],
}));
},
[setQueryParams]

View file

@ -50,11 +50,11 @@ import {
HOST_METADATA_LIST_ROUTE,
metadataTransformPrefix,
METADATA_UNITED_TRANSFORM,
RESPONDER_CAPABILITIES,
} from '../../../../../common/endpoint/constants';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../../common/components/user_privileges/user_privileges_context';
import { getUserPrivilegesMockDefaultValue } from '../../../../common/components/user_privileges/__mocks__';
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
// but sure enough it needs to be inline in this one file
@ -1019,7 +1019,7 @@ describe('when on the endpoint list page', () => {
...hosts[0].metadata,
Endpoint: {
...hosts[0].metadata.Endpoint,
capabilities: [...RESPONDER_CAPABILITIES],
capabilities: [...ENDPOINT_CAPABILITIES],
state: {
...hosts[0].metadata.Endpoint.state,
isolation: false,

View file

@ -19,7 +19,7 @@ import type { SecuritySolutionRequestHandlerContext } from '../../../types';
import type { EndpointAppContext } from '../../types';
import { errorHandler } from '../error_handler';
import type {
ResponseActions,
ResponseActionsApiCommandNames,
ResponseActionStatus,
} from '../../../../common/endpoint/service/response_actions/constants';
import { doesLogsEndpointActionsIndexExist } from '../../utils';
@ -28,8 +28,8 @@ const formatStringIds = (value: string | string[] | undefined): undefined | stri
typeof value === 'string' ? [value] : value;
const formatCommandValues = (
value: ResponseActions | ResponseActions[] | undefined
): undefined | ResponseActions[] => (typeof value === 'string' ? [value] : value);
value: ResponseActionsApiCommandNames | ResponseActionsApiCommandNames[] | undefined
): undefined | ResponseActionsApiCommandNames[] => (typeof value === 'string' ? [value] : value);
const formatStatusValues = (
value: ResponseActionStatus | ResponseActionStatus[]

View file

@ -43,6 +43,7 @@ import {
GET_PROCESSES_ROUTE,
ISOLATE_HOST_ROUTE,
UNISOLATE_HOST_ROUTE,
GET_FILE_ROUTE,
} from '../../../../common/endpoint/constants';
import type {
ActionDetails,
@ -412,6 +413,17 @@ describe('Response actions', () => {
expect(actionDoc.data.command).toEqual('running-processes');
});
it('sends the get-file command payload from the get file route', async () => {
const ctx = await callRoute(GET_FILE_ROUTE, {
body: { endpoint_ids: ['XYZ'], parameters: { path: '/one/two/three' } },
});
const actionDoc: EndpointAction = (
ctx.core.elasticsearch.client.asInternalUser.index.mock
.calls[0][0] as estypes.IndexRequest<EndpointAction>
).body!;
expect(actionDoc.data.command).toEqual('get-file');
});
describe('With endpoint data streams', () => {
it('handles unisolation', async () => {
const ctx = await callRoute(
@ -553,6 +565,33 @@ describe('Response actions', () => {
expect(responseBody.action).toBeUndefined();
});
it('handles get-file', async () => {
const ctx = await callRoute(
GET_FILE_ROUTE,
{
body: { endpoint_ids: ['XYZ'], parameters: { path: '/one/two/three' } },
},
{ endpointDsExists: true }
);
const indexDoc = ctx.core.elasticsearch.client.asInternalUser.index;
const actionDocs: [
{ index: string; body?: LogsEndpointAction },
{ index: string; body?: EndpointAction }
] = [
indexDoc.mock.calls[0][0] as estypes.IndexRequest<LogsEndpointAction>,
indexDoc.mock.calls[1][0] as estypes.IndexRequest<EndpointAction>,
];
expect(actionDocs[0].index).toEqual(ENDPOINT_ACTIONS_INDEX);
expect(actionDocs[1].index).toEqual(AGENT_ACTIONS_INDEX);
expect(actionDocs[0].body!.EndpointActions.data.command).toEqual('get-file');
expect(actionDocs[1].body!.data.command).toEqual('get-file');
expect(mockResponse.ok).toBeCalled();
const responseBody = mockResponse.ok.mock.calls[0][0]?.body as ResponseActionApiResponse;
expect(responseBody.action).toBeUndefined();
});
it('handles errors', async () => {
const ErrMessage = 'Uh oh!';
await callRoute(

View file

@ -18,6 +18,7 @@ import type { ResponseActionBodySchema } from '../../../../common/endpoint/schem
import {
NoParametersRequestSchema,
KillOrSuspendProcessRequestSchema,
EndpointActionGetFileSchema,
} from '../../../../common/endpoint/schema/actions';
import { APP_ID } from '../../../../common/constants';
import {
@ -32,6 +33,7 @@ import {
ISOLATE_HOST_ROUTE,
UNISOLATE_HOST_ROUTE,
ENDPOINT_ACTIONS_INDEX,
GET_FILE_ROUTE,
} from '../../../../common/endpoint/constants';
import type {
EndpointAction,
@ -42,7 +44,7 @@ import type {
LogsEndpointActionResponse,
ResponseActionParametersWithPidOrEntityId,
} from '../../../../common/endpoint/types';
import type { ResponseActions } from '../../../../common/endpoint/service/response_actions/constants';
import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants';
import type {
SecuritySolutionPluginRouter,
SecuritySolutionRequestHandlerContext,
@ -157,21 +159,35 @@ export function registerResponseActionRoutes(
responseActionRequestHandler(endpointContext, 'running-processes')
)
);
router.post(
{
path: GET_FILE_ROUTE,
validate: EndpointActionGetFileSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
withEndpointAuthz(
{ all: ['canWriteFileOperations'] },
logger,
responseActionRequestHandler(endpointContext, 'get-file')
)
);
}
const commandToFeatureKeyMap = new Map<ResponseActions, FeatureKeys>([
const commandToFeatureKeyMap = new Map<ResponseActionsApiCommandNames, FeatureKeys>([
['isolate', 'HOST_ISOLATION'],
['unisolate', 'HOST_ISOLATION'],
['kill-process', 'KILL_PROCESS'],
['suspend-process', 'SUSPEND_PROCESS'],
['running-processes', 'RUNNING_PROCESSES'],
['get-file', 'GET_FILE'],
]);
const returnActionIdCommands: ResponseActions[] = ['isolate', 'unisolate'];
const returnActionIdCommands: ResponseActionsApiCommandNames[] = ['isolate', 'unisolate'];
function responseActionRequestHandler<T extends EndpointActionDataParameterTypes>(
endpointContext: EndpointAppContext,
command: ResponseActions
command: ResponseActionsApiCommandNames
): RequestHandler<
unknown,
unknown,
@ -233,8 +249,7 @@ function responseActionRequestHandler<T extends EndpointActionDataParameterTypes
} as EndpointActionData<T>,
} as Omit<EndpointAction, 'agents' | 'user_id' | '@timestamp'>,
user: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: user!.username,
id: user ? user.username : 'unknown',
},
};

View file

@ -7,7 +7,7 @@
import type { ElasticsearchClient } from '@kbn/core/server';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ResponseActions } from '../../../../common/endpoint/service/response_actions/constants';
import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint/service/response_actions/constants';
import {
ENDPOINT_ACTIONS_DS,
ENDPOINT_ACTION_RESPONSES_DS,
@ -55,7 +55,7 @@ interface NormalizedActionRequest {
agents: string[];
createdBy: string;
createdAt: string;
command: ResponseActions;
command: ResponseActionsApiCommandNames;
comment?: string;
parameters?: EndpointActionDataParameterTypes;
}

View file

@ -21,6 +21,7 @@ const FEATURES = {
KILL_PROCESS: 'Kill process',
SUSPEND_PROCESS: 'Suspend process',
RUNNING_PROCESSES: 'Get running processes',
GET_FILE: 'Get file',
ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry',
} as const;