grithdocs

14. Behavioural anomaly

How unusual an authority-changing call is against the baseline this session has built.

Phase3, context
Score1.0, 2.0 or 3.0
Sourcecrates/grith-proxy/src/filters/behavioural.rs
Config[proxy.filters.behavioural] - the only filter with real tunables

Each session builds its own distribution of call categories. Once there are enough calls to compare against, a call whose category is rare in that distribution scores.

RuleFires when the category isScore
unseen-call-typeabsent from the baseline entirely3.0
rare-call-typeunder 2% of the baseline2.0
uncommon-call-typeunder 5% of the baseline1.0

config.toml

toml
[proxy.filters.behavioural]
enabled                    = true
min_calls_for_baseline     = 200
mild_deviation_score       = 1.0
significant_deviation_score = 3.0

mild_deviation_score is the uncommon-call-type score and significant_deviation_score is the unseen-call-type score. rare-call-type is a fixed 2.0 midpoint and is not tunable.

It only scores five categories

⚠️Ordinary calls are recorded but never scored here

Only ownership_change, filesystem_mutation, cross_process_access, namespace_op and dbus_method_call can produce an anomaly score. Reads, writes, spawns, links and network calls contribute to the baseline every anomaly is measured against, but this filter never scores them.

That is an allowlist, and it is inverted from an earlier denylist on purpose. The denylist had to name every ordinary operation by hand, and file_link was never added - so the first symlink of a session scored 3.0 as an unseen call type. Every npm install ends in a burst of node_modules/.bin/* symlinks and every incremental rustc build makes hardlinks, so the omission spent modal prompts on the observation "this tool has not made a symlink before".

Nothing an attacker was constrained by was lost. The rule scored the novelty of an operation, not the operation: it fires on the first one and decays to silence by about the seventh, so an attacker only had to make theirs the eighth. What actually prices a link is what it exposes - the target and the link path are both evaluated by path-match, sensitive-path-heuristic and allowlist, and the worst wins.

Cold start, and the window

Below min_calls_for_baseline the filter returns nothing at all - a fresh session cannot be anomalous against a baseline it has not built. The history is a sliding window capped at 1000 records, so the baseline stays recent rather than degrading over a long session.

State is keyed per supervised session and dropped when the session ends. Unlike taint, it does not follow a conversation across sessions: a behavioural baseline is per-process-lifetime, so a new session correctly cold-starts.

See also

Last updated: 2026-08-24Edit this page on GitHub →