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.

k2gl/openvex version on Packagist k2gl/openvex total downloads k2gl/openvex license

Install

composer require k2gl/openvex
PHP
>=8.1

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)serializationfromJson() / 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_affected needs a justification or an impact statement, affected needs an action statement, and so on) are checked in the Statement constructor — 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): self
  • fromArray(array $data): self
  • toArray(): array
  • canonicalFragment(): string
  • matchesIdentifier(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): self
  • fromArray(array $data): self
  • toArray(): array
  • toJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE): string
  • canonicalHash(): string
  • generateId(): string
  • withCanonicalId(): self
  • statementsFor(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 = '', ): self
  • statement( string|Vulnerability $vulnerability, Status $status, array $products = [], ?Justification $justification = null, string $impactStatement = '', string $actionStatement = '', string $statusNotes = '', ): self
  • build(): Document
  • toJson(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): self
  • fromArray(array $data): self
  • toArray(): array
  • canonicalFragment(): string
  • matchesIdentifier(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): self
  • toArray(): array
  • effectiveTimestamp(DateTimeImmutable $documentTimestamp): DateTimeImmutable

K2gl\OpenVex\Status enum

K2gl\OpenVex\Subcomponent class

  • fromArray(array $data): self

K2gl\OpenVex\Timestamp class

  • parse(string $value): DateTimeImmutable
  • format(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): self
  • fromArray(array $data): self
  • toArray(): array
  • canonicalFragment(): string