Core concepts
Deterministic evidence engine#
With input artifacts and run context (including --as-of) pinned, SBOMFlow produces stable identifiers, content hashes, and byte-reproducible release artifacts. Opt-in operational phase durations are excluded from determinism checks. This is what makes an evidence record re-checkable and diffable.
Timestamps are absolute instants, normalized to UTC. Every date-sensitive comparison — waiver expiry, source-staleness windows, and --as-of math — treats each timestamp as an absolute instant: a trailing Z or an explicit offset is honored, and a naive timestamp (no zone) is assumed to be UTC. Comparisons are timezone-aware, so the host machine's local timezone never changes an outcome — a pinned --as-of run is byte-identical whether it runs in UTC or in UTC+14 (proven by the offline clock/timezone adversarial test suite).
--as-of has one canonical form. The flag accepts ISO-8601 with a trailing Z, any explicit offset, or a bare date (2026-07-10, read as 00:00:00Z); a timestamp without a timezone is rejected as ambiguous (stable error E006) before anything is written. Whatever accepted form you supply, artifacts record the one canonical UTC form (YYYY-MM-DDTHH:MM:SS+00:00), so …Z, …+00:00, and offset-equivalent inputs produce byte-identical artifacts — determinism never depends on timestamp spelling.
Values you type are held to that one form; values already recorded are read as written. --as-of rejects the ISO-8601 end-of-day spelling T24:00:00 and asks for the next day's 00:00:00 instead, so there is a single spelling per instant on the command line. That refusal covers every way of writing hour 24 — the abbreviated T24, T2400 and T240000 included, and each of them behind a basic-format YYYYMMDD date as well — and it covers a fraction with no seconds field (T12.5, T12:00.5), for the reason given three paragraphs down. The same rule reaches the flags that share this parser: --since, --received-at, --issued-at and the Article 14 incident anchors, each naming itself in the error rather than the flag you did not type. Timestamps SBOMFlow only reads — a support_period_ends_at in your config, a date inside a snapshot or a stored release record — are not yours to re-type, so the end-of-day spelling is accepted there and read as next-day midnight. Both halves are the same guarantee from opposite sides: one instant has one meaning, on every supported Python version. The value you recorded is still played back verbatim in the artifact, so what SBOMFlow reports always matches what you wrote.
Accepting the end-of-day spelling is not repairing a broken one. Only a true end-of-day is read that way: T24:00, T24:00:00, and a fractional part that is zero, each with an optional offset. Every other way of spelling hour 24 reads as unreadable. That covers a non-zero minute, second or fraction — T24:00:59, T24:00:00.500, T24:30:00 — and equally the abbreviated forms T24, T2400 and T240000. The abbreviated forms are listed explicitly because the newest supported Python parses them on its own: SBOMFlow refuses them anyway, so one recorded value cannot mean "end of day" on one supported version and "unreadable" on another. Unreadable, for a time-boxed exception, means the exception does not apply: a waiver whose expires_at cannot be read is reported as invalid_expiry and suppresses nothing. The recorded intent was to time-box, and the one thing certain about an unreadable bound is that it is unknown. Fix the recorded value; it is never guessed.
A fractional part belongs on the seconds. …T00:00:00.5Z — a fraction on the seconds field — is read identically on every Python version SBOMFlow supports. A fraction hung on the hour (…T00.5Z) or on the minute (…T00:00.5Z) is read by some supported versions and refused by others, so a value recorded that way would mean one thing on your machine and something else on a colleague's. Rather than pick a winner and invent a reading, SBOMFlow refuses that spelling everywhere and reports it as unreadable, with the same consequence as above. The end-of-day spelling follows the same rule: T24:00:00.0 is a true end of day and is read, while T24:00.0 hangs its fraction on the minute and is not. Write the seconds out.
Not every recorded timestamp is a release-decision timestamp, and the end-of-day spelling is only read where a day-end reading means something. The paragraphs above describe the values the release decision turns on — a waiver or approval expiry, a support-period end, a stored release record, a snapshot date. Other timestamps SBOMFlow reads belong to their own contracts and are point-in- time instants, where "end of day" is not a meaning they have: an entitlement's issued_at and not_before, a signer's validity window in a trust policy, a claimed_at in the review queue, a reviewer's reviewed_at, the expires in TUF or Uptane update metadata, a declared release date in a backfill manifest, the incident instants an Article 14 draft reckons from, and the timestamp stamped on a decision you record. For those, every spelling of hour 24 reads as unreadable — T24:00:00 included — as does a fraction with no seconds field.
The guarantee is the same one, stated for a different set of fields: one recorded value has one meaning on every supported Python version. What differs is which meaning is available. Where a day-end reading is part of the contract, a true end-of-day is read as next-day midnight; where it is not, the spelling is refused rather than given a reading its field was never defined to carry. In both cases the consequence of unreadable is the one described above — the value is reported back to you exactly as you wrote it, nothing is guessed, and a bound nobody can read grants nothing. Write the hour as 00:00:00 of the next day and it is read everywhere, by every one of these.
What --as-of pins#
This is the authoritative statement of the contract; everywhere else links here.
--as-of pins the evidence instant: the "now" that every date-sensitive comparison in the run is measured from — waiver expiry, advisory-source staleness, freshness horizons, support-period end, and the generated_at recorded in the artifacts. Pinning it is what makes a re-run byte-reproducible.
It is not a knowledge horizon. SBOMFlow does not reconstruct what was known at that instant, and it hides nothing for being newer than the pin. The advisory data evaluated is exactly the snapshot you supplied — a local snapshot file, a vulndb store, or an opt-in online source — evaluated in full. Two halves, and they are independent:
- The inputs are yours. SBOMFlow evaluates the data you supplied, whenever you fetched it. It cannot obtain the advisory data as it stood at some past instant; only a snapshot taken then can do that.
- The clock is pinned. Every day-count, expiry, and staleness window is measured from the instant you named, never from the wall clock.
The consequence is the part operators do not expect, so it is stated plainly: a current snapshot evaluated under an old --as-of produces facts that post-date the pinned instant. An advisory published after the instant still becomes a finding. That is correct — it is what re-checking a frozen release against newer advisory data means — but it is not "what we knew then", and no artifact claims it is.
One case would otherwise leave you comparing two timestamps by hand: an advisory withdrawn after the pinned instant. It was live at the instant the evidence describes, and it is still excluded from active findings (a withdrawn advisory shown as active is a false positive). The run says so with the advisory_withdrawn_after_as_of warning — the historical record moved under a pin that describes an earlier instant. Visibility, not a changed decision.
If you need the advisory data to match the instant, pin the snapshot, not the clock: keep the snapshot you shipped with, and re-supply it.
Product and release identity#
Evidence is anchored to a product, a declared product class, and a specific release/version. Identity is what lets decisions and drift be compared release to release.
Provenance and hashing#
Every input file is hashed (SHA-256). Every release decision traces back to the exact bytes it was made from. Provenance is always recorded.
Warnings vs errors#
- Errors stop a command and return a stable exit code and error code with a fix.
- Warnings never silently drop input. Malformed or unsupported inputs surface a warning (in
scan-warnings.json) rather than being ignored. Underanalyze --strictwarnings can be promoted to a distinct exit code.
Release gate and policies#
A release gate evaluates manufacturer-selected policy (--fail-on-* flags or a named policy profile). With no policy, the gate is informational (exit 0). The gate is an engineering control, not a compliance decision.
Policy packs#
Each shipped preset carries a pack: the recorded purpose and scope of the policy, and for every rule, why it exists, what evidence it needs to be evaluable at all, what informed it, and how seriously the author intends a hit to be taken. sbomflow policy show <preset> prints it, and every version carries a history saying what changed and what to expect when migrating.
A pack documents a policy — it never is one. There is exactly one place a rule is turned on, and the pack is not it. Three things follow, and each is checked mechanically every time a pack loads:
- A rule the engine does not have is refused. A pack naming a rule that does not exist would promise a protection nobody is providing, and it would survive review precisely because it reads well.
- A pack that misdescribes its own policy is refused. A rule marked enforced whose policy leaves it off — or the reverse — is a contradiction between the document and the thing it documents. Reviewers read the pack, not the JSON underneath.
- A pack that claims conformity is refused. Not by matching two forbidden phrases but by shape: a regulation or standard token next to a conformity, certification or compliance token. A pack named "CRA certified" would turn a set of gate knobs into a legal claim on the strength of its filename.
The reverse asymmetry is deliberate. A pack that omits a rule its policy enforces is incomplete documentation of something real — the gate still blocks, nobody is misled about a protection they lack — so it is reported rather than refused, and sbomflow policy show names the undocumented rules.
Source references are references, never coverage. A rule may record the regulation article or standard clause that informed it. That records where the engineering intent came from and asserts nothing about satisfying what it cites — a rule citing an article is not a rule that discharges it.
Every rule is a real policy field, so every rule can be overridden by your own policy file. A preset is a starting point somebody documented, not a fixed setting.
Inspecting and comparing policies#
sbomflow policy show lists every setting a policy enforces, including the ones left off. A knob that is off and a knob that does not exist in your version call for different actions, so they must not look alike on screen.
sbomflow policy diff compares that same complete list, which is what makes it total and symmetric: diff(a, b) is diff(b, a) with the two sides swapped, and a setting that differs is reported whichever file you name first.
The content hash and the settings list answer different questions. The hash identifies the policy a gate result was produced under, so gate-verify can tell whether the recorded policy is the one you are holding. It deliberately covers only the settings that can change a gate decision, and it deliberately does not change when you leave an additive knob at its default — otherwise every policy ever recorded would rehash and honest historical records would read as tampered. So two policies can differ in the list while hashing the same. When they do, the list is the one to read: it is the complete answer, and the hash is not trying to be one.
Evidence lifecycle and decision memory#
Reviewer decisions, VEX justifications, and prior determinations are recorded and can be surfaced on later releases as context. Release drift compares engineering artifacts across releases. Both are context, never an automatic decision.
The reviewer boundary#
Read Observed vs reviewed — the single most important concept in SBOMFlow.