# 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

```bash
composer require k2gl/openvex
```

## Requirements

- PHP >=8.1

## Documentation

# k2gl/openvex

[![CI](https://img.shields.io/github/actions/workflow/status/k2gl/openvex/ci.yml?branch=main&label=CI&logo=github)](https://github.com/k2gl/openvex/actions/workflows/ci.yml)
[![Latest Stable Version](https://img.shields.io/packagist/v/k2gl/openvex?logo=packagist&logoColor=white)](https://packagist.org/packages/k2gl/openvex)
[![Total Downloads](https://img.shields.io/packagist/dt/k2gl/openvex?logo=packagist&logoColor=white)](https://packagist.org/packages/k2gl/openvex)
[![PHPStan Level](https://img.shields.io/badge/PHPStan-level%209-2a5ea7?logo=php&logoColor=white)](https://phpstan.org)
[![License](https://img.shields.io/packagist/l/k2gl/openvex?color=yellowgreen)](https://packagist.org/packages/k2gl/openvex)

Read, write and canonicalize [OpenVEX](https://github.com/openvex/spec) 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`](https://github.com/openvex/go-vex)).

## Install

```bash
composer require k2gl/openvex
```

Requires PHP 8.1+ and `ext-json` (bundled with PHP). No other dependencies.

## Usage

### Author a document

```php
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](https://github.com/package-url/purl-spec); 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

```php
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.

```php
$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](LICENSE).

## API

### 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)

_no public methods_

### 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)

_no public methods_

### K2gl\OpenVex\Exception\InvalidStatementException (class)

_no public methods_

### K2gl\OpenVex\Exception\OpenVexException (class)

_no public methods_

### K2gl\OpenVex\Justification (enum)

_no public methods_

### 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)

_no public methods_

### 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`

## Links

- GitHub: https://github.com/k2gl/openvex
- Packagist: https://packagist.org/packages/k2gl/openvex
