[Security Solution] Add blocklist switch to malware card (#127031)

This commit is contained in:
Kevin Logan 2022-03-15 10:21:31 -04:00 committed by GitHub
parent 67566ab8e2
commit 7cbfed0f02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 374 additions and 20 deletions

View file

@ -36,6 +36,7 @@ import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migra
import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0';
import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0';
import { migrateInstallationToV800, migrateOutputToV800 } from './migrations/to_v8_0_0';
import { migratePackagePolicyToV820 } from './migrations/to_v8_2_0';
/*
* Saved object types and mappings
@ -206,6 +207,7 @@ const getSavedObjectTypes = (
'7.14.0': migratePackagePolicyToV7140,
'7.15.0': migratePackagePolicyToV7150,
'7.16.0': migratePackagePolicyToV7160,
'8.2.0': migratePackagePolicyToV820,
},
},
[PACKAGES_SAVED_OBJECT_TYPE]: {

View file

@ -11,3 +11,4 @@ export { migrateEndpointPackagePolicyToV7130 } from './to_v7_13_0';
export { migrateEndpointPackagePolicyToV7140 } from './to_v7_14_0';
export { migratePackagePolicyToV7150 } from './to_v7_15_0';
export { migratePackagePolicyToV7160 } from './to_v7_16_0';
export { migratePackagePolicyToV820 } from './to_v8_2_0';

View file

@ -0,0 +1,175 @@
/*
* 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 { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server';
import type { PackagePolicy } from '../../../../common';
import { migratePackagePolicyToV820 as migration } from './to_v8_2_0';
describe('8.2.0 Endpoint Package Policy migration', () => {
const policyDoc = ({ windowsMalware = {}, macMalware = {}, linuxMalware = {} }) => {
return {
id: 'mock-saved-object-id',
attributes: {
name: 'Some Policy Name',
package: {
name: 'endpoint',
title: '',
version: '',
},
id: 'endpoint',
policy_id: '',
enabled: true,
namespace: '',
output_id: '',
revision: 0,
updated_at: '',
updated_by: '',
created_at: '',
created_by: '',
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
policy: {
value: {
windows: {
...windowsMalware,
},
mac: {
...macMalware,
},
linux: {
...linuxMalware,
},
},
},
},
},
],
},
type: ' nested',
};
};
it('adds blocklist defaulted to false if malware is off', () => {
const initialDoc = policyDoc({
windowsMalware: { malware: { mode: 'off' } },
macMalware: { malware: { mode: 'off' } },
linuxMalware: { malware: { mode: 'off' } },
});
const migratedDoc = policyDoc({
windowsMalware: { malware: { mode: 'off', blocklist: false } },
macMalware: { malware: { mode: 'off', blocklist: false } },
linuxMalware: { malware: { mode: 'off', blocklist: false } },
});
expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc);
});
it('adds blocklist defaulted to true if malware is prevent', () => {
const initialDoc = policyDoc({
windowsMalware: { malware: { mode: 'prevent' } },
macMalware: { malware: { mode: 'prevent' } },
linuxMalware: { malware: { mode: 'prevent' } },
});
const migratedDoc = policyDoc({
windowsMalware: { malware: { mode: 'prevent', blocklist: true } },
macMalware: { malware: { mode: 'prevent', blocklist: true } },
linuxMalware: { malware: { mode: 'prevent', blocklist: true } },
});
expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc);
});
it('adds blocklist defaulted to true if malware is detect', () => {
const initialDoc = policyDoc({
windowsMalware: { malware: { mode: 'detect' } },
macMalware: { malware: { mode: 'detect' } },
linuxMalware: { malware: { mode: 'detect' } },
});
const migratedDoc = policyDoc({
windowsMalware: { malware: { mode: 'detect', blocklist: true } },
macMalware: { malware: { mode: 'detect', blocklist: true } },
linuxMalware: { malware: { mode: 'detect', blocklist: true } },
});
expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc);
});
it('does not modify non-endpoint package policies', () => {
const doc: SavedObjectUnsanitizedDoc<PackagePolicy> = {
id: 'mock-saved-object-id',
attributes: {
name: 'Some Policy Name',
package: {
name: 'notEndpoint',
title: '',
version: '',
},
id: 'notEndpoint',
policy_id: '',
enabled: true,
namespace: '',
output_id: '',
revision: 0,
updated_at: '',
updated_by: '',
created_at: '',
created_by: '',
inputs: [
{
type: 'notEndpoint',
enabled: true,
streams: [],
config: {},
},
],
},
type: ' nested',
};
expect(
migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc<PackagePolicy>
).toEqual({
attributes: {
name: 'Some Policy Name',
package: {
name: 'notEndpoint',
title: '',
version: '',
},
id: 'notEndpoint',
policy_id: '',
enabled: true,
namespace: '',
output_id: '',
revision: 0,
updated_at: '',
updated_by: '',
created_at: '',
created_by: '',
inputs: [
{
type: 'notEndpoint',
enabled: true,
streams: [],
config: {},
},
],
},
type: ' nested',
id: 'mock-saved-object-id',
});
});
});

View file

@ -0,0 +1,34 @@
/*
* 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 { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { cloneDeep } from 'lodash';
import type { PackagePolicy } from '../../../../common';
export const migratePackagePolicyToV820: SavedObjectMigrationFn<PackagePolicy, PackagePolicy> = (
packagePolicyDoc
) => {
if (packagePolicyDoc.attributes.package?.name !== 'endpoint') {
return packagePolicyDoc;
}
const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc<PackagePolicy> =
cloneDeep(packagePolicyDoc);
const input = updatedPackagePolicyDoc.attributes.inputs[0];
if (input && input.config) {
const policy = input.config.policy.value;
policy.windows.malware.blocklist = policy.windows.malware.mode !== 'off';
policy.mac.malware.blocklist = policy.mac.malware.mode !== 'off';
policy.linux.malware.blocklist = policy.linux.malware.mode !== 'off';
}
return updatedPackagePolicyDoc;
};

View file

@ -0,0 +1,26 @@
/*
* 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 { SavedObjectMigrationFn } from 'kibana/server';
import type { PackagePolicy } from '../../../common';
import { migratePackagePolicyToV820 as SecSolMigratePackagePolicyToV820 } from './security_solution';
export const migratePackagePolicyToV820: SavedObjectMigrationFn<PackagePolicy, PackagePolicy> = (
packagePolicyDoc,
migrationContext
) => {
let updatedPackagePolicyDoc = packagePolicyDoc;
// Endpoint specific migrations
if (packagePolicyDoc.attributes.package?.name === 'endpoint') {
updatedPackagePolicyDoc = SecSolMigratePackagePolicyToV820(packagePolicyDoc, migrationContext);
}
return updatedPackagePolicyDoc;
};

View file

@ -24,6 +24,7 @@ export const policyFactory = (): PolicyConfig => {
},
malware: {
mode: ProtectionModes.prevent,
blocklist: true,
},
ransomware: {
mode: ProtectionModes.prevent,
@ -70,6 +71,7 @@ export const policyFactory = (): PolicyConfig => {
},
malware: {
mode: ProtectionModes.prevent,
blocklist: true,
},
behavior_protection: {
mode: ProtectionModes.prevent,
@ -105,6 +107,7 @@ export const policyFactory = (): PolicyConfig => {
},
malware: {
mode: ProtectionModes.prevent,
blocklist: true,
},
behavior_protection: {
mode: ProtectionModes.prevent,

View file

@ -920,7 +920,7 @@ export interface PolicyConfig {
registry: boolean;
security: boolean;
};
malware: ProtectionFields;
malware: ProtectionFields & BlocklistFields;
memory_protection: ProtectionFields & SupportedFields;
behavior_protection: ProtectionFields & SupportedFields;
ransomware: ProtectionFields & SupportedFields;
@ -956,7 +956,7 @@ export interface PolicyConfig {
process: boolean;
network: boolean;
};
malware: ProtectionFields;
malware: ProtectionFields & BlocklistFields;
behavior_protection: ProtectionFields & SupportedFields;
memory_protection: ProtectionFields & SupportedFields;
popup: {
@ -984,7 +984,7 @@ export interface PolicyConfig {
process: boolean;
network: boolean;
};
malware: ProtectionFields;
malware: ProtectionFields & BlocklistFields;
behavior_protection: ProtectionFields & SupportedFields;
memory_protection: ProtectionFields & SupportedFields;
popup: {
@ -1051,6 +1051,10 @@ export interface SupportedFields {
supported: boolean;
}
export interface BlocklistFields {
blocklist: boolean;
}
/** Policy protection mode options */
export enum ProtectionModes {
detect = 'detect',
@ -1258,6 +1262,12 @@ interface BaseListResponse<D = unknown> {
total: number;
}
export interface AdditionalOnSwitchChangeParams {
value: boolean;
policyConfigData: UIPolicyConfig;
protectionOsList: ImmutableArray<Partial<keyof UIPolicyConfig>>;
}
/**
* Returned by the server via GET /api/endpoint/metadata
*/

View file

@ -283,7 +283,7 @@ describe('policy details: ', () => {
registry: true,
security: true,
},
malware: { mode: 'prevent' },
malware: { mode: 'prevent', blocklist: true },
memory_protection: { mode: 'off', supported: false },
behavior_protection: { mode: 'off', supported: false },
ransomware: { mode: 'off', supported: false },
@ -312,7 +312,7 @@ describe('policy details: ', () => {
},
mac: {
events: { process: true, file: true, network: true },
malware: { mode: 'prevent' },
malware: { mode: 'prevent', blocklist: true },
behavior_protection: { mode: 'off', supported: false },
memory_protection: { mode: 'off', supported: false },
popup: {
@ -334,7 +334,7 @@ describe('policy details: ', () => {
linux: {
events: { process: true, file: true, network: true },
logging: { file: 'info' },
malware: { mode: 'prevent' },
malware: { mode: 'prevent', blocklist: true },
behavior_protection: { mode: 'off', supported: false },
memory_protection: { mode: 'off', supported: false },
popup: {

View file

@ -18,6 +18,7 @@ import {
ImmutableArray,
ProtectionModes,
UIPolicyConfig,
AdditionalOnSwitchChangeParams,
} from '../../../../../../../common/endpoint/types';
import { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types';
@ -26,10 +27,16 @@ export const ProtectionSwitch = React.memo(
protection,
protectionLabel,
osList,
additionalOnSwitchChange,
}: {
protection: PolicyProtection;
protectionLabel?: string;
osList: ImmutableArray<Partial<keyof UIPolicyConfig>>;
additionalOnSwitchChange?: ({
value,
policyConfigData,
protectionOsList,
}: AdditionalOnSwitchChangeParams) => UIPolicyConfig;
}) => {
const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
const isPlatinumPlus = useLicense().isPlatinumPlus();
@ -83,13 +90,26 @@ export const ProtectionSwitch = React.memo(
}
}
}
dispatch({
type: 'userChangedPolicyConfig',
payload: { policyConfig: newPayload },
});
if (additionalOnSwitchChange) {
dispatch({
type: 'userChangedPolicyConfig',
payload: {
policyConfig: additionalOnSwitchChange({
value: event.target.checked,
policyConfigData: newPayload,
protectionOsList: osList,
}),
},
});
} else {
dispatch({
type: 'userChangedPolicyConfig',
payload: { policyConfig: newPayload },
});
}
}
},
[dispatch, policyDetailsConfig, isPlatinumPlus, protection, osList]
[dispatch, policyDetailsConfig, isPlatinumPlus, protection, osList, additionalOnSwitchChange]
);
return (

View file

@ -5,14 +5,28 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import {
EuiCallOut,
EuiSpacer,
EuiSwitch,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
} from '@elastic/eui';
import { OperatingSystem } from '@kbn/securitysolution-utils';
import { useDispatch } from 'react-redux';
import { cloneDeep } from 'lodash';
import { APP_UI_ID } from '../../../../../../../common/constants';
import { SecurityPageName } from '../../../../../../app/types';
import { Immutable, PolicyOperatingSystem } from '../../../../../../../common/endpoint/types';
import {
Immutable,
PolicyOperatingSystem,
AdditionalOnSwitchChangeParams,
UIPolicyConfig,
} from '../../../../../../../common/endpoint/types';
import { MalwareProtectionOSes } from '../../../types';
import { ConfigForm } from '../../components/config_form';
import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
@ -20,16 +34,18 @@ import { useLicense } from '../../../../../../common/hooks/use_license';
import { RadioButtons } from '../components/radio_buttons';
import { UserNotification } from '../components/user_notification';
import { ProtectionSwitch } from '../components/protection_switch';
import { policyConfig } from '../../../store/policy_details/selectors';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { AppAction } from '../../../../../../common/store/actions';
/** The Malware Protections form for policy details
* which will configure for all relevant OSes.
*/
export const MalwareProtections = React.memo(() => {
const OSes: Immutable<MalwareProtectionOSes[]> = [
PolicyOperatingSystem.windows,
PolicyOperatingSystem.mac,
PolicyOperatingSystem.linux,
];
const OSes: Immutable<MalwareProtectionOSes[]> = useMemo(
() => [PolicyOperatingSystem.windows, PolicyOperatingSystem.mac, PolicyOperatingSystem.linux],
[]
);
const protection = 'malware';
const protectionLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.malware',
@ -37,7 +53,45 @@ export const MalwareProtections = React.memo(() => {
defaultMessage: 'Malware protections',
}
);
const blocklistLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.blocklist',
{
defaultMessage: 'Blocklist enabled',
}
);
const isPlatinumPlus = useLicense().isPlatinumPlus();
const dispatch = useDispatch<(action: AppAction) => void>();
const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
const blocklistUpdate = ({
value,
policyConfigData,
protectionOsList,
}: AdditionalOnSwitchChangeParams): UIPolicyConfig => {
const newPayload: UIPolicyConfig = cloneDeep(policyConfigData);
for (const os of protectionOsList) {
newPayload[os][protection].blocklist = value;
}
return newPayload;
};
const handleBlocklistSwitchChange = useCallback(
(event) => {
if (policyDetailsConfig) {
const newPayload = blocklistUpdate({
value: event.target.checked,
policyConfigData: cloneDeep(policyDetailsConfig),
protectionOsList: OSes,
});
dispatch({
type: 'userChangedPolicyConfig',
payload: { policyConfig: newPayload },
});
}
},
[dispatch, OSes, policyDetailsConfig]
);
return (
<ConfigForm
@ -47,10 +101,39 @@ export const MalwareProtections = React.memo(() => {
supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]}
dataTestSubj="malwareProtectionsForm"
rightCorner={
<ProtectionSwitch protection={protection} protectionLabel={protectionLabel} osList={OSes} />
<ProtectionSwitch
protection={protection}
protectionLabel={protectionLabel}
osList={OSes}
additionalOnSwitchChange={blocklistUpdate}
/>
}
>
<RadioButtons protection={protection} osList={OSes} />
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiSwitch
label={blocklistLabel}
checked={policyDetailsConfig.windows[protection].blocklist}
onChange={handleBlocklistSwitchChange}
disabled={policyDetailsConfig.windows[protection].mode === 'off'}
/>
</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>
{isPlatinumPlus && <UserNotification protection={protection} osList={OSes} />}
<EuiSpacer size="m" />
<EuiCallOut iconType="iInCircle">