[CTI] explicit threat_indicator_path for IM rules (#118821) (#119075)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Ece Özalp <ozale272@newschool.edu>
This commit is contained in:
Kibana Machine 2021-11-18 15:15:42 -05:00 committed by GitHub
parent 4ea934c2ce
commit 4ec95de6f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 231 additions and 7 deletions

View file

@ -40,6 +40,7 @@ import {
INDICATOR_INDEX_PATTERNS,
INDICATOR_INDEX_QUERY,
INDICATOR_MAPPING,
INDICATOR_PREFIX_OVERRIDE,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
@ -448,6 +449,10 @@ describe('indicator match', () => {
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', getNewThreatIndicatorRule().severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', getNewThreatIndicatorRule().riskScore);
getDetails(INDICATOR_PREFIX_OVERRIDE).should(
'have.text',
getNewThreatIndicatorRule().threatIndicatorPath
);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});

View file

@ -77,6 +77,7 @@ export interface ThreatIndicatorRule extends CustomRule {
indicatorIndexPattern: string[];
indicatorMappingField: string;
indicatorIndexField: string;
threatIndicatorPath: string;
type?: string;
atomic?: string;
}
@ -405,6 +406,7 @@ export const getNewThreatIndicatorRule = (): ThreatIndicatorRule => ({
atomic: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3',
timeline: getIndicatorMatchTimelineTemplate(),
maxSignals: 100,
threatIndicatorPath: 'threat.indicator',
});
export const duplicatedRuleName = `${getNewThreatIndicatorRule().name} [Duplicate]`;

View file

@ -64,6 +64,8 @@ export const RULE_NAME_OVERRIDE_DETAILS = 'Rule name override';
export const RISK_SCORE_DETAILS = 'Risk score';
export const INDICATOR_PREFIX_OVERRIDE = 'Indicator prefix override';
export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override';
export const REFERENCE_URLS_DETAILS = 'Reference URLs';

View file

@ -20,6 +20,7 @@ import {
AboutStepRule,
RuleStepsFormHooks,
RuleStep,
DefineStepRule,
} from '../../../pages/detection_engine/rules/types';
import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers';
import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock';
@ -105,6 +106,63 @@ describe.skip('StepAboutRuleComponent', () => {
});
});
it('is invalid if threat match rule and threat_indicator_path is not present', async () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<StepAboutRule
addPadding={true}
defaultValues={stepAboutDefaultValue}
defineRuleData={{ ruleType: 'threat_match' } as DefineStepRule}
descriptionColumns="multi"
isReadOnlyView={false}
setForm={setFormHook}
isLoading={false}
/>
</ThemeProvider>
);
await act(async () => {
if (!formHook) {
throw new Error('Form hook not set, but tests depend on it');
}
wrapper
.find('[data-test-subj="detectionEngineStepAboutThreatIndicatorPath"] input')
.first()
.simulate('change', { target: { value: '' } });
const result = await formHook();
expect(result?.isValid).toEqual(false);
});
});
it('is valid if is not a threat match rule and threat_indicator_path is not present', async () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<StepAboutRule
addPadding={true}
defaultValues={stepAboutDefaultValue}
descriptionColumns="multi"
isReadOnlyView={false}
setForm={setFormHook}
isLoading={false}
/>
</ThemeProvider>
);
await act(async () => {
if (!formHook) {
throw new Error('Form hook not set, but tests depend on it');
}
wrapper
.find('[data-test-subj="detectionEngineStepAboutThreatIndicatorPath"] input')
.first()
.simulate('change', { target: { value: '' } });
const result = await formHook();
expect(result?.isValid).toEqual(true);
});
});
it('is invalid if no "name" is present', async () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>

View file

@ -6,7 +6,7 @@
*/
import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui';
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
import React, { FC, memo, useCallback, useEffect, useState, useMemo } from 'react';
import styled from 'styled-components';
import {
@ -31,7 +31,7 @@ import {
import { defaultRiskScoreBySeverity, severityOptions } from './data';
import { stepAboutDefaultValue } from './default_value';
import { isUrlInvalid } from '../../../../common/utils/validators';
import { schema } from './schema';
import { schema as defaultSchema, threatIndicatorPathRequiredSchemaValue } from './schema';
import * as I18n from './translations';
import { StepContentWrapper } from '../step_content_wrapper';
import { NextStep } from '../next_step';
@ -73,7 +73,28 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
onSubmit,
setForm,
}) => {
const initialState = defaultValues ?? stepAboutDefaultValue;
const isThreatMatchRuleValue = useMemo(
() => isThreatMatchRule(defineRuleData?.ruleType),
[defineRuleData?.ruleType]
);
const initialState: AboutStepRule = useMemo(
() =>
defaultValues ??
(isThreatMatchRuleValue
? { ...stepAboutDefaultValue, threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH }
: stepAboutDefaultValue),
[defaultValues, isThreatMatchRuleValue]
);
const schema = useMemo(
() =>
isThreatMatchRuleValue
? { ...defaultSchema, threatIndicatorPath: threatIndicatorPathRequiredSchemaValue }
: defaultSchema,
[isThreatMatchRuleValue]
);
const [severityValue, setSeverityValue] = useState<string>(initialState.severity.value);
const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []);
@ -300,7 +321,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
/>
</EuiFormRow>
<EuiSpacer size="l" />
{isThreatMatchRule(defineRuleData?.ruleType) && (
{isThreatMatchRuleValue && (
<>
<CommonUseField
path="threatIndicatorPath"

View file

@ -291,3 +291,33 @@ export const schema: FormSchema<AboutStepRule> = {
labelAppend: OptionalFieldLabel,
},
};
export const threatIndicatorPathRequiredSchemaValue = {
type: FIELD_TYPES.TEXT,
label: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel',
{
defaultMessage: 'Indicator prefix override',
}
),
helpText: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText',
{
defaultMessage:
'Specify the document prefix containing your indicator fields. Used for enrichment of indicator match alerts.',
}
),
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError',
{
defaultMessage: 'Indicator prefix override must not be empty',
}
)
),
type: VALIDATION_TYPES.FIELD,
},
],
};

View file

@ -117,3 +117,69 @@ export const getCreateMlRulesOptionsMock = (
exceptionsList: [],
actions: [],
});
export const getCreateThreatMatchRulesOptionsMock = (
isRuleRegistryEnabled: boolean
): CreateRulesOptions => ({
actions: [],
anomalyThreshold: undefined,
author: ['Elastic'],
buildingBlockType: undefined,
concurrentSearches: undefined,
description: 'some description',
enabled: true,
eventCategoryOverride: undefined,
exceptionsList: [],
falsePositives: ['false positive 1', 'false positive 2'],
filters: [],
from: 'now-1m',
immutable: false,
index: ['*'],
interval: '5m',
isRuleRegistryEnabled,
itemsPerSearch: undefined,
language: 'kuery',
license: 'Elastic License',
machineLearningJobId: undefined,
maxSignals: 100,
meta: {},
name: 'Query with a rule id',
note: '# sample markdown',
outputIndex: 'output-1',
query: 'user.name: root or user.name: admin',
references: ['http://www.example.com'],
riskScore: 80,
riskScoreMapping: [],
ruleId: 'rule-1',
ruleNameOverride: undefined,
rulesClient: rulesClientMock.create(),
savedId: 'savedId-123',
severity: 'high',
severityMapping: [],
tags: [],
threat: [],
threatFilters: undefined,
threatIndex: ['filebeat-*'],
threatIndicatorPath: 'threat.indicator',
threatLanguage: 'kuery',
threatMapping: [
{
entries: [
{
field: 'file.hash.md5',
type: 'mapping',
value: 'threat.indicator.file.hash.md5',
},
],
},
],
threatQuery: '*:*',
threshold: undefined,
throttle: null,
timelineId: 'timelineid-123',
timelineTitle: 'timeline-title-123',
timestampOverride: undefined,
to: 'now',
type: 'threat_match',
version: 1,
});

View file

@ -6,7 +6,11 @@
*/
import { createRules } from './create_rules';
import { getCreateMlRulesOptionsMock } from './create_rules.mock';
import {
getCreateMlRulesOptionsMock,
getCreateThreatMatchRulesOptionsMock,
} from './create_rules.mock';
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../common/constants';
describe.each([
['Legacy', false],
@ -44,4 +48,34 @@ describe.each([
})
);
});
it('populates a threatIndicatorPath value for threat_match rule if empty', async () => {
const ruleOptions = getCreateThreatMatchRulesOptionsMock(isRuleRegistryEnabled);
delete ruleOptions.threatIndicatorPath;
await createRules(ruleOptions);
expect(ruleOptions.rulesClient.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
params: expect.objectContaining({
threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH,
}),
}),
})
);
});
it('does not populate a threatIndicatorPath value for other rules if empty', async () => {
const ruleOptions = getCreateMlRulesOptionsMock(isRuleRegistryEnabled);
delete ruleOptions.threatIndicatorPath;
await createRules(ruleOptions);
expect(ruleOptions.rulesClient.create).not.toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
params: expect.objectContaining({
threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH,
}),
}),
})
);
});
});

View file

@ -13,7 +13,11 @@ import {
} from '../../../../common/detection_engine/utils';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { SanitizedAlert } from '../../../../../alerting/common';
import { NOTIFICATION_THROTTLE_NO_ACTIONS, SERVER_APP_ID } from '../../../../common/constants';
import {
DEFAULT_INDICATOR_SOURCE_PATH,
NOTIFICATION_THROTTLE_NO_ACTIONS,
SERVER_APP_ID,
} from '../../../../common/constants';
import { CreateRulesOptions } from './types';
import { addTags } from './add_tags';
import { PartialFilter, RuleTypeParams } from '../types';
@ -115,7 +119,9 @@ export const createRules = async ({
*/
threatFilters: threatFilters as PartialFilter[] | undefined,
threatIndex,
threatIndicatorPath,
threatIndicatorPath:
threatIndicatorPath ??
(type === 'threat_match' ? DEFAULT_INDICATOR_SOURCE_PATH : undefined),
threatQuery,
concurrentSearches,
itemsPerSearch,