The detection primitive
GRAFT is built on a single fact about lossless compression: a model that predicts the next byte well assigns it a short code, and a model thatâs surprised assigns a long one. Train that predictor on a corpus and you get a numeric readout, bits-per-byte, of how well any new input matches what the model learned.
GRAFT applies this at the file level. Train the predictor on a repoâs own benign source, then run it over every file a PR touches. Files that compress the way the rest of the repo compresses score low BPB. Files carrying something the model never saw, obfuscated logic, encoded payloads, foreign binary content, score high.
This sidesteps the two usual weaknesses of supply chain scanning. Thereâs no signature database to keep current against novel payload formats. Thereâs no labeled set of âattack examplesâ to train a classifier on, which means no assumption about what tomorrowâs attack looks like syntactically. The only assumption is that foreign content introduced into a codebase tends to have a different byte distribution than the codebase itself. Base64 blobs, eval obfuscation chains, null-byte stuffed configs, and minified JS all satisfy that assumption without needing to be enumerated in advance.
Baseline construction
The baseline is built fresh per repo:
git ls-filesenumerates everything tracked at HEAD.- Binaries and files under 64 bytes are dropped.
- Whatâs left is packed into a binary corpus in SUBSTRATEâs format.
- A MICRO-config model (~3.2M params, 512-byte context window, 64 chunk positions, 128 local / 256 global dim, 1/2/1 encoder/global/decoder layers) trains for
train-steps(default 3000) on CPU, AdamW with cosine LR, finishing in roughly 3 minutes. - A random 20% holdout of that corpus calibrates
baseline_meanandbaseline_stdfor the repo. - The checkpoint is cached under a weekly key and retrained automatically on that cadence.
Because the model only ever sees this repoâs files, the resulting baseline is repo-shaped, not language-shaped. A Python/YAML repo and a pure Go repo produce different checkpoints and different thresholds, each calibrated to its own holdout.
Scoring and severity
At PR time, GRAFT scores each changed file that matches its scanned extension list, which spans common source languages, CI/build/infra files, and package manifests or lockfiles. Files under 64 bytes or over 512 KB, and anything binary, are skipped automatically, as is anything beyond max-files changed files per PR (default 50).
Severity is sigma above the calibrated baseline:
| Tier | Condition |
|---|---|
| CRITICAL | BPB > baseline + 5Ï |
| HIGH | BPB > baseline + 3Ï |
| MEDIUM | BPB > baseline + 2Ï |
| CLEAN | within 2Ï |
threshold-sigma (default 3.0) controls the flag line; lowering it raises sensitivity. On the reference run (mean 1.09, std 0.48, threshold 2.52), the four tested attack payloads landed between 9.04 and 13.33 max BPB, 8 to 26 sigma out, against normal Python and YAML sitting at 1.97 to 2.31 BPB. The separation is wide enough that threshold tuning is about false-positive control, not about catching those payload classes at all.
Results post as a ranked PR comment: a table of flagged files by severity, max BPB, mean BPB, sigma, and window count, followed by a count of files that scanned clean. fail-on-anomaly (default false) controls whether HIGH/CRITICAL findings block the check or just annotate it.
Where the model is blind
GRAFTâs detector has no semantic layer. It doesnât parse the file, doesnât know what an eval() call does, doesnât track data flow. It only measures whether the byte sequence was predictable given the repoâs history. That gives it two failure directions.
False negatives come from attacks that mimic the surrounding fileâs style closely enough, matching variable naming, whitespace, and import conventions, to stay inside the learned distribution. A backdoor written in the same hand as the rest of the file can score clean.
False positives come from legitimate files that are byte-unusual relative to hand-written source: generated protobuf output, minified vendored JS, binary test fixtures. These are addressed with .graftignore, max-files exclusion, or a raised threshold-sigma, rather than by the model learning to distinguish âgeneratedâ from âmalicious,â which it has no basis to do.