CLI overview
SBOMFlow is one coherent sbomflow command tree. Everything runs offline by default. This page groups the commands by the job you are doing; run sbomflow help <command> or sbomflow <command> --help for the authoritative, generated help for any command.
Note
Help text and options come from the CLI itself (a single source of truth), so
this overview never drifts from real behavior. If a command or option is not
shown by --help, it is not available.
analyzeopt-in networkannotateapproveauditopt-in networkauditor-packbackfillbatch-runopt-in networkbundlecaseaccept-riskassigncommunicatecorrelatediscloseexport-advisoryintakeopenpropose-propagationremediatereopenshowtransitiontriagecmake-evidencecompare-releasesconfigshowcosign-verifydashboarddecisionsrecovertransactionsverify-transactionsdiffoscope-comparedoctorelf-evidenceemit-user-infoesp-idf-evidenceexamplesexplainfirmware-extractgatesimulatehelpindexqueryrebuildvalidateindex-releasesingest-codex-securityingest-grypeingest-sarifingest-trivyinitlicenseinspectinstallstatusverifylinker-map-evidencemcuboot-evidencemcuboot-verifymender-evidencemigrate-innext-actionsnotifyopt-in networkoci-evidencepassportissueverifypolicydiffshowportfolioprov-mapprovenance-checkquickstartrauc-evidencerecheckregime-profileregime-viewrelease-chainverifyreportfreshnessreproducible-comparereviewreview-mergereview-viewschemalistshowsetupsign-bundlestandards-viewstoreaddempty-trashexportlistqueryregister-renamereindexreissuereissue-planretainshowtimelineverifyverify-gatessupport-bundleswupdate-evidencesyft-scansync-dependencytrackopt-in networksync-issuesopt-in networksync-jiraopt-in networksync-servicenowopt-in networktwister-evidenceupdate-manifest-evidenceupgradeinspectmigrate-copyplanvalidateuptane-evidencevalidatevalidate-annexii-modelvalidate-article14-modelvalidate-configvalidate-graphvalidate-ssvc-modelvalidate-techdoc-modelverify-bundleverify-package-artifactsvulndbopt-in networkinspectpinreindexretentionsourcesupdateverifyzephyr-evidence
Get started#
sbomflow setup— guided interactive onboarding (a real terminal only).sbomflow quickstart <dir>— auto-detect, scaffold config, run an offline audit.sbomflow doctor <dir>— report your environment and config facts.sbomflow init <dir>— scaffold a placeholdersbomflow.yaml.sbomflow validate-config <file>— check config syntax and keys.sbomflow examples [command]— worked examples that are guaranteed to run.
Worked examples#
Help epilogs carry a couple of examples each. For more — "show me waiver usage", "how do I diff two releases?" — ask the CLI:
sbomflow examples # every worked example, grouped by command
sbomflow examples store # just the ones for `store`
sbomflow examples audit --json # machine-readableThese are not prose. Every example is stored as data and executed against the bundled demo fixture by the test suite, so a renamed flag or a newly required argument fails the build rather than failing you at the terminal. None of them touch the network, which is what makes running them in tests possible.
Core evidence loop#
sbomflow audit <dir> --output <dir>— full offline evidence pack for a release.sbomflow analyze <dir> --output <dir>— the analysis stage.sbomflow explain <output> --gap <id>— explain an evidence gap.sbomflow explain <output> --finding <id>— explain an advisory finding.sbomflow explain <output> --gate— explain the release-gate decision.sbomflow explain <output> --warning <code>— explain a scan-warning code and the exact inputs that triggered it in this run (omit the code to list the codes seen). See the Warning codes reference.sbomflow review <output>— record human review decisions.sbomflow approve/sbomflow store— approvals and the local evidence store.
--as-of (accepted by analyze, audit, and the decision commands) pins the evidence instant the run measures every date against, which is what makes a re-run byte-reproducible. It pins the clock, not the advisory data — the contract, and what follows from it, is stated once in What --as-of pins.
Compare and track releases#
sbomflow compare-releases·sbomflow index-releases·sbomflow dashboard·sbomflow portfolio.
With a family.json beside the portfolio config, portfolio also reports the latest release per channel|variant across a declared family's member products — the question a multi-variant manufacturer asks. Two members can legitimately claim the same channel|variant: the product name is part of a product's identity, so two products differing only by name are distinct products that share one key. When that happens no winner is picked, because releases of different products are not comparable. The key is left out of the merged map, every contender is listed under latest_by_channel_variant_collisions, and a warning names them.
Validate and share#
sbomflow validate— validate a generated output directory.sbomflow bundle/sbomflow sign-bundle/sbomflow verify-bundle/sbomflow auditor-pack— portable reviewer/auditor handoffs.
Integrations (dry-run by default)#
sbomflow sync-issues(GitHub),sync-jira,sync-servicenow,sync-dependencytrack,notify.
Network — opt-in
Sync/notify commands are dry-run by default. They only contact an external
system when you pass --apply, and credentials come from environment variables,
never from evidence artifacts.
Embedded, firmware & other inputs#
Specialist importers exist for CMake, Zephyr, ESP-IDF, containers/OCI, firmware extraction, update managers, SARIF, and more. Run sbomflow --help for the full list; use only the importers your build actually produces.
Discoverability#
sbomflow --help— grouped top-level help.sbomflow help [command|topic]— the same help, addressable by name.sbomflow help <command>— for the option-heavy commands (analyze,audit,review,approve) this opens a short overview instead of the whole option list: what the command reads and writes, whether it touches the network, its exit codes, its required inputs, the five-to-seven most common options, and a directory of the remaining options grouped by job.sbomflow help <command> <group>— one option group, e.g.sbomflow help audit release-gate-policy. A mistyped group name suggests the closest matches, lists the groups, and runs nothing.sbomflow <command> --help(orsbomflow help <command> all) — every option, unchanged. The overview hides nothing: it is a shorter route to the same listing, and every option belongs to exactly one addressable group.sbomflow store export --output <dir>— copy the whole estate out offline, plus acompleteness-manifest.jsonthat reconciles the export against the store's own index: every indexed release appears, exported or not, with a reason when not, alongside the whole-store seal and an explicit list of what the export does not cover. The copy mirrors the store layout, so it re-opens with the ordinarystorecommands — no import step, no SBOMFlow-only container. Exits non-zero if the estate is not fully accounted for.--dry-runreconciles and reports without writing copies, and reaches the same completeness verdict and exit code as a real export — so you can check whether an estate would travel intact before committing the disk.
A store this run could not enumerate is refused before anything is written. An export replaces whatever is at its destination, and it is assembled from the store index alone — release directories on disk are never consulted. So an index that is absent, will not parse, or records zero releases stops the export rather than producing an empty copy that declares itself complete: the export you already had is worth more than a fresh one that vouches for nothing. The manifest records store_index_state so a receiver can check that precondition rather than infer it, and complete can only be true when that state is read. Repair the index with sbomflow store reindex (see PB-12), confirm with sbomflow store verify, then export again.
An export is a share boundary, so it runs a preflight first and REFUSES rather than publishing something sensitive. The preflight reads the store's own members before anything is written, so a refusal exports nothing at all and any previous export at that destination is left exactly as it was. It reports the member path and the violation class — never the value. Inspected classes: secret-shaped filenames, private-key material, credential/token material, absolute machine paths, and the same sharing contract the reviewer bundle uses, applied to JSON and JSONL members.
"Absolute machine path" means exactly that. A repository-relative path that merely passes through a directory called home/ or Users/ — web/home/page.tsx — is ordinary evidence and does not refuse the export. Likewise the sharing contract's hostname class is a machine name, not any word that resembles one: a component called node-red or a file called host_config.json travels. See the redaction contract for the exact shapes.
SBOMFlow will not filter or redact the offending member for you: dropping a record would break the completeness guarantee the export exists to provide, and rewriting stored bytes would invalidate their recorded hashes. Resolve it at the source (remove the file from the release output and re-run store add), then export again.
What a clean preflight does and does not mean. It means those classes were not observed in the members that could be inspected. Members that could not be inspected — binary or non-UTF-8 content, or a structured member that did not parse — are listed and counted separately in the manifest, because "not inspected" is not "clean". It is not proof that an export is free of every possible secret, and it is never a statement about legal safety.
Note that analyzing with an absolute path records that path in the pack as provenance, and the preflight refuses to publish it at the share boundary. Run analyze from inside your workspace with a relative target if the estate is intended to travel.
sbomflow store reissue-plan— whenstore exportrefuses, diagnose why and plan a safe reissue. Read-only: it writesportable-reissue-plan.jsonand a rendering, never an export, and never modifies the store. It reads the export's OWN preflight result rather than a second detector, so a plan and an export cannot disagree. Each release is reportedportable,requires_reanalysis_inputorblocked; each finding names the member relative to its release and the violation class — never the matched value, path, credential or key material, because a diagnosis must not republish what the refusal exists to contain. Next actions are emitted as copy-pasteable commands only where every value is known and safe to print; otherwise they carry<PLACEHOLDERS>plus the exact inputs you must supply, because SBOMFlow does not retain your source checkout and will not invent one. Exits 0 even when work is required — a diagnosis is not a failure.
If the original source or config is gone, a release whose generated evidence carries machine identity cannot be made shareable by rewriting it: the audit log is hash-chained and every stored digest is asserted by the completeness manifest. That evidence stays fully usable locally.
A store this run could not enumerate is refused, not diagnosed. This is the command you reach for when store export already refused, so it runs precisely when the store is in trouble — including when the index is the thing that is missing. Every count and verdict in the plan is folded over the index entries, so an index nobody could read produced zero releases inspected and reissue_needed: false — "all of them can travel, nothing to do", for a store it never opened. It exits non-zero and names the state instead. See PB-12.
store verify,store retain,store empty-trashandstore reissue-planrefuse an index they could not enumerate. Each folds its verdict over the store index, and an empty fold reads exactly like a healthy one — so a store that had lost itsindex.jsonverified clean, retained nothing, reported no release on hold, and needed no reissue. The read is a precondition now, and the refusal names which state it found:absent,unreadable(bytes denied or an I/O error) ormalformed(bytes read, not a usable index). An index that was read and honestly records zero releases is the fourth state,empty, and every one of these commands behaves over it exactly as it always did. Repair withsbomflow store reindex— see PB-12.
store empty-trash --apply is the only path in the store that permanently deletes, and whether a staged release is under a preservation hold is recorded only in the index. It therefore cannot tell "no release is held" from "no hold could be read", and treats the second as a refusal: an unread input never widens what an irreversible step may delete. store retain --apply likewise writes nothing at all when the index it would rewrite could not be read — it used to create one asserting the store held zero releases.
store queryandstore timelinerefuse the same three states. They only report, so nothing is deleted or overwritten — but "no release matches your filters", "no stored release contains a matching finding" and "not observed in any stored release" are exactly what an operator sees when the index is gone and every pack is still on disk. That last one is decision memory answering the audit-day question, so the reassuring answer is the costly one. They exit non-zero and name the state instead. An index that was read and honestly records zero releases still answers normally.
store list is the exception, and only for absent. It shares its reader with the cross-release context analyze builds (aging, regression, carry-forward), which runs against a store directory you may have created and never added to — a CI job that pre-creates its evidence store has exactly that shape on its first run, and refusing there would fail an analysis with nothing wrong with it. So store list still prints an empty list for a store whose index.json is missing. Use store verify, which refuses, to tell an empty store from a store that lost its index. An index that is present and unusable (unreadable or malformed) is refused by store list.
sbomflow migrate-in <dir>— import another tool's estate (Trivy / Grype / SARIF exports) as origin-labelled claims, composing the importers that already ship. Every row carriesclaim_origin; nothing becomes a reviewer decision, VEX status, approval or gate input, because an incumbent tool's status is a claim about what that tool said, not authority over your product. Files it cannot read or recognise are listed with the reason rather than skipped, so a migration never looks more complete than it was. That holds at row level too: a file can be imported and incomplete, so rows an importer could not read are counted per file, totalled asunreadable_rows_total, and the summary names those files under Imported INCOMPLETELY. A row carrying no vulnerability id, package name or purl is reported as unreadable rather than imported as a finding that names nothing — most often that means another tool's export was detected as a format it is not.sbomflow batch-run <manifest>— runanalyzefor every product declared in one batch manifest, sequentially. Each job gets its own output directory (enforced before anything runs), every job runs even when an earlier one fails, and the exit code is the worst job's — so a batch never looks healthier than its unhealthiest product. A shell loop does the same thing; this adds per-product isolation and partial-failure aggregation.sbomflow next-actions <output-dir>— the release-scoped ship-readiness line (exit N BECAUSE policy X in mode Y) plus the top few outstanding items, with--jsonfor machine use and--limitto widen the list. Aggregation only: it restates the gate's verdict rather than making one, and always exits 0 — the release gate remains the only machine verdict.
Every action names the pack you actually passed and the real decision key, so the printed command is one you can run rather than a template to fill in. When an input genuinely is not knowable from an output directory, the action says so and names where to get it instead of printing a command that would fail. The JSON carries argv as an array alongside the display string, so a caller never has to re-parse a shell line to run one.
The list also declares what it rests on, in a freshness block:
- live_decisions — the reviewer decision file beside the pack was re-read for this run, so a decision recorded since the analysis run is already reflected, and findings that were ruled on appear under already decided rather than in the worklist; - snapshot_only — no decision file sits beside the pack, so the list rests on the frozen analysis snapshot and names that fact; - snapshot_unreadable_overlay — a decision file is there but could not be read. The list falls back to the snapshot; treat items as possibly stale. This is deliberately not the same state as an absent file.
Each action carries its own freshness_basis too, so a category with no live overlay is labelled rather than borrowing the freshness of the ones that have one.
sbomflow help error [code]— offline explanation of an error code (unknown codes suggest the closest matches); with no code it lists the whole error catalog with each code's exit status.sbomflow help warning [code]— offline explanation of a scan-warning code (unknown codes suggest the closest matches); with no code it lists the whole warning catalog grouped by category. Run-independent, unlikesbomflow explain --warning <code>, which reads a specific output directory — so a reviewer holding only a colleague'sscan-warnings.jsoncan still look a code up.sbomflow help exit [code]— offline explanation of a process exit code, including whether re-running is safe. Mirrors the published exit-code reference and is held in agreement by a drift test.sbomflow schema list— list SBOMFlow's own output-artifact families.sbomflow schema show <family>— show one artifact's fields, offline (add--jsonfor a machine-readable description).sbomflow config show <file>— print the merged effective config with each key markedconfigordefault, offline (add--json).
Long --help pages are sent to a pager only on an interactive terminal; when output is piped or redirected the bytes are unchanged (safe for scripts and CI). Control it with SBOMFLOW_PAGER or PAGER (default less -FRX), or disable paging with NO_PAGER (or SBOMFLOW_NO_PAGER). Errors are never paged.
Output streams#
SBOMFlow keeps a strict stream contract so its output is pipeline-safe: stdout carries the primary/machine output, stderr carries diagnostics. With --json, stdout is pure, parseable JSON and nothing else — human notes, progress, and warnings go to stderr — so sbomflow … --json | jq never breaks on a stray line. Error text always goes to stderr.
Output is plain text by default: redirected or piped output never carries ANSI escape sequences, meaning is never carried by colour alone, and the one interactive surface that uses colour (sbomflow setup) honours both the NO_COLOR convention and its own --no-color flag. Screen readers, narrow terminals, and log processors see the same stable, deterministically ordered text as everyone else.
Embedding & scripting: the supported integration surface#
If you are integrating SBOMFlow into a platform, a pipeline, or another tool, bind to the artifact contract, not to SBOMFlow's Python internals:
- The output files are the API. Every artifact family is versioned and self-describing —
sbomflow schema listnames them,sbomflow schema show <family>documents the fields offline, and stable families evolve additively. - Operator commands speak
--json. Read/status commands emit one pure, versioned JSON document on stdout (schema_versionon every payload), with the same exit codes as the human output and machine-readable errors carrying stable[E0xx]codes. Which commands take the flag — and how every other command is machine-readable instead — is audited per command in Machine output coverage, drift-checked against the real parser. - The Python modules are internal.
import sbomflowis not a supported integration surface: module layout and function signatures may change between releases without notice. A stable in-process embedding API is a recorded decision that is currently deferred — if artifacts +--jsoncannot serve your integration (for example, spawning a subprocess is prohibited in your environment), tell us in writing; two such requests re-open the decision.
This split is deliberate: one language-neutral contract that every consumer shares is easier to keep stable — and to test — than a parallel Python API.
Progress#
A long scan should not look hung, so analyze (and audit, which runs it) reports each phase on stderr as the phase completes:
sbomflow: scanning project — 22 components
sbomflow: matching vulnerabilities — 2 findings
sbomflow: assessing evidence — 7 evidence gapsOn an interactive terminal the same text is drawn on a single line that is rewritten in place; when stderr is redirected to a pipe, a file, or a CI log you get the plain lines above. Redirected progress carries no ANSI escape sequences and no carriage returns, and it is deterministic: no timestamps, no durations, no ETA, so two runs over the same inputs print the same bytes. Progress never writes to stdout and never changes an artifact.
Silence it with SBOMFLOW_NO_PROGRESS=1, or set TERM=dumb to force the plain rendering on a terminal.