[Security Solution][Detection Rules] Makes threat techniques optional (#85481)

This commit is contained in:
Davis Plumlee 2020-12-15 12:08:15 -05:00 committed by GitHub
parent 07f395f7dd
commit 13e5e55901
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 104 additions and 223 deletions

View file

@ -413,6 +413,7 @@ export const threat_tactic = t.type({
name: threat_tactic_name, name: threat_tactic_name,
reference: threat_tactic_reference, reference: threat_tactic_reference,
}); });
export type ThreatTactic = t.TypeOf<typeof threat_tactic>;
export const threat_subtechnique_id = t.string; export const threat_subtechnique_id = t.string;
export const threat_subtechnique_name = t.string; export const threat_subtechnique_name = t.string;
export const threat_subtechnique_reference = t.string; export const threat_subtechnique_reference = t.string;
@ -421,6 +422,7 @@ export const threat_subtechnique = t.type({
name: threat_subtechnique_name, name: threat_subtechnique_name,
reference: threat_subtechnique_reference, reference: threat_subtechnique_reference,
}); });
export type ThreatSubtechnique = t.TypeOf<typeof threat_subtechnique>;
export const threat_subtechniques = t.array(threat_subtechnique); export const threat_subtechniques = t.array(threat_subtechnique);
export const threat_technique_id = t.string; export const threat_technique_id = t.string;
export const threat_technique_name = t.string; export const threat_technique_name = t.string;
@ -439,21 +441,22 @@ export const threat_technique = t.intersection([
}) })
), ),
]); ]);
export type ThreatTechnique = t.TypeOf<typeof threat_technique>;
export const threat_techniques = t.array(threat_technique); export const threat_techniques = t.array(threat_technique);
export const threat = t.array( export const threat = t.exact(
t.exact( t.type({
t.type({ framework: threat_framework,
framework: threat_framework, tactic: threat_tactic,
tactic: threat_tactic, technique: threat_techniques,
technique: threat_techniques, })
})
)
); );
export type Threat = t.TypeOf<typeof threat>; export type Threat = t.TypeOf<typeof threat>;
export const threatOrUndefined = t.union([threat, t.undefined]); export const threats = t.array(threat);
export type ThreatOrUndefined = t.TypeOf<typeof threatOrUndefined>; export type Threats = t.TypeOf<typeof threats>;
export const threatsOrUndefined = t.union([threats, t.undefined]);
export type ThreatsOrUndefined = t.TypeOf<typeof threatsOrUndefined>;
export const threshold = t.exact( export const threshold = t.exact(
t.type({ t.type({

View file

@ -23,7 +23,7 @@ import {
Tags, Tags,
To, To,
type, type,
Threat, Threats,
threshold, threshold,
ThrottleOrNull, ThrottleOrNull,
note, note,
@ -171,7 +171,7 @@ export type AddPrepackagedRulesSchemaDecoded = Omit<
severity_mapping: SeverityMapping; severity_mapping: SeverityMapping;
tags: Tags; tags: Tags;
to: To; to: To;
threat: Threat; threat: Threats;
throttle: ThrottleOrNull; throttle: ThrottleOrNull;
exceptions_list: ListArray; exceptions_list: ListArray;
}; };

View file

@ -25,7 +25,7 @@ import {
Tags, Tags,
To, To,
type, type,
Threat, Threats,
threshold, threshold,
ThrottleOrNull, ThrottleOrNull,
note, note,
@ -193,7 +193,7 @@ export type ImportRulesSchemaDecoded = Omit<
severity_mapping: SeverityMapping; severity_mapping: SeverityMapping;
tags: Tags; tags: Tags;
to: To; to: To;
threat: Threat; threat: Threats;
throttle: ThrottleOrNull; throttle: ThrottleOrNull;
version: Version; version: Version;
exceptions_list: ListArray; exceptions_list: ListArray;

View file

@ -31,7 +31,7 @@ import {
from, from,
enabled, enabled,
tags, tags,
threat, threats,
threshold, threshold,
throttle, throttle,
references, references,
@ -98,7 +98,7 @@ export const patchRulesSchema = t.exact(
severity_mapping, severity_mapping,
tags, tags,
to, to,
threat, threat: threats,
threshold, threshold,
throttle, throttle,
timestamp_override, timestamp_override,

View file

@ -42,7 +42,7 @@ import {
max_signals, max_signals,
risk_score, risk_score,
severity, severity,
threat, threats,
to, to,
references, references,
version, version,
@ -167,7 +167,7 @@ const commonParams = {
max_signals, max_signals,
risk_score_mapping, risk_score_mapping,
severity_mapping, severity_mapping,
threat, threat: threats,
to, to,
references, references,
version, version,

View file

@ -43,7 +43,7 @@ import {
timeline_id, timeline_id,
timeline_title, timeline_title,
type, type,
threat, threats,
threshold, threshold,
throttle, throttle,
job_status, job_status,
@ -106,7 +106,7 @@ export const requiredRulesSchema = t.type({
tags, tags,
to, to,
type, type,
threat, threat: threats,
created_at, created_at,
updated_at, updated_at,
created_by, created_by,

View file

@ -8,11 +8,11 @@ import { DefaultThreatArray } from './default_threat_array';
import { pipe } from 'fp-ts/lib/pipeable'; import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either'; import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils'; import { foldLeftRight, getPaths } from '../../../test_utils';
import { Threat } from '../common/schemas'; import { Threats } from '../common/schemas';
describe('default_threat_null', () => { describe('default_threat_null', () => {
test('it should validate an empty array', () => { test('it should validate an empty array', () => {
const payload: Threat = []; const payload: Threats = [];
const decoded = DefaultThreatArray.decode(payload); const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight); const message = pipe(decoded, foldLeftRight);
@ -21,7 +21,7 @@ describe('default_threat_null', () => {
}); });
test('it should validate an array of threats', () => { test('it should validate an array of threats', () => {
const payload: Threat = [ const payload: Threats = [
{ {
framework: 'MITRE ATTACK', framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],

View file

@ -6,16 +6,16 @@
import * as t from 'io-ts'; import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either'; import { Either } from 'fp-ts/lib/Either';
import { Threat, threat } from '../common/schemas'; import { Threats, threats } from '../common/schemas';
/** /**
* Types the DefaultThreatArray as: * Types the DefaultThreatArray as:
* - If null or undefined, then an empty array will be set * - If null or undefined, then an empty array will be set
*/ */
export const DefaultThreatArray = new t.Type<Threat, Threat | undefined, unknown>( export const DefaultThreatArray = new t.Type<Threats, Threats | undefined, unknown>(
'DefaultThreatArray', 'DefaultThreatArray',
threat.is, threats.is,
(input, context): Either<t.Errors, Threat> => (input, context): Either<t.Errors, Threats> =>
input == null ? t.success([]) : threat.validate(input, context), input == null ? t.success([]) : threats.validate(input, context),
t.identity t.identity
); );

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Threat } from '../common/schemas'; import { Threats } from '../common/schemas';
export const getThreatMock = (): Threat => [ export const getThreatMock = (): Threats => [
{ {
framework: 'MITRE ATT&CK', framework: 'MITRE ATT&CK',
tactic: { tactic: {

View file

@ -18,11 +18,7 @@ import {
} from '../../../../../../../../src/plugins/data/public'; } from '../../../../../../../../src/plugins/data/public';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import { useKibana } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../common/lib/kibana';
import { import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types';
AboutStepRiskScore,
AboutStepSeverity,
IMitreEnterpriseAttack,
} from '../../../pages/detection_engine/rules/types';
import { FieldValueTimeline } from '../pick_timeline'; import { FieldValueTimeline } from '../pick_timeline';
import { FormSchema } from '../../../../shared_imports'; import { FormSchema } from '../../../../shared_imports';
import { ListItems } from './types'; import { ListItems } from './types';
@ -42,7 +38,7 @@ import {
import { buildMlJobDescription } from './ml_job_description'; import { buildMlJobDescription } from './ml_job_description';
import { buildActionsDescription } from './actions_description'; import { buildActionsDescription } from './actions_description';
import { buildThrottleDescription } from './throttle_description'; import { buildThrottleDescription } from './throttle_description';
import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Threats, Type } from '../../../../../common/detection_engine/schemas/common/schemas';
import { THREAT_QUERY_LABEL } from './translations'; import { THREAT_QUERY_LABEL } from './translations';
import { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers'; import { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers';
@ -179,7 +175,7 @@ export const getDescriptionItem = (
indexPatterns, indexPatterns,
}); });
} else if (field === 'threat') { } else if (field === 'threat') {
const threats: IMitreEnterpriseAttack[] = get(field, data); const threats: Threats = get(field, data);
return buildThreatDescription({ label, threat: filterEmptyThreats(threats) }); return buildThreatDescription({ label, threat: filterEmptyThreats(threats) });
} else if (field === 'threshold') { } else if (field === 'threshold') {
const threshold = get(field, data); const threshold = get(field, data);

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
import { import {
IIndexPattern, IIndexPattern,
Filter, Filter,
FilterManager, FilterManager,
} from '../../../../../../../../src/plugins/data/public'; } from '../../../../../../../../src/plugins/data/public';
import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
export interface ListItems { export interface ListItems {
title: NonNullable<ReactNode>; title: NonNullable<ReactNode>;
@ -29,5 +29,5 @@ export interface BuildQueryBarDescription {
export interface BuildThreatDescription { export interface BuildThreatDescription {
label: string; label: string;
threat: IMitreEnterpriseAttack[]; threat: Threats;
} }

View file

@ -5,51 +5,11 @@
*/ */
import { getValidThreat } from '../../../mitre/valid_threat_mock'; import { getValidThreat } from '../../../mitre/valid_threat_mock';
import { hasSubtechniqueOptions, isMitreAttackInvalid } from './helpers'; import { hasSubtechniqueOptions } from './helpers';
const mockTechniques = getValidThreat()[0].technique; const mockTechniques = getValidThreat()[0].technique;
describe('helpers', () => { describe('helpers', () => {
describe('isMitreAttackInvalid', () => {
describe('when technique param is undefined', () => {
it('returns false', () => {
expect(isMitreAttackInvalid('', undefined)).toBe(false);
});
});
describe('when technique param is empty', () => {
it('returns false if tacticName is `none`', () => {
expect(isMitreAttackInvalid('none', [])).toBe(false);
});
it('returns true if tacticName exists and is not `none`', () => {
expect(isMitreAttackInvalid('Test', [])).toBe(true);
});
});
describe('when technique param exists', () => {
describe('and contains valid techniques', () => {
const validTechniques = mockTechniques;
it('returns false', () => {
expect(isMitreAttackInvalid('Test', validTechniques)).toBe(false);
});
});
describe('and contains empty techniques', () => {
const emptyTechniques = [
{
reference: 'https://test.com',
name: 'none',
id: '',
},
];
it('returns true', () => {
expect(isMitreAttackInvalid('Test', emptyTechniques)).toBe(true);
});
});
});
});
describe('hasSubtechniqueOptions', () => { describe('hasSubtechniqueOptions', () => {
describe('when technique has subtechnique options', () => { describe('when technique has subtechnique options', () => {
const technique = mockTechniques[0]; const technique = mockTechniques[0];

View file

@ -3,32 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { isEmpty } from 'lodash/fp'; import { ThreatTechnique } from '../../../../../common/detection_engine/schemas/common/schemas';
import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import { IMitreAttackTechnique } from '../../../pages/detection_engine/rules/types';
export const isMitreAttackInvalid = (
tacticName: string | null | undefined,
technique: IMitreAttackTechnique[] | null | undefined
) => {
if (
tacticName !== 'none' &&
technique != null &&
(isEmpty(technique) || !containsTechniques(technique))
) {
return true;
}
return false;
};
const containsTechniques = (techniques: IMitreAttackTechnique[]) => {
return techniques.some((technique) => technique.name !== 'none');
};
/** /**
* Returns true if the given mitre technique has any subtechniques * Returns true if the given mitre technique has any subtechniques
*/ */
export const hasSubtechniqueOptions = (technique: IMitreAttackTechnique) => { export const hasSubtechniqueOptions = (technique: ThreatTechnique) => {
return subtechniquesOptions.some((subtechnique) => subtechnique.techniqueId === technique.id); return subtechniquesOptions.some((subtechnique) => subtechnique.techniqueId === technique.id);
}; };

View file

@ -6,19 +6,18 @@
import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty, camelCase } from 'lodash/fp'; import { isEmpty, camelCase } from 'lodash/fp';
import React, { memo, useCallback, useMemo, useState } from 'react'; import React, { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { Threat, Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques'; import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import { FieldHook } from '../../../../shared_imports';
import { threatDefault } from '../step_about_rule/default_value'; import { threatDefault } from '../step_about_rule/default_value';
import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form'; import { MyAddItemButton } from '../add_item_form';
import * as i18n from './translations'; import * as i18n from './translations';
import { MitreAttackTechniqueFields } from './technique_fields'; import { MitreAttackTechniqueFields } from './technique_fields';
import { isMitreAttackInvalid } from './helpers';
const MitreAttackContainer = styled.div` const MitreAttackContainer = styled.div`
margin-top: 16px; margin-top: 16px;
@ -40,12 +39,9 @@ interface AddItemProps {
} }
export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItemProps) => { export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItemProps) => {
const [showValidation, setShowValidation] = useState(false);
const { errorMessage } = getFieldValidityAndErrorMessage(field);
const removeTactic = useCallback( const removeTactic = useCallback(
(index: number) => { (index: number) => {
const values = [...(field.value as IMitreEnterpriseAttack[])]; const values = [...(field.value as Threats)];
values.splice(index, 1); values.splice(index, 1);
if (isEmpty(values)) { if (isEmpty(values)) {
field.setValue(threatDefault); field.setValue(threatDefault);
@ -57,7 +53,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
); );
const addMitreAttackTactic = useCallback(() => { const addMitreAttackTactic = useCallback(() => {
const values = [...(field.value as IMitreEnterpriseAttack[])]; const values = [...(field.value as Threats)];
if (!isEmpty(values[values.length - 1])) { if (!isEmpty(values[values.length - 1])) {
field.setValue([ field.setValue([
...values, ...values,
@ -70,7 +66,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
const updateTactic = useCallback( const updateTactic = useCallback(
(index: number, value: string) => { (index: number, value: string) => {
const values = [...(field.value as IMitreEnterpriseAttack[])]; const values = [...(field.value as Threats)];
const { id, reference, name } = tacticsOptions.find((t) => t.value === value) || { const { id, reference, name } = tacticsOptions.find((t) => t.value === value) || {
id: '', id: '',
name: '', name: '',
@ -87,15 +83,11 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
); );
const values = useMemo(() => { const values = useMemo(() => {
return [...(field.value as IMitreEnterpriseAttack[])]; return [...(field.value as Threats)];
}, [field]); }, [field]);
const isTacticValid = useCallback((threat: IMitreEnterpriseAttack) => {
return isMitreAttackInvalid(threat.tactic.name, threat.technique);
}, []);
const getSelectTactic = useCallback( const getSelectTactic = useCallback(
(threat: IMitreEnterpriseAttack, index: number, disabled: boolean) => { (threat: Threat, index: number, disabled: boolean) => {
const tacticName = threat.tactic.name; const tacticName = threat.tactic.name;
return ( return (
<EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexGroup gutterSize="s" alignItems="center">
@ -125,8 +117,6 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
valueOfSelected={camelCase(tacticName)} valueOfSelected={camelCase(tacticName)}
data-test-subj="mitreAttackTactic" data-test-subj="mitreAttackTactic"
placeholder={i18n.TACTIC_PLACEHOLDER} placeholder={i18n.TACTIC_PLACEHOLDER}
isInvalid={showValidation && isTacticValid(threat)}
onBlur={() => setShowValidation(true)}
/> />
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
@ -141,7 +131,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
</EuiFlexGroup> </EuiFlexGroup>
); );
}, },
[field, isDisabled, removeTactic, showValidation, updateTactic, values, isTacticValid] [field, isDisabled, removeTactic, updateTactic, values]
); );
/** /**
@ -150,7 +140,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
* Value is memoized on top level props, any deep changes will have to be new objects * Value is memoized on top level props, any deep changes will have to be new objects
*/ */
const onFieldChange = useCallback( const onFieldChange = useCallback(
(threats: IMitreEnterpriseAttack[]) => { (threats: Threats) => {
field.setValue(threats); field.setValue(threats);
}, },
[field] [field]
@ -166,16 +156,12 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
label={`${field.label} ${i18n.THREATS}`} label={`${field.label} ${i18n.THREATS}`}
labelAppend={field.labelAppend} labelAppend={field.labelAppend}
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined} describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
isInvalid={showValidation && isTacticValid(threat)}
error={errorMessage}
> >
<>{getSelectTactic(threat, index, isDisabled)}</> <>{getSelectTactic(threat, index, isDisabled)}</>
</InitialMitreAttackFormRow> </InitialMitreAttackFormRow>
) : ( ) : (
<EuiFormRow <EuiFormRow
fullWidth fullWidth
isInvalid={showValidation && isTacticValid(threat)}
error={errorMessage}
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined} describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
> >
{getSelectTactic(threat, index, isDisabled)} {getSelectTactic(threat, index, isDisabled)}

View file

@ -16,10 +16,13 @@ import { camelCase } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {
Threats,
ThreatSubtechnique,
} from '../../../../../common/detection_engine/schemas/common/schemas';
import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
import { FieldHook } from '../../../../shared_imports'; import { FieldHook } from '../../../../shared_imports';
import { IMitreAttack, IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form'; import { MyAddItemButton } from '../add_item_form';
import * as i18n from './translations'; import * as i18n from './translations';
@ -33,7 +36,7 @@ interface AddSubtechniqueProps {
techniqueIndex: number; techniqueIndex: number;
idAria: string; idAria: string;
isDisabled: boolean; isDisabled: boolean;
onFieldChange: (threats: IMitreEnterpriseAttack[]) => void; onFieldChange: (threats: Threats) => void;
} }
export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
@ -44,7 +47,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
techniqueIndex, techniqueIndex,
onFieldChange, onFieldChange,
}): JSX.Element => { }): JSX.Element => {
const values = field.value as IMitreEnterpriseAttack[]; const values = field.value as Threats;
const technique = useMemo(() => { const technique = useMemo(() => {
return values[threatIndex].technique[techniqueIndex]; return values[threatIndex].technique[techniqueIndex];
@ -52,7 +55,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
const removeSubtechnique = useCallback( const removeSubtechnique = useCallback(
(index: number) => { (index: number) => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
if (subtechniques != null) { if (subtechniques != null) {
subtechniques.splice(index, 1); subtechniques.splice(index, 1);
@ -68,7 +71,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
); );
const addMitreAttackSubtechnique = useCallback(() => { const addMitreAttackSubtechnique = useCallback(() => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
@ -89,7 +92,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
const updateSubtechnique = useCallback( const updateSubtechnique = useCallback(
(index: number, value: string) => { (index: number, value: string) => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
const { id, reference, name } = subtechniquesOptions.find((t) => t.value === value) || { const { id, reference, name } = subtechniquesOptions.find((t) => t.value === value) || {
id: '', id: '',
name: '', name: '',
@ -127,7 +130,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
); );
const getSelectSubtechnique = useCallback( const getSelectSubtechnique = useCallback(
(index: number, disabled: boolean, subtechnique: IMitreAttack) => { (index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => {
const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id); const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id);
return ( return (

View file

@ -16,13 +16,13 @@ import { kebabCase, camelCase } from 'lodash/fp';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import {
Threats,
ThreatTechnique,
} from '../../../../../common/detection_engine/schemas/common/schemas';
import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
import { FieldHook } from '../../../../shared_imports'; import { FieldHook } from '../../../../shared_imports';
import {
IMitreAttackTechnique,
IMitreEnterpriseAttack,
} from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form'; import { MyAddItemButton } from '../add_item_form';
import { hasSubtechniqueOptions } from './helpers'; import { hasSubtechniqueOptions } from './helpers';
import * as i18n from './translations'; import * as i18n from './translations';
@ -41,7 +41,7 @@ interface AddTechniqueProps {
threatIndex: number; threatIndex: number;
idAria: string; idAria: string;
isDisabled: boolean; isDisabled: boolean;
onFieldChange: (threats: IMitreEnterpriseAttack[]) => void; onFieldChange: (threats: Threats) => void;
} }
export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
@ -51,11 +51,11 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
threatIndex, threatIndex,
onFieldChange, onFieldChange,
}): JSX.Element => { }): JSX.Element => {
const values = field.value as IMitreEnterpriseAttack[]; const values = field.value as Threats;
const removeTechnique = useCallback( const removeTechnique = useCallback(
(index: number) => { (index: number) => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
const techniques = threats[threatIndex].technique; const techniques = threats[threatIndex].technique;
techniques.splice(index, 1); techniques.splice(index, 1);
threats[threatIndex] = { threats[threatIndex] = {
@ -68,7 +68,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
); );
const addMitreAttackTechnique = useCallback(() => { const addMitreAttackTechnique = useCallback(() => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
threats[threatIndex] = { threats[threatIndex] = {
...threats[threatIndex], ...threats[threatIndex],
technique: [ technique: [
@ -81,7 +81,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
const updateTechnique = useCallback( const updateTechnique = useCallback(
(index: number, value: string) => { (index: number, value: string) => {
const threats = [...(field.value as IMitreEnterpriseAttack[])]; const threats = [...(field.value as Threats)];
const { id, reference, name } = techniquesOptions.find((t) => t.value === value) || { const { id, reference, name } = techniquesOptions.find((t) => t.value === value) || {
id: '', id: '',
name: '', name: '',
@ -109,7 +109,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
); );
const getSelectTechnique = useCallback( const getSelectTechnique = useCallback(
(tacticName: string, index: number, disabled: boolean, technique: IMitreAttackTechnique) => { (tacticName: string, index: number, disabled: boolean, technique: ThreatTechnique) => {
const options = techniquesOptions.filter((t) => t.tactics.includes(kebabCase(tacticName))); const options = techniquesOptions.filter((t) => t.tactics.includes(kebabCase(tacticName)));
return ( return (
<> <>

View file

@ -13,8 +13,7 @@ import {
ValidationFunc, ValidationFunc,
ERROR_CODE, ERROR_CODE,
} from '../../../../shared_imports'; } from '../../../../shared_imports';
import { IMitreEnterpriseAttack, AboutStepRule } from '../../../pages/detection_engine/rules/types'; import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
import { isMitreAttackInvalid } from '../mitre/helpers';
import { OptionalFieldLabel } from '../optional_field_label'; import { OptionalFieldLabel } from '../optional_field_label';
import { isUrlInvalid } from '../../../../common/utils/validators'; import { isUrlInvalid } from '../../../../common/utils/validators';
import * as I18n from './translations'; import * as I18n from './translations';
@ -192,29 +191,6 @@ export const schema: FormSchema<AboutStepRule> = {
} }
), ),
labelAppend: OptionalFieldLabel, labelAppend: OptionalFieldLabel,
validations: [
{
validator: (
...args: Parameters<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
const [{ value, path }] = args;
let hasTechniqueError = false;
(value as IMitreEnterpriseAttack[]).forEach((v) => {
if (isMitreAttackInvalid(v.tactic.name, v.technique)) {
hasTechniqueError = true;
}
});
return hasTechniqueError
? {
code: 'ERR_FIELD_MISSING',
path: `${path}.tactic`,
message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED,
}
: undefined;
},
exitOnFail: false,
},
],
}, },
timestampOverride: { timestampOverride: {
type: FIELD_TYPES.TEXT, type: FIELD_TYPES.TEXT,

View file

@ -69,13 +69,6 @@ export const CRITICAL = i18n.translate(
} }
); );
export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError',
{
defaultMessage: 'At least one Technique is required with a Tactic.',
}
);
export const URL_FORMAT_INVALID = i18n.translate( export const URL_FORMAT_INVALID = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError', 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError',
{ {

View file

@ -17,6 +17,7 @@ import {
timestamp_override, timestamp_override,
threshold, threshold,
type, type,
threats,
} from '../../../../../common/detection_engine/schemas/common/schemas'; } from '../../../../../common/detection_engine/schemas/common/schemas';
import { import {
listArray, listArray,
@ -77,6 +78,7 @@ const StatusTypes = t.union([
t.literal('partial failure'), t.literal('partial failure'),
]); ]);
// TODO: make a ticket
export const RuleSchema = t.intersection([ export const RuleSchema = t.intersection([
t.type({ t.type({
author, author,
@ -100,7 +102,7 @@ export const RuleSchema = t.intersection([
tags: t.array(t.string), tags: t.array(t.string),
type, type,
to: t.string, to: t.string,
threat: t.array(t.unknown), threat: threats,
updated_at: t.string, updated_at: t.string,
updated_by: t.string, updated_by: t.string,
actions: t.array(action), actions: t.array(action),

View file

@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Threat } from '../../../common/detection_engine/schemas/common/schemas'; import { Threats } from '../../../common/detection_engine/schemas/common/schemas';
import { mockThreatData } from './mitre_tactics_techniques'; import { mockThreatData } from './mitre_tactics_techniques';
const { tactic, technique, subtechnique } = mockThreatData; const { tactic, technique, subtechnique } = mockThreatData;
const { tactics, ...mockTechnique } = technique; const { tactics, ...mockTechnique } = technique;
const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique; const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique;
export const getValidThreat = (): Threat => [ export const getValidThreat = (): Threats => [
{ {
framework: 'MITRE ATT&CK', framework: 'MITRE ATT&CK',
tactic, tactic,

View file

@ -20,7 +20,6 @@ import {
ActionsStepRule, ActionsStepRule,
ScheduleStepRule, ScheduleStepRule,
DefineStepRule, DefineStepRule,
IMitreEnterpriseAttack,
} from '../types'; } from '../types';
import { import {
getTimeTypeValue, getTimeTypeValue,
@ -40,6 +39,7 @@ import {
mockActionsStepRule, mockActionsStepRule,
} from '../all/__mocks__/mock'; } from '../all/__mocks__/mock';
import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock';
import { Threat, Threats } from '../../../../../../common/detection_engine/schemas/common/schemas';
describe('helpers', () => { describe('helpers', () => {
describe('getTimeTypeValue', () => { describe('getTimeTypeValue', () => {
@ -87,14 +87,14 @@ describe('helpers', () => {
}); });
describe('filterEmptyThreats', () => { describe('filterEmptyThreats', () => {
let mockThreat: IMitreEnterpriseAttack; let mockThreat: Threat;
beforeEach(() => { beforeEach(() => {
mockThreat = mockAboutStepRule().threat[0]; mockThreat = mockAboutStepRule().threat[0];
}); });
test('filters out fields with empty tactics', () => { test('filters out fields with empty tactics', () => {
const threat: IMitreEnterpriseAttack[] = [ const threat: Threats = [
mockThreat, mockThreat,
{ ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } }, { ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } },
]; ];

View file

@ -14,7 +14,12 @@ import { transformAlertToRuleAction } from '../../../../../../common/detection_e
import { List } from '../../../../../../common/detection_engine/schemas/types'; import { List } from '../../../../../../common/detection_engine/schemas/types';
import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports'; import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports';
import { Rule } from '../../../../containers/detection_engine/rules'; import { Rule } from '../../../../containers/detection_engine/rules';
import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas'; import {
Threats,
ThreatSubtechnique,
ThreatTechnique,
Type,
} from '../../../../../../common/detection_engine/schemas/common/schemas';
import { import {
AboutStepRule, AboutStepRule,
@ -27,9 +32,6 @@ import {
ActionsStepRuleJson, ActionsStepRuleJson,
RuleStepsFormData, RuleStepsFormData,
RuleStep, RuleStep,
IMitreEnterpriseAttack,
IMitreAttack,
IMitreAttackTechnique,
} from '../types'; } from '../types';
export const getTimeTypeValue = (time: string): { unit: string; value: number } => { export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
@ -164,7 +166,7 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
assertUnreachable(type); assertUnreachable(type);
}; };
function trimThreatsWithNoName<T extends IMitreAttack | IMitreAttackTechnique>( function trimThreatsWithNoName<T extends ThreatSubtechnique | ThreatTechnique>(
filterable: T[] filterable: T[]
): T[] { ): T[] {
return filterable.filter((item) => item.name !== 'none'); return filterable.filter((item) => item.name !== 'none');
@ -173,7 +175,7 @@ function trimThreatsWithNoName<T extends IMitreAttack | IMitreAttackTechnique>(
/** /**
* Filter out unfilled/empty threat, technique, and subtechnique fields based on if their name is `none` * Filter out unfilled/empty threat, technique, and subtechnique fields based on if their name is `none`
*/ */
export const filterEmptyThreats = (threats: IMitreEnterpriseAttack[]): IMitreEnterpriseAttack[] => { export const filterEmptyThreats = (threats: Threats): Threats => {
return threats return threats
.filter((singleThreat) => singleThreat.tactic.name !== 'none') .filter((singleThreat) => singleThreat.tactic.name !== 'none')
.map((threat) => { .map((threat) => {

View file

@ -22,7 +22,6 @@ import {
AboutStepRule, AboutStepRule,
AboutStepRuleDetails, AboutStepRuleDetails,
DefineStepRule, DefineStepRule,
IMitreEnterpriseAttack,
ScheduleStepRule, ScheduleStepRule,
ActionsStepRule, ActionsStepRule,
} from './types'; } from './types';
@ -30,6 +29,7 @@ import {
SeverityMapping, SeverityMapping,
Type, Type,
Severity, Severity,
Threats,
} from '../../../../../common/detection_engine/schemas/common/schemas'; } from '../../../../../common/detection_engine/schemas/common/schemas';
import { severityOptions } from '../../../components/rules/step_about_rule/data'; import { severityOptions } from '../../../components/rules/step_about_rule/data';
@ -177,7 +177,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
isMappingChecked: riskScoreMapping.length > 0, isMappingChecked: riskScoreMapping.length > 0,
}, },
falsePositives, falsePositives,
threat: threat as IMitreEnterpriseAttack[], threat: threat as Threats,
}; };
}; };

View file

@ -21,6 +21,7 @@ import {
TimestampOverride, TimestampOverride,
Type, Type,
Severity, Severity,
Threats,
} from '../../../../../common/detection_engine/schemas/common/schemas'; } from '../../../../../common/detection_engine/schemas/common/schemas';
import { import {
List, List,
@ -99,7 +100,7 @@ export interface AboutStepRule {
ruleNameOverride: string; ruleNameOverride: string;
tags: string[]; tags: string[];
timestampOverride: string; timestampOverride: string;
threat: IMitreEnterpriseAttack[]; threat: Threats;
note: string; note: string;
} }
@ -178,7 +179,7 @@ export interface AboutStepRuleJson {
false_positives: string[]; false_positives: string[];
rule_name_override?: RuleNameOverride; rule_name_override?: RuleNameOverride;
tags: string[]; tags: string[];
threat: IMitreEnterpriseAttack[]; threat: Threats;
timestamp_override?: TimestampOverride; timestamp_override?: TimestampOverride;
note?: string; note?: string;
} }
@ -196,22 +197,3 @@ export interface ActionsStepRuleJson {
throttle?: string | null; throttle?: string | null;
meta?: unknown; meta?: unknown;
} }
export interface IMitreAttack {
id: string;
name: string;
reference: string;
}
export interface IMitreAttackTechnique {
id: string;
name: string;
reference: string;
subtechnique?: IMitreAttack[];
}
export interface IMitreEnterpriseAttack {
framework: string;
tactic: IMitreAttack;
technique: IMitreAttackTechnique[];
}

View file

@ -28,7 +28,7 @@ import {
Name, Name,
Severity, Severity,
Tags, Tags,
Threat, Threats,
To, To,
Type, Type,
References, References,
@ -59,7 +59,7 @@ import {
SeverityOrUndefined, SeverityOrUndefined,
TagsOrUndefined, TagsOrUndefined,
ToOrUndefined, ToOrUndefined,
ThreatOrUndefined, ThreatsOrUndefined,
ThresholdOrUndefined, ThresholdOrUndefined,
TypeOrUndefined, TypeOrUndefined,
ReferencesOrUndefined, ReferencesOrUndefined,
@ -231,7 +231,7 @@ export interface CreateRulesOptions {
severity: Severity; severity: Severity;
severityMapping: SeverityMapping; severityMapping: SeverityMapping;
tags: Tags; tags: Tags;
threat: Threat; threat: Threats;
threshold: ThresholdOrUndefined; threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined; threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined; threatIndex: ThreatIndexOrUndefined;
@ -288,7 +288,7 @@ export interface PatchRulesOptions {
severity: SeverityOrUndefined; severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined; severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined; tags: TagsOrUndefined;
threat: ThreatOrUndefined; threat: ThreatsOrUndefined;
itemsPerSearch: ItemsPerSearchOrUndefined; itemsPerSearch: ItemsPerSearchOrUndefined;
concurrentSearches: ConcurrentSearchesOrUndefined; concurrentSearches: ConcurrentSearchesOrUndefined;
threshold: ThresholdOrUndefined; threshold: ThresholdOrUndefined;

View file

@ -28,7 +28,7 @@ import {
SeverityOrUndefined, SeverityOrUndefined,
TagsOrUndefined, TagsOrUndefined,
ToOrUndefined, ToOrUndefined,
ThreatOrUndefined, ThreatsOrUndefined,
ThresholdOrUndefined, ThresholdOrUndefined,
TypeOrUndefined, TypeOrUndefined,
ReferencesOrUndefined, ReferencesOrUndefined,
@ -93,7 +93,7 @@ export interface UpdateProperties {
severity: SeverityOrUndefined; severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined; severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined; tags: TagsOrUndefined;
threat: ThreatOrUndefined; threat: ThreatsOrUndefined;
threshold: ThresholdOrUndefined; threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined; threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined; threatIndex: ThreatIndexOrUndefined;

View file

@ -43,7 +43,7 @@ import {
severityMappingOrUndefined, severityMappingOrUndefined,
tags, tags,
timestampOverrideOrUndefined, timestampOverrideOrUndefined,
threat, threats,
to, to,
references, references,
version, version,
@ -85,7 +85,7 @@ export const baseRuleParams = t.exact(
severity, severity,
severityMapping: severityMappingOrUndefined, severityMapping: severityMappingOrUndefined,
timestampOverride: timestampOverrideOrUndefined, timestampOverride: timestampOverrideOrUndefined,
threat, threat: threats,
to, to,
references, references,
version, version,

View file

@ -8,7 +8,7 @@ import {
AnomalyThresholdOrUndefined, AnomalyThresholdOrUndefined,
Description, Description,
NoteOrUndefined, NoteOrUndefined,
ThreatOrUndefined, ThreatsOrUndefined,
ThresholdOrUndefined, ThresholdOrUndefined,
FalsePositives, FalsePositives,
From, From,
@ -82,7 +82,7 @@ export interface RuleTypeParams {
ruleNameOverride: RuleNameOverrideOrUndefined; ruleNameOverride: RuleNameOverrideOrUndefined;
severity: Severity; severity: Severity;
severityMapping: SeverityMappingOrUndefined; severityMapping: SeverityMappingOrUndefined;
threat: ThreatOrUndefined; threat: ThreatsOrUndefined;
threshold: ThresholdOrUndefined; threshold: ThresholdOrUndefined;
threatFilters: PartialFilter[] | undefined; threatFilters: PartialFilter[] | undefined;
threatIndex: ThreatIndexOrUndefined; threatIndex: ThreatIndexOrUndefined;

View file

@ -16668,7 +16668,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "Tacticには1つ以上のTechniqueが必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQLが無効です", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQLが無効です",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "カスタムクエリが必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "カスタムクエリが必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。",

View file

@ -16685,7 +16685,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "一个策略至少需要一个技术。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQL 无效", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQL 无效",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "需要定制查询。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "需要定制查询。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。",