Security review readiness

This page is an honest self-assessment of the trust and security controls SBOMFlow already ships, written for a design partner's security or procurement team. It maps each control to where it is enforced so you can verify the claim against the source yourself, and it states — plainly — the surfaces that an independent review would still examine.

Important

This is a self-assessment, not an external audit and not a certification.
No third party has reviewed these controls. Everything below is an
engineering-gap assessment of SBOMFlow the tool — never a compliance,
conformity, or "we are secure" claim. Where a control is partial or a surface
is not yet independently reviewed, this page says so; an honest gap list is
more useful to a reviewer than a clean bill of health.

How to read this page

  • Every control names where it is enforced — a function, a command, or a guard test you can grep and run. A claim you cannot trace to running code is a bug on this page, not a feature of the product.
  • The strongest control leads. Untrusted-input handling is the most developed defence and is described first.
  • Scope feeds the threat model. SBOMFlow ships an engineering threat model for the tool itself (THREAT_MODEL.md in the source), with each mitigation linked to the test that proves it. The what an external review would still examine section is the honest input to that model, not a marketing summary of it.

Untrusted-input handling (the strongest control)

Every file SBOMFlow reads is untrusted — a customer's product tree, a supplier's SBOM, firmware built by someone else's toolchain. A parser that can be crashed, hung, or made to exhaust memory by a hostile or merely malformed input is a denial-of-service in the one tool a release is meant to trust. Ingestion is therefore bounded by construction, and the bound is mechanical.

Claim (honest, one line)Where it is enforced (verify it)
One place declares every resource budget, so no scattered magic number can drift.ResourceLimits in the limits module: max_file_bytes 256 MiB, max_expanded_bytes 8 GiB, max_decompression_ratio 200:1, max_entries 5,000,000, max_json_depth 200, max_field_bytes 8 MiB, plus an opt-in refusal of non-finite NaN/Infinity.
Every untrusted JSON read routes through one bounded loader that enforces the size, structural, and depth budgets.limits.read_json_file (from disk) and limits.loads_bounded (from a string) are the bounded replacements for json.loads. An ordinary SBOM parses byte-identically; only pathological input is refused.
A parser-level stack overflow becomes a catalogued warning, never a crashed run.The loader converts RecursionError into a LimitExceeded, surfaced as a stable resource_limit_exceeded warning; depth guards also sit in the YAML, lockfile, version, and compose parsers.
A raw json.loads on untrusted text cannot be reintroduced without the build failing.The mechanical drift guard test_no_unbounded_json_loads.py AST-parses every module and fails unless each json.load(s) site is the bounded loader or a justified, counted allowlist entry.
A bounded cap is never a silent cap.The silent-truncation audit test_silent_truncation_audit.py discovers every MAX cap in the analysis parsers and fails unless each one warns at the cap, carries its true remainder, or refuses the input whole.
A directory flood degrades to honest indeterminate coverage, never a false clean.Bounded file discovery stops enumerating past the structural budget and reports coverage as indeterminate; an in-budget scan keeps its byte-identical file ordering.
A symlink whose target resolves outside the scan root is skipped, so a planted link cannot pull host secrets into evidence.The walk emits a symlink_outside_root warning and never reads the target.

Verify: make test; the "Reading untrusted input safely" section of the source architecture reference; threat-model row T2 (test_parser_fuzz.py — parsers warn, never crash, on an adversarial corpus).

Note

Bounding the primary parse path was not theoretical: it surfaced and fixed two
real latent defects — a manifest declaring 100,000 dependencies that parsed to
100,000 components with no warning, and a version string with a ~4300-digit
numeric run that crashed the comparator. Both now degrade to a warned, skipped
input. This is not a sandbox; it is the difference between an input the engine
declines out loud and one that takes the run down with it.

Verification and signing

A valid bundle signature authenticates only the manifest bytes — it proves nothing about whether the listed member artifacts are present, unaltered, or complete. Reading "signature valid" as "bundle trustworthy" is exactly the over-broad conclusion the layered verifier exists to prevent.

Claim (honest, one line)Where it is enforced (verify it)
Bundle trust is reported as four independent layers that are never collapsed into a single verdict.sbomflow verify-bundle --layered (the bundle_verify module): content (every listed member exists and matches its recorded sha256 and size), envelope (archive/signature-envelope safety and exact DSSE pre-authentication bytes), semantics (DSSE payload type, in-toto Statement shape, subject binding), and authority (signer identity and key validity).
A layer this build does not evaluate says so honestly, never a silent pass.Each dimension owns a closed status vocabulary; an unevaluated dimension reports not_checked, and trusted_use is true only when every dimension is in its trusted set.
A cryptographically valid but unauthorized signer is a first-class non-trusted outcome, not a pass.The authority layer speaks authorized / valid_but_unauthorized / no_policy_supplied, kept separate from signature validity.
No security branch of the verifier can ship with zero exercising tests.The completeness guard test_bundle_verify_vocabulary.py asserts that some corpus case produces every declared status and problem kind; adding a branch without a producing case fails the suite.

Verify: sbomflow verify-bundle --layered against a bundle; make onboard-check drives install to a four-layer verify, a tamper, and recovery end to end. See also the read an evidence bundle guide.

Decision-transaction integrity

The engine only observes; a human's review, VEX, waiver, and approval decisions are the consequential writes, and they are protected as transactions.

Claim (honest, one line)Where it is enforced (verify it)
Concurrent decision writes cannot silently lose each other.A per-file compare-and-swap captures the file hash before a read-modify-write and an exclusive OS lock (flock(LOCK_EX) on POSIX, msvcrt on Windows) serialises the check-and-write across processes.
The decision/audit history detects an edited, inserted, removed, reordered, or duplicated entry.An append-only, unkeyed SHA-256 hash chain; sbomflow validate refuses a tampered chain and new decisions are refused after tampering.
An interrupted decision write recovers to old-complete or new-complete — never a torn half-state.Crash-consistency is enumerated exhaustively at every interruption point; sbomflow decisions verify-transactions and sbomflow decisions recover expose and converge the state.
Separation of duties is enforced: a reviewer cannot approve their own submission, and delegation is one hop only.Self-approval is rejected; a chained delegation beyond one hop is refused.

Important

Honest boundary: the audit log is tamper-evident, not immutable. A local
chain cannot prove that its final entries were removed (a valid prefix of a
valid chain is itself a valid chain). Detecting trailing truncation needs an
anchor kept outside the file — audit_chain_head / verify_audit_chain_anchor
support a caller-supplied anchor, but SBOMFlow adds no external system by
default. It is never described as a ledger, blockchain, or immutable storage.

Verify: make onboard-check; sbomflow validate; the recovery playbooks.

Archive and zip handling

Claim (honest, one line)Where it is enforced (verify it)
A decompression bomb is refused rather than expanded.Every archive member is read under the shared limits budgets — total expanded bytes, decompression ratio, and member count — via read_zip_member_bounded; a breach raises LimitExceeded.
A path-traversal, absolute, drive-prefixed, or backslash-separated member is rejected identically by every reader.One helper, is_safe_relative_member, gates every zip reader; a differential corpus test_zip_differential.py pins that the readers agree case by case.

Verify: make test; the untrusted-input section of the source architecture reference; threat-model residual-risk table.

Trust boundaries and no-fabrication

The value of the evidence is that it never fabricates a human judgement or an identity it did not observe.

Claim (honest, one line)Where it is enforced (verify it)
Observed status is never review status.VEX not_affected/fixed, evidence acceptance, and release approval come only from human-authored review files; the gate enforces the boundary. See observed vs reviewed.
An unknown identity, hash, or licence is disclosed as unknown, never invented.A licence the engine cannot conclude is recorded as NOASSERTION; the analyzer raises a hand for an unrecognised file rather than guessing a component.
Nothing leaves the machine on a default path; network actions are explicit and individually named.The offline-by-default network policy is a generated table in security & privacy; adding a command without a network declaration fails tests.
A tool that audits a supply chain is not itself a supply-chain risk.Zero required runtime dependencies (standard library only); optional extras are isolated and skipped when absent.
Attacker-influenced component names cannot inject script into a generated report a partner opens.Every HTML surface escapes interpolated data and filters javascript:/data: hrefs; locked by the adversarial corpus test_html_injection_corpus.py (threat-model row T9).
A mitigation that silently loses its test is caught.The threat-model traceability guard test_threat_model_traceability.py fails if a threat row has no verifying reference or a referenced test disappears.

Determinism (tamper-evidence)

Claim (honest, one line)Where it is enforced (verify it)
The same input produces byte-identical evidence, including the bundle ZIP, so a post-hoc change to any artifact is detectable.Stable IDs, sorted keys, a pinned --as-of, and a reproducible ZIP; determinism is asserted across a timezone-by-locale matrix, not just re-runs. sbomflow validate recomputes and re-checks artifact hashes.

Verify: make test; make audit; testing & trust.

What an external review would still examine

The controls above are self-tested, not independently reviewed. SBOMFlow's roadmap names an external security review of four surfaces — verification, signing, decision-transaction, and archive handling — before any general- availability claim. That review has not happened. A reviewer should treat the following as the honest scope and the known residual risks, stated plainly.

Surfaces named for an outside firm

  • Verification and signing — the four-layer bundle verifier, the DSSE envelope and in-toto semantics checks, and the signer-authority model.
  • Decision-transaction integrity — the compare-and-swap lock, the hash chain, and the crash-recovery protocol.
  • Archive handling — the decompression and path-safety defences on hostile bundles and archives.

Known residual risks (not closed, disclosed on purpose)

  • Signed-bundle transitive coverage. The layered store verify re-hashes the files a stored manifest lists, but the standalone signed-bundle verifier authenticates the manifest bytes without re-hashing each listed file. Until that per-file check lands, do not read a successful manifest signature as transitive verification of bundle contents.
  • Audit trailing-truncation. As above, the hash chain is tamper-evident, not immutable; removing entries from the end of a local chain is only detectable with an anchor preserved off-machine or a signed bundle.
  • DNS-rebinding (TOCTOU) on opt-in network paths. The SSRF guard requires every resolved answer to be globally routable at check time and re-validates each redirect, but the narrow window between validation and the connection's own name resolution is not closed.
  • External analyzer adapters are not OS/network sandboxed on the standard library. The declared adapter network_policy is recorded, not enforced — run an untrusted adapter inside your own container or sandbox. Adapter output is an observation, never a decision.
  • Firmware signature verification proves key possession, not signer trust. A verified result checks bytes against an operator-supplied public key; no built-in key is trusted.
  • SBOMFlow trusts the operator's host. It defends the integrity of the evidence it produces on a trusted machine; it does not defend against a compromised host, a malicious operator, or tampering with its own installed code.

Explicitly out of scope (no server today)

Multi-tenant isolation, an authentication/authorization layer, and a network service surface — SBOMFlow is a local command-line tool and library with no server. If a hosted surface is ever added, this scope will be extended.

How to verify each claim yourself

Everything here runs offline from a source checkout. A reviewer can reproduce the controls without trusting this page.

Run thisWhat it exercises
make testThe full offline suite, including the untrusted-loader drift guard, the silent-truncation audit, the zip differential corpus, the parser fuzz corpus, the four-layer bundle-verify completeness guard, the hash-chain tamper tests, the HTML-injection corpus, and the threat-model traceability guard.
make qualityLint (ruff) plus strict type-checking (mypy) across the security-sensitive modules — the loader, signing, audit, atomic I/O, run-lock, reviewing, and SSRF guard among them.
make onboard-checkA clean-machine installed journey: install to review to gate to bundle to four-layer verify to a deliberate tamper to recovery.
make auditComposes the deterministic artifacts into a reviewer bundle and records the audit-chain anchor; the bundle adds no authority of its own.
make check-docs-publicationThe fail-closed leak guard that keeps this page and every public page free of internal repository paths and forbidden phrases.

Named guard tests a reviewer can grep and run directly: test_no_unbounded_json_loads.py, test_silent_truncation_audit.py, test_zip_differential.py, test_bundle_verify_vocabulary.py, test_threat_model_traceability.py, test_html_injection_corpus.py, test_parser_fuzz.py.

What this page is not

It is not a certification, an audit report, or a statement of conformity, and it does not claim SBOMFlow is free of defects. No self-assessment can prove the absence of a vulnerability — that is precisely why an independent review of the surfaces above is on the roadmap. Any claim on this page that cannot be traced to running code and a test is a bug.

Next: security & privacy · testing & trust · known limitations · recovery playbooks