Skip to content

v0.16.0 — the merged report, not just the tree

Choose a tag to compare

@cwayne18 cwayne18 released this 14 Sep 15:14
· 28 commits to main since this release
3463d90

The file everyone actually reads

Some hubs publish, alongside the per-product pkg/ tree, one document with every product's statements merged into it — rancher/vexhub has three under reports/. A CI run scanning thirty images hands its scanner one --vex reports/rancher.openvex.json rather than assembling thirty documents, which is the whole reason the file exists.

It is also the file most consumers read. So a --vex-out contribution that updates pkg/ and leaves it behind changes nothing for them — and until now, that is what every pull request this tool produced into such a hub was (#21).

vexscan --images-from fleet.txt --all \
  --vexhub ./vexhub --vex-out ./vexhub \
  --vex-author 'Acme Security' \
  --vex-merge-into reports/rancher.openvex.json
vex-out: wrote 41 statement(s) across 12 product(s) to ./vexhub
  …
  reports/rancher.openvex.json: +41 statement(s) across 12 product(s), merged

The merge is the same byte-preserving round-trip the per-product documents get, which matters more here than anywhere else: rancher's report is 127 MB of statements on a single line, and it comes back out as 127 MB on a single line with the statements appended and timestamp moved. Their @id, author and version are left alone — you are adding to their document, not reissuing it — and dedupe runs against everything already in it, so a claim it already carries is not written twice even when the per-product document is missing it. At 316,242 statements, that dedupe is an index from every id and alias to the statements mentioning it, built once per run.

Three rules the shape of these files forces

An aggregate is named, never discovered — and never indexed. Nothing in the VEX Repository specification describes them; index.json maps a product to one document, and none of rancher's appear in it. From the outside a merged report is indistinguishable from any other JSON in the tree, so guessing would eventually mean writing VEX statements into somebody's changelog. The same reasoning keeps it out of index.json: indexing one would tell every reader the merged report is the document for a single product, which is exactly what it is not.

A named aggregate that cannot be written fails the run, where an unreadable per-product document is reported and stepped over. The difference is who chose the file — a product's location comes from the hub's index, and one bad document among a thousand is no reason to withhold the rest; an aggregate was named on the command line because the contribution is not usable without it. The write runs on the finished plan, so the exit leaves the clone untouched rather than half updated.

An unfetched Git LFS pointer is refused by name. A clone made without git-lfs holds 133 bytes where the report should be:

error: vex-out: vexpr: --vex-merge-into reports/rancher.openvex.json: it is an
unfetched Git LFS pointer, not the document it stands for; fetch it
(git lfs pull --include=<path>) and re-run

"Could not be parsed" would send someone hunting for malformed JSON when what they need is one git command — and the other direction, writing a small document over the pointer, is a pull request that silently replaces a hub's merged report with a fraction of its contents.

CSAF declines the job entirely: an advisory is identified by document.tracking.id, revised as a unit and attributed to one publisher, so one holding every product's claims would claim authority over all of them at once. --vex-merge-into with --vex-format csaf is rejected before the scan starts.

A fleet contributes once

Batch mode now collects its results and writes once. Each image is still its own product with its own document, but the hub is read once, the index resolved once and the merged report rewritten once — not forty times.

contrib/vexhub-pr.sh --merge-into PATH

Clones with GIT_LFS_SKIP_SMUDGE=1 and fetches only the aggregates you named, so a 127 MB object is pulled only when it is actually being merged into. Stops early if git-lfs is missing or the file is still a pointer. Excludes aggregates from the diff it prints for review — a one-line 127 MB file has no reviewable diff — and lists them in the PR body instead. And it says what pushing a fresh copy of that object costs the hub's Git LFS quota, before it asks.

Also in this release

A trailing-newline bug found in passing: layout was detected on the whitespace-trimmed bytes, where a trailing newline can never be seen, so every merge into an existing document dropped that document's final newline and added a \ No newline at end of file to a diff nothing else in it justified.

Verified against the real thing

A 126,788,465-byte copy of reports/rancher.openvex.json alongside rancher's own index.json: 316,236 → 316,242 statements, +2,174 bytes, still one line with its trailing newline intact, top-level key order and the hub's @id/author/version preserved, and absent from index.json. A re-run writes nothing. With the per-product document deleted but the aggregate still carrying the claims, the product document was rewritten and the master left byte-identical.

Full changelog: v0.15.0...v0.16.0