Skip to content

v0.1.5 - rules stop scanning payloads they cannot match

Latest

Choose a tag to compare

@k3vs3c k3vs3c released this 25 Aug 00:48
· 23 commits to main since this release

Performance release. Matching the bundled rule pack against a 32 KB tool
call was 23 ms in 0.1.4. It is now 2.8 ms.

It also corrects something 0.1.4 got wrong. That release shipped a README
claiming sub-millisecond median overhead, a figure measured when the pack
had 2 rules, alongside the 32 rules it actually released with. Anyone running
python scripts/benchmark.py, the command the README itself gives, would have
seen the contradiction immediately. The claim is gone and the numbers are
published in full, including the row that looks worst.

Rules skip regexes that cannot match

A rule looking for auditctl cannot fire on a payload with no auditctl in it,
but it was scanning every byte to find that out. Each pattern is now read once
and reduced to the literals it requires, and cheap substring searches decide
whether the regex runs at all. On a benign 32 KB call, 17 of the 19 patterns
that would otherwise scan the payload never run.

Requirements are conjunctions. Rule aileron-162 needs systemctl near
disable near auditd, so requiring only the first set meant ordinary prose
containing the word "service" still paid for a full scan.

This changes speed and nothing else. The risk is one-sided: skipping a regex
that would have matched is a rule that silently stops firing while the journal
still looks clean. So anything the extractor cannot fully read returns "run the
regex", and AILERON_NO_PREFILTER=1 disables it entirely.

Case folding was the trap. The prefilter needs a case-insensitive
containment test agreeing with re.IGNORECASE, and neither obvious choice
works. re.search("s", "ſ", re.I) matches but "ſ".lower() does not change,
and re.search("i", "ı", re.I) matches but "ı".casefold() does not change
either. Worse, U+0130 casefolds to two codepoints, splitting a
multi-character literal apart. Exactly four codepoints in Unicode are case-equal
to an ASCII character, and a test proves over all 1,114,112 of them that each
folds to exactly the character it equals.

Soundness is checked by a differential run over all 337 rule-pack examples, a
property test on generated patterns, a mutation fuzz over 31,570 mutants that
still match their pattern, and an independently written adversarial attack that
verified 2,324,740 matching pairs. None found a verdict difference.

The benchmark was flattering itself

It sent "x" * n, which is the friendliest possible input both to a regex
engine and to a literal prefilter. That overstated the result by about 3x, and
the README publishes those numbers. It now sends fixed text that looks like real
tool arguments, with a test asserting no bundled rule fires on it.

Measured on the CI runner with all 32 rules loaded:

tool arguments added by proxy added by rules added total
64 B 0.16 ms 0.31 ms 0.47 ms
4 KB 0.20 ms 0.46 ms 0.66 ms
32 KB 0.55 ms 2.24 ms 2.79 ms

The baseline now records the rule count and the payload shape, so changing
either is reported as more work rather than as slower code.

Fixed

The adoption metrics counted our own snapshot bot as an external contributor,
so the headline read 1 when the truth was 0. That figure exists to show whether
anyone outside the project cares, so it inflating itself was the worst place
for the bug to be.

Upgrading

No format changes, no API changes, no action required. Journals written by
0.1.2 through 0.1.4 verify unchanged.

pip install --upgrade aileron