[Security Solution][Endpoint] Unit tests for Policy settings form (#161814)

## Summary

- Adds unit tests for all components of the Policy settings form


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Paul Tavares 2023-07-19 14:17:42 -04:00 committed by GitHub
parent a811538ee8
commit 4b38775515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3494 additions and 279 deletions

View file

@ -8,4 +8,4 @@
import { createLicenseServiceMock } from '../../../../common/license/mocks';
export const licenseService = createLicenseServiceMock();
export const useLicense = () => licenseService;
export const useLicense = jest.fn(() => licenseService);

View file

@ -0,0 +1,139 @@
/*
* 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 { useQuery as _useQuery } from '@tanstack/react-query';
import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { allFleetHttpMocks } from '../../mocks';
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import { useFetchEndpointPolicy } from './use_fetch_endpoint_policy';
import type { PolicyData } from '../../../../common/endpoint/types';
import {
DefaultPolicyNotificationMessage,
DefaultPolicyRuleNotificationMessage,
} from '../../../../common/endpoint/models/policy_config';
import { set } from 'lodash';
const useQueryMock = _useQuery as jest.Mock;
jest.mock('@tanstack/react-query', () => {
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
return {
...actualReactQueryModule,
useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
};
});
describe('When using the `useGetFileInfo()` hook', () => {
type HookRenderer = ReactQueryHookRenderer<
Parameters<typeof useFetchEndpointPolicy>,
ReturnType<typeof useFetchEndpointPolicy>
>;
let policy: PolicyData;
let queryOptions: NonNullable<Parameters<typeof useFetchEndpointPolicy>[1]>;
let http: AppContextTestRender['coreStart']['http'];
let apiMocks: ReturnType<typeof allFleetHttpMocks>;
let renderHook: () => ReturnType<HookRenderer>;
beforeEach(() => {
const testContext = createAppRootMockRenderer();
queryOptions = {};
http = testContext.coreStart.http;
apiMocks = allFleetHttpMocks(http);
policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
renderHook = () => {
return (testContext.renderReactQueryHook as HookRenderer)(() =>
useFetchEndpointPolicy(policy.id, queryOptions)
);
};
});
it('should call the correct api with expected value', async () => {
await renderHook();
expect(apiMocks.responseProvider.endpointPackagePolicy).toHaveBeenCalledWith({
path: `/api/fleet/package_policies/${policy.id}`,
});
});
it('should return the expected output', async () => {
apiMocks.responseProvider.endpointPackagePolicy.mockReturnValueOnce({
item: policy,
});
const { data } = await renderHook();
expect(data).toEqual({
item: policy,
settings: policy.inputs[0].config.policy.value,
artifactManifest: policy.inputs[0].config.artifact_manifest.value,
});
});
it('should apply defaults to the policy data if necessary', async () => {
policy.updated_at = expect.any(String);
policy.created_at = expect.any(String);
// Expected updates by the hook
const policySettings = policy.inputs[0].config.policy.value;
[
'windows.popup.malware.message',
'mac.popup.malware.message',
'linux.popup.malware.message',
'windows.popup.ransomware.message',
].forEach((keyPath) => {
set(policySettings, keyPath, DefaultPolicyNotificationMessage);
});
[
'windows.popup.memory_protection.message',
'windows.popup.behavior_protection.message',
'mac.popup.behavior_protection.message',
'linux.popup.behavior_protection.message',
].forEach((keyPath) => {
set(policySettings, keyPath, DefaultPolicyRuleNotificationMessage);
});
// These should not be updated by the hook since the API response has them already defined
set(policySettings, 'mac.popup.memory_protection.message', 'hello world for mac');
set(policySettings, 'linux.popup.memory_protection.message', 'hello world for linux');
// Setup API response with two of the messages having a value defined.
const apiResponsePolicy = new FleetPackagePolicyGenerator(
'seed'
).generateEndpointPackagePolicy();
set(
apiResponsePolicy.inputs[0].config.policy.value,
'mac.popup.memory_protection.message',
'hello world for mac'
);
set(
apiResponsePolicy.inputs[0].config.policy.value,
'linux.popup.memory_protection.message',
'hello world for linux'
);
apiMocks.responseProvider.endpointPackagePolicy.mockReturnValueOnce({
item: apiResponsePolicy,
});
const { data } = await renderHook();
expect(data).toEqual({
item: policy,
settings: policy.inputs[0].config.policy.value,
artifactManifest: policy.inputs[0].config.artifact_manifest.value,
});
});
it('should apply default values to api returned data', async () => {
queryOptions.queryKey = ['a', 'b'];
queryOptions.retry = false;
queryOptions.refetchInterval = 5;
await renderHook();
expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining(queryOptions));
});
});

View file

@ -27,7 +27,7 @@ interface ApiDataResponse {
artifactManifest: ManifestSchema;
}
type UseFetchEndpointPolicyResponse = UseQueryResult<ApiDataResponse, IHttpFetchError>;
export type UseFetchEndpointPolicyResponse = UseQueryResult<ApiDataResponse, IHttpFetchError>;
/**
* Retrieve a single endpoint integration policy (details)
@ -36,7 +36,7 @@ type UseFetchEndpointPolicyResponse = UseQueryResult<ApiDataResponse, IHttpFetch
*/
export const useFetchEndpointPolicy = (
policyId: string,
options: UseQueryOptions<ApiDataResponse, IHttpFetchError> = {}
options: Omit<UseQueryOptions<ApiDataResponse, IHttpFetchError>, 'queryFn'> = {}
): UseFetchEndpointPolicyResponse => {
const http = useHttp();

View file

@ -0,0 +1,81 @@
/*
* 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 { useQuery as _useQuery } from '@tanstack/react-query';
import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import type { PolicyData } from '../../../../common/endpoint/types';
import { allFleetHttpMocks } from '../../mocks';
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import { useFetchAgentByAgentPolicySummary } from './use_fetch_endpoint_policy_agent_summary';
import { agentRouteService } from '@kbn/fleet-plugin/common';
const useQueryMock = _useQuery as jest.Mock;
jest.mock('@tanstack/react-query', () => {
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
return {
...actualReactQueryModule,
useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
};
});
describe('When using the `useFetchEndpointPolicyAgentSummary()` hook', () => {
type HookRenderer = ReactQueryHookRenderer<
Parameters<typeof useFetchAgentByAgentPolicySummary>,
ReturnType<typeof useFetchAgentByAgentPolicySummary>
>;
let policy: PolicyData;
let queryOptions: NonNullable<Parameters<typeof useFetchAgentByAgentPolicySummary>[1]>;
let http: AppContextTestRender['coreStart']['http'];
let apiMocks: ReturnType<typeof allFleetHttpMocks>;
let renderHook: () => ReturnType<HookRenderer>;
beforeEach(() => {
const testContext = createAppRootMockRenderer();
queryOptions = {};
http = testContext.coreStart.http;
apiMocks = allFleetHttpMocks(http);
policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
renderHook = () => {
return (testContext.renderReactQueryHook as HookRenderer)(() =>
useFetchAgentByAgentPolicySummary(policy.policy_id, queryOptions)
);
};
});
it('should call the correct api with expected value', async () => {
const { data } = await renderHook();
expect(apiMocks.responseProvider.agentStatus).toHaveBeenCalledWith({
path: agentRouteService.getStatusPath(),
query: { policyId: policy.policy_id },
});
expect(data).toEqual({
total: 50,
inactive: 5,
online: 40,
error: 0,
offline: 5,
updating: 0,
other: 0,
events: 0,
});
});
it('should apply default values to api returned data', async () => {
queryOptions.queryKey = ['a', 'b'];
queryOptions.retry = false;
queryOptions.refetchInterval = 5;
await renderHook();
expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining(queryOptions));
});
});

View file

@ -19,7 +19,7 @@ export const useFetchAgentByAgentPolicySummary = (
* The Fleet Agent Policy ID (NOT the endpoint policy id)
*/
agentPolicyId: string,
options: UseQueryOptions<EndpointPolicyAgentSummary, IHttpFetchError> = {}
options: Omit<UseQueryOptions<EndpointPolicyAgentSummary, IHttpFetchError>, 'queryFn'> = {}
): UseQueryResult<EndpointPolicyAgentSummary, IHttpFetchError> => {
const http = useHttp();

View file

@ -0,0 +1,79 @@
/*
* 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 { useMutation as _useMutation } from '@tanstack/react-query';
import type { AppContextTestRender } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { allFleetHttpMocks } from '../../mocks';
import type {
UseUpdateEndpointPolicyOptions,
UseUpdateEndpointPolicyResult,
} from './use_update_endpoint_policy';
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
import { useUpdateEndpointPolicy } from './use_update_endpoint_policy';
import type { PolicyData } from '../../../../common/endpoint/types';
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy';
const useMutationMock = _useMutation as jest.Mock;
jest.mock('@tanstack/react-query', () => {
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
return {
...actualReactQueryModule,
useMutation: jest.fn((...args) => actualReactQueryModule.useMutation(...args)),
};
});
describe('When using the `useFetchEndpointPolicyAgentSummary()` hook', () => {
let customOptions: UseUpdateEndpointPolicyOptions;
let http: AppContextTestRender['coreStart']['http'];
let apiMocks: ReturnType<typeof allFleetHttpMocks>;
let policy: PolicyData;
let renderHook: () => RenderHookResult<
UseUpdateEndpointPolicyOptions,
UseUpdateEndpointPolicyResult
>;
beforeEach(() => {
const testContext = createAppRootMockRenderer();
http = testContext.coreStart.http;
apiMocks = allFleetHttpMocks(http);
policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
customOptions = {};
renderHook = () => {
return testContext.renderHook(() => useUpdateEndpointPolicy(customOptions));
};
});
it('should send expected update payload', async () => {
const {
result: {
current: { mutateAsync },
},
} = renderHook();
const result = await mutateAsync({ policy });
expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalledWith({
path: packagePolicyRouteService.getUpdatePath(policy.id),
body: JSON.stringify(getPolicyDataForUpdate(policy)),
});
expect(result).toEqual({ item: expect.any(Object) });
});
it('should allow custom options to be passed to ReactQuery', async () => {
customOptions.mutationKey = ['abc-123'];
customOptions.cacheTime = 10;
renderHook();
expect(useMutationMock).toHaveBeenCalledWith(expect.any(Function), customOptions);
});
});

View file

@ -18,13 +18,12 @@ interface UpdateParams {
policy: PolicyData;
}
type UseUpdateEndpointPolicyOptions = UseMutationOptions<
UpdatePolicyResponse,
IHttpFetchError,
UpdateParams
export type UseUpdateEndpointPolicyOptions = Omit<
UseMutationOptions<UpdatePolicyResponse, IHttpFetchError, UpdateParams>,
'mutationFn'
>;
type UseUpdateEndpointPolicyResult = UseMutationResult<
export type UseUpdateEndpointPolicyResult = UseMutationResult<
UpdatePolicyResponse,
IHttpFetchError,
UpdateParams

View file

@ -25,9 +25,16 @@ import {
PACKAGE_POLICY_API_ROUTES,
} from '@kbn/fleet-plugin/common';
import type { ResponseProvidersInterface } from '../../common/mock/endpoint/http_handler_mock_factory';
import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_mock_factory';
import {
composeHttpHandlerMocks,
httpHandlerMockFactory,
} from '../../common/mock/endpoint/http_handler_mock_factory';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import type { GetPolicyListResponse, GetPolicyResponse } from '../pages/policy/types';
import type {
GetPolicyListResponse,
GetPolicyResponse,
UpdatePolicyResponse,
} from '../pages/policy/types';
import { FleetAgentPolicyGenerator } from '../../../common/endpoint/data_generators/fleet_agent_policy_generator';
import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator';
@ -169,7 +176,7 @@ export const fleetGetEndpointPackagePolicyHttpMock =
method: 'get',
handler: () => {
const response: GetPolicyResponse = {
item: new EndpointDocGenerator('seed').generatePolicyPackagePolicy(),
item: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy(),
};
return response;
},
@ -250,12 +257,12 @@ export const fleetGetAgentPolicyListHttpMock =
]);
export type FleetBulkGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{
agentPolicy: () => BulkGetAgentPoliciesResponse;
bulkAgentPolicy: () => BulkGetAgentPoliciesResponse;
}>;
export const fleetBulkGetAgentPolicyListHttpMock =
httpHandlerMockFactory<FleetGetAgentPolicyListHttpMockInterface>([
httpHandlerMockFactory<FleetBulkGetAgentPolicyListHttpMockInterface>([
{
id: 'agentPolicy',
id: 'bulkAgentPolicy',
path: AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN,
method: 'post',
handler: ({ body }) => {
@ -288,12 +295,12 @@ export const fleetBulkGetAgentPolicyListHttpMock =
]);
export type FleetBulkGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{
packagePolicies: () => BulkGetPackagePoliciesResponse;
bulkPackagePolicies: () => BulkGetPackagePoliciesResponse;
}>;
export const fleetBulkGetPackagePoliciesListHttpMock =
httpHandlerMockFactory<FleetBulkGetPackagePoliciesListHttpMockInterface>([
{
id: 'packagePolicies',
id: 'bulkPackagePolicies',
path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN,
method: 'post',
handler: ({ body }) => {
@ -427,3 +434,49 @@ export const fleetGetAgentStatusHttpMock =
},
},
]);
export type FleetPutEndpointPackagePolicyHttpMockInterface = ResponseProvidersInterface<{
updateEndpointPolicy: () => UpdatePolicyResponse;
}>;
export const fleetPutEndpointPackagePolicyHttpMock =
httpHandlerMockFactory<FleetPutEndpointPackagePolicyHttpMockInterface>([
{
id: 'updateEndpointPolicy',
path: PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN,
method: 'put',
handler: ({ body }) => {
const updatedPolicy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy(
JSON.parse(body as string)
);
return {
item: updatedPolicy,
};
},
},
]);
export type AllFleetHttpMocksInterface = FleetGetAgentStatusHttpMockInterface &
FleetGetEndpointPackagePolicyHttpMockInterface &
FleetGetEndpointPackagePolicyListHttpMockInterface &
FleetGetPackageHttpMockInterface &
FleetBulkGetAgentPolicyListHttpMockInterface &
FleetGetAgentPolicyListHttpMockInterface &
FleetGetPackageListHttpMockInterface &
FleetGetPackagePoliciesListHttpMockInterface &
FleetBulkGetPackagePoliciesListHttpMockInterface &
FleetPutEndpointPackagePolicyHttpMockInterface;
export const allFleetHttpMocks = composeHttpHandlerMocks<AllFleetHttpMocksInterface>([
fleetGetAgentStatusHttpMock,
fleetGetEndpointPackagePolicyHttpMock,
fleetGetEndpointPackagePolicyListHttpMock,
fleetGetPackageHttpMock,
fleetBulkGetAgentPolicyListHttpMock,
fleetGetAgentPolicyListHttpMock,
fleetGetPackageListHttpMock,
fleetGetPackageListHttpMock,
fleetGetPackagePoliciesListHttpMock,
fleetBulkGetPackagePoliciesListHttpMock,
fleetPutEndpointPackagePolicyHttpMock,
]);

View file

@ -0,0 +1,181 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../mocks';
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
import type { AdvancedSectionProps } from './advanced_section';
import { AdvancedSection } from './advanced_section';
import userEvent from '@testing-library/user-event';
import { AdvancedPolicySchema } from '../../../models/advanced_policy_schema';
import { within } from '@testing-library/dom';
import { set } from 'lodash';
jest.mock('../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy Advanced Settings section', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').advancedSection;
let formProps: AdvancedSectionProps;
let render: (expanded?: boolean) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
const clickShowHideButton = () => {
userEvent.click(renderResult.getByTestId(testSubj.showHideButton));
};
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.container,
};
render = (expanded = true) => {
renderResult = mockedContext.render(<AdvancedSection {...formProps} />);
if (expanded) {
clickShowHideButton();
expect(renderResult.getByTestId(testSubj.settingsContainer));
}
return renderResult;
};
});
it('should render initially collapsed', () => {
render(false);
expect(renderResult.queryByTestId(testSubj.settingsContainer)).toBeNull();
});
it('should expand and collapse section when button is clicked', () => {
render(false);
expect(renderResult.queryByTestId(testSubj.settingsContainer)).toBeNull();
clickShowHideButton();
expect(renderResult.getByTestId(testSubj.settingsContainer));
});
it('should show warning callout', () => {
const { getByTestId } = render(true);
expect(getByTestId(testSubj.warningCallout));
});
it('should render all advanced options', () => {
const fieldsWithDefaultValues = [
'mac.advanced.capture_env_vars',
'linux.advanced.capture_env_vars',
];
render(true);
for (const advancedOption of AdvancedPolicySchema) {
const optionTestSubj = testSubj.settingRowTestSubjects(advancedOption.key);
const renderedRow = within(renderResult.getByTestId(optionTestSubj.container));
expect(renderedRow.getByTestId(optionTestSubj.textField));
expect(renderedRow.getByTestId(optionTestSubj.label)).toHaveTextContent(
exactMatchText(advancedOption.key)
);
expect(renderedRow.getByTestId(optionTestSubj.versionInfo)).toHaveTextContent(
advancedOption.first_supported_version
);
if (advancedOption.last_supported_version) {
expect(renderedRow.getByTestId(optionTestSubj.versionInfo)).toHaveTextContent(
advancedOption.last_supported_version
);
}
if (fieldsWithDefaultValues.includes(advancedOption.key)) {
expect(renderedRow.getByTestId<HTMLInputElement>(optionTestSubj.textField).value).not.toBe(
''
);
} else {
expect(renderedRow.getByTestId<HTMLInputElement>(optionTestSubj.textField).value).toBe('');
}
}
});
describe('and when license is lower than Platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should not render options that require platinum license', () => {
render(true);
for (const advancedOption of AdvancedPolicySchema) {
if (advancedOption.license) {
if (advancedOption.license === 'platinum') {
expect(
renderResult.queryByTestId(
testSubj.settingRowTestSubjects(advancedOption.key).container
)
).toBeNull();
} else {
throw new Error(
`${advancedOption.key}: Unknown license value: ${advancedOption.license}`
);
}
}
}
});
});
describe('and when rendered in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render with no form fields', () => {
render();
expectIsViewOnly(renderResult.getByTestId(testSubj.settingsContainer));
});
it('should render options in expected content', () => {
const option1 = AdvancedPolicySchema[0];
const option2 = AdvancedPolicySchema[4];
set(formProps.policy, option1.key, 'foo');
set(formProps.policy, option2.key, ''); // test empty value
const { getByTestId } = render();
expectIsViewOnly(renderResult.getByTestId(testSubj.settingsContainer));
expect(getByTestId(testSubj.settingRowTestSubjects(option1.key).container)).toHaveTextContent(
exactMatchText('linux.advanced.agent.connection_delayInfo 7.9+foo')
);
expect(getByTestId(testSubj.settingRowTestSubjects(option2.key).container)).toHaveTextContent(
exactMatchText('linux.advanced.artifacts.global.intervalInfo 7.9+—')
);
});
});
});

View file

@ -197,18 +197,25 @@ export const AdvancedSection = memo<AdvancedSectionProps>(
<EuiFormRow
key={key}
fullWidth
data-test-subj={getTestId(`${key}-container`)}
label={
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={true}>{key}</EuiFlexItem>
<EuiFlexItem grow={true} data-test-subj={getTestId(`${key}-label`)}>
{key}
</EuiFlexItem>
{documentation && (
<EuiFlexItem grow={false}>
<EuiIconTip content={documentation} position="right" />
<EuiIconTip
content={documentation}
position="right"
anchorProps={{ 'data-test-subj': getTestId(`${key}-tooltipIcon`) }}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
}
labelAppend={
<EuiText size="xs">
<EuiText size="xs" data-test-subj={getTestId(`${key}-versionInfo`)}>
{lastVersion ? `${firstVersion}-${lastVersion}` : `${firstVersion}+`}
</EuiText>
}
@ -220,10 +227,11 @@ export const AdvancedSection = memo<AdvancedSectionProps>(
name={key}
value={value as string}
onChange={handleAdvancedSettingUpdate}
disabled={!isEditMode}
/>
) : (
<EuiText size="xs">{value || getEmptyValue()}</EuiText>
<EuiText size="xs" data-test-subj={getTestId(`${key}-viewValue`)}>
{value || getEmptyValue()}
</EuiText>
)}
</EuiFormRow>
);

View file

@ -0,0 +1,125 @@
/*
* 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 from 'react';
import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../../mocks';
import type { AntivirusRegistrationCardProps } from './antivirus_registration_card';
import {
NOT_REGISTERED_LABEL,
REGISTERED_LABEL,
AntivirusRegistrationCard,
} from './antivirus_registration_card';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import userEvent from '@testing-library/user-event';
import { cloneDeep, set } from 'lodash';
describe('Policy Form Antivirus Registration Card', () => {
const antivirusTestSubj = getPolicySettingsFormTestSubjects('test').antivirusRegistration;
let formProps: AntivirusRegistrationCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': antivirusTestSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<AntivirusRegistrationCard {...formProps} />));
});
it('should render in edit mode', () => {
render();
expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'false'
);
});
it('should display for windows OS with restriction', () => {
render();
expect(renderResult.getByTestId(antivirusTestSubj.osValueContainer)).toHaveTextContent(
'Windows RestrictionsInfo'
);
});
it('should be able to enable the option', () => {
const expectedUpdate = cloneDeep(formProps.policy);
set(expectedUpdate, 'windows.antivirus_registration.enabled', true);
render();
expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'false'
);
userEvent.click(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdate,
});
});
it('should be able to disable the option', async () => {
set(formProps.policy, 'windows.antivirus_registration.enabled', true);
const expectedUpdate = cloneDeep(formProps.policy);
set(expectedUpdate, 'windows.antivirus_registration.enabled', false);
render();
expect(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'true'
);
userEvent.click(renderResult.getByTestId(antivirusTestSubj.enableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdate,
});
});
describe('And rendered in View only mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render in view mode (option disabled)', () => {
render();
expectIsViewOnly(renderResult.getByTestId(antivirusTestSubj.card));
expect(renderResult.getByTestId(antivirusTestSubj.viewOnlyValue)).toHaveTextContent(
NOT_REGISTERED_LABEL
);
});
it('should render in view mode (option enabled)', () => {
formProps.policy.windows.antivirus_registration.enabled = true;
render();
expectIsViewOnly(renderResult.getByTestId(antivirusTestSubj.card));
expect(renderResult.getByTestId(antivirusTestSubj.viewOnlyValue)).toHaveTextContent(
REGISTERED_LABEL
);
});
});
});

View file

@ -21,7 +21,7 @@ const CARD_TITLE = i18n.translate(
}
);
const DESCRIPTON = i18n.translate(
const DESCRIPTION = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation',
{
defaultMessage:
@ -30,21 +30,21 @@ const DESCRIPTON = i18n.translate(
}
);
const REGISTERED_LABEL = i18n.translate(
export const REGISTERED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type',
{
defaultMessage: 'Register as antivirus',
}
);
const NOT_REGISTERED_LABEL = i18n.translate(
export const NOT_REGISTERED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.notRegisteredLabel',
{
defaultMessage: 'Do not register as antivirus',
}
);
type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps;
export type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps;
export const AntivirusRegistrationCard = memo<AntivirusRegistrationCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@ -76,14 +76,19 @@ export const AntivirusRegistrationCard = memo<AntivirusRegistrationCardProps>(
}
)}
>
{isEditMode && <EuiText size="s">{DESCRIPTON}</EuiText>}
{isEditMode && <EuiText size="s">{DESCRIPTION}</EuiText>}
<EuiSpacer size="s" />
{isEditMode ? (
<EuiSwitch label={label} checked={isChecked} onChange={handleSwitchChange} />
<EuiSwitch
label={label}
checked={isChecked}
onChange={handleSwitchChange}
data-test-subj={getTestId('switch')}
/>
) : (
<div>{label}</div>
<div data-test-subj={getTestId('value')}>{label}</div>
)}
</SettingCard>
);

View file

@ -0,0 +1,170 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type { AttackSurfaceReductionCardProps } from './attack_surface_reduction_card';
import {
AttackSurfaceReductionCard,
LOCKED_CARD_ATTACK_SURFACE_REDUCTION,
SWITCH_DISABLED_LABEL,
SWITCH_ENABLED_LABEL,
} from './attack_surface_reduction_card';
import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
import { cloneDeep, set } from 'lodash';
import userEvent from '@testing-library/user-event';
import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
jest.mock('../../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy Attack Surface Reduction Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').attackSurface;
let formProps: AttackSurfaceReductionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<AttackSurfaceReductionCard {...formProps} />));
});
it('should show card in edit mode', () => {
render();
expect(renderResult.getByTestId(testSubj.enableDisableSwitch));
});
it('should show correct OS support', () => {
render();
expect(renderResult.getByTestId(testSubj.osValues)).toHaveTextContent('Windows');
});
it('should show option enabled', () => {
render();
expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'true'
);
});
it('should show option disabled', () => {
set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
render();
expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'false'
);
});
it('should be able to toggle to disabled', () => {
const expectedUpdate = cloneDeep(formProps.policy);
set(expectedUpdate, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
render();
expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'true'
);
userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdate,
});
});
it('should should be able to toggle to enabled', () => {
set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
const expectedUpdate = cloneDeep(formProps.policy);
set(expectedUpdate, 'windows.attack_surface_reduction.credential_hardening.enabled', true);
render();
expect(renderResult.getByTestId(testSubj.enableDisableSwitch)).toHaveAttribute(
'aria-checked',
'false'
);
userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdate,
});
});
describe('and license is lower than Platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should show locked card if license not platinum+', () => {
render();
expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
LOCKED_CARD_ATTACK_SURFACE_REDUCTION
);
});
});
describe('and displayed in View Mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render in view mode', () => {
render();
expectIsViewOnly(renderResult.getByTestId(testSubj.card));
});
it('should show correct value when disabled', () => {
render();
expect(renderResult.getByTestId(testSubj.viewModeValue)).toHaveTextContent(
SWITCH_ENABLED_LABEL
);
});
it('should show correct value when enabled', () => {
set(formProps.policy, 'windows.attack_surface_reduction.credential_hardening.enabled', false);
render();
expect(renderResult.getByTestId(testSubj.viewModeValue)).toHaveTextContent(
SWITCH_DISABLED_LABEL
);
});
});
});

View file

@ -18,7 +18,7 @@ import { SettingCard } from '../setting_card';
const ATTACK_SURFACE_OS_LIST = [OperatingSystem.WINDOWS];
const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate(
export const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.attack_surface_reduction',
{
defaultMessage: 'Attack Surface Reduction',
@ -32,21 +32,21 @@ const CARD_TITLE = i18n.translate(
}
);
const SWITCH_ENABLED_LABEL = i18n.translate(
export const SWITCH_ENABLED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleEnabled',
{
defaultMessage: 'Credential hardening enabled',
}
);
const SWITCH_DISABLED_LABEL = i18n.translate(
export const SWITCH_DISABLED_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleDisabled',
{
defaultMessage: 'Credential hardening disabled',
}
);
type AttackSurfaceReductionCardProps = PolicyFormComponentCommonProps;
export type AttackSurfaceReductionCardProps = PolicyFormComponentCommonProps;
export const AttackSurfaceReductionCard = memo<AttackSurfaceReductionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@ -69,7 +69,12 @@ export const AttackSurfaceReductionCard = memo<AttackSurfaceReductionCardProps>(
);
if (!isPlatinumPlus) {
return <SettingLockedCard title={LOCKED_CARD_ATTACK_SURFACE_REDUCTION} />;
return (
<SettingLockedCard
title={LOCKED_CARD_ATTACK_SURFACE_REDUCTION}
data-test-subj={getTestId('locked')}
/>
);
}
return (
@ -86,7 +91,7 @@ export const AttackSurfaceReductionCard = memo<AttackSurfaceReductionCardProps>(
data-test-subj={getTestId('enableDisableSwitch')}
/>
) : (
<>{label}</>
<span data-test-subj={getTestId('valueLabel')}>{label}</span>
)}
</SettingCard>
);

View file

@ -0,0 +1,159 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type { BehaviourProtectionCardProps } from './behaviour_protection_card';
import { BehaviourProtectionCard, LOCKED_CARD_BEHAVIOR_TITLE } from './behaviour_protection_card';
import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
import { set } from 'lodash';
import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
jest.mock('../../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy Behaviour Protection Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').behaviour;
let formProps: BehaviourProtectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<BehaviourProtectionCard {...formProps} />));
});
it('should render the card with expected components', () => {
const { getByTestId } = render();
expect(getByTestId(testSubj.enableDisableSwitch));
expect(getByTestId(testSubj.protectionPreventRadio));
expect(getByTestId(testSubj.notifyUserCheckbox));
expect(getByTestId(testSubj.rulesCallout));
});
it('should show supported OS values', () => {
const { getByTestId } = render();
expect(getByTestId(testSubj.osValuesContainer)).toHaveTextContent('Windows, Mac, Linux');
});
describe('and license is lower than Platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should show locked card if license not platinum+', () => {
render();
expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
LOCKED_CARD_BEHAVIOR_TITLE
);
});
});
describe('and displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should display correctly when overall card is enabled', () => {
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
'Type' +
'Malicious behavior' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malicious behavior protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules.'
);
});
it('should display correctly when overall card is disabled', () => {
set(formProps.policy, 'windows.behavior_protection.mode', ProtectionModes.off);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Malicious behavior' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malicious behavior protections disabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display user notification disabled', () => {
set(formProps.policy, 'windows.popup.behavior_protection.enabled', false);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Malicious behavior' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malicious behavior protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
"Don't notify user" +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
});
});

View file

@ -8,8 +8,7 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { SettingCard } from '../setting_card';
import { NotifyUserOption } from '../notify_user_option';
@ -18,13 +17,12 @@ import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { BehaviorProtectionOSes } from '../../../../types';
import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
import type { PolicyFormComponentCommonProps } from '../../types';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate(
export const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.behavior',
{
defaultMessage: 'Malicious Behavior',
@ -37,7 +35,7 @@ const BEHAVIOUR_OS_VALUES: Immutable<BehaviorProtectionOSes[]> = [
PolicyOperatingSystem.linux,
];
type BehaviourProtectionCardProps = PolicyFormComponentCommonProps;
export type BehaviourProtectionCardProps = PolicyFormComponentCommonProps;
export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@ -52,7 +50,12 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
);
if (!isPlatinumPlus) {
return <SettingLockedCard title={LOCKED_CARD_BEHAVIOR_TITLE} />;
return (
<SettingLockedCard
title={LOCKED_CARD_BEHAVIOR_TITLE}
data-test-subj={getTestId('locked')}
/>
);
}
return (
@ -70,7 +73,7 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
protection={protection}
protectionLabel={protectionLabel}
osList={BEHAVIOUR_OS_VALUES}
data-test-subj={getTestId()}
data-test-subj={getTestId('enableDisableSwitch')}
/>
}
>
@ -80,6 +83,7 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
mode={mode}
protection={protection}
osList={BEHAVIOUR_OS_VALUES}
data-test-subj={getTestId('protectionLevel')}
/>
<NotifyUserOption
@ -88,25 +92,11 @@ export const BehaviourProtectionCard = memo<BehaviourProtectionCardProps>(
mode={mode}
protection={protection}
osList={BEHAVIOUR_OS_VALUES}
data-test-subj={getTestId('notifyUser')}
/>
<EuiSpacer size="m" />
<EuiCallOut iconType="iInCircle">
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesMessage"
defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page."
values={{
detectionRulesLink: (
<LinkToApp appId={APP_UI_ID} deepLinkId={SecurityPageName.rules}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink"
defaultMessage="related detection rules"
/>
</LinkToApp>
),
}}
/>
</EuiCallOut>
<RelatedDetectionRulesCallout data-test-subj={getTestId('rulesCallout')} />
</SettingCard>
);
}

View file

@ -0,0 +1,106 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type { LinuxEventCollectionCardProps } from './linux_event_collection_card';
import { LinuxEventCollectionCard } from './linux_event_collection_card';
import { set } from 'lodash';
describe('Policy Linux Event Collection Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').linuxEvents;
let formProps: LinuxEventCollectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<LinuxEventCollectionCard {...formProps} />));
});
it('should render card with expected content', () => {
const { getByTestId } = render();
expect(
getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
).toHaveLength(3);
expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Linux'));
expect(getByTestId(testSubj.sessionDataCheckbox)).not.toBeChecked();
expect(getByTestId(testSubj.captureTerminalCheckbox)).toBeDisabled();
});
describe('and is displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render card with expected content when session data collection is disabled', () => {
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Linux ' +
'3 / 3 event collections enabled' +
'Events' +
'File' +
'Network' +
'Process'
)
);
});
it('should render card with expected content when session data collection is enabled', () => {
set(formProps.policy, 'linux.events.session_data', true);
set(formProps.policy, 'linux.events.tty_io', true);
set(formProps.policy, 'linux.events.file', false);
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Linux ' +
'2 / 3 event collections enabled' +
'Events' +
'Network' +
'Process' +
'Session data' +
'Collect session data' +
'Capture terminal output' +
'Info' +
'beta'
)
);
});
});
});

View file

@ -10,8 +10,8 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
import { i18n } from '@kbn/i18n';
import type { PolicyFormComponentCommonProps } from '../../types';
import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types';
import type { EventFormOption, SupplementalEventFormOption } from './event_collection_card';
import { EventCollectionCard } from './event_collection_card';
import type { EventFormOption, SupplementalEventFormOption } from '../event_collection_card';
import { EventCollectionCard } from '../event_collection_card';
const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.LINUX>> = [
{
@ -102,7 +102,7 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray<SupplementalEventFormOption<OperatingS
},
];
type LinuxEventCollectionCardProps = PolicyFormComponentCommonProps;
export type LinuxEventCollectionCardProps = PolicyFormComponentCommonProps;
export const LinuxEventCollectionCard = memo<LinuxEventCollectionCardProps>((props) => {
const supplementalOptions = useMemo(() => {

View file

@ -0,0 +1,96 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { set } from 'lodash';
import type { MacEventCollectionCardProps } from './mac_event_collection_card';
import { MacEventCollectionCard } from './mac_event_collection_card';
describe('Policy Mac Event Collection Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').macEvents;
let formProps: MacEventCollectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () => (renderResult = mockedContext.render(<MacEventCollectionCard {...formProps} />));
});
it('should render card with expected content', () => {
const { getByTestId } = render();
expect(
getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
).toHaveLength(3);
expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Mac'));
});
describe('and is displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render card with expected content when session data collection is disabled', () => {
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Mac ' +
'3 / 3 event collections enabled' +
'Events' +
'File' +
'Process' +
'Network'
)
);
});
it('should render card with expected content when certain events are un-checked', () => {
set(formProps.policy, 'mac.events.file', false);
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Mac ' +
'2 / 3 event collections enabled' +
'Events' +
'Process' +
'Network'
)
);
});
});
});

View file

@ -8,8 +8,8 @@
import React, { memo } from 'react';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { i18n } from '@kbn/i18n';
import type { EventFormOption } from './event_collection_card';
import { EventCollectionCard } from './event_collection_card';
import type { EventFormOption } from '../event_collection_card';
import { EventCollectionCard } from '../event_collection_card';
import type { PolicyFormComponentCommonProps } from '../../types';
const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.MAC>> = [
@ -33,7 +33,7 @@ const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.MAC>> = [
},
];
type MacEventCollectionCardProps = PolicyFormComponentCommonProps;
export type MacEventCollectionCardProps = PolicyFormComponentCommonProps;
export const MacEventCollectionCard = memo<MacEventCollectionCardProps>((props) => {
return (

View file

@ -0,0 +1,191 @@
/*
* 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 {
expectIsViewOnly,
getPolicySettingsFormTestSubjects,
exactMatchText,
setMalwareMode,
} from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type { MalwareProtectionsProps } from './malware_protections_card';
import { MalwareProtectionsCard } from './malware_protections_card';
import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
import { cloneDeep, set } from 'lodash';
import userEvent from '@testing-library/user-event';
jest.mock('../../../../../../../common/hooks/use_license');
describe('Policy Malware Protections Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').malware;
let formProps: MalwareProtectionsProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () => (renderResult = mockedContext.render(<MalwareProtectionsCard {...formProps} />));
});
it('should render the card with expected components', () => {
const { getByTestId } = render();
expect(getByTestId(testSubj.enableDisableSwitch));
expect(getByTestId(testSubj.protectionPreventRadio));
expect(getByTestId(testSubj.notifyUserCheckbox));
expect(getByTestId(testSubj.rulesCallout));
});
it('should show supported OS values', () => {
render();
expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
'Windows, Mac, Linux'
);
});
it('should set Blocklist to disabled if malware is turned off', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
setMalwareMode(expectedUpdatedPolicy, true);
render();
userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should allow blocklist to be disabled', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.malware.blocklist', false);
set(expectedUpdatedPolicy, 'mac.malware.blocklist', false);
set(expectedUpdatedPolicy, 'linux.malware.blocklist', false);
render();
userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should allow blocklist to be enabled', () => {
set(formProps.policy, 'windows.malware.blocklist', false);
set(formProps.policy, 'mac.malware.blocklist', false);
set(formProps.policy, 'linux.malware.blocklist', false);
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.malware.blocklist', true);
set(expectedUpdatedPolicy, 'mac.malware.blocklist', true);
set(expectedUpdatedPolicy, 'linux.malware.blocklist', true);
render();
userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
describe('and displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should display correctly when overall card is enabled', () => {
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Malware' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malware protections enabled' +
'Protection level' +
'Prevent' +
'Blocklist enabled' +
'Info' +
'User notification' +
'Agent version 7.11+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display correctly when overall card is disabled', () => {
set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Malware' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malware protections disabled' +
'Protection level' +
'Prevent' +
'Blocklist enabled' +
'Info' +
'User notification' +
'Agent version 7.11+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display user notification disabled', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Malware' +
'Operating system' +
'Windows, Mac, Linux ' +
'Malware protections enabled' +
'Protection level' +
'Prevent' +
'Blocklist enabled' +
'Info' +
'User notification' +
'Agent version 7.11+' +
"Don't notify user" +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
});
});

View file

@ -8,25 +8,16 @@
import React, { memo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiCallOut,
EuiSpacer,
EuiSwitch,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
} from '@elastic/eui';
import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { cloneDeep } from 'lodash';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
import { NotifyUserOption } from '../notify_user_option';
import { SettingCard } from '../setting_card';
import type { PolicyFormComponentCommonProps } from '../../types';
import { APP_UI_ID } from '../../../../../../../../common';
import { SecurityPageName } from '../../../../../../../app/types';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { MalwareProtectionOSes } from '../../../../types';
import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import type { ProtectionSettingCardSwitchProps } from '../protection_setting_card_switch';
import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level';
@ -112,7 +103,7 @@ export const MalwareProtectionsCard = React.memo<MalwareProtectionsProps>(
policy={policy}
onChange={onChange}
mode={mode}
data-test-subj={getTestId('enableDisableBlocklist')}
data-test-subj={getTestId('blocklist')}
/>
<NotifyUserOption
@ -125,22 +116,7 @@ export const MalwareProtectionsCard = React.memo<MalwareProtectionsProps>(
/>
<EuiSpacer size="m" />
<EuiCallOut iconType="iInCircle">
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesMessage"
defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page."
values={{
detectionRulesLink: (
<LinkToApp appId={APP_UI_ID} deepLinkId={SecurityPageName.rules}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink"
defaultMessage="related detection rules"
/>
</LinkToApp>
),
}}
/>
</EuiCallOut>
<RelatedDetectionRulesCallout data-test-subj={getTestId('rulesCallout')} />
</SettingCard>
);
}
@ -150,56 +126,60 @@ MalwareProtectionsCard.displayName = 'MalwareProtectionsCard';
type EnableDisableBlocklistProps = PolicyFormComponentCommonProps;
const EnableDisableBlocklist = memo<EnableDisableBlocklistProps>(({ policy, onChange, mode }) => {
const checked = policy.windows.malware.blocklist;
const isDisabled = policy.windows.malware.mode === 'off';
const isEditMode = mode === 'edit';
const label = checked ? BLOCKLIST_ENABLED_LABEL : BLOCKLIST_DISABLED_LABEL;
const EnableDisableBlocklist = memo<EnableDisableBlocklistProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const checked = policy.windows.malware.blocklist;
const isDisabled = policy.windows.malware.mode === 'off';
const isEditMode = mode === 'edit';
const label = checked ? BLOCKLIST_ENABLED_LABEL : BLOCKLIST_DISABLED_LABEL;
const handleBlocklistSwitchChange = useCallback(
(event) => {
const value = event.target.checked;
const newPayload = cloneDeep(policy);
const handleBlocklistSwitchChange = useCallback(
(event) => {
const value = event.target.checked;
const newPayload = cloneDeep(policy);
adjustBlocklistSettingsOnProtectionSwitch({
value,
policyConfigData: newPayload,
protectionOsList: MALWARE_OS_VALUES,
});
adjustBlocklistSettingsOnProtectionSwitch({
value,
policyConfigData: newPayload,
protectionOsList: MALWARE_OS_VALUES,
});
onChange({ isValid: true, updatedPolicy: newPayload });
},
[onChange, policy]
);
onChange({ isValid: true, updatedPolicy: newPayload });
},
[onChange, policy]
);
return (
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
{isEditMode ? (
<EuiSwitch
label={label}
checked={checked}
onChange={handleBlocklistSwitchChange}
disabled={isDisabled}
return (
<EuiFlexGroup gutterSize="xs" data-test-subj={getTestId()}>
<EuiFlexItem grow={false}>
{isEditMode ? (
<EuiSwitch
label={label}
checked={checked}
onChange={handleBlocklistSwitchChange}
disabled={isDisabled}
data-test-subj={getTestId('enableDisableSwitch')}
/>
) : (
<>{label}</>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
position="right"
content={
<>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip"
defaultMessage="Enables or disables the blocklist associated with this policy. The blocklist is a collection hashes, paths, or signers which extends the list of processes the endpoint considers malicious. See the blocklist tab for entry details."
/>
</>
}
/>
) : (
<>{label}</>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
position="right"
content={
<>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip"
defaultMessage="Enables or disables the blocklist associated with this policy. The blocklist is a collection hashes, paths, or signers which extends the list of processes the endpoint considers malicious. See the blocklist tab for entry details."
/>
</>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
});
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
EnableDisableBlocklist.displayName = 'EnableDisableBlocklist';

View file

@ -0,0 +1,164 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
import { set } from 'lodash';
import type { MemoryProtectionCardProps } from './memory_protection_card';
import { LOCKED_CARD_MEMORY_TITLE, MemoryProtectionCard } from './memory_protection_card';
import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
jest.mock('../../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy Memory Protections Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').memory;
let formProps: MemoryProtectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () => (renderResult = mockedContext.render(<MemoryProtectionCard {...formProps} />));
});
it('should render the card with expected components', () => {
const { getByTestId } = render();
expect(getByTestId(testSubj.enableDisableSwitch));
expect(getByTestId(testSubj.protectionPreventRadio));
expect(getByTestId(testSubj.notifyUserCheckbox));
expect(getByTestId(testSubj.rulesCallout));
});
it('should show supported OS values', () => {
render();
expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
'Windows, Mac, Linux'
);
});
describe('and license is lower than Platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should show locked card if license not platinum+', () => {
render();
expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
exactMatchText(LOCKED_CARD_MEMORY_TITLE)
);
});
});
describe('and displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should display correctly when overall card is enabled', () => {
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Memory threat' +
'Operating system' +
'Windows, Mac, Linux ' +
'Memory threat protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display correctly when overall card is disabled', () => {
set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Memory threat' +
'Operating system' +
'Windows, Mac, Linux ' +
'Memory threat protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display user notification disabled', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Memory threat' +
'Operating system' +
'Windows, Mac, Linux ' +
'Memory threat protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.15+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
});
});

View file

@ -8,8 +8,7 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { NotifyUserOption } from '../notify_user_option';
import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level';
@ -18,13 +17,12 @@ import { SettingLockedCard } from '../setting_locked_card';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { MemoryProtectionOSes } from '../../../../types';
import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import type { PolicyFormComponentCommonProps } from '../../types';
import { SettingCard } from '../setting_card';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
const LOCKED_CARD_MEMORY_TITLE = i18n.translate(
export const LOCKED_CARD_MEMORY_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.memory',
{
defaultMessage: 'Memory Threat',
@ -37,7 +35,7 @@ const MEMORY_PROTECTION_OS_VALUES: Immutable<MemoryProtectionOSes[]> = [
PolicyOperatingSystem.linux,
];
type MemoryProtectionCardProps = PolicyFormComponentCommonProps;
export type MemoryProtectionCardProps = PolicyFormComponentCommonProps;
export const MemoryProtectionCard = memo<MemoryProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@ -52,7 +50,9 @@ export const MemoryProtectionCard = memo<MemoryProtectionCardProps>(
);
if (!isPlatinumPlus) {
return <SettingLockedCard title={LOCKED_CARD_MEMORY_TITLE} />;
return (
<SettingLockedCard title={LOCKED_CARD_MEMORY_TITLE} data-test-subj={getTestId('locked')} />
);
}
return (
@ -93,22 +93,7 @@ export const MemoryProtectionCard = memo<MemoryProtectionCardProps>(
/>
<EuiSpacer size="m" />
<EuiCallOut iconType="iInCircle">
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesMessage"
defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page."
values={{
detectionRulesLink: (
<LinkToApp appId={APP_UI_ID} deepLinkId={SecurityPageName.rules}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink"
defaultMessage="related detection rules"
/>
</LinkToApp>
),
}}
/>
</EuiCallOut>
<RelatedDetectionRulesCallout data-test-subj={getTestId('rulesCallout')} />
</SettingCard>
);
}

View file

@ -0,0 +1,168 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { ProtectionModes } from '../../../../../../../../common/endpoint/types';
import { set } from 'lodash';
import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license';
import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license';
import type { RansomwareProtectionCardProps } from './ransomware_protection_card';
import {
LOCKED_CARD_RAMSOMWARE_TITLE,
RansomwareProtectionCard,
} from './ransomware_protection_card';
jest.mock('../../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy Ransomware Protections Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').ransomware;
let formProps: RansomwareProtectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<RansomwareProtectionCard {...formProps} />));
});
it('should render the card with expected components', () => {
const { getByTestId } = render();
expect(getByTestId(testSubj.enableDisableSwitch));
expect(getByTestId(testSubj.protectionPreventRadio));
expect(getByTestId(testSubj.notifyUserCheckbox));
expect(getByTestId(testSubj.rulesCallout));
});
it('should show supported OS values', () => {
render();
expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent(
exactMatchText('Windows')
);
});
describe('and license is lower than Platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should show locked card if license not platinum+', () => {
render();
expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent(
exactMatchText(LOCKED_CARD_RAMSOMWARE_TITLE)
);
});
});
describe('and displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should display correctly when overall card is enabled', () => {
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Ransomware' +
'Operating system' +
'Windows ' +
'Ransomware protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.12+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display correctly when overall card is disabled', () => {
set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Ransomware' +
'Operating system' +
'Windows ' +
'Ransomware protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.12+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should display user notification disabled', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
const { getByTestId } = render();
expectIsViewOnly(getByTestId(testSubj.card));
expect(getByTestId(testSubj.card)).toHaveTextContent(
exactMatchText(
'Type' +
'Ransomware' +
'Operating system' +
'Windows ' +
'Ransomware protections enabled' +
'Protection level' +
'Prevent' +
'User notification' +
'Agent version 7.12+' +
'Notify user' +
'Notification message' +
'—' +
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
});
});

View file

@ -8,8 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch';
import { NotifyUserOption } from '../notify_user_option';
@ -19,23 +18,22 @@ import type { PolicyFormComponentCommonProps } from '../../types';
import type { Immutable } from '../../../../../../../../common/endpoint/types';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { RansomwareProtectionOSes } from '../../../../types';
import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app';
import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common';
import { useLicense } from '../../../../../../../common/hooks/use_license';
import { SettingLockedCard } from '../setting_locked_card';
import { RelatedDetectionRulesCallout } from '../related_detection_rules_callout';
const RANSOMEWARE_OS_VALUES: Immutable<RansomwareProtectionOSes[]> = [
PolicyOperatingSystem.windows,
];
const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate(
export const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.ransomware',
{
defaultMessage: 'Ransomware',
}
);
type RansomwareProtectionCardProps = PolicyFormComponentCommonProps;
export type RansomwareProtectionCardProps = PolicyFormComponentCommonProps;
export const RansomwareProtectionCard = React.memo<RansomwareProtectionCardProps>(
({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => {
@ -50,7 +48,12 @@ export const RansomwareProtectionCard = React.memo<RansomwareProtectionCardProps
);
if (!isPlatinumPlus) {
return <SettingLockedCard title={LOCKED_CARD_RAMSOMWARE_TITLE} />;
return (
<SettingLockedCard
title={LOCKED_CARD_RAMSOMWARE_TITLE}
data-test-subj={getTestId('locked')}
/>
);
}
return (
@ -91,22 +94,7 @@ export const RansomwareProtectionCard = React.memo<RansomwareProtectionCardProps
/>
<EuiSpacer size="m" />
<EuiCallOut iconType="iInCircle">
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesMessage"
defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page."
values={{
detectionRulesLink: (
<LinkToApp appId={APP_UI_ID} deepLinkId={SecurityPageName.rules}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink"
defaultMessage="related detection rules"
/>
</LinkToApp>
),
}}
/>
</EuiCallOut>
<RelatedDetectionRulesCallout data-test-subj={getTestId('rulesCallout')} />
</SettingCard>
);
}

View file

@ -0,0 +1,111 @@
/*
* 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks';
import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { set } from 'lodash';
import type { WindowsEventCollectionCardProps } from './windows_event_collection_card';
import { WindowsEventCollectionCard } from './windows_event_collection_card';
describe('Policy Windows Event Collection Card', () => {
const testSubj = getPolicySettingsFormTestSubjects('test').windowsEvents;
let formProps: WindowsEventCollectionCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': testSubj.card,
};
render = () =>
(renderResult = mockedContext.render(<WindowsEventCollectionCard {...formProps} />));
});
it('should render card with expected content', () => {
const { getByTestId } = render();
expect(
getByTestId(testSubj.optionsContainer).querySelectorAll('input[type="checkbox"]')
).toHaveLength(8);
expect(getByTestId(testSubj.credentialsCheckbox)).toBeChecked();
expect(getByTestId(testSubj.dllCheckbox)).toBeChecked();
expect(getByTestId(testSubj.dnsCheckbox)).toBeChecked();
expect(getByTestId(testSubj.fileCheckbox)).toBeChecked();
expect(getByTestId(testSubj.networkCheckbox)).toBeChecked();
expect(getByTestId(testSubj.processCheckbox)).toBeChecked();
expect(getByTestId(testSubj.registryCheckbox)).toBeChecked();
expect(getByTestId(testSubj.securityCheckbox)).toBeChecked();
expect(getByTestId(testSubj.osValueContainer)).toHaveTextContent(exactMatchText('Windows'));
});
describe('and is displayed in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render card with expected content when session data collection is disabled', () => {
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Windows 8 / 8 event collections enabled' +
'Events' +
'Credential Access' +
'DLL and Driver Load' +
'DNS' +
'File' +
'Network' +
'Process' +
'Registry' +
'Security'
)
);
});
it('should render card with expected content when some events are un-checked', () => {
set(formProps.policy, 'windows.events.file', false);
set(formProps.policy, 'windows.events.dns', false);
render();
const card = renderResult.getByTestId(testSubj.card);
expectIsViewOnly(card);
expect(card).toHaveTextContent(
exactMatchText(
'Type' +
'Event collection' +
'Operating system' +
'Windows ' +
'6 / 8 event collections enabled' +
'Events' +
'Credential Access' +
'DLL and Driver Load' +
'Network' +
'Process' +
'Registry' +
'Security'
)
);
});
});
});

View file

@ -8,8 +8,8 @@
import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import type { EventFormOption } from './event_collection_card';
import { EventCollectionCard } from './event_collection_card';
import type { EventFormOption } from '../event_collection_card';
import { EventCollectionCard } from '../event_collection_card';
import type { PolicyFormComponentCommonProps } from '../../types';
const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.WINDOWS>> = [
@ -84,7 +84,7 @@ const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.WINDOWS>> = [
},
];
type WindowsEventCollectionCardProps = PolicyFormComponentCommonProps;
export type WindowsEventCollectionCardProps = PolicyFormComponentCommonProps;
export const WindowsEventCollectionCard = memo<WindowsEventCollectionCardProps>((props) => {
return (

View file

@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type { DetectPreventProtectionLevelProps } from './detect_prevent_protection_level';
import { DetectPreventProtectionLevel } from './detect_prevent_protection_level';
import userEvent from '@testing-library/user-event';
import { cloneDeep, set } from 'lodash';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import { expectIsViewOnly, exactMatchText } from '../mocks';
import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
import { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
jest.mock('../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy form Detect Prevent Protection level component', () => {
let formProps: DetectPreventProtectionLevelProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
const clickProtection = (level: 'detect' | 'prevent') => {
userEvent.click(renderResult.getByTestId(`test-${level}Radio`).querySelector('label')!);
};
const isProtectionChecked = (level: 'detect' | 'prevent'): boolean => {
return renderResult.getByTestId(`test-${level}Radio`)!.querySelector('input')!.checked ?? false;
};
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': 'test',
protection: 'malware',
osList: ['windows', 'mac', 'linux'],
};
render = () => {
renderResult = mockedContext.render(<DetectPreventProtectionLevel {...formProps} />);
return renderResult;
};
});
it('should render expected options', () => {
const { getByTestId } = render();
expect(getByTestId('test-detectRadio'));
expect(getByTestId('test-preventRadio'));
});
it('should allow detect mode to be selected', () => {
const expectedPolicyUpdate = cloneDeep(formProps.policy);
set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.detect);
set(expectedPolicyUpdate, 'mac.malware.mode', ProtectionModes.detect);
set(expectedPolicyUpdate, 'linux.malware.mode', ProtectionModes.detect);
set(expectedPolicyUpdate, 'windows.popup.malware.enabled', false);
set(expectedPolicyUpdate, 'mac.popup.malware.enabled', false);
set(expectedPolicyUpdate, 'linux.popup.malware.enabled', false);
render();
expect(isProtectionChecked('prevent')).toBe(true);
clickProtection('detect');
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedPolicyUpdate,
});
});
it('should allow prevent mode to be selected', () => {
formProps.osList = ['windows'];
set(formProps.policy, 'windows.malware.mode', ProtectionModes.detect);
const expectedPolicyUpdate = cloneDeep(formProps.policy);
set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.prevent);
render();
expect(isProtectionChecked('detect')).toBe(true);
clickProtection('prevent');
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedPolicyUpdate,
});
});
describe('and license is lower than platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should NOT update user notification options', () => {
const expectedPolicyUpdate = cloneDeep(formProps.policy);
set(expectedPolicyUpdate, 'windows.malware.mode', ProtectionModes.detect);
set(expectedPolicyUpdate, 'mac.malware.mode', ProtectionModes.detect);
set(expectedPolicyUpdate, 'linux.malware.mode', ProtectionModes.detect);
render();
expect(isProtectionChecked('prevent')).toBe(true);
clickProtection('detect');
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedPolicyUpdate,
});
});
});
describe('and rendered in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should display prevent', () => {
render();
expectIsViewOnly(renderResult.getByTestId('test'));
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText('Protection levelPrevent')
);
});
it('should display detect', () => {
set(formProps.policy, 'windows.malware.mode', ProtectionModes.detect);
render();
expectIsViewOnly(renderResult.getByTestId('test'));
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText('Protection levelDetect')
);
});
});
});

View file

@ -8,7 +8,7 @@
import React, { memo, useCallback, useMemo } from 'react';
import { cloneDeep } from 'lodash';
import type { EuiFlexItemProps } from '@elastic/eui';
import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem, useGeneratedHtmlId } from '@elastic/eui';
import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
@ -31,12 +31,12 @@ const PREVENT_LABEL = i18n.translate('xpack.securitySolution.endpoint.policy.det
defaultMessage: 'Prevent',
});
export type DetectPreventProtectionLavelProps = PolicyFormComponentCommonProps & {
export type DetectPreventProtectionLevelProps = PolicyFormComponentCommonProps & {
protection: PolicyProtection;
osList: ImmutableArray<Partial<keyof UIPolicyConfig>>;
};
export const DetectPreventProtectionLevel = memo<DetectPreventProtectionLavelProps>(
export const DetectPreventProtectionLevel = memo<DetectPreventProtectionLevelProps>(
({ policy, protection, osList, mode, onChange, 'data-test-subj': dataTestSubj }) => {
const isEditMode = mode === 'edit';
const getTestId = useTestIdGenerator(dataTestSubj);
@ -127,11 +127,14 @@ const ProtectionRadio = React.memo(
mode,
'data-test-subj': dataTestSubj,
}: ProtectionRadioProps) => {
const radioButtonId = useGeneratedHtmlId();
const selected = policy.windows[protection].mode;
const isPlatinumPlus = useLicense().isPlatinumPlus();
const showEditableFormFields = mode === 'edit';
const radioId = useMemo(() => {
return `${osList.join('-')}-${protection}-${protectionMode}`;
}, [osList, protection, protectionMode]);
const handleRadioChange = useCallback(() => {
const newPayload = cloneDeep(policy);
@ -172,7 +175,7 @@ const ProtectionRadio = React.memo(
return (
<EuiRadio
label={label}
id={radioButtonId}
id={radioId}
checked={selected === protectionMode}
onChange={handleRadioChange}
disabled={!showEditableFormFields || selected === ProtectionModes.off}

View file

@ -0,0 +1,264 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import type {
EventCollectionCardProps,
SupplementalEventFormOption,
} from './event_collection_card';
import { EventCollectionCard } from './event_collection_card';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { expectIsViewOnly, exactMatchText } from '../mocks';
import userEvent from '@testing-library/user-event';
import { cloneDeep, set } from 'lodash';
import { within } from '@testing-library/dom';
describe('Policy Event Collection Card common component', () => {
let formProps: EventCollectionCardProps<OperatingSystem.WINDOWS>;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
const isChecked = (selector: string): boolean => {
return (renderResult.getByTestId(selector) as HTMLInputElement).checked;
};
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value;
formProps = {
policy,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': 'test',
os: OperatingSystem.WINDOWS,
selection: {
file: policy.windows.events.file,
network: policy.windows.events.network,
// For testing purposes, limit the number of events to only 2
} as typeof policy.windows.events,
options: [
{
name: 'File',
protectionField: 'file',
},
{
name: 'Network',
protectionField: 'network',
},
],
};
render = () => {
renderResult = mockedContext.render(
<EventCollectionCard<OperatingSystem.WINDOWS> {...formProps} />
);
return renderResult;
};
});
it('should render card with expected content', () => {
const { getByTestId } = render();
expect(getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('2 / 2 event collections enabled')
);
expect(getByTestId('test-osValues')).toHaveTextContent(exactMatchText('Windows'));
expect(isChecked('test-file')).toBe(true);
expect(isChecked('test-network')).toBe(true);
});
it('should allow items to be unchecked', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.events.file', false);
render();
userEvent.click(renderResult.getByTestId('test-file'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should allow items to be checked', () => {
set(formProps.policy, 'windows.events.file', false);
formProps.selection.file = false;
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.events.file', true);
const { getByTestId } = render();
expect(getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('1 / 2 event collections enabled')
);
expect(isChecked('test-file')).toBe(false);
userEvent.click(getByTestId('test-file'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
describe('and supplementalOptions are used', () => {
let supplementalEntry: SupplementalEventFormOption<OperatingSystem.WINDOWS>;
beforeEach(() => {
formProps.selection.dns = true;
supplementalEntry = {
protectionField: 'dns',
name: 'Collect DNS',
id: 'dns',
title: 'DNS collection',
uncheckedName: 'Do not collect DNS',
description: 'This collects info about DNS',
tooltipText: 'This is a tooltip',
};
formProps.supplementalOptions = [supplementalEntry];
});
it('should render supplemental option', () => {
const { getByTestId } = render();
expect(getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('2 / 2 event collections enabled')
);
expect(isChecked('test-dns')).toBe(true);
const optionContainer = within(getByTestId('test-dnsContainer'));
expect(optionContainer.getByTestId('test-dnsTitle')).toHaveTextContent(
exactMatchText(supplementalEntry.title!)
);
expect(optionContainer.getByTestId('test-dnsDescription')).toHaveTextContent(
exactMatchText(supplementalEntry.description!)
);
expect(optionContainer.getAllByLabelText(supplementalEntry.name));
expect(optionContainer.getByTestId('test-dnsTooltipIcon'));
});
it('should render with minimum set of options defined', () => {
supplementalEntry = {
name: supplementalEntry.name,
protectionField: supplementalEntry.protectionField,
};
formProps.supplementalOptions = [supplementalEntry];
render();
expect(renderResult.getByTestId('test-dnsContainer')).toHaveTextContent(
exactMatchText(supplementalEntry.name)
);
});
it('should include BETA badge', () => {
supplementalEntry.beta = true;
render();
expect(renderResult.getByTestId('test-dnsBadge')).toHaveTextContent(exactMatchText('beta'));
});
it('should indent entry', () => {
supplementalEntry.indented = true;
render();
expect(renderResult.getByTestId('test-dnsContainer').getAttribute('style')).toMatch(
/padding-left/
);
});
it('should should render it disabled', () => {
supplementalEntry.isDisabled = () => true;
render();
expect(renderResult.getByTestId('test-dns')).toBeDisabled();
});
});
describe('and when rendered in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should render with expected content', () => {
render();
expectIsViewOnly(renderResult.getByTestId('test'));
expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('2 / 2 event collections enabled')
);
expect(renderResult.getByTestId('test-options')).toHaveTextContent(
exactMatchText('FileNetwork')
);
});
it('should only display events that were checked', () => {
set(formProps.policy, 'windows.events.file', false);
formProps.selection.file = false;
render();
expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('1 / 2 event collections enabled')
);
expect(renderResult.getByTestId('test-options')).toHaveTextContent(exactMatchText('Network'));
});
it('should show empty value if no events are selected', () => {
set(formProps.policy, 'windows.events.file', false);
set(formProps.policy, 'windows.events.network', false);
formProps.selection.file = false;
formProps.selection.network = false;
render();
expect(renderResult.getByTestId('test-selectedCount')).toHaveTextContent(
exactMatchText('0 / 2 event collections enabled')
);
expect(renderResult.getByTestId('test-options')).toHaveTextContent(exactMatchText('—'));
});
describe('and supplemental options are used', () => {
let supplementalEntry: SupplementalEventFormOption<OperatingSystem.WINDOWS>;
beforeEach(() => {
formProps.selection.dns = true;
supplementalEntry = {
protectionField: 'dns',
name: 'Collect DNS',
id: 'dns',
title: 'DNS collection',
uncheckedName: 'Do not collect DNS',
description: 'This collects info about DNS',
tooltipText: 'This is a tooltip',
};
formProps.supplementalOptions = [supplementalEntry];
});
it('should render expected content when option is checked', () => {
render();
const dnsOption = renderResult.getByTestId('test-dnsContainer');
expectIsViewOnly(dnsOption);
expect(dnsOption).toHaveTextContent(
exactMatchText('DNS collectionThis collects info about DNSCollect DNSInfo')
);
});
it('should not render option if un-checked', () => {
formProps.policy.windows.events.dns = false;
formProps.selection.dns = false;
render();
expect(renderResult.queryByTestId('test-dnsContainer')).toBeNull();
});
});
});
});

View file

@ -18,16 +18,15 @@ import {
EuiIconTip,
EuiSpacer,
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { cloneDeep, get, set } from 'lodash';
import type { EuiCheckboxProps } from '@elastic/eui/src/components/form/checkbox/checkbox';
import { getEmptyValue } from '../../../../../../../common/components/empty_value';
import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator';
import type { PolicyFormComponentCommonProps } from '../../types';
import { SettingCard, SettingCardHeader } from '../setting_card';
import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types';
import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types';
import { getEmptyValue } from '../../../../../../common/components/empty_value';
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
import type { PolicyFormComponentCommonProps } from '../types';
import { SettingCard, SettingCardHeader } from './setting_card';
import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types';
import type { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
const mapOperatingSystemToPolicyOsKey = {
[OperatingSystem.WINDOWS]: PolicyOperatingSystem.windows,
@ -48,15 +47,15 @@ export interface EventFormOption<T extends OperatingSystem> {
}
export interface SupplementalEventFormOption<T extends OperatingSystem> {
name: string;
protectionField: ProtectionField<T>;
indented?: boolean;
id?: string;
title?: string;
description?: string;
name: string;
uncheckedName?: string;
protectionField: ProtectionField<T>;
tooltipText?: string;
beta?: boolean;
indented?: boolean;
isDisabled?(policyConfig: UIPolicyConfig): boolean;
}
@ -112,7 +111,7 @@ export const EventCollectionCard = memo(
})}
supportedOss={[os]}
rightCorner={
<EuiText size="s" color="subdued">
<EuiText size="s" color="subdued" data-test-subj={getTestId('selectedCount')}>
{i18n.translate(
'xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled',
{
@ -134,23 +133,25 @@ export const EventCollectionCard = memo(
</SettingCardHeader>
<EuiSpacer size="s" />
{options.map(({ name, protectionField }) => {
const keyPath = `${policyOs}.events.${protectionField}`;
<div data-test-subj={getTestId('options')}>
{options.map(({ name, protectionField }) => {
const keyPath = `${policyOs}.events.${protectionField}`;
return (
<EventCheckbox
label={name}
key={keyPath}
keyPath={keyPath}
policy={policy}
onChange={onChange}
mode={mode}
data-test-subj={getTestId(protectionField as string)}
/>
);
})}
return (
<EventCheckbox
label={name}
key={keyPath}
keyPath={keyPath}
policy={policy}
onChange={onChange}
mode={mode}
data-test-subj={getTestId(protectionField as string)}
/>
);
})}
{selectedCount === 0 && !isEditMode && <div>{getEmptyValue()}</div>}
{selectedCount === 0 && !isEditMode && <div>{getEmptyValue()}</div>}
</div>
{supplementalOptions &&
supplementalOptions.map(
@ -167,6 +168,7 @@ export const EventCollectionCard = memo(
}) => {
const keyPath = `${policyOs}.events.${protectionField}`;
const isChecked = get(policy, keyPath);
const fieldString = protectionField as string;
if (!isEditMode && !isChecked) {
return null;
@ -176,18 +178,25 @@ export const EventCollectionCard = memo(
<div
key={String(protectionField)}
style={indented ? { paddingLeft: theme.eui.euiSizeL } : {}}
data-test-subj={getTestId(`${fieldString}Container`)}
>
{title && (
<>
<EuiSpacer size="m" />
<SettingCardHeader>{title}</SettingCardHeader>
<SettingCardHeader data-test-subj={getTestId(`${fieldString}Title`)}>
{title}
</SettingCardHeader>
</>
)}
{description && (
<>
<EuiSpacer size="s" />
<EuiText size="xs" color="subdued">
<EuiText
size="xs"
color="subdued"
data-test-subj={getTestId(`${fieldString}Description`)}
>
{description}
</EuiText>
</>
@ -206,19 +215,27 @@ export const EventCollectionCard = memo(
onChange={onChange}
mode={mode}
disabled={isDisabled ? isDisabled(policy) : false}
data-test-subj={getTestId(protectionField as string)}
data-test-subj={getTestId(fieldString)}
/>
</EuiFlexItem>
{tooltipText && (
<EuiFlexItem grow={false}>
<EuiIconTip position="right" content={tooltipText} />
<EuiIconTip
position="right"
content={tooltipText}
anchorProps={{ 'data-test-subj': getTestId(`${fieldString}TooltipIcon`) }}
/>
</EuiFlexItem>
)}
{beta && (
<EuiFlexItem grow={false}>
<EuiBetaBadge label="beta" size="s" />
<EuiBetaBadge
label="beta"
size="s"
data-test-subj={getTestId(`${fieldString}Badge`)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
@ -250,7 +267,6 @@ const EventCheckbox = memo<EventCheckboxProps>(
disabled,
'data-test-subj': dataTestSubj,
}) => {
const checkboxId = useGeneratedHtmlId();
const isChecked: boolean = get(policy, keyPath);
const isEditMode = mode === 'edit';
const displayLabel = isChecked ? label : unCheckedLabel ? unCheckedLabel : label;
@ -268,7 +284,7 @@ const EventCheckbox = memo<EventCheckboxProps>(
return isEditMode ? (
<EuiCheckbox
key={keyPath}
id={checkboxId}
id={keyPath}
label={displayLabel}
data-test-subj={dataTestSubj}
checked={isChecked}
@ -276,7 +292,7 @@ const EventCheckbox = memo<EventCheckboxProps>(
disabled={disabled}
/>
) : isChecked ? (
<div>{displayLabel}</div>
<div data-test-subj={dataTestSubj}>{displayLabel}</div>
) : null;
}
);

View file

@ -0,0 +1,198 @@
/*
* 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 { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
import type { NotifyUserOptionProps } from './notify_user_option';
import {
CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL,
NOTIFY_USER_CHECKBOX_LABEL,
NOTIFY_USER_SECTION_TITLE,
NotifyUserOption,
} from './notify_user_option';
import { expectIsViewOnly, exactMatchText } from '../mocks';
import { cloneDeep, set } from 'lodash';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import userEvent from '@testing-library/user-event';
jest.mock('../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy form Detect Prevent Protection level component', () => {
let formProps: NotifyUserOptionProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
const isChecked = (selector: string): boolean => {
return (renderResult.getByTestId(selector) as HTMLInputElement).checked;
};
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': 'test',
protection: 'malware',
osList: ['windows', 'mac', 'linux'],
};
render = () => {
renderResult = mockedContext.render(<NotifyUserOption {...formProps} />);
return renderResult;
};
});
it('should render with expected content', () => {
set(formProps.policy, 'windows.popup.malware.message', 'hello world');
const { getByTestId } = render();
expect(getByTestId('test-title')).toHaveTextContent(exactMatchText(NOTIFY_USER_SECTION_TITLE));
expect(getByTestId('test-supportedVersion')).toHaveTextContent(
exactMatchText('Agent version 7.11+')
);
expect(isChecked('test-checkbox')).toBe(true);
expect(renderResult.getByLabelText(NOTIFY_USER_CHECKBOX_LABEL));
expect(getByTestId('test-customMessageTitle')).toHaveTextContent(
exactMatchText(CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL)
);
expect(getByTestId('test-customMessage')).toHaveValue('hello world');
});
it('should render with options un-checked', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
render();
expect(isChecked('test-checkbox')).toBe(false);
expect(renderResult.queryByTestId('test-customMessage')).toBeNull();
});
it('should render checked disabled if protection mode is OFF', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
render();
expect(renderResult.getByTestId('test-checkbox')).toBeDisabled();
});
it('should be able to un-check the option', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.popup.malware.enabled', false);
set(expectedUpdatedPolicy, 'mac.popup.malware.enabled', false);
set(expectedUpdatedPolicy, 'linux.popup.malware.enabled', false);
render();
userEvent.click(renderResult.getByTestId('test-checkbox'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should be able to check the option', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.popup.malware.enabled', true);
set(expectedUpdatedPolicy, 'mac.popup.malware.enabled', true);
set(expectedUpdatedPolicy, 'linux.popup.malware.enabled', true);
render();
userEvent.click(renderResult.getByTestId('test-checkbox'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should be able to change the notification message', () => {
const msg = 'a';
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
set(expectedUpdatedPolicy, 'windows.popup.malware.message', msg);
set(expectedUpdatedPolicy, 'mac.popup.malware.message', msg);
set(expectedUpdatedPolicy, 'linux.popup.malware.message', msg);
render();
userEvent.type(renderResult.getByTestId('test-customMessage'), msg);
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
describe('and license is lower than platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should NOT render the component', () => {
render();
expect(renderResult.queryByTestId('test')).toBeNull();
});
});
describe('and rendered in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
set(formProps.policy, 'windows.popup.malware.message', 'you got owned');
});
it('should render with no form elements', () => {
render();
expectIsViewOnly(renderResult.getByTestId('test'));
});
it('should render with expected output when checked', () => {
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText(
'User notification' +
'Agent version 7.11+' +
'Notify user' +
'Notification message' +
'you got owned'
)
);
});
it('should render with expected output when checked with empty message', () => {
set(formProps.policy, 'windows.popup.malware.message', '');
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText('User notificationAgent version 7.11+Notify userNotification message—')
);
});
it('should render with expected output when un-checked', () => {
set(formProps.policy, 'windows.popup.malware.enabled', false);
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText("User notificationAgent version 7.11+Don't notify user")
);
});
});
});

View file

@ -28,14 +28,19 @@ import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
export const NOTIFY_USER_SECTION_TITLE = i18n.translate(
'xpack.securitySolution.endpoint.policyDetailsConfig.userNotification',
{ defaultMessage: 'User notification' }
);
export const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetail.notifyUser',
{
defaultMessage: 'Notify user',
}
);
const DO_NOT_NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
export const DO_NOT_NOTIFY_USER_CHECKBOX_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetail.doNotNotifyUser',
{
defaultMessage: "Don't notify user",
@ -49,14 +54,14 @@ const NOTIFICATION_MESSAGE_LABEL = i18n.translate(
}
);
const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate(
export const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate(
'xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification',
{
defaultMessage: 'Customize notification message',
}
);
interface NotifyUserOptionProps extends PolicyFormComponentCommonProps {
export interface NotifyUserOptionProps extends PolicyFormComponentCommonProps {
protection: PolicyProtection;
osList: ImmutableArray<Partial<keyof UIPolicyConfig>>;
}
@ -161,11 +166,8 @@ export const NotifyUserOption = React.memo(
return (
<div data-test-subj={getTestId()}>
<EuiSpacer size="m" />
<SettingCardHeader>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetailsConfig.userNotification"
defaultMessage="User notification"
/>
<SettingCardHeader data-test-subj={getTestId('title')}>
{NOTIFY_USER_SECTION_TITLE}
</SettingCardHeader>
<SupportedVersionForProtectionNotice
@ -194,7 +196,7 @@ export const NotifyUserOption = React.memo(
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiText size="s" data-test-subj={getTestId('customMessageTitle')}>
<h4>{CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}</h4>
</EuiText>
</EuiFlexItem>

View file

@ -0,0 +1,182 @@
/*
* 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 { useLicense as _useLicense } from '../../../../../../common/hooks/use_license';
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import React from 'react';
import { createLicenseServiceMock } from '../../../../../../../common/license/mocks';
import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license';
import type { ProtectionSettingCardSwitchProps } from './protection_setting_card_switch';
import { ProtectionSettingCardSwitch } from './protection_setting_card_switch';
import { exactMatchText, expectIsViewOnly, setMalwareMode } from '../mocks';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import { cloneDeep, set } from 'lodash';
import userEvent from '@testing-library/user-event';
jest.mock('../../../../../../common/hooks/use_license');
const useLicenseMock = _useLicense as jest.Mock;
describe('Policy form ProtectionSettingCardSwitch component', () => {
let formProps: ProtectionSettingCardSwitchProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': 'test',
protection: 'malware',
protectionLabel: 'Malware',
osList: ['windows', 'mac', 'linux'],
};
render = () => {
renderResult = mockedContext.render(<ProtectionSettingCardSwitch {...formProps} />);
return renderResult;
};
});
it('should render expected output when enabled', () => {
const { getByTestId } = render();
expect(getByTestId('test')).toHaveAttribute('aria-checked', 'true');
expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Malware enabled'));
});
it('should render expected output when disabled', () => {
set(formProps.policy, 'windows.malware.mode', ProtectionModes.off);
const { getByTestId } = render();
expect(getByTestId('test')).toHaveAttribute('aria-checked', 'false');
expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Malware disabled'));
});
it('should be able to disable it', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
setMalwareMode(expectedUpdatedPolicy, true, true, false);
render();
userEvent.click(renderResult.getByTestId('test'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should be able to enable it', () => {
setMalwareMode(formProps.policy, true, true, false);
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
setMalwareMode(expectedUpdatedPolicy, false, true, false);
render();
userEvent.click(renderResult.getByTestId('test'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should invoke `additionalOnSwitchChange` callback if one was defined', () => {
formProps.additionalOnSwitchChange = jest.fn(({ policyConfigData }) => {
const updated = cloneDeep(policyConfigData);
updated.windows.popup.malware.message = 'foo';
return updated;
});
const expectedPolicyDataBeforeAdditionalCallback = cloneDeep(formProps.policy);
setMalwareMode(expectedPolicyDataBeforeAdditionalCallback, true, true, false);
const expectedUpdatedPolicy = cloneDeep(expectedPolicyDataBeforeAdditionalCallback);
expectedUpdatedPolicy.windows.popup.malware.message = 'foo';
render();
userEvent.click(renderResult.getByTestId('test'));
expect(formProps.additionalOnSwitchChange).toHaveBeenCalledWith({
value: false,
policyConfigData: expectedPolicyDataBeforeAdditionalCallback,
protectionOsList: formProps.osList,
});
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
describe('and license is lower than platinum', () => {
beforeEach(() => {
const licenseServiceMock = createLicenseServiceMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
useLicenseMock.mockReturnValue(licenseServiceMock);
});
afterEach(() => {
useLicenseMock.mockReturnValue(licenseServiceMocked);
});
it('should NOT update notification settings when disabling', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
setMalwareMode(expectedUpdatedPolicy, true, false, false);
render();
userEvent.click(renderResult.getByTestId('test'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
it('should NOT update notification settings when enabling', () => {
const expectedUpdatedPolicy = cloneDeep(formProps.policy);
setMalwareMode(formProps.policy, true, false, false);
render();
userEvent.click(renderResult.getByTestId('test'));
expect(formProps.onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: expectedUpdatedPolicy,
});
});
});
describe('and rendered in View mode', () => {
beforeEach(() => {
formProps.mode = 'view';
});
it('should not include any form elements', () => {
render();
expectIsViewOnly(renderResult.getByTestId('test'));
});
it('should show option enabled', () => {
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(exactMatchText('Malware enabled'));
});
it('should show option disabled', () => {
setMalwareMode(formProps.policy, true, true, false);
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText('Malware disabled')
);
});
});
});

View file

@ -5,19 +5,20 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSwitch } from '@elastic/eui';
import { cloneDeep } from 'lodash';
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
import type { PolicyFormComponentCommonProps } from '../types';
import { useLicense } from '../../../../../../common/hooks/use_license';
import type {
ImmutableArray,
UIPolicyConfig,
PolicyConfig,
UIPolicyConfig,
} from '../../../../../../../common/endpoint/types';
import { ProtectionModes } from '../../../../../../../common/endpoint/types';
import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
import type { LinuxPolicyProtection, MacPolicyProtection, PolicyProtection } from '../../../types';
export interface ProtectionSettingCardSwitchProps extends PolicyFormComponentCommonProps {
protection: PolicyProtection;
@ -45,19 +46,20 @@ export const ProtectionSettingCardSwitch = React.memo(
mode,
'data-test-subj': dataTestSubj,
}: ProtectionSettingCardSwitchProps) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const isPlatinumPlus = useLicense().isPlatinumPlus();
const isEditMode = mode === 'edit';
const selected = policy && policy.windows[protection].mode;
const switchLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.details.protectionsEnabled',
{
const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off;
const switchLabel = useMemo(() => {
return i18n.translate('xpack.securitySolution.endpoint.policy.details.protectionsEnabled', {
defaultMessage: '{protectionLabel} {mode, select, true {enabled} false {disabled}}',
values: {
protectionLabel,
mode: selected !== ProtectionModes.off,
mode: selected,
},
}
);
});
}, [protectionLabel, selected]);
const handleSwitchChange = useCallback(
(event) => {
@ -122,16 +124,16 @@ export const ProtectionSettingCardSwitch = React.memo(
);
if (!isEditMode) {
return <>{switchLabel}</>;
return <span data-test-subj={getTestId()}>{switchLabel}</span>;
}
return (
<EuiSwitch
label={switchLabel}
checked={selected !== ProtectionModes.off}
labelProps={{ 'data-test-subj': getTestId('label') }}
checked={selected}
onChange={handleSwitchChange}
disabled={!isEditMode}
data-test-subj={dataTestSubj}
data-test-subj={getTestId()}
/>
);
}

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import React from 'react';
import { RelatedDetectionRulesCallout } from './related_detection_rules_callout';
import { exactMatchText } from '../mocks';
import userEvent from '@testing-library/user-event';
describe('Policy form RelatedDetectionRulesCallout component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let history: AppContextTestRender['history'];
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
history = mockedContext.history;
render = () => {
renderResult = mockedContext.render(<RelatedDetectionRulesCallout data-test-subj="test" />);
return renderResult;
};
});
it('should render with expected content', () => {
render();
expect(renderResult.getByTestId('test')).toHaveTextContent(
exactMatchText(
'View related detection rules. Prebuilt rules are tagged “Elastic” on the Detection Rules page.'
)
);
});
it('should navigate to Detection Rules when link is clicked', () => {
render();
userEvent.click(renderResult.getByTestId('test-link'));
expect(history.location.pathname).toEqual('/rules');
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut } from '@elastic/eui';
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
import { APP_UI_ID, SecurityPageName } from '../../../../../../../common';
export const RelatedDetectionRulesCallout = memo<{ 'data-test-subj'?: string }>(
({ 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
return (
<EuiCallOut iconType="iInCircle" data-test-subj={getTestId()}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesMessage"
defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page."
values={{
detectionRulesLink: (
<LinkToApp
appId={APP_UI_ID}
deepLinkId={SecurityPageName.rules}
data-test-subj={getTestId('link')}
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink"
defaultMessage="related detection rules"
/>
</LinkToApp>
),
}}
/>
</EuiCallOut>
);
}
);
RelatedDetectionRulesCallout.displayName = 'RelatedDetectionRulesCallout';

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import React from 'react';
import type { SettingCardProps } from './setting_card';
import { SettingCard } from './setting_card';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { exactMatchText } from '../mocks';
describe('Policy form SettingCard component', () => {
let formProps: SettingCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
type: 'Malware',
supportedOss: [OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX],
osRestriction: undefined,
rightCorner: undefined,
dataTestSubj: 'test',
children: <div data-test-subj="test-bodyContent">{'body content here'}</div>,
};
render = () => {
renderResult = mockedContext.render(<SettingCard {...formProps} />);
return renderResult;
};
});
it('should render with expected content', () => {
const { getByTestId } = render();
expect(getByTestId('test-osValues')).toHaveTextContent(exactMatchText('Windows, Mac, Linux'));
expect(getByTestId('test-type')).toHaveTextContent(exactMatchText('Malware'));
expect(getByTestId('test-rightCornerContainer')).toBeEmptyDOMElement();
expect(getByTestId('test-bodyContent'));
});
it('should show OS restriction info', () => {
formProps.osRestriction = <>{'some content here'}</>;
render();
expect(renderResult.getByTestId('test-osRestriction')).toHaveTextContent(
exactMatchText('RestrictionsInfo')
);
});
it('should show right corner content', () => {
formProps.rightCorner = <div data-test-subj="test-rightContent">{'foo'}</div>;
render();
expect(renderResult.getByTestId('test-rightCornerContainer')).not.toBeEmptyDOMElement();
expect(renderResult.getByTestId('test-rightContent'));
});
});

View file

@ -35,7 +35,16 @@ const TITLES = {
}),
};
interface SettingCardProps {
export const SettingCardHeader = memo<{ children: React.ReactNode; 'data-test-subj'?: string }>(
({ children, 'data-test-subj': dataTestSubj }) => (
<EuiTitle size="xxs" data-test-subj={dataTestSubj}>
<h5>{children}</h5>
</EuiTitle>
)
);
SettingCardHeader.displayName = 'SettingCardHeader';
export type SettingCardProps = React.PropsWithChildren<{
/**
* A subtitle for this component.
**/
@ -48,16 +57,7 @@ interface SettingCardProps {
dataTestSubj?: string;
/** React Node to be put on the right corner of the card */
rightCorner?: ReactNode;
}
export const SettingCardHeader = memo<{ children: React.ReactNode; 'data-test-subj'?: string }>(
({ children, 'data-test-subj': dataTestSubj }) => (
<EuiTitle size="xxs" data-test-subj={dataTestSubj}>
<h5>{children}</h5>
</EuiTitle>
)
);
SettingCardHeader.displayName = 'SettingCardHeader';
}>;
export const SettingCard: FC<SettingCardProps> = memo(
({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => {
@ -73,19 +73,26 @@ export const SettingCard: FC<SettingCardProps> = memo(
style={{ padding: `${paddingSize} ${paddingSize} 0 ${paddingSize}` }}
>
<EuiFlexItem grow={1}>
<SettingCardHeader data-test-subj={getTestId('title')}>{TITLES.type}</SettingCardHeader>
<EuiText size="s">{type}</EuiText>
<SettingCardHeader>{TITLES.type}</SettingCardHeader>
<EuiText size="s" data-test-subj={getTestId('type')}>
{type}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<SettingCardHeader data-test-subj={getTestId('osTitle')}>{TITLES.os}</SettingCardHeader>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<SettingCardHeader>{TITLES.os}</SettingCardHeader>
<EuiFlexGroup
direction="row"
gutterSize="s"
alignItems="center"
data-test-subj={getTestId('osValueContainer')}
>
<EuiFlexItem grow={false}>
<EuiText size="s" data-test-subj={getTestId('osValues')}>
{supportedOss.map((os) => OS_TITLES[os]).join(', ')}{' '}
</EuiText>
</EuiFlexItem>
{osRestriction && (
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} data-test-subj={getTestId('osRestriction')}>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTextColor color="subdued">
@ -96,7 +103,12 @@ export const SettingCard: FC<SettingCardProps> = memo(
</EuiTextColor>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip type="warning" color="warning" content={osRestriction} />
<EuiIconTip
type="warning"
color="warning"
content={osRestriction}
anchorProps={{ 'data-test-subj': getTestId('osRestrictionTooltipIcon') }}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
@ -106,12 +118,16 @@ export const SettingCard: FC<SettingCardProps> = memo(
<EuiShowFor sizes={['m', 'l', 'xl']}>
<EuiFlexItem grow={3}>
<EuiFlexGroup direction="row" gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>{rightCorner}</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={getTestId('rightCornerContainer')}>
{rightCorner}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiShowFor>
<EuiShowFor sizes={rightCorner ? ['s', 'xs'] : []}>
<EuiFlexItem>{rightCorner}</EuiFlexItem>
<EuiFlexItem data-test-subj={getTestId('rightCornerContainer')}>
{rightCorner}
</EuiFlexItem>
</EuiShowFor>
</EuiFlexGroup>

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint';
import React from 'react';
import { exactMatchText } from '../mocks';
import type { SettingLockedCardProps } from './setting_locked_card';
import { SettingLockedCard } from './setting_locked_card';
describe('Policy form SettingLockedCard component', () => {
let formProps: SettingLockedCardProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
title: 'Malware locked',
'data-test-subj': 'test',
};
render = () => {
renderResult = mockedContext.render(<SettingLockedCard {...formProps} />);
return renderResult;
};
});
it('should render with expected content', () => {
const { getByTestId } = render();
expect(getByTestId('test')).toHaveTextContent(
exactMatchText(
'Malware locked' +
'Upgrade to Elastic Platinum' +
'To turn on this protection, you must upgrade your license to Platinum, start a free 30-day ' +
'trial, or spin up a ' +
'cloud deployment' +
'External link(opens in a new tab or window) ' +
'on AWS, GCP, or Azure.Platinum'
)
);
});
});

View file

@ -31,8 +31,13 @@ const LockedPolicyDiv = styled.div`
}
`;
export interface SettingLockedCardProps {
title: string;
'data-test-subj'?: string;
}
export const SettingLockedCard = memo(
({ title, 'data-test-subj': dataTestSubj }: { title: string; 'data-test-subj'?: string }) => {
({ title, 'data-test-subj': dataTestSubj }: SettingLockedCardProps) => {
const getTestId = useTestIdGenerator(dataTestSubj);
return (
@ -40,6 +45,7 @@ export const SettingLockedCard = memo(
<EuiCard
data-test-subj={getTestId()}
betaBadgeProps={{
'data-test-subj': getTestId('badge'),
label: i18n.translate('xpack.securitySolution.endpoint.policy.details.platinum', {
defaultMessage: 'Platinum',
}),
@ -47,7 +53,7 @@ export const SettingLockedCard = memo(
isDisabled={true}
icon={<EuiIcon size="xl" type="lock" />}
title={
<h3>
<h3 data-test-subj={getTestId('title')}>
<strong>{title}</strong>
</h3>
}

View file

@ -5,6 +5,10 @@
* 2.0.
*/
import { set } from 'lodash';
import type { PolicyConfig } from '../../../../../../common/endpoint/types';
import { ProtectionModes } from '../../../../../../common/endpoint/types';
interface TestSubjGenerator {
(suffix?: string): string;
withPrefix: (prefix: string) => TestSubjGenerator;
@ -37,6 +41,8 @@ export const getPolicySettingsFormTestSubjects = (
const windowsEventsTestSubj = genTestSubj.withPrefix('windowsEvents');
const macEventsTestSubj = genTestSubj.withPrefix('macEvents');
const linuxEventsTestSubj = genTestSubj.withPrefix('linuxEvents');
const antivirusTestSubj = genTestSubj.withPrefix('antivirusRegistration');
const attackSurfaceTestSubj = genTestSubj.withPrefix('attackSurface');
const testSubj = {
form: genTestSubj(),
@ -52,9 +58,14 @@ export const getPolicySettingsFormTestSubjects = (
notifyCustomMessageTooltipIcon: malwareTestSubj('notifyUser-tooltipIcon'),
notifyCustomMessageTooltipInfo: malwareTestSubj('notifyUser-tooltipInfo'),
osValuesContainer: malwareTestSubj('osValues'),
rulesCallout: malwareTestSubj('rulesCallout'),
blocklistContainer: malwareTestSubj('blocklist'),
blocklistEnableDisableSwitch: malwareTestSubj('blocklist-enableDisableSwitch'),
},
ransomware: {
card: ransomwareTestSubj(),
lockedCard: ransomwareTestSubj('locked'),
lockedCardTitle: ransomwareTestSubj('locked-title'),
enableDisableSwitch: ransomwareTestSubj('enableDisableSwitch'),
protectionPreventRadio: ransomwareTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: ransomwareTestSubj('protectionLevel-detectRadio'),
@ -64,53 +75,147 @@ export const getPolicySettingsFormTestSubjects = (
notifyCustomMessageTooltipIcon: ransomwareTestSubj('notifyUser-tooltipIcon'),
notifyCustomMessageTooltipInfo: ransomwareTestSubj('notifyUser-tooltipInfo'),
osValuesContainer: ransomwareTestSubj('osValues'),
rulesCallout: ransomwareTestSubj('rulesCallout'),
},
memory: {
card: memoryTestSubj(),
lockedCard: memoryTestSubj('locked'),
lockedCardTitle: memoryTestSubj('locked-title'),
enableDisableSwitch: memoryTestSubj('enableDisableSwitch'),
protectionPreventRadio: memoryTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: memoryTestSubj('protectionLevel-detectRadio'),
notifyUserCheckbox: memoryTestSubj('notifyUser-checkbox'),
osValuesContainer: memoryTestSubj('osValues'),
rulesCallout: memoryTestSubj('rulesCallout'),
},
behaviour: {
card: behaviourTestSubj(),
lockedCard: behaviourTestSubj('locked'),
lockedCardTitle: behaviourTestSubj('locked-title'),
enableDisableSwitch: behaviourTestSubj('enableDisableSwitch'),
protectionPreventRadio: behaviourTestSubj('protectionLevel-preventRadio'),
protectionDetectRadio: behaviourTestSubj('protectionLevel-detectRadio'),
notifyUserCheckbox: behaviourTestSubj('notifyUser-checkbox'),
osValuesContainer: behaviourTestSubj('osValues'),
rulesCallout: behaviourTestSubj('rulesCallout'),
},
attachSurface: {
card: genTestSubj('attackSurface'),
enableDisableSwitch: genTestSubj('attachSurface-enableDisableSwitch'),
osValuesContainer: genTestSubj('attackSurface-osValues'),
attackSurface: {
card: attackSurfaceTestSubj(),
lockedCard: attackSurfaceTestSubj('locked'),
lockedCardTitle: attackSurfaceTestSubj('locked-title'),
enableDisableSwitch: attackSurfaceTestSubj('enableDisableSwitch'),
osValues: attackSurfaceTestSubj('osValues'),
viewModeValue: attackSurfaceTestSubj('valueLabel'),
},
windowsEvents: {
card: windowsEventsTestSubj(),
osValueContainer: windowsEventsTestSubj('osValueContainer'),
optionsContainer: windowsEventsTestSubj('options'),
credentialsCheckbox: windowsEventsTestSubj('credential_access'),
dllCheckbox: windowsEventsTestSubj('dll_and_driver_load'),
dnsCheckbox: windowsEventsTestSubj('dns'),
processCheckbox: windowsEventsTestSubj('process'),
fileCheckbox: windowsEventsTestSubj('file'),
networkCheckbox: windowsEventsTestSubj('network'),
processCheckbox: windowsEventsTestSubj('process'),
registryCheckbox: windowsEventsTestSubj('registry'),
securityCheckbox: windowsEventsTestSubj('security'),
},
macEvents: {
card: macEventsTestSubj(),
osValueContainer: macEventsTestSubj('osValueContainer'),
optionsContainer: macEventsTestSubj('options'),
fileCheckbox: macEventsTestSubj('file'),
networkCheckbox: macEventsTestSubj('network'),
processCheckbox: macEventsTestSubj('process'),
},
linuxEvents: {
card: linuxEventsTestSubj(),
osValueContainer: linuxEventsTestSubj('osValueContainer'),
optionsContainer: linuxEventsTestSubj('options'),
fileCheckbox: linuxEventsTestSubj('file'),
networkCheckbox: linuxEventsTestSubj('network'),
processCheckbox: linuxEventsTestSubj('process'),
sessionDataCheckbox: linuxEventsTestSubj('session_data'),
captureTerminalCheckbox: linuxEventsTestSubj('tty_io'),
},
antivirusRegistration: {
card: genTestSubj('antivirusRegistration'),
card: antivirusTestSubj(),
enableDisableSwitch: antivirusTestSubj('switch'),
osValueContainer: antivirusTestSubj('osValueContainer'),
viewOnlyValue: antivirusTestSubj('value'),
},
advancedSection: {
container: advancedSectionTestSubj(''),
showHideButton: advancedSectionTestSubj('showButton'),
settingsContainer: advancedSectionTestSubj('settings'),
warningCallout: advancedSectionTestSubj('warning'),
settingRowTestSubjects: (settingKeyPath: string) => {
const testSubjForSetting = advancedSectionTestSubj.withPrefix(settingKeyPath);
return {
container: testSubjForSetting('container'),
label: testSubjForSetting('label'),
tooltipIcon: testSubjForSetting('tooltipIcon'),
versionInfo: testSubjForSetting('versionInfo'),
textField: settingKeyPath,
viewValue: testSubjForSetting('viewValue'),
};
},
},
};
return testSubj;
};
export const expectIsViewOnly = (ele: HTMLElement): void => {
expect(
ele.querySelectorAll(
'button:not(.euiLink, [data-test-subj*="advancedSection-showButton"]),input,select,textarea'
)
).toHaveLength(0);
};
/**
* Create a regular expression with the provided text that ensure it matches the entire string.
* @param text
*/
export const exactMatchText = (text: string): RegExp => {
// RegExp below taken from: https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
return new RegExp(`^${text.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')}$`);
};
/**
* Sets malware off or on (to prevent protection level) in the given policy settings
*
* NOTE: this utiliy MUTATES `policy` provided on input
*
* @param policy
* @param turnOff
* @param includePopup
*/
export const setMalwareMode = (
policy: PolicyConfig,
turnOff: boolean = false,
includePopup: boolean = true,
includeBlocklist: boolean = true
) => {
const mode = turnOff ? ProtectionModes.off : ProtectionModes.prevent;
const enableValue = mode !== ProtectionModes.off;
set(policy, 'windows.malware.mode', mode);
set(policy, 'mac.malware.mode', mode);
set(policy, 'linux.malware.mode', mode);
if (includeBlocklist) {
set(policy, 'windows.malware.blocklist', enableValue);
set(policy, 'mac.malware.blocklist', enableValue);
set(policy, 'linux.malware.blocklist', enableValue);
}
if (includePopup) {
set(policy, 'windows.popup.malware.enabled', enableValue);
set(policy, 'mac.popup.malware.enabled', enableValue);
set(policy, 'linux.popup.malware.enabled', enableValue);
}
};

View file

@ -0,0 +1,62 @@
/*
* 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 from 'react';
import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from './mocks';
import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import type { PolicySettingsFormProps } from './policy_settings_form';
import { PolicySettingsForm } from './policy_settings_form';
import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
jest.mock('../../../../../common/hooks/use_license');
describe('Endpoint Policy Settings Form', () => {
const testSubj = getPolicySettingsFormTestSubjects('test');
let formProps: PolicySettingsFormProps;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
formProps = {
policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0]
.config.policy.value,
onChange: jest.fn(),
mode: 'edit',
'data-test-subj': 'test',
};
render = () => (renderResult = mockedContext.render(<PolicySettingsForm {...formProps} />));
});
it.each([
['malware', testSubj.malware.card],
['ransomware', testSubj.ransomware.card],
['memory', testSubj.memory.card],
['behaviour', testSubj.behaviour.card],
['attack surface', testSubj.attackSurface.card],
['windows events', testSubj.windowsEvents.card],
['mac events', testSubj.macEvents.card],
['linux events', testSubj.linuxEvents.card],
['antivirus registration', testSubj.antivirusRegistration.card],
['advanced settings', testSubj.advancedSection.container],
])('should include %s card', (_, testSubjSelector) => {
render();
expect(renderResult.getByTestId(testSubjSelector));
});
it('should render in View mode', () => {
formProps.mode = 'view';
render();
expectIsViewOnly(renderResult.getByTestId('test'));
});
});

View file

@ -0,0 +1,217 @@
/*
* 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 from 'react';
import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import type { PolicyData } from '../../../../../../common/endpoint/types';
import type { AppContextTestRender } from '../../../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import { PolicySettingsLayout } from './policy_settings_layout';
import { useUserPrivileges as _useUserPrivileges } from '../../../../../common/components/user_privileges';
import { getUserPrivilegesMockDefaultValue } from '../../../../../common/components/user_privileges/__mocks__';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
import { allFleetHttpMocks } from '../../../../mocks';
import userEvent from '@testing-library/user-event';
import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../policy_settings_form/mocks';
import { cloneDeep, set } from 'lodash';
import { ProtectionModes } from '../../../../../../common/endpoint/types';
import { waitFor, cleanup } from '@testing-library/react';
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
import { getPolicyDataForUpdate } from '../../../../../../common/endpoint/service/policy';
import { getDeferred } from '../../../../mocks/utils';
jest.mock('../../../../../common/hooks/use_license');
jest.mock('../../../../../common/components/user_privileges');
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
describe('When rendering PolicySettingsLayout', () => {
jest.setTimeout(15000);
const testSubj = getPolicySettingsFormTestSubjects();
let policyData: PolicyData;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let apiMocks: ReturnType<typeof allFleetHttpMocks>;
let toasts: AppContextTestRender['coreStart']['notifications']['toasts'];
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
toasts = mockedContext.coreStart.notifications.toasts;
apiMocks = allFleetHttpMocks(mockedContext.coreStart.http);
policyData = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy();
render = () => {
renderResult = mockedContext.render(<PolicySettingsLayout policy={policyData} />);
return renderResult;
};
});
afterEach(() => {
cleanup();
});
describe('and user has Edit permissions', () => {
const clickSave = async (andConfirm: boolean = true, ensureApiIsCalled: boolean = true) => {
const { getByTestId } = renderResult;
userEvent.click(getByTestId('policyDetailsSaveButton'));
await waitFor(() => {
expect(getByTestId('confirmModalConfirmButton'));
});
if (andConfirm) {
userEvent.click(getByTestId('confirmModalConfirmButton'));
if (ensureApiIsCalled) {
await waitFor(() => {
expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalled();
});
}
}
};
/**
* Makes updates to the policy form on the UI and return back a new (cloned) `PolicyData`
* with the updates reflected in it
*/
const makeUpdates = () => {
const { getByTestId } = renderResult;
const expectedUpdates = cloneDeep(policyData);
const policySettings = expectedUpdates.inputs[0].config.policy.value;
// Turn off malware
userEvent.click(getByTestId(testSubj.malware.enableDisableSwitch));
set(policySettings, 'windows.malware.mode', ProtectionModes.off);
set(policySettings, 'mac.malware.mode', ProtectionModes.off);
set(policySettings, 'linux.malware.mode', ProtectionModes.off);
set(policySettings, 'windows.malware.blocklist', false);
set(policySettings, 'mac.malware.blocklist', false);
set(policySettings, 'linux.malware.blocklist', false);
set(policySettings, 'windows.popup.malware.enabled', false);
set(policySettings, 'mac.popup.malware.enabled', false);
set(policySettings, 'linux.popup.malware.enabled', false);
// Turn off Behaviour Protection
userEvent.click(getByTestId(testSubj.behaviour.enableDisableSwitch));
set(policySettings, 'windows.behavior_protection.mode', ProtectionModes.off);
set(policySettings, 'mac.behavior_protection.mode', ProtectionModes.off);
set(policySettings, 'linux.behavior_protection.mode', ProtectionModes.off);
set(policySettings, 'windows.popup.behavior_protection.enabled', false);
set(policySettings, 'mac.popup.behavior_protection.enabled', false);
set(policySettings, 'linux.popup.behavior_protection.enabled', false);
// Set Ransomware User Notification message
userEvent.type(getByTestId(testSubj.ransomware.notifyCustomMessage), 'foo message');
set(policySettings, 'windows.popup.ransomware.message', 'foo message');
userEvent.click(getByTestId(testSubj.advancedSection.showHideButton));
userEvent.type(getByTestId('linux.advanced.agent.connection_delay'), '1000');
set(policySettings, 'linux.advanced.agent.connection_delay', '1000');
return expectedUpdates;
};
it('should render layout with expected content', () => {
const { getByTestId } = render();
expect(getByTestId('endpointPolicyForm'));
expect(getByTestId('policyDetailsCancelButton')).not.toBeDisabled();
expect(getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
});
it('should allow updates to be made', async () => {
render();
const expectedUpdatedPolicy = makeUpdates();
await clickSave();
expect(apiMocks.responseProvider.updateEndpointPolicy).toHaveBeenCalledWith({
path: packagePolicyRouteService.getUpdatePath(policyData.id),
body: JSON.stringify(getPolicyDataForUpdate(expectedUpdatedPolicy)),
});
});
it('should show buttons disabled while update is in flight', async () => {
const deferred = getDeferred();
apiMocks.responseProvider.updateEndpointPolicy.mockDelay.mockReturnValue(deferred.promise);
const { getByTestId } = render();
await clickSave(true, false);
await waitFor(() => {
expect(getByTestId('policyDetailsCancelButton')).toBeDisabled();
});
expect(getByTestId('policyDetailsSaveButton')).toBeDisabled();
expect(
getByTestId('policyDetailsSaveButton').querySelector('.euiLoadingSpinner')
).not.toBeNull();
deferred.resolve();
});
it('should show success toast on update success', async () => {
render();
await clickSave();
await waitFor(() => {
expect(renderResult.getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
});
expect(toasts.addSuccess).toHaveBeenCalledWith({
'data-test-subj': 'policyDetailsSuccessMessage',
text: 'Integration Endpoint Policy {ku5j) has been updated.',
title: 'Success!',
});
});
it('should show Danger toast on update failure', async () => {
apiMocks.responseProvider.updateEndpointPolicy.mockImplementation(() => {
throw new Error('oh oh!');
});
render();
await clickSave();
await waitFor(() => {
expect(renderResult.getByTestId('policyDetailsSaveButton')).not.toBeDisabled();
});
expect(toasts.addDanger).toHaveBeenCalledWith({
'data-test-subj': 'policyDetailsFailureMessage',
text: 'oh oh!',
title: 'Failed!',
});
});
});
describe('and user has View Only permissions', () => {
beforeEach(() => {
const privileges = getUserPrivilegesMockDefaultValue();
privileges.endpointPrivileges = getEndpointPrivilegesInitialStateMock({
canWritePolicyManagement: false,
});
useUserPrivilegesMock.mockReturnValue(privileges);
});
afterEach(() => {
useUserPrivilegesMock.mockReset();
useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue);
});
it('should render form in view mode', () => {
render();
expectIsViewOnly(renderResult.getByTestId(testSubj.form));
});
it('should not include the Save button', () => {
render();
expect(renderResult.queryByTestId('policyDetailsSaveButton')).toBeNull();
});
});
});

View file

@ -165,6 +165,7 @@ export const PolicySettingsLayout = memo<PolicySettingsLayoutProps>(({ policy: _
color="text"
onClick={handleCancelOnClick}
data-test-subj="policyDetailsCancelButton"
disabled={isUpdating}
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.cancel"