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,