Skip to content

Commit a201179

Browse files
Mikaayensontradebot-elastic
authored andcommitted
[FR] Add negate DOES NOT MATCH capability to IM rule type (>=9.2) (#5041)
(cherry picked from commit 35b000b)
1 parent fb2f007 commit a201179

File tree

2 files changed

+38
-2
lines changed

2 files changed

+38
-2
lines changed

detection_rules/rule.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,10 +999,14 @@ class ThreatMatchRuleData(QueryRuleData):
999999
@dataclass(frozen=True)
10001000
class Entries:
10011001
@dataclass(frozen=True)
1002-
class ThreatMapEntry:
1002+
class ThreatMapEntry(StackCompatMixin):
10031003
field: definitions.NonEmptyStr
10041004
type: Literal["mapping"]
10051005
value: definitions.NonEmptyStr
1006+
# Use dataclasses.field to avoid shadowing by attribute name "field"
1007+
negate: bool | None = dataclasses.field( # type: ignore[reportIncompatibleVariableOverride]
1008+
metadata={"metadata": {"min_compat": "9.2"}}
1009+
)
10061010

10071011
entries: list[ThreatMapEntry]
10081012

@@ -1035,6 +1039,38 @@ def validate_query(self, meta: RuleMeta) -> None:
10351039

10361040
threat_query_validator.validate(self, meta)
10371041

1042+
def validate(self, meta: RuleMeta) -> None: # noqa: ARG002
1043+
"""Validate negate usage and group semantics for threat mapping."""
1044+
1045+
for idx, group in enumerate(self.threat_mapping or []):
1046+
entries = group.entries or []
1047+
1048+
# Enforce: DOES NOT MATCH entries are allowed only if there is at least
1049+
# one MATCH (non-negated) entry in the same group
1050+
has_negate = any(bool(getattr(e, "negate", False)) for e in entries)
1051+
has_match = any(not bool(getattr(e, "negate", False)) for e in entries)
1052+
if has_negate and not has_match:
1053+
msg = (
1054+
f"threat_mapping group {idx}: DOES NOT MATCH entries require at least one MATCH "
1055+
"(non-negated) entry in the same group."
1056+
)
1057+
raise ValidationError(msg)
1058+
1059+
# Track negate presence per (source.field, indicator.field) pair to detect
1060+
# conflicts where both MATCH and DOES NOT MATCH are defined for the same pair
1061+
pair_to_negates: dict[tuple[str, str], set[bool]] = {}
1062+
for e in entries:
1063+
is_neg = bool(getattr(e, "negate", False))
1064+
pair_to_negates.setdefault((e.field, e.value), set()).add(is_neg)
1065+
1066+
for (src_field, ind_field), flags in pair_to_negates.items():
1067+
if True in flags and False in flags:
1068+
msg = (
1069+
f"threat_mapping group {idx}: cannot define both MATCH and DOES NOT MATCH for the same "
1070+
f"source and indicator fields: '{src_field}' <-> '{ind_field}'."
1071+
)
1072+
raise ValidationError(msg)
1073+
10381074

10391075
# All of the possible rule types
10401076
# Sort inverse of any inheritance - see comment in TOMLRuleContents.to_dict

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "detection_rules"
3-
version = "1.3.30"
3+
version = "1.3.31"
44
description = "Detection Rules is the home for rules used by Elastic Security. This repository is used for the development, maintenance, testing, validation, and release of rules for Elastic Security’s Detection Engine."
55
readme = "README.md"
66
requires-python = ">=3.12"

0 commit comments

Comments
 (0)