[security solution][endpoint] Add new experimental feature flag for get-file and use it hide/display get-file response action (#145042)

## Summary

- Adds new experimental feature flag that controls the availability of
the `get-file` response action
- UI updated to remove `get-file` from the console if FF is `false`
- Server APIs updated to not register `get-file` related APIs if FF is
`false`
- Hides the "File Operation" kibana feature privilege
This commit is contained in:
Paul Tavares 2022-11-14 10:41:52 -05:00 committed by GitHub
parent 843eefa7a7
commit 9fda59f512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 105 additions and 24 deletions

View file

@ -81,6 +81,11 @@ export const allowedExperimentalValues = Object.freeze({
* Enables the alert details page currently only accessible via the alert details flyout and alert table context menu
*/
alertDetailsPageEnabled: false,
/**
* Enables the `get-file` endpoint response action
*/
responseActionGetFileEnabled: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -0,0 +1,25 @@
/*
* 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.
*/
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import { allowedExperimentalValues } from '../../../common/experimental_features';
const ExperimentalFeaturesServiceMock = {
init: jest.fn(),
get: jest.fn(() => {
const ff: ExperimentalFeatures = {
...allowedExperimentalValues,
responseActionGetFileEnabled: true,
};
return ff;
}),
};
export { ExperimentalFeaturesServiceMock as ExperimentalFeaturesService };

View file

@ -30,6 +30,7 @@ import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser';
import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes';
jest.mock('../../../../../common/components/user_privileges');
jest.mock('../../../../../common/experimental_features_service');
describe('When using get-file action from response actions console', () => {
let render: (

View file

@ -20,6 +20,8 @@ import { getEndpointAuthzInitialState } from '../../../../../../common/endpoint/
import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants';
jest.mock('../../../../../common/experimental_features_service');
describe('When using processes action from response actions console', () => {
let render: (
capabilities?: EndpointCapabilities[]

View file

@ -21,6 +21,8 @@ import { getEndpointAuthzInitialState } from '../../../../../../common/endpoint/
import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants';
jest.mock('../../../../../common/experimental_features_service');
describe('When using isolate action from response actions console', () => {
let render: (
capabilities?: EndpointCapabilities[]

View file

@ -25,6 +25,8 @@ import type {
} from '../../../../../../common/endpoint/types';
import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes';
jest.mock('../../../../../common/experimental_features_service');
describe('When using the kill-process action from response actions console', () => {
let render: (
capabilities?: EndpointCapabilities[]

View file

@ -21,6 +21,8 @@ import { getEndpointAuthzInitialState } from '../../../../../../common/endpoint/
import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants';
import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants';
jest.mock('../../../../../common/experimental_features_service');
describe('When using the release action from response actions console', () => {
let render: (
capabilities?: EndpointCapabilities[]

View file

@ -25,6 +25,8 @@ import type {
} from '../../../../../../common/endpoint/types';
import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes';
jest.mock('../../../../../common/experimental_features_service');
describe('When using the suspend-process action from response actions console', () => {
let render: (
capabilities?: EndpointCapabilities[]

View file

@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { ExperimentalFeaturesService } from '../../../../common/experimental_features_service';
import type {
EndpointCapabilities,
ConsoleResponseActionCommands,
@ -134,6 +135,8 @@ export const getEndpointConsoleCommands = ({
endpointCapabilities: ImmutableArray<string>;
endpointPrivileges: EndpointPrivileges;
}): CommandDefinition[] => {
const isGetFileEnabled = ExperimentalFeaturesService.get().responseActionGetFileEnabled;
const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => {
const responderCapability = commandToCapabilitiesMap.get(commandName);
if (responderCapability) {
@ -142,7 +145,7 @@ export const getEndpointConsoleCommands = ({
return false;
};
return [
const consoleCommands: CommandDefinition[] = [
{
name: 'isolate',
about: getCommandAboutInfo({
@ -365,7 +368,11 @@ export const getEndpointConsoleCommands = ({
helpDisabled: doesEndpointSupportCommand('processes') === false,
helpHidden: !getRbacControl({ commandName: 'processes', privileges: endpointPrivileges }),
},
{
];
// `get-file` is currently behind feature flag
if (isGetFileEnabled) {
consoleCommands.push({
name: 'get-file',
about: getCommandAboutInfo({
aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.getFile.about', {
@ -411,6 +418,8 @@ export const getEndpointConsoleCommands = ({
commandName: 'get-file',
privileges: endpointPrivileges,
}),
},
];
});
}
return consoleCommands;
};

View file

@ -10,6 +10,7 @@ import type {
DurationRange,
OnRefreshChangeProps,
} from '@elastic/eui/src/components/date_picker/types';
import { ExperimentalFeaturesService } from '../../../../common/experimental_features_service';
import type {
ConsoleResponseActionCommands,
ResponseActionsApiCommandNames,
@ -232,7 +233,17 @@ export const useActionsLogFilter = ({
}))
: isHostsFilter
? []
: RESPONSE_ACTION_API_COMMANDS_NAMES.map((commandName) => ({
: RESPONSE_ACTION_API_COMMANDS_NAMES.filter((commandName) => {
// `get-file` is currently behind FF
if (
commandName === 'get-file' &&
!ExperimentalFeaturesService.get().responseActionGetFileEnabled
) {
return false;
}
return true;
}).map((commandName) => ({
key: commandName,
label: getUiCommand(commandName),
checked:

View file

@ -119,6 +119,8 @@ 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');
let mockUseGetFileInfo: {

View file

@ -20,6 +20,8 @@ import { MANAGEMENT_PATH } from '../../../../../common/constants';
import { getActionListMock } from '../../../components/endpoint_response_actions_list/mocks';
import { useGetEndpointsList } from '../../../hooks/endpoint/use_get_endpoints_list';
jest.mock('../../../../common/experimental_features_service');
let mockUseGetEndpointActionList: {
isFetched?: boolean;
isFetching?: boolean;

View file

@ -11,7 +11,10 @@ import { parseExperimentalConfigValue } from '../common/experimental_features';
import type { ConfigType } from './config';
export const createMockConfig = (): ConfigType => {
const enableExperimental: string[] = [];
const enableExperimental: Array<keyof ExperimentalFeatures> = [
// Remove property below once `get-file` FF is enabled or removed
'responseActionGetFileEnabled',
];
return {
[SIGNALS_INDEX_KEY]: DEFAULT_SIGNALS_INDEX,

View file

@ -25,7 +25,11 @@ export function registerActionRoutes(
registerActionAuditLogRoutes(router, endpointContext);
registerActionListRoutes(router, endpointContext);
registerActionDetailsRoutes(router, endpointContext);
registerActionFileDownloadRoutes(router, endpointContext);
registerResponseActionRoutes(router, endpointContext);
registerActionFileInfoRoute(router, endpointContext);
// APIs specific to `get-file` are behind FF
if (endpointContext.experimentalFeatures.responseActionGetFileEnabled) {
registerActionFileDownloadRoutes(router, endpointContext);
registerActionFileInfoRoute(router, endpointContext);
}
}

View file

@ -160,18 +160,21 @@ export function registerResponseActionRoutes(
)
);
router.post(
{
path: GET_FILE_ROUTE,
validate: EndpointActionGetFileSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
withEndpointAuthz(
{ all: ['canWriteFileOperations'] },
logger,
responseActionRequestHandler(endpointContext, 'get-file')
)
);
// `get-file` currently behind FF
if (endpointContext.experimentalFeatures.responseActionGetFileEnabled) {
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<ResponseActionsApiCommandNames, FeatureKeys>([

View file

@ -495,15 +495,21 @@ const subFeatures: SubFeatureConfig[] = [
];
function getSubFeatures(experimentalFeatures: ConfigType['experimentalFeatures']) {
let filteredSubFeatures: SubFeatureConfig[] = [];
if (experimentalFeatures.endpointRbacEnabled) {
return subFeatures;
filteredSubFeatures = subFeatures;
} else if (experimentalFeatures.endpointRbacV1Enabled) {
filteredSubFeatures = responseActionSubFeatures;
}
if (experimentalFeatures.endpointRbacV1Enabled) {
return responseActionSubFeatures;
if (!experimentalFeatures.responseActionGetFileEnabled) {
filteredSubFeatures = filteredSubFeatures.filter((subFeat) => {
return subFeat.name !== 'File Operations';
});
}
return [];
return filteredSubFeatures;
}
export const getKibanaPrivilegesFeaturePrivileges = (