Changelog¶
All notable changes are listed here. The format follows Keep a Changelog, and versions follow SemVer.
[Unreleased]¶
[1.1.2] - 2026-10-03¶
Changed¶
- The repository was renamed to
rakshit-737/rootline-provenance-forensics. Docs now live at https://rakshit-737.github.io/rootline-provenance-forensics/ and the container image isghcr.io/rakshit-737/rootline-provenance-forensics. Older entries below keep the old names.
[1.1.1] - 2026-10-03¶
Added¶
rootline.stats: standard-library Wilson, Student-t and log-cluster bootstrap intervals, a paired t-test, exact sign and sign-flip tests, and p-value formatting that never prints 0.rootline.provenance: every result file written byscripts/bench.pyandscripts/ablation.pycarries a provenance block (script, version, commit, dirty flag, GitHub workflow and run id, Python, platform);results/RESULTS.mdnames the run of each section.- Manual
benchworkflow: regeneratesresults/atlas.json,ablation.jsonandlog4shell.jsonin CI, plus an in-sample re-score of dev/dev2 at HEAD (results/coverage_head.json). The committed files come from run 37092501921. scripts/live/collect_repeatability.py: one row per live-ebpf CI job (checks, root causes, story size, lost events) from the run artefacts, for a stated run-id window.scripts/verify_freeze.py --ref REF(hashgit show REF:path),--ast BASE(docstring-insensitive logic check) and--files; afreeze-recordCI job asserts that the freeze holds at 153eade and 60e3561, that the rule logic at HEAD is unchanged, and thatfreeze_v03.jsonandresults/coverage.jsonare untouched since scoring.scripts/render_tables.py --checkand a "Headline results" block with a source and run-id column; tests keep the README and docs-home headline tables identical to it, and the README's quickstart output identical to the real CLI output.rootline.bench.sealed_excluding_v03_targets()recomputes coverage.json's derived sealed key from the committed rows (it matches).results/figures/ablation_stops.png: the session-root stop effect per held-out log.download_data.py --only DEST...;docs/protocol.md"State after scoring"; a References section in the README.
Changed¶
- Ablation statistics are over logs, not pivots: exact sign and sign-flip tests and a paired t over per-log means, p-values to 4 significant digits; S1-S4 is reported as per-log ranges with a t(3) interval on the difference, because four logs cannot support a distribution-free test.
render_tables.pycomputes every Wilson interval once from the counts, gives the alert-precision proxy its pre-registered interval, adds across-log intervals and paired tests to the IOC-pivot and anomaly tables, the M2/M4 (new exploit) split, ATLAS's own cleaned list under our scorer as the like-for-like reference, 5-seed ranges for the LSTM reproduction, and separate rows for the detailed live run and the repeatability window.scripts/bench.pyruns atlas + log4shell by default. Rule coverage runs only with--only coverage, behind the freeze check;--allow-unfrozenscores dev/dev2 intocoverage_head.json. Parts that were not run print "reused ... (not re-run)".- The replay UI routes edges around vertex labels and shows each IOC once; the static demo serves Mermaid and Cypher exports as text. Hero screenshot refreshed.
- Supply chain: first-party actions pinned by SHA, the base and Neo4j images by digest, and the
image installs its dependencies from the hash-locked
docker/requirements.lock(--require-hashes), which pip-audit also checks; the lock sits outside Dependabot's pip scope, where single-pin bumps (PR #6, pydantic-core) would break it. httpx2joins theapianddevextras (starlette's TestClient prefers it).- Docstrings on every public function and class outside the frozen sources; ruff D101-D103 in lint.
build_demo.py,render_tables.pyandverify_freeze.pyhave--help.
Fixed¶
- A JSONL capture that is not UTF-8 text crashed
analyze/verifywith a traceback and madePOST /api/analyzereturn 500; it is now a one-line error and HTTP 422. bench.py --only atlasandablation.pyoverwrote their results with empty files when the ATLAS data was missing; they now exit with an error and write nothing.bench.py --only log4shellfinds the OTRF captures where the downloader puts them.- Full-method held-out precision is 0.62 (0.6249), not 0.63; Wilson bounds rounded twice are corrected (27/45 is [0.45, 0.73], 4/64 is [0.02, 0.15]; ADR 0009 erratum).
- Live eBPF repeatability: the window missed run 36998890924 and over-counted the Dependabot runs. Over 15 pushes, 71 of 72 completed jobs pass (Wilson [0.925, 0.998]) and the chain is recovered in 72 of 72; the 10 Dependabot runs (50/50 jobs) are listed by id.
- Wording: hit@3 is 0.40-0.50; time + stops is the highest held-out F1 point estimate, not a
significant winner; the paper's Table 3 event counts equal the
+lines for 9 of 10 attacks; all of Table 4's starting entities are listed; the LSTM setup is attributed to ATLAS's released code.
Documented¶
- The sealed numbers belong to commit 60e3561: the normaliser, graph and Sysmon/auditd loaders changed in logic after scoring (f1e71e6, bc6984d), and docstrings changed in frozen rule files (284c125, 580470b). Re-scoring dev/dev2 with the current code reproduces all 73 rows.
- 8 of the 12 held-out ATLAS logs replay an S1-S4 exploit; the stop effect also holds on the four logs with a new exploit (M2, M4: +0.12 precision, 4 of 4 higher).
- Roadmap entries for the live multi-sensor ablation, the leave-one-scenario-out redesign with LSTM retraining on M2-M6, and the 1M-event story demo.
- The v1.1.0 tag produced duplicate release runs (37003642235, 37003644268) that failed with "a release with the same tag name already exists"; the release workflow was fixed in 2aad2d0 (serialised per tag, tolerates an existing release).
Results that got worse (published as is)¶
- Held-out full-method precision is reported as 0.62 instead of 0.63.
- The S1-S4 intervals are wider: full F1 0.66 is now given with the per-log range 0.42-0.78 and ΔF1 t(3) interval [0.24, 0.58] instead of a 4-log bootstrap [0.51, 0.78], and the pivot-level p-values (printed as p=0 or p < 1e-6) are replaced by log-level ones (stops: p = 0.0005; full vs naive F1 on held-out logs: p = 0.28).
- IsolationForest vs the user-dir heuristic: the heuristic's advantage is p = 0.039 by sign test with a mean difference CI that includes 0 ([-0.02, 0.33]).
[1.1.0] - 2026-10-02¶
Added¶
- Live eBPF in CI (
live-ebpfjob, five runs per push): the bpftrace probe runs with sudo on the ubuntu-24.04 runner kernel while a benign chain runs entirely inside the runner (TEST-NET server on a dummy interface, throwawayHOME, dummy files); one query must recover the download socket as a root cause plus every chain step. Results inresults/live_ebpf.json; ADR 0008 supersedes ADR 0007. - v0.3 rules RL-019..024 (sudo/doas/setuid abuse, kernel modules, library
hijacking and related techniques), written from the
devanddev2captures. - dev / dev2 / sealed rule protocol (
docs/protocol.md, ADR 0009): 160 sealed Splunk captures pinned atattack_data@b4573ed3by LFS SHA-256, a freeze check (scripts/verify_freeze.py) and the manualsealed-coverageworkflow that scored them once (run 36994398382). Wilson 95 % intervals and an alert-precision proxy. - Ablation study (
rootline.ablation,scripts/ablation.py, ADR 0010): time-respecting traversal, session-root stops, causal spine and "accessed" expansion switched off one at a time, every ground-truth entity as a pivot, ATLAS S1-S4 (design data) vs the 12 M1-M6 host logs (held out), scenario-cluster bootstrap CIs and sign tests. - ATLAS reproduction (
repro/atlas_repro.py, manualatlas-reproworkflow): PyTorch reimplementation of the ATLAS LSTM under the paper's setup, S1-S4 x 5 seeds, with ATLAS's shipped artefacts scored the same way (results/atlas_repro.json). - IsolationForest baselines: a user-directory heuristic and a run without the
exec_user_dirfeature (IForestTagger(drop=...),userdir_ranking). - ATLAS M1 (multi-host) logs in the benchmarks; all ATLAS zips pinned to commit
e46096dwith SHA-256 and size. - Docs: How it works, Evaluation, Reproduce and protocol pages; generated CLI
reference;
docs/hooks.pyso a plainmkdocs build --strictworks. - Repo: issue/PR templates, CODEOWNERS, Dependabot, CITATION.cff; CI on Python 3.10-3.14; private vulnerability reporting and secret scanning enabled.
Changed¶
- Probe v1.2: self filtering by PID instead of process name; process creation
from
wake_up_new_taskfor new thread groups only (threads no longer appear as processes);-f jsonoutput with a fixed record layout; fd maps keyed by process start time; dup2/dup3 path tracking;tsns=timestamps. - The default rule set is v0.3 (RL-001..024). The v1.0
holdoutis renameddev2and is in-sample for v0.3. - IOCs:
ipholds all external addresses,ipv4/ipv6split them by family; loopback, unspecified and link-local addresses and/dev,/proc,/sys,/runpaths are never IOCs; IPv6 sockets are labelled[addr]:port. - API: loopback-only Host allowlist,
X-Rootline: 1required on POST, streamed upload cap, parsing in a worker thread, bounded story store, docs endpoints off by default;servewarns when bound off loopback. - CLI:
verifyreads any format and fails on zero parsed events; one-line errors for missing files;--iforestchecks for its extra first; help text on every option. - Downloads: Splunk URLs pinned to a commit, size check, fail closed without a
SHA-256; default data directory
~/.cache/rootline. - Packaging: SPDX licence string (setuptools >= 77), tests shipped in the sdist,
reproextra, uvicorn indev. Actions bumped to their Node-24 majors; release actions pinned by SHA.
Fixed¶
- Cypher export: a root-cause path containing a newline could escape a
//comment and inject a statement. Telemetry text is now only ever a quoted literal. - Mermaid labels escape quotes and control characters.
- Sysmon
<Event>extraction was quadratic on unterminated tags (ReDoS); it is now linear with a 1 MB line cap. - Malformed numbers, deep JSON and non-finite timestamps no longer crash ingestion.
- The
/demo/URL served a docs page instead of the replay UI (page renamed tolive-demo.md, plus a CI guard). - Release notes extraction from this file (awk escape bug); the v1.0.0 release body was corrected.
mean_ciclips the 95 % t-interval to the metric's valid range (recall in [0, 1], hits in [0, min(k, malicious)], first-hit rank >= 1), so results no longer report impossible bounds such as recall@10 CI [0.837, 1.063].scripts/bench.py --render-onlyre-renders RESULTS.md from cached JSON.- The live chain no longer writes into the real
~/.awsand~/.configof whoever runs it; it refuses to run outside CI unlessROOTLINE_LAB=1.
Results that got worse (published as is)¶
- Rule coverage on the sealed split: v0.3 detects 4 of 64 parsed captures (Wilson 95 % [0.03, 0.15]); 96 of 160 sealed captures are not parsed by the frozen loaders.
- The ATLAS LSTM reproduction does not reach the paper's numbers (entity F1 0.24-0.75 vs 0.89-1.00).
- On held-out ATLAS hosts the full reconstructor's F1 gain over naive reachability is not significant (recall 0.72), and from the analyst IOC the story collapses on 5 of 6 second-hop hosts.
- IsolationForest process ranking is beaten by a one-line user-directory heuristic
(new baseline), and falls to recall@10 0.49 without its
exec_user_dirfeature.
[1.0.0] - 2026-09-26¶
Docs site, static demo, release pipeline, and statistically honest anomaly numbers.
Added¶
- MkDocs Material documentation site on GitHub Pages
(https://rakshit-737.github.io/rootline/), with architecture, datasets,
benchmarks, CLI, Python API (mkdocstrings) and HTTP API reference, the threat
model, ADRs and limitations. It is built with
mkdocs build --strict. - A static build of the attack-replay UI under
/demo/(scripts/build_demo.py). It is rendered from the committed Log4Shell excerpt and one synthetic intrusion. - Release workflow: on a
v*tag it pushes the image toghcr.io/rakshit-737/rootlineand creates a GitHub Release with the wheel and sdist. - CI: a Docker image job that checks the preloaded story is served, and a docker-compose job that imports the Cypher export into Neo4j.
- Committed benchmark results (
results/): ATLAS S1-S4, Splunk dev/holdout coverage, OTRF Log4Shell fusion, plus figures. rootline serve --fuseloads several sensor captures as one story.Dockerfileanddocker-compose.yml, with a Neo4j service for Cypher imports.
Changed¶
- The IsolationForest benchmark now runs 10 seeds and reports the mean with a 95 % t-interval, instead of a single seed.
Fixed¶
reduction_statsrebuilt the attack-sequence set for every edge (O(E*N)). ATLAS S3 took about 10 minutes and now takes under a second. Each ATLAS graph is also built once and shared across benchmarks.
[0.2.0] - 2026-09-26¶
Real public data, real benchmarks, and the spec features the MVP left as TODO.
Added¶
- Real-data loaders (
rootline.loaders): - Sysmon for Linux and Windows XML, including Syslog-JSON and syslog-prefixed lines. DTDs are rejected.
- Raw auditd, with multi-line records grouped by serial.
- Microsoft AUOMS.
- ATLAS pre-processed, line-labelled audit logs.
- Format sniffing, plus
merge_sources()to fuse several sensors on one host (exec dedup). dnsevent kind. Socket vertices carry their resolved domains.- Command-line ATT&CK rules RL-008..RL-018 (
rules_linux.py): GTFOBins sudo shell escapes, decode/obfuscation, ingress transfer, destruction, impaired defenses, tunnelling, scripted ssh, clipboard reads, PwnKit /login -f/ user-dir execution, system-lib implants, discovery bursts. The v0.1 rules stay available asRULES_V01. - IsolationForest process-vertex tagger (
anomaly.py,[ml]extra) and a degree-ranking baseline. - Multi-seed IOC reconstruction. An attacker IP that maps to several sockets is traced from first contact.
- Benchmarks (
rootline.bench,scripts/bench.py): - ATLAS S1-S4 reconstruction against IOC-grep and naive-reachability baselines.
- Reduction and attack-edge preservation.
- Anomaly ranking.
- Tagger coverage on Splunk attack_data with a never-inspected held-out split.
- OTRF Log4Shell single-sensor vs fused.
- Results go to
results/. scripts/download_data.pywith a checksummed manifest (ATLAS, OTRF, Splunk). Idempotent zip extraction with path-traversal checks.- FastAPI service and attack-replay UI (
rootline serve,[api]extra). - Neo4j Cypher export (
--cypher,to_cypher). The STIX bundle is validated withstix2in tests. - CLI: several captures per
analyze, plus--format,--iforest,--cypherandserve. - Docs:
docs/adr/0001-0007,docs/datasets.md,CONTRIBUTING.md,CHANGELOG.md,LICENSE(MIT). - CI: ruff lint job, standard-library-only job, real-capture smoke test.
Changed¶
- Forward traversal now crosses a socket root's
RECEIVEDedge. Before this, the causal spine from a network entry point collapsed to the socket itself. - A root-cause candidate must feed the slice strictly before the pivot. The C2 socket the pivot opened is no longer reported as its own cause.
- Forward traversal no longer expands through session and service roots. The "accessed" set is limited to processes the intrusion created. Entry scoring understands Windows paths and penalises app-state churn.
- Kill-chain stages add privilege-escalation, discovery, lateral-movement and collection.
Fixed¶
- The AUOMS header (
audit(...)withoutmsg=) was not parsed, so the loader returned 0 records.
[0.1.0] - 2026-09¶
- MVP:
- Normalizer, append-only hash-chained provenance graph and CPR-style reduction.
- Seven ATT&CK-mapped rules and a rare-transition model.
- Time-respecting backward/forward reconstruction.
- Story JSON, STIX 2.1 and Mermaid exports.
- Synthetic generator, CLI and bpftrace reference probe.