diff --git a/x-pack/plugins/security_solution/common/license/mocks.ts b/x-pack/plugins/security_solution/common/license/mocks.ts new file mode 100644 index 00000000000000..f352932b446139 --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/mocks.ts @@ -0,0 +1,21 @@ +/* + * 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 { LicenseService } from './license'; + +export const createLicenseServiceMock = (): jest.Mocked => { + return ({ + start: jest.fn(), + stop: jest.fn(), + getLicenseInformation: jest.fn(), + getLicenseInformation$: jest.fn(), + isAtLeast: jest.fn(), + isGoldPlus: jest.fn().mockReturnValue(true), + isPlatinumPlus: jest.fn().mockReturnValue(true), + isEnterprise: jest.fn().mockReturnValue(true), + } as unknown) as jest.Mocked; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts b/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts new file mode 100644 index 00000000000000..2d34e4b22a14e9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/__mocks__/use_license.ts @@ -0,0 +1,11 @@ +/* + * 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 { createLicenseServiceMock } from '../../../../common/license/mocks'; + +export const licenseService = createLicenseServiceMock(); +export const useLicense = () => licenseService; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx index 356d44a8105287..04708ea90cd349 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx @@ -15,8 +15,10 @@ import React from 'react'; import { act } from '@testing-library/react'; import { endpointPageHttpMock } from '../../../mocks'; import { fireEvent } from '@testing-library/dom'; +import { licenseService } from '../../../../../../common/hooks/use_license'; jest.mock('../../../../../../common/lib/kibana'); +jest.mock('../../../../../../common/hooks/use_license'); describe('When using the Endpoint Details Actions Menu', () => { let render: () => Promise>; @@ -112,4 +114,25 @@ describe('When using the Endpoint Details Actions Menu', () => { expect(coreStart.application.navigateToApp).toHaveBeenCalled(); }); }); + + describe('and license is NOT PlatinumPlus', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + beforeEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(false)); + + afterEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(true)); + + it('should not show the `isoalte` action', async () => { + setEndpointMetadataResponse(); + await render(); + expect(renderResult.queryByTestId('isolateLink')).toBeNull(); + }); + + it('should still show `unisolate` action for endpoints that are currently isolated', async () => { + setEndpointMetadataResponse(true); + await render(); + expect(renderResult.queryByTestId('isolateLink')).toBeNull(); + expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index e03427671798da..7c38c935a0b9f3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -17,7 +17,8 @@ import { useEndpointSelector } from './hooks'; import { agentPolicies, uiQueryParams } from '../../store/selectors'; import { useKibana } from '../../../../../common/lib/kibana'; import { ContextMenuItemNavByRouterProps } from '../components/context_menu_item_nav_by_rotuer'; -import { isEndpointHostIsolated } from '../../../../../common/utils/validators/is_endpoint_host_isolated'; +import { isEndpointHostIsolated } from '../../../../../common/utils/validators'; +import { useLicense } from '../../../../../common/hooks/use_license'; /** * Returns a list (array) of actions for an individual endpoint @@ -26,6 +27,7 @@ import { isEndpointHostIsolated } from '../../../../../common/utils/validators/i export const useEndpointActionItems = ( endpointMetadata: MaybeImmutable | undefined ): ContextMenuItemNavByRouterProps[] => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); const { formatUrl } = useFormatUrl(SecurityPageName.administration); const fleetAgentPolicies = useEndpointSelector(agentPolicies); const allCurrentUrlParams = useEndpointSelector(uiQueryParams); @@ -58,40 +60,48 @@ export const useEndpointActionItems = ( selected_endpoint: endpointId, }); + const isolationActions = []; + + if (isIsolated) { + // Un-isolate is always available to users regardless of license level + isolationActions.push({ + 'data-test-subj': 'unIsolateLink', + icon: 'logoSecurity', + key: 'unIsolateHost', + navigateAppId: MANAGEMENT_APP_ID, + navigateOptions: { + path: endpointUnIsolatePath, + }, + href: formatUrl(endpointUnIsolatePath), + children: ( + + ), + }); + } else if (isPlatinumPlus) { + // For Platinum++ licenses, users also have ability to isolate + isolationActions.push({ + 'data-test-subj': 'isolateLink', + icon: 'logoSecurity', + key: 'isolateHost', + navigateAppId: MANAGEMENT_APP_ID, + navigateOptions: { + path: endpointIsolatePath, + }, + href: formatUrl(endpointIsolatePath), + children: ( + + ), + }); + } + return [ - isIsolated - ? { - 'data-test-subj': 'unIsolateLink', - icon: 'logoSecurity', - key: 'unIsolateHost', - navigateAppId: MANAGEMENT_APP_ID, - navigateOptions: { - path: endpointUnIsolatePath, - }, - href: formatUrl(endpointUnIsolatePath), - children: ( - - ), - } - : { - 'data-test-subj': 'isolateLink', - icon: 'logoSecurity', - key: 'isolateHost', - navigateAppId: MANAGEMENT_APP_ID, - navigateOptions: { - path: endpointIsolatePath, - }, - href: formatUrl(endpointIsolatePath), - children: ( - - ), - }, + ...isolationActions, { 'data-test-subj': 'hostLink', icon: 'logoSecurity', @@ -183,5 +193,12 @@ export const useEndpointActionItems = ( } return []; - }, [allCurrentUrlParams, endpointMetadata, fleetAgentPolicies, formatUrl, getUrlForApp]); + }, [ + allCurrentUrlParams, + endpointMetadata, + fleetAgentPolicies, + formatUrl, + getUrlForApp, + isPlatinumPlus, + ]); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 1ac5c289c87cf7..14f9662ad9b0bb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -37,6 +37,7 @@ import { isUninitialisedResourceState, } from '../../../state'; import { getCurrentIsolationRequestState } from '../store/selectors'; +import { licenseService } from '../../../../common/hooks/use_license'; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; // but sure enough it needs to be inline in this one file @@ -59,6 +60,7 @@ jest.mock('../../policy/store/services/ingest', () => { }); jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_license'); describe('when on the endpoint list page', () => { const docGenerator = new EndpointDocGenerator(); @@ -70,6 +72,9 @@ describe('when on the endpoint list page', () => { let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; let abortSpy: jest.SpyInstance; + + (licenseService as jest.Mocked).isPlatinumPlus.mockReturnValue(true); + beforeAll(() => { const mockAbort = new AbortController(); mockAbort.abort(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index d01ccea5ba1f4a..1766048a3985aa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -17,17 +17,7 @@ import { policyListApiPathHandlers } from '../store/test_mock_utils'; import { licenseService } from '../../../../common/hooks/use_license'; jest.mock('../../../../common/components/link_to'); -jest.mock('../../../../common/hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); +jest.mock('../../../../common/hooks/use_license'); describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>;