[Security Solution][Detections] Fixes Risk Score and Severity mapping issues (#73233)
## Summary Fixes the following issues around Risk Score/Severity mapping: * Severity override option cannot be unselected during rule creation * Risk score override option cannot be unselected during rule creation * Cannot fill Critical Severity override at the first attempt * Cannot create a rule with just a Critical severity override Note: When editing rules there is the possibility of the mapping fields remaining `disabled` as they are locked to the 'isLoading' flag from the gql `useFetchIndexPatterns` call, which can sometimes not return/get stuck as loading. @patrykkopycinski has a draft PR to fix this here: https://github.com/elastic/kibana/pull/73199 cc @MadameSheema ##### Severity Mapping Fixes: <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/88497829-b653de00-cf7e-11ea-8e14-c351117b4282.gif" /> </p> Now distinguishes between empty string/value <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/88497776-94f2f200-cf7e-11ea-821e-3766b7bed3dc.png" /> </p> ##### Risk Score Mapping Fixes: <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/88497842-c075dc80-cf7e-11ea-8c41-606b20a6ac1c.gif" /> </p> ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [X] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials * Working with @benskelker on API docs. This PR adds `risk_score` (can be `undefined`) to `risk_score.mapping` for future compatibility with mapping to specific risk score values. - [X] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
This commit is contained in:
parent
5e624502f8
commit
0b3dab7318
|
@ -222,8 +222,9 @@ export const risk_score_mapping_value = t.string;
|
|||
export const risk_score_mapping_item = t.exact(
|
||||
t.type({
|
||||
field: risk_score_mapping_field,
|
||||
operator,
|
||||
value: risk_score_mapping_value,
|
||||
operator,
|
||||
risk_score: riskScoreOrUndefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -331,6 +331,7 @@ describe('helpers', () => {
|
|||
const result: ListItems[] = buildSeverityDescription({
|
||||
value: 'low',
|
||||
mapping: [{ field: 'host.name', operator: 'equals', value: 'hello', severity: 'high' }],
|
||||
isMappingChecked: true,
|
||||
});
|
||||
|
||||
expect(result[0].title).toEqual('Severity');
|
||||
|
|
|
@ -35,6 +35,7 @@ import { SeverityBadge } from '../severity_badge';
|
|||
import ListTreeIcon from './assets/list_tree_icon.svg';
|
||||
import { assertUnreachable } from '../../../../common/lib/helpers';
|
||||
import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types';
|
||||
import { defaultToEmptyTag } from '../../../../common/components/empty_value';
|
||||
|
||||
const NoteDescriptionContainer = styled(EuiFlexItem)`
|
||||
height: 105px;
|
||||
|
@ -236,35 +237,44 @@ export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems
|
|||
title: i18nSeverity.DEFAULT_SEVERITY,
|
||||
description: <SeverityBadge value={severity.value} />,
|
||||
},
|
||||
...severity.mapping.map((severityItem, index) => {
|
||||
return {
|
||||
title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '',
|
||||
description: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<OverrideColumn>
|
||||
<EuiToolTip
|
||||
content={severityItem.field}
|
||||
data-test-subj={`severityOverrideField${index}`}
|
||||
>
|
||||
<>{severityItem.field}</>
|
||||
</EuiToolTip>
|
||||
</OverrideColumn>
|
||||
<EuiToolTip content={severityItem.value} data-test-subj={`severityOverrideValue${index}`}>
|
||||
<>{severityItem.value}</>
|
||||
</EuiToolTip>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SeverityBadge
|
||||
data-test-subj={`severityOverrideSeverity${index}`}
|
||||
value={severityItem.severity}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
}),
|
||||
...(severity.isMappingChecked
|
||||
? severity.mapping
|
||||
.filter((severityItem) => severityItem.field !== '')
|
||||
.map((severityItem, index) => {
|
||||
return {
|
||||
title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '',
|
||||
description: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<OverrideColumn>
|
||||
<EuiToolTip
|
||||
content={severityItem.field}
|
||||
data-test-subj={`severityOverrideField${index}`}
|
||||
>
|
||||
<>{`${severityItem.field}:`}</>
|
||||
</EuiToolTip>
|
||||
</OverrideColumn>
|
||||
<OverrideColumn>
|
||||
<EuiToolTip
|
||||
content={severityItem.value}
|
||||
data-test-subj={`severityOverrideValue${index}`}
|
||||
>
|
||||
{defaultToEmptyTag(severityItem.value)}
|
||||
</EuiToolTip>
|
||||
</OverrideColumn>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SeverityBadge
|
||||
data-test-subj={`severityOverrideSeverity${index}`}
|
||||
value={severityItem.severity}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
})
|
||||
: []),
|
||||
];
|
||||
|
||||
export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListItems[] => [
|
||||
|
@ -272,27 +282,31 @@ export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListIt
|
|||
title: i18nRiskScore.RISK_SCORE,
|
||||
description: riskScore.value,
|
||||
},
|
||||
...riskScore.mapping.map((riskScoreItem, index) => {
|
||||
return {
|
||||
title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '',
|
||||
description: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={riskScoreItem.field}
|
||||
data-test-subj={`riskScoreOverrideField${index}`}
|
||||
>
|
||||
<>{riskScoreItem.field}</>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{'signal.rule.risk_score'}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
}),
|
||||
...(riskScore.isMappingChecked
|
||||
? riskScore.mapping
|
||||
.filter((riskScoreItem) => riskScoreItem.field !== '')
|
||||
.map((riskScoreItem, index) => {
|
||||
return {
|
||||
title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '',
|
||||
description: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<OverrideColumn>
|
||||
<EuiToolTip
|
||||
content={riskScoreItem.field}
|
||||
data-test-subj={`riskScoreOverrideField${index}`}
|
||||
>
|
||||
<>{riskScoreItem.field}</>
|
||||
</EuiToolTip>
|
||||
</OverrideColumn>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{'signal.rule.risk_score'}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
};
|
||||
})
|
||||
: []),
|
||||
];
|
||||
|
||||
const MyRefUrlLink = styled(EuiLink)`
|
||||
|
|
|
@ -14,8 +14,9 @@ import {
|
|||
EuiIcon,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { noop } from 'lodash/fp';
|
||||
import * as i18n from './translations';
|
||||
import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
import { CommonUseField } from '../../../../cases/components/create';
|
||||
|
@ -24,6 +25,10 @@ import { FieldComponent } from '../../../../common/components/autocomplete/field
|
|||
import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
const RiskScoreMappingEuiFormRow = styled(EuiFormRow)`
|
||||
width: 468px;
|
||||
`;
|
||||
|
||||
const NestedContent = styled.div`
|
||||
margin-left: 24px;
|
||||
`;
|
||||
|
@ -41,6 +46,7 @@ interface RiskScoreFieldProps {
|
|||
field: FieldHook;
|
||||
idAria: string;
|
||||
indices: IIndexPattern;
|
||||
isDisabled: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
|
@ -49,40 +55,23 @@ export const RiskScoreField = ({
|
|||
field,
|
||||
idAria,
|
||||
indices,
|
||||
isDisabled,
|
||||
placeholder,
|
||||
}: RiskScoreFieldProps) => {
|
||||
const [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked] = useState(false);
|
||||
const [initialFieldCheck, setInitialFieldCheck] = useState(true);
|
||||
|
||||
const fieldTypeFilter = useMemo(() => ['number'], []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isRiskScoreMappingChecked &&
|
||||
initialFieldCheck &&
|
||||
(field.value as AboutStepRiskScore).mapping?.length > 0
|
||||
) {
|
||||
setIsRiskScoreMappingChecked(true);
|
||||
setInitialFieldCheck(false);
|
||||
}
|
||||
}, [
|
||||
field,
|
||||
initialFieldCheck,
|
||||
isRiskScoreMappingChecked,
|
||||
setIsRiskScoreMappingChecked,
|
||||
setInitialFieldCheck,
|
||||
]);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
([newField]: IFieldType[]): void => {
|
||||
const values = field.value as AboutStepRiskScore;
|
||||
field.setValue({
|
||||
value: values.value,
|
||||
isMappingChecked: values.isMappingChecked,
|
||||
mapping: [
|
||||
{
|
||||
field: newField?.name ?? '',
|
||||
operator: 'equals',
|
||||
value: '',
|
||||
value: undefined,
|
||||
riskScore: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -99,8 +88,13 @@ export const RiskScoreField = ({
|
|||
}, [field.value, indices]);
|
||||
|
||||
const handleRiskScoreMappingChecked = useCallback(() => {
|
||||
setIsRiskScoreMappingChecked(!isRiskScoreMappingChecked);
|
||||
}, [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked]);
|
||||
const values = field.value as AboutStepRiskScore;
|
||||
field.setValue({
|
||||
value: values.value,
|
||||
mapping: [...values.mapping],
|
||||
isMappingChecked: !values.isMappingChecked,
|
||||
});
|
||||
}, [field]);
|
||||
|
||||
const riskScoreLabel = useMemo(() => {
|
||||
return (
|
||||
|
@ -117,11 +111,16 @@ export const RiskScoreField = ({
|
|||
const riskScoreMappingLabel = useMemo(() => {
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" onClick={handleRiskScoreMappingChecked}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
onClick={!isDisabled ? handleRiskScoreMappingChecked : noop}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id={`risk_score-mapping-override`}
|
||||
checked={isRiskScoreMappingChecked}
|
||||
checked={(field.value as AboutStepRiskScore).isMappingChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={handleRiskScoreMappingChecked}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -133,7 +132,7 @@ export const RiskScoreField = ({
|
|||
</NestedContent>
|
||||
</div>
|
||||
);
|
||||
}, [handleRiskScoreMappingChecked, isRiskScoreMappingChecked]);
|
||||
}, [field.value, handleRiskScoreMappingChecked, isDisabled]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
@ -153,6 +152,7 @@ export const RiskScoreField = ({
|
|||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleRiskScore',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleRiskScore',
|
||||
isDisabled,
|
||||
euiFieldProps: {
|
||||
max: 100,
|
||||
min: 0,
|
||||
|
@ -166,11 +166,11 @@ export const RiskScoreField = ({
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
<RiskScoreMappingEuiFormRow
|
||||
label={riskScoreMappingLabel}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={
|
||||
isRiskScoreMappingChecked ? (
|
||||
(field.value as AboutStepRiskScore).isMappingChecked ? (
|
||||
<NestedContent>{i18n.RISK_SCORE_MAPPING_DETAILS}</NestedContent>
|
||||
) : (
|
||||
''
|
||||
|
@ -184,7 +184,7 @@ export const RiskScoreField = ({
|
|||
>
|
||||
<NestedContent>
|
||||
<EuiSpacer size="s" />
|
||||
{isRiskScoreMappingChecked && (
|
||||
{(field.value as AboutStepRiskScore).isMappingChecked && (
|
||||
<EuiFlexGroup direction={'column'} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
|
@ -208,11 +208,11 @@ export const RiskScoreField = ({
|
|||
fieldTypeFilter={fieldTypeFilter}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isDisabled={isDisabled}
|
||||
onChange={handleFieldChange}
|
||||
data-test-subj={dataTestSubj}
|
||||
aria-label={idAria}
|
||||
fieldInputWidth={230}
|
||||
fieldInputWidth={270}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItemIconColumn grow={false}>
|
||||
|
@ -226,7 +226,7 @@ export const RiskScoreField = ({
|
|||
</EuiFlexGroup>
|
||||
)}
|
||||
</NestedContent>
|
||||
</EuiFormRow>
|
||||
</RiskScoreMappingEuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,8 @@ import {
|
|||
EuiIcon,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
|
@ -27,10 +28,16 @@ import {
|
|||
} from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
import { FieldComponent } from '../../../../common/components/autocomplete/field';
|
||||
import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match';
|
||||
import {
|
||||
Severity,
|
||||
SeverityMapping,
|
||||
SeverityMappingItem,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
const SeverityMappingParentContainer = styled(EuiFlexItem)`
|
||||
max-width: 471px;
|
||||
const SeverityMappingEuiFormRow = styled(EuiFormRow)`
|
||||
width: 468px;
|
||||
`;
|
||||
|
||||
const NestedContent = styled.div`
|
||||
margin-left: 24px;
|
||||
`;
|
||||
|
@ -48,6 +55,7 @@ interface SeverityFieldProps {
|
|||
field: FieldHook;
|
||||
idAria: string;
|
||||
indices: IIndexPattern;
|
||||
isDisabled: boolean;
|
||||
options: SeverityOptionItem[];
|
||||
}
|
||||
|
||||
|
@ -56,42 +64,20 @@ export const SeverityField = ({
|
|||
field,
|
||||
idAria,
|
||||
indices,
|
||||
isDisabled,
|
||||
options,
|
||||
}: SeverityFieldProps) => {
|
||||
const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false);
|
||||
const [initialFieldCheck, setInitialFieldCheck] = useState(true);
|
||||
const fieldValueInputWidth = 160;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isSeverityMappingChecked &&
|
||||
initialFieldCheck &&
|
||||
(field.value as AboutStepSeverity).mapping?.length > 0
|
||||
) {
|
||||
setIsSeverityMappingChecked(true);
|
||||
setInitialFieldCheck(false);
|
||||
}
|
||||
}, [
|
||||
field,
|
||||
initialFieldCheck,
|
||||
isSeverityMappingChecked,
|
||||
setIsSeverityMappingChecked,
|
||||
setInitialFieldCheck,
|
||||
]);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(index: number, severity: string, [newField]: IFieldType[]): void => {
|
||||
const handleFieldValueChange = useCallback(
|
||||
(newMappingItems: SeverityMapping, index: number): void => {
|
||||
const values = field.value as AboutStepSeverity;
|
||||
field.setValue({
|
||||
value: values.value,
|
||||
isMappingChecked: values.isMappingChecked,
|
||||
mapping: [
|
||||
...values.mapping.slice(0, index),
|
||||
{
|
||||
...values.mapping[index],
|
||||
field: newField?.name ?? '',
|
||||
operator: 'equals',
|
||||
severity,
|
||||
},
|
||||
...newMappingItems,
|
||||
...values.mapping.slice(index + 1),
|
||||
],
|
||||
});
|
||||
|
@ -99,40 +85,59 @@ export const SeverityField = ({
|
|||
[field]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(index: number, severity: Severity, [newField]: IFieldType[]): void => {
|
||||
const values = field.value as AboutStepSeverity;
|
||||
const newMappingItems: SeverityMapping = [
|
||||
{
|
||||
...values.mapping[index],
|
||||
field: newField?.name ?? '',
|
||||
value: newField != null ? values.mapping[index].value : '',
|
||||
operator: 'equals',
|
||||
severity,
|
||||
},
|
||||
];
|
||||
handleFieldValueChange(newMappingItems, index);
|
||||
},
|
||||
[field, handleFieldValueChange]
|
||||
);
|
||||
|
||||
const handleFieldMatchValueChange = useCallback(
|
||||
(index: number, severity: string, newMatchValue: string): void => {
|
||||
(index: number, severity: Severity, newMatchValue: string): void => {
|
||||
const values = field.value as AboutStepSeverity;
|
||||
field.setValue({
|
||||
value: values.value,
|
||||
mapping: [
|
||||
...values.mapping.slice(0, index),
|
||||
{
|
||||
...values.mapping[index],
|
||||
value: newMatchValue,
|
||||
operator: 'equals',
|
||||
severity,
|
||||
},
|
||||
...values.mapping.slice(index + 1),
|
||||
],
|
||||
});
|
||||
const newMappingItems: SeverityMapping = [
|
||||
{
|
||||
...values.mapping[index],
|
||||
field: values.mapping[index].field,
|
||||
value:
|
||||
values.mapping[index].field != null && values.mapping[index].field !== ''
|
||||
? newMatchValue
|
||||
: '',
|
||||
operator: 'equals',
|
||||
severity,
|
||||
},
|
||||
];
|
||||
handleFieldValueChange(newMappingItems, index);
|
||||
},
|
||||
[field]
|
||||
[field, handleFieldValueChange]
|
||||
);
|
||||
|
||||
const selectedState = useMemo(() => {
|
||||
return (
|
||||
(field.value as AboutStepSeverity).mapping?.map((mapping) => {
|
||||
const [newSelectedField] = indices.fields.filter(
|
||||
({ name }) => mapping.field != null && mapping.field === name
|
||||
);
|
||||
return { field: newSelectedField, value: mapping.value };
|
||||
}) ?? []
|
||||
);
|
||||
}, [field.value, indices]);
|
||||
const getIFieldTypeFromFieldName = (
|
||||
fieldName: string | undefined,
|
||||
iIndexPattern: IIndexPattern
|
||||
): IFieldType | undefined => {
|
||||
const [iFieldType] = iIndexPattern.fields.filter(({ name }) => fieldName === name);
|
||||
return iFieldType;
|
||||
};
|
||||
|
||||
const handleSeverityMappingSelected = useCallback(() => {
|
||||
setIsSeverityMappingChecked(!isSeverityMappingChecked);
|
||||
}, [isSeverityMappingChecked, setIsSeverityMappingChecked]);
|
||||
const handleSeverityMappingChecked = useCallback(() => {
|
||||
const values = field.value as AboutStepSeverity;
|
||||
field.setValue({
|
||||
value: values.value,
|
||||
mapping: [...values.mapping],
|
||||
isMappingChecked: !values.isMappingChecked,
|
||||
});
|
||||
}, [field]);
|
||||
|
||||
const severityLabel = useMemo(() => {
|
||||
return (
|
||||
|
@ -149,12 +154,17 @@ export const SeverityField = ({
|
|||
const severityMappingLabel = useMemo(() => {
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" onClick={handleSeverityMappingSelected}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
onClick={!isDisabled ? handleSeverityMappingChecked : noop}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id={`severity-mapping-override`}
|
||||
checked={isSeverityMappingChecked}
|
||||
onChange={handleSeverityMappingSelected}
|
||||
checked={(field.value as AboutStepSeverity).isMappingChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={handleSeverityMappingChecked}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{i18n.SEVERITY_MAPPING}</EuiFlexItem>
|
||||
|
@ -165,7 +175,7 @@ export const SeverityField = ({
|
|||
</NestedContent>
|
||||
</div>
|
||||
);
|
||||
}, [handleSeverityMappingSelected, isSeverityMappingChecked]);
|
||||
}, [field.value, handleSeverityMappingChecked, isDisabled]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
@ -185,6 +195,7 @@ export const SeverityField = ({
|
|||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleSeverity',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleSeverity',
|
||||
isDisabled,
|
||||
euiFieldProps: {
|
||||
fullWidth: false,
|
||||
disabled: false,
|
||||
|
@ -195,12 +206,12 @@ export const SeverityField = ({
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<SeverityMappingParentContainer>
|
||||
<EuiFormRow
|
||||
<EuiFlexItem>
|
||||
<SeverityMappingEuiFormRow
|
||||
label={severityMappingLabel}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={
|
||||
isSeverityMappingChecked ? (
|
||||
(field.value as AboutStepSeverity).isMappingChecked ? (
|
||||
<NestedContent>{i18n.SEVERITY_MAPPING_DETAILS}</NestedContent>
|
||||
) : (
|
||||
''
|
||||
|
@ -214,7 +225,7 @@ export const SeverityField = ({
|
|||
>
|
||||
<NestedContent>
|
||||
<EuiSpacer size="s" />
|
||||
{isSeverityMappingChecked && (
|
||||
{(field.value as AboutStepSeverity).isMappingChecked && (
|
||||
<EuiFlexGroup direction={'column'} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
|
@ -231,53 +242,72 @@ export const SeverityField = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{options.map((option, index) => (
|
||||
<EuiFlexItem key={option.value}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<FieldComponent
|
||||
placeholder={''}
|
||||
selectedField={selectedState[index]?.field ?? ''}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
indexPattern={indices}
|
||||
fieldInputWidth={fieldValueInputWidth}
|
||||
onChange={handleFieldChange.bind(null, index, option.value)}
|
||||
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingField${option.value}`}
|
||||
aria-label={`detectionEngineStepAboutRuleSeverityMappingField${option.value}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{(field.value as AboutStepSeverity).mapping.map(
|
||||
(severityMappingItem: SeverityMappingItem, index) => (
|
||||
<EuiFlexItem key={`${severityMappingItem.severity}-${index}`}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<FieldComponent
|
||||
placeholder={''}
|
||||
selectedField={getIFieldTypeFromFieldName(
|
||||
severityMappingItem.field,
|
||||
indices
|
||||
)}
|
||||
isLoading={false}
|
||||
isDisabled={isDisabled}
|
||||
isClearable={false}
|
||||
indexPattern={indices}
|
||||
fieldInputWidth={fieldValueInputWidth}
|
||||
onChange={handleFieldChange.bind(
|
||||
null,
|
||||
index,
|
||||
severityMappingItem.severity
|
||||
)}
|
||||
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingField-${severityMappingItem.severity}-${index}`}
|
||||
aria-label={`detectionEngineStepAboutRuleSeverityMappingField-${severityMappingItem.severity}-${index}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder={''}
|
||||
selectedField={selectedState[index]?.field ?? ''}
|
||||
selectedValue={selectedState[index]?.value ?? ''}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
indexPattern={indices}
|
||||
fieldInputWidth={fieldValueInputWidth}
|
||||
onChange={handleFieldMatchValueChange.bind(null, index, option.value)}
|
||||
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingValue${option.value}`}
|
||||
aria-label={`detectionEngineStepAboutRuleSeverityMappingValue${option.value}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItemIconColumn grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItemIconColumn>
|
||||
<EuiFlexItemSeverityColumn grow={false}>
|
||||
{option.inputDisplay}
|
||||
</EuiFlexItemSeverityColumn>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
<EuiFlexItem>
|
||||
<AutocompleteFieldMatchComponent
|
||||
placeholder={''}
|
||||
selectedField={getIFieldTypeFromFieldName(
|
||||
severityMappingItem.field,
|
||||
indices
|
||||
)}
|
||||
selectedValue={severityMappingItem.value}
|
||||
isClearable={false}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={false}
|
||||
indexPattern={indices}
|
||||
fieldInputWidth={fieldValueInputWidth}
|
||||
onChange={handleFieldMatchValueChange.bind(
|
||||
null,
|
||||
index,
|
||||
severityMappingItem.severity
|
||||
)}
|
||||
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingValue-${severityMappingItem.severity}-${index}`}
|
||||
aria-label={`detectionEngineStepAboutRuleSeverityMappingValue-${severityMappingItem.severity}-${index}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItemIconColumn grow={false}>
|
||||
<EuiIcon type={'sortRight'} />
|
||||
</EuiFlexItemIconColumn>
|
||||
<EuiFlexItemSeverityColumn grow={false}>
|
||||
{
|
||||
options.find((o) => o.value === severityMappingItem.severity)
|
||||
?.inputDisplay
|
||||
}
|
||||
</EuiFlexItemSeverityColumn>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</NestedContent>
|
||||
</EuiFormRow>
|
||||
</SeverityMappingParentContainer>
|
||||
</SeverityMappingEuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers';
|
||||
|
||||
export const threatDefault = [
|
||||
{
|
||||
|
@ -21,8 +22,8 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
isAssociatedToEndpointList: false,
|
||||
isBuildingBlock: false,
|
||||
isNew: true,
|
||||
severity: { value: 'low', mapping: [] },
|
||||
riskScore: { value: 50, mapping: [] },
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
riskScore: { value: 50, mapping: [], isMappingChecked: false },
|
||||
references: [''],
|
||||
falsePositives: [''],
|
||||
license: '',
|
||||
|
|
|
@ -16,6 +16,7 @@ import { stepAboutDefaultValue } from './default_value';
|
|||
// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
|
||||
import { wait as waitFor } from '@testing-library/react';
|
||||
import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers';
|
||||
|
||||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
|
||||
|
@ -176,8 +177,8 @@ describe('StepAboutRuleComponent', () => {
|
|||
name: 'Test name text',
|
||||
note: '',
|
||||
references: [''],
|
||||
riskScore: { value: 50, mapping: [] },
|
||||
severity: { value: 'low', mapping: [] },
|
||||
riskScore: { value: 50, mapping: [], isMappingChecked: false },
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
tags: [],
|
||||
threat: [
|
||||
{
|
||||
|
@ -236,8 +237,8 @@ describe('StepAboutRuleComponent', () => {
|
|||
name: 'Test name text',
|
||||
note: '',
|
||||
references: [''],
|
||||
riskScore: { value: 80, mapping: [] },
|
||||
severity: { value: 'low', mapping: [] },
|
||||
riskScore: { value: 80, mapping: [], isMappingChecked: false },
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
tags: [],
|
||||
threat: [
|
||||
{
|
||||
|
|
|
@ -117,18 +117,16 @@ export const schema: FormSchema = {
|
|||
},
|
||||
],
|
||||
},
|
||||
mapping: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
mapping: {},
|
||||
isMappingChecked: {},
|
||||
},
|
||||
riskScore: {
|
||||
value: {
|
||||
type: FIELD_TYPES.RANGE,
|
||||
serializer: (input: string) => Number(input),
|
||||
},
|
||||
mapping: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
mapping: {},
|
||||
isMappingChecked: {},
|
||||
},
|
||||
references: {
|
||||
label: i18n.translate(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Rule, RuleError } from '../../../../../containers/detection_engine/rule
|
|||
import { List } from '../../../../../../../common/detection_engine/schemas/types';
|
||||
import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types';
|
||||
import { FieldValueQueryBar } from '../../../../../components/rules/query_bar';
|
||||
import { fillEmptySeverityMappings } from '../../helpers';
|
||||
|
||||
export const mockQueryBar: FieldValueQueryBar = {
|
||||
query: {
|
||||
|
@ -175,8 +176,8 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({
|
|||
license: 'Elastic License',
|
||||
name: 'Query with rule-id',
|
||||
description: '24/7',
|
||||
severity: { value: 'low', mapping: [] },
|
||||
riskScore: { value: 21, mapping: [] },
|
||||
riskScore: { value: 21, mapping: [], isMappingChecked: false },
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
references: ['www.test.co'],
|
||||
falsePositives: ['test'],
|
||||
tags: ['tag1', 'tag2'],
|
||||
|
|
|
@ -189,10 +189,14 @@ export const formatAboutStepData = (
|
|||
false_positives: falsePositives.filter((item) => !isEmpty(item)),
|
||||
references: references.filter((item) => !isEmpty(item)),
|
||||
risk_score: riskScore.value,
|
||||
risk_score_mapping: riskScore.mapping,
|
||||
risk_score_mapping: riskScore.isMappingChecked
|
||||
? riskScore.mapping.filter((m) => m.field != null && m.field !== '')
|
||||
: [],
|
||||
rule_name_override: ruleNameOverride !== '' ? ruleNameOverride : undefined,
|
||||
severity: severity.value,
|
||||
severity_mapping: severity.mapping,
|
||||
severity_mapping: severity.isMappingChecked
|
||||
? severity.mapping.filter((m) => m.field != null && m.field !== '' && m.value != null)
|
||||
: [],
|
||||
threat: threat
|
||||
.filter((singleThreat) => singleThreat.tactic.name !== 'none')
|
||||
.map((singleThreat) => ({
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
getPrePackagedTimelineStatus,
|
||||
determineDetailsValue,
|
||||
userHasNoPermissions,
|
||||
fillEmptySeverityMappings,
|
||||
} from './helpers';
|
||||
import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock';
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/public';
|
||||
|
@ -97,9 +98,9 @@ describe('rule helpers', () => {
|
|||
name: 'Query with rule-id',
|
||||
note: '# this is some markdown documentation',
|
||||
references: ['www.test.co'],
|
||||
riskScore: { value: 21, mapping: [] },
|
||||
riskScore: { value: 21, mapping: [], isMappingChecked: false },
|
||||
ruleNameOverride: 'message',
|
||||
severity: { value: 'low', mapping: [] },
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: [
|
||||
{
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from './types';
|
||||
import { SeverityMapping } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { severityOptions } from '../../../components/rules/step_about_rule/data';
|
||||
|
||||
export interface GetStepsData {
|
||||
aboutRuleData: AboutStepRule;
|
||||
|
@ -150,18 +152,38 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
|
|||
references,
|
||||
severity: {
|
||||
value: severity,
|
||||
mapping: severityMapping,
|
||||
mapping: fillEmptySeverityMappings(severityMapping),
|
||||
isMappingChecked: severityMapping.length > 0,
|
||||
},
|
||||
tags,
|
||||
riskScore: {
|
||||
value: riskScore,
|
||||
mapping: riskScoreMapping,
|
||||
isMappingChecked: riskScoreMapping.length > 0,
|
||||
},
|
||||
falsePositives,
|
||||
threat: threat as IMitreEnterpriseAttack[],
|
||||
};
|
||||
};
|
||||
|
||||
const severitySortMapping = {
|
||||
low: 0,
|
||||
medium: 1,
|
||||
high: 2,
|
||||
critical: 3,
|
||||
};
|
||||
|
||||
export const fillEmptySeverityMappings = (mappings: SeverityMapping): SeverityMapping => {
|
||||
const missingMappings: SeverityMapping = severityOptions.flatMap((so) =>
|
||||
mappings.find((mapping) => mapping.severity === so.value) == null
|
||||
? [{ field: '', value: '', operator: 'equals', severity: so.value }]
|
||||
: []
|
||||
);
|
||||
return [...mappings, ...missingMappings].sort(
|
||||
(a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity]
|
||||
);
|
||||
};
|
||||
|
||||
export const determineDetailsValue = (
|
||||
rule: Rule,
|
||||
detailsView: boolean
|
||||
|
|
|
@ -88,11 +88,13 @@ export interface AboutStepRuleDetails {
|
|||
export interface AboutStepSeverity {
|
||||
value: string;
|
||||
mapping: SeverityMapping;
|
||||
isMappingChecked: boolean;
|
||||
}
|
||||
|
||||
export interface AboutStepRiskScore {
|
||||
value: number;
|
||||
mapping: RiskScoreMapping;
|
||||
isMappingChecked: boolean;
|
||||
}
|
||||
|
||||
export interface DefineStepRule extends StepRuleData {
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
RiskScoreMappingOrUndefined,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { SignalSourceHit } from '../types';
|
||||
import { RiskScore as RiskScoreIOTS } from '../../../../../common/detection_engine/schemas/types';
|
||||
|
||||
interface BuildRiskScoreFromMappingProps {
|
||||
doc: SignalSourceHit;
|
||||
|
@ -33,8 +32,12 @@ export const buildRiskScoreFromMapping = ({
|
|||
const mappedField = riskScoreMapping[0].field;
|
||||
// TODO: Expand by verifying fieldType from index via doc._index
|
||||
const mappedValue = get(mappedField, doc._source);
|
||||
// TODO: This doesn't seem to validate...identified riskScore > 100 😬
|
||||
if (RiskScoreIOTS.is(mappedValue)) {
|
||||
if (
|
||||
typeof mappedValue === 'number' &&
|
||||
Number.isSafeInteger(mappedValue) &&
|
||||
mappedValue >= 0 &&
|
||||
mappedValue <= 100
|
||||
) {
|
||||
return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue