diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx index d1287472601d5..04cbfd3f3a012 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/threat_match_mapping_edit.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import type { DataViewBase } from '@kbn/es-query'; import type { ThreatMapEntries } from '../../../../common/components/threat_match/types'; import type { FieldConfig } from '../../../../shared_imports'; import { UseField } from '../../../../shared_imports'; -import { threatMatchMappingValidator } from './validators/threat_match_mapping_validator'; +import { threatMatchMappingValidatorFactory } from './validators/threat_match_mapping_validator_factory'; import { ThreatMatchMappingField } from './threat_match_mapping_field'; import * as i18n from './translations'; @@ -25,10 +25,22 @@ export const ThreatMatchMappingEdit = memo(function ThreatMatchMappingEdit({ indexPatterns, threatIndexPatterns, }: ThreatMatchMappingEditProps): JSX.Element { + const fieldConfig: FieldConfig = useMemo( + () => ({ + label: i18n.THREAT_MATCH_MAPPING_FIELD_LABEL, + validations: [ + { + validator: threatMatchMappingValidatorFactory({ indexPatterns, threatIndexPatterns }), + }, + ], + }), + [indexPatterns, threatIndexPatterns] + ); + return ( ); }); - -const THREAT_MATCH_MAPPING_FIELD_CONFIG: FieldConfig = { - label: i18n.THREAT_MATCH_MAPPING_FIELD_LABEL, - validations: [ - { - validator: threatMatchMappingValidator, - }, - ], -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts new file mode 100644 index 0000000000000..24a4096107be1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/error_codes.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum THREAT_MATCH_MAPPING_ERROR_CODES { + ERR_FIELD_MISSING = 'ERR_FIELD_MISSING', + ERR_FIELDS_UNKNOWN = 'ERR_FIELDS_UNKNOWN', +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts new file mode 100644 index 0000000000000..eceae001bbd16 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/get_unknown_threat_match_mapping_field_names.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataViewBase } from '@kbn/es-query'; +import type { ThreatMapEntries } from '../../../../../common/components/threat_match/types'; + +interface GetUnknownThreatMatchMappingFieldNamesParams { + entries: ThreatMapEntries[]; + indexPatterns: DataViewBase; + threatIndexPatterns: DataViewBase; +} + +interface GetUnknownThreatMatchMappingFieldNamesResult { + unknownSourceIndicesFields: string[]; + unknownThreatMatchIndicesFields: string[]; +} + +export function getUnknownThreatMatchMappingFieldNames({ + entries, + indexPatterns, + threatIndexPatterns, +}: GetUnknownThreatMatchMappingFieldNamesParams): GetUnknownThreatMatchMappingFieldNamesResult { + const knownIndexPatternsFields = new Set(indexPatterns.fields.map(({ name }) => name)); + const knownThreatMatchIndexPatternsFields = new Set( + threatIndexPatterns.fields.map(({ name }) => name) + ); + + const unknownSourceIndicesFields: string[] = []; + const unknownThreatMatchIndicesFields: string[] = []; + + for (const { entries: subEntries } of entries) { + for (const subEntry of subEntries) { + if (subEntry.field && !knownIndexPatternsFields.has(subEntry.field)) { + unknownSourceIndicesFields.push(subEntry.field); + } + + if (subEntry.value && !knownThreatMatchIndexPatternsFields.has(subEntry.value)) { + unknownThreatMatchIndicesFields.push(subEntry.value); + } + } + } + + return { + unknownSourceIndicesFields, + unknownThreatMatchIndicesFields, + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator.ts deleted file mode 100644 index c9ba13d618470..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { - containsInvalidItems, - singleEntryThreat, -} from '../../../../../common/components/threat_match/helpers'; -import type { FormData, ValidationFunc } from '../../../../../shared_imports'; -import type { ThreatMapEntries } from '../../../../../common/components/threat_match/types'; - -export const threatMatchMappingValidator: ValidationFunc = ( - ...args -) => { - const [{ path, value }] = args; - - if (singleEntryThreat(value)) { - return { - code: 'ERR_FIELD_MISSING', - path, - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError', - { - defaultMessage: 'At least one indicator match is required.', - } - ), - }; - } - - if (containsInvalidItems(value)) { - return { - code: 'ERR_FIELD_MISSING', - path, - message: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError', - { - defaultMessage: 'All matches require both a field and threat index field.', - } - ), - }; - } -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts new file mode 100644 index 0000000000000..15ecc7a600cbb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threat_match_mapping_edit/validators/threat_match_mapping_validator_factory.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { DataViewBase } from '@kbn/es-query'; +import { + containsInvalidItems, + singleEntryThreat, +} from '../../../../../common/components/threat_match/helpers'; +import type { FormData, ValidationFunc } from '../../../../../shared_imports'; +import type { ThreatMapEntries } from '../../../../../common/components/threat_match/types'; +import { THREAT_MATCH_MAPPING_ERROR_CODES } from './error_codes'; +import { getUnknownThreatMatchMappingFieldNames } from './get_unknown_threat_match_mapping_field_names'; + +interface ThreatMatchMappingValidatorFactoryParams { + indexPatterns: DataViewBase; + threatIndexPatterns: DataViewBase; +} + +export function threatMatchMappingValidatorFactory({ + indexPatterns, + threatIndexPatterns, +}: ThreatMatchMappingValidatorFactoryParams): ValidationFunc { + return (...args) => { + const [{ path, value }] = args; + + if (singleEntryThreat(value)) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELD_MISSING, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.createRule.threatMappingField.requiredError', + { + defaultMessage: 'At least one indicator match is required.', + } + ), + }; + } + + if (containsInvalidItems(value)) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELD_MISSING, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.bothFieldNamesRequiredError', + { + defaultMessage: 'All matches require both a field and threat index field.', + } + ), + }; + } + + const { unknownSourceIndicesFields, unknownThreatMatchIndicesFields } = + getUnknownThreatMatchMappingFieldNames({ + entries: value, + indexPatterns, + threatIndexPatterns, + }); + + if (unknownSourceIndicesFields.length > 0 && unknownThreatMatchIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownSourceIndicesFields} field(s) not found in the source events indices and {unknownThreatMatchIndicesFields} field(s) not found in the indicator indices.', + values: { + unknownSourceIndicesFields: `"${unknownSourceIndicesFields.join('", "')}"`, + unknownThreatMatchIndicesFields: `"${unknownThreatMatchIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + + if (unknownSourceIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownSourceIndicesFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownSourceIndicesFields} field(s) not found in the source events indices.', + values: { + unknownSourceIndicesFields: `"${unknownSourceIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + + if (unknownSourceIndicesFields.length > 0) { + return { + code: THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.threatMappingField.unknownIndicatorIndicesFields', + { + defaultMessage: + 'Indicator mapping has unknown fields. {unknownThreatMatchIndicesFields} field(s) not found in the indicator indices.', + values: { + unknownThreatMatchIndicesFields: `"${unknownThreatMatchIndicesFields.join('", "')}"`, + }, + } + ), + }; + } + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts index 9593324a9c224..7990007fae580 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/constants/validation_warning_codes.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api'; import { ESQL_ERROR_CODES } from '../components/esql_query_edit'; +import { THREAT_MATCH_MAPPING_ERROR_CODES } from '../components/threat_match_mapping_edit/validators/error_codes'; const ESQL_FIELD_NAME = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.esqlFieldName', @@ -23,11 +24,19 @@ const EQL_FIELD_NAME = i18n.translate( } ); +const THREAT_MATCH_MAPPING_FIELD_NAME = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.nonBlockingErrorCodes.threatMatchMapping', + { + defaultMessage: 'Indicator mapping', + } +); + export const VALIDATION_WARNING_CODES = [ ESQL_ERROR_CODES.INVALID_ESQL, EQL_ERROR_CODES.FAILED_REQUEST, EQL_ERROR_CODES.INVALID_EQL, EQL_ERROR_CODES.MISSING_DATA_SOURCE, + THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN, ] as const; export const VALIDATION_WARNING_CODE_FIELD_NAME_MAP: Readonly> = { @@ -35,4 +44,5 @@ export const VALIDATION_WARNING_CODE_FIELD_NAME_MAP: Readonly