diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts index 9ac53f9be609f..30f45e54c2005 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts @@ -42,6 +42,8 @@ export const generatePolicy = (): PolicyConfig => { mac: { events: { process: true, + file: true, + network: true, }, malware: { mode: ProtectionModes.detect, @@ -67,6 +69,8 @@ export const generatePolicy = (): PolicyConfig => { linux: { events: { process: true, + file: true, + network: true, }, logging: { stdout: 'debug', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts index 1145d1d19242a..bf96942e83a91 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts @@ -43,3 +43,33 @@ export function clone(policyDetailsConfig: UIPolicyConfig): UIPolicyConfig { */ return clonedConfig as UIPolicyConfig; } + +/** + * Returns value from `configuration` + */ +export const getIn = (a: UIPolicyConfig) => (key: Key) => < + subKey extends keyof UIPolicyConfig[Key] +>( + subKey: subKey +) => ( + leafKey: LeafKey +): UIPolicyConfig[Key][subKey][LeafKey] => { + return a[key][subKey][leafKey]; +}; + +/** + * Returns cloned `configuration` with `value` set by the `keyPath`. + */ +export const setIn = (a: UIPolicyConfig) => (key: Key) => < + subKey extends keyof UIPolicyConfig[Key] +>( + subKey: subKey +) => (leafKey: LeafKey) => < + V extends UIPolicyConfig[Key][subKey][LeafKey] +>( + v: V +): UIPolicyConfig => { + const c = clone(a); + c[key][subKey][leafKey] = v; + return c; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts index cf14092953227..e09a62b235e35 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts @@ -7,7 +7,7 @@ import { PolicyDetailsState } from '../../types'; import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; -import { policyConfig, windowsEventing } from './selectors'; +import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; import { generatePolicy } from '../../models/policy'; @@ -55,7 +55,7 @@ describe('policy details: ', () => { }); }); - describe('when the user has enabled windows process eventing', () => { + describe('when the user has enabled windows process events', () => { beforeEach(() => { const config = policyConfig(getState()); if (!config) { @@ -71,8 +71,31 @@ describe('policy details: ', () => { }); }); - it('windows process eventing is enabled', async () => { - expect(windowsEventing(getState())!.process).toEqual(true); + it('windows process events is enabled', () => { + const config = policyConfig(getState()); + expect(config!.windows.events.process).toEqual(true); + }); + }); + + describe('when the user has enabled mac file events', () => { + beforeEach(() => { + const config = policyConfig(getState()); + if (!config) { + throw new Error(); + } + + const newPayload1 = clone(config); + newPayload1.mac.events.file = true; + + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload1 }, + }); + }); + + it('mac file events is enabled', () => { + const config = policyConfig(getState()); + expect(config!.mac.events.file).toEqual(true); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts index fb3e26157ef32..fb0f371cdae0d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts @@ -5,7 +5,7 @@ */ import { Reducer } from 'redux'; -import { PolicyData, PolicyDetailsState, UIPolicyConfig } from '../../types'; +import { PolicyDetailsState, UIPolicyConfig } from '../../types'; import { AppAction } from '../action'; import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; @@ -89,10 +89,12 @@ export const policyDetailsReducer: Reducer = ( } if (action.type === 'userChangedPolicyConfig') { - const newState = { ...state, policyItem: { ...(state.policyItem as PolicyData) } }; - const newPolicy = (newState.policyItem.inputs[0].config.policy.value = { - ...fullPolicy(state), - }); + if (!state.policyItem) { + return state; + } + const newState = { ...state, policyItem: { ...state.policyItem } }; + const newPolicy: any = { ...fullPolicy(state) }; + newState.policyItem.inputs[0].config.policy.value = newPolicy; Object.entries(action.payload.policyConfig).forEach(([section, newSettings]) => { newPolicy[section as keyof UIPolicyConfig] = { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts index 0d505931c9ec5..4b4dc9d9bee43 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts @@ -79,14 +79,8 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); -/** Returns an object of all the windows eventing configuration */ -export const windowsEventing = (state: PolicyDetailsState) => { - const config = policyConfig(state); - return config && config.windows.events; -}; - /** Returns the total number of possible windows eventing configurations */ -export const totalWindowsEventing = (state: PolicyDetailsState): number => { +export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); if (config) { return Object.keys(config.windows.events).length; @@ -95,7 +89,7 @@ export const totalWindowsEventing = (state: PolicyDetailsState): number => { }; /** Returns the number of selected windows eventing configurations */ -export const selectedWindowsEventing = (state: PolicyDetailsState): number => { +export const selectedWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); if (config) { return Object.values(config.windows.events).reduce((count, event) => { @@ -105,6 +99,26 @@ export const selectedWindowsEventing = (state: PolicyDetailsState): number => { return 0; }; +/** Returns the total number of possible mac eventing configurations */ +export const totalMacEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.keys(config.mac.events).length; + } + return 0; +}; + +/** Returns the number of selected mac eventing configurations */ +export const selectedMacEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.values(config.mac.events).reduce((count, event) => { + return event === true ? count + 1 : count; + }, 0); + } + return 0; +}; + /** is there an api call in flight */ export const isLoading = (state: PolicyDetailsState) => state.isLoading; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index d4f6d2457254e..dda50847169e7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -118,34 +118,21 @@ export interface PolicyDetailsState { * Endpoint Policy configuration */ export interface PolicyConfig { - windows: { - events: { - process: boolean; - network: boolean; - }; - /** malware mode can be off, detect, prevent or prevent and notify user */ - malware: MalwareFields; + windows: UIPolicyConfig['windows'] & { logging: { stdout: string; file: string; }; advanced: PolicyConfigAdvancedOptions; }; - mac: { - events: { - process: boolean; - }; - malware: MalwareFields; + mac: UIPolicyConfig['mac'] & { logging: { stdout: string; file: string; }; advanced: PolicyConfigAdvancedOptions; }; - linux: { - events: { - process: boolean; - }; + linux: UIPolicyConfig['linux'] & { logging: { stdout: string; file: string; @@ -168,29 +155,39 @@ interface PolicyConfigAdvancedOptions { }; } -/** - * Windows-specific policy configuration that is supported via the UI - */ -type WindowsPolicyConfig = Pick; - -/** - * Mac-specific policy configuration that is supported via the UI - */ -type MacPolicyConfig = Pick; - -/** - * Linux-specific policy configuration that is supported via the UI - */ -type LinuxPolicyConfig = Pick; - /** * The set of Policy configuration settings that are show/edited via the UI */ -export interface UIPolicyConfig { - windows: WindowsPolicyConfig; - mac: MacPolicyConfig; - linux: LinuxPolicyConfig; -} +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +export type UIPolicyConfig = { + windows: { + events: { + process: boolean; + network: boolean; + }; + /** malware mode can be off, detect, prevent or prevent and notify user */ + malware: MalwareFields; + }; + mac: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + malware: MalwareFields; + }; + + /** + * Linux-specific policy configuration that is supported via the UI + */ + linux: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + }; +}; /** OS used in Policy */ export enum OS { @@ -203,6 +200,7 @@ export enum OS { export enum EventingFields { process = 'process', network = 'network', + file = 'file', } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index bc56e5e6f6329..1e723e32615eb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -29,12 +29,12 @@ import { isLoading, apiError, } from '../../store/policy_details/selectors'; -import { WindowsEventing } from './policy_forms/eventing/windows'; import { PageView, PageViewHeaderTitle } from '../../components/page_view'; import { AppAction } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; +import { WindowsEvents, MacEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; export const PolicyDetails = React.memo(() => { @@ -206,7 +206,9 @@ export const PolicyDetails = React.memo(() => { - + + + ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx deleted file mode 100644 index 8b7fb89ed1646..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { EuiCheckbox } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig, windowsEventing } from '../../../../store/policy_details/selectors'; -import { PolicyDetailsAction } from '../../../../store/policy_details'; -import { OS, EventingFields } from '../../../../types'; -import { clone } from '../../../../models/policy_details_config'; - -export const EventingCheckbox: React.FC<{ - id: string; - name: string; - os: OS; - protectionField: EventingFields; -}> = React.memo(({ id, name, os, protectionField }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const eventing = usePolicyDetailsSelector(windowsEventing); - const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); - - const handleRadioChange = useCallback( - (event: React.ChangeEvent) => { - if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); - if (os === OS.linux || os === OS.mac) { - newPayload[os].events.process = event.target.checked; - } else { - newPayload[os].events[protectionField] = event.target.checked; - } - - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, os, policyDetailsConfig, protectionField] - ); - - return ( - - ); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx new file mode 100644 index 0000000000000..bec6b33b85c7f --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiCheckbox } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { htmlIdGenerator } from '@elastic/eui'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { policyConfig } from '../../../../store/policy_details/selectors'; +import { PolicyDetailsAction } from '../../../../store/policy_details'; +import { UIPolicyConfig } from '../../../../types'; + +export const EventsCheckbox = React.memo(function({ + name, + setter, + getter, +}: { + name: string; + setter: (config: UIPolicyConfig, checked: boolean) => UIPolicyConfig; + getter: (config: UIPolicyConfig) => boolean; +}) { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const selected = getter(policyDetailsConfig); + const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); + + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent) => { + if (policyDetailsConfig) { + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setter(policyDetailsConfig, event.target.checked) }, + }); + } + }, + [dispatch, policyDetailsConfig, setter] + ); + + return ( + htmlIdGenerator()(), [])} + label={name} + checked={selected} + onChange={handleCheckboxChange} + /> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx new file mode 100644 index 0000000000000..44716d8183041 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WindowsEvents } from './windows'; +export { MacEvents } from './mac'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx new file mode 100644 index 0000000000000..3b69c21d2b150 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EventsCheckbox } from './checkbox'; +import { OS, UIPolicyConfig } from '../../../../types'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { selectedMacEvents, totalMacEvents } from '../../../../store/policy_details/selectors'; +import { ConfigForm } from '../config_form'; +import { getIn, setIn } from '../../../../models/policy_details_config'; + +export const MacEvents = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedMacEvents); + const total = usePolicyDetailsSelector(totalMacEvents); + + const checkboxes: Array<{ + name: string; + os: 'mac'; + protectionField: keyof UIPolicyConfig['mac']['events']; + }> = useMemo( + () => [ + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.file', { + defaultMessage: 'File', + }), + os: OS.mac, + protectionField: 'file', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.process', { + defaultMessage: 'Process', + }), + os: OS.mac, + protectionField: 'process', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.network', { + defaultMessage: 'Network', + }), + os: OS.mac, + protectionField: 'network', + }, + ], + [] + ); + + const renderCheckboxes = () => { + return ( + <> + +
+ +
+
+ + {checkboxes.map((item, index) => { + return ( + + setIn(config)(item.os)('events')(item.protectionField)(checked) + } + getter={config => getIn(config)(item.os)('events')(item.protectionField)} + /> + ); + })} + + ); + }; + + const collectionsEnabled = () => { + return ( + + + + ); + }; + + return ( + + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx similarity index 61% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx index 7bec2c4c742d2..63a140912437d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx @@ -8,40 +8,45 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; -import { EventingCheckbox } from './checkbox'; -import { OS, EventingFields } from '../../../../types'; +import { EventsCheckbox } from './checkbox'; +import { OS, UIPolicyConfig } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { - selectedWindowsEventing, - totalWindowsEventing, + selectedWindowsEvents, + totalWindowsEvents, } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; +import { setIn, getIn } from '../../../../models/policy_details_config'; -export const WindowsEventing = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedWindowsEventing); - const total = usePolicyDetailsSelector(totalWindowsEventing); +export const WindowsEvents = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedWindowsEvents); + const total = usePolicyDetailsSelector(totalWindowsEvents); - const checkboxes = useMemo( + const checkboxes: Array<{ + name: string; + os: 'windows'; + protectionField: keyof UIPolicyConfig['windows']['events']; + }> = useMemo( () => [ { - name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingProcess', { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.process', { defaultMessage: 'Process', }), os: OS.windows, - protectionField: EventingFields.process, + protectionField: 'process', }, { - name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingNetwork', { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.network', { defaultMessage: 'Network', }), os: OS.windows, - protectionField: EventingFields.network, + protectionField: 'network', }, ], [] ); - const renderCheckboxes = () => { + const renderCheckboxes = useMemo(() => { return ( <> @@ -55,20 +60,21 @@ export const WindowsEventing = React.memo(() => { {checkboxes.map((item, index) => { return ( - + setIn(config)(item.os)('events')(item.protectionField)(checked) + } + getter={config => getIn(config)(item.os)('events')(item.protectionField)} /> ); })} ); - }; + }, [checkboxes]); - const collectionsEnabled = () => { + const collectionsEnabled = useMemo(() => { return ( { /> ); - }; + }, [selected, total]); return ( [ + i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), + ], + [] + )} id="windowsEventingForm" - rightCorner={collectionsEnabled()} - children={renderCheckboxes()} + rightCorner={collectionsEnabled} + children={renderCheckboxes} /> ); });