Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Security Solution][Endpoint] Isolate Action should only be available to Platinum+ licenses (#102374) #102434

Merged
merged 1 commit into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions x-pack/plugins/security_solution/common/license/mocks.ts
Original file line number Diff line number Diff line change
@@ -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<LicenseService> => {
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<LicenseService>;
};
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<AppContextTestRender['render']>>;
Expand Down Expand Up @@ -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<typeof licenseService>;

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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +27,7 @@ import { isEndpointHostIsolated } from '../../../../../common/utils/validators/i
export const useEndpointActionItems = (
endpointMetadata: MaybeImmutable<HostMetadata> | undefined
): ContextMenuItemNavByRouterProps[] => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
const fleetAgentPolicies = useEndpointSelector(agentPolicies);
const allCurrentUrlParams = useEndpointSelector(uiQueryParams);
Expand Down Expand Up @@ -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: (
<FormattedMessage
id="xpack.securitySolution.endpoint.actions.unIsolateHost"
defaultMessage="Unisolate host"
/>
),
});
} 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: (
<FormattedMessage
id="xpack.securitySolution.endpoint.actions.isolateHost"
defaultMessage="Isolate host"
/>
),
});
}

return [
isIsolated
? {
'data-test-subj': 'unIsolateLink',
icon: 'logoSecurity',
key: 'unIsolateHost',
navigateAppId: MANAGEMENT_APP_ID,
navigateOptions: {
path: endpointUnIsolatePath,
},
href: formatUrl(endpointUnIsolatePath),
children: (
<FormattedMessage
id="xpack.securitySolution.endpoint.actions.unIsolateHost"
defaultMessage="Unisolate host"
/>
),
}
: {
'data-test-subj': 'isolateLink',
icon: 'logoSecurity',
key: 'isolateHost',
navigateAppId: MANAGEMENT_APP_ID,
navigateOptions: {
path: endpointIsolatePath,
},
href: formatUrl(endpointIsolatePath),
children: (
<FormattedMessage
id="xpack.securitySolution.endpoint.actions.isolateHost"
defaultMessage="Isolate host"
/>
),
},
...isolationActions,
{
'data-test-subj': 'hostLink',
icon: 'logoSecurity',
Expand Down Expand Up @@ -183,5 +193,12 @@ export const useEndpointActionItems = (
}

return [];
}, [allCurrentUrlParams, endpointMetadata, fleetAgentPolicies, formatUrl, getUrlForApp]);
}, [
allCurrentUrlParams,
endpointMetadata,
fleetAgentPolicies,
formatUrl,
getUrlForApp,
isPlatinumPlus,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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<typeof licenseService>).isPlatinumPlus.mockReturnValue(true);

beforeAll(() => {
const mockAbort = new AbortController();
mockAbort.abort();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof render>['find']>;
Expand Down