mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add blocklist switch to malware card (#127031)
This commit is contained in:
parent
67566ab8e2
commit
7cbfed0f02
10 changed files with 374 additions and 20 deletions
|
@ -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]: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue