[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:
Garrett Spong 2020-07-28 14:25:32 -06:00 committed by GitHub
parent 5e624502f8
commit 0b3dab7318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 294 additions and 215 deletions

View file

@ -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,
})
);

View file

@ -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');

View file

@ -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)`

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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: '',

View file

@ -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: [
{

View file

@ -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(

View file

@ -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'],

View file

@ -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) => ({

View file

@ -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: [
{

View file

@ -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

View file

@ -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 {

View file

@ -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 } };
}
}