Lesson 10: Ingest and the unified flow model¶
So far every command has worked on atom's own slices. This lesson widens the aperture to the whole ecosystem: dosai, golem, rusi and kosi reports, plus atom, all normalised into one document that every report-taking command accepts.
The problem ingest solves¶
Each engine has its own vocabulary. dosai says SinkCategory and
ReachableFromEntryPoint, golem says endpoints and callGraph, atom says
reachables and tags them sql, ssrf, framework-input. If you run different
engines across a polyglot estate, comparing their findings means writing a
translator per engine, per schema version. ingest is that translator, written
once and tested against committed fixtures from every engine.
The unified flow model is one JSON document with a flows array. Every flow,
wherever it came from, carries the same fields: source_category,
sink_category, severity, severity_source, taint_kinds, sanitized,
path_truncated, purls, nodes with role markers, and the engine's own
record verbatim under raw as an escape hatch.
dosai dataflows ──┐
golem analyze ───┤
rusi analyze ───┼──> atom-tools ingest ──> unified-flows.json
kosi analyze ───┤ (engine detected │
atom reachables ──┘ from the envelope) ├──> stats
├──> explain
├──> attack-surface
├──> drift
└──> crypto-reach (out of scope, says so)
Ingesting one report¶
Start with the dosai report that sits in the cdxgen checkout (a real artefact of a real run):
$ atom-tools ingest -i ~/work/cdxgen/cdxgen/data-flow.slices.json -o unified-flows.json
Unified report with 74 flow(s) from 1 file(s) written to unified-flows.json.
One line of output, but the document is worth reading. The top level records where everything came from:
{
"schema_version": "...",
"engine": "mixed",
"analysis_mode": "dataflows",
"provenance": { "sources": [ { "engine": "dosai", "engine_version": "5.0.0.0", "file": ".../data-flow.slices.json", "generated_at": "2026-09-19T13:10:54.398773+00:00", "severity": "engine" } ] },
"diagnostics": [ "Resolved 22 package purl(s) from paket.lock paket.lock.", "PURL version ambiguity for bootstrap: packages.config says 3.3.7; keeping 3.4.1.", "..." ],
"endpoints": [],
"packages": [],
"flows": [ ... ]
}
And here is the first flow, trimmed to the load-bearing fields:
{
"id": "dfs1",
"engine": "dosai",
"severity": "error",
"severity_source": "engine",
"source_category": "message",
"sink_category": "file",
"taint_kinds": ["message"],
"path_truncated": false,
"sanitized": false,
"purls": ["pkg:nuget/Microsoft.NETCore.App@2.1.0"],
"nodes": [
{ "role": "source", "code": "request", "file": "nuget.exe", "line": 1, "function": ".ctor", "tags": ["message"] },
{ "role": "sink", "code": "System.IO.Path.Combine(string,string):string", "file": "nuget.exe", "line": 73, "function": "GetAssetsFilePath", "purl": "pkg:nuget/Microsoft.NETCore.App@2.1.0", "tags": ["file"] }
],
"raw": { "Id": "dfs1", "Severity": "high", "SinkCategory": "file", "Summary": "Assembly IL data flows from dfn1556 to Combine argument 0.", "...": "..." }
}
Three things to notice. severity_source says "engine", meaning dosai itself
emitted the severity and ingest passed it through; when an engine emits none, the
taxonomy derives one and the field says so. path_truncated is false here; when
an engine truncated a witness path, the flag is true, and downstream commands
render such flows differently rather than presenting them as complete. And raw
keeps the engine's original record untouched, so anything the model dropped is
still reachable.
Mixing engines in one invocation¶
The interesting invocation takes several reports at once, engines mixed freely. Feed it the cdxgen dosai report plus the committed golem (Go, ipsw) and rusi (Rust, kafka microservices) fixtures:
$ atom-tools ingest \
-i "~/work/cdxgen/cdxgen/data-flow.slices.json,\
test/data/ecosystem/go-ipsw-golem.json,\
test/data/ecosystem/rust-microservices-kafka-rusi.json" \
-o unified-mixed.json
Unified report with 476 flow(s) from 3 file(s) written to unified-mixed.json.
The provenance section now has three sources, and the flows are tagged by engine:
{
"provenance": {
"sources": [
{ "engine": "dosai", "engine_version": "5.0.0.0", "analysis_mode": "dataflows", "...": "..." },
{ "engine": "golem", "engine_version": "3.2.0", "analysis_mode": "all", "callgraph_mode": "static", "dataflow_truncated": true, "...": "..." },
{ "engine": "rusi", "engine_version": "3.2.0", "analysis_mode": "security", "...": "..." }
]
}
}
That dataflow_truncated: true on the golem source is the honesty habit again:
golem's dataflow witness is a capped sample, so every flow it contributed is
marked truncated, and any command rendering those flows will say so.
476 flows, three engines, one document. Every report-taking command now works on the mixture exactly as it worked on a single-engine report.
Emitting reachables instead¶
The unified model is atom-tools' own schema. Some tooling in the wider ecosystem
expects atom's reachables shape (the {"reachables": [...]} document), so ingest
can emit that instead:
$ atom-tools ingest -i test/data/ecosystem/go-ipsw-golem.json --emit reachables -o golem-as-reachables.json
Reachables document with 397 flow group(s) written to golem-as-reachables.json.
The point of this form is that the atom-native commands, the ones you learned in
lessons 4 to 9, work on any engine's report through it. stats on the golem
emission, trimmed:
$ atom-tools stats -i golem-as-reachables.json
Slice type: reachables
flow_groups: 397
flow_nodes: 2171
sources: 393
sinks: 160
unique_files: 66
top_files:
/Users/prabhu/sandbox/ipsw/cmd/ipsw/cmd/diff.go: 585
/Users/prabhu/sandbox/ipsw/api/server/routes/idev/idev.go: 376
...
purls:
count: 21
list: ['pkg:golang/cloud.google.com/go/auth', 'pkg:golang/crypto/x509', 'pkg:golang/github.com/blacktop/ipsw', ..., 'pkg:golang/net/http']
tags:
framework-input: 393
panic: 143
http-input: 141
configuration: 131
A Go report rendered by a command that was written for atom slices, with no
translation work on your side. The same trick works with visualize,
convert -f sarif, check-reachable and filter.
Engine detection and the escape hatch¶
The engine is detected from the report envelope: dosai reports carry a Tool and
PascalCase tables, golem and rusi carry their schema URLs, atom slices have their
own shape. When a report is ambiguous or a new engine version changes its
envelope, --engine forces the interpretation, for example
--engine rusi. Use it as a precision tool, not a habit: forcing the wrong
engine on a report produces a confidently wrong document.
What can go wrong¶
Ingest never invents severity, reach or sanitiser facts, but it does resolve
package purls from whatever the report carries, and where the report is
internally inconsistent it says so in diagnostics and picks a value. The dosai
report above produced diagnostics like "PURL version ambiguity for bootstrap:
packages.config says 3.3.7; keeping 3.4.1." Those strings are part of the
document; when a downstream number surprises you, read them first.
Exercises¶
Run the kosi fixture through both emissions and compare what survives:
$ atom-tools ingest -i test/data/ecosystem/kotlin-command-exec-kosi.json -o kosi-unified.json
$ atom-tools ingest -i test/data/ecosystem/kotlin-command-exec-kosi.json --emit reachables -o kosi-reachables.json
$ atom-tools stats -i kosi-reachables.json
Then feed the unified document of the three-engine mixture to explain and read
one flow from each engine. Lesson 14 does this in anger.
Where the fixtures come from
Every test/data/ecosystem/ fixture is a real engine run against a real
repository, recorded in test/data/ecosystem/PROVENANCE.md. The summary is on
the fixtures reference page.