Skip to content

Commit

Permalink
add warning validation for indicator mapping field names
Browse files Browse the repository at this point in the history
  • Loading branch information
maximpn committed Dec 19, 2024
1 parent b9ffde1 commit 20905ab
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,10 +25,22 @@ export const ThreatMatchMappingEdit = memo(function ThreatMatchMappingEdit({
indexPatterns,
threatIndexPatterns,
}: ThreatMatchMappingEditProps): JSX.Element {
const fieldConfig: FieldConfig<ThreatMapEntries[]> = useMemo(
() => ({
label: i18n.THREAT_MATCH_MAPPING_FIELD_LABEL,
validations: [
{
validator: threatMatchMappingValidatorFactory({ indexPatterns, threatIndexPatterns }),
},
],
}),
[indexPatterns, threatIndexPatterns]
);

return (
<UseField
path={path}
config={THREAT_MATCH_MAPPING_FIELD_CONFIG}
config={fieldConfig}
component={ThreatMatchMappingField}
componentProps={{
indexPatterns,
Expand All @@ -37,12 +49,3 @@ export const ThreatMatchMappingEdit = memo(function ThreatMatchMappingEdit({
/>
);
});

const THREAT_MATCH_MAPPING_FIELD_CONFIG: FieldConfig<ThreatMapEntries[]> = {
label: i18n.THREAT_MATCH_MAPPING_FIELD_LABEL,
validations: [
{
validator: threatMatchMappingValidator,
},
],
};
Original file line number Diff line number Diff line change
@@ -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',
}
Original file line number Diff line number Diff line change
@@ -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,
};
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<FormData, string, ThreatMapEntries[]> {
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('", "')}"`,
},
}
),
};
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -23,16 +24,25 @@ 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<Record<string, string>> = {
[ESQL_ERROR_CODES.INVALID_ESQL]: ESQL_FIELD_NAME,
[EQL_ERROR_CODES.FAILED_REQUEST]: EQL_FIELD_NAME,
[EQL_ERROR_CODES.INVALID_EQL]: EQL_FIELD_NAME,
[EQL_ERROR_CODES.MISSING_DATA_SOURCE]: EQL_FIELD_NAME,
[THREAT_MATCH_MAPPING_ERROR_CODES.ERR_FIELDS_UNKNOWN]: THREAT_MATCH_MAPPING_FIELD_NAME,
};

0 comments on commit 20905ab

Please sign in to comment.