Skip to content

M2: rule engine — YAML rules, field matching, ATT&CK, trace replay - #2

Merged
dylanpatriarchi merged 1 commit into
mainfrom
feat/m2-rule-engine
Aug 1, 2026
Merged

dylanpatriarchi merged 1 commit into
mainfrom
feat/m2-rule-engine

Conversation

@dylanpatriarchi

Copy link
Copy Markdown
Owner

Milestone 2 — rule engine v1

Stack position: 1 of 6 · base main · next: M3 process collector

Educational project — read-only and defensive. Argus observes and reports; it never injects into, tampers with, hides from, or modifies another process.

Declarative 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 result

target.path on a network event is absent, not empty, and every operator except not_exists fails 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, CIDR

Each 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_under compares whole components — /etcetera/passwd is not under /etc, the trap a naive startsWith falls into. Traversal is normalized first.
  • CIDR families never mix; an IPv4 address is never inside an IPv6 range.

rule.nim / matcher.nim — 19 operators

and/or/not composition, 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 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:

mypack.yaml: rule B: rules[1].match.field: unknown field path 'bogus.path'

engine.nim — matching and replay

Alerts are timestamped from the triggering event, not the wall clock, so a replayed trace reproduces exactly the alerts it recorded. Per-rule statistics. argus rules and argus replay on 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 KnownFields and getField drifting apart — it already caught a real gap while I was writing it.

$ nimble test    # 296 [OK], 0 [FAILED]
$ ./argus replay tests/data/sample_trace.jsonl tests/data/rules_replay.yaml

Three rules fire on the sample trace and three stay silent — including the /etc rule correctly not tripping on the benign apt activity in the trace, which is how a rule pack becomes noise.

Dependencies

NimYAML, and pure-Nim regex rather than std/re — no libpcre to install, and it builds identically on macOS and Linux.

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.
@dylanpatriarchi
dylanpatriarchi merged commit 9932d31 into main Aug 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant