M2: rule engine — YAML rules, field matching, ATT&CK, trace replay - #2
Merged
Merged
Conversation
Declarative YAML rules matched against normalized events, with ATT&CK on every alert and trace replay as the testing mechanism. - fields.nim: dotted field paths, with absence as a first-class result. target.path on a network event is absent, not empty, and every operator except not_exists fails against absent — that is what lets rules skip defensive category guards. A field the collector could not read is absent for the same reason: 'not observed' and 'observed empty' differ. - patterns.nim: glob, path containment and CIDR as pure functions. Glob's * does not cross '/', so a rule watching one directory does not silently watch a tree. path_under compares whole components, so /etcetera is not inside /etc. CIDR families never mix. - rule.nim / matcher.nim: 19 operators, and/or/not composition, cheap category+action prefilters. Negated operators over list fields mean 'no element matches' — the other reading is true of nearly every command line and would quietly neuter the rule. - ruleload.nim: YAML loading that validates everything checkable up front — unknown field paths, unknown operators, uncompilable regexes, unparseable CIDRs, gt with two values, duplicate ids, one-step correlation rules. A detection that silently never fires is worse than one that refuses to load, so errors name the file, the rule and the path inside it. - engine.nim: single-event matching, per-rule statistics, and replay. Alerts are timestamped from the triggering event rather than the wall clock, so a replayed trace reproduces exactly the alerts it recorded. - CLI: argus rules and argus replay. Correlation rules parse and validate here but stay inert until milestone 6. 198 new tests (296 total), including a guard that KnownFields and getField cannot drift apart. Dependencies: NimYAML, and pure-Nim regex so there is no libpcre to install.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Milestone 2 — rule engine v1
Stack position: 1 of 6 · base
main· next: M3 process collectorDeclarative YAML rules matched against normalized events, with MITRE ATT&CK on every alert and trace replay as the testing mechanism.
fields.nim— field paths, with absence as a first-class resulttarget.pathon a network event is absent, not empty, and every operator exceptnot_existsfails against absent. That's what lets rules skip defensive category guards. A field the collector could not read is absent for the same reason — "not observed" and "observed empty" are different facts, and conflating them fires rules on every process Argus lacked permission to inspect.patterns.nim— glob, path containment, CIDREach is easy to get subtly wrong in ways that become false negatives, so they're pure functions tested directly:
*does not cross/, so a rule watching/etc/*doesn't silently watch the whole tree.path_undercompares whole components —/etcetera/passwdis not under/etc, the trap a naivestartsWithfalls into. Traversal is normalized first.rule.nim/matcher.nim— 19 operatorsand/or/notcomposition, plus a cheap category+action prefilter so most rules are skipped for most events. One subtlety worth the review: negated operators over list fields mean "no element matches". The other reading — "some argument differs" — is true of nearly every command line and would quietly neuter the rule.ruleload.nim— validate everything up frontUnknown field paths, unknown operators, uncompilable regexes, unparseable CIDRs,
gtwith two values, duplicate ids, one-step correlation rules. A detection that silently never fires is worse than one that refuses to load, so errors name the file, the rule, and the path inside it:engine.nim— matching and replayAlerts are timestamped from the triggering event, not the wall clock, so a replayed trace reproduces exactly the alerts it recorded. Per-rule statistics.
argus rulesandargus replayon the CLI.Correlation rules parse and validate here but stay inert until M6.
Verification
198 new tests, 296 total, all green. One of them guards against
KnownFieldsandgetFielddrifting apart — it already caught a real gap while I was writing it.Three rules fire on the sample trace and three stay silent — including the
/etcrule correctly not tripping on the benignaptactivity in the trace, which is how a rule pack becomes noise.Dependencies
NimYAML, and pure-Nim
regexrather thanstd/re— no libpcre to install, and it builds identically on macOS and Linux.