Security & privacy

SBOMFlow is designed so that sensitive build information stays with you.

Your machine — everything runs here

source & build treeSBOMFlow engineoutput directory (plain files)

162 of 174 commands in the registry are fully offline — no gate exists that could take them online.

Named public sources — only behind an exact gate

  • sbomflow analyze--use-osv --use-nvd --use-kev --use-epss --resolve-west-imports config: osv/nvd enabled with scan.offline=false config: exploitation.use_kev/use_epss=true with scan.offline=false
  • sbomflow audit--use-osv --use-nvd --use-kev --use-epss --resolve-west-imports config: osv/nvd enabled with scan.offline=false config: exploitation.use_kev/use_epss=true with scan.offline=false
  • sbomflow batch-runeach job's own config: osv/nvd enabled with scan.offline=false each job's own config: exploitation.use_kev/use_epss=true with scan.offline=false
  • sbomflow component-supportsubcommand: fetch
  • sbomflow component-support fetchexplicit `component-support fetch` invocation
  • sbomflow notify--apply
  • sbomflow sync-dependencytrack--apply
  • sbomflow sync-issues--apply
  • sbomflow sync-jira--apply
  • sbomflow sync-servicenow--apply
  • sbomflow vulndbsubcommand: update
  • sbomflow vulndb updateexplicit `vulndb update` invocation --source csaf-provider
Generated from the same network-policy registry as the table below; the counts and gates cannot drift from it.

Offline by default#

Offline

The baseline engine runs entirely offline. A default audit makes zero
network requests. This is verified by tests in the project.

By default, none of the following leaves your environment: source code, firmware, SBOMs, file or directory paths, vulnerability details, reviewer notes, or build artifacts.

Terminal and text-output safety#

Attacker-controlled names, paths, and warning text cross one display-safe boundary before human console, Markdown, or plain-text rendering. C0 controls (except tab), C1 controls, ANSI/OSC introducers, carriage returns, and Unicode bidirectional formatting controls are rendered as visible escape sequences, so they cannot clear a terminal, forge a line, set a title, create a live terminal hyperlink, or visually reverse a filename. Ordinary text remains byte-identical. Structured JSON retains the original value and its normal JSON escaping so machine-readable evidence is not rewritten by the display layer.

That the boundary is reached at all is enforced mechanically, not by review: a build guard fails if any shipped module can write to the console without crossing it. The interactive sbomflow setup onboarding keeps its bold headings, while the text inside them — a detected product name, a path you typed, an identifier read out of an evidence pack — still crosses the boundary.

In the HTML report a URL becomes a live link only when its scheme is allow-listed (http://, https://, mailto:) or it carries no scheme at all; anything else is rendered as text with no anchor. That check trims the same leading and trailing bytes a browser's URL parser trims — every codepoint U+0000–U+0020, which is wider than the set Python calls whitespace — so a control byte placed in front of a scheme cannot make the filter and the browser disagree about what the URL says.

Model files are read, never loaded#

Every default run inventories the AI model artifacts in your tree (ai-evidence.json, experimental). That inventory adds no network path: model files are read as bytes and hashed on your machine, and nothing about them is sent to a model registry, hub, or inference service.

  • Nothing is loaded, deserialised, imported, or executed. No model runtime is imported. Identification reads a small fixed prefix and matches documented magic numbers (GGUF, TFLite) or documented structural invariants (SafeTensors; ONNX, which defines no file magic at all). Hashing streams in fixed blocks, so a very large artifact does not become a very large memory cost.
  • Formats whose only faithful reader runs code are recognised and refused. Anything pickle-backed, an executable graph, or an archive that can carry one is recorded and hashed — presence is evidence a reviewer should see — and deliberately never opened.
  • Bytes outrank filenames. A recognised-unsafe signature beats every extension, so a pickle wearing a .onnx name is caught by its bytes and the disagreement is raised rather than swallowed. A file identified only by its name says so, so you can see when a claim rests on a filename.
  • The walk is bounded and does not follow symlinks out of the scan root. If an entry-count or directory-depth budget stops or prunes it, the inventory is reported as PARTIAL and names the budget that fired — a truncated inventory is never presented as a complete one.
  • An empty inventory is stated honestly. It means the scan looked and recognised nothing. It is never a claim that your product contains no AI: unrecognised formats, models fetched at runtime, and models behind an external service are all outside what a filesystem walk can see.

What this does not do: it observes nothing about a running model or agent, assesses no model's behaviour, quality or safety, and reaches no verdict. Recording what an agent is declared to be allowed to do does not constrain that agent and does not mitigate runtime prompt injection. See known limitations.

Named, opt-in network actions#

Network-capable actions are explicit, individually named, and documented. The table below is generated from the same registry that powers command help and sbomflow help --json; adding a command without a declaration fails tests.

CommandNetwork classExact gate(s)
sbomflow analyzeopt-in-network--use-osv, --use-nvd, --use-kev, --use-epss, --resolve-west-imports, config: osv/nvd enabled with scan.offline=false, config: exploitation.use_kev/use_epss=true with scan.offline=false
sbomflow annotateofflineNone
sbomflow approveofflineNone
sbomflow auditopt-in-network--use-osv, --use-nvd, --use-kev, --use-epss, --resolve-west-imports, config: osv/nvd enabled with scan.offline=false, config: exploitation.use_kev/use_epss=true with scan.offline=false
sbomflow auditor-packofflineNone
sbomflow backfillofflineNone
sbomflow batch-runopt-in-networkeach job's own config: osv/nvd enabled with scan.offline=false, each job's own config: exploitation.use_kev/use_epss=true with scan.offline=false
sbomflow build-applicability-evidenceofflineNone
sbomflow bundleofflineNone
sbomflow caseofflineNone
sbomflow case accept-riskofflineNone
sbomflow case advisory-treeofflineNone
sbomflow case assignofflineNone
sbomflow case communicateofflineNone
sbomflow case correlateofflineNone
sbomflow case discloseofflineNone
sbomflow case export-advisoryofflineNone
sbomflow case intakeofflineNone
sbomflow case openofflineNone
sbomflow case propose-propagationofflineNone
sbomflow case remediateofflineNone
sbomflow case reopenofflineNone
sbomflow case showofflineNone
sbomflow case transitionofflineNone
sbomflow case triageofflineNone
sbomflow cmake-evidenceofflineNone
sbomflow commandsofflineNone
sbomflow compare-releasesofflineNone
sbomflow component-supportopt-in-networksubcommand: fetch
sbomflow component-support fetchopt-in-networkexplicit component-support fetch invocation
sbomflow configofflineNone
sbomflow config showofflineNone
sbomflow cosign-verifyofflineNone
sbomflow custodyofflineNone
sbomflow custody addofflineNone
sbomflow custody compareofflineNone
sbomflow custody exportofflineNone
sbomflow custody holdofflineNone
sbomflow custody initofflineNone
sbomflow custody listofflineNone
sbomflow custody release-holdofflineNone
sbomflow custody reviewofflineNone
sbomflow custody showofflineNone
sbomflow custody verifyofflineNone
sbomflow dashboardofflineNone
sbomflow decisionsofflineNone
sbomflow decisions quarantineofflineNone
sbomflow decisions recoverofflineNone
sbomflow decisions transactionsofflineNone
sbomflow decisions verify-transactionsofflineNone
sbomflow diffoscope-compareofflineNone
sbomflow doctorofflineNone
sbomflow elf-evidenceofflineNone
sbomflow emit-user-infoofflineNone
sbomflow esp-idf-evidenceofflineNone
sbomflow examplesofflineNone
sbomflow explainofflineNone
sbomflow export-cleofflineNone
sbomflow export-reqifofflineNone
sbomflow firmware-extractofflineNone
sbomflow gateofflineNone
sbomflow gate simulateofflineNone
sbomflow helpofflineNone
sbomflow indexofflineNone
sbomflow index queryofflineNone
sbomflow index rebuildofflineNone
sbomflow index validateofflineNone
sbomflow index-releasesofflineNone
sbomflow ingest-build-cve-reportofflineNone
sbomflow ingest-codex-securityofflineNone
sbomflow ingest-cyclonedx-vdrofflineNone
sbomflow ingest-grypeofflineNone
sbomflow ingest-preserved-artifactofflineNone
sbomflow ingest-sarifofflineNone
sbomflow ingest-trivyofflineNone
sbomflow initofflineNone
sbomflow licenseofflineNone
sbomflow license inspectofflineNone
sbomflow license installofflineNone
sbomflow license statusofflineNone
sbomflow license verifyofflineNone
sbomflow linker-map-evidenceofflineNone
sbomflow mcuboot-evidenceofflineNone
sbomflow mcuboot-verifyofflineNone
sbomflow mender-evidenceofflineNone
sbomflow migrate-inofflineNone
sbomflow next-actionsofflineNone
sbomflow notifyapply-gated--apply
sbomflow oci-evidenceofflineNone
sbomflow passportofflineNone
sbomflow passport issueofflineNone
sbomflow passport verifyofflineNone
sbomflow policyofflineNone
sbomflow policy diffofflineNone
sbomflow policy showofflineNone
sbomflow portfolioofflineNone
sbomflow prov-mapofflineNone
sbomflow provenance-checkofflineNone
sbomflow quickstartofflineNone
sbomflow rauc-evidenceofflineNone
sbomflow recheckofflineNone
sbomflow regime-profileofflineNone
sbomflow regime-viewofflineNone
sbomflow release-chainofflineNone
sbomflow release-chain verifyofflineNone
sbomflow reportofflineNone
sbomflow report freshnessofflineNone
sbomflow reproducible-compareofflineNone
sbomflow reviewofflineNone
sbomflow review-mergeofflineNone
sbomflow review-viewofflineNone
sbomflow sbom-setofflineNone
sbomflow sbom-set compareofflineNone
sbomflow schemaofflineNone
sbomflow schema listofflineNone
sbomflow schema showofflineNone
sbomflow setupofflineNone
sbomflow sign-bundleofflineNone
sbomflow standards-viewofflineNone
sbomflow storeofflineNone
sbomflow store addofflineNone
sbomflow store empty-trashofflineNone
sbomflow store exploitation-deltaofflineNone
sbomflow store exportofflineNone
sbomflow store holdofflineNone
sbomflow store listofflineNone
sbomflow store queryofflineNone
sbomflow store register-renameofflineNone
sbomflow store reindexofflineNone
sbomflow store reissueofflineNone
sbomflow store reissue-planofflineNone
sbomflow store release-holdofflineNone
sbomflow store retainofflineNone
sbomflow store showofflineNone
sbomflow store timelineofflineNone
sbomflow store verifyofflineNone
sbomflow store verify-gatesofflineNone
sbomflow store verify-replayofflineNone
sbomflow supplier-packageofflineNone
sbomflow supplier-package emitofflineNone
sbomflow support-bundleofflineNone
sbomflow swupdate-evidenceofflineNone
sbomflow syft-scanofflineNone
sbomflow sync-dependencytrackapply-gated--apply
sbomflow sync-issuesapply-gated--apply
sbomflow sync-jiraapply-gated--apply
sbomflow sync-servicenowapply-gated--apply
sbomflow twister-evidenceofflineNone
sbomflow update-manifest-evidenceofflineNone
sbomflow upgradeofflineNone
sbomflow upgrade inspectofflineNone
sbomflow upgrade migrate-copyofflineNone
sbomflow upgrade planofflineNone
sbomflow upgrade validateofflineNone
sbomflow uptane-evidenceofflineNone
sbomflow validateofflineNone
sbomflow validate-annexii-modelofflineNone
sbomflow validate-article14-modelofflineNone
sbomflow validate-configofflineNone
sbomflow validate-graphofflineNone
sbomflow validate-ssvc-modelofflineNone
sbomflow validate-techdoc-modelofflineNone
sbomflow verify-bundleofflineNone
sbomflow verify-package-artifactsofflineNone
sbomflow vulndbopt-in-networksubcommand: update
sbomflow vulndb change-impactofflineNone
sbomflow vulndb inspectofflineNone
sbomflow vulndb pinofflineNone
sbomflow vulndb reindexofflineNone
sbomflow vulndb retentionofflineNone
sbomflow vulndb sourcesofflineNone
sbomflow vulndb updateopt-in-networkexplicit vulndb update invocation, --source csaf-provider
sbomflow vulndb verifyofflineNone
sbomflow zephyr-evidenceofflineNone

Network — opt-in

These send identifiers, never your source or firmware. Offline snapshot
files (--nvd-file, --kev-file, --epss-file) keep even enrichment offline.

Air-gapped use#

SBOMFlow works fully air-gapped: run offline, use snapshot files for enrichment, and never enable a network flag. Nothing is required to phone home.

The advisory recipe, with no conversion step:

  1. On a connected machine, download OSV's per-ecosystem archive, for example https://storage.googleapis.com/osv-vulnerabilities/PyPI/all.zip, and record its SHA-256.
  2. Copy the file to the air-gapped machine and check the SHA-256 there.
  3. Point the run at it as it is: sbomflow analyze . --osv-snapshot all.zip (or osv.snapshot: all.zip in the configuration).

Each member of the archive is read as one OSV record, bounded, in name order, and nothing is extracted to disk; the archive's declared totals are checked against the same archive-wide budgets as every other archive SBOMFlow reads before any member is expanded, and a member holding NaN or Infinity is refused by name. A duplicate member name, an unsafe member path, a member that is not one JSON record or a duplicate JSON key refuses the snapshot by name, because a partly read advisory set would under-report. The archive's SHA-256 is recorded as the snapshot digest of the run. A .json or .jsonl snapshot is read as strictly: NaN, Infinity, a repeated key, and a line or list entry that is not an OSV record object each refuse the snapshot, naming the file and the line.

Network-failure behavior (opt-in paths)#

When you do enable a network path (OSV / NVD / CISA KEV / FIRST EPSS), every failure mode is handled the same way: the failure is recorded as an error and the run continues with the findings left intact — SBOMFlow never produces a half-enriched pack that looks complete, and never raises an uncaught error at the operator. Timeouts, connection errors, HTTP 4xx/5xx (including rate-limit 429s), malformed or truncated JSON, and oversized responses are each caught: enrichment that could not run leaves findings unchanged rather than silently asserting "not exploited", and the error is surfaced in the run's provenance. Each request uses a bounded timeout and a response-size cap, and requests are not retried in a loop (a failure is reported, not hammered). This is proven offline by the network-failure conformance test suite.

The same response-size cap applies to the opt-in sync and notify paths (sync-dependencytrack, sync-issues, sync-jira, sync-servicenow, notify --apply): the reply from your tracker or webhook is read up to a bounded size and refused beyond it rather than truncated, so a runaway or hostile endpoint cannot exhaust memory and a partial reply is never recorded as a complete one. The refusal is reported as an error on the run.

Running behind a corporate proxy#

A pilot's CI environment is often behind an egress proxy with a private CA. The opt-in network paths (OSV / NVD / CISA KEV / FIRST EPSS) share one transport built on the Python standard library, which honors the usual environment variables — no SBOMFlow-specific flags exist or are needed:

  • HTTP/HTTPS proxy: set http_proxy / https_proxy (and no_proxy to exclude specific hosts) before running an opt-in command. urllib picks these up automatically.
  • Private/custom CA: set SSL_CERT_FILE (a single PEM bundle) or SSL_CERT_DIR (a hashed certificate directory) so TLS verification trusts your corporate root — this is standard OpenSSL behavior, not a SBOMFlow override. Without it, a certificate signed by an untrusted private CA fails verification and is recorded as an error, per the network-failure behavior above — it never silently proceeds unverified, and SBOMFlow never offers a "skip verification" flag.

A proxy that is misconfigured or unreachable fails the same way as any other network error above: the failure is recorded, the run continues with findings left intact, and nothing crashes. The offline-by-default path never reads these variables at all — a poisoned or unreachable proxy has zero effect unless you explicitly pass an opt-in network flag. This is proven offline against loopback-only proxy and TLS fixtures — no live network is used to verify it.

Generated HTML reports are self-contained and CSP-locked#

Every HTML surface SBOMFlow generates (the assessment report, reviewer console, release drift report, dashboard, and shared/auditor pages) is a single self-contained file — no scripts, no external stylesheets, fonts, or images, and inline CSS only. Each one embeds a strict Content-Security-Policy meta (default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none') so that, when opened from an internal web server, the browser mechanically enforces that claim: nothing loads from the network and no script can run. unsafe-inline applies to the single inline <style> block only; scripts are never permitted.

Accessible, log-safe terminal output#

SBOMFlow's command-line output is plain text and ANSI-free by default. No colour or escape sequences appear on any non-interactive path, so output stays readable when piped into CI logs, redirected to a file, or read through a screen reader or a refreshable Braille display.

  • NO_COLOR is honoured. Setting the NO_COLOR environment variable to any non-empty value disables the only colour SBOMFlow can emit. TERM=dumb and the sbomflow setup --no-color flag have the same effect.
  • Colour is opt-in and terminal-only. The single place SBOMFlow emits ANSI colour is the optional interactive sbomflow setup onboarding, and only when it is attached to a real terminal. Redirected or piped output is never coloured.
  • Progress is terminal-only and escape-free. The live progress line shown during a long scan appears only on an attached terminal, where it redraws a single line with a carriage return — never a cursor- or colour-control escape sequence. Redirected output is the same plain text, byte for byte. Set SBOMFLOW_NO_PROGRESS to any non-empty value to silence it entirely.

This behaviour is locked by a repository-wide test that runs representative commands under NO_COLOR on a pseudo-terminal and asserts that zero ANSI escape bytes are emitted.

Local metrics stay local#

sbomflow analyze --emit-metrics can write a local metrics.json containing aggregate counts, gate summary, phase durations, tool version, and any local release metadata supplied for that run. It records that it contains no source content, credentials, or personal contact details, but you should still inspect release metadata before sharing it. This artifact is yours — SBOMFlow does not upload it and there is no vendor telemetry.

Secrets#

Credentials for opt-in integrations come from environment variables, never from command-line flags (which would leak into shell history) and never written into evidence artifacts.

A lockfile or manifest can itself carry a credential: a token in the address a package was fetched from (https://<token>@host/..., https://user:password@host/...) or a signature or token parameter in its query. SBOMFlow replaces that part with *** everywhere it records the URL, including the SBOMs and the evidence pack, keeps the component and the rest of the address, and warns once per file with source_url_credentials_withheld without repeating the value. The source file is not changed, so rotate a credential that was ever committed or shared. A bare ssh account name such as git@ is not a credential and is kept.

No regulatory submission#

SBOMFlow never files, transmits, signs, or submits a regulatory report, and never contacts a CSIRT, ENISA, or a reporting platform. CRA Article 14 outputs are always unsigned drafts for a human to complete.

Organisational continuity and identity-bearing records#

SBOMFlow records evidence and human decisions; it does not decide the legal basis, retention period, or disclosure audience for personal data. Establish those rules with your privacy and legal advisers before operating a shared store. The procedures below preserve the distinction between historical integrity and present-day access:

  • Erasure request versus frozen history: do not edit a frozen release, approval ledger, or hash-chained journal in place. Restrict access while the request is assessed. When removal is required under your organisation's policy, record an authorised superseding action and retain only the minimum separately controlled linkage that policy requires. If the original must be deleted, record the custody/retention action outside the deleted artifact; never rewrite history so its hashes appear unchanged.
  • Ownership transfer: inventory the store, snapshots, keys, approval policies, and open cases; record the former and receiving custodian, scope, time, and transfer verification in the organisation's controlled handover system. Rotate access and integration credentials. A transfer changes custody, not the meaning or author of an earlier observation or sign-off.
  • Consultants and manufacturers: identify the human actor and the capacity in which they acted. A consultant's analysis must not be relabelled as a manufacturer approval, and a free-form author value is attribution—not proof of employment, authority, identity, or legal responsibility.
  • Identity scope: reviewer names, email addresses, notes, and reason text are identity-bearing data. Limit collection to the intended workflow and use a share profile for external exports. A no-profile approvals export emits an IDENTITY-BEARING EXPORT warning because it deliberately retains reviewer identities and authored reason text.

These are operational safeguards, not legal advice. A superseding record does not erase the original, and a hash chain proves only integrity of the records it still covers—not that retention or erasure was legally sufficient.