mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Detection Rules] Makes threat techniques optional (#85481)
This commit is contained in:
parent
07f395f7dd
commit
13e5e55901
30 changed files with 104 additions and 223 deletions
|
@ -413,6 +413,7 @@ export const threat_tactic = t.type({
|
|||
name: threat_tactic_name,
|
||||
reference: threat_tactic_reference,
|
||||
});
|
||||
export type ThreatTactic = t.TypeOf<typeof threat_tactic>;
|
||||
export const threat_subtechnique_id = t.string;
|
||||
export const threat_subtechnique_name = t.string;
|
||||
export const threat_subtechnique_reference = t.string;
|
||||
|
@ -421,6 +422,7 @@ export const threat_subtechnique = t.type({
|
|||
name: threat_subtechnique_name,
|
||||
reference: threat_subtechnique_reference,
|
||||
});
|
||||
export type ThreatSubtechnique = t.TypeOf<typeof threat_subtechnique>;
|
||||
export const threat_subtechniques = t.array(threat_subtechnique);
|
||||
export const threat_technique_id = 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 = t.array(
|
||||
t.exact(
|
||||
t.type({
|
||||
framework: threat_framework,
|
||||
tactic: threat_tactic,
|
||||
technique: threat_techniques,
|
||||
})
|
||||
)
|
||||
export const threat = t.exact(
|
||||
t.type({
|
||||
framework: threat_framework,
|
||||
tactic: threat_tactic,
|
||||
technique: threat_techniques,
|
||||
})
|
||||
);
|
||||
|
||||
export type Threat = t.TypeOf<typeof threat>;
|
||||
|
||||
export const threatOrUndefined = t.union([threat, t.undefined]);
|
||||
export type ThreatOrUndefined = t.TypeOf<typeof threatOrUndefined>;
|
||||
export const threats = t.array(threat);
|
||||
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(
|
||||
t.type({
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
Tags,
|
||||
To,
|
||||
type,
|
||||
Threat,
|
||||
Threats,
|
||||
threshold,
|
||||
ThrottleOrNull,
|
||||
note,
|
||||
|
@ -171,7 +171,7 @@ export type AddPrepackagedRulesSchemaDecoded = Omit<
|
|||
severity_mapping: SeverityMapping;
|
||||
tags: Tags;
|
||||
to: To;
|
||||
threat: Threat;
|
||||
threat: Threats;
|
||||
throttle: ThrottleOrNull;
|
||||
exceptions_list: ListArray;
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
Tags,
|
||||
To,
|
||||
type,
|
||||
Threat,
|
||||
Threats,
|
||||
threshold,
|
||||
ThrottleOrNull,
|
||||
note,
|
||||
|
@ -193,7 +193,7 @@ export type ImportRulesSchemaDecoded = Omit<
|
|||
severity_mapping: SeverityMapping;
|
||||
tags: Tags;
|
||||
to: To;
|
||||
threat: Threat;
|
||||
threat: Threats;
|
||||
throttle: ThrottleOrNull;
|
||||
version: Version;
|
||||
exceptions_list: ListArray;
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
from,
|
||||
enabled,
|
||||
tags,
|
||||
threat,
|
||||
threats,
|
||||
threshold,
|
||||
throttle,
|
||||
references,
|
||||
|
@ -98,7 +98,7 @@ export const patchRulesSchema = t.exact(
|
|||
severity_mapping,
|
||||
tags,
|
||||
to,
|
||||
threat,
|
||||
threat: threats,
|
||||
threshold,
|
||||
throttle,
|
||||
timestamp_override,
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
max_signals,
|
||||
risk_score,
|
||||
severity,
|
||||
threat,
|
||||
threats,
|
||||
to,
|
||||
references,
|
||||
version,
|
||||
|
@ -167,7 +167,7 @@ const commonParams = {
|
|||
max_signals,
|
||||
risk_score_mapping,
|
||||
severity_mapping,
|
||||
threat,
|
||||
threat: threats,
|
||||
to,
|
||||
references,
|
||||
version,
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
timeline_id,
|
||||
timeline_title,
|
||||
type,
|
||||
threat,
|
||||
threats,
|
||||
threshold,
|
||||
throttle,
|
||||
job_status,
|
||||
|
@ -106,7 +106,7 @@ export const requiredRulesSchema = t.type({
|
|||
tags,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
threat: threats,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
|
|
|
@ -8,11 +8,11 @@ import { DefaultThreatArray } from './default_threat_array';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../test_utils';
|
||||
import { Threat } from '../common/schemas';
|
||||
import { Threats } from '../common/schemas';
|
||||
|
||||
describe('default_threat_null', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
const payload: Threat = [];
|
||||
const payload: Threats = [];
|
||||
const decoded = DefaultThreatArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
|
@ -21,7 +21,7 @@ describe('default_threat_null', () => {
|
|||
});
|
||||
|
||||
test('it should validate an array of threats', () => {
|
||||
const payload: Threat = [
|
||||
const payload: Threats = [
|
||||
{
|
||||
framework: 'MITRE ATTACK',
|
||||
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
import { Threat, threat } from '../common/schemas';
|
||||
import { Threats, threats } from '../common/schemas';
|
||||
|
||||
/**
|
||||
* Types the DefaultThreatArray as:
|
||||
* - 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',
|
||||
threat.is,
|
||||
(input, context): Either<t.Errors, Threat> =>
|
||||
input == null ? t.success([]) : threat.validate(input, context),
|
||||
threats.is,
|
||||
(input, context): Either<t.Errors, Threats> =>
|
||||
input == null ? t.success([]) : threats.validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* 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',
|
||||
tactic: {
|
||||
|
|
|
@ -18,11 +18,7 @@ import {
|
|||
} from '../../../../../../../../src/plugins/data/public';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
AboutStepRiskScore,
|
||||
AboutStepSeverity,
|
||||
IMitreEnterpriseAttack,
|
||||
} from '../../../pages/detection_engine/rules/types';
|
||||
import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types';
|
||||
import { FieldValueTimeline } from '../pick_timeline';
|
||||
import { FormSchema } from '../../../../shared_imports';
|
||||
import { ListItems } from './types';
|
||||
|
@ -42,7 +38,7 @@ import {
|
|||
import { buildMlJobDescription } from './ml_job_description';
|
||||
import { buildActionsDescription } from './actions_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 { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers';
|
||||
|
||||
|
@ -179,7 +175,7 @@ export const getDescriptionItem = (
|
|||
indexPatterns,
|
||||
});
|
||||
} else if (field === 'threat') {
|
||||
const threats: IMitreEnterpriseAttack[] = get(field, data);
|
||||
const threats: Threats = get(field, data);
|
||||
return buildThreatDescription({ label, threat: filterEmptyThreats(threats) });
|
||||
} else if (field === 'threshold') {
|
||||
const threshold = get(field, data);
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import { Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
import {
|
||||
IIndexPattern,
|
||||
Filter,
|
||||
FilterManager,
|
||||
} from '../../../../../../../../src/plugins/data/public';
|
||||
import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
|
||||
|
||||
export interface ListItems {
|
||||
title: NonNullable<ReactNode>;
|
||||
|
@ -29,5 +29,5 @@ export interface BuildQueryBarDescription {
|
|||
|
||||
export interface BuildThreatDescription {
|
||||
label: string;
|
||||
threat: IMitreEnterpriseAttack[];
|
||||
threat: Threats;
|
||||
}
|
||||
|
|
|
@ -5,51 +5,11 @@
|
|||
*/
|
||||
|
||||
import { getValidThreat } from '../../../mitre/valid_threat_mock';
|
||||
import { hasSubtechniqueOptions, isMitreAttackInvalid } from './helpers';
|
||||
import { hasSubtechniqueOptions } from './helpers';
|
||||
|
||||
const mockTechniques = getValidThreat()[0].technique;
|
||||
|
||||
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('when technique has subtechnique options', () => {
|
||||
const technique = mockTechniques[0];
|
||||
|
|
|
@ -3,32 +3,12 @@
|
|||
* or more contributor license agreements. Licensed under 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 { 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
|
||||
*/
|
||||
export const hasSubtechniqueOptions = (technique: IMitreAttackTechnique) => {
|
||||
export const hasSubtechniqueOptions = (technique: ThreatTechnique) => {
|
||||
return subtechniquesOptions.some((subtechnique) => subtechnique.techniqueId === technique.id);
|
||||
};
|
||||
|
|
|
@ -6,19 +6,18 @@
|
|||
|
||||
import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
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 { isEqual } from 'lodash';
|
||||
import { Threat, Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
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 { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
|
||||
import { MyAddItemButton } from '../add_item_form';
|
||||
import * as i18n from './translations';
|
||||
import { MitreAttackTechniqueFields } from './technique_fields';
|
||||
import { isMitreAttackInvalid } from './helpers';
|
||||
|
||||
const MitreAttackContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
|
@ -40,12 +39,9 @@ interface AddItemProps {
|
|||
}
|
||||
|
||||
export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItemProps) => {
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const { errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
const removeTactic = useCallback(
|
||||
(index: number) => {
|
||||
const values = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const values = [...(field.value as Threats)];
|
||||
values.splice(index, 1);
|
||||
if (isEmpty(values)) {
|
||||
field.setValue(threatDefault);
|
||||
|
@ -57,7 +53,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
);
|
||||
|
||||
const addMitreAttackTactic = useCallback(() => {
|
||||
const values = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const values = [...(field.value as Threats)];
|
||||
if (!isEmpty(values[values.length - 1])) {
|
||||
field.setValue([
|
||||
...values,
|
||||
|
@ -70,7 +66,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
|
||||
const updateTactic = useCallback(
|
||||
(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) || {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -87,15 +83,11 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return [...(field.value as IMitreEnterpriseAttack[])];
|
||||
return [...(field.value as Threats)];
|
||||
}, [field]);
|
||||
|
||||
const isTacticValid = useCallback((threat: IMitreEnterpriseAttack) => {
|
||||
return isMitreAttackInvalid(threat.tactic.name, threat.technique);
|
||||
}, []);
|
||||
|
||||
const getSelectTactic = useCallback(
|
||||
(threat: IMitreEnterpriseAttack, index: number, disabled: boolean) => {
|
||||
(threat: Threat, index: number, disabled: boolean) => {
|
||||
const tacticName = threat.tactic.name;
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
|
@ -125,8 +117,6 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
valueOfSelected={camelCase(tacticName)}
|
||||
data-test-subj="mitreAttackTactic"
|
||||
placeholder={i18n.TACTIC_PLACEHOLDER}
|
||||
isInvalid={showValidation && isTacticValid(threat)}
|
||||
onBlur={() => setShowValidation(true)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -141,7 +131,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
</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
|
||||
*/
|
||||
const onFieldChange = useCallback(
|
||||
(threats: IMitreEnterpriseAttack[]) => {
|
||||
(threats: Threats) => {
|
||||
field.setValue(threats);
|
||||
},
|
||||
[field]
|
||||
|
@ -166,16 +156,12 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
|
|||
label={`${field.label} ${i18n.THREATS}`}
|
||||
labelAppend={field.labelAppend}
|
||||
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
|
||||
isInvalid={showValidation && isTacticValid(threat)}
|
||||
error={errorMessage}
|
||||
>
|
||||
<>{getSelectTactic(threat, index, isDisabled)}</>
|
||||
</InitialMitreAttackFormRow>
|
||||
) : (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={showValidation && isTacticValid(threat)}
|
||||
error={errorMessage}
|
||||
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
|
||||
>
|
||||
{getSelectTactic(threat, index, isDisabled)}
|
||||
|
|
|
@ -16,10 +16,13 @@ import { camelCase } from 'lodash/fp';
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
Threats,
|
||||
ThreatSubtechnique,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
|
||||
import { FieldHook } from '../../../../shared_imports';
|
||||
import { IMitreAttack, IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
|
||||
import { MyAddItemButton } from '../add_item_form';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -33,7 +36,7 @@ interface AddSubtechniqueProps {
|
|||
techniqueIndex: number;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
onFieldChange: (threats: IMitreEnterpriseAttack[]) => void;
|
||||
onFieldChange: (threats: Threats) => void;
|
||||
}
|
||||
|
||||
export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
||||
|
@ -44,7 +47,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
|||
techniqueIndex,
|
||||
onFieldChange,
|
||||
}): JSX.Element => {
|
||||
const values = field.value as IMitreEnterpriseAttack[];
|
||||
const values = field.value as Threats;
|
||||
|
||||
const technique = useMemo(() => {
|
||||
return values[threatIndex].technique[techniqueIndex];
|
||||
|
@ -52,7 +55,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
|||
|
||||
const removeSubtechnique = useCallback(
|
||||
(index: number) => {
|
||||
const threats = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const threats = [...(field.value as Threats)];
|
||||
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
|
||||
if (subtechniques != null) {
|
||||
subtechniques.splice(index, 1);
|
||||
|
@ -68,7 +71,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
|||
);
|
||||
|
||||
const addMitreAttackSubtechnique = useCallback(() => {
|
||||
const threats = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const threats = [...(field.value as Threats)];
|
||||
|
||||
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
|
||||
|
||||
|
@ -89,7 +92,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
|||
|
||||
const updateSubtechnique = useCallback(
|
||||
(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) || {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -127,7 +130,7 @@ export const MitreAttackSubtechniqueFields: React.FC<AddSubtechniqueProps> = ({
|
|||
);
|
||||
|
||||
const getSelectSubtechnique = useCallback(
|
||||
(index: number, disabled: boolean, subtechnique: IMitreAttack) => {
|
||||
(index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => {
|
||||
const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id);
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,13 +16,13 @@ import { kebabCase, camelCase } from 'lodash/fp';
|
|||
import React, { useCallback } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Threats,
|
||||
ThreatTechnique,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
|
||||
import { FieldHook } from '../../../../shared_imports';
|
||||
import {
|
||||
IMitreAttackTechnique,
|
||||
IMitreEnterpriseAttack,
|
||||
} from '../../../pages/detection_engine/rules/types';
|
||||
import { MyAddItemButton } from '../add_item_form';
|
||||
import { hasSubtechniqueOptions } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
@ -41,7 +41,7 @@ interface AddTechniqueProps {
|
|||
threatIndex: number;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
onFieldChange: (threats: IMitreEnterpriseAttack[]) => void;
|
||||
onFieldChange: (threats: Threats) => void;
|
||||
}
|
||||
|
||||
export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
|
||||
|
@ -51,11 +51,11 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
|
|||
threatIndex,
|
||||
onFieldChange,
|
||||
}): JSX.Element => {
|
||||
const values = field.value as IMitreEnterpriseAttack[];
|
||||
const values = field.value as Threats;
|
||||
|
||||
const removeTechnique = useCallback(
|
||||
(index: number) => {
|
||||
const threats = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const threats = [...(field.value as Threats)];
|
||||
const techniques = threats[threatIndex].technique;
|
||||
techniques.splice(index, 1);
|
||||
threats[threatIndex] = {
|
||||
|
@ -68,7 +68,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
|
|||
);
|
||||
|
||||
const addMitreAttackTechnique = useCallback(() => {
|
||||
const threats = [...(field.value as IMitreEnterpriseAttack[])];
|
||||
const threats = [...(field.value as Threats)];
|
||||
threats[threatIndex] = {
|
||||
...threats[threatIndex],
|
||||
technique: [
|
||||
|
@ -81,7 +81,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
|
|||
|
||||
const updateTechnique = useCallback(
|
||||
(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) || {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -109,7 +109,7 @@ export const MitreAttackTechniqueFields: React.FC<AddTechniqueProps> = ({
|
|||
);
|
||||
|
||||
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)));
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -13,8 +13,7 @@ import {
|
|||
ValidationFunc,
|
||||
ERROR_CODE,
|
||||
} from '../../../../shared_imports';
|
||||
import { IMitreEnterpriseAttack, AboutStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import { isMitreAttackInvalid } from '../mitre/helpers';
|
||||
import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import { OptionalFieldLabel } from '../optional_field_label';
|
||||
import { isUrlInvalid } from '../../../../common/utils/validators';
|
||||
import * as I18n from './translations';
|
||||
|
@ -192,29 +191,6 @@ export const schema: FormSchema<AboutStepRule> = {
|
|||
}
|
||||
),
|
||||
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: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
|
|
|
@ -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(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError',
|
||||
{
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
timestamp_override,
|
||||
threshold,
|
||||
type,
|
||||
threats,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import {
|
||||
listArray,
|
||||
|
@ -77,6 +78,7 @@ const StatusTypes = t.union([
|
|||
t.literal('partial failure'),
|
||||
]);
|
||||
|
||||
// TODO: make a ticket
|
||||
export const RuleSchema = t.intersection([
|
||||
t.type({
|
||||
author,
|
||||
|
@ -100,7 +102,7 @@ export const RuleSchema = t.intersection([
|
|||
tags: t.array(t.string),
|
||||
type,
|
||||
to: t.string,
|
||||
threat: t.array(t.unknown),
|
||||
threat: threats,
|
||||
updated_at: t.string,
|
||||
updated_by: t.string,
|
||||
actions: t.array(action),
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* 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';
|
||||
|
||||
const { tactic, technique, subtechnique } = mockThreatData;
|
||||
const { tactics, ...mockTechnique } = technique;
|
||||
const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique;
|
||||
|
||||
export const getValidThreat = (): Threat => [
|
||||
export const getValidThreat = (): Threats => [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic,
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
ActionsStepRule,
|
||||
ScheduleStepRule,
|
||||
DefineStepRule,
|
||||
IMitreEnterpriseAttack,
|
||||
} from '../types';
|
||||
import {
|
||||
getTimeTypeValue,
|
||||
|
@ -40,6 +39,7 @@ import {
|
|||
mockActionsStepRule,
|
||||
} from '../all/__mocks__/mock';
|
||||
import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock';
|
||||
import { Threat, Threats } from '../../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getTimeTypeValue', () => {
|
||||
|
@ -87,14 +87,14 @@ describe('helpers', () => {
|
|||
});
|
||||
|
||||
describe('filterEmptyThreats', () => {
|
||||
let mockThreat: IMitreEnterpriseAttack;
|
||||
let mockThreat: Threat;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThreat = mockAboutStepRule().threat[0];
|
||||
});
|
||||
|
||||
test('filters out fields with empty tactics', () => {
|
||||
const threat: IMitreEnterpriseAttack[] = [
|
||||
const threat: Threats = [
|
||||
mockThreat,
|
||||
{ ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } },
|
||||
];
|
||||
|
|
|
@ -14,7 +14,12 @@ import { transformAlertToRuleAction } from '../../../../../../common/detection_e
|
|||
import { List } from '../../../../../../common/detection_engine/schemas/types';
|
||||
import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports';
|
||||
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 {
|
||||
AboutStepRule,
|
||||
|
@ -27,9 +32,6 @@ import {
|
|||
ActionsStepRuleJson,
|
||||
RuleStepsFormData,
|
||||
RuleStep,
|
||||
IMitreEnterpriseAttack,
|
||||
IMitreAttack,
|
||||
IMitreAttackTechnique,
|
||||
} from '../types';
|
||||
|
||||
export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
|
||||
|
@ -164,7 +166,7 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
|||
assertUnreachable(type);
|
||||
};
|
||||
|
||||
function trimThreatsWithNoName<T extends IMitreAttack | IMitreAttackTechnique>(
|
||||
function trimThreatsWithNoName<T extends ThreatSubtechnique | ThreatTechnique>(
|
||||
filterable: T[]
|
||||
): T[] {
|
||||
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`
|
||||
*/
|
||||
export const filterEmptyThreats = (threats: IMitreEnterpriseAttack[]): IMitreEnterpriseAttack[] => {
|
||||
export const filterEmptyThreats = (threats: Threats): Threats => {
|
||||
return threats
|
||||
.filter((singleThreat) => singleThreat.tactic.name !== 'none')
|
||||
.map((threat) => {
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
AboutStepRule,
|
||||
AboutStepRuleDetails,
|
||||
DefineStepRule,
|
||||
IMitreEnterpriseAttack,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from './types';
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
SeverityMapping,
|
||||
Type,
|
||||
Severity,
|
||||
Threats,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
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,
|
||||
},
|
||||
falsePositives,
|
||||
threat: threat as IMitreEnterpriseAttack[],
|
||||
threat: threat as Threats,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
TimestampOverride,
|
||||
Type,
|
||||
Severity,
|
||||
Threats,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import {
|
||||
List,
|
||||
|
@ -99,7 +100,7 @@ export interface AboutStepRule {
|
|||
ruleNameOverride: string;
|
||||
tags: string[];
|
||||
timestampOverride: string;
|
||||
threat: IMitreEnterpriseAttack[];
|
||||
threat: Threats;
|
||||
note: string;
|
||||
}
|
||||
|
||||
|
@ -178,7 +179,7 @@ export interface AboutStepRuleJson {
|
|||
false_positives: string[];
|
||||
rule_name_override?: RuleNameOverride;
|
||||
tags: string[];
|
||||
threat: IMitreEnterpriseAttack[];
|
||||
threat: Threats;
|
||||
timestamp_override?: TimestampOverride;
|
||||
note?: string;
|
||||
}
|
||||
|
@ -196,22 +197,3 @@ export interface ActionsStepRuleJson {
|
|||
throttle?: string | null;
|
||||
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[];
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
Name,
|
||||
Severity,
|
||||
Tags,
|
||||
Threat,
|
||||
Threats,
|
||||
To,
|
||||
Type,
|
||||
References,
|
||||
|
@ -59,7 +59,7 @@ import {
|
|||
SeverityOrUndefined,
|
||||
TagsOrUndefined,
|
||||
ToOrUndefined,
|
||||
ThreatOrUndefined,
|
||||
ThreatsOrUndefined,
|
||||
ThresholdOrUndefined,
|
||||
TypeOrUndefined,
|
||||
ReferencesOrUndefined,
|
||||
|
@ -231,7 +231,7 @@ export interface CreateRulesOptions {
|
|||
severity: Severity;
|
||||
severityMapping: SeverityMapping;
|
||||
tags: Tags;
|
||||
threat: Threat;
|
||||
threat: Threats;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: ThreatFiltersOrUndefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
|
@ -288,7 +288,7 @@ export interface PatchRulesOptions {
|
|||
severity: SeverityOrUndefined;
|
||||
severityMapping: SeverityMappingOrUndefined;
|
||||
tags: TagsOrUndefined;
|
||||
threat: ThreatOrUndefined;
|
||||
threat: ThreatsOrUndefined;
|
||||
itemsPerSearch: ItemsPerSearchOrUndefined;
|
||||
concurrentSearches: ConcurrentSearchesOrUndefined;
|
||||
threshold: ThresholdOrUndefined;
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
SeverityOrUndefined,
|
||||
TagsOrUndefined,
|
||||
ToOrUndefined,
|
||||
ThreatOrUndefined,
|
||||
ThreatsOrUndefined,
|
||||
ThresholdOrUndefined,
|
||||
TypeOrUndefined,
|
||||
ReferencesOrUndefined,
|
||||
|
@ -93,7 +93,7 @@ export interface UpdateProperties {
|
|||
severity: SeverityOrUndefined;
|
||||
severityMapping: SeverityMappingOrUndefined;
|
||||
tags: TagsOrUndefined;
|
||||
threat: ThreatOrUndefined;
|
||||
threat: ThreatsOrUndefined;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: ThreatFiltersOrUndefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
severityMappingOrUndefined,
|
||||
tags,
|
||||
timestampOverrideOrUndefined,
|
||||
threat,
|
||||
threats,
|
||||
to,
|
||||
references,
|
||||
version,
|
||||
|
@ -85,7 +85,7 @@ export const baseRuleParams = t.exact(
|
|||
severity,
|
||||
severityMapping: severityMappingOrUndefined,
|
||||
timestampOverride: timestampOverrideOrUndefined,
|
||||
threat,
|
||||
threat: threats,
|
||||
to,
|
||||
references,
|
||||
version,
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
AnomalyThresholdOrUndefined,
|
||||
Description,
|
||||
NoteOrUndefined,
|
||||
ThreatOrUndefined,
|
||||
ThreatsOrUndefined,
|
||||
ThresholdOrUndefined,
|
||||
FalsePositives,
|
||||
From,
|
||||
|
@ -82,7 +82,7 @@ export interface RuleTypeParams {
|
|||
ruleNameOverride: RuleNameOverrideOrUndefined;
|
||||
severity: Severity;
|
||||
severityMapping: SeverityMappingOrUndefined;
|
||||
threat: ThreatOrUndefined;
|
||||
threat: ThreatsOrUndefined;
|
||||
threshold: ThresholdOrUndefined;
|
||||
threatFilters: PartialFilter[] | undefined;
|
||||
threatIndex: ThreatIndexOrUndefined;
|
||||
|
|
|
@ -16668,7 +16668,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
|
||||
"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.customQueryFieldRequiredError": "カスタムクエリが必要です。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。",
|
||||
|
|
|
@ -16685,7 +16685,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "一个策略至少需要一个技术。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQL 无效",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "需要定制查询。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue