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 how it is enforced — a command you can run, a refusal you can observe, or a build-time guard that fails the suite. A claim that cannot be traced 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 maintains an engineering threat model for the tool itself, with each mitigation linked to the test that proves it; it is available under the private-review process. 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. | One registry declares every budget: 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. The same figures are on known limitations. |
| Every untrusted JSON read routes through one bounded loader that enforces the size, structural, and depth budgets. | Every untrusted JSON read — from disk or from a string — goes through one bounded loader. An ordinary SBOM parses byte-identically; only pathological input is refused. |
| A parser-level stack overflow becomes a catalogued warning, never a crashed run. | A parser recursion overflow is converted into a refusal surfaced as a stable resource_limit_exceeded warning; depth guards also sit in the YAML, lockfile, version, and compose parsers. |
| An unbounded JSON read on untrusted text cannot be reintroduced without the build failing. | A build-time guard fails if any module reintroduces an unbounded JSON read. |
| A bounded cap is never a silent cap. | A build-time audit fails if any parser cap can truncate silently: every cap 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. | A symlink_outside_root warning is emitted and the target is never read. Every reader that discovers files from the scan target — the file walk, the supplier-evidence intake, and product-config discovery — shares one containment helper, so the refusal is the same rule in every case. |
Verify: hand sbomflow analyze a malformed or oversized input and read scan-warnings.json — the refusal is named with its path and the run completes. An adversarial parser corpus asserts warn-never-crash on every build.
Note
Bounding the primary parse path was not theoretical: it surfaced and fixed two
real latent defects (a resource exhaustion and a comparator crash). 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: 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. | A completeness guard asserts that every declared verifier status and problem kind is produced by some corpus case; adding a branch without a producing case fails the suite. |
Verify: sbomflow verify-bundle --layered against a bundle, then against a copy with one member altered; the installed journey 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-level lock 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 — the chain head can be anchored outside the
file by a caller, but SBOMFlow adds no external system by default. It is never
described as a ledger, blockchain, or immutable storage.
Verify: sbomflow validate after editing one audit-log entry; sbomflow decisions verify-transactions; 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 budgets — total expanded bytes, decompression ratio, and member count — and a breach is refused rather than expanded. |
| A path-traversal, absolute, drive-prefixed, or backslash-separated member is rejected identically by every reader. | One shared path-safety check gates every archive reader, and a differential corpus pins that the readers agree case by case. |
Verify: hand sbomflow verify-bundle --layered a zip carrying a traversal-named or oversized member; the refusal is reported, never expanded.
Model artifacts and AI manifests#
A model file is hostile input, and the inventory that reads it runs on the default path, inside CI, over a tree that may be attacker-authored.
| Claim (honest, one line) | Where it is enforced (verify it) |
|---|---|
| No model is loaded, deserialised, imported, or executed, and no model runtime is imported. | Identification reads a small fixed prefix and matches documented magic numbers or structural invariants; a test asserts that no model runtime is imported and that hashing a real artifact stays memory-bounded. |
| Formats whose only faithful reader runs code are recognised and refused, not skipped. | They are recorded and hashed with ai_model_unsafe_format_not_parsed and never opened; every refused extension is asserted to be recorded rather than dropped. |
A pickle wearing a .onnx name is caught by its bytes. | A recognised-unsafe signature outranks every extension and raises ai_model_extension_signature_mismatch; a SafeTensors header longer than its file does not identify. |
| The tree walk is bounded on entry count and directory depth, and a bounded walk is reported as partial. | Both budgets come from the shared limits defaults, name themselves in bounds_hit, and mark the inventory partial; one over-deep branch never erases its siblings. |
| A symlink out of the scan root is skipped, not followed. | The walk never follows symlinks and reports ai_model_symlink_skipped, so a crafted tree cannot make the scanner hash arbitrary host files. |
| A credential pasted into a declared AI manifest is refused, and the refusal never echoes it. | Credential shapes are matched on the value, not the field name, and neither the manifest error nor the agent vocabulary error repeats the rejected value. |
| An empty or truncated AI inventory is never presented as "no AI" or as agreement. | The artifact is written on every run with ai_scan_performed; reconciliation carries compared, observed_only and declared_only, so zero conflicts never reads as verified. |
Verify: sbomflow analyze over a tree holding a pickle-backed model file, then read ai-evidence.json; capabilities; known limitations.
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 an adversarial HTML-injection corpus. |
| A mitigation that silently loses its test is caught. | A traceability guard fails if a threat-model 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: run sbomflow analyze twice with the same --as-of and compare the artifact digests; 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)
- A manifest-only signature does not cover members.
sbomflow verify-bundlewithout--layeredauthenticates the manifest bytes only. Always verify a handed-over bundle with--layered, which re-hashes every listed member, and never read a manifest-only signature as proof of the bundle's contents. See verifying a handed-over bundle. - 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.
- Opt-in network paths are hardened, not sandboxed. They refuse loopback, private and cloud-metadata addresses, re-validate every redirect and cap the response size; they are not a substitute for network egress controls. Run them from an egress-controlled network.
- 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.
- The AI inventory is bounded, format-limited, and observed-only today. Formats it does not recognise, models fetched at runtime, and models behind an external service are outside what any filesystem walk can see, so an empty inventory means "recognised nothing", never "contains no AI". A run without
--ai-manifestis observed-only and says so: zero conflicts is reported as no comparison, never as agreement. Identification strength is not integrity: a matched magic number establishes the container, not that the weights are the intended ones. - Recording an agent's declared authority is not a control. Nothing observes, constrains, sandboxes or monitors a running model or agent, and inventorying agents does not mitigate runtime prompt injection.
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 against an installed build. A reviewer can reproduce the observable controls without trusting this page.
| Run this | What it exercises |
|---|---|
sbomflow analyze <dir> over a tree holding malformed, oversized and traversal-named inputs | Bounded ingestion: every refusal lands in scan-warnings.json with its path and a stable code, and the run completes. |
sbomflow verify-bundle <bundle> --layered, then again after altering one member | The four-layer verifier: the content layer reports member_hash_mismatch and the bundle is NOT TRUSTED; a manifest-only check would not have noticed. |
sbomflow validate <dir> after editing one audit-log line | Hash-chain tamper evidence: exit 4 with the entry named, and new decisions are refused. |
sbomflow decisions verify-transactions <dir> | Decision-transaction integrity and crash recovery. |
| The installed journey | A clean-machine journey: install to review to gate to bundle to four-layer verify to a deliberate tamper to recovery. |
The build-time guards named above — the unbounded-read guard, the silent-truncation audit, the archive differential corpus, the parser fuzz corpus, the verifier completeness guard, the HTML-injection corpus and the threat-model traceability guard — run in the suite behind every release. Their sources are part of the private-review pack.
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