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:

bash
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 facts

Online: 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 happenedGo to
The command exited non-zero with a [Ennn] codeError codes → then the full error reference
The command exited non-zero without a codeExit codes — match the number
It succeeded but the results look wrongResults look wrong
A release gate blocked the run (exit 1)Release gate blocked
sbomflow validate failed (exit 4)Integrity: tamper &amp; validation
A reviewer decision did not take effectReviewer, VEX, waivers &amp; approvals
An input, SBOM, or lockfile was skipped or rejectedInputs &amp; imports
A sbomflow license questionLicensing &amp; entitlements
An install / environment / filesystem problemEnvironment &amp; installation
A network / proxy / sync problemNetwork (opt-in only)
A crash / integrity / staleness state to recover fromRecovery playbooks — tested diagnose → recover paths
The store lives on a network share, cloud-synced folder, or removable driveFilesystem & 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>.

CodeExitSymptomFix
E0012Target directory not foundpass an existing product directory; try sbomflow quickstart .
E0022Target path is not a directorypoint at a directory; import a single SBOM with --import-sbom
E0032Output path is not a directorychoose a directory for --output (created if missing)
E0042Filesystem error reading inputs or writing outputsfree disk space; make --output writable; not a read-only mount — see filesystem errors
E0102Malformed or unknown-key configurationsbomflow validate-config <file> then sbomflow doctor .
E0112Reviewer decision file changed since it was reada concurrent reviewer wrote first; nothing was overwritten — re-run to pick up their decision and retry
E0122Unsafe network endpoint URLuse 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:

  1. Check which sources ran. vuln-source-health.json records every vulnerability source used, its freshness, and whether any produced no usable intelligence.
  2. 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 a vulndb snapshot) or explicitly opt into the network with --use-osv / --use-nvd.
  3. 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.
  4. Stale snapshot? A stale_vulnerability_data warning means your local snapshot is older than the configured window — refresh the snapshot file.
  5. 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.

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 two recoverable traces, and sbomflow doctor <dir> now reports both — with the exact fix — under a recovery line (also in doctor --json as a recovery block):

  • Stale run lock — a leftover .sbomflow.lock from 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.lock file 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 (or sbomflow audit) to regenerate a complete evidence pack.

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 python executable 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.
  • 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_MAX limits (path_length in doctor --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) and sbomflow help exit (the exit-code contract), with their code counts (docs_availability in doctor --json). The web docs URLs error messages will print are recorded — including a SBOMFLOW_DOCS_URL mirror 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.

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.


Reviewer, VEX, waivers &amp; 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 seeWhat happenedWhere to look
no waivers lineEither no waivers were supplied, or every one was honoured and none is near expiry.waiver_status.active
N expiring within …d, M expiredThe 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 readNo 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. 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 &amp; imports#

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.

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 &amp; 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.

From delivery to first run#

  1. 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.
  2. Verify it before installing anything: sbomflow license verify ./entitlement.json --public-key issuer.pem.
  3. Install it where SBOMFlow looks: sbomflow license install ./entitlement.json --public-key issuer.pem (or pipe it: cat entitlement.json | sbomflow license install -).
  4. Confirm with sbomflow license status, which reads the same file every other command reads.

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#

outcomeMeaningExit
installednothing was there; the verified entitlement is now in place0
unchangedthe same entitlement was already installed; nothing was written0
replaceda different entitlement was superseded (clean renewal, or you instructed it)0
refused_sourcethe delivered file could not be read safely (absent, unreadable, over budget, not a JSON object, duplicate keys)2
refused_verificationschema, signature, trusted key, revocation, or declared scope failed the offline check2
refused_replacementsuperseding what is installed needs --replace (or a yes at the prompt)2
refused_destinationthe destination is not a regular file, or could not be written2
refused_concurrent_changeanother process wrote the destination mid-install; re-run2

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 with pip install "sbomflow[sign]". install refuses rather than filing a file it could not check.
  • Lost or corrupted install — re-run sbomflow license install with your delivered file; --replace is 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.
statusMeaningWhat to do
validgenuine and in-windownothing — exit 0
expiring_soonvalid, expires within 30 daysplan a renewal — exit 0, still fully functional
graceexpired, within the 30-day grace windowrenew soon — exit 0, still functional
expiredpast expiry + gracerenew — exit 1; past evidence is never touched
not_yet_validnot_before is in the futurecheck the entitlement's start date / your clock — exit 1
revokedlisted in a supplied revocation listcontact the issuer — exit 1
untrusted_keyno trusted public key matchedsupply the correct --public-key — exit 2
invalid_signaturesignature does not verifythe file was modified or is for a different key — exit 2
clock_suspiciousissued_at is in the future vs your clockfix system time — exit 2
malformednot a valid entitlement documentcheck the file — exit 2
as_of_invalidthe --as-of you supplied is not a readable ISO-8601 UTC timestampfix 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_extrathe optional cryptography extra is not installedpip install "sbomflow[sign]" to verify signatures
no_entitlementno --file given and nothing installed at the per-user paththis is fine — SBOMFlow enforces nothing; exit 0

Exit-code contract: 0 = genuine &amp; 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 &amp; 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.


Environment &amp; 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.cli --version. Re-installing with python -m pip install . re-creates the entry point.

Optional extras &amp; "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 need pip install "sbomflow[yaml]".
  • cryptography (pip install "sbomflow[sign]") — signature verification for sign-bundle/verify-bundle, cosign-verify, and sbomflow license. Without it, those report unverifiable_no_extra instead of failing the whole run.

sbomflow doctor . lists which extras are present.

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).

Config or review file 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.

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 &amp; 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. Recognized evidence and lockfiles under build/ and dist/ remain scan inputs.

"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#

CodeMeaning
0success, or an informational (non-enforced / dry-run) gate
1an enforced release gate blocked the run — a policy decision, not a crash
2usage, input, IO, or config error — see the [Ennn] error code
3init refused / bundle verification failed
4structural validation failure (validate)
5strict 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 seeFamilyWhere it is documented
[E001][E099] on a failed commandError codes — why this command could not do its workerror reference, or offline sbomflow help error <code>
unsupported_file, stale_vulnerability_data, … in scan-warnings.jsonWarning codes — inputs observed but not silently trustedwarning catalog
GATE_KEV_PRESENT, GATE_EVIDENCE_GAPS, … in release-gate.jsonGate reason codes — why an enforced policy said "do not release"exit codes
The process exit status (05, 130, 141)Exit codes — what the process outcome means for scripts/CIexit codes, or offline sbomflow help exit <code>

Reporting a problem well#

A report that names three things is usually solvable in one round-trip:

  1. Your version and platformsbomflow --version, plus OS.
  2. The exact command and its [Ennn] code (or exit code). For an unexpected internal error (E099), re-run with SBOMFLOW_TRACEBACK=1 to capture the stack for the report.
  3. A sanitized support bundle from the output directory involved:
bash
sbomflow support-bundle ./evidence --yes

The 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?#

  1. 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.
  2. sbomflow doctor . reports environment + config facts that explain most setup problems.
  3. 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.