Skip to content

Commit 613f82f

Browse files
jr-araquekibanamachineelasticmachine
authored andcommitted
[Security Solution][Detection Engine] Adding validations for escaped wildcards on the match operator on rules exceptions (elastic#268397)
## Summary Closes [security-team#17120](elastic/security-team#17120). When a user creates a rule exception with the `matches` operator and types a value containing escaped wildcard metacharacters (`\*`, `\?`), the UI saves silently. Under the hood, the Detection Engine passes the value verbatim to an Elasticsearch `wildcard` query where `\` is the escape character, so `\*` matches a literal asterisk, killing the wildcard semantics the user expected from `matches`. The exception then doesn't behave as intended. This PR extends the existing wildcard-warning machinery (originally added in elastic#182903 for the wrong-operator case) with a second signal that fires when a `matches` entry contains a lone escaped metacharacter. ## Examples ### Valid usage of matches + `*` wildcard <img width="1089" height="894" alt="Screenshot 2026-05-19 at 11 44 01" src="https://github.com/user-attachments/assets/ec9e449b-333e-4bc7-868e-236571abfcc4" /> ### Invalid usage of matches + `\*` escaped wildcard <img width="1475" height="1009" alt="Screenshot 2026-05-19 at 10 55 55" src="https://github.com/user-attachments/assets/926babf9-f393-4b0b-ae24-391088e1107f" /> ### Invalid usage of matches + `\?` escaped wildcard <img width="1476" height="1011" alt="Screenshot 2026-05-19 at 10 56 33" src="https://github.com/user-attachments/assets/39a62fef-1b6e-4058-b9d2-82009c444a95" /> ### Alert dialog before saving rule exception <img width="1474" height="1014" alt="Screenshot 2026-05-19 at 10 56 54" src="https://github.com/user-attachments/assets/a293383a-6441-4c90-b698-f1349fcfd0c8" /> ### Supports multiple validation warnings <img width="1475" height="1011" alt="Screenshot 2026-05-19 at 10 56 43" src="https://github.com/user-attachments/assets/b8c5b4f6-9a73-4378-89b5-85e9d908b941" /> ### Validation on Edti <img width="1476" height="1008" alt="Screenshot 2026-05-19 at 11 44 38" src="https://github.com/user-attachments/assets/976697ef-178f-4614-a0e1-69412d964cf3" /> ### Backslashes handling are ignored <img width="1068" height="468" alt="Screenshot 2026-05-18 at 17 12 10" src="https://github.com/user-attachments/assets/c063fd00-089d-4ea2-8af8-2c2804d28fce" /> ### Changes **`@kbn/securitysolution-list-utils`** - `hasMalformedMatchesValue(items): boolean` — validator using negative-lookbehind regex `/(?<!\\)\\[*?]/`. Fires on lone `\*` / `\?`; deliberately does NOT fire on `\\*` (documented Windows-path pattern, e.g. `C:\\Windows\\*.dll`). - `getMalformedMatchesFields(items): string[]` — companion function returning the field name of each affected entry, used to generate per-entry modal warnings. **`@kbn/securitysolution-exception-list-components`** - `<MalformedMatchesValueCallout />` — aggregate callout explaining that escape sequences match literal characters and suggesting `is` for exact matching. **Both add and edit flyouts (`add_exception_flyout`, `edit_exception_flyout`)** - `malformedMatchesValueExists: boolean` reducer flag — submit gate: `if (wildcardWarningExists || malformedMatchesValueExists)` shows the confirm modal. - `malformedMatchesFields: string[]` reducer field — list of affected entry field names, passed to the modal to produce one warning item per affected entry. - Non-blocking: the modal lets the user confirm and save proceeds unchanged. - Applies to both create and edit flows. **`ArtifactConfirmModal` / `CONFIRM_WARNING_MODAL_LABELS`** (shared with endpoint track, elastic#268477) - Added `hasMalformedMatchesValue?: string[]` to the warnings object. When multiple AND/OR conditions each contain an escape issue, the modal shows one warning item per affected field, leveraging the `listOfWarnings: Array<React.ReactNode>` array introduced by elastic#268477. **Tests** - Unit tests in `kbn-securitysolution-list-utils/src/helpers/index.test.ts` covering: escaped `\*` / `\?`, documented Windows paths (negative — must not trigger), single-backslash paths (positive), `match` entries (negative — only `wildcard` type checked), empty entries, OR conditions, and multiple AND entries producing per-field results. ### Exception filter behavior across rule types Traced the full backend execution path to confirm that wildcard exception entries (`matches` operator, `type: "wildcard"`) behave identically across all rule types. **Central path:** `create_security_rule_type_wrapper.ts` builds the exception filter once via `buildExceptionFilter()` (from `@kbn/lists-plugin`) and passes it to every rule executor through `sharedParams.exceptionFilter`. The resulting ES clause is: ```json { "wildcard": { "<field>": "<value>" } } ``` > ES|QL note: > ES|QL uses esClient.esql.asyncQuery() instead of _search, which accepts a separate filter parameter (standard ES DSL). The exception filter is passed there unchanged; Elasticsearch applies it at the shard level before the ES|QL query runs. No translation to a WHERE clause is performed or needed. So, the Elasticsearch wildcard semantics apply uniformly across all rule types (query, EQL, ES|QL, threshold, new terms, ML, indicator match, saved query): - \* matches a literal asterisk, not a wildcard - \? matches a literal question mark, not a wildcard - \\ matches a literal backslash ### Out of scope (and maybe for a follow-up) - For bare `*` / `?` values in `matches` (e.g., a value of just `*` matching everything) we could handle it by a separate validator with its own callout copy. The AC mentions this as a separate signal. - Lone `\` (single backslash) and unpaired `\\` cases, could also be on a separate validator. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials -- Already in place, under: https://www.elastic.co/docs/solutions/security/detect-and-alert/add-manage-exceptions#detection-rule-exceptions - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Identify risks - [x] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | Risk | Severity | Mitigation | |---|---|---| | **False positives on documented Windows paths** (e.g. `C:\\Windows\\*.dll`) | Medium → Low | Negative-lookbehind regex `(?<!\\)\\[*?]` only flags `\` immediately before `*`/`?` when not itself preceded by `\`. Test coverage explicitly asserts the documented Windows-path pattern does not trigger. | | **Per-entry modal warnings on large exception items** | Low | `malformedMatchesFields` is bounded by the number of entries the user has added in the flyout — not a pagination or unbounded list concern. | | **Co-firing with existing `wildcardWarningExists` callout** | Low | Both are non-blocking, use the same modal gate, and render as separate list items in the modal's `listOfWarnings`. UX copy sign-off with @approksiu recommended before marking ready for review. | | **Behavioral change to save flow** | None | Validator only adds a confirmation step; on confirm, save proceeds via the unchanged `submitException()` path. No data shape changes. | ## Release note Adds a warning callout and confirmation modal to the rule exceptions form when a user enters escaped characters (e.g. `\*` or `\?`) with the matches operator, indicating they may have intended wildcards instead. The warning is non-blocking, users can acknowledge and save. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent c12ca27 commit 613f82f

11 files changed

Lines changed: 391 additions & 43 deletions

File tree

x-pack/solutions/security/packages/kbn-securitysolution-exception-list-components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export * from './src/header_menu';
1717
export * from './src/generate_linked_rules_menu_item';
1818
export * from './src/unnecessary_escaping_callout';
1919
export * from './src/wildcard_with_wrong_operator_callout';
20+
export * from './src/malformed_matches_value_callout';
2021
export * from './src/partial_code_signature_callout';

x-pack/solutions/security/packages/kbn-securitysolution-exception-list-components/moon.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependsOn:
2222
- '@kbn/i18n'
2323
- '@kbn/i18n-react'
2424
- '@kbn/kibana-react-plugin'
25+
- '@kbn/securitysolution-list-utils'
2526
tags:
2627
- shared-browser
2728
- package
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { FormattedMessage } from '@kbn/i18n-react';
10+
import { i18n } from '@kbn/i18n';
11+
import { EuiCallOut } from '@elastic/eui';
12+
import { isOperator, matchesOperator } from '@kbn/securitysolution-list-utils';
13+
14+
export const MalformedMatchesValueCallout = () => {
15+
return (
16+
<EuiCallOut
17+
title={i18n.translate('exceptionList-components.malformedMatchesValueCallout.title', {
18+
defaultMessage: 'Please review your entries',
19+
})}
20+
iconType="warning"
21+
color="warning"
22+
size="s"
23+
data-test-subj="malformedMatchesValueCallout"
24+
>
25+
<p>
26+
<FormattedMessage
27+
id="exceptionList-components.malformedMatchesValueCallout.body"
28+
defaultMessage="An entry uses {matches} with a value containing escape sequences (e.g. {escapedStar}, {escapedQuestion}). These will {literalCharacters}, not wildcard patterns (for example {escapedStar2} matches a literal asterisk). If you intended an exact match, {changeOperator} to {is}."
29+
values={{
30+
matches: <strong>{matchesOperator.message}</strong>,
31+
is: <strong>{isOperator.message}</strong>,
32+
escapedStar: <code>{'\\*'}</code>,
33+
escapedStar2: <code>{'\\*'}</code>,
34+
escapedQuestion: <code>{'\\?'}</code>,
35+
literalCharacters: (
36+
<strong>
37+
{i18n.translate(
38+
'exceptionList-components.malformedMatchesValueCallout.literalCharacters',
39+
{ defaultMessage: 'match literal characters' }
40+
)}
41+
</strong>
42+
),
43+
changeOperator: (
44+
<strong>
45+
{i18n.translate(
46+
'exceptionList-components.malformedMatchesValueCallout.changeOperator',
47+
{ defaultMessage: 'change the operator' }
48+
)}
49+
</strong>
50+
),
51+
}}
52+
/>
53+
</p>
54+
</EuiCallOut>
55+
);
56+
};

x-pack/solutions/security/packages/kbn-securitysolution-exception-list-components/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@kbn/i18n",
2121
"@kbn/i18n-react",
2222
"@kbn/kibana-react-plugin",
23+
"@kbn/securitysolution-list-utils",
2324
],
2425
"exclude": [
2526
"target/**/*",

x-pack/solutions/security/packages/kbn-securitysolution-list-utils/src/helpers/index.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
hasWrongOperatorWithWildcard,
1212
hasEscaping,
1313
hasPartialCodeSignatureEntry,
14+
getMalformedMatchesFields,
1415
getOperatorOptions,
1516
hasEntryEscaping,
1617
} from '.';
@@ -643,6 +644,188 @@ describe('Helpers', () => {
643644
});
644645
});
645646

647+
describe('getMalformedMatchesFields', () => {
648+
test('it returns the field name for a wildcard entry with an escaped asterisk', () => {
649+
expect(
650+
getMalformedMatchesFields([
651+
{
652+
description: '',
653+
name: '',
654+
type: 'simple',
655+
entries: [
656+
{
657+
type: 'wildcard',
658+
value: 'app\\*.exe',
659+
field: 'process.name',
660+
operator: 'included',
661+
},
662+
],
663+
},
664+
])
665+
).toEqual(['process.name']);
666+
});
667+
668+
test('it returns the field name for a wildcard entry with an escaped question mark', () => {
669+
expect(
670+
getMalformedMatchesFields([
671+
{
672+
description: '',
673+
name: '',
674+
type: 'simple',
675+
entries: [
676+
{ type: 'wildcard', value: 'file\\?.txt', field: 'file.name', operator: 'included' },
677+
],
678+
},
679+
])
680+
).toEqual(['file.name']);
681+
});
682+
683+
test('it returns multiple field names when multiple entries have escaped wildcards', () => {
684+
expect(
685+
getMalformedMatchesFields([
686+
{
687+
description: '',
688+
name: '',
689+
type: 'simple',
690+
entries: [
691+
{
692+
type: 'wildcard',
693+
value: 'app\\*.exe',
694+
field: 'process.name',
695+
operator: 'included',
696+
},
697+
{
698+
type: 'wildcard',
699+
value: 'file\\?.txt',
700+
field: 'file.name',
701+
operator: 'included',
702+
},
703+
],
704+
},
705+
])
706+
).toEqual(['process.name', 'file.name']);
707+
});
708+
709+
test('it collects fields across OR conditions (multiple items)', () => {
710+
expect(
711+
getMalformedMatchesFields([
712+
{
713+
description: '',
714+
name: '',
715+
type: 'simple',
716+
entries: [
717+
{
718+
type: 'wildcard',
719+
value: 'app\\*.exe',
720+
field: 'process.name',
721+
operator: 'included',
722+
},
723+
],
724+
},
725+
{
726+
description: '',
727+
name: '',
728+
type: 'simple',
729+
entries: [
730+
{
731+
type: 'wildcard',
732+
value: 'file\\?.txt',
733+
field: 'file.name',
734+
operator: 'included',
735+
},
736+
],
737+
},
738+
])
739+
).toEqual(['process.name', 'file.name']);
740+
});
741+
742+
test('it returns empty array when no entries have escaped wildcards', () => {
743+
expect(
744+
getMalformedMatchesFields([
745+
{
746+
description: '',
747+
name: '',
748+
type: 'simple',
749+
entries: [
750+
{
751+
type: 'wildcard',
752+
value: 'chrome*.exe',
753+
field: 'process.name',
754+
operator: 'included',
755+
},
756+
],
757+
},
758+
])
759+
).toEqual([]);
760+
});
761+
762+
test('it returns empty array for an empty entries list', () => {
763+
expect(
764+
getMalformedMatchesFields([
765+
{
766+
description: '',
767+
name: '',
768+
type: 'simple',
769+
entries: [],
770+
},
771+
])
772+
).toEqual([]);
773+
});
774+
775+
test('it excludes entries with an empty field name', () => {
776+
expect(
777+
getMalformedMatchesFields([
778+
{
779+
description: '',
780+
name: '',
781+
type: 'simple',
782+
entries: [{ type: 'wildcard', value: 'app\\*.exe', field: '', operator: 'included' }],
783+
},
784+
])
785+
).toEqual([]);
786+
});
787+
788+
test('it does not include match entries even if value contains escape sequences', () => {
789+
expect(
790+
getMalformedMatchesFields([
791+
{
792+
description: '',
793+
name: '',
794+
type: 'simple',
795+
entries: [
796+
{
797+
type: 'match',
798+
value: 'app\\*.exe',
799+
field: 'process.name',
800+
operator: 'included',
801+
},
802+
],
803+
},
804+
])
805+
).toEqual([]);
806+
});
807+
808+
test('it does not flag a double-backslash before a wildcard (Windows path separator)', () => {
809+
expect(
810+
getMalformedMatchesFields([
811+
{
812+
description: '',
813+
name: '',
814+
type: 'simple',
815+
entries: [
816+
{
817+
type: 'wildcard',
818+
value: 'C:\\\\Windows\\\\*.dll',
819+
field: 'file.path',
820+
operator: 'included',
821+
},
822+
],
823+
},
824+
])
825+
).toEqual([]);
826+
});
827+
});
828+
646829
describe('getOperatorOptions', () => {
647830
const getItem = (overrides?: {
648831
nested?: FormattedBuilderEntry['nested'];

x-pack/solutions/security/packages/kbn-securitysolution-list-utils/src/helpers/index.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,49 +1027,43 @@ export const getMappingConflictsInfo = (field: DataViewField): FieldConflictsInf
10271027
return conflicts;
10281028
};
10291029

1030+
/** Flattens exception items into a single list of leaf entries, expanding nested entry children. */
1031+
const flattenExceptionEntries = (
1032+
items: Array<Pick<ExceptionsBuilderReturnExceptionItem, 'entries'>>
1033+
): BuilderEntry[] =>
1034+
items
1035+
.flatMap((item) => item.entries)
1036+
.flatMap((item) => (item.type === 'nested' ? item.entries : item));
1037+
1038+
/** Narrows a BuilderEntry to one that uses the wildcard operator with a string value. */
1039+
const isWildcardStringEntry = (
1040+
builderEntry: BuilderEntry
1041+
): builderEntry is BuilderEntry & { type: 'wildcard'; value: string } =>
1042+
builderEntry.type === 'wildcard' &&
1043+
'value' in builderEntry &&
1044+
typeof builderEntry.value === 'string';
1045+
10301046
/**
10311047
* Given an exceptions list, determine if any entries have an "IS" operator with a wildcard value
10321048
*/
10331049
export const hasWrongOperatorWithWildcard = (
10341050
items: ExceptionsBuilderReturnExceptionItem[]
1035-
): boolean => {
1036-
// flattens array of multiple entries added with OR
1037-
const multipleEntries = items.flatMap((item) => item.entries);
1038-
// flattens nested entries
1039-
const allEntries = multipleEntries.flatMap((item) => {
1040-
if (item.type === 'nested') {
1041-
return item.entries;
1042-
}
1043-
return item;
1044-
});
1045-
1046-
// eslint-disable-next-line array-callback-return
1047-
return allEntries.some((e) => {
1048-
if (e.type !== 'list' && 'value' in e) {
1051+
): boolean =>
1052+
flattenExceptionEntries(items).some((e) => {
1053+
if (e.type !== 'list' && 'value' in e && e.value != null) {
10491054
return validateHasWildcardWithWrongOperator({
10501055
operator: e.type,
10511056
value: e.value,
10521057
});
10531058
}
1059+
return false;
10541060
});
1055-
};
10561061

10571062
export const hasEscaping = (
10581063
items: Array<Pick<ExceptionsBuilderReturnExceptionItem, 'entries'>>,
10591064
osTypes: ExceptionsBuilderReturnExceptionItem['os_types'] = ['linux']
1060-
): boolean => {
1061-
// flattens array of multiple entries added with OR
1062-
const multipleEntries = items.flatMap((item) => item.entries);
1063-
// flattens nested entries
1064-
const allEntries = multipleEntries.flatMap((item) => {
1065-
if (item.type === 'nested') {
1066-
return item.entries;
1067-
}
1068-
return item;
1069-
});
1070-
1071-
return allEntries.some((builderEntry) => hasEntryEscaping(builderEntry, osTypes));
1072-
};
1065+
): boolean =>
1066+
flattenExceptionEntries(items).some((builderEntry) => hasEntryEscaping(builderEntry, osTypes));
10731067

10741068
export const hasEntryEscaping = (
10751069
builderEntry: BuilderEntry,
@@ -1104,6 +1098,29 @@ export const hasEntryEscaping = (
11041098
);
11051099
};
11061100

1101+
// Detect `matches` entries with escaped wildcard metacharacters (\* or \?). Uses a negative
1102+
// lookbehind so that \\* (double-backslash path separator + wildcard) does not trigger — only
1103+
// a lone \* or \? (escaped metacharacter) fires the warning.
1104+
const ESCAPED_WILDCARD_REGEX = /(?<!\\)\\[*?]/;
1105+
1106+
export const getMalformedMatchesFields = (
1107+
items: ExceptionsBuilderReturnExceptionItem[]
1108+
): string[] => {
1109+
const flattenedEntries = flattenExceptionEntries(items);
1110+
1111+
return flattenedEntries.reduce<string[]>((result, builderEntry) => {
1112+
if (
1113+
isWildcardStringEntry(builderEntry) &&
1114+
builderEntry.field !== '' &&
1115+
ESCAPED_WILDCARD_REGEX.test(builderEntry.value)
1116+
) {
1117+
return [...result, builderEntry.field];
1118+
}
1119+
1120+
return result;
1121+
}, []);
1122+
};
1123+
11071124
/**
11081125
* Event filters helper where given an exceptions list,
11091126
* determine if both 'subject_name' and 'trusted' are

0 commit comments

Comments
 (0)