[SIEM] Detection Engine Create Rule Design Review #1 (#54442)

This commit is contained in:
patrykkopycinski 2020-01-13 21:59:45 +01:00 committed by GitHub
parent 51d96e52ec
commit e9319360e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 588 additions and 426 deletions

View file

@ -8,7 +8,6 @@
import {
EuiBadge,
EuiHealth,
EuiIconTip,
EuiLink,
EuiTextColor,
@ -17,7 +16,6 @@ import {
} from '@elastic/eui';
import * as H from 'history';
import React from 'react';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { getEmptyTagValue } from '../../../../components/empty_value';
import {
deleteRulesAction,
@ -32,6 +30,7 @@ import { TableData } from '../types';
import * as i18n from '../translations';
import { PreferenceFormattedDate } from '../../../../components/formatted_date';
import { RuleSwitch } from '../components/rule_switch';
import { SeverityBadge } from '../components/severity_badge';
const getActions = (dispatch: React.Dispatch<Action>, history: H.History) => [
{
@ -92,21 +91,7 @@ export const getColumns = (
{
field: 'severity',
name: i18n.COLUMN_SEVERITY,
render: (value: TableData['severity']) => (
<EuiHealth
color={
value === 'low'
? euiLightVars.euiColorVis0
: value === 'medium'
? euiLightVars.euiColorVis5
: value === 'high'
? euiLightVars.euiColorVis7
: euiLightVars.euiColorVis9
}
>
{value}
</EuiHealth>
),
render: (value: TableData['severity']) => <SeverityBadge value={value} />,
truncateText: true,
},
{

View file

@ -37,6 +37,25 @@ const MyEuiFormRow = styled(EuiFormRow)`
}
`;
export const MyAddItemButton = styled(EuiButtonEmpty)`
margin-top: 4px;
&.euiButtonEmpty--xSmall {
font-size: 12px;
}
.euiIcon {
width: 12px;
height: 12px;
}
`;
MyAddItemButton.defaultProps = {
flush: 'left',
iconType: 'plusInCircle',
size: 'xs',
};
export const AddItem = ({
addText,
dataTestSubj,
@ -160,9 +179,9 @@ export const AddItem = ({
);
})}
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled} iconType="plusInCircle">
<MyAddItemButton onClick={addItem} isDisabled={isDisabled}>
{addText}
</EuiButtonEmpty>
</MyAddItemButton>
</>
</MyEuiFormRow>
);

View file

@ -0,0 +1 @@
<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M1 0H0v8h8V7H1V0z" fill="#D3DAE6"/></svg>

After

Width:  |  Height:  |  Size: 163 B

View file

@ -9,12 +9,10 @@ import {
EuiLoadingSpinner,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiLink,
EuiText,
EuiListGroup,
EuiButtonEmpty,
EuiSpacer,
} from '@elastic/eui';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { isEmpty } from 'lodash/fp';
import React from 'react';
@ -27,6 +25,11 @@ import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_
import { FilterLabel } from './filter_label';
import * as i18n from './translations';
import { BuildQueryBarDescription, BuildThreatsDescription, ListItems } from './types';
import { SeverityBadge } from '../severity_badge';
import ListTreeIcon from './assets/list_tree_icon.svg';
const isNotEmptyArray = (values: string[]) =>
!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0;
const EuiBadgeWrap = styled(EuiBadge)`
.euiBadge__text {
@ -97,10 +100,17 @@ const ThreatsEuiFlexGroup = styled(EuiFlexGroup)`
}
`;
const MyEuiListGroup = styled(EuiListGroup)`
padding: 0px;
.euiListGroupItem__button {
padding: 0px;
const TechniqueLinkItem = styled(EuiButtonEmpty)`
.euiIcon {
width: 8px;
height: 8px;
}
`;
const ReferenceLinkItem = styled(EuiButtonEmpty)`
.euiIcon {
width: 12px;
height: 12px;
}
`;
@ -118,28 +128,31 @@ export const buildThreatsDescription = ({
const tactic = tacticsOptions.find(t => t.name === threat.tactic.name);
return (
<EuiFlexItem key={`${threat.tactic.name}-${index}`}>
<EuiText grow={false} size="s">
<h5>
<EuiLink href={threat.tactic.reference} target="_blank">
{tactic != null ? tactic.text : ''}
</EuiLink>
</h5>
<MyEuiListGroup
flush={false}
bordered={false}
listItems={threat.techniques.map(technique => {
const myTechnique = techniquesOptions.find(t => t.name === technique.name);
return {
label: myTechnique != null ? myTechnique.label : '',
href: technique.reference,
target: '_blank',
};
})}
/>
</EuiText>
<EuiLink href={threat.tactic.reference} target="_blank">
{tactic != null ? tactic.text : ''}
</EuiLink>
<EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column">
{threat.techniques.map(technique => {
const myTechnique = techniquesOptions.find(t => t.name === technique.name);
return (
<EuiFlexItem>
<TechniqueLinkItem
href={technique.reference}
target="_blank"
iconType={ListTreeIcon}
size="xs"
flush="left"
>
{myTechnique != null ? myTechnique.label : ''}
</TechniqueLinkItem>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
);
})}
<EuiSpacer />
</ThreatsEuiFlexGroup>
),
},
@ -148,12 +161,34 @@ export const buildThreatsDescription = ({
return [];
};
export const buildUnorderedListArrayDescription = (
label: string,
field: string,
values: string[]
): ListItems[] => {
if (isNotEmptyArray(values)) {
return [
{
title: label,
description: (
<ul>
{values.map((val: string) =>
isEmpty(val) ? null : <li key={`${field}-${val}`}>{val}</li>
)}
</ul>
),
},
];
}
return [];
};
export const buildStringArrayDescription = (
label: string,
field: string,
values: string[]
): ListItems[] => {
if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) {
if (isNotEmptyArray(values)) {
return [
{
title: label,
@ -174,46 +209,34 @@ export const buildStringArrayDescription = (
return [];
};
export const buildSeverityDescription = (label: string, value: string): ListItems[] => {
return [
{
title: label,
description: (
<EuiHealth
color={
value === 'low'
? euiLightVars.euiColorVis0
: value === 'medium'
? euiLightVars.euiColorVis5
: value === 'high'
? euiLightVars.euiColorVis7
: euiLightVars.euiColorVis9
}
>
{value}
</EuiHealth>
),
},
];
};
export const buildSeverityDescription = (label: string, value: string): ListItems[] => [
{
title: label,
description: <SeverityBadge value={value} />,
},
];
export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => {
if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) {
if (isNotEmptyArray(values)) {
return [
{
title: label,
description: (
<EuiListGroup
flush={true}
bordered={false}
listItems={values.map((val: string) => ({
label: val,
href: val,
iconType: 'link',
size: 'xs',
target: '_blank',
}))}
/>
<EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column">
{values.map((val: string) => (
<EuiFlexItem>
<ReferenceLinkItem
href={val}
target="_blank"
iconType="link"
size="xs"
flush="left"
>
{val}
</ReferenceLinkItem>
</EuiFlexItem>
))}
</EuiFlexGroup>
),
},
];

View file

@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui';
import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty, chunk, get, pick } from 'lodash/fp';
import React, { memo, useState } from 'react';
import styled from 'styled-components';
import {
IIndexPattern,
@ -26,6 +25,7 @@ import {
buildSeverityDescription,
buildStringArrayDescription,
buildThreatsDescription,
buildUnorderedListArrayDescription,
buildUrlsDescription,
} from './helpers';
@ -36,15 +36,6 @@ interface StepRuleDescriptionProps {
schema: FormSchema;
}
const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>`
${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')};
`;
const MyEuiTextArea = styled(EuiTextArea)`
max-width: 100%;
height: 80px;
`;
const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
data,
direction = 'row',
@ -62,13 +53,24 @@ const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
],
[]
);
if (direction === 'row') {
return (
<EuiFlexGroup>
{chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => (
<EuiFlexItem key={`description-step-rule-${index}`}>
<EuiDescriptionList listItems={chunkListItems} />
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup gutterSize="none" direction={direction} justifyContent="spaceAround">
{chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => (
<EuiFlexItemWidth direction={direction} key={`description-step-rule-${index}`} grow={false}>
<EuiDescriptionList listItems={chunkListItems} />
</EuiFlexItemWidth>
))}
<EuiFlexGroup>
<EuiFlexItem key={`description-step-rule`}>
<EuiDescriptionList listItems={listItems} />
</EuiFlexItem>
</EuiFlexGroup>
);
};
@ -123,18 +125,28 @@ const getDescriptionItem = (
return [
{
title: label,
description: <MyEuiTextArea value={get(field, value)} readOnly={true} />,
description: get(field, value),
},
];
} else if (field === 'references') {
const urls: string[] = get(field, value);
return buildUrlsDescription(label, urls);
} else if (field === 'falsePositives') {
const values: string[] = get(field, value);
return buildUnorderedListArrayDescription(label, field, values);
} else if (Array.isArray(get(field, value))) {
const values: string[] = get(field, value);
return buildStringArrayDescription(label, field, values);
} else if (field === 'severity') {
const val: string = get(field, value);
return buildSeverityDescription(label, val);
} else if (field === 'riskScore') {
return [
{
title: label,
description: get(field, value),
},
];
} else if (field === 'timeline') {
const timeline = get(field, value) as FieldValueTimeline;
return [

View file

@ -5,7 +5,6 @@
*/
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFormRow,
EuiSuperSelect,
@ -24,6 +23,7 @@ import * as Rulei18n from '../../translations';
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
import { threatsDefault } from '../step_about_rule/default_value';
import { IMitreEnterpriseAttack } from '../../types';
import { MyAddItemButton } from '../add_item_form';
import { isMitreAttackInvalid } from './helpers';
import * as i18n from './translations';
@ -134,13 +134,19 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => {
const invalid = isMitreAttackInvalid(item.tactic.name, item.techniques);
const options = techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name)));
const selectedOptions = item.techniques.map(technic => ({
...technic,
label: `${technic.name} (${technic.id})`, // API doesn't allow for label field
}));
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow>
<EuiComboBox
placeholder={item.tactic.name === 'none' ? '' : i18n.TECHNIQUES_PLACEHOLDER}
options={techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name)))}
selectedOptions={item.techniques}
options={options}
selectedOptions={selectedOptions}
onChange={updateTechniques.bind(null, index)}
isDisabled={disabled || item.tactic.name === 'none'}
fullWidth={true}
@ -202,9 +208,9 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
{values.length - 1 !== index && <EuiSpacer size="s" />}
</div>
))}
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled} iconType="plusInCircle">
<MyAddItemButton onClick={addItem} isDisabled={isDisabled}>
{i18n.ADD_MITRE_ATTACK}
</EuiButtonEmpty>
</MyAddItemButton>
</MitreContainer>
);
};

View file

@ -0,0 +1,16 @@
/*
* 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 { EuiText } from '@elastic/eui';
import React from 'react';
import * as RuleI18n from '../../translations';
export const OptionalFieldLabel = (
<EuiText color="subdued" size="xs">
{RuleI18n.OPTIONAL_FIELD}
</EuiText>
);

View file

@ -51,7 +51,7 @@ interface QueryBarDefineRuleProps {
const StyledEuiFormRow = styled(EuiFormRow)`
.kbnTypeahead__items {
max-height: 14vh !important;
max-height: 45vh !important;
}
.globalQueryBar {
padding: 4px 0px 0px 0px;

View file

@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFieldNumber,
EuiFormRow,
EuiSelect,
EuiFormControlLayout,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
@ -26,10 +33,28 @@ const timeTypeOptions = [
{ value: 'h', text: I18n.HOURS },
];
// move optional label to the end of input
const StyledLabelAppend = styled(EuiFlexItem)`
&.euiFlexItem.euiFlexItem--flexGrowZero {
margin-left: 31px;
}
`;
const StyledEuiFormRow = styled(EuiFormRow)`
max-width: none;
.euiFormControlLayout {
max-width: 200px !important;
}
.euiFormControlLayout__childrenWrapper > *:first-child {
box-shadow: none;
height: 38px;
}
.euiFormControlLayout:not(:first-child) {
border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
}
`;
const MyEuiSelect = styled(EuiSelect)`
@ -89,9 +114,9 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
<EuiFlexItem grow={false} component="span">
{field.label}
</EuiFlexItem>
<EuiFlexItem grow={false} component="span">
<StyledLabelAppend grow={false} component="span">
{field.labelAppend}
</EuiFlexItem>
</StyledLabelAppend>
</EuiFlexGroup>
),
[field.label, field.labelAppend]
@ -107,7 +132,7 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
data-test-subj={dataTestSubj}
describedByIds={idAria ? [idAria] : undefined}
>
<EuiFieldNumber
<EuiFormControlLayout
append={
<MyEuiSelect
fullWidth={false}
@ -117,12 +142,9 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
{...rest}
/>
}
fullWidth
min={0}
onChange={onChangeTimeVal}
value={timeVal}
{...rest}
/>
>
<EuiFieldNumber fullWidth min={0} onChange={onChangeTimeVal} value={timeVal} {...rest} />
</EuiFormControlLayout>
</StyledEuiFormRow>
);
};

View file

@ -0,0 +1,32 @@
/*
* 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 { upperFirst } from 'lodash/fp';
import React from 'react';
import { EuiHealth } from '@elastic/eui';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
interface Props {
value: string;
}
const SeverityBadgeComponent: React.FC<Props> = ({ value }) => (
<EuiHealth
color={
value === 'low'
? euiLightVars.euiColorVis0
: value === 'medium'
? euiLightVars.euiColorVis5
: value === 'high'
? euiLightVars.euiColorVis7
: euiLightVars.euiColorVis9
}
>
{upperFirst(value)}
</EuiHealth>
);
export const SeverityBadge = React.memo(SeverityBadgeComponent);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import styled from 'styled-components';
import { EuiHealth } from '@elastic/eui';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
@ -16,22 +17,30 @@ interface SeverityOptionItem {
inputDisplay: React.ReactElement;
}
const StyledEuiHealth = styled(EuiHealth)`
line-height: inherit;
`;
export const severityOptions: SeverityOptionItem[] = [
{
value: 'low',
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis0}>{I18n.LOW}</EuiHealth>,
inputDisplay: <StyledEuiHealth color={euiLightVars.euiColorVis0}>{I18n.LOW}</StyledEuiHealth>,
},
{
value: 'medium',
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis5}>{I18n.MEDIUM} </EuiHealth>,
inputDisplay: (
<StyledEuiHealth color={euiLightVars.euiColorVis5}>{I18n.MEDIUM}</StyledEuiHealth>
),
},
{
value: 'high',
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis7}>{I18n.HIGH} </EuiHealth>,
inputDisplay: <StyledEuiHealth color={euiLightVars.euiColorVis7}>{I18n.HIGH}</StyledEuiHealth>,
},
{
value: 'critical',
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis9}>{I18n.CRITICAL} </EuiHealth>,
inputDisplay: (
<StyledEuiHealth color={euiLightVars.euiColorVis9}>{I18n.CRITICAL}</StyledEuiHealth>
),
},
];

View file

@ -6,7 +6,7 @@
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useState } from 'react';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
@ -22,6 +22,7 @@ import { isUrlInvalid } from './helpers';
import { schema } from './schema';
import * as I18n from './translations';
import { PickTimeline } from '../pick_timeline';
import { StepContentWrapper } from '../step_content_wrapper';
const CommonUseField = getUseField({ component: Field });
@ -33,64 +34,67 @@ const TagContainer = styled.div`
margin-top: 16px;
`;
export const StepAboutRule = memo<StepAboutRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isUpdateView = false,
isLoading,
setForm,
setStepData,
}) => {
const [myStepData, setMyStepData] = useState<AboutStepRule>(stepAboutDefaultValue);
const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
addPadding = false,
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isUpdateView = false,
isLoading,
setForm,
setStepData,
}) => {
const [myStepData, setMyStepData] = useState<AboutStepRule>(stepAboutDefaultValue);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(async () => {
if (setStepData) {
setStepData(RuleStep.aboutRule, null, false);
const { isValid, data } = await form.submit();
if (isValid) {
setStepData(RuleStep.aboutRule, data, isValid);
setMyStepData({ ...data, isNew: false } as AboutStepRule);
}
const onSubmit = useCallback(async () => {
if (setStepData) {
setStepData(RuleStep.aboutRule, null, false);
const { isValid, data } = await form.submit();
if (isValid) {
setStepData(RuleStep.aboutRule, data, isValid);
setMyStepData({ ...data, isNew: false } as AboutStepRule);
}
}, [form]);
}
}, [form]);
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}, [defaultValues]);
}
}, [defaultValues]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.aboutRule, form);
}
}, [form]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.aboutRule, form);
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
return isReadOnlyView && myStepData != null ? (
<StepContentWrapper addPadding={addPadding}>
<StepRuleDescription direction={descriptionDirection} schema={schema} data={myStepData} />
) : (
<>
</StepContentWrapper>
) : (
<>
<StepContentWrapper addPadding={!isUpdateView}>
<Form form={form} data-test-subj="stepAboutRule">
<CommonUseField
path="name"
@ -205,24 +209,26 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
}}
</FormDataProvider>
</Form>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{RuleI18n.CONTINUE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}
);
</StepContentWrapper>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{RuleI18n.CONTINUE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
};
export const StepAboutRule = memo(StepAboutRuleComponent);

View file

@ -4,11 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import * as RuleI18n from '../../translations';
import { IMitreEnterpriseAttack } from '../../types';
import {
FIELD_TYPES,
@ -18,6 +15,7 @@ import {
ERROR_CODE,
} from '../shared_imports';
import { isMitreAttackInvalid } from '../mitre/helpers';
import { OptionalFieldLabel } from '../optional_field_label';
import { isUrlInvalid } from './helpers';
import * as I18n from './translations';
@ -108,7 +106,7 @@ export const schema: FormSchema = {
defaultMessage: 'Reference URLs',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: OptionalFieldLabel,
validations: [
{
validator: (
@ -136,10 +134,10 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel',
{
defaultMessage: 'False positives examples',
defaultMessage: 'False positive examples',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: OptionalFieldLabel,
},
threats: {
label: i18n.translate(
@ -148,7 +146,7 @@ export const schema: FormSchema = {
defaultMessage: 'MITRE ATT&CK\\u2122',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: OptionalFieldLabel,
validations: [
{
validator: (
@ -184,6 +182,6 @@ export const schema: FormSchema = {
'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: OptionalFieldLabel,
},
};

View file

@ -0,0 +1,18 @@
/*
* 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 styled from 'styled-components';
const StyledDiv = styled.div<{ addPadding: boolean }>`
padding-left: ${({ addPadding }) => addPadding && '53px'}; /* to align with the step title */
`;
StyledDiv.defaultProps = {
addPadding: false,
};
export const StepContentWrapper = React.memo(StyledDiv);

View file

@ -12,7 +12,8 @@ import {
EuiButton,
} from '@elastic/eui';
import { isEmpty, isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useState, useEffect } from 'react';
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
import styled from 'styled-components';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
@ -22,6 +23,7 @@ import * as RuleI18n from '../../translations';
import { DefineStepRule, RuleStep, RuleStepProps } from '../../types';
import { StepRuleDescription } from '../description_step';
import { QueryBarDefineRule } from '../query_bar';
import { StepContentWrapper } from '../step_content_wrapper';
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
import { schema } from './schema';
import * as i18n from './translations';
@ -42,6 +44,20 @@ const stepDefineDefaultValue = {
},
};
const MyLabelButton = styled(EuiButtonEmpty)`
height: 18px;
font-size: 12px;
.euiIcon {
width: 14px;
height: 14px;
}
`;
MyLabelButton.defaultProps = {
flush: 'right',
};
const getStepDefaultValue = (
indicesConfig: string[],
defaultValues: DefineStepRule | null
@ -59,106 +75,104 @@ const getStepDefaultValue = (
}
};
export const StepDefineRule = memo<StepDefineRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
resizeParentContainer,
setForm,
setStepData,
}) => {
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false);
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState(
defaultValues != null ? defaultValues.index : indicesConfig ?? []
);
const [
{
browserFields,
indexPatterns: indexPatternQueryBar,
isLoading: indexPatternLoadingQueryBar,
},
] = useFetchIndexPatterns(mylocalIndicesConfig);
const [myStepData, setMyStepData] = useState<DefineStepRule>(
getStepDefaultValue(indicesConfig, null)
);
const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
addPadding = false,
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
setForm,
setStepData,
}) => {
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false);
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState(
defaultValues != null ? defaultValues.index : indicesConfig ?? []
);
const [
{ browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar },
] = useFetchIndexPatterns(mylocalIndicesConfig);
const [myStepData, setMyStepData] = useState<DefineStepRule>(
getStepDefaultValue(indicesConfig, null)
);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(async () => {
if (setStepData) {
setStepData(RuleStep.defineRule, null, false);
const { isValid, data } = await form.submit();
if (isValid && setStepData) {
setStepData(RuleStep.defineRule, data, isValid);
setMyStepData({ ...data, isNew: false } as DefineStepRule);
const onSubmit = useCallback(async () => {
if (setStepData) {
setStepData(RuleStep.defineRule, null, false);
const { isValid, data } = await form.submit();
if (isValid && setStepData) {
setStepData(RuleStep.defineRule, data, isValid);
setMyStepData({ ...data, isNew: false } as DefineStepRule);
}
}
}, [form]);
useEffect(() => {
if (indicesConfig != null && defaultValues != null) {
const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues);
if (!isEqual(myDefaultValues, myStepData)) {
setMyStepData(myDefaultValues);
setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig));
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}
}, [form]);
}
}, [defaultValues, indicesConfig]);
useEffect(() => {
if (indicesConfig != null && defaultValues != null) {
const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues);
if (!isEqual(myDefaultValues, myStepData)) {
setMyStepData(myDefaultValues);
setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig));
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
}
}
}, [defaultValues, indicesConfig]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.defineRule, form);
}
}, [form]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.defineRule, form);
}
}, [form]);
const handleResetIndices = useCallback(() => {
const indexField = form.getFields().index;
indexField.setValue(indicesConfig);
}, [form, indicesConfig]);
const handleResetIndices = useCallback(() => {
const indexField = form.getFields().index;
indexField.setValue(indicesConfig);
}, [form, indicesConfig]);
const handleOpenTimelineSearch = useCallback(() => {
setOpenTimelineSearch(true);
}, []);
const handleOpenTimelineSearch = useCallback(() => {
setOpenTimelineSearch(true);
}, []);
const handleCloseTimelineSearch = useCallback(() => {
setOpenTimelineSearch(false);
}, []);
const handleCloseTimelineSearch = useCallback(() => {
setOpenTimelineSearch(false);
}, []);
return isReadOnlyView && myStepData != null ? (
return isReadOnlyView && myStepData != null ? (
<StepContentWrapper addPadding={addPadding}>
<StepRuleDescription
direction={descriptionDirection}
indexPatterns={indexPatternQueryBar as IIndexPattern}
schema={schema}
data={myStepData}
/>
) : (
<>
</StepContentWrapper>
) : (
<>
<StepContentWrapper addPadding={!isUpdateView}>
<Form form={form} data-test-subj="stepDefineRule">
<CommonUseField
path="index"
config={{
...schema.index,
labelAppend: !localUseIndicesConfig ? (
<EuiButtonEmpty size="xs" onClick={handleResetIndices}>
<small>{i18n.RESET_DEFAULT_INDEX}</small>
</EuiButtonEmpty>
<MyLabelButton onClick={handleResetIndices} iconType="refresh">
{i18n.RESET_DEFAULT_INDEX}
</MyLabelButton>
) : null,
}}
componentProps={{
@ -176,9 +190,9 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
config={{
...schema.queryBar,
labelAppend: (
<EuiButtonEmpty size="xs" onClick={handleOpenTimelineSearch}>
<small>{i18n.IMPORT_TIMELINE_QUERY}</small>
</EuiButtonEmpty>
<MyLabelButton onClick={handleOpenTimelineSearch}>
{i18n.IMPORT_TIMELINE_QUERY}
</MyLabelButton>
),
}}
component={QueryBarDefineRule}
@ -192,7 +206,6 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
dataTestSubj: 'detectionEngineStepDefineRuleQueryBar',
openTimelineSearch,
onCloseTimelineSearch: handleCloseTimelineSearch,
resizeParentContainer,
}}
/>
<FormDataProvider pathsToWatch="index">
@ -212,24 +225,26 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
}}
</FormDataProvider>
</Form>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{RuleI18n.CONTINUE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}
);
</StepContentWrapper>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
{RuleI18n.CONTINUE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
};
export const StepDefineRule = memo(StepDefineRuleComponent);

View file

@ -6,12 +6,13 @@
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { isEqual, get } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useState } from 'react';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types';
import { StepRuleDescription } from '../description_step';
import { ScheduleItem } from '../schedule_item_form';
import { Form, UseField, useForm } from '../shared_imports';
import { StepContentWrapper } from '../step_content_wrapper';
import { schema } from './schema';
import * as I18n from './translations';
@ -26,67 +27,70 @@ const stepScheduleDefaultValue = {
from: '0m',
};
export const StepScheduleRule = memo<StepScheduleRuleProps>(
({
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
setStepData,
setForm,
}) => {
const [myStepData, setMyStepData] = useState<ScheduleStepRule>(stepScheduleDefaultValue);
const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
addPadding = false,
defaultValues,
descriptionDirection = 'row',
isReadOnlyView,
isLoading,
isUpdateView = false,
setStepData,
setForm,
}) => {
const [myStepData, setMyStepData] = useState<ScheduleStepRule>(stepScheduleDefaultValue);
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const { form } = useForm({
defaultValue: myStepData,
options: { stripEmptyFields: false },
schema,
});
const onSubmit = useCallback(
async (enabled: boolean) => {
if (setStepData) {
setStepData(RuleStep.scheduleRule, null, false);
const { isValid: newIsValid, data } = await form.submit();
if (newIsValid) {
setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid);
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
const onSubmit = useCallback(
async (enabled: boolean) => {
if (setStepData) {
setStepData(RuleStep.scheduleRule, null, false);
const { isValid: newIsValid, data } = await form.submit();
if (newIsValid) {
setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid);
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
}
}
},
[form]
);
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
}
},
[form]
);
useEffect(() => {
const { isNew, ...initDefaultValue } = myStepData;
if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) {
const myDefaultValues = {
...defaultValues,
isNew: false,
};
setMyStepData(myDefaultValues);
if (!isReadOnlyView) {
Object.keys(schema).forEach(key => {
const val = get(key, myDefaultValues);
if (val != null) {
form.setFieldValue(key, val);
}
});
}
});
}
}, [defaultValues]);
}
}, [defaultValues]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.scheduleRule, form);
}
}, [form]);
useEffect(() => {
if (setForm != null) {
setForm(RuleStep.scheduleRule, form);
}
}, [form]);
return isReadOnlyView && myStepData != null ? (
return isReadOnlyView && myStepData != null ? (
<StepContentWrapper addPadding={addPadding}>
<StepRuleDescription direction={descriptionDirection} schema={schema} data={myStepData} />
) : (
<>
</StepContentWrapper>
) : (
<>
<StepContentWrapper addPadding={!isUpdateView}>
<Form form={form} data-test-subj="stepScheduleRule">
<UseField
path="interval"
@ -107,40 +111,42 @@ export const StepScheduleRule = memo<StepScheduleRuleProps>(
}}
/>
</Form>
</StepContentWrapper>
{!isUpdateView && (
<>
<EuiHorizontalRule margin="s" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton
fill={false}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, false)}
>
{I18n.COMPLETE_WITHOUT_ACTIVATING}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, true)}
>
{I18n.COMPLETE_WITH_ACTIVATING}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}
);
{!isUpdateView && (
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup
alignItems="center"
justifyContent="flexEnd"
gutterSize="xs"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton
fill={false}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, false)}
>
{I18n.COMPLETE_WITHOUT_ACTIVATING}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit.bind(null, true)}
>
{I18n.COMPLETE_WITH_ACTIVATING}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
};
export const StepScheduleRule = memo(StepScheduleRuleComponent);

View file

@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiText } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import * as RuleI18n from '../../translations';
import { OptionalFieldLabel } from '../optional_field_label';
import { FormSchema } from '../shared_imports';
export const schema: FormSchema = {
@ -33,7 +31,7 @@ export const schema: FormSchema = {
defaultMessage: 'Additional look-back',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
labelAppend: OptionalFieldLabel,
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText',
{

View file

@ -27,26 +27,17 @@ import * as i18n from './translations';
const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule];
const ResizeEuiPanel = styled(EuiPanel)<{
height?: number;
const MyEuiPanel = styled(EuiPanel)<{
zIndex?: number;
}>`
position: relative;
z-index: ${props => props.zIndex}; /* ugly fix to allow searchBar to overflow the EuiPanel */
.euiAccordion__iconWrapper {
display: none;
}
.euiAccordion__childWrapper {
height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')};
}
.euiAccordion__button {
cursor: default !important;
&:hover {
text-decoration: none !important;
}
}
`;
const MyEuiPanel = styled(EuiPanel)`
.euiAccordion__iconWrapper {
display: none;
overflow: visible;
}
.euiAccordion__button {
cursor: default !important;
@ -64,7 +55,6 @@ export const CreateRuleComponent = React.memo(() => {
canUserCRUD,
hasManageApiKey,
} = useUserInfo();
const [heightAccordion, setHeightAccordion] = useState(-1);
const [openAccordionId, setOpenAccordionId] = useState<RuleStep>(RuleStep.defineRule);
const defineRuleRef = useRef<EuiAccordion | null>(null);
const aboutRuleRef = useRef<EuiAccordion | null>(null);
@ -239,7 +229,7 @@ export const CreateRuleComponent = React.memo(() => {
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
/>
<ResizeEuiPanel height={heightAccordion}>
<MyEuiPanel zIndex={3}>
<EuiAccordion
initialIsOpen={true}
id={RuleStep.defineRule}
@ -259,18 +249,19 @@ export const CreateRuleComponent = React.memo(() => {
)
}
>
<EuiHorizontalRule margin="xs" />
<EuiHorizontalRule margin="m" />
<StepDefineRule
addPadding={true}
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
isLoading={isLoading || loading}
setForm={setStepsForm}
setStepData={setStepData}
resizeParentContainer={height => setHeightAccordion(height)}
descriptionDirection="row"
/>
</EuiAccordion>
</ResizeEuiPanel>
<EuiSpacer size="s" />
<MyEuiPanel>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zIndex={2}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.aboutRule}
@ -290,8 +281,10 @@ export const CreateRuleComponent = React.memo(() => {
)
}
>
<EuiHorizontalRule margin="xs" />
<EuiHorizontalRule margin="m" />
<StepAboutRule
addPadding={true}
descriptionDirection="row"
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
isLoading={isLoading || loading}
setForm={setStepsForm}
@ -299,8 +292,8 @@ export const CreateRuleComponent = React.memo(() => {
/>
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="s" />
<MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zIndex={1}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.scheduleRule}
@ -320,8 +313,10 @@ export const CreateRuleComponent = React.memo(() => {
)
}
>
<EuiHorizontalRule margin="xs" />
<EuiHorizontalRule margin="m" />
<StepScheduleRule
addPadding={true}
descriptionDirection="row"
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
isLoading={isLoading || loading}
setForm={setStepsForm}

View file

@ -237,7 +237,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
<StepPanel loading={isLoading} title={ruleI18n.ABOUT}>
{aboutRuleData != null && (
<StepAboutRule
descriptionDirection="column"
descriptionDirection="row"
isReadOnlyView={true}
isLoading={false}
defaultValues={aboutRuleData}

View file

@ -57,6 +57,7 @@ export interface RuleStepData {
}
export interface RuleStepProps {
addPadding?: boolean;
descriptionDirection?: 'row' | 'column';
setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void;
isReadOnlyView: boolean;