From 4ec95de6f6a7f0aaf7124202493ddef102163038 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 18 Nov 2021 15:15:42 -0500 Subject: [PATCH] [CTI] explicit threat_indicator_path for IM rules (#118821) (#119075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ece Özalp --- .../indicator_match_rule.spec.ts | 5 ++ .../security_solution/cypress/objects/rule.ts | 2 + .../cypress/screens/rule_details.ts | 2 + .../rules/step_about_rule/index.test.tsx | 58 ++++++++++++++++ .../rules/step_about_rule/index.tsx | 29 ++++++-- .../rules/step_about_rule/schema.tsx | 30 +++++++++ .../rules/create_rules.mock.ts | 66 +++++++++++++++++++ .../rules/create_rules.test.ts | 36 +++++++++- .../detection_engine/rules/create_rules.ts | 10 ++- 9 files changed, 231 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 378de8f0bc59..ef6db14dba89 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -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); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 0a9eecf83c7f..1c81099d43dd 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -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]`; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index fb1fded1fe8a..cdad6096ece1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 9340ca2af151..01ba47f728e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + 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( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 91e428382dc3..5f5b636d6afe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -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 = ({ 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(initialState.severity.value); const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []); @@ -300,7 +321,7 @@ const StepAboutRuleComponent: FC = ({ /> - {isThreatMatchRule(defineRuleData?.ruleType) && ( + {isThreatMatchRuleValue && ( <> = { 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, + }, + ], +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index 2c25134cc376..c3e15b061842 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -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, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts index 0fd708791712..3d5619ab1306 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts @@ -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, + }), + }), + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 1d0010b38578..5ff5358fbc4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -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,