Troubleshooting
Every SBOMFlow error carries a stable code, a cause, a concrete fix, and a docs link back here. Nothing fails silently: unexpected or ambiguous inputs surface a warning rather than being dropped.
Get help three ways:
sbomflow help error E001 # read any error code offline (air-gapped friendly)
sbomflow help exit 2 # what a process exit code means + whether rerun is safe
sbomflow help <command> # full help for a command, offline
sbomflow doctor . # report your environment + config factsOnline: the error reference, exit codes, and warning catalog mirror the offline catalogs. The docs base URL is overridable with the SBOMFLOW_DOCS_URL environment variable for an internal mirror — the codes and anchors stay identical.
Start here — which section do I need?#
| What happened | Go to |
|---|---|
The command exited non-zero with a [Ennn] code | Error codes → then the full error reference |
| The command exited non-zero without a code | Exit codes — match the number |
| It succeeded but the results look wrong | Results look wrong |
| A release gate blocked the run (exit 1) | Release gate blocked |
sbomflow validate failed (exit 4) | Integrity: tamper & validation |
| A command is taking minutes with no output — is it hung? | validate is taking minutes |
| A reviewer decision did not take effect | Reviewer, VEX, waivers & approvals |
| An input, SBOM, or lockfile was skipped or rejected | Inputs & imports |
A sbomflow license question | Licensing & entitlements |
| An install / environment / filesystem problem | Environment & installation |
| A network / proxy / sync problem | Network (opt-in only) |
| A crash / integrity / staleness state to recover from | Recovery playbooks — tested diagnose → recover paths |
| The store lives on a network share, cloud-synced folder, or removable drive | Filesystem & concurrency support — the tested support contract, and what is explicitly out of it |
If your exact symptom is not here, check the run's own diagnostics first: scan-warnings.json (every warning raised), vuln-source-health.json (what vulnerability sources ran), and release-gate.json (exactly what a gate decided). Every artifact is always written, even on a blocked gate or a failure that still produced partial evidence.
Error codes at a glance#
Each code is stable forever and maps to a section in the error reference (anchor #e001 … #e012) and to sbomflow help error <code>.
| Code | Exit | Symptom | Fix |
|---|---|---|---|
E001 | 2 | Target directory not found | pass an existing product directory; try sbomflow quickstart . |
E002 | 2 | Target path is not a directory | point at a directory; import a single SBOM with --import-sbom |
E003 | 2 | Output path is not a directory | choose a directory for --output (created if missing) |
E004 | 2 | Filesystem error reading inputs or writing outputs | free disk space; make --output writable; not a read-only mount — see filesystem errors |
E010 | 2 | Malformed or unknown-key configuration | sbomflow validate-config <file> then sbomflow doctor . |
E011 | 2 | Reviewer decision file changed since it was read | a concurrent reviewer wrote first; nothing was overwritten — re-run to pick up their decision and retry |
E012 | 2 | Unsafe network endpoint URL | use an https:// URL with no embedded credentials (plain http:// only for loopback in testing) |
See the full error reference for the cause + copy-paste fix of each.
Results look wrong#
Evidence I configured was not used#
If a run reports gaps for evidence you know you supplied, check scan-warnings.json for unknown_config_key. A key in sbomflow.yaml that SBOMFlow does not recognise is ignored, so a misspelled input key — say annex_i_evidenc instead of annex_i_evidence — means the file it names is never read, and the run reports the resulting gaps exactly as if nothing had been supplied. The warning names the offending key, and sbomflow validate-config lists every one.
It stays a warning, not an error: a config written for a newer SBOMFlow must still run on an older one. The policy: section is stricter on purpose — an unrecognised key there fails the run (exit 2) rather than warning, because a release gate that silently does not apply is worse than a noisy one.
No vulnerabilities found (no CVEs)#
Zero findings usually means a source question, not a clean bill of health:
- Check which sources ran.
vuln-source-health.jsonrecords every vulnerability source used, its freshness, and whether any produced no usable intelligence. - The default is offline. Without flags, SBOMFlow matches against the bundled, clearly-labelled non-real sample feed (
CVE-SAMPLE-*). For real advisories, supply a local snapshot (--nvd-file,--kev-file,--epss-file, or avulndbsnapshot) or explicitly opt into the network with--use-osv/--use-nvd. - Check component identity. Advisories match on ecosystems and purls; a component inventoried as
generic(e.g. from a firmware manifest with no ecosystem context) may not match registry advisories. - Stale snapshot? A
stale_vulnerability_datawarning means your local snapshot is older than the configured window — refresh the snapshot file. - Source contributing nothing? A requested source that added nothing to a run that did have findings raises
vulnerability_source_zero_yield— the snapshot/file is empty, all-withdrawn, or for the wrong ecosystem.
Zero findings with healthy sources is a legitimate result; SBOMFlow never invents findings and does not claim to find every vulnerability.
A finding has no NVD CVSS score#
This is normally not a problem, and the run summary's nvd row says which of these it is:
not requested— nothing asked NVD anything. SBOMFlow is offline by default, so on a run without--use-nvdor--nvd-filethe absence means never asked for — not looked up and not found. This is the commonest cause.- requested, and the finding still has no score — NVD does not analyse every CVE it holds, so this is a normal reading of that source rather than a defect in it or a gap in your run. The scanner note reports how many findings matched an NVD record and, separately, how many of those carry a CVSS base score: matching a record is not being scored by one.
could not be read— the snapshot did not open or the API call failed. This one is worth acting on: the absence is unexplained, so the run knows nothing about what NVD holds. The error is invuln-source-health.json, and thenvdrow says so rather than reporting the ordinary case — a failed read and a clean read are never the same row.
In none of these cases is the missing score evidence about severity. An absent NVD score is never a low severity, a zero, or an all-clear — supply the primary advisory feed's own rating, or a human review, for that.
CVE-SAMPLE-* findings#
That is the labelled non-real offline sample feed — the default so a first run produces something to look at. Enable a real source (e.g. --use-osv, or a snapshot file) for real advisories. This is by design and never presented as real intelligence.
Recovering after a crash (stale lock or incomplete output)#
If a run is interrupted (crash, Ctrl-C, killed process) it can leave three recoverable traces, and sbomflow doctor <dir> reports each — with the exact fix — under a recovery line (also in doctor --json as a recovery block, whose stale_lock, incomplete_output and missing_completion_token keys are null when there is nothing to report):
- Stale run lock — a leftover
.sbomflow.lockfrom a run that died mid-write. A live run's lock is respected; a provably-stale one (its process is gone, or it is past the staleness horizon) is flagged. Fix: re-run the same command with--break-stale-lock(it reports the stale holder, then removes the lock), or delete the.sbomflow.lockfile yourself once you are sure no run is active. - Incomplete output — a directory with only some of the required artifacts, i.e. an interrupted run. Writes are atomic, so a partial directory is safe to overwrite: re-run
sbomflow analyze(orsbomflow audit) to regenerate a complete evidence pack. - Missing run seal — every required artifact is present,
run-completion.jsonis not, and the pack records a producer (tool_version0.5.0or later) that always writes it: the run stopped after its required artifacts, or the seal was removed.validateexits4over the same directory. Fix: re-runsbomflow analyze(orsbomflow audit); do not recreate the file by hand. A pack from an older producer is read as legacy instead, andvalidatesays so in a note.
doctor is read-only — it never removes a lock or touches a partial output; it only tells you what it found and the command to run.
What doctor knows about your machine#
sbomflow doctor <dir> reports the local facts behind most setup failures, so a bug report can be triaged without a round-trip. Alongside the Python, optional extra, adapter and config lines it prints (and returns under environment in doctor --json, except where a bullet names its own key):
- platform — operating system, release, CPU architecture, interpreter implementation and the exact
pythonexecutable running SBOMFlow. This is what distinguishes "unsupported interpreter" (E005) from "wrong virtualenv". - target — whether the path you passed exists, and whether it is readable and writable. An unreadable target is the direct cause of
E003. A path that exists but is a file saysnot a directoryrather than reporting its permissions:analyze/auditwalk a directory tree, so the permissions of a single file answer a question you did not ask. - disk — free and total bytes on the filesystem holding the target (nearest existing parent if the target does not exist yet).
- umask — the process umask, restored immediately after it is read.
- path length — how long the absolute target path is (characters and bytes — POSIX limits are byte limits) next to the filesystem's own reported
PATH_MAX/NAME_MAXlimits (path_lengthindoctor --json). Where the platform reports no limit, doctor says "not reported" — it never guesses a constant, and it creates nothing to measure: the probe is a read-only OS query. - offline docs — whether this install's built-in offline references render:
sbomflow help error(the error catalog) andsbomflow help exit(the exit-code contract), with their code counts (docs_availabilityindoctor --json). The web docs URLs error messages will print are recorded — including aSBOMFLOW_DOCS_URLmirror override, useful on air-gapped sites — but never fetched, so this is a statement about the install, not about any website's reachability.
Two of these are context rather than verdicts. analyze reads the target and writes to --output, so a read-only target is often entirely normal — free space and permissions on the output directory are what cause E004. And artifacts are written with an explicit 0o644 mode regardless of umask, so the umask is reported to explain surprising file modes elsewhere, never as a cause.
The filesystem row is labelled [system volume] when the mount it found is the operating system's own root — sealed and read-only on macOS, where the writable data volume is firmlinked beneath it. Those flags describe that volume, not the directory this run writes to; the target row above is the one that answers that.
doctor gathers these facts and stops there: it installs nothing, changes nothing about your environment (not even the umask it reads), and makes no network request.
Paths in doctor output are redacted; doctor --json is not#
The text report is what this page tells you to paste into a bug report, so every path in it renders your home directory as ~ — the account name does not travel with the report. A path outside your home stays verbatim on purpose: an unexpected location is exactly the thing you need to see.
doctor --json is a machine contract and its path fields are unchanged. If you are pasting the JSON somewhere public, redact it yourself.
Reviewer, VEX, waivers & approvals#
Machine observations never change a decision by themselves; only reviewer files do. See Observed vs reviewed.
A finding I marked not_affected still shows or still blocks#
A not_affected VEX statement is honoured only with a valid CISA justification. Without one it is downgraded (kept as context, not treated as suppressing) and you will see vex_not_affected_no_explanation or, for supplier VEX, supplier_vex_invalid_justification. A vex_justification_status_mismatch warning means the justification does not match the stated status. Fix: use one of the recognised CISA justification categories (see sbomflow help review) and a short explanation. A fixed statement without evidence raises vex_fixed_no_evidence — record the fixing version/reference.
The gate still blocks despite a waiver#
A waiver suppresses a gate item only when it is unexpired and its finding-key/scope matches exactly. Check release-gate.json — it lists the responsible finding/gap IDs; the waiver's key must match one of them. An expired waiver does not suppress; renew it deliberately (a renewal requires a fresh reason and a later expiry — a stale re-waiver is rejected). A missing_waivers_file warning means the waivers file the run expected was not found.
Start with the waivers line in the run summary. When a waiver you recorded did not take effect, the analyze summary and sbomflow explain <output-dir> --gate both name it, so you do not have to infer it from the blocking IDs. The states are reported separately, because they need different repairs:
| What you see | What happened | Where to look |
|---|---|---|
no waivers line | Either no waivers were supplied, or every one was honoured and none is near expiry. | waiver_status.active |
N expiring within …d, M expired | The waivers were read and applied; some are near or past their bound. | waiver_status.expiring / .expired |
N waiver row(s) refused (…) | The row was read, but a field the gate must be able to read could not be — so it waives nothing and what it named is still blocking. The count names each recorded state (invalid_expiry, invalid_created_at, invalid). | waiver_status.invalid |
N waiver row(s) carry no readable identity (…) | The row names nothing this run could match, so what it was meant to waive is unknown. It is kept in the file exactly as written. | waiver_status.unreadable, plus the waiver_entry_unreadable warning |
the waivers file could not be read | No waiver record reached the gate. Which records the file held is unknown — not zero. | malformed_waivers_file / missing_waivers_file in scan-warnings.json |
None of these lines changes a gate decision, an exit code or an artifact. Refusing an exception nobody can validate was always the behaviour; the lines state that it happened instead of leaving you to deduce it.
An approval is not being accepted#
Multi-role sign-off enforces separation of duties and quorum: the same identity cannot fill two required roles, and the required number of distinct approvers must sign. Both separation-of-duties rules are refused when you record the approval — [E021], exit 2, nothing written — so the message names what to fix: have a different person approve, or --revoke the role the approver already holds. The role-coverage rule is judged against the roles you declared with --require-role (or a policy's required_approval_roles); declare none and no role is required, so nothing is refused and nothing is reported. missing_approvals_file means the approvals file was not found where expected. Use sbomflow review-view <output-dir> to see the current queue and what is still required.
[E011] — another reviewer wrote first#
Two reviewers wrote to the same decision file concurrently. Nothing was overwritten — SBOMFlow refuses the second write rather than lose the first. Re-run the command to read their decision, then re-apply yours.
Inputs & imports#
The directory holds an SBOM and the run still reports 0 components#
sbom_document_not_imported (info) means the walk found a CycloneDX or SPDX document, recognised it, read it for evidence — and did not inventory it. The document contributed no components, so the run's component count says nothing about what that document lists.
This is deliberate, not a malfunction. SBOMFlow never imports a document it merely walked past: a supplier document lying in the tree is a different claim from one you deliberately supplied, and auto-importing it would change your composition provenance with nobody asking. To inventory it, name it:
sbomflow audit ./my-product --import-sbom ./my-product/rootfs.cdx.json --output ./evidenceThe warning names the document and the exact flag, and it disappears once the document is imported. It changes no exit code and no gate.
A lockfile or manifest was skipped#
unsupported_lockfile means the file is recognised but not parsed for components yet — it is reported, never silently dropped. Supply an SBOM for that ecosystem (--import-sbom) or an equivalent supported lockfile. sbomflow doctor . lists what was and was not recognised.
A supplier SBOM or VEX was rejected#
malformed_supplier_vex / malformed_supplier_vex_statement mean the imported document could not be parsed as valid OpenVEX/CSAF. An imported SBOM's components are always labelled as a supplier claim, never shown as your own observed evidence — that trust boundary is deliberate. Validate the supplier file against its schema before re-importing.
An external SBOM that contributed nothing is reported in one of five distinct states, because they call for different fixes and only one of them means the file is not there:
| Code | What SBOMFlow observed |
|---|---|
missing_external_sbom | the path does not exist — proven, never inferred from a failed read |
unreadable_external_sbom | something is there and its bytes would not come back: denied permission, an unsearchable parent directory, a directory in the file's place. Whether it holds components is unknown; this is not a finding that it is absent |
empty_external_sbom | the file was opened, read to completion, and holds zero bytes — usually a truncated or failed export upstream |
malformed_external_sbom | the bytes came back and are not valid UTF-8 JSON |
unrecognized_external_sbom_format | the JSON parsed and is not a CycloneDX or SPDX document this build interprets (see also the per-format spec-version codes) |
A preserved artifact is refused as too large#
ingest-preserved-artifact exits 2 with E019 and says the artifact is larger than the limit (64 GiB), and nothing is recorded. In a run, preserved-artifacts.json shows the declared original or firmware image as unreadable with the reason artifact_oversize. The limit catches a wrong path, so first check the path names the capture and not a disk image or an archive that contains it. If it is the capture, split it into files under the limit and record each, or report the size so the limit can be reconsidered. No command references a file without hashing it.
External scanner reports (Trivy / Grype / SARIF)#
ingest-trivy / ingest-grype / ingest-sarif bring third-party findings in as sourced observations, correlated with native findings, never blindly merged. sarif_ingest_unsupported_version / sarif_ingest_malformed mean the report version is unsupported or the file is malformed — re-export from the tool in a supported format.
Licensing & entitlements#
sbomflow license inspect|verify|status is read-only and offline — it reports status and enforces nothing. An absent or expired entitlement blocks no command. sbomflow license install is the one licensing command that writes, and it writes exactly one file: your own per-user entitlement.
An entitlement is not the licence, and this section will not answer a question about your terms: for the licence notice's location in a source tree or an install, and how to request evaluation, pilot or design-partner terms, see Evaluation terms & access.
From delivery to first run#
- Download the entitlement file your issuer sent you, and check its checksum against the one they published alongside it. SBOMFlow never downloads it for you and never contacts an issuer.
- Verify it before installing anything:
sbomflow license verify ./entitlement.json --public-key issuer.pem. - Install it where SBOMFlow looks:
sbomflow license install ./entitlement.json --public-key issuer.pem(or pipe it:cat entitlement.json | sbomflow license install -). - Confirm with
sbomflow license status, which reads the same file every other command reads.
sbomflow doctor and the sbomflow quickstart pre-run disclosure report that same state, from the same reader, so the three surfaces cannot disagree about whether an entitlement is installed. Each reports a status, never a permission: none of them gates anything, and none prints your customer id, your entitlement id, the signature, or the path the file lives at — an entitlement is identified by a short ent: reference that reverses to nothing.
Neither doctor nor quickstart claims an answer it did not get. Where nothing supplied the state, the row reads not_established, which means this report did not look — never no entitlement is installed.
Your evidence does not change. An analyze run before an install and the same run after one produce byte-identical artifacts. That is a tested guarantee rather than a policy: the suite runs both and compares them artifact by artifact, along with the exit code and everything printed.
install verifies before it writes, so an unverified, revoked, or unreadable file can never displace the entitlement you already have. The file is written atomically and owner-readable only (0600), stored in a canonical JSON form whose signature still verifies, and no network is ever opened.
Where it is installed#
$SBOMFLOW_ENTITLEMENT if you set it, otherwise $XDG_CONFIG_HOME/sbomflow/entitlement.json, otherwise ~/.config/sbomflow/entitlement.json. Install and discovery resolve the same path, so an install that reports success is the file every command then reads. While SBOMFLOW_ENTITLEMENT is set, that path is the install target and the default per-user file is neither created nor modified.
What install reports, and when it asks#
outcome | Meaning | Exit |
|---|---|---|
installed | nothing was there; the verified entitlement is now in place | 0 |
unchanged | the same entitlement was already installed; nothing was written | 0 |
replaced | a different entitlement was superseded (clean renewal, or you instructed it) | 0 |
refused_source | the delivered file could not be read safely (absent, unreadable, over budget, not a JSON object, duplicate keys) | 2 |
refused_verification | schema, signature, trusted key, revocation, or declared scope failed the offline check | 2 |
refused_replacement | superseding what is installed needs --replace (or a yes at the prompt) | 2 |
refused_destination | the destination is not a regular file, or could not be written | 2 |
refused_concurrent_change | another process wrote the destination mid-install; re-run | 2 |
A clean renewal — same organisation and signing key, no scope dropped, issued no earlier and expiring no earlier — is accepted without asking. Everything else asks: a downgrade, a different organisation or key, a reduced scope, a candidate that is expired or not yet valid, and any current file that could not be read as an entitlement (empty, corrupt, unreadable). In a terminal you get a yes/no question; in a script, pass --replace. With neither, the install is refused and nothing is written — it never waits on input that will not come.
Every refusal leaves the previously installed entitlement exactly as it was, and leaves no temporary file behind.
What install never prints#
Your customer id, the signature, the entitlement id (which embeds the customer id), and your home directory path. --json returns the same non-secret result contract, so it is safe to paste into a support ticket. Quote entitlement_ref — a stable one-way fingerprint — when you need to identify a specific grant. sbomflow license inspect <file> still shows you the full fields locally.
Installing an entitlement changes no analysis behaviour: licensing is not enforced, every feature runs without one, and the evidence bytes of a licensed and an unlicensed run are identical. A verified signature proves issuance and integrity only — what an entitlement grants is a matter for the parties to it.
If something goes wrong#
untrusted_key— no trusted key matched. Pass the issuer's public key with--public-key; no issuer key is bundled yet, so this is expected today.unverifiable_no_extra— install the optional extra's package into the same environment withpython -m pip install cryptography(v0.5.5 is not on a package index, sopip install "sbomflow[sign]"would fail; from a source tree usepython -m pip install ".[sign]").installrefuses rather than filing a file it could not check.- Lost or corrupted install — re-run
sbomflow license installwith your delivered file;--replaceis needed because the current file cannot be read. Ask your issuer to re-send it if you no longer have it; SBOMFlow keeps no copy. - Renewal — just install the new file. A clean renewal needs no flags.
status | Meaning | What to do |
|---|---|---|
valid | genuine and in-window | nothing — exit 0 |
expiring_soon | valid, expires within 30 days | plan a renewal — exit 0, still fully functional |
grace | expired, within the 30-day grace window | renew soon — exit 0, still functional |
expired | past expiry + grace | renew — exit 1; past evidence is never touched |
not_yet_valid | not_before is in the future | check the entitlement's start date / your clock — exit 1 |
revoked | listed in a supplied revocation list | contact the issuer — exit 1 |
untrusted_key | no trusted public key matched | supply the correct --public-key — exit 2 |
invalid_signature | signature does not verify | the file was modified or is for a different key — exit 2 |
clock_suspicious | issued_at is in the future vs your clock | fix system time — exit 2 |
malformed | not a valid entitlement document | check the file — exit 2 |
as_of_invalid | the --as-of you supplied is not a readable ISO-8601 UTC timestamp | fix the pin — exit 2. Nothing is wrong with the entitlement; the verdict is refused rather than computed against a substituted date. Omitting --as-of (meaning "now") is always fine |
unverifiable_no_extra | the optional cryptography extra is not installed | python -m pip install cryptography into the same environment to verify signatures |
no_entitlement | no --file given and nothing installed at the per-user path | this is fine — SBOMFlow enforces nothing; exit 0 |
Exit-code contract: 0 = genuine & usable (or none installed), 1 = genuine but expired/revoked/not-yet-valid, 2 = malformed/untrusted/unverifiable.
Release gate blocked#
exit 1 means a policy you enforced matched. release-gate.json records exactly which --fail-on-* policy blocked and which finding/gap IDs are responsible; sbomflow explain <output-dir> --gate walks through the reason. Reviewed VEX (fixed / valid not_affected) and unexpired waivers suppress a block — machine observations alone never do. All artifacts are still written when a gate blocks, so you never lose the evidence.
Rehearse before enforcing. Add --gate-dry-run to analyze/audit to see exactly what would block: the gate is evaluated and reported in full, release-gate.json is marked dry_run: true (its exit_code still shows the would-be result), and the process always exits 0. Ideal for wiring a gate into CI before you let it fail the build.
Integrity: tamper & validation#
sbomflow validate fails (exit 4) when a file's hash no longer matches the manifest, when cross-artifact counts disagree, or when the append-only audit-log.jsonl hash chain is broken. That is the integrity model working: regenerate the output from source rather than editing artifacts in place, and treat an unexplained chain break as an incident, not a nuisance.
validate is taking minutes — is it hung?#
Almost certainly not. On a large device, sbomflow validate genuinely takes minutes, and nearly all of it is one step: checking spdx3-sbom.json against the official SPDX 3.0.1 schema. That step is only reached when the optional validate extra is installed and the run emitted an SPDX 3.0.1 SBOM (--emit-spdx3); without either, validate finishes in seconds and says so.
validate now tells you before it starts, and names every artifact as it reaches it. On a large output you will see, on stderr:
Validating artifacts in: ./out
... validating cyclonedx-sbom.json (cyclonedx-bom-1.6.schema.json)
... validating spdx3-sbom.json (spdx-3.0.1.schema.json)
... spdx3-sbom.json: schema-checking 18,712 graph nodes against
spdx-3.0.1.schema.json - expect roughly 4m40s on a laptop, longer on a
slower machine. This step prints nothing until it finishes.
... spdx3-sbom.json: schema check finished in 4m12s - no problems.The validating ... line is printed for every schema job, including the ones no cost estimate is attached to. A job that cannot be given a number is still a job you are waiting on, and those used to run in complete silence.
The cost scales with the number of @graph nodes, not the file size. Rough figures from one laptop — treat them as an order of magnitude, not a promise:
| components | @graph nodes | roughly |
|---|---|---|
| 681 | 1,557 | 20s |
| 4,943 | 10,668 | 2m10s |
| 6,971 | 15,045 | 3m10s |
| 8,662 | 18,712 | 4m00s |
If you want it to finish sooner:
- Drop
--emit-spdx3if you do not need the SPDX 3.0.1 projection.cyclonedx-sbom.jsonandspdx-sbom.jsonare validated either way and cost a fraction of the time.
That was not always true. The CycloneDX check used to be minutes of its own on a large SBOM, because the schema requires the components array to be unique and jsonschema decides that by comparing every component against every component before it. On a 9,025-component pack that step alone measured 71.7 s of a 73.3 s check. SBOMFlow now decides that uniqueness by hashing instead — the same verdict, reached without the pairwise walk — and the whole validate run on that pack went from 1m02s to 6s. Nothing is skipped: a document that really does repeat an entry is still handed to jsonschema's own check and still fails with jsonschema's own message.
- Run it once, in CI, rather than on every local iteration.
- Use
--jsonif a machine is consuming the result; progress goes to stderr, so stdout stays exactly one JSON document.
A run that prints the announcement and then sits there is working. A run that prints nothing at all for minutes is a different problem — check whether another process holds the directory lock (E005).
unrecognized option(s) on a store or vulndb verb#
Some options belong to the command GROUP, not to the verb, and go before the verb: sbomflow store --store ./evidence-store list, sbomflow vulndb --dir ./snapshots inspect. sbomflow store list --store … is refused for that reason, and the refusal says where the option goes.
A flag the group does not have either — --root, say — is a different case. The refusal offers the closest option by spelling, prints what that option is for, and then names the group options separately, because the closest spelling is often not the answer: --product filters by product name and cannot point a verb at a store. Read the description before taking the suggestion. sbomflow help store list lists the verb's own options and, at the end, the group options it accepts but cannot show in its own option list.
Environment & installation#
sbomflow: command not found#
The entry point is not on your PATH. Ensure your Python scripts directory is on PATH, or run via the module: python -m sbomflow --version. Re-installing with python -m pip install . re-creates the entry point.
Run python -m sbomflow, never python -m sbomflow.cli. The package entry point runs the minimum-Python gate and the last-resort error handler before the rest of the package is imported; the submodule form skips both. On an unsupported interpreter python -m sbomflow prints SBOMFlow requires Python 3.11 or newer (found …) and exits 2, while python -m sbomflow.cli fails partway down the import chain with an ImportError traceback that says nothing about the actual cause.
Optional extras & "zero required dependencies"#
SBOMFlow's core runs on the standard library only — no required third-party runtime dependencies. A few optional capabilities use extras, and their absence is reported (never a crash):
- PyYAML — nicer YAML config parsing. Without it, a bundled fallback loader handles
sbomflow.yaml; complex YAML may needpython -m pip install pyyaml. - cryptography (
python -m pip install cryptography) — signature verification forsign-bundle/verify-bundle,cosign-verify, andsbomflow license. Without it, those reportunverifiable_no_extrainstead of failing the whole run.
sbomflow doctor . lists which extras are present. It reports three states, not two, because "not installed" and "installed and unusable" need opposite remedies:
present— importable and in use.missing— not installed. A supported configuration: the path that needs it is skipped, never faked.present but BROKEN— installed here, but importing it raises. doctor prints what it raised. This is an installation to repair (reinstall or uninstall it), not one to add: installing it again changes nothing, because it is already there. A wheel built against a shared library that is no longer on the machine, an interrupted upgrade, or another package shadowing the name all land here.
A broken extra is still not a failure. The path it serves is skipped exactly as if the extra were absent, so validate-config parses your sbomflow.yaml through the bundled loader and reports the broken install as a warning beside an otherwise-normal result, rather than refusing the file.
Filesystem errors: disk full, read-only, permission denied#
[E004] means SBOMFlow could not read an input or write an output because the disk is full (ENOSPC), the target is on a read-only filesystem (EROFS), or permission was denied (EACCES). SBOMFlow writes every artifact atomically and refuses rather than leave a truncated or half-written file. Fix: free space, make --output writable, and confirm the target is not a read-only mount. No partial/corrupt artifacts are left behind.
Output on a different filesystem ("cross-device" / EXDEV)#
Artifacts are written atomically (temp file next to the target, then rename). If --output is a symlink or bind-mount whose rename would cross a filesystem boundary, the rename cannot be atomic, so SBOMFlow refuses rather than fall back to a non-atomic copy. Point --output at a directory on a single filesystem (a symlink whose target is on the same filesystem is fine).
A config, review, SBOM or other JSON input saved with a BOM / as UTF-16#
A UTF-8 byte-order mark is tolerated automatically (stripped, so the first key still matches). A UTF-16/UTF-32 file is refused with a message naming the encoding — save the file as plain UTF-8. SBOMFlow never guesses an encoding.
The same rule applies to every JSON input, not only to sbomflow.yaml and review files: an SBOM given to --import-sbom, supplier VEX, scanner reports, advisory snapshots, component-support files, update metadata and the other JSON documents SBOMFlow reads (RFC 8259 section 8.1 lets a parser ignore the mark, and PowerShell Out-File and some .NET generators write one). Exactly one leading mark is ignored; a UTF-16 or UTF-32 export is refused as malformed_external_sbom (or the input's own "not valid UTF-8 JSON" warning) naming the encoding, with "re-export as UTF-8".
Optional external tools reported as "skipped"#
Some importers use external tools. When a tool is not installed, SBOMFlow reports it as skipped / available later — it does not fail the run and never installs anything for you.
Network (opt-in only)#
Proxies, TLS, timeouts#
Network actions are opt-in and individually named. If an enabled online lookup fails behind a proxy or TLS-inspecting gateway, the failure is surfaced as a warning (never silently ignored) and does not corrupt already-written evidence. Each request has a bounded timeout and a response-size cap and is not retried in a loop. Prefer offline snapshot files (--nvd-file, --kev-file, --epss-file) in restricted networks. See Security & privacy for the full per-command network table.
[E012] — unsafe endpoint URL#
A configured endpoint used plain http:// to a non-loopback host or embedded credentials in the URL. Use https:// and keep credentials in environment variables, never in the URL or flags. Plain http:// is allowed only for a loopback host (localhost/127.0.0.1/::1) in testing.
A sync --apply command cannot authenticate#
sync-issues (GitHub), sync-jira, sync-servicenow, and sync-dependencytrack read credentials from environment variables, never from flags (which would leak into shell history). Without --apply they are dry-run and offline; with --apply they need the documented environment variables set. Run the dry-run first to preview exactly what would be sent.
Deterministic output differs between runs#
Provide a fixed --as-of timestamp for fully reproducible runs, and compare with sbomflow validate / compare-releases. Operational timing data is kept in a separate section so it never affects the byte-reproducible artifacts.
An output directory created by current SBOMFlow versions contains the exact .sbomflow-output marker. If it lives under the scanned product, later runs exclude that directory with the named excluded_sbomflow_output disposition; the tool will not mistake its own evidence-pack.json or release record for a product input. If an older output directory lacks the marker, regenerate it once with a current version or move it outside the product tree. Do not add the marker to ordinary product directories: only SBOMFlow-owned output should be excluded. A custody set created by sbomflow custody init carries the exact .sbomflow-custody-set marker instead, and is excluded under the same excluded_sbomflow_output disposition; the coverage note then says how many of those exclusions were custody sets. Every directory excluded because of either marker is also named, with its marker, by the scan_directory_excluded_by_marker warning, so a marker placed on a product directory cannot hide it silently: if the warning names a directory that holds product source, remove the marker or move the directory and re-run. An unreadable custody-set marker is handled like an unreadable output marker below, and the warning names that marker. Recognized evidence and lockfiles under build/ and dist/ remain scan inputs.
If the marker is there but its bytes cannot be read — file mode 000, or undecodable content — the scan cannot establish whether the directory is one of its own evidence packs, so it does not descend, and it records the sbomflow_output_marker_unreadable warning naming the directory. That is deliberately not excluded_sbomflow_output, which asserts the exact marker was read: an unreadable marker is not the same as no marker, and reading it as "not output" is what let a previous run's artifacts come back as product components. Nothing under such a directory is examined, so its absence from the scan is not evidence that it holds nothing. Fix the permissions, or move the directory outside the product tree, and re-run. A directory that cannot be listed at all is a different fault and is reported as an unreadable directory rather than as a marker problem.
"Another run holds this directory's lock" (E005)#
Two runs writing the same --output directory would interleave artifact writes and corrupt the evidence pack and the hash-chained audit log, so every analyze/audit holds a transient .sbomflow.lock in the output directory for the duration of its writes (store mutations — store add, store retain --apply, store empty-trash --apply — hold one in the store root). A second run exits with the stable code E005 naming the holder instead of corrupting anything.
- Another run really is active: wait for it, or use a different
--output. - The message says the lock appears STALE (the holding process crashed, or a lock from another host is hours old): confirm nothing is running, then re-run with
--break-stale-lock. A stale lock is never removed automatically. - The lock is advisory and transient: it never appears in artifacts, the bundle ZIP, or stored release copies, and it contains only operational metadata (pid, hostname, start time). On network filesystems its atomicity depends on the file server (NFSv3 caveats) and staleness falls back to age alone — treat it as coordination between cooperating SBOMFlow runs, not a security boundary.
Exit codes in CI#
| Code | Meaning |
|---|---|
0 | success, or an informational (non-enforced / dry-run) gate |
1 | an enforced release gate blocked the run — a policy decision, not a crash |
2 | usage, input, IO, or config error — see the [Ennn] error code |
3 | init refused / bundle verification failed |
4 | structural validation failure (validate) |
5 | strict warnings-as-errors matched (--strict) |
Full detail: exit codes.
CI behaves exactly like your terminal — by design. SBOMFlow does not detect CI environment variables and does not change behaviour when it thinks it is "in a pipeline". The command you debugged locally is byte-for-byte the command CI runs: same output, same exit codes, same offline default. The only differences are automatic and cosmetic: progress and paging degrade to plain, deterministic output when not attached to a terminal.
One map of every code family#
Four stable, separate code families appear in SBOMFlow output. They never overlap, and none of them is ever renamed or renumbered once shipped:
| You see | Family | Where it is documented |
|---|---|---|
[E001] … [E099] on a failed command | Error codes — why this command could not do its work | error reference, or offline sbomflow help error <code> |
unsupported_file, stale_vulnerability_data, … in scan-warnings.json | Warning codes — inputs observed but not silently trusted | warning catalog |
GATE_KEV_PRESENT, GATE_EVIDENCE_GAPS, … in release-gate.json | Gate reason codes — why an enforced policy said "do not release" | exit codes |
The process exit status (0–5, 130, 141) | Exit codes — what the process outcome means for scripts/CI | exit codes, or offline sbomflow help exit <code> |
Reporting a problem well#
A report that names three things is usually solvable in one round-trip:
- Your version and platform —
sbomflow --version, plus OS. - The exact command and its
[Ennn]code (or exit code). For an unexpected internal error (E099), re-run withSBOMFLOW_TRACEBACK=1to capture the stack for the report. - A sanitized support bundle from the output directory involved:
sbomflow support-bundle ./evidence --yesThe bundle is built for sharing: it contains tool version, capability matrix, config shape (keys and types, never values), per-code warning and error counts, and artifact names/sizes/hashes — and by construction it excludes source contents, component lists, findings, full paths, environment values, and reviewer identities. It is written locally and never transmitted; review it yourself before attaching it. See the redaction contract for the exact guarantees.
Still stuck?#
- Read the run's own diagnostics:
scan-warnings.json,vuln-source-health.json,release-gate.json, and the warning catalog for any code you see. sbomflow doctor .reports environment + config facts that explain most setup problems.- For a suspected security issue, follow the private reporting process in the project's security policy — do not open a public issue for a vulnerability.