# vex attestor

Source: https://www.testifysec.com/docs/cilock/attestors/vex

The cilock vex attestor captures an OpenVEX document found among a step's product files and attaches it as in-toto evidence under the canonical OpenVEX predicate type URI.

Captures an OpenVEX document discovered among the step's product files and attaches it as an in-toto predicate with the canonical OpenVEX type URI.

 

| Name | `vex` |
| --- | --- |
| Predicate type | `https://openvex.dev/ns` |
| Lifecycle | `postproduct` |
| Default binary? | No |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when | - postproduct\_glob: \*.openvex.json \| \*\*/\*.openvex.json \| \*.vex.json \| \*\*/\*.vex.json<br>- postproduct\_glob: vex.json \| \*\*/vex.json |

The facts in this box are generated from the CI/lock binary's own catalog (`cilock tools list`). Do not hand-edit — run `npm run gen:catalog`.

 

## What it captures

 

The attestor walks `ctx.Products()`, recomputes each file's digest, verifies it matches the recorded product digest, then attempts to `json.Unmarshal` the bytes into the inlined `openvex.VEX` type and validates the result. The first product that both decodes **and** validates is captured; one that decodes into something that isn't a usable VEX document is skipped. Decoding alone proves nothing — every VEX field is optional to `encoding/json` and unknown keys are ignored, so `{}` and any unrelated JSON object decode into an empty document. Validation is the OpenVEX required set (`@context`, `@id`, `author`, `timestamp`, `version`, at least one statement; per statement a vulnerability, at least one identifiable product, and a known status) plus the spec's status rules: `not_affected` needs a justification or an impact statement, `affected` needs an action statement, and no other status may carry a justification. The serialized predicate has three fields:

 

- `vexDocument` (`openvex.VEX`) — the full parsed OpenVEX document (see types below).
 - `reportFileName` (`string`) — path of the source product file.
 - `reportDigestSet` (`cryptoutil.DigestSet`) — digest set carried over from the product, recorded so verifiers can re-anchor the document to the step's product attestor.

 

`--attestor-vex-file <path>` pins the attestor to a specific document instead of scanning the product set — use it when the document already existed when the step started (so it is a material, not a product). On that path the recorded `reportDigestSet` is computed from the same bytes that were decoded, and an unreadable, unparseable, or invalid document is a hard error rather than a skip: the operator named this file, so attesting an empty predicate instead would be the wrong answer. `cilock attest vex` authors a document and wires this flag for you.

 

The `openvex` types are **inlined** at `plugins/attestors/vex/openvex/types.go` from `github.com/openvex/go-vex` commit `3185a64ed27703fc3fe4af8cd5e1ce0ed2fa2569` (tag `v0.2.7`, Apache-2.0). Only the type definitions and constants required for JSON decode/encode are present — no upstream methods (`Validate`, `Matches`, `Builder`, etc.) are reproduced. The `Build` and `Validate` helpers that sit beside the types in that package are rookery's own, written against the spec rather than ported. The inlined surface:

 

- `VEX` — embeds `Metadata` plus `Statements []Statement`. Has a custom `MarshalJSON` that normalizes `timestamp` / `last_updated` to UTC RFC3339 (no nanoseconds), byte-compatible with upstream go-vex output.
 - `Metadata` — `@context`, `@id`, `author`, `role`, `timestamp`, `last_updated`, `version`, `tooling`, `supplier`.
 - `Statement` — `@id`, `vulnerability`, `timestamp`, `last_updated`, `products`, `status`, `status_notes`, `justification`, `impact_statement`, `action_statement`, `action_statement_timestamp`.
 - `Vulnerability` — `@id`, `name` (`VulnerabilityID`), `description`, `aliases`.
 - `Product` — embeds `Component`, plus `subcomponents []Subcomponent`.
 - `Subcomponent` — embeds `Component`.
 - `Component` — `@id`, `hashes` (`map[Algorithm]Hash`), `identifiers` (`map[IdentifierType]string`), `supplier`.
 - `Status` constants: `not_affected`, `affected`, `fixed`, `under_investigation`.
 - `Justification` constants: `component_not_present`, `vulnerable_code_not_present`, `vulnerable_code_not_in_execute_path`, `vulnerable_code_cannot_be_controlled_by_adversary`, `inline_mitigations_already_exist`.
 - `IdentifierType` constants: `purl`, `cpe22`, `cpe23`.
 - `Algorithm` constants: `md5`, `sha1`, `sha-256`, `sha-384`, `sha-512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake2s-256`, `blake2b-256`, `blake2b-512`, `blake3`.

 

## When to use

 

Pair with the `sbom` attestor to deliver SBOM + VEX evidence in a single step: the SBOM enumerates components, the VEX document records exploitability statements (not\_affected / affected / fixed / under\_investigation) against those components. Policies can then gate releases on the presence and contents of both predicate types.

 

## Flags

 

| Flag | Default | What it does |
| --- | --- | --- |
| `--attestor-vex-file <path>` | (none) | Attest this document directly instead of probing the product set. Relative paths resolve against the working directory. Use it when the document was authored before the wrapped command ran — it is then a material, not a product, so probing would find nothing |

 

With no flags, selection is implicit: the first product that decodes into a valid OpenVEX document is captured.

 

## Output shape

 

```json
{
  "vexDocument": {
    "@context": "https://openvex.dev/ns/v0.2.0",
    "@id": "https://openvex.example/docs/vex-001",
    "author": "Example, Inc.",
    "timestamp": "2026-05-21T12:00:00Z",
    "version": 1,
    "statements": [
      {
        "vulnerability": { "name": "CVE-2024-12345" },
        "products": [
          { "@id": "pkg:oci/example@sha256:abcd..." }
        ],
        "status": "not_affected",
        "justification": "vulnerable_code_not_in_execute_path"
      }
    ]
  },
  "reportFileName": "build/vex.json",
  "reportDigestSet": {
    "sha256": "..."
  }
}
```

 

## Gotchas

 

- **Detection is unmarshal-probing, not MIME-based.** The attestor calls `json.Unmarshal` against every product in turn and picks the first that parses into `openvex.VEX`. There is no content-type check, file-extension filter, or `@context` validation — any JSON whose top-level shape happens to fit (e.g. a document with a `statements` array) will be accepted. Keep VEX files distinct in the product set.
 - **`Attest` returns `no VEX file found` if no product decodes.** Add the VEX file to the step's products (e.g. via the `product` attestor's path config) or the step will fail.
 - **Digest mismatches are skipped, not fatal.** If `CalculateDigestSetFromFile` disagrees with the recorded product digest, the file is logged at debug and skipped — no error is surfaced unless every candidate fails.
 - **Inlined types, not the upstream library.** Documents are decoded against the rookery-internal `openvex` package, which is byte-compatible with OpenVEX 0.2.0 but does not call upstream `Validate` / `Matches`. Schema drift in newer OpenVEX versions will silently lose unknown fields.
 - **Timestamps re-serialize to RFC3339 UTC.** `VEX.MarshalJSON` strips sub-second precision and forces UTC. Round-tripping a document through the attestor produces a canonical form, not a byte-identical copy of the input.

 

## CLI example

 

Real OpenVEX statement emitted alongside an SBOM, expressing CVE-not-affected status.

 

```bash
cilock run --step vex-capture \
  --signer-file-key-path key.pem --outfile attestation.json \
  --attestations vex,environment,git \
  -- vexctl create --product pkg:apk/alpine/curl@8.0.0 --vuln CVE-2023-38545 --status not_affected --justification vulnerable_code_not_in_execute_path --file app.openvex.json
```

 

Validated with a real OpenVEX (openvex.dev/ns) statement. See the full real-data example at [https://github.com/aflock-ai/attestor-compliance-examples/tree/main/37-vex](https://github.com/aflock-ai/attestor-compliance-examples/tree/main/37-vex).

 

## See also

 

- [Catalog row](https://www.testifysec.com/docs/cilock/reference/attestor-catalog)
 - [`sbom`](https://www.testifysec.com/docs/cilock/attestors/sbom)
 - [OpenVEX spec](https://github.com/openvex/spec)
 - Upstream: [witness/vex.md](https://github.com/in-toto/witness/blob/main/docs/attestors/vex.md)

 

---

**This page is generated from the CI/lock tool catalog.** Don't edit it here — the source is [`attestation/detection/docs/vex.doc.md`](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/vex.doc.md) in [aflock-ai/rookery](https://github.com/aflock-ai/rookery). The same catalog powers `cilock tools show vex` in the CLI, so a single edit keeps the terminal and the website in sync.

[Edit this page →](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/vex.doc.md) ·  [Add a new tool →](https://github.com/aflock-ai/rookery/new/main/attestation/detection/docs)

When you open the pull request: if you are an **AI agent**, add the `ai-agent` label. If you are a **human**, add the `human` label.

Reference generated from the product documentation. Match commands and support details to your installed release.
