sentinel-sbom: the SBOM as a deterministic witness, not a post-hoc scan
This is a lab notebook entry, not a marketing brief. Format is claim → why it matters → architecture → evidence → contract → honest limits → roadmap, every claim graded against the controlled evidence vocabulary, every assertion footnoted to the repo's README or CHANGELOG. The substrate under examination is [sentinel-sbom][repo] — a single Zig binary that reads a Nix flake.lock and emits a deterministic SPDX 2.3 document, including an in-tree NAR encoder that reproduces nix path-info's narHash without shelling out to nix for the hashing step.
[repo]: https://github.com/SMC17/sentinel-sbom
1. The claim
sentinel-sbom takes a Nix flake.lock in and emits a byte-deterministic SPDX 2.3 document out (either tag-value or JSON-LD), with optional narHash verification against the local Nix store. The headline properties, in the README's own words12:
- Single Zig binary, ~4,250 lines across five
.zigfiles, zero
third-party Zig dependencies.
- Byte-deterministic output: identical
flake.lock→ identical bytes
across runs, asserted by unit test and by a CI sha256sum check.
- In-tree NAR encoder + SHA-256 that reproduces
nix path-info's
base32-SRI narHash byte-for-byte on a regular-file / directory golden corpus.
- 62 / 62 tests pass on Zig 0.16.0 via
zig build testat the v1.0.2
tag2.
<!— runnable-claim: sbom-determinism —>
Evidence grade: unit-tested. The repo's own README is explicit about the scope of that grade — "62 passing tests and end-to-end runs against five flake.locks on one workstation, and that is the entire evidence base"3. This entry repeats that framing honestly; the evidence is real but narrow.
2. Why this matters — the witness, not the scan
Most SBOM tooling answers a different question than the one the SBOM should answer. The typical pipeline is: build the artifact, then scan the binary or the installed file tree, then infer a parts list from what the scanner finds. That answer is downstream of the build — it sees what landed on disk, not what the build promised to install. A scanner that misses an artifact misses it forever; a scanner that flags a phantom is hard to refute without re-running the build.
A Nix flake.lock is structurally different. It is a content-addressed parts list written at evaluation time, pinned to a git rev and a narHash for every input, and signed-into-place by the flake-lock machinery before any build runs. Translating that lock file into SPDX is mostly a serialization problem, not an inference problem4.
This is the engineering move sentinel-sbom makes: treat the lock file as a witness that already exists, and emit SPDX deterministically from it. The tool does not parse a build graph at compile time; that phrasing belonged to a hypothetical version that doesn't exist in the repo. The tool does read the lock file that Nix produces at evaluation time, which is the closest analogue to a compile-time SBOM the Nix ecosystem offers — the lock is content-addressed and pinned before the build store paths are even resolved.
The narrow, accurate framing from the README5:
If you're already using Syft and it works, keep using Syft. If you
want a tiny binary that does one thing well and lives next to your
Nix flake, this is for you.
Syft and sentinel-sbom are not direct competitors. Syft is a broad multi-source SBOM emitter; sentinel-sbom does one shape (flake.lock → SPDX 2.3) very precisely and ships byte-determinism + offline narHash verification as the differentiators.
3. The architecture — five files, one binary
The full source surface, with line counts as cited in the README6:
| File | Role | | ---------------------------— | -------------------------------------------------------— | | src/main.zig | CLI dispatch, flake.lock parser, SBOM emitter, license resolver | | src/nar.zig | In-tree NAR encoder + SHA-256 + base32-SRI encoding (~720 LOC) | | src/spdx.zig | SPDX 2.3 license-expression grammar + canonical-list validation | | src/spdx_license_list.zig | Vendored snapshot of spdx/license-list-data@v3.24.0 (628 active + 31 deprecated + 70 exception IDs) | | src/license_scan.zig | LICENSE-file template matcher (22 canonical SPDX templates, ~750 LOC) |
The CLI surface, frozen on the v1.x line7:
sentinel-sbom emit <flake.lock> # SPDX 2.3 tag-value
sentinel-sbom emit --format=json <flake.lock> # SPDX 2.3 JSON-LD
sentinel-sbom emit --scan <flake-dir> <flake.lock> # LICENSE-file fallback
sentinel-sbom verify <flake.lock> # rev-substring scan in /nix/store
sentinel-sbom verify --strict <flake-dir> # real narHash compare via `nix path-info`
sentinel-sbom verify --strict --in-tree <flake-dir> # same compare, no shell-out for hashing
sentinel-sbom scan-license <dir> # standalone LICENSE → SPDX ID
The two surfaces that do the load-bearing work are nar.zig (the in-tree hash path) and license_scan.zig (the LICENSE-file fallback). Everything else is glue and serialization.
3.1 The NAR encoder
src/nar.zig implements the canonical NAR format from Eelco Dolstra's PhD thesis §5.2 — nix-archive-1 magic header, sorted directory entries, 8-byte aligned little-endian length prefixes, regular / executable / symlink type tags — and hashes the resulting byte stream with std.crypto.hash.sha2.Sha256, then base32-SRI-encodes the digest to produce the sha256-... strings that nix path-info emits in its narHash field8.
The point of doing this in-tree is to remove nix path-info from the hashing path. The shell-out version (verify --strict) still works; the in-tree version (verify --strict --in-tree) verifies the same byte equality without depending on a working nix binary for the hash. Store-path discovery — finding the cached path inside /nix/store for a given lock entry — still calls nix flake archive, so full air-gapped operation is not yet done9.
3.2 The LICENSE-file fallback
src/license_scan.zig is the disambiguation surface that fires when a lock entry has neither an SPDX expression nor a row in the built-in license table. The match pipeline10:
- Walk the fetched store path (root + one level deep,
case-insensitive) looking for LICENSE / LICENCE / COPYING.
- Normalize the content: lowercase, strip non-alphanumerics,
single-space-joined.
- SHA-256 the normalized text and try a fast-path exact match against
22 baked-in canonical templates.
- If the exact match misses, fall back to Jaccard similarity ≥ 0.85
over the same templates.
- Disambiguate family-overlap pairs (GPL ↔ LGPL ↔ AGPL,
BSD-2 ↔ BSD-3) by distinguishing-clause text — the linking exception for LGPL, the network-use clause for AGPL.
The 22 baked templates are: MIT, Apache-2.0 / 1.1, BSD-2-Clause, BSD-3-Clause, GPL-2.0 / GPL-3.0 in -only and -or-later shapes, LGPL-2.1 / LGPL-3.0 in -only and -or-later shapes, AGPL-3.0 in -only and -or-later shapes, ISC, MPL-2.0, CC0-1.0, Unlicense, WTFPL, Zlib, and BSL-1.010.
4. Evidence — what the tests actually cover
The 62 in-source tests fall into seven groups, as enumerated by the README's evidence block2:
| Group | What it pins | | ----------------------------------------------— | ---------------------------------------------------------------------------— | | SRI ↔ hex round-trip | Base32-SRI encoder reproduces the canonical sha256-... shape | | Byte-determinism (tag-value) | Two consecutive buildSbomTag calls on the same lock produce identical bytes | | Byte-determinism (JSON-LD) | Two consecutive buildSbomJson calls produce identical bytes | | SPDX compound-expression parsing | AND / OR / WITH / parens / LicenseRef-* / + round-trip | | NAR encoder vs nix-store --dump (golden corpus)| Empty file, single byte, embedded-zero blob, executable, two-file dir, 3-deep nested tree, symlink | | Allocator-independence of the NAR encoder | Same byte stream regardless of which allocator the test threads in | | LICENSE-template disambiguation | BSD-2 / BSD-3 separation, GPL / LGPL / AGPL family overlap | | SPDX 3.24.0 canonical-list validation | Every license ID + exception ID checked against vendored snapshot |
Plus two layers that sit above the in-source suite:
- Integration-tested (one workstation):
emitagainst five real
flake.locks (sovereign-edge, <private-substrate>, <internal-lab>/nix-config, symbols, symbols-wt/pr-i-adversarial) produces valid SPDX 2.3 documents that are byte-identical across two consecutive runs, checked via sha256sum2.
- NAR encoder audit, one corpus: the in-tree NAR encoder
reproduces nix path-info's base32-SRI narHash on a ~200 MB nixpkgs source tree. The README is explicit that this was tested on regular-file and directory inputs; symlink and executable-bit NAR paths are exercised by unit tests but have not yet been re-validated against nix-store --dump on a real symlink-heavy tree23.
The Unreleased CHANGELOG section adds three things that are not yet tagged but live in the working tree15:
- Documentation tests (
tools/doctest.sh, wired as `zig build
doctest). Runs the installed binary against a vendored tests/fixtures/flake.lock, asserts the documented SPDX header prefix, the Tool: sentinel-sbom-<version> line tracking build.zig.zon's .version, the determinism property (two runs sha256-equal) for both tag-value and JSON-LD outputs, and that —help enumerates emit / verify / scan-license`. 15 / 15 checks pass on first run; no README / binary drift found.
- Randomized determinism property tests — 1 000 trials of random
flake.lock shapes (1 – 6 inputs, randomized name / owner / rev / lastModified) emit byte-identically across two consecutive buildSbomTag and buildSbomJson calls; plus 500 trials of Fisher-Yates-shuffled input ordering producing byte-identical SBOM output in both formats. Deterministic seeds (0xDE75_2026_0514_B0B0, 0xD157_2026_0514_AAAA) so a failure is reproducible.
readStdinAllboundary-test refactor — three new tests pin the
exact-equality boundary of the 16 MiB stdin-size guard: max-byte succeeds, max + 1-byte returns StreamTooLong, empty stream returns an empty slice.
These bring the in-source test count from 62 → 67 in the Unreleased branch, and the doctest harness adds 15 additional README-binary consistency checks that run outside the in-source count15.
The mutation-testing run from the same CHANGELOG section deserves a note. tools/mutation-test.sh applied 9 hand-picked operators across src/spdx.zig and src/main.zig; the result was 6 effective mutations killed, 1 equivalent mutant, 1 narrow gap (the stdin size-limit > → >= mutant on readStdinAll, which the boundary refactor above was written to close). The CHANGELOG flags this as the first mutation-testing pass and is explicit that "the 9 stylized operators are far from exhaustive"15. The 6 / 8 ratio is a useful baseline, not a guarantee.
<!— runnable-claim: sbom-mutation-testing —>
5. The deterministic-output contract
The single property that distinguishes sentinel-sbom from "a tool that emits an SBOM" is byte-determinism. The CI assertion is the single-line evidence the README ships verbatim11:
$ sentinel-sbom emit flake.lock | sha256sum
554eee32cb295b77b3a437de130e4300dae9f849cf1e7894a665189c67aed4e9 -
$ sentinel-sbom emit flake.lock | sha256sum
554eee32cb295b77b3a437de130e4300dae9f849cf1e7894a665189c67aed4e9 -
The mechanism, from src/main.zig's buildSbomTag and buildSbomJson12:
- Fixed node order. All packages are emitted in alphabetical order
by name. The lock-file insertion order is discarded.
- Fixed JSON key order. The JSON-LD emitter writes keys in a
pinned sequence (name, SPDXID, versionInfo, downloadLocation, filesAnalyzed, licenseConcluded, licenseDeclared, comment, checksums). No reflective walk.
- Timestamp derived from lock content. The
Createdfield is the
maximum lastModified across all lock entries — a function of the input, not the wall clock. Two runs of the same lock on different days produce the same timestamp.
- Asserted in both unit tests and CI. The two-call equality is a
unit test; the cross-run sha256sum equality is a CI check that fails the build on mismatch.
Why this is a contract rather than a coincidence: a single piece of SBOM machinery that emits identical bytes on identical inputs is the substrate primitive needed for hash-equality compliance use cases (CI gating, reproducibility attestation, build-twice-and-compare). A tool that emits equivalent SBOMs but with cosmetic differences (whitespace, key order, timestamp) cannot serve that role.
The CHANGELOG's Fisher-Yates randomized determinism test — 500 trials of input-ordering permutations producing byte-identical output — guards exactly the failure mode where the emit path silently leaks input insertion order via a hidden allocator-ordering bug. A failure on that test would break the hash-equality use case, and the test is seeded so a regression is reproducible15.
6. Honest limitations
Six things this evidence package does not cover. Most are lifted directly from the README's "Honest risks — Type I and Type II" section, because the README's framing is already calibrated and adding to it would over-claim3:
- Single-workstation evidence base. 62 passing tests + end-to-end
runs against five flake.locks on one workstation. No third-party adoption, no production deployment of the tool itself, no independent audit.
- **"Byte-correct against
nix path-info" refers to one ~200 MB
nixpkgs source tree.** Symlink and executable-bit NAR paths are exercised by unit tests but not yet re-validated against nix-store --dump on a real symlink-heavy tree.
- "No third-party Zig deps" applies to compile time, not runtime.
At runtime, verify --strict shells out to nix path-info and verify --strict --in-tree still calls nix flake archive for store-path discovery. Fully air-gapped operation is not yet done.
- 22 LICENSE templates, not full SPDX coverage. Adding more is a
generator-only change (drop the canonical .txt into data/spdx-licenses/, run zig build gen-spdx-list). Until that happens, a fork shipping only a GPL preamble or a truncated COPYING file can match the wrong family member.
- No CycloneDX yet. SPDX 2.3 only (tag-value + JSON-LD).
CycloneDX is named in the roadmap13; it does not yet ship.
- Production-runtime scope is Linux-only. The NAR encoder
(src/nar.zig) and the LICENSE scanner (src/license_scan.zig) call Linux-specific syscalls directly (statx, getdents64, raw open / read / write / close, getrandom, clock_gettime) chosen for tight performance control over the NAR-hash hot path. Non-Linux CI runners build and run non-filesystem tests cleanly via a SkipZigTest gate; they do not exercise the NAR encoder. macOS / *BSD parity is tracked in STATUS.md as a future port2.
The README's Type-II honest call-out is worth reading in full3:
SBOM correctness for supply-chain decisions is downstream of the
lock file's correctness: this tool faithfully serializes whatever
flake.locksays, including mis-licensed forks, replaced inputs, orstale narHashes that match the lock but no longer match upstream.
This is the right framing. A deterministic SBOM emitter is a truthful witness to the lock; it is not a truthful witness to the upstream projects the lock points at. Consumers that auto-act on licenseConcluded (e.g. CI gating on license-family policy) should treat anything other than an SPDX expression originating in the lock entry itself as advisory.
7. The version contract
The v1.0.x line locks the CLI surface and the wire format. Breaking changes (renamed subcommands, removed fields in the emitted SPDX, incompatible CLI flag shape) move to v2.x. Patch and minor releases within v1.x refine without breaking716.
The v1.0 → v1.0.1 → v1.0.2 progression in the CHANGELOG illustrates what "honest 1.0" means in practice161718:
- v1.0.0 (2026-05-13) — Production-grade hygiene milestone:
SECURITY.md present (coordinated disclosure), LICENSE / README / CONTRIBUTING / CI workflow verified, the v1.x stability promise written down. No new features.
- v1.0.1 (2026-05-13) — Patch: extends the
license-template-collision guard with the known-overlapping GPL / LGPL / AGPL pairs, which share ~93% textual overlap and were failing a local template-collision unit test before the fix.
- v1.0.2 (2026-05-14) — Honest-1.0 alignment pass. No code changes;
build.zig.zon .version bumped from 0.6.0 to 1.0.2 so that sentinel-sbom --version and the SBOM's Creator: Tool: field match the actual tag line. README LOC count corrected (~1,446 → ~4,250 — the v0.6 cycle added license_scan.zig and spdx_license_list.zig without bumping the README number).
The pattern is the same one [zig-h3][zigh3] uses: a 1.0 means "the existing surface stands and breaking it bumps to 2.x," not "feature- complete." The Unreleased section already lists the next moves honestly: CycloneDX output, reproducibility attestation, more LICENSE templates, recursive walking of nested flake inputs, fuzz harness, Darwin / aarch64 CI13.
[zigh3]: ../zig-h3-pure-zig-vs-libh3/
8. Reproducibility
The full reproduction recipe, lifted from the README14:
git clone https://github.com/SMC17/sentinel-sbom
cd sentinel-sbom
zig build # produces zig-out/bin/sentinel-sbom
zig build test # 62 / 62 tests on Zig 0.16.0
# Determinism property — same flake.lock → identical bytes:
./zig-out/bin/sentinel-sbom emit /path/to/flake.lock | sha256sum
./zig-out/bin/sentinel-sbom emit /path/to/flake.lock | sha256sum
# (same hash both runs)
# Strict-mode narHash verification without `nix path-info` shell-out:
./zig-out/bin/sentinel-sbom verify --strict --in-tree /path/to/flake-dir
Or via Nix directly:
nix run github:SMC17/sentinel-sbom -- emit /path/to/flake.lock
The documentation-test harness (zig build doctest, in the Unreleased section15) is the closest substrate to a runnable claim for the README itself — it asserts that every shape the README documents (header prefix, tool-version field, determinism property, --help subcommand list, --version equality with build.zig.zon) still holds at HEAD. A future README change that drifts from the binary fails zig build doctest.
9. Status footer
- Evidence grade:
unit-tested. 62 / 62 tests pass on Zig 0.16.0
at v1.0.22. The "byte-correct against nix path-info" claim is a single-corpus audit on one workstation, not a multi-source differential test; an upgrade to differential-tested is gated on running the NAR encoder against nix-store --dump over a symlink-heavy tree and over additional upstream nixpkgs revisions3.
- Reproducible:
true. The commands in §8 are the canonical
reproduction recipe.
- Last verified: 2026-05-18, on the maintainer's workstation at
v1.0.2. The Unreleased CHANGELOG section (doctest, randomized determinism property tests, readStdinAll boundary tests, mutation testing pass) lives in the working tree and brings in-source test count to 67 plus 15 doctest checks; the next tag will carry that evidence.
- Open gaps: CycloneDX output; fully offline operation (replace
nix flake archive for store-path discovery); Darwin / aarch64 CI; fuzz harness; recursive walking of nested flake inputs; symlink-heavy NAR re-validation; standing mutation-testing harness beyond the 9-operator hand-picked baseline. All filed honestly in the README's roadmap and the Unreleased CHANGELOG section1315.
10. Cross-references
- Repository:
SMC17/sentinel-sbom - License: AGPL-3.0-or-later (integrators-wrap-the-upstream
posture; see LICENSE)
- Companion repos in the Sovereign Stack:
sovereign-edge, sovereign-offense-harness.
- Portfolio design system: this lab entry conforms to the evidence
vocabulary defined in ~/codex/methods/stax-dev-portfolio-design-system.md, the canonical contract for the /lab surface.
- External standards: SPDX 2.3 license-expression grammar (the
spec the parser implements)19; NTIA Minimum Elements for an SBOM (the U.S. policy baseline that motivates SBOM-as-witness framing)20; Eelco Dolstra's PhD thesis §5.2 (the canonical NAR-format source the encoder implements)21.
Footnotes
README.mdlines 13–29 — "What it is" section. The single-paragraph framing ("sentinel-sbomreads a Nix flake'sflake.lockand emits an SPDX 2.3 document — tag-value or JSON-LD — describing every locked input as a package with its git rev, narHash, download URL, and a license identifier resolved from one of three sources") is the canonical claim shape the v1.x surface freezes around. ↩README.mdlines 30–74 — "Evidence" section. Enumerates: 62 / 62 unit tests on Zig 0.16.0; integration-tested against five flake.locks on one workstation withsha256sumdeterminism check; NAR-encoder audit againstnix path-infoon a ~200 MB nixpkgs source tree (regular-file + directory inputs); multi-platform CI on Linux x86_64 / aarch64 + macOS arm64 withSkipZigTestgates on non-Linux for filesystem-touching paths; "no published encode/verify throughput numbers." The Linux-only production-runtime scope (NAR encoder + LICENSE scanner callstatx,getdents64, rawopen/read/write/close,getrandom,clock_gettimedirectly) is documented at lines 60–74 with theSTATUS.mdfollow-up tracked for the future port tostd.Io.Dir. ↩README.mdlines 356–379 — "Honest risks — Type I and Type II" section. Type-I framing: "'1.0' is an API-stability commitment, not a maturity certificate." Type-II framing: "SBOM correctness for supply-chain decisions is downstream of the lock file's correctness: this tool faithfully serializes whateverflake.locksays, including mis-licensed forks, replaced inputs, or stale narHashes that match the lock but no longer match upstream." ↩README.mdlines 81–104 — "Why" section. "Every Nixflake.lockis already a content-addressed bill of materials — each input has a git rev, a narHash, an upstream URL. Translating that to SPDX is mostly a serialization problem." ↩README.mdlines 151–177 — "Honest comparison" table comparing against Syft (Anchore), Grype, Snyk. Picks: "When to picksentinel-sbom" / "When to pick Syft" / "When to pick Snyk" are each one paragraph long with explicit trade-off framing. ↩README.mdlines 91–94 — "~4,250 LOC acrosssrc/main.zig,nar.zig,spdx.zig,license_scan.zig, andspdx_license_list.zig."src/nar.zigis "~720 LOC" per line 182;src/license_scan.zigis "~750 LOC, 22 canonical SPDX templates baked in" per line 191. ↩README.mdlines 22–28 — frozen v1.x CLI surface. The same enumeration appears in the "Usage" section at lines 255–265 with literal invocations. ↩README.mdlines 181–190 — "In-tree NAR encoder + sha256" detail. The encoder targets the canonical NAR format from Eelco Dolstra's PhD thesis §5.2, withnix-archive-1magic, sorted directory entries, 8-byte-aligned little-endian length prefixes, and regular / executable / symlink type tags. Output is base32-SRI-encoded to matchnix path-info'snarHashfield. Used byverify --strict --in-treeto remove thenix path-infoshell-out from the hashing path; store-path discovery still usesnix flake archive. ↩README.mdlines 224–240 — "What is not yet done" list under "Details — what each piece does and how it's checked". Names: no fuzz suite (property tests exist for NAR encoder + license parser, no AFL-style fuzz harness); no published deployments; 22 LICENSE templates, not full SPDX coverage; family disambiguation depends on full LICENSE text; air-gapped operation still callsnix flake archivefor store-path discovery. ↩README.mdlines 191–202 — "LICENSE-file scanner" detail. The 22 templates are: MIT, Apache-2.0 / 1.1, BSD-2-Clause, BSD-3-Clause, GPL-2.0-{only,or-later}, GPL-3.0-{only,or-later}, LGPL-2.1-{only,or-later}, LGPL-3.0-{only,or-later}, AGPL-3.0-{only,or-later}, ISC, MPL-2.0, CC0-1.0, Unlicense, WTFPL, Zlib, BSL-1.0. Family-overlap pairs (GPL ↔ LGPL ↔ AGPL, BSD-2 ↔ BSD-3) disambiguated by distinguishing-clause text. ↩README.mdlines 107–149 — "Demo" section. Includes the byte-deterministic property verbatim: sameflake.lock→ identical bytes across runs, demonstrated by runningsentinel-sbom emit flake.lock | sha256sumtwice and showing both runs produce554eee32cb295b77b3a437de130e4300dae9f849cf1e7894a665189c67aed4e9. ↩README.mdlines 218–223 — "Determinism" detail under "Details — what each piece does and how it's checked". Fixed node order (alphabetical), fixed JSON key order, timestamps derived from the lock's maxlastModified. Asserted by unit tests on both formats; CI runssha256sumtwice and fails on mismatch. ↩README.mdlines 292–303 — "Roadmap" section. Shipped on the 1.0 line: in-tree NAR encoder + sha256,--strict --in-treehashing mode, LICENSE-file scanner (22 templates), full SPDX 3.24.0 canonical-list ID validation, family-overlap disambiguation. Next: CycloneDX output (--format=cyclonedx); reproducibility attestation (build twice, hash-compare); expand the LICENSE-template bank past 22 entries; recursive walking of nested flake inputs. Later: fully offline operation (replacenix flake archivefor store-path discovery); fuzz harness gated in CI; Darwin / aarch64 CI. ↩README.mdlines 242–251 — "Install" section.git clone && zig buildproduces./zig-out/bin/sentinel-sbom; Nix alternative isnix run github:SMC17/sentinel-sbom -- emit /path/to/flake.lock. ↩CHANGELOG.md"## Unreleased" section. Documentation tests (tools/doctest.sh, wired aszig build doctest, 15 / 15 checks pass on first run, no README / binary drift found). Randomized determinism property tests: 1 000 trials of random flake.lock shapes + 500 trials of Fisher-Yates-shuffled input ordering, both seeded for reproducibility.readStdinAllboundary-test refactor pinning exact-max/max+1/ empty-stream behaviour. Mutation testing harness (tools/mutation-test.sh, 9 operators acrosssrc/spdx.zig+src/main.zig): 6 effective mutations killed, 1 equivalent mutant (rev-needle length boundary>= 8→> 8, whererev.len == 8produces identical output), 1 narrow gap (stream-length>→>=at the 16 MiB stdin guard, closed by thereadStdinAllrefactor). Total in-source tests: 62 → 67. ↩CHANGELOG.md"## v1.0.0 — 2026-05-13" section. Hygiene milestone: SECURITY.md present (coordinated disclosure policy), LICENSE / README / CONTRIBUTING / CI workflow verified, v1.x cycle = surface stable + breaking changes bump to v2.x. Engineering posture: rough-but-versioned convention adapted for OSS — v1.0 means the existing surface stands; v1.x refines without breaking. ↩CHANGELOG.md"## v1.0.1 — 2026-05-13" section. Patch release.license_scan.zigsameCanonicalText()extended with the known-overlapping license-family pairs (GPL ↔ LGPL with ~93% textual overlap, GPL ↔ AGPL, LGPL ↔ AGPL, all -only / -or-later cross-pairs within those families). Same disambiguation pattern as the existing BSD-2 / BSD-3 and GPL-only / -or-later handling. Resolves a local template-collision unit test that was failing withGPL-3.0-only vs LGPL-3.0-only = 0.93Jaccard score. ↩CHANGELOG.md"## v1.0.2 — 2026-05-14" section. Honest-1.0 alignment pass; no code changes.build.zig.zon.version0.6.0 → 1.0.2 sosentinel-sbom --versionand theCreator: Tool:SBOM field match the tag. README LOC count corrected ~1,446 → ~4,250 (the v0.6 cycle addedlicense_scan.zigandspdx_license_list.zigwithout bumping the README number). End-to-end verification on five locally available flake.locks, byte-deterministic across two consecutive runs. ↩- SPDX 2.3 license expression grammar —
https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/. The parser insrc/spdx.zigimplements the grammar shapes enumerated atREADME.mdlines 274–285: simple ID, dual / disjunctive (OR), conjunctive (AND), exception (WITH), parenthesized compound,LicenseRef-*custom identifiers, trailing+"or-later" suffix. ↩ - U.S. Department of Commerce / NTIA, The Minimum Elements For a Software Bill of Materials (SBOM), July 2021. Defines the baseline data fields any SBOM should carry (supplier, component name, version, unique identifier, dependency relationship, author, timestamp).
sentinel-sbom's SPDX 2.3 output populates all seven by construction from theflake.lockcontent. ↩ - Eelco Dolstra, The Purely Functional Software Deployment Model (PhD thesis, Utrecht University, 2006), §5.2 — canonical NAR-format definition.
src/nar.zigis the in-tree Zig implementation of the format described there. ↩