mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security solution][Endpoint] Remove unnecessary experimental feature flags from plugin config (#158969)
## Summary - Changes the behaviour of the validation of `xpack.securitySolution.enableExperimental` values (defined in the `kibana.yml`) so that unknown/unsupported values will no longer prevent the application from starting (will no longer `throw`) - This change is only done in the config schema validation - Plugin's `setup` will now output a message (warning) to the log if it finds experimental feature values that are not supported. - Removes the following experimental feature flags (no longer needed): - `policyListEnabled` - `diableIsolationUIPendingStatuses` - `responseActionsConsoleEnabled` - `endpointRbacEnabled` - `endpointRbacV1Enabled` - `responseActionGetFileEnabled` - `responseActionExecuteEnabled` - `pendingActionResponsesWithAck` - `policyResponseInFleetEnabled` - `riskyUsersEnabled` - `riskyHostsEnabled` -------- Message to kibana log when unsupported values are defined: ``` [2023-06-02T16:37:48.997-04:00][WARN ][plugins.securitySolution.config] Unsupported "xpack.securitySolution.enableExperimental" values detected. The following configuration values are no longer supported and should be removed from the kibana configuration file: xpack.securitySolution.enableExperimental: - endpointRbacEnabled - responseActionGetFileEnabled - responseActionExecuteEnabled ```
This commit is contained in:
parent
9d15681cbc
commit
3bb4edf4ce
40 changed files with 394 additions and 783 deletions
|
@ -14,12 +14,11 @@ export type ExperimentalFeatures = { [K in keyof typeof allowedExperimentalValue
|
|||
export const allowedExperimentalValues = Object.freeze({
|
||||
tGridEnabled: true,
|
||||
tGridEventRenderedViewEnabled: true,
|
||||
|
||||
// FIXME:PT delete?
|
||||
excludePoliciesInFilterEnabled: false,
|
||||
|
||||
kubernetesEnabled: true,
|
||||
disableIsolationUIPendingStatuses: false,
|
||||
pendingActionResponsesWithAck: true,
|
||||
policyListEnabled: true,
|
||||
policyResponseInFleetEnabled: true,
|
||||
chartEmbeddablesEnabled: true,
|
||||
donutChartEmbeddablesEnabled: false, // Depends on https://github.com/elastic/kibana/issues/136409 item 2 - 6
|
||||
alertsPreviewChartEmbeddablesEnabled: false, // Depends on https://github.com/elastic/kibana/issues/136409 item 9
|
||||
|
@ -33,11 +32,6 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
previewTelemetryUrlEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the Endpoint response actions console in various areas of the app
|
||||
*/
|
||||
responseActionsConsoleEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables the insights module for related alerts by process ancestry
|
||||
*/
|
||||
|
@ -73,33 +67,13 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
endpointResponseActionsEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables endpoint package level rbac
|
||||
*/
|
||||
endpointRbacEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables endpoint package level rbac for response actions only.
|
||||
* if endpointRbacEnabled is enabled, it will take precedence.
|
||||
*/
|
||||
endpointRbacV1Enabled: true,
|
||||
/**
|
||||
* 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: true,
|
||||
|
||||
/**
|
||||
* Enables the `execute` endpoint response action
|
||||
*/
|
||||
responseActionExecuteEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables the `upload` endpoint response action
|
||||
* Enables the `upload` endpoint response action (v8.9)
|
||||
*/
|
||||
responseActionUploadEnabled: false,
|
||||
|
||||
|
@ -153,7 +127,6 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
const SecuritySolutionInvalidExperimentalValue = class extends Error {};
|
||||
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
|
||||
|
||||
/**
|
||||
|
@ -163,25 +136,27 @@ const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<Experimen
|
|||
* @param configValue
|
||||
* @throws SecuritySolutionInvalidExperimentalValue
|
||||
*/
|
||||
export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => {
|
||||
export const parseExperimentalConfigValue = (
|
||||
configValue: string[]
|
||||
): { features: ExperimentalFeatures; invalid: string[] } => {
|
||||
const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {};
|
||||
const invalidKeys: string[] = [];
|
||||
|
||||
for (const value of configValue) {
|
||||
if (!isValidExperimentalValue(value)) {
|
||||
throw new SecuritySolutionInvalidExperimentalValue(`[${value}] is not valid.`);
|
||||
if (!allowedKeys.includes(value as keyof ExperimentalFeatures)) {
|
||||
invalidKeys.push(value);
|
||||
} else {
|
||||
enabledFeatures[value as keyof ExperimentalFeatures] = true;
|
||||
}
|
||||
|
||||
enabledFeatures[value as keyof ExperimentalFeatures] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...allowedExperimentalValues,
|
||||
...enabledFeatures,
|
||||
features: {
|
||||
...allowedExperimentalValues,
|
||||
...enabledFeatures,
|
||||
},
|
||||
invalid: invalidKeys,
|
||||
};
|
||||
};
|
||||
|
||||
export const isValidExperimentalValue = (value: string): value is keyof ExperimentalFeatures => {
|
||||
return allowedKeys.includes(value as keyof ExperimentalFeatures);
|
||||
};
|
||||
|
||||
export const getExperimentalAllowedValues = (): string[] => [...allowedKeys];
|
||||
|
|
|
@ -22,7 +22,6 @@ import { HOST_STATUS_TO_BADGE_COLOR } from '../../../../management/pages/endpoin
|
|||
import { getEmptyValue } from '../../empty_value';
|
||||
import type { ResponseActionsApiCommandNames } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { RESPONSE_ACTION_API_COMMANDS_TO_CONSOLE_COMMAND_MAP } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||
import { useGetEndpointPendingActionsSummary } from '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary';
|
||||
import { useTestIdGenerator } from '../../../../management/hooks/use_test_id_generator';
|
||||
import type { HostInfo, EndpointPendingActions } from '../../../../../common/endpoint/types';
|
||||
|
@ -187,9 +186,6 @@ interface EndpointHostResponseActionsStatusProps {
|
|||
const EndpointHostResponseActionsStatus = memo<EndpointHostResponseActionsStatusProps>(
|
||||
({ pendingActions, isIsolated, 'data-test-subj': dataTestSubj }) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
const isPendingStatusDisabled = useIsExperimentalFeatureEnabled(
|
||||
'disableIsolationUIPendingStatuses'
|
||||
);
|
||||
|
||||
interface PendingActionsState {
|
||||
actionList: Array<{ label: string; count: number }>;
|
||||
|
@ -269,15 +265,6 @@ const EndpointHostResponseActionsStatus = memo<EndpointHostResponseActionsStatus
|
|||
);
|
||||
}, [dataTestSubj]);
|
||||
|
||||
if (isPendingStatusDisabled) {
|
||||
// If nothing is pending and host is not currently isolated, then render nothing
|
||||
if (!isIsolated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isolatedBadge;
|
||||
}
|
||||
|
||||
// If nothing is pending
|
||||
if (totalPending === 0) {
|
||||
// and host is either releasing and or currently released, then render nothing
|
||||
|
|
|
@ -36,9 +36,6 @@ jest.mock('../../../hooks/use_license', () => {
|
|||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../../../hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn((feature: string) => feature === 'endpointRbacEnabled'),
|
||||
}));
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const useHttpMock = _useHttp as jest.Mock;
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
getEndpointAuthzInitialState,
|
||||
} from '../../../../../common/endpoint/service/authz';
|
||||
import { useSecuritySolutionStartDependencies } from './security_solution_start_dependencies';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||
|
||||
/**
|
||||
* Retrieve the endpoint privileges for the current user.
|
||||
|
@ -49,9 +48,6 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
|
|||
const [userRolesCheckDone, setUserRolesCheckDone] = useState<boolean>(false);
|
||||
const [userRoles, setUserRoles] = useState<MaybeImmutable<string[]>>([]);
|
||||
|
||||
const isEndpointRbacEnabled = useIsExperimentalFeatureEnabled('endpointRbacEnabled');
|
||||
const isEndpointRbacV1Enabled = useIsExperimentalFeatureEnabled('endpointRbacV1Enabled');
|
||||
|
||||
const [checkHostIsolationExceptionsDone, setCheckHostIsolationExceptionsDone] =
|
||||
useState<boolean>(false);
|
||||
const [hasHostIsolationExceptionsItems, setHasHostIsolationExceptionsItems] =
|
||||
|
@ -67,7 +63,7 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
|
|||
licenseService,
|
||||
fleetAuthz,
|
||||
userRoles,
|
||||
isEndpointRbacEnabled || isEndpointRbacV1Enabled,
|
||||
true,
|
||||
hasHostIsolationExceptionsItems
|
||||
)
|
||||
: getEndpointAuthzInitialState()),
|
||||
|
@ -81,8 +77,6 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
|
|||
fleetAuthz,
|
||||
licenseService,
|
||||
userRoles,
|
||||
isEndpointRbacEnabled,
|
||||
isEndpointRbacV1Enabled,
|
||||
hasHostIsolationExceptionsItems,
|
||||
]);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('createInitialState', () => {
|
|||
>;
|
||||
const defaultState = {
|
||||
defaultDataView: mockSourcererState.defaultDataView,
|
||||
enableExperimental: parseExperimentalConfigValue([]),
|
||||
enableExperimental: parseExperimentalConfigValue([]).features,
|
||||
kibanaDataViews: [mockSourcererState.defaultDataView],
|
||||
signalIndexName: 'siem-signals-default',
|
||||
};
|
||||
|
|
|
@ -13,16 +13,12 @@ import {
|
|||
isTimelineEventItemAnAlert,
|
||||
} from '../../../common/utils/endpoint_alert_check';
|
||||
import { ResponderContextMenuItem } from './responder_context_menu_item';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { getFieldValue } from '../host_isolation/helpers';
|
||||
|
||||
export const useResponderActionItem = (
|
||||
eventDetailsData: TimelineEventsDetailsItem[] | null,
|
||||
onClick: () => void
|
||||
): JSX.Element[] => {
|
||||
const isResponseActionsConsoleEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsConsoleEnabled'
|
||||
);
|
||||
const { loading: isAuthzLoading, canAccessResponseConsole } =
|
||||
useUserPrivileges().endpointPrivileges;
|
||||
|
||||
|
@ -42,7 +38,7 @@ export const useResponderActionItem = (
|
|||
return useMemo(() => {
|
||||
const actions: JSX.Element[] = [];
|
||||
|
||||
if (isResponseActionsConsoleEnabled && !isAuthzLoading && canAccessResponseConsole && isAlert) {
|
||||
if (!isAuthzLoading && canAccessResponseConsole && isAlert) {
|
||||
actions.push(
|
||||
<ResponderContextMenuItem
|
||||
key="endpointResponseActions-action-item"
|
||||
|
@ -53,13 +49,5 @@ export const useResponderActionItem = (
|
|||
}
|
||||
|
||||
return actions;
|
||||
}, [
|
||||
canAccessResponseConsole,
|
||||
endpointId,
|
||||
isAlert,
|
||||
isAuthzLoading,
|
||||
isEndpointAlert,
|
||||
isResponseActionsConsoleEnabled,
|
||||
onClick,
|
||||
]);
|
||||
}, [canAccessResponseConsole, endpointId, isAlert, isAuthzLoading, isEndpointAlert, onClick]);
|
||||
};
|
||||
|
|
|
@ -22,7 +22,6 @@ import { useKibana, useGetUserCasesPermissions, useHttp } from '../../../common/
|
|||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
NOT_FROM_ENDPOINT_HOST_TOOLTIP,
|
||||
HOST_ENDPOINT_UNENROLLED_TOOLTIP,
|
||||
|
@ -454,27 +453,6 @@ describe('take action dropdown', () => {
|
|||
apiMocks = endpointMetadataHttpMocks(mockStartServicesMock.http as jest.Mocked<HttpSetup>);
|
||||
});
|
||||
|
||||
describe('when the `responseActionsConsoleEnabled` feature flag is false', () => {
|
||||
beforeAll(() => {
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((featureKey) => {
|
||||
if (featureKey === 'responseActionsConsoleEnabled') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true);
|
||||
});
|
||||
|
||||
it('should hide the button if feature flag if off', async () => {
|
||||
render();
|
||||
|
||||
expect(findLaunchResponderButton()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display the button if user is not allowed to write event filters', async () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
...mockInitialUserPrivilegesState(),
|
||||
|
|
|
@ -143,8 +143,6 @@ export const getEndpointConsoleCommands = ({
|
|||
}): CommandDefinition[] => {
|
||||
const featureFlags = ExperimentalFeaturesService.get();
|
||||
|
||||
const isGetFileEnabled = featureFlags.responseActionGetFileEnabled;
|
||||
const isExecuteEnabled = featureFlags.responseActionExecuteEnabled;
|
||||
const isUploadEnabled = featureFlags.responseActionUploadEnabled;
|
||||
|
||||
const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => {
|
||||
|
@ -379,11 +377,7 @@ 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', {
|
||||
|
@ -429,13 +423,8 @@ export const getEndpointConsoleCommands = ({
|
|||
commandName: 'get-file',
|
||||
privileges: endpointPrivileges,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// `execute` is currently behind feature flag
|
||||
// planned for 8.8
|
||||
if (isExecuteEnabled) {
|
||||
consoleCommands.push({
|
||||
},
|
||||
{
|
||||
name: 'execute',
|
||||
about: getCommandAboutInfo({
|
||||
aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.execute.about', {
|
||||
|
@ -487,8 +476,8 @@ export const getEndpointConsoleCommands = ({
|
|||
commandName: 'execute',
|
||||
privileges: endpointPrivileges,
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// `upload` command
|
||||
// planned for 8.9
|
||||
|
|
|
@ -25,11 +25,6 @@ describe('When displaying Endpoint Response Actions', () => {
|
|||
beforeEach(() => {
|
||||
const testSetup = getConsoleTestSetup();
|
||||
|
||||
testSetup.setExperimentalFlag({
|
||||
responseActionGetFileEnabled: true,
|
||||
responseActionExecuteEnabled: true,
|
||||
});
|
||||
|
||||
const endpointMetadata = new EndpointMetadataGenerator().generate();
|
||||
const commands = getEndpointConsoleCommands({
|
||||
endpointAgentId: '123',
|
||||
|
|
|
@ -247,17 +247,6 @@ export const useActionsLogFilter = ({
|
|||
: RESPONSE_ACTION_API_COMMANDS_NAMES.filter((commandName) => {
|
||||
const featureFlags = ExperimentalFeaturesService.get();
|
||||
|
||||
// `get-file` is currently behind FF
|
||||
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' && !featureFlags.responseActionExecuteEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// upload - v8.9
|
||||
if (commandName === 'upload' && !featureFlags.responseActionUploadEnabled) {
|
||||
return false;
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { NoPermissions } from './no_permissions';
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const NoPermissions = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
titleSize="l"
|
||||
data-test-subj="noIngestPermissions"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpointManagemnet.noPermissionsText"
|
||||
defaultMessage="You do not have the required Kibana permissions to use Elastic Security Administration"
|
||||
/>
|
||||
}
|
||||
body={
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpointManagement.noPermissionsSubText"
|
||||
defaultMessage="You must have the superuser role to use this feature. If you do not have the superuser role and do not have permissions to edit user roles, contact your Kibana administrator."
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
NoPermissions.displayName = 'NoPermissions';
|
|
@ -11,19 +11,15 @@ import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
|||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { PrivilegedRoute } from './privileged_route';
|
||||
import type { PrivilegedRouteProps } from './privileged_route';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { AdministrationSubTab } from '../../types';
|
||||
import { MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH } from '../../common/constants';
|
||||
import { MANAGEMENT_PATH } from '../../../../common/constants';
|
||||
|
||||
jest.mock('../../../common/hooks/use_experimental_features');
|
||||
|
||||
describe('PrivilegedRoute', () => {
|
||||
const noPrivilegesPageTestId = 'noPrivilegesPage';
|
||||
const noPermissionsPageTestId = 'noIngestPermissions';
|
||||
|
||||
const componentTestId = 'component-to-render';
|
||||
let featureFlags: { endpointRbacEnabled: boolean; endpointRbacV1Enabled: boolean };
|
||||
|
||||
let currentPath: string;
|
||||
let renderProps: PrivilegedRouteProps;
|
||||
|
@ -31,7 +27,6 @@ describe('PrivilegedRoute', () => {
|
|||
let render: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
featureFlags = { endpointRbacEnabled: false, endpointRbacV1Enabled: false };
|
||||
currentPath = 'path';
|
||||
renderProps = {
|
||||
component: () => <div data-test-subj={componentTestId} />,
|
||||
|
@ -50,96 +45,45 @@ describe('PrivilegedRoute', () => {
|
|||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const useIsExperimentalFeatureEnabledMock = (feature: keyof typeof featureFlags) =>
|
||||
featureFlags[feature];
|
||||
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
|
||||
useIsExperimentalFeatureEnabledMock
|
||||
);
|
||||
});
|
||||
|
||||
const testCommonPathsForAllFeatureFlags = () => {
|
||||
it('renders component if it has privileges and on correct path', async () => {
|
||||
render();
|
||||
it('renders component if it has privileges and on correct path', async () => {
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(componentTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing if path is different', async () => {
|
||||
renderProps.path = 'different';
|
||||
|
||||
render();
|
||||
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
});
|
||||
};
|
||||
|
||||
describe('no feature flags', () => {
|
||||
testCommonPathsForAllFeatureFlags();
|
||||
|
||||
it('renders `you need to be superuser` if no privileges', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(noPermissionsPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
});
|
||||
expect(renderResult.getByTestId(componentTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
});
|
||||
|
||||
describe('endpointRbacV1Enabled', () => {
|
||||
beforeEach(() => {
|
||||
featureFlags.endpointRbacV1Enabled = true;
|
||||
});
|
||||
it('renders nothing if path is different', async () => {
|
||||
renderProps.path = 'different';
|
||||
|
||||
testCommonPathsForAllFeatureFlags();
|
||||
render();
|
||||
|
||||
describe('no privileges', () => {
|
||||
it('renders `you need to have privileges` on Response actions history', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
renderProps.path = MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH;
|
||||
currentPath = `${MANAGEMENT_PATH}/${AdministrationSubTab.responseActionsHistory}`;
|
||||
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(noPrivilegesPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders `you need to be superuser` on other pages', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(noPermissionsPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
});
|
||||
});
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(noPrivilegesPageTestId)).toBeNull();
|
||||
});
|
||||
|
||||
describe('endpointRbacEnabled', () => {
|
||||
beforeEach(() => {
|
||||
featureFlags.endpointRbacEnabled = true;
|
||||
});
|
||||
it('renders `you need to have privileges` on Response actions history', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
renderProps.path = MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH;
|
||||
currentPath = `${MANAGEMENT_PATH}/${AdministrationSubTab.responseActionsHistory}`;
|
||||
|
||||
testCommonPathsForAllFeatureFlags();
|
||||
render();
|
||||
|
||||
it('renders `you need to have RBAC privileges` if no privileges', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
expect(renderResult.getByTestId(noPrivilegesPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
});
|
||||
|
||||
render();
|
||||
it('renders `you need to have RBAC privileges` if no privileges', async () => {
|
||||
renderProps.hasPrivilege = false;
|
||||
|
||||
expect(renderResult.getByTestId(noPrivilegesPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
});
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(noPrivilegesPageTestId)).toBeTruthy();
|
||||
expect(renderResult.queryByTestId(noPermissionsPageTestId)).toBeNull();
|
||||
expect(renderResult.queryByTestId(componentTestId)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { ComponentType } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { Route } from '@kbn/kibana-react-plugin/public';
|
||||
import type { DocLinks } from '@kbn/doc-links';
|
||||
import { NoPrivilegesPage } from '../../../common/components/no_privileges';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { NoPermissions } from '../no_permissons';
|
||||
import { MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH } from '../../common/constants';
|
||||
|
||||
export interface PrivilegedRouteProps {
|
||||
path: string;
|
||||
|
@ -20,22 +17,18 @@ export interface PrivilegedRouteProps {
|
|||
}
|
||||
|
||||
export const PrivilegedRoute = memo(({ component, hasPrivilege, path }: PrivilegedRouteProps) => {
|
||||
const isEndpointRbacEnabled = useIsExperimentalFeatureEnabled('endpointRbacEnabled');
|
||||
const isEndpointRbacV1Enabled = useIsExperimentalFeatureEnabled('endpointRbacV1Enabled');
|
||||
const docLinkSelector = useCallback((docLinks: DocLinks) => {
|
||||
return docLinks.securitySolution.privileges;
|
||||
}, []);
|
||||
|
||||
const docLinkSelector = (docLinks: DocLinks) => docLinks.securitySolution.privileges;
|
||||
const componentToRender = useMemo(() => {
|
||||
if (!hasPrivilege) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return () => <NoPrivilegesPage docLinkSelector={docLinkSelector} />;
|
||||
}
|
||||
|
||||
let componentToRender = component;
|
||||
|
||||
if (!hasPrivilege) {
|
||||
const shouldUseMissingPrivilegesScreen =
|
||||
isEndpointRbacEnabled ||
|
||||
(isEndpointRbacV1Enabled && path === MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH);
|
||||
|
||||
componentToRender = shouldUseMissingPrivilegesScreen
|
||||
? () => <NoPrivilegesPage docLinkSelector={docLinkSelector} />
|
||||
: NoPermissions;
|
||||
}
|
||||
return component;
|
||||
}, [component, docLinkSelector, hasPrivilege]);
|
||||
|
||||
return <Route path={path} component={componentToRender} />;
|
||||
});
|
||||
|
|
|
@ -146,9 +146,6 @@ describe('links', () => {
|
|||
|
||||
fakeHttpServices.get.mockResolvedValue({ total: 0 });
|
||||
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
|
||||
});
|
||||
|
||||
const filteredLinks = await getManagementFilteredLinks(
|
||||
coreMockStarted,
|
||||
|
@ -187,9 +184,6 @@ describe('links', () => {
|
|||
|
||||
fakeHttpServices.get.mockResolvedValue({ total: 100 });
|
||||
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
|
||||
});
|
||||
|
||||
const filteredLinks = await getManagementFilteredLinks(
|
||||
coreMockStarted,
|
||||
|
@ -222,31 +216,7 @@ describe('links', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// this can be removed after removing endpointRbacEnabled feature flag
|
||||
describe('without endpointRbacEnabled', () => {
|
||||
beforeAll(() => {
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Trusted Applications for non-superuser, too', async () => {
|
||||
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
|
||||
|
||||
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
|
||||
|
||||
expect(filteredLinks).toEqual(links);
|
||||
});
|
||||
});
|
||||
|
||||
// this can be the default after removing endpointRbacEnabled feature flag
|
||||
describe('with endpointRbacEnabled', () => {
|
||||
beforeAll(() => {
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
describe('RBAC checks', () => {
|
||||
it('should return all links for user with all sub-feature privileges', async () => {
|
||||
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ import { IconHostIsolation } from './icons/host_isolation';
|
|||
import { IconSiemRules } from './icons/siem_rules';
|
||||
import { IconTrustedApplications } from './icons/trusted_applications';
|
||||
import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client';
|
||||
import { ExperimentalFeaturesService } from '../common/experimental_features_service';
|
||||
|
||||
const categories = [
|
||||
{
|
||||
|
@ -165,7 +164,6 @@ export const links: LinkItem = {
|
|||
path: POLICIES_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
experimentalKey: 'policyListEnabled',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.trustedApps,
|
||||
|
@ -241,14 +239,8 @@ export const getManagementFilteredLinks = async (
|
|||
plugins: StartPlugins
|
||||
): Promise<LinkItem> => {
|
||||
const fleetAuthz = plugins.fleet?.authz;
|
||||
|
||||
const { endpointRbacEnabled, endpointRbacV1Enabled } = ExperimentalFeaturesService.get();
|
||||
const isEndpointRbacEnabled = endpointRbacEnabled || endpointRbacV1Enabled;
|
||||
|
||||
const linksToExclude: SecurityPageName[] = [];
|
||||
|
||||
const currentUser = await plugins.security.authc.getCurrentUser();
|
||||
|
||||
const isPlatinumPlus = licenseService.isPlatinumPlus();
|
||||
let hasHostIsolationExceptions: boolean = isPlatinumPlus;
|
||||
|
||||
|
@ -264,7 +256,7 @@ export const getManagementFilteredLinks = async (
|
|||
fleetAuthz &&
|
||||
hasKibanaPrivilege(
|
||||
fleetAuthz,
|
||||
isEndpointRbacEnabled,
|
||||
true,
|
||||
currentUser.roles.includes('superuser'),
|
||||
'readHostIsolationExceptions'
|
||||
)
|
||||
|
@ -288,7 +280,7 @@ export const getManagementFilteredLinks = async (
|
|||
licenseService,
|
||||
fleetAuthz,
|
||||
currentUser.roles,
|
||||
isEndpointRbacEnabled,
|
||||
true,
|
||||
hasHostIsolationExceptions
|
||||
)
|
||||
: getEndpointAuthzInitialState();
|
||||
|
|
|
@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { pagePathGetters } from '@kbn/fleet-plugin/public';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { useWithShowEndpointResponder } from '../../../../hooks';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { APP_UI_ID } from '../../../../../../common/constants';
|
||||
import { getEndpointDetailsPath, getEndpointListPath } from '../../../../common/routing';
|
||||
import type { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/types';
|
||||
|
@ -37,9 +36,6 @@ export const useEndpointActionItems = (
|
|||
const fleetAgentPolicies = useEndpointSelector(agentPolicies);
|
||||
const allCurrentUrlParams = useEndpointSelector(uiQueryParams);
|
||||
const showEndpointResponseActionsConsole = useWithShowEndpointResponder();
|
||||
const isResponseActionsConsoleEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsConsoleEnabled'
|
||||
);
|
||||
const {
|
||||
canAccessResponseConsole,
|
||||
canIsolateHost,
|
||||
|
@ -123,7 +119,7 @@ export const useEndpointActionItems = (
|
|||
|
||||
return [
|
||||
...isolationActions,
|
||||
...(isResponseActionsConsoleEnabled && canAccessResponseConsole
|
||||
...(canAccessResponseConsole
|
||||
? [
|
||||
{
|
||||
'data-test-subj': 'console',
|
||||
|
@ -268,7 +264,6 @@ export const useEndpointActionItems = (
|
|||
endpointMetadata,
|
||||
fleetAgentPolicies,
|
||||
getAppUrl,
|
||||
isResponseActionsConsoleEnabled,
|
||||
showEndpointResponseActionsConsole,
|
||||
options?.isEndpointList,
|
||||
canIsolateHost,
|
||||
|
|
|
@ -13,8 +13,6 @@ import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
|||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import { endpointPageHttpMock } from '../endpoint_hosts/mocks';
|
||||
import { ExperimentalFeaturesService } from '../../../common/experimental_features_service';
|
||||
import { allowedExperimentalValues } from '../../../../common/experimental_features';
|
||||
|
||||
jest.mock('../../../common/components/user_privileges');
|
||||
|
||||
|
@ -24,12 +22,6 @@ describe('when in the Administration tab', () => {
|
|||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
|
||||
beforeAll(() => {
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues },
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
endpointPageHttpMock(mockedContext.coreStart.http);
|
||||
render = () => mockedContext.render(<ManagementContainer />);
|
||||
|
@ -41,13 +33,6 @@ describe('when in the Administration tab', () => {
|
|||
});
|
||||
|
||||
describe('when the user has no permissions', () => {
|
||||
// remove this beforeAll hook when feature flag is removed
|
||||
beforeAll(() => {
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should display `no permission` if no `canAccessEndpointManagement`', async () => {
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: { loading: false, canAccessEndpointManagement: false },
|
||||
|
@ -112,13 +97,6 @@ describe('when in the Administration tab', () => {
|
|||
});
|
||||
|
||||
describe('when the user has permissions', () => {
|
||||
// remove this beforeAll hook when feature flag is removed
|
||||
beforeAll(() => {
|
||||
ExperimentalFeaturesService.init({
|
||||
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the Management view if user has privileges', async () => {
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: { loading: false, canReadEndpointList: true, canAccessFleet: true },
|
||||
|
|
|
@ -21,10 +21,8 @@ import {
|
|||
} from '../../common/constants';
|
||||
import { NotFoundPage } from '../../../app/404';
|
||||
import { getPolicyDetailPath } from '../../common/routing';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
export const PolicyContainer = memo(() => {
|
||||
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
|
@ -43,9 +41,7 @@ export const PolicyContainer = memo(() => {
|
|||
exact
|
||||
render={(props) => <Redirect to={getPolicyDetailPath(props.match.params.policyId)} />}
|
||||
/>
|
||||
{isPolicyListEnabled && (
|
||||
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} exact component={PolicyList} />
|
||||
)}
|
||||
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} exact component={PolicyList} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -27,16 +27,13 @@ import {
|
|||
import { policyListApiPathHandlers } from '../store/test_mock_utils';
|
||||
import { PolicyDetails } from './policy_details';
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
jest.mock('./policy_forms/components/policy_form_layout', () => ({
|
||||
PolicyFormLayout: () => <></>,
|
||||
}));
|
||||
jest.mock('../../../../common/components/user_privileges');
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
|
||||
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
|
||||
const useIsExperimentalFeatureMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
describe('Policy Details', () => {
|
||||
const policyDetailsPathUrl = getPolicyDetailPath('1');
|
||||
|
@ -66,9 +63,6 @@ describe('Policy Details', () => {
|
|||
let releaseApiFailure: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
useIsExperimentalFeatureMock.mockReturnValue({
|
||||
policyListEnabled: true,
|
||||
});
|
||||
http.get.mockImplementation(async () => {
|
||||
await new Promise((_, reject) => {
|
||||
releaseApiFailure = reject.bind(null, new Error('policy not found'));
|
||||
|
|
|
@ -21,13 +21,11 @@ import { AdministrationListPage } from '../../../components/administration_list_
|
|||
import type { BackToExternalAppButtonProps } from '../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import type { PolicyDetailsRouteState } from '../../../../../common/endpoint/types';
|
||||
import { getEndpointListPath, getPoliciesPath } from '../../../common/routing';
|
||||
import { getPoliciesPath } from '../../../common/routing';
|
||||
import { useAppUrl } from '../../../../common/lib/kibana';
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
export const PolicyDetails = React.memo(() => {
|
||||
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
|
||||
const { state: routeState = {} } = useLocation<PolicyDetailsRouteState>();
|
||||
const { getAppUrl } = useAppUrl();
|
||||
|
||||
|
@ -49,38 +47,20 @@ export const PolicyDetails = React.memo(() => {
|
|||
};
|
||||
}
|
||||
|
||||
if (isPolicyListEnabled) {
|
||||
// default is to go back to the policy list
|
||||
const policyListPath = getPoliciesPath();
|
||||
return {
|
||||
backButtonLabel: i18n.translate('xpack.securitySolution.policyDetails.backToPolicyButton', {
|
||||
defaultMessage: 'Back to policy list',
|
||||
}),
|
||||
backButtonUrl: getAppUrl({ path: policyListPath }),
|
||||
onBackButtonNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: policyListPath,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
// remove else block once policy list is not hidden behind feature flag
|
||||
const endpointListPath = getEndpointListPath({ name: 'endpointList' });
|
||||
return {
|
||||
backButtonLabel: i18n.translate('xpack.securitySolution.policyDetails.backToEndpointList', {
|
||||
defaultMessage: 'View all endpoints',
|
||||
}),
|
||||
backButtonUrl: getAppUrl({ path: endpointListPath }),
|
||||
onBackButtonNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: endpointListPath,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}, [getAppUrl, routeState?.backLink, isPolicyListEnabled]);
|
||||
const policyListPath = getPoliciesPath();
|
||||
return {
|
||||
backButtonLabel: i18n.translate('xpack.securitySolution.policyDetails.backToPolicyButton', {
|
||||
defaultMessage: 'Back to policy list',
|
||||
}),
|
||||
backButtonUrl: getAppUrl({ path: policyListPath }),
|
||||
onBackButtonNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: policyListPath,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [getAppUrl, routeState.backLink]);
|
||||
|
||||
const headerRightContent = (
|
||||
<AgentsSummary
|
||||
|
|
|
@ -21,7 +21,6 @@ import { useDispatch } from 'react-redux';
|
|||
import { useLocation } from 'react-router-dom';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
|
||||
import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks';
|
||||
import {
|
||||
policyDetails,
|
||||
|
@ -32,7 +31,7 @@ import {
|
|||
|
||||
import { useToasts, useKibana } from '../../../../../../common/lib/kibana';
|
||||
import type { AppAction } from '../../../../../../common/store/actions';
|
||||
import { getEndpointListPath, getPoliciesPath } from '../../../../../common/routing';
|
||||
import { getPoliciesPath } from '../../../../../common/routing';
|
||||
import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
|
||||
import { APP_UI_ID } from '../../../../../../../common/constants';
|
||||
import type { PolicyDetailsRouteState } from '../../../../../../../common/endpoint/types';
|
||||
|
@ -62,7 +61,6 @@ export const PolicyFormLayout = React.memo(() => {
|
|||
const [showConfirm, setShowConfirm] = useState<boolean>(false);
|
||||
const [routeState, setRouteState] = useState<PolicyDetailsRouteState>();
|
||||
const policyName = policyItem?.name ?? '';
|
||||
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
|
||||
|
||||
const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo;
|
||||
const navigateToAppArguments = useMemo((): Parameters<ApplicationStart['navigateToApp']> => {
|
||||
|
@ -73,12 +71,10 @@ export const PolicyFormLayout = React.memo(() => {
|
|||
return [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: isPolicyListEnabled
|
||||
? getPoliciesPath()
|
||||
: getEndpointListPath({ name: 'endpointList' }),
|
||||
path: getPoliciesPath(),
|
||||
},
|
||||
];
|
||||
}, [isPolicyListEnabled, routingOnCancelNavigateTo]);
|
||||
}, [routingOnCancelNavigateTo]);
|
||||
|
||||
// Handle showing update statuses
|
||||
useEffect(() => {
|
||||
|
|
|
@ -86,7 +86,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
|
||||
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []);
|
||||
this.experimentalFeatures = parseExperimentalConfigValue(
|
||||
this.config.enableExperimental || []
|
||||
).features;
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
this.kibanaBranch = initializerContext.env.packageInfo.branch;
|
||||
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
|
||||
|
@ -268,18 +270,17 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
Component: getLazyEndpointPolicyEditExtension(core, plugins),
|
||||
});
|
||||
|
||||
if (this.experimentalFeatures.policyResponseInFleetEnabled) {
|
||||
registerExtension({
|
||||
package: 'endpoint',
|
||||
view: 'package-policy-response',
|
||||
Component: getLazyEndpointPolicyResponseExtension(core, plugins),
|
||||
});
|
||||
registerExtension({
|
||||
package: 'endpoint',
|
||||
view: 'package-generic-errors-list',
|
||||
Component: getLazyEndpointGenericErrorsListExtension(core, plugins),
|
||||
});
|
||||
}
|
||||
registerExtension({
|
||||
package: 'endpoint',
|
||||
view: 'package-policy-response',
|
||||
Component: getLazyEndpointPolicyResponseExtension(core, plugins),
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'endpoint',
|
||||
view: 'package-generic-errors-list',
|
||||
Component: getLazyEndpointGenericErrorsListExtension(core, plugins),
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'endpoint',
|
||||
|
|
|
@ -11,13 +11,7 @@ import { parseExperimentalConfigValue } from '../common/experimental_features';
|
|||
import type { ConfigType } from './config';
|
||||
|
||||
export const createMockConfig = (): ConfigType => {
|
||||
const enableExperimental: Array<keyof ExperimentalFeatures> = [
|
||||
// Remove property below once `get-file` FF is enabled or removed
|
||||
'responseActionGetFileEnabled',
|
||||
// remove property below once `execute` FF is enabled or removed
|
||||
'responseActionExecuteEnabled',
|
||||
'responseActionUploadEnabled',
|
||||
];
|
||||
const enableExperimental: Array<keyof ExperimentalFeatures> = ['responseActionUploadEnabled'];
|
||||
|
||||
return {
|
||||
[SIGNALS_INDEX_KEY]: DEFAULT_SIGNALS_INDEX,
|
||||
|
@ -32,7 +26,7 @@ export const createMockConfig = (): ConfigType => {
|
|||
alertIgnoreFields: [],
|
||||
maxUploadResponseActionFileBytes: 26214400,
|
||||
|
||||
experimentalFeatures: parseExperimentalConfigValue(enableExperimental),
|
||||
experimentalFeatures: parseExperimentalConfigValue(enableExperimental).features,
|
||||
enabled: true,
|
||||
};
|
||||
};
|
||||
|
@ -45,7 +39,7 @@ const withExperimentalFeature = (
|
|||
return {
|
||||
...config,
|
||||
enableExperimental,
|
||||
experimentalFeatures: parseExperimentalConfigValue(enableExperimental),
|
||||
experimentalFeatures: parseExperimentalConfigValue(enableExperimental).features,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,13 +10,7 @@ import { schema } from '@kbn/config-schema';
|
|||
import type { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants';
|
||||
import type { ExperimentalFeatures } from '../common/experimental_features';
|
||||
import {
|
||||
getExperimentalAllowedValues,
|
||||
isValidExperimentalValue,
|
||||
parseExperimentalConfigValue,
|
||||
} from '../common/experimental_features';
|
||||
|
||||
const allowedExperimentalValues = getExperimentalAllowedValues();
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
maxRuleImportExportSize: schema.number({ defaultValue: 10000 }),
|
||||
|
@ -94,15 +88,6 @@ export const configSchema = schema.object({
|
|||
*/
|
||||
enableExperimental: schema.arrayOf(schema.string(), {
|
||||
defaultValue: () => [],
|
||||
validate(list) {
|
||||
for (const key of list) {
|
||||
if (!isValidExperimentalValue(key)) {
|
||||
return `[${key}] is not allowed. Allowed values are: ${allowedExperimentalValues.join(
|
||||
', '
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -141,7 +126,20 @@ export type ConfigType = ConfigSchema & {
|
|||
|
||||
export const createConfig = (context: PluginInitializerContext): ConfigType => {
|
||||
const pluginConfig = context.config.get<TypeOf<typeof configSchema>>();
|
||||
const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental);
|
||||
const logger = context.logger.get('config');
|
||||
|
||||
const { invalid, features: experimentalFeatures } = parseExperimentalConfigValue(
|
||||
pluginConfig.enableExperimental
|
||||
);
|
||||
|
||||
if (invalid.length) {
|
||||
logger.warn(`Unsupported "xpack.securitySolution.enableExperimental" values detected.
|
||||
The following configuration values are no longer supported and should be removed from the kibana configuration file:
|
||||
|
||||
xpack.securitySolution.enableExperimental:
|
||||
${invalid.map((key) => ` - ${key}`).join('\n')}
|
||||
`);
|
||||
}
|
||||
|
||||
return {
|
||||
...pluginConfig,
|
||||
|
|
|
@ -162,7 +162,6 @@ export class EndpointAppContextService {
|
|||
public async getEndpointAuthz(request: KibanaRequest): Promise<EndpointAuthz> {
|
||||
const fleetAuthz = await this.getFleetAuthzService().fromRequest(request);
|
||||
const userRoles = this.security?.authc.getCurrentUser(request)?.roles ?? [];
|
||||
const { endpointRbacEnabled, endpointRbacV1Enabled } = this.experimentalFeatures;
|
||||
const isPlatinumPlus = this.getLicenseService().isPlatinumPlus();
|
||||
const listClient = this.getExceptionListsClient();
|
||||
|
||||
|
@ -174,7 +173,7 @@ export class EndpointAppContextService {
|
|||
this.getLicenseService(),
|
||||
fleetAuthz,
|
||||
userRoles,
|
||||
endpointRbacEnabled || endpointRbacV1Enabled,
|
||||
true,
|
||||
hasExceptionsListItems
|
||||
);
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export const createMockEndpointAppContext = (
|
|||
config: () => Promise.resolve(config),
|
||||
serverConfig: config,
|
||||
service: createMockEndpointAppContextService(mockManifestManager),
|
||||
experimentalFeatures: parseExperimentalConfigValue(config.enableExperimental),
|
||||
experimentalFeatures: parseExperimentalConfigValue(config.enableExperimental).features,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -29,10 +29,6 @@ export function registerActionRoutes(
|
|||
registerActionListRoutes(router, endpointContext);
|
||||
registerActionDetailsRoutes(router, endpointContext);
|
||||
registerResponseActionRoutes(router, endpointContext);
|
||||
|
||||
// APIs specific to `get-file` are behind FF
|
||||
if (endpointContext.experimentalFeatures.responseActionGetFileEnabled) {
|
||||
registerActionFileDownloadRoutes(router, endpointContext);
|
||||
registerActionFileInfoRoute(router, endpointContext);
|
||||
}
|
||||
registerActionFileDownloadRoutes(router, endpointContext);
|
||||
registerActionFileInfoRoute(router, endpointContext);
|
||||
}
|
||||
|
|
|
@ -145,37 +145,31 @@ export function registerResponseActionRoutes(
|
|||
)
|
||||
);
|
||||
|
||||
// `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')
|
||||
)
|
||||
);
|
||||
}
|
||||
router.post(
|
||||
{
|
||||
path: GET_FILE_ROUTE,
|
||||
validate: EndpointActionGetFileSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ all: ['canWriteFileOperations'] },
|
||||
logger,
|
||||
responseActionRequestHandler(endpointContext, 'get-file')
|
||||
)
|
||||
);
|
||||
|
||||
// `execute` currently behind FF (planned for 8.8)
|
||||
if (endpointContext.experimentalFeatures.responseActionExecuteEnabled) {
|
||||
router.post(
|
||||
{
|
||||
path: EXECUTE_ROUTE,
|
||||
validate: ExecuteActionRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ all: ['canWriteExecuteOperations'] },
|
||||
logger,
|
||||
responseActionRequestHandler<ResponseActionsExecuteParameters>(endpointContext, 'execute')
|
||||
)
|
||||
);
|
||||
}
|
||||
router.post(
|
||||
{
|
||||
path: EXECUTE_ROUTE,
|
||||
validate: ExecuteActionRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ all: ['canWriteExecuteOperations'] },
|
||||
logger,
|
||||
responseActionRequestHandler<ResponseActionsExecuteParameters>(endpointContext, 'execute')
|
||||
)
|
||||
);
|
||||
|
||||
registerActionFileUploadRoute(router, endpointContext);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ describe('Endpoint Pending Action Summary API', () => {
|
|||
endpointResponses: LogsEndpointActionResponse[]
|
||||
) => void;
|
||||
|
||||
const setupRouteHandler = (pendingActionResponsesWithAck: boolean = true): void => {
|
||||
const setupRouteHandler = (): void => {
|
||||
const esClientMock = elasticsearchServiceMock.createScopedClusterClient();
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
|
||||
|
@ -71,7 +71,6 @@ describe('Endpoint Pending Action Summary API', () => {
|
|||
service: endpointAppContextService,
|
||||
experimentalFeatures: {
|
||||
...endpointContextMock.experimentalFeatures,
|
||||
pendingActionResponsesWithAck,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -126,6 +125,10 @@ describe('Endpoint Pending Action Summary API', () => {
|
|||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setupRouteHandler();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (endpointAppContextService) {
|
||||
endpointAppContextService.stop();
|
||||
|
@ -158,261 +161,240 @@ describe('Endpoint Pending Action Summary API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['when pendingActionResponsesWithAck is TRUE', true],
|
||||
['when pendingActionResponsesWithAck is FALSE', false],
|
||||
])('response %s', (_, pendingActionResponsesWithAck) => {
|
||||
const getExpected = (value: number): number => {
|
||||
return pendingActionResponsesWithAck ? value : 0;
|
||||
};
|
||||
it('should include agent IDs in the output, even if they have no actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses([], []);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setupRouteHandler(pendingActionResponsesWithAck);
|
||||
it('should include total counts for large (more than a page) action counts', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
const actions: LogsEndpointAction[] = Array.from({ length: 1400 }, () =>
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
})
|
||||
);
|
||||
havingActionsAndResponses(actions, []);
|
||||
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
|
||||
it('should include agent IDs in the output, even if they have no actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses([], []);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
1400
|
||||
);
|
||||
});
|
||||
|
||||
it('should include total counts for large (more than a page) action counts', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
const actions: LogsEndpointAction[] = Array.from({ length: 1400 }, () =>
|
||||
it('should respond with a valid pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
});
|
||||
it('should include a total count of a pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
})
|
||||
);
|
||||
havingActionsAndResponses(actions, []);
|
||||
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
getExpected(1400)
|
||||
);
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
|
||||
it('should respond with a valid pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(2);
|
||||
});
|
||||
it('should show multiple pending actions, and their counts', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
Array.from<LogsEndpointAction['EndpointActions']['data']['command'], LogsEndpointAction>(
|
||||
['isolate', 'isolate', 'isolate', 'unisolate', 'unisolate'],
|
||||
(command) =>
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
EndpointActions: { data: { command } },
|
||||
})
|
||||
),
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
it('should include a total count of a pending action', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
getExpected(2)
|
||||
);
|
||||
});
|
||||
it('should show multiple pending actions, and their counts', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
Array.from<LogsEndpointAction['EndpointActions']['data']['command'], LogsEndpointAction>(
|
||||
['isolate', 'isolate', 'isolate', 'unisolate', 'unisolate'],
|
||||
(command) =>
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID] },
|
||||
EndpointActions: { data: { command } },
|
||||
})
|
||||
),
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
getExpected(3)
|
||||
);
|
||||
expect(
|
||||
(response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.unisolate
|
||||
).toEqual(getExpected(2));
|
||||
});
|
||||
it('should calculate correct pending counts from grouped/bulked actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID, 'IRRELEVANT-OTHER-AGENT', 'ANOTHER-POSSIBLE-AGENT'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID, 'YET-ANOTHER-AGENT-ID'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: ['YET-ANOTHER-AGENT-ID'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
getExpected(2)
|
||||
);
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(3);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.unisolate).toEqual(
|
||||
2
|
||||
);
|
||||
});
|
||||
it('should calculate correct pending counts from grouped/bulked actions', async () => {
|
||||
const mockID = 'XYZABC-000';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID, 'IRRELEVANT-OTHER-AGENT', 'ANOTHER-POSSIBLE-AGENT'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockID, 'YET-ANOTHER-AGENT-ID'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: ['YET-ANOTHER-AGENT-ID'] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(2);
|
||||
});
|
||||
|
||||
it('should exclude actions that have responses from the pending count', async () => {
|
||||
const mockAgentID = 'XYZABC-000';
|
||||
const actionID = 'some-known-actionid';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { action_id: actionID, data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[
|
||||
endpointActionGenerator.generateResponse({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { action_id: actionID, data: { command: 'isolate' } },
|
||||
}),
|
||||
]
|
||||
);
|
||||
(endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockAgentID],
|
||||
},
|
||||
it('should exclude actions that have responses from the pending count', async () => {
|
||||
const mockAgentID = 'XYZABC-000';
|
||||
const actionID = 'some-known-actionid';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { action_id: actionID, data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
[
|
||||
endpointActionGenerator.generateResponse({
|
||||
agent: { id: [mockAgentID] },
|
||||
EndpointActions: { action_id: actionID, data: { command: 'isolate' } },
|
||||
}),
|
||||
]
|
||||
);
|
||||
(endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockAgentID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(
|
||||
getExpected(1)
|
||||
);
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [mockAgentID],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(1);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].agent_id).toEqual(mockAgentID);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data[0].pending_actions.isolate).toEqual(1);
|
||||
});
|
||||
|
||||
it('should have accurate counts for multiple agents, bulk actions, and responses', async () => {
|
||||
const agentOne = 'XYZABC-000';
|
||||
const agentTwo = 'DEADBEEF';
|
||||
const agentThree = 'IDIDIDID';
|
||||
it('should have accurate counts for multiple agents, bulk actions, and responses', async () => {
|
||||
const agentOne = 'XYZABC-000';
|
||||
const agentTwo = 'DEADBEEF';
|
||||
const agentThree = 'IDIDIDID';
|
||||
|
||||
const actionTwoID = 'ID-TWO';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentOne, agentTwo, agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentTwo, agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' }, action_id: actionTwoID },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
const actionTwoID = 'ID-TWO';
|
||||
havingActionsAndResponses(
|
||||
[
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentOne, agentTwo, agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentTwo, agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' }, action_id: actionTwoID },
|
||||
}),
|
||||
endpointActionGenerator.generate({
|
||||
agent: { id: [agentThree] },
|
||||
EndpointActions: { data: { command: 'isolate' } },
|
||||
}),
|
||||
],
|
||||
|
||||
[
|
||||
endpointActionGenerator.generateResponse({
|
||||
agent: { id: [agentThree] },
|
||||
EndpointActions: {
|
||||
action_id: actionTwoID,
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
(endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [agentOne, agentTwo, agentThree],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(3);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentOne,
|
||||
pending_actions: {
|
||||
isolate: getExpected(1),
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentTwo,
|
||||
pending_actions: {
|
||||
isolate: getExpected(2),
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentThree,
|
||||
pending_actions: {
|
||||
isolate: getExpected(2), // present in all three actions, but second one has a response, therefore not pending
|
||||
},
|
||||
[
|
||||
endpointActionGenerator.generateResponse({
|
||||
agent: { id: [agentThree] },
|
||||
EndpointActions: {
|
||||
action_id: actionTwoID,
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
(endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const response = await getPendingStatus({
|
||||
query: {
|
||||
agent_ids: [agentOne, agentTwo, agentThree],
|
||||
},
|
||||
});
|
||||
expect(response.ok).toBeCalled();
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toHaveLength(3);
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentOne,
|
||||
pending_actions: {
|
||||
isolate: 1,
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentTwo,
|
||||
pending_actions: {
|
||||
isolate: 2,
|
||||
},
|
||||
});
|
||||
expect((response.ok.mock.calls[0][0]?.body as any)?.data).toContainEqual({
|
||||
agent_id: agentThree,
|
||||
pending_actions: {
|
||||
isolate: 2, // present in all three actions, but second one has a response, therefore not pending
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -59,8 +59,7 @@ export const actionStatusRequestHandler = function (
|
|||
esClient,
|
||||
endpointContext.service.getEndpointMetadataService(),
|
||||
logger,
|
||||
agentIDs,
|
||||
endpointContext.experimentalFeatures.pendingActionResponsesWithAck
|
||||
agentIDs
|
||||
);
|
||||
|
||||
return res.ok({
|
||||
|
|
|
@ -24,8 +24,7 @@ export const getPendingActionsSummary = async (
|
|||
metadataService: EndpointMetadataService,
|
||||
logger: Logger,
|
||||
/** The Fleet Agent IDs to be checked */
|
||||
agentIDs: string[],
|
||||
isPendingActionResponsesWithAckEnabled: boolean
|
||||
agentIDs: string[]
|
||||
): Promise<EndpointPendingActions[]> => {
|
||||
const { data: unExpiredActionList } = await getActionList({
|
||||
esClient,
|
||||
|
@ -60,12 +59,8 @@ export const getPendingActionsSummary = async (
|
|||
for (const agentID of agentIDs) {
|
||||
const agentPendingActions: EndpointPendingActions['pending_actions'] = {};
|
||||
const setActionAsPending = (commandName: string) => {
|
||||
// Add the command to the list of pending actions, but set it to zero if the
|
||||
// `pendingActionResponsesWithAck` feature flag is false.
|
||||
// Otherwise, just increment the count for this command
|
||||
agentPendingActions[commandName] = !isPendingActionResponsesWithAckEnabled
|
||||
? 0
|
||||
: (agentPendingActions[commandName] ?? 0) + 1;
|
||||
// Add the command to the list of pending actions and increment the count for this command
|
||||
agentPendingActions[commandName] = (agentPendingActions[commandName] ?? 0) + 1;
|
||||
};
|
||||
|
||||
pending.push({
|
||||
|
|
|
@ -90,7 +90,7 @@ export const buildManifestManagerContextMock = (
|
|||
...fullOpts,
|
||||
artifactClient: createEndpointArtifactClientMock(),
|
||||
logger: loggingSystemMock.create().get() as jest.Mocked<Logger>,
|
||||
experimentalFeatures: parseExperimentalConfigValue([]),
|
||||
experimentalFeatures: parseExperimentalConfigValue([]).features,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -19,10 +19,10 @@ import {
|
|||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
} from '@kbn/securitysolution-rules';
|
||||
import type { ExperimentalFeatures } from '../../../common';
|
||||
import { SecuritySubFeatureId } from './security_kibana_sub_features';
|
||||
import { APP_ID, LEGACY_NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../common/constants';
|
||||
import { savedObjectTypes } from '../../saved_objects';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { SecuritySubFeatureId } from './security_kibana_sub_features';
|
||||
import type { AppFeaturesSecurityConfig, BaseKibanaFeatureConfig } from './types';
|
||||
import { AppFeatureSecurityKey } from '../../../common/types/app_features';
|
||||
|
||||
|
@ -123,35 +123,21 @@ export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
|||
});
|
||||
|
||||
export const getSecurityBaseKibanaSubFeatureIds = (
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
|
||||
): SecuritySubFeatureId[] => {
|
||||
const subFeatureIds: SecuritySubFeatureId[] = [];
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled) {
|
||||
subFeatureIds.push(
|
||||
SecuritySubFeatureId.endpointList,
|
||||
SecuritySubFeatureId.trustedApplications,
|
||||
SecuritySubFeatureId.hostIsolationExceptions,
|
||||
SecuritySubFeatureId.blocklist,
|
||||
SecuritySubFeatureId.eventFilters,
|
||||
SecuritySubFeatureId.policyManagement
|
||||
);
|
||||
}
|
||||
|
||||
if (experimentalFeatures.endpointRbacEnabled || experimentalFeatures.endpointRbacV1Enabled) {
|
||||
subFeatureIds.push(
|
||||
SecuritySubFeatureId.responseActionsHistory,
|
||||
SecuritySubFeatureId.hostIsolation,
|
||||
SecuritySubFeatureId.processOperations
|
||||
);
|
||||
}
|
||||
if (experimentalFeatures.responseActionGetFileEnabled) {
|
||||
subFeatureIds.push(SecuritySubFeatureId.fileOperations);
|
||||
}
|
||||
// planned for 8.8
|
||||
if (experimentalFeatures.responseActionExecuteEnabled) {
|
||||
subFeatureIds.push(SecuritySubFeatureId.executeAction);
|
||||
}
|
||||
const subFeatureIds: SecuritySubFeatureId[] = [
|
||||
SecuritySubFeatureId.endpointList,
|
||||
SecuritySubFeatureId.trustedApplications,
|
||||
SecuritySubFeatureId.hostIsolationExceptions,
|
||||
SecuritySubFeatureId.blocklist,
|
||||
SecuritySubFeatureId.eventFilters,
|
||||
SecuritySubFeatureId.policyManagement,
|
||||
SecuritySubFeatureId.responseActionsHistory,
|
||||
SecuritySubFeatureId.hostIsolation,
|
||||
SecuritySubFeatureId.processOperations,
|
||||
SecuritySubFeatureId.fileOperations,
|
||||
SecuritySubFeatureId.executeAction,
|
||||
];
|
||||
|
||||
return subFeatureIds;
|
||||
};
|
||||
|
|
|
@ -182,13 +182,9 @@ export const getHostEndpoint = async (
|
|||
const fleetAgentId = endpointData.metadata.elastic.agent.id;
|
||||
|
||||
const pendingActions = fleetAgentId
|
||||
? getPendingActionsSummary(
|
||||
esClient.asInternalUser,
|
||||
endpointMetadataService,
|
||||
logger,
|
||||
[fleetAgentId],
|
||||
endpointContext.experimentalFeatures.pendingActionResponsesWithAck
|
||||
)
|
||||
? getPendingActionsSummary(esClient.asInternalUser, endpointMetadataService, logger, [
|
||||
fleetAgentId,
|
||||
])
|
||||
.then((results) => {
|
||||
return results[0].pending_actions;
|
||||
})
|
||||
|
|
|
@ -32086,8 +32086,6 @@
|
|||
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "Requête de libération de l'hôte reçue par Endpoint",
|
||||
"xpack.securitySolution.endpointDetails.overview": "Aperçu",
|
||||
"xpack.securitySolution.endpointDetails.responseActionsHistory": "Historique des actions de réponse",
|
||||
"xpack.securitySolution.endpointManagement.noPermissionsSubText": "Vous devez disposer du rôle de superutilisateur pour utiliser cette fonctionnalité. Si vous ne disposez pas de ce rôle, ni d'autorisations pour modifier les rôles d'utilisateur, contactez votre administrateur Kibana.",
|
||||
"xpack.securitySolution.endpointManagemnet.noPermissionsText": "Vous ne disposez pas des autorisations Kibana requises pour utiliser Elastic Security Administration",
|
||||
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "Politique appliquée",
|
||||
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "L'erreur suivante a été rencontrée :",
|
||||
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "L'exécution de la commande a réussi.",
|
||||
|
@ -33081,7 +33079,6 @@
|
|||
"xpack.securitySolution.policy.list.subtitle": "Utiliser les politiques pour personnaliser les protections des points de terminaison et de charge de travail cloud, et d'autres configurations",
|
||||
"xpack.securitySolution.policy.list.title": "Politiques",
|
||||
"xpack.securitySolution.policy.list.updatedAt": "Dernière mise à jour",
|
||||
"xpack.securitySolution.policyDetails.backToEndpointList": "Afficher tous les points de terminaison",
|
||||
"xpack.securitySolution.policyDetails.backToPolicyButton": "Retour à la liste des politiques",
|
||||
"xpack.securitySolution.policyDetails.missingArtifactAccess": "Vous ne disposez pas des autorisations Kibana requises pour utiliser l'artefact donné.",
|
||||
"xpack.securitySolution.policyList.packageVersionError": "Erreur lors de la récupération de la version du pack de points de terminaison",
|
||||
|
|
|
@ -32067,8 +32067,6 @@
|
|||
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "エンドポイントが受信したホストリリースリクエスト",
|
||||
"xpack.securitySolution.endpointDetails.overview": "概要",
|
||||
"xpack.securitySolution.endpointDetails.responseActionsHistory": "対応アクション履歴",
|
||||
"xpack.securitySolution.endpointManagement.noPermissionsSubText": "この機能を使用するには、スーパーユーザーロールが必要です。スーパーユーザーロールがなく、ユーザーロールを編集する権限もない場合は、Kibana管理者に問い合わせてください。",
|
||||
"xpack.securitySolution.endpointManagemnet.noPermissionsText": "Elastic Security Administrationを使用するために必要なKibana権限がありません。",
|
||||
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "ポリシーが適用されました",
|
||||
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "次のエラーが発生しました:",
|
||||
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "コマンド実行が成功しました。",
|
||||
|
@ -33062,7 +33060,6 @@
|
|||
"xpack.securitySolution.policy.list.subtitle": "ポリシーを使用して、エンドポイントおよびクラウドワークロード保護、ならびに他の構成をカスタマイズ",
|
||||
"xpack.securitySolution.policy.list.title": "ポリシー",
|
||||
"xpack.securitySolution.policy.list.updatedAt": "最終更新",
|
||||
"xpack.securitySolution.policyDetails.backToEndpointList": "すべてのエンドポイントを表示",
|
||||
"xpack.securitySolution.policyDetails.backToPolicyButton": "ポリシーリストに戻る",
|
||||
"xpack.securitySolution.policyDetails.missingArtifactAccess": "特定のアーティファクトを使用するために必要なKibana権限がありません。",
|
||||
"xpack.securitySolution.policyList.packageVersionError": "エンドポイントパッケージバージョンの取得エラー",
|
||||
|
|
|
@ -32063,8 +32063,6 @@
|
|||
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "终端收到释放主机请求",
|
||||
"xpack.securitySolution.endpointDetails.overview": "概览",
|
||||
"xpack.securitySolution.endpointDetails.responseActionsHistory": "响应操作历史记录",
|
||||
"xpack.securitySolution.endpointManagement.noPermissionsSubText": "您必须具有超级用户角色才能使用此功能。如果您不具有超级用户角色,且无权编辑用户角色,请与 Kibana 管理员联系。",
|
||||
"xpack.securitySolution.endpointManagemnet.noPermissionsText": "您没有所需的 Kibana 权限,无法使用 Elastic Security 管理",
|
||||
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "已应用策略",
|
||||
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "遇到以下错误:",
|
||||
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "命令执行成功。",
|
||||
|
@ -33058,7 +33056,6 @@
|
|||
"xpack.securitySolution.policy.list.subtitle": "使用策略定制终端和云工作负载防护及其他配置",
|
||||
"xpack.securitySolution.policy.list.title": "策略",
|
||||
"xpack.securitySolution.policy.list.updatedAt": "上次更新时间",
|
||||
"xpack.securitySolution.policyDetails.backToEndpointList": "查看所有终端",
|
||||
"xpack.securitySolution.policyDetails.backToPolicyButton": "返回到策略列表",
|
||||
"xpack.securitySolution.policyDetails.missingArtifactAccess": "您没有所需 Kibana 权限,无法使用给定项目。",
|
||||
"xpack.securitySolution.policyList.packageVersionError": "检索终端软件包版本时出错",
|
||||
|
|
|
@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
const config = defendWorkflowsCypressConfig.getAll();
|
||||
const hostIp = getLocalhostRealIp();
|
||||
|
||||
const enabledFeatureFlags: Array<keyof ExperimentalFeatures> = ['responseActionExecuteEnabled'];
|
||||
const enabledFeatureFlags: Array<keyof ExperimentalFeatures> = [];
|
||||
|
||||
return {
|
||||
...config,
|
||||
|
|
|
@ -31,12 +31,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
`--xpack.fleet.packages.0.version=latest`,
|
||||
// this will be removed in 8.7 when the file upload feature is released
|
||||
`--xpack.fleet.enableExperimental.0=diagnosticFileUploadEnabled`,
|
||||
// this will be removed in 8.7 when the artifacts RBAC is released
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'endpointRbacEnabled',
|
||||
'responseActionGetFileEnabled',
|
||||
'responseActionExecuteEnabled',
|
||||
])}`,
|
||||
// set any experimental feature flags for testing
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([])}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue