Repository navigation
v0.15.0 — CSAF-VEX, read and written
Half the VEX in the world is not OpenVEX
SUSE, Red Hat, Cisco and Oracle publish theirs as CSAF 2.0, the OASIS format. A VEX hub is free to hold either, and until now --vexhub could read only OpenVEX and --vex-out could write only OpenVEX (#20).
Reading: whichever the vendor published
A hub document is now decoded as CSAF or OpenVEX based on its bytes, not its file name. A hub's index.json publishes locations rather than a naming convention, so the name at the end of one is data and not a promise, and sniffing is the only thing that is actually true about it.
CSAF says the same things through more indirection, all of which is resolved on the way in:
| OpenVEX | CSAF |
|---|---|
products[].@id |
a full_product_names entry reached through product_tree branches, identified by the purl in its helper |
products[].subcomponents[].@id |
a default_component_of relationship from the component to the product |
justification |
flags[].label — the same five values, spelled identically |
impact_statement |
threats[] with category: impact |
vulnerability.aliases |
ids[], each with the system_name that issued it |
Both arrive at the matcher as the same thing, so an ALREADY VEXED row looks the same either way, --details prints the vendor's own sentence out of either, and the alias bridging that makes a GHSA- finding meet a CVE- statement works out of either.
Writing: --vex-format csaf
vexscan --image myorg/app:latest --all \
--vexhub ./vexhub --vex-out ./vexhub \
--vex-author 'Acme Security' \
--vex-format csaf \
--vex-publisher-namespace https://acme.example \
--vex-publisher-category vendorscan.csaf.json goes where scan.openvex.json would have, in the same pkg/ tree, indexed the same way. Which findings are selected, how they are deduplicated against the hub, that the diff is preserved byte for byte, that only a complete scan writes — all unchanged. Only the serialisation differs.
CSAF requires a publisher block, so --vex-publisher-namespace is required and has no default, for the same reason --vex-author does not: it is the URI that says who published the advisory. --vex-publisher-category defaults to other. Both are an error on any other format rather than quietly ignored.
Two rules fall out of CSAF's own model, and both are worth knowing before you use it:
- A CSAF document is an advisory, and amending one issues a new version of it in its publisher's name. So only a document vexscan itself wrote is added to. Ownership is decided by
document.tracking.id, derived from the product (VEXSCAN-OCI-INDEX-DOCKER-IO-EXAMPLE-SYNTHETIC); if the hub's id is not the one this run would have generated, nothing is written for that product and awarning:says whose advisory it is. When it does match, the version increments, arevision_historyentry is appended andcurrent_release_datemoves — which is why a re-run that adds nothing writes nothing at all rather than bumping a date. - A hub points each product at one document, so the two formats do not mix per product. Asking for CSAF where the hub already publishes OpenVEX for that product leaves the OpenVEX file alone and names the flag that would have worked, rather than writing a second document
index.jsonnever points at. New products are filed in whichever format you asked for, so a hub can hold both — just not two for the same product.
A hazard that predated the feature
A CSAF document decodes as a perfectly valid empty OpenVEX document. Nothing errors: no statements key means no statements. The old merge would have appended its own statements[] array to somebody's advisory and written it back over the original. The OpenVEX encoder now checks the format before it parses, and declines.
In the same vein, the reader got stricter about truncation. Bytes after the document that are not even valid JSON are exactly the shape a cut-off HTTP response arrives in, and were being accepted; they are now refused. A document that cannot be read must never be mistaken for a publisher with nothing to say.
contrib/vexhub-pr.sh
--format,--publisher-namespaceand--publisher-categorypass through.- It stages before it diffs. A product the hub has never carried arrives as an untracked file, which a plain
git diffdoes not show — so a brand-new document could reach a pull request without appearing in the review this script exists to insist on. - It stops with an error when every document it had statements for was left untouched, rather than reporting the empty diff as "the hub already covers everything".
--helpreads the comment block instead of a fixed line range that had already begun truncating it.
Internals
internal/csaf is a shared decoder, internal/vexpr is split at a format-neutral seam — selection produces neutral claims and only the encoder differs, so dedupe, the index, path safety and the hub reads are written once — and internal/distrofeed/suse drops its own reduced CSAF parser onto the shared one.
Not built, deliberately: merging into a third party's CSAF document.