VEX in PHP: suppress a CVE with an audit trail
A scanner maps your SBOM to CVEs; most of them don’t actually reach the code you ship.
VEX is how you say so — not_affected, with a machine-readable reason — instead of
muting the alert in a spreadsheet. This walks authoring an OpenVEX document, its
content-addressable id, and (optionally) attesting it with the packages you already have.
Author a document
use K2gl\OpenVex\OpenVex;
use K2gl\OpenVex\Status;
use K2gl\OpenVex\Justification;
$document = OpenVex::create(author: 'Acme, Inc. <security@acme.example>')
->statement(
vulnerability: 'CVE-2024-1234',
status: Status::NotAffected,
products: ['pkg:composer/acme/app@2.1.0'],
justification: Justification::VulnerableCodeNotInExecutePath,
)
->build();
file_put_contents('vex.json', $document->toJson());
A product is any IRI or purl; pass a string
for the common case, or a full Product (with subcomponents, hashes and other
identifiers) when you need it. The status rules are enforced on construction — a
not_affected statement without a justification or impact statement throws rather than
producing an invalid document.
The canonical id
Two documents with the same impact statements get the same @id, regardless of author or
timestamp — so a VEX document is content-addressable and easy to deduplicate. The hash is
byte-compatible with the go-vex reference.
$document->canonicalHash(); // "8ed99017…" — sha256 over the statements only
$document->generateId(); // "https://openvex.dev/docs/public/vex-8ed99017…"
Read and query
use K2gl\OpenVex\Document;
use K2gl\OpenVex\Status;
$document = Document::fromJson(file_get_contents('vex.json'));
foreach ($document->statementsFor('pkg:composer/acme/app@2.1.0') as $statement) {
if ($statement->status === Status::NotAffected) {
// suppress this CVE for that product; $statement->justification is the reason
}
}
statementsFor() matches an IRI, purl, CPE or hash digest against each statement’s
products and their subcomponents.
Attest it (optional)
To make the document tamper-evident, wrap it as an in-toto statement — OpenVEX is an
official in-toto predicate type — and sign it with a DSSE envelope. This uses the
existing in-toto-attestation and
dsse packages; the OpenVEX document array drops straight in as the
predicate.
use K2gl\InToto\ResourceDescriptor;
use K2gl\InToto\Statement;
use K2gl\Dsse\EcdsaP256Signer;
$statement = new Statement(
subject: [
new ResourceDescriptor(
name: 'app.phar',
digest: ['sha256' => hash_file('sha256', 'dist/app.phar')],
),
],
predicateType: 'https://openvex.dev/ns',
predicate: $document->toArray(),
);
$envelope = $statement->sign(EcdsaP256Signer::fromPem($privatePem, keyId: 'vex-key'));
file_put_contents('vex.dsse.json', $envelope->toJson());
The predicateType is the bare https://openvex.dev/ns — the in-toto predicate id, not
the document’s @context (…/ns/v0.2.0).
Packages
openvex · in-toto-attestation · dsse. For the signing key in CI, see keyless signing in GitHub Actions; for the whole provenance chain, from in-toto statement to signed SLSA provenance.