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.mdin 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 verifyre-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_policyis 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
verifiedresult 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 this | What it exercises |
|---|---|
make test | The 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 quality | Lint (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-check | A clean-machine installed journey: install to review to gate to bundle to four-layer verify to a deliberate tamper to recovery. |
make audit | Composes the deterministic artifacts into a reviewer bundle and records the audit-chain anchor; the bundle adds no authority of its own. |
make check-docs-publication | The 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