Packages · Supply-chain security
k2gl/openvex
Read, write and canonicalize OpenVEX documents in PHP.
Say a CVE in your SBOM doesn’t affect the shipped artifact — with a machine-readable justification and a stable, content-addressable document id.
Install
composer require k2gl/openvex Reach for it when
- You produce or consume OpenVEX (not_affected / affected / fixed) statements.
- You want the canonical @id, byte-compatible with the go-vex reference.
Look elsewhere when
- You want to sign the VEX — wrap Document::toArray() in an in-toto Statement (in-toto-attestation) and a DSSE envelope (dsse).
k2gl/openvex
Read, write and canonicalize OpenVEX documents in PHP.
VEX (Vulnerability Exploitability eXchange) answers the question a scanner can't: a CVE
appears in your SBOM, but does it actually affect the shipped artifact? An OpenVEX document
records that judgement — not_affected, affected, fixed or under_investigation,
with a machine-readable reason — so a consumer can suppress the noise with an audit trail.
It gives you:
- Model — immutable value objects for the whole spec (documents, statements, vulnerabilities, products and subcomponents) that enforce the status/justification rules on construction, so an invalid statement can't exist.
- (De)serialization —
fromJson()/toJson()round-trips real-world documents. - Canonical hash & IRI — the deterministic document
@id, byte-for-byte compatible with the reference implementation (openvex/go-vex).
Install
composer require k2gl/openvex
Requires PHP 8.1+ and ext-json (bundled with PHP). No other dependencies.
Usage
Author a document
use K2gl\OpenVex\OpenVex;
use K2gl\OpenVex\Status;
use K2gl\OpenVex\Justification;
$json = OpenVex::create(author: 'Acme, Inc.')
->statement(
vulnerability: 'CVE-2024-1234',
status: Status::NotAffected,
products: ['pkg:composer/k2gl/dsse@1.3.0'],
justification: Justification::VulnerableCodeNotInExecutePath,
)
->toJson();
A product is any IRI or package URL; pass a
string for the common case, or a full Product (with subcomponents, hashes and other
identifiers) when you need it. build() returns the Document instead of JSON and stamps
its canonical @id.
Read and query a document
use K2gl\OpenVex\Document;
use K2gl\OpenVex\Status;
$document = Document::fromJson($json);
foreach ($document->statementsFor('pkg:composer/k2gl/dsse@1.3.0') as $statement) {
if ($statement->status === Status::NotAffected) {
// suppress this CVE for that product, with $statement->justification as the reason
}
}
statementsFor() matches an IRI, purl, CPE or hash digest against each statement's
products and their subcomponents.
Canonical identity
Two documents with the same impact statements always get the same @id, regardless of
metadata. That makes documents content-addressable and easy to deduplicate.
$document->canonicalHash(); // "8ed99017…" — sha256 over the statements only
$document->generateId(); // "https://openvex.dev/docs/public/vex-8ed99017…"
Design
- The status rules of the spec (
not_affectedneeds a justification or an impact statement,affectedneeds an action statement, and so on) are checked in theStatementconstructor — parsing an invalid document throws rather than yielding a half-valid object. - Canonicalization follows go-vex exactly and is verified against its published test vectors. Where go-vex leaves component hash/identifier ordering to Go's random map iteration, this port sorts the keys, which is identical for the single-entry maps that occur in practice and deterministic otherwise.
- Timestamps finer than microseconds (Go emits nanoseconds) are truncated on parse; the canonical hash only uses whole seconds, so a document's identity is unaffected.
License
MIT — see LICENSE.
API
Public classes and methods, generated from the source.
K2gl\OpenVex\Component class
__construct( public readonly string $id = '', public readonly array $hashes = [], public readonly array $identifiers = [], public readonly string $supplier = '', )of(string $id): selffromArray(array $data): selftoArray(): arraycanonicalFragment(): stringmatchesIdentifier(string $identifier): bool
K2gl\OpenVex\DecodesJson trait
K2gl\OpenVex\Document class
__construct( public readonly string $author, public readonly DateTimeImmutable $timestamp, public readonly array $statements, public readonly int $version = 1, public readonly string $id = '', public readonly string $context = self::CONTEXT, public readonly string $role = '', public readonly ?DateTimeImmutable $lastUpdated = null, public readonly string $tooling = '', public readonly string $supplier = '', )fromJson(string $json): selffromArray(array $data): selftoArray(): arraytoJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): stringcanonicalHash(): stringgenerateId(): stringwithCanonicalId(): selfstatementsFor(string $identifier): array(Statement $statement) use ($identifier): bool(Statement $a, Statement $b): int
K2gl\OpenVex\Exception\InvalidDocumentException class
K2gl\OpenVex\Exception\InvalidStatementException class
K2gl\OpenVex\Exception\OpenVexException class
K2gl\OpenVex\Justification enum
K2gl\OpenVex\OpenVex class
create( string $author, ?DateTimeImmutable $timestamp = null, int $version = 1, string $role = '', string $tooling = '', string $supplier = '', ): selfstatement( string|Vulnerability $vulnerability, Status $status, array $products = [], ?Justification $justification = null, string $impactStatement = '', string $actionStatement = '', string $statusNotes = '', ): selfbuild(): DocumenttoJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): string
K2gl\OpenVex\Product class
__construct( string $id = '', array $subcomponents = [], array $hashes = [], array $identifiers = [], string $supplier = '', )of(string $id): selffromArray(array $data): selftoArray(): arraycanonicalFragment(): stringmatchesIdentifier(string $identifier): bool
K2gl\OpenVex\Statement class
__construct( public readonly Vulnerability $vulnerability, public readonly Status $status, public readonly array $products = [], public readonly ?Justification $justification = null, public readonly string $impactStatement = '', public readonly string $actionStatement = '', public readonly string $statusNotes = '', public readonly string $id = '', public readonly ?DateTimeImmutable $timestamp = null, public readonly ?DateTimeImmutable $lastUpdated = null, public readonly ?DateTimeImmutable $actionStatementTimestamp = null, )fromArray(array $data): selftoArray(): arrayeffectiveTimestamp(DateTimeImmutable $documentTimestamp): DateTimeImmutable
K2gl\OpenVex\Status enum
K2gl\OpenVex\Subcomponent class
fromArray(array $data): self
K2gl\OpenVex\Timestamp class
parse(string $value): DateTimeImmutableformat(DateTimeImmutable $value): string
K2gl\OpenVex\Vulnerability class
__construct( public readonly string $name, public readonly string $id = '', public readonly string $description = '', public readonly array $aliases = [], )of(string $name): selffromArray(array $data): selftoArray(): arraycanonicalFragment(): string