Task/windows eventing form (#60690) (#61469)

Policy Details: Windows Eventing UI card

Co-authored-by: kevinlog <kevin.logan@elastic.co>
This commit is contained in:
Candace Park 2020-03-26 14:53:30 -04:00 committed by GitHub
parent 420ee5c788
commit a18633da23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 550 additions and 40 deletions

View file

@ -0,0 +1,45 @@
/*
* 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 { PolicyConfig } from '../types';
/**
* A typed Object.entries() function where the keys and values are typed based on the given object
*/
const entries = <T extends object>(o: T): Array<[keyof T, T[keyof T]]> =>
Object.entries(o) as Array<[keyof T, T[keyof T]]>;
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
/**
* Returns a deep copy of PolicyDetailsConfig
*/
export function clone(policyDetailsConfig: PolicyConfig): PolicyConfig {
const clonedConfig: DeepPartial<PolicyConfig> = {};
for (const [key, val] of entries(policyDetailsConfig)) {
if (typeof val === 'object') {
const valClone: Partial<typeof val> = {};
clonedConfig[key] = valClone;
for (const [key2, val2] of entries(val)) {
if (typeof val2 === 'object') {
valClone[key2] = {
...val2,
};
} else {
clonedConfig[key] = {
...val,
};
}
}
} else {
clonedConfig[key] = val;
}
}
/**
* clonedConfig is typed as DeepPartial so we can construct the copy from an empty object
*/
return clonedConfig as PolicyConfig;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PolicyData } from '../../types';
import { PolicyData, PolicyConfig } from '../../types';
interface ServerReturnedPolicyDetailsData {
type: 'serverReturnedPolicyDetailsData';
@ -13,4 +13,14 @@ interface ServerReturnedPolicyDetailsData {
};
}
export type PolicyDetailsAction = ServerReturnedPolicyDetailsData;
/**
* When users change a policy via forms, this action is dispatched with a payload that modifies the configuration of a cloned policy config.
*/
interface UserChangedPolicyConfig {
type: 'userChangedPolicyConfig';
payload: {
policyConfig: PolicyConfig;
};
}
export type PolicyDetailsAction = ServerReturnedPolicyDetailsData | UserChangedPolicyConfig;

View file

@ -0,0 +1,92 @@
/*
* 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 { PolicyDetailsState } from '../../types';
import { createStore, Dispatch, Store } from 'redux';
import { policyDetailsReducer, PolicyDetailsAction } from './index';
import { policyConfig, windowsEventing } from './selectors';
import { clone } from '../../models/policy_details_config';
describe('policy details: ', () => {
let store: Store<PolicyDetailsState>;
let getState: typeof store['getState'];
let dispatch: Dispatch<PolicyDetailsAction>;
beforeEach(() => {
store = createStore(policyDetailsReducer);
getState = store.getState;
dispatch = store.dispatch;
dispatch({
type: 'serverReturnedPolicyDetailsData',
payload: {
policyItem: {
id: '',
name: '',
description: '',
config_id: '',
enabled: true,
output_id: '',
inputs: [],
namespace: '',
package: {
name: '',
title: '',
version: '',
},
revision: 1,
},
policyConfig: {
windows: {
malware: {
mode: 'detect',
},
eventing: {
process: false,
network: false,
},
},
mac: {
malware: {
mode: '',
},
eventing: {
process: false,
network: false,
},
},
linux: {
eventing: {
process: false,
network: false,
},
},
},
},
});
});
describe('when the user has enabled windows process eventing', () => {
beforeEach(() => {
const config = policyConfig(getState());
if (!config) {
throw new Error();
}
const newPayload1 = clone(config);
newPayload1.windows.eventing.process = true;
dispatch({
type: 'userChangedPolicyConfig',
payload: { policyConfig: newPayload1 },
});
});
it('windows process eventing is enabled', async () => {
expect(windowsEventing(getState())!.process).toEqual(true);
});
});
});

View file

@ -5,7 +5,7 @@
*/
import { MiddlewareFactory, PolicyDetailsState } from '../../types';
import { selectPolicyIdFromParams, isOnPolicyDetailsPage } from './selectors';
import { policyIdFromParams, isOnPolicyDetailsPage } from './selectors';
import { sendGetDatasource } from '../../services/ingest';
export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsState> = coreStart => {
@ -16,7 +16,7 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat
const state = getState();
if (action.type === 'userChangedUrl' && isOnPolicyDetailsPage(state)) {
const id = selectPolicyIdFromParams(state);
const id = policyIdFromParams(state);
const { item: policyItem } = await sendGetDatasource(http, id);
@ -24,6 +24,19 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat
type: 'serverReturnedPolicyDetailsData',
payload: {
policyItem,
policyConfig: {
windows: {
malware: {
mode: 'detect',
},
eventing: {
process: true,
network: true,
},
},
mac: {},
linux: {},
},
},
});
}

View file

@ -11,6 +11,7 @@ import { AppAction } from '../action';
const initialPolicyDetailsState = (): PolicyDetailsState => {
return {
policyItem: undefined,
policyConfig: undefined,
isLoading: false,
};
};
@ -34,5 +35,12 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
};
}
if (action.type === 'userChangedPolicyConfig') {
return {
...state,
policyConfig: action.payload.policyConfig,
};
}
return state;
};

View file

@ -6,9 +6,12 @@
import { createSelector } from 'reselect';
import { PolicyDetailsState } from '../../types';
import { Immutable } from '../../../../../common/types';
export const selectPolicyDetails = (state: PolicyDetailsState) => state.policyItem;
/** Returns the policy details */
export const policyDetails = (state: PolicyDetailsState) => state.policyItem;
/** Returns a boolean of whether the user is on the policy details page or not */
export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => {
if (state.location) {
const pathnameParts = state.location.pathname.split('/');
@ -18,7 +21,8 @@ export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => {
}
};
export const selectPolicyIdFromParams: (state: PolicyDetailsState) => string = createSelector(
/** Returns the policyId from the url */
export const policyIdFromParams: (state: PolicyDetailsState) => string = createSelector(
(state: PolicyDetailsState) => state.location,
(location: PolicyDetailsState['location']) => {
if (location) {
@ -27,3 +31,32 @@ export const selectPolicyIdFromParams: (state: PolicyDetailsState) => string = c
return '';
}
);
/** Returns the policy configuration */
export const policyConfig = (state: Immutable<PolicyDetailsState>) => state.policyConfig;
/** Returns an object of all the windows eventing configuration */
export const windowsEventing = (state: PolicyDetailsState) => {
const config = policyConfig(state);
return config && config.windows.eventing;
};
/** Returns the total number of possible windows eventing configurations */
export const totalWindowsEventing = (state: PolicyDetailsState): number => {
const config = policyConfig(state);
if (config) {
return Object.keys(config.windows.eventing).length;
}
return 0;
};
/** Returns the number of selected windows eventing configurations */
export const selectedWindowsEventing = (state: PolicyDetailsState): number => {
const config = policyConfig(state);
if (config) {
return Object.values(config.windows.eventing).reduce((count, event) => {
return event === true ? count + 1 : count;
}, 0);
}
return 0;
};

View file

@ -75,17 +75,77 @@ export interface PolicyListState {
}
/**
* Policy list store state
* Policy details store state
*/
export interface PolicyDetailsState {
/** A single policy item */
policyItem: PolicyData | undefined;
policyItem?: PolicyData;
/** data is being retrieved from server */
policyConfig?: PolicyConfig;
isLoading: boolean;
/** current location of the application */
location?: Immutable<EndpointAppLocation>;
}
/**
* Policy Details configuration
*/
export interface PolicyConfig {
windows: WindowsPolicyConfig;
mac: MacPolicyConfig;
linux: LinuxPolicyConfig;
}
/**
* Windows-specific policy configuration
*/
interface WindowsPolicyConfig {
/** malware mode can be detect, prevent or prevent and notify user */
malware: {
mode: string;
};
eventing: {
process: boolean;
network: boolean;
};
}
/**
* Mac-specific policy configuration
*/
interface MacPolicyConfig {
/** malware mode can be detect, prevent or prevent and notify user */
malware: {
mode: string;
};
eventing: {
process: boolean;
network: boolean;
};
}
/**
* Linux-specific policy configuration
*/
interface LinuxPolicyConfig {
eventing: {
process: boolean;
network: boolean;
};
}
/** OS used in Policy */
export enum OS {
windows = 'windows',
mac = 'mac',
linux = 'linux',
}
/** Used in Policy */
export enum EventingFields {
process = 'process',
network = 'network',
}
export interface GlobalState {
readonly hostList: HostListState;
readonly alertList: AlertListState;

View file

@ -5,13 +5,26 @@
*/
import React from 'react';
import { EuiTitle } from '@elastic/eui';
import {
EuiTitle,
EuiPage,
EuiPageBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
EuiText,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { usePolicyDetailsSelector } from './policy_hooks';
import { selectPolicyDetails } from '../../store/policy_details/selectors';
import { policyDetails } from '../../store/policy_details/selectors';
import { WindowsEventing } from './policy_forms/eventing/windows';
export const PolicyDetails = React.memo(() => {
const policyItem = usePolicyDetailsSelector(selectPolicyDetails);
const policyItem = usePolicyDetailsSelector(policyDetails);
function policyName() {
if (policyItem) {
@ -29,8 +42,41 @@ export const PolicyDetails = React.memo(() => {
}
return (
<EuiTitle size="l">
<h1 data-test-subj="policyDetailsViewTitle">{policyName()}</h1>
</EuiTitle>
<EuiPage data-test-subj="policyDetailsPage">
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="m">
<h1 data-test-subj="policyDetailsViewTitle">{policyName()}</h1>
</EuiTitle>
</EuiPageHeaderSection>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty>
<FormattedMessage
id="xpack.endpoint.policy.details.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill={true} iconType="save">
<FormattedMessage id="xpack.endpoint.policy.details.save" defaultMessage="Save" />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageHeader>
<EuiText size="xs" color="subdued">
<h4>
<FormattedMessage
id="xpack.endpoint.policy.details.settings"
defaultMessage="Settings"
/>
</h4>
</EuiText>
<EuiSpacer size="xs" />
<WindowsEventing />
</EuiPageBody>
</EuiPage>
);
});

View file

@ -0,0 +1,110 @@
/*
* 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 from 'react';
import {
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiHorizontalRule,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import styled from 'styled-components';
const PolicyDetailCard = styled.div`
.policyDetailTitleOS {
flex-grow: 2;
}
.policyDetailTitleFlexItem {
margin: 0;
}
`;
export const ConfigForm: React.FC<{
type: string;
supportedOss: string[];
children: React.ReactNode;
id: string;
selectedEventing: number;
totalEventing: number;
}> = React.memo(({ type, supportedOss, children, id, selectedEventing, totalEventing }) => {
const typeTitle = () => {
return (
<EuiFlexGroup direction="row" gutterSize="none" alignItems="center">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem className="policyDetailTitleFlexItem">
<EuiTitle size="xxxs">
<h6>
<FormattedMessage id="xpack.endpoint.policyDetailType" defaultMessage="Type" />
</h6>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem className="policyDetailTitleFlexItem">
<EuiText size="m">{type}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="none" className="policyDetailTitleOS">
<EuiFlexItem className="policyDetailTitleFlexItem">
<EuiTitle size="xxxs">
<h6>
<FormattedMessage
id="xpack.endpoint.policyDetailOS"
defaultMessage="Operating System"
/>
</h6>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem className="policyDetailTitleFlexItem">
<EuiText>{supportedOss.join(', ')}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.endpoint.policy.details.eventCollectionsEnabled"
defaultMessage="{selectedEventing} / {totalEventing} event collections enabled"
values={{ selectedEventing, totalEventing }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const events = () => {
return (
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.endpoint.policyDetailsConfig.eventingEvents"
defaultMessage="Events"
/>
</h5>
</EuiTitle>
);
};
return (
<PolicyDetailCard>
<EuiCard
data-test-subj={id}
textAlign="left"
title={typeTitle()}
description=""
children={
<>
<EuiHorizontalRule margin="m" />
{events()}
<EuiSpacer size="s" />
{children}
</>
}
/>
</PolicyDetailCard>
);
});

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;
* 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<HTMLInputElement>) => {
if (policyDetailsConfig) {
const newPayload = clone(policyDetailsConfig);
newPayload[os].eventing[protectionField] = event.target.checked;
dispatch({
type: 'userChangedPolicyConfig',
payload: { policyConfig: newPayload },
});
}
},
[dispatch, os, policyDetailsConfig, protectionField]
);
return (
<EuiCheckbox
id={id}
label={name}
checked={eventing && eventing[protectionField]}
onChange={handleRadioChange}
/>
);
});

View file

@ -0,0 +1,70 @@
/*
* 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 { EventingCheckbox } from './checkbox';
import { OS, EventingFields } from '../../../../types';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import {
selectedWindowsEventing,
totalWindowsEventing,
} from '../../../../store/policy_details/selectors';
import { ConfigForm } from '../config_form';
export const WindowsEventing = React.memo(() => {
const checkboxes = useMemo(
() => [
{
name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingProcess', {
defaultMessage: 'Process',
}),
os: OS.windows,
protectionField: EventingFields.process,
},
{
name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingNetwork', {
defaultMessage: 'Network',
}),
os: OS.windows,
protectionField: EventingFields.network,
},
],
[]
);
const renderCheckboxes = () => {
return checkboxes.map((item, index) => {
return (
<EventingCheckbox
id={`eventing${item.name}`}
name={item.name}
key={index}
os={item.os}
protectionField={item.protectionField}
/>
);
});
};
const selected = usePolicyDetailsSelector(selectedWindowsEventing);
const total = usePolicyDetailsSelector(totalWindowsEventing);
return (
<ConfigForm
type={i18n.translate('xpack.endpoint.policy.details.eventCollection', {
defaultMessage: 'Event Collection',
})}
supportedOss={[
i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }),
]}
id="windowsEventingForm"
children={renderCheckboxes()}
selectedEventing={selected}
totalEventing={total}
/>
);
});

View file

@ -14,7 +14,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./header_nav'));
loadTestFile(require.resolve('./host_list'));
loadTestFile(require.resolve('./policy_list'));
loadTestFile(require.resolve('./policy_details'));
loadTestFile(require.resolve('./alerts'));
});
}

View file

@ -1,25 +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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'endpoint']);
const testSubjects = getService('testSubjects');
// Skipped until we can figure out how to load data for Ingest
describe.skip('Endpoint Policy Details', function() {
this.tags(['ciGroup7']);
it('loads the Policy Details Page', async () => {
await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy/123');
await testSubjects.existOrFail('policyDetailsViewTitle');
const policyDetailsNotFoundTitle = await testSubjects.getVisibleText('policyDetailsName');
expect(policyDetailsNotFoundTitle).to.equal('policy with some protections 123');
});
});
}