Testing and trust
Trust in an evidence tool has to be earned mechanically, not claimed. This page describes how SBOMFlow is tested, what the checks actually verify, how the product behaves when inputs go wrong, and how you can re-run the verification yourself. All figures below were produced by running the listed commands against the current source tree; re-run them to check us.
| Lab area | Cases | Passed | Failed |
|---|---|---|---|
| customer-isolation | 1 | 1 | 0 |
| determinism | 1 | 1 | 0 |
| identity-accuracy | 8 | 8 | 0 |
| launch-readiness | 3 | 3 | 0 |
| license-accuracy | 1 | 1 | 0 |
| release-gate | 10 | 10 | 0 |
| resilience | 10 | 10 | 0 |
| security-redaction | 10 | 10 | 0 |
| usability-contracts | 8 | 8 | 0 |
| vulnerability-accuracy | 9 | 9 | 0 |
| Total | 61 | 61 | 0 |
Current verified figures#
| Check | Result |
|---|---|
Offline test suite (make test) | 15,000+ tests, passing (optional-extra paths are reported as explicit skips — never hidden, and a guard test fails if any test hard-imports an optional extra, so the suite stays green on a stdlib-only interpreter); a drift test fails if this figure falls behind the real suite |
| Tests an interpreter did not run | Counted and named per optional extra by scripts/gauntlet_scope.py --census, and printed inside the make gates banner and every gauntlet's SCOPE OF THIS PASS block. A green run states how many tests it held back and which extra would release them, so "the suite passed" cannot be read as "the suite ran everything". A missing extra is kept separate from every other kind of skip, because only the first is recoverable by installing something |
| Required runtime dependencies | 0 — standard library only; optional extras are exactly that, and a guard test fails if importing any runtime module would pull one in |
Offline demo (make demo) | Full evidence pack from a bundled example, no network |
| Warning catalog | 400+ stable codes, every one documented; the docs page is generated from the catalog and a test fails on drift |
| Generated files | 14 mandatory artifacts on every successful analysis run (analyze/audit; a run that fails early writes no output directory at all, and other commands write no evidence pack), pinned by a count-free regression test and each structurally validated or exact-content checked. The total is INPUT-DEPENDENT: a richer project adds conditional artifacts, plus the deterministic .sbomflow-output ownership marker. For reference, the bundled example writes 25 top-level files; a minimal single-manifest project writes 23 instead. Counting basis is top-level files — a recursive count is one higher, because approved decisions land in an applied-decisions/ subdirectory |
| Clean-environment install | Wheel builds and installs into a fresh virtual environment with no network and no dependencies, and the installed CLI runs the demo |
What the test suite actually covers#
- Parser tests — positive fixtures for every supported manifest, lockfile, and embedded build format, plus negative and malformed fixtures for each.
- Malformed-input behaviour — broken JSON/YAML/CSV/archives must produce a cataloged warning with the exact path, never a crash and never silence.
- Resource-bound tests — oversized archive members, deep nesting, and decompression limits are enforced and surfaced as warnings.
- Memory budgets are enforced, not just measured — a per-push CI lane runs a deterministic synthetic scan and fails on a gross regression against a committed budget: peak process memory (
ru_maxrss) and a per-phase peak that names which phase grew. The thresholds carry large cross-machine headroom, so the lane alarms on a real regression rather than on runner variance. - Identity tests — version epochs, vendor suffixes, prereleases, range specifiers, container digests, and same-name/different-origin components must survive unchanged; a structural-garbage guard is tested against both garbage and legitimate unusual values.
- Determinism tests — repeat runs under a fixed
--as-ofmust be byte-identical, including the evidence-bundle ZIP. Determinism is asserted across machines, not just reruns: an environment matrix re-runs a full analysis under four timezone × locale combinations (UTC through Kiritimati and Anchorage; the C locale through a comma-decimal one) and requires every produced artifact — HTML report, PDF summary,issues.csv, and the JSON surfaces — to have identical SHA-256 digests. Source guards keep it that way by construction: library code never touches the locale, the renderers use no locale-aware number formatting or locale-dependentstrftimedirectives, and they never read the wall clock. - Time-bomb guard — a test fixture pinned to a near-future date passes for months, then the wall clock crosses it and the suite goes red with no code change and no obvious cause. A guard scans every test for future-dated literals and requires each one to be a shared far-future fixture, built relative to the run, or on a reviewed list stating why it cannot expire. The guard is itself time-independent: it compares literals against fixed constants, never against "now", so its own verdict cannot drift either.
- Boundary tests — reviewer status defaults, VEX justification enforcement, self-approval rejection, gate/VEX consistency, and draft watermarks are asserted directly.
- Integrity tests — the audit-log hash chain, artifact hashes, and cross-artifact references are validated, and corrupted or tampered outputs must fail validation.
- Crash-consistency tests — every consequential human-decision write is crash-tested by exhaustively enumerating every interruption point in its transaction protocol (and interruption pairs for concurrent operations), asserting after each schedule that state is old-complete or new-complete, the audit log carries at most one event per operation, and recovery either converges or refuses visibly.
- Stateful property testing (optional dev lane) — beyond the enumerated schedules, a separate
make hypothesis-statefullane drives two model-checked state machines over randomized operation sequences: decision writes, idempotent retries, crashes and recoveries against a real store, and the full waiver lifecycle (create, renew, revoke, expire under a pinned clock, gate evaluation). The lane runs a fixed, derandomized profile so results are reproducible; it needs one optional dev-only package and is never part of the zero-dependencymake testbaseline — a baseline test enforces that separation. - Error-contract tests — every CLI error code renders with a fix and a docs link, and every code has a matching section in the error reference.
- Docs and site checks — every relative documentation link must resolve, and the public build is checked against a fail-closed allowlist so internal material cannot be published.
How CVE and advisory matching is tested#
Matching a component to a CVE is where a scanner earns or loses your trust: a false positive wastes a reviewer's afternoon, and a false negative is the one that matters. Both directions are tested, and both failure modes are handled conservatively rather than guessed.
- Against false positives — a withdrawn advisory (OSV
withdrawn) or a rejected one (NVDRejected) is excluded from active findings and recorded in the scan's provenance so the exclusion stays auditable. A disputed advisory is not dropped: it stays active and is flagged for a human. If an advisory'swithdrawnvalue is itself malformed, the finding stays active and warns (advisory_withdrawn_malformed) — bad data never silently removes a finding. - Against false negatives — pinned known-affected oracles run the full local matching path (package URL → version → advisory affected-range) against a pinned advisory snapshot, and assert that each oracle's affected version matches while its paired fixed version does not.
- Against quiet guessing — when two versions cannot be ordered under a component's ecosystem scheme, or an advisory range cannot be resolved locally, the finding is surfaced for human verification with lowered match confidence. An unresolvable advisory range also carries the
advisory_range_unresolvedwarning. It is never silently asserted as affected, and never silently dropped. - Component identity — a package-URL conformance corpus pins how versions, epochs, vendor suffixes, and qualifiers are parsed, so the coordinate a CVE is matched against does not drift between releases.
A CVE match is an observation, never a decision. Suppressing a finding — not_affected, fixed, or a waiver — comes only from human review, never from the matcher.
Synthetic stress testing (TestBuz)#
Beyond unit tests, SBOMFlow is exercised against TestBuz: a fully synthetic, deterministic corpus of ten distinct connected-device manufacturer archetypes — from a 19-person sensor company to multinational network, maritime, metering, EV-charging, medical, robotics, and industrial estates — with realistic build systems, supplier evidence, release rooms, deliberately malformed files, and planted operational inconsistencies.
Runs against this corpus are checked by an executable acceptance contract (complete workflow coverage across all ten businesses) and a per-fact semantic evaluation that fails on any silent omission, false merge, false positive, missing provenance, or overclaim.
What the acceptance contract does not check. It runs on an interpreter with no optional packages installed at all, so it proves the standard-library-only baseline on every machine — and the surfaces that need an optional package are not exercised there. In particular, JSON-Schema validation of emitted SBOMs against the vendored official CycloneDX and SPDX schemas runs in its own lane (make conformance), which installs the schema validator into a throwaway environment and validates against the pinned official schemas. The acceptance run states this in its own output rather than leaving a green result to be read as wider than it is. The reason is cost, and it is measured: schema-validating the largest corpus outputs takes minutes per output, which does not belong in a per-change gate. That cost is tracked as a defect in its own right, not treated as settled.
AI surface hardening and its measured budgets#
The AI scan runs on the default analyze/audit path, so every hostile shape below reaches it on an ordinary run with no flag. Three adversarial corpus cases cover them, each with the control that keeps a refusal-only suite honest:
| Hostile input | What must happen |
|---|---|
| A SafeTensors header declaring more bytes than the file holds | never produces a bytes-based claim; it may still be inventoried, but identified_by reads extension_only |
A pickle wearing a .onnx name | recorded and hashed as unsupported, deliberately never parsed |
| Empty and truncated model files | inventoried or skipped, never guessed at, never fatal |
| An archive whose compressed size hides a large declared one | never expanded to look inside for models |
| A model symlinked to a target outside the scan root | not followed; nothing behind it reaches any artifact |
| A manifest with an unknown field, a traversal or absolute path, an embedded credential, a malformed digest, or an unknown schema version | each refused with its JSON pointer, and no refusal echoes the offending value |
| Positive control — a well-formed model in the same hostile tree | still identified from its bytes |
| Negative control — a tree with no models at all | still writes the record, still says a scan ran, and still refuses to read as "this product has no AI" |
Resource limits are measured, not assumed — the numbers below come from running the shipped code against a generated tree, not from reading the source:
| Measurement | Observed | Budget |
|---|---|---|
| Peak allocation hashing a 64 MiB artifact | 2.1 MiB | 4 MiB — a whole-file read would show up as a peak near the file size |
| Peak allocation scanning a realistic tree (12 models, 300 other files) | 1.2 MiB | 8 MiB |
| Directory entries walked for that tree | 324 for 312 files | under 2× the file count |
| Models identified in that tree | 12 of 12, walk complete | — |
Memory is measured with tracemalloc, never the process-wide RSS high-water mark, which is contaminated by whatever ran before it. No budget here is a wall-clock assertion: several agents work this repository concurrently, so the performance budget is expressed in work — bytes per read, entries walked — which is a property of the algorithm rather than of the machine's load. Elapsed times are recorded for a human and asserted on by nothing.
A traversal budget that fires is never silent: the walk stops or prunes, names the budget in bounds_hit, and the inventory is marked PARTIAL. A depth bomb prunes its own branch without erasing its siblings, so planting one deep chain cannot turn into an inventory-erasure primitive.
Latest full validation (July 2026)#
The most recent end-to-end validation pass ran every tier of this laboratory and then went further than "the commands exited zero":
- Business-scale advisory controls. One estate's full scan (about 4,900 components) is fed a deterministic offline advisory snapshot using the official ecosystem naming conventions, with positive and negative controls: three advisories that must match through the version-range engine, and one fixed-before-shipped advisory that must not match — so "producing more findings" can never fake a better score. A known-exploited entry must surface first in the issue queue with the exploited flag and the matching triage uplift, while unreviewed findings stay
under_investigation(the reviewer boundary is asserted, not assumed). - Every artifact re-checked from outside. All generated outputs across all ten estates were re-parsed and reconciled independently: product identity agreement, count reconciliation across SBOM/VEX/issue surfaces, every bundle and sharing-pack hash recomputed, no forbidden conformity language, and no leaked secrets or machine-specific paths on shareable surfaces.
- Hostile and offline pressure. A network tripwire fails any accidental connection attempt; mutation self-checks deliberately corrupt outputs and require the oracles to catch each corruption, proving the assertions are not vacuous.
The acceptance contract currently passes all 20 cases and all 22 feature groups. The full offline suite total is stated once, at the top of this page, and guarded there — repeating it here is how it went stale before.
This process found real defects — which is its job. Two matter to users and are fixed with regression coverage in the current release:
- Maven advisory coordinates from real-world advisory data (
group:artifact) could fail to match the equivalent component identity recorded from build files (group/artifact), silently missing advisories. Matching is now separator-consistent, with no-overmatch guards. - Very large recognized lockfiles (over the general text-scan limit) previously produced no components and no explanation. Recognized structured inputs now parse up to a much higher dedicated cap, and anything beyond it is hashed and surfaced with a documented resource warning and a concrete remedy — never silently dropped. See known limitations for the current caps and a measured large-input example.
Be clear about what this is and is not:
- It is synthetic. Every company, product, and file is fictional; sample advisories are labelled non-real.
- It is not real-customer validation, and it is not proof of conformity or universal format compatibility.
- Its purpose is reproducible engineering stress testing — large realistic inputs, hostile file shapes, and honest scoring of what was detected, warned, or left to human review.
How SBOMFlow behaves when things go wrong#
| Situation | Behaviour |
|---|---|
| Malformed input file | Cataloged warning with the exact path; the scan continues |
| Recognized but unsupported evidence | Hashed into the artifact manifest and surfaced with a stable warning — visible, never silent |
| Warnings you cannot accept in CI | --strict[=codes] exits 5 after writing all artifacts, so evidence is never lost to a policy |
| Enforced gate blocks a release | Exit 1 with the exact blocking reason recorded; every artifact is still produced |
| Output directory corrupted or edited | sbomflow validate fails with exit 4 and names the mismatch |
| Audit log tampered | The hash chain breaks; validation fails and new decisions are refused |
| Reviewer tries to approve their own submission | Rejected — separation of duties is enforced |
| Human VEX decision | Applies only with a valid justification and only to its scope; invalid not_affected is downgraded and stays visible |
| Symlink escaping the scan root | Refused with a warning — outside content is never read into evidence |
| Optional tool or extra missing | Explicit "skipped" note; baseline operation continues. In the test lanes the skip is also counted and attributed to the extra that would release it, so a passing run cannot imply it exercised a surface it never reached |
Telling whether an audit was changed#
SBOMFlow is built to answer "was this audit changed after the fact?" honestly. Every audit is tamper-evident: the audit log is an append-only hash chain and each artifact carries a SHA-256, so if the recorded evidence was changed, sbomflow validate fails (exit 4) and new reviewer decisions are refused. To see what changed between two audits of successive releases, compare their release records — drift lists the components, findings, and gaps that changed.
Verify it yourself#
All of these run offline from a source checkout:
make test # full offline suite
make demo # end-to-end offline evidence pack
make wave4-check # importer/integration gauntlet
make validate-artifacts # structural validation of generated outputs
make check-docs-links # every relative docs link resolves
make check-docs-publication # fail-closed public-content leak check
make install-check # clean-venv wheel install + installed CLI runThe TestBuz acceptance contract and per-fact semantic evaluation run offline from a source checkout as part of the same trust gauntlet, writing their results to a fresh output directory you choose.
Optional developer lanes sit beside the baseline without gating it: make hypothesis-stateful runs the stateful property-testing machines described above (it installs one dev-only package into an isolated virtual environment on first use; every later run is offline).
What we do not claim#
No test suite proves the absence of defects, and a synthetic corpus cannot guarantee every future customer input. SBOMFlow does not claim perfect accuracy, universal format support, or legal conformity — and any claim on this site that cannot be traced to running code and tests is a bug. Real, sanitised customer artifacts remain the standard we validate against next.
Next: capabilities · security & privacy · what's new