Skip to content

Latest commit

 

History

History
312 lines (253 loc) · 13.6 KB

JSON.md

File metadata and controls

312 lines (253 loc) · 13.6 KB

Atomdrift Scan JSON Report

Every analysis — whether returned by the server, posted by a worker, or written to disk by the CLI — uses the same envelope:

{
  "ml":  { ... curated classification ... },
  "raw": { ... full cleave report ... }
}

ml is Atomdrift Scan. raw is the cleave compact report; the cleave repo owns its schema. Scan stores it with facts reduced to what the models read (named by fp), after the models have read the full set.

Why the field names are short

Field names inside ml are abbreviated on purpose. The envelope is designed to be stored — in object stores, in row-oriented databases, in append-only log streams — and replayed later. The cost of every field name is paid once per record, and there are a lot of records.

prob instead of probability saves seven bytes per record. lvl instead of spelling out the level/verdict is the same trade. rte, why, skip, files follow the same rule. Over a billion analyses the storage difference is not academic, and the names map cleanly to obvious things. The tables below are the key.

ml — MlSection

src/engine.rs.

JSON Rust Type Meaning
v v string Envelope schema version. Currently "7".
prob probability f32 in [0, 1] Probability the verdict was decided on.
lvl level i32 or null Lowest-false-positive-level marker. See The verdict encoding below.
conf conf u8 percent or null Pessimistic display/export confidence derived from lvl; null when no level table applies.
mods model_scores array of RouteScore Per-route ensemble scores. Omitted if empty.
skip skipped_models array of SkippedRoute Routes that were applicable but unused.
version version string Model version: spec, ABI, hash prefix.
eng eng string Scan engine build (CARGO_PKG_VERSION) that produced this report.
analyzed_at analyzed_at string (RFC 3339, UTC) Completion timestamp.
files files array of file summaries Per-file model summaries, including archive members. See ml.files[] entry below.
pids pids array of u32 Running PIDs. Present only on process scans.
deleted deleted bool Whether the on-disk binary was deleted (process scan).

The verdict encoding

The envelope carries no class or threshold. Instead it reports a single number — lvl — that is a property of the file and the model, not of your deploy setting:

lvl is the lowest false-positive budget — in false positives per 100 million benign files — at which the model flags this file as hostile.

A consumer reads it as:

  • lvl in the calibrated grid (0..=25000 today; consumers should tolerate 50000) → the lowest level at which the file fires. Lower means more obviously hostile: l=2 fires even under an extremely strict 2-FP-per-100M budget, while lvl=500 is only caught once you tolerate 500 (lvl=50 == 0.5 FP/M, lvl=1000 == 10 FP/M).
  • lvl == 25001 or lvl == 25002 → off-grid trait-floor override markers (grid_max + 1/2) where Atomdrift Scan manually raised a model-clean result to suspicious because cleave found confident severe traits.
  • lvl == -1 (sentinel) → the file fires at no grid level. Nothing short of disabling the model would flag it — it is clean.
  • lvl == null → manual --threshold-hostile / --threshold-suspicious were supplied, so no level table applies.

conf is the same lvl rendered as a deliberately pessimistic integer percentage for humans and APIs that need a confidence-style field. It is not a posterior probability and it does not replace prob. The current anchors are:

lvl conf
0 100
1 99
2 98
3 97
4 96
5 95
10 94
20 93
30 92
40 91
50 90
60 89
70 88
80 87
90 86
100 85
200 82
300 80
500 78
1000 75
2000 66
5000 54
7500 49
10000 45
15000 38
20000 33
25000 29
25001 28
25002 27
50000 17

Future 50001/50002 override markers are reserved as 16/15. Intermediate non-grid levels round down to the next-lower confidence bucket.

lvl does not depend on -l. It is computed by sweeping the full level grid regardless of the deploy level, so the entire ml envelope (including prob and mods[]) is byte-identical no matter what -l the caller used. That is deliberate: a result can be cached once and shared across every deploy level.

Deriving the verdict

The hostile/suspicious/benign label is not stored — the consumer derives it from lvl and the active level N. N defaults to the model bundle's own default_severity_level (25 on current bundles; see DEFAULT_SEVERITY_LEVEL):

  • hostile when lvl <= N (default: lvl <= 25),
  • suspicious when lvl <= min(grid_max, 3000) (default: lvl <= 3000). The suspicious ceiling is a flat constant (SUSPICIOUS_LEVEL_CEILING, mirrored as SUSPICIOUS_CEILING in server/decision.rs), not a multiple of N — it does not move when the operator changes -l. It is currently set wide (an EXPERIMENTAL 2026-07 widening to surface the weak-signal tail), which knowingly re-admits a low-precision band; see the note on the constant before relying on it.
  • benign otherwise. Note a file with, say, lvl = 5000 is benign under the default caps yet still reports lvl = 5000. Raising -l moves the hostile line only: it can turn that same envelope hostile, but the suspicious ceiling stays where it is.

The Atomdrift Scan CLI/server applies these caps internally to pick exit codes and terminal output; downstream consumers reading stored envelopes apply whichever caps they prefer.

prob is the value the firing decision was made on — the firing route's probability for OR-rule policies, the blend's sigmoid output for learned-blend policies, or the elevating embedded file's probability when an archive member outranked its parent. It is raw model confidence, not a verdict; use conf for the level-derived confidence-style figure.

Each ml.files[] entry carries its own prob, lvl, and conf: the root file (dp=0) repeats the envelope's, and every archive member reports the lowest firing level and derived confidence for that specific member.

RouteScore

src/model.rs.

JSON Rust Type Meaning
rte model string Route name, e.g. az, az/native, az/elf.
prob probability f32 This route's calibrated probability — the value thresholds live in and the verdict is decided on.
raw raw f32 Raw (pre-isotonic) score; surfaced for triage because the calibrated prob saturates its upper tail.
cls classification u8 This route's classification.

SkippedRoute

src/model.rs.

JSON Rust Type Meaning
rte model string Route name.
why reason string Why this route was not scored.

ml.files[] entry

src/engine.rs (build_ml_files). One compact summary per analyzed file: the root file (id = 0), plus one per archive member when the input is an archive. Listing-only members — carried in the raw manifest for --show=all but never classified — are excluded here.

JSON Type Meaning
id u64 File id, matching the entry's id in the raw cleave manifest.
type string Detected file type (empty string if unknown).
prob f32 Per-file model probability. The root repeats ml.prob.
lvl i32 or null Per-file lowest-firing-level marker (same encoding as ml.lvl).
conf u8 percent or null Per-file confidence derived from lvl.

Per-file findings, molecular formulae, and route breakdowns are not duplicated here — they live in the raw cleave report, whose schema the cleave repo owns.

Errors

Errors share a single shape regardless of status code:

{ "error": "string", "detail": "optional chain" }

The status code carries the category; see SERVER_API.md#status-codes.

Records that are not analyses

A file scan writes one line per artifact, and not every line is an analysis. Two kinds answer without one, and say so with source:

  • "source": "bloom" — the known-good/known-bad filters decided it (verdict, scanned).
  • "source": "remote" — under --enriched, isotope13 already held a verdict for these bytes (or this package), and it stands in place of local analysis:
{"path": "pkg:npm/left-pad@1.3.0", "source": "remote", "verdict": "benign",
 "sha256": "870c0fe1…", "fires_at": -1, "analyzed_at": "2026-10-10T23:13:19Z",
 "engine_version": null, "findings": [], "purl": "pkg:npm/left-pad@1.3.0"}

verdict is hostile, suspicious, or benign, graded from fires_at at this run's level exactly as a local verdict is; findings holds at most three (id, crit, desc). --analyze=local never produces one.

A complete example

A hostile verdict produced at level 3:

{
  "ml": {
    "v": "7",
    "prob": 0.998,
    "lvl": 3,
    "conf": 97,
    "mods": [
      { "rte": "az/native", "prob": 0.998, "raw": 0.94, "cls": 2 },
      { "rte": "az",        "prob": 0.71,  "raw": 0.68, "cls": 1 }
    ],
    "skip": [
      { "rte": "az/elf", "why": "wrong-format" }
    ],
    "version": "spec=4 abi=1 hash=8f3a91",
    "eng": "2.2.0",
    "analyzed_at": "2026-05-14T18:22:01Z",
    "files": [
      { "id": 0, "type": "elf", "prob": 0.998, "lvl": 3, "conf": 97 }
    ]
  },
  "raw": { "...": "full cleave AnalysisReport" }
}

A benign verdict (sentinel lvl = -1):

{
  "ml": {
    "v": "7",
    "prob": 0.04,
    "lvl": -1,
    "conf": 0,
    "version": "spec=4 abi=1 hash=8f3a91",
    "eng": "2.2.0",
    "analyzed_at": "2026-05-14T18:22:01Z",
    "files": [
      { "id": 0, "type": "elf", "prob": 0.04, "lvl": -1, "conf": 0 }
    ]
  },
  "raw": { "...": "full cleave AnalysisReport" }
}

The hostile envelope above fired because prob (0.998) crossed the level-3 hostile cutoff. The az/native specialist route drove the decision; the general az route alone would have been suspicious.

Render saved reports locally

Experimental: the render command and its candidate JSON formats may change.

atomscan render report.json --format interpret reads a saved cleave report (or a scan envelope's raw report) and uses the live primary-context budget and annotation rewrite. It does not load models, analyze the original files, fetch references, or update rules. --format tiny and terminal render cleave's corresponding context views; they do not synthesize an ML verdict. Use - as the input path to read one JSON document from stdin.

With --llm (and the usual --llm-model/--llm-key/SCAN_LLM* settings), render sends the interpret render to the LLM with the live grading prompt and prints {"grade","model","reason"} instead: the grade alone, with no ML admission gate or blend, for checking that a saved report grades as its live scan did. It queues at background priority.

For a byte-for-byte comparison, use:

atomscan render before.json --format interpret > before.txt
atomscan render after.json --format interpret > after.txt
diff -u before.txt after.txt

Rendered output goes to stdout, and warnings go to stderr. Run both inputs with the same renderer build and SCAN_INTERPRET_BUDGET_BYTES setting.

The reader accepts compact v9 or v8 reports and cleave's raw v3 snapshots (cleave --format snapshot SAMPLE -o before.json); unknown schemas fail. Reports store no context annotations: the renderer derives them from each file's findings exactly as a live analysis does, so a saved report renders as its analysis did.

The initial interpretation replay supports artifacts and their archive members. Fetched subjects and registry sidecars additionally require saved dependency verdicts and registry provenance, which the compact report does not fully carry; those inputs are rejected rather than rendered with missing subjects. Existing scan JSON raw remains the compact storage representation.