vulnlog vex openvex

The Vulnlog OpenVEX export feature is currently incubating. Its behavior and configuration options may change in the upcoming releases.

Writes an OpenVEX document from your Vulnlog file. Hand it to Trivy or Grype and the findings you triaged disappear from the scan, for you and for everyone who scans your software.

vulnlog vex openvex <file> [--release <release-id>] [--tag <tag>]... [--baseline <path>] [-o <path>]
Flag Description

--release <release-id>

Write the document for this release only, even one without published_at. Without it the document covers every published release that declares purls. See One document per release and artifact.

--tag <tag>

Keep only the vulnerabilities and release purls carrying this tag. Repeatable; those carrying any of the given tags are kept. See One document per release and artifact.

--baseline <path>

The OpenVEX document you published before. Its @id is kept and its version counts up when the content changed. See Keep a published document stable.

-o, --output <path>

Output file path, or - to write to stdout. Defaults to vex.json in the current directory.

-

Use as the file argument to read from stdin.

Declare and tag release purls

A statement names the products it applies to, so every release the document should cover needs purls: the Package URLs of the artifacts you ship. A statement names only the release purls that share a tag with the vulnerability, so tag both.

releases:
  - id: 1.2.0
    published_at: 2026-04-20
    purls:
      - purl: "pkg:maven/com.acme/acme-web-app@1.2.0"
        tags: [ library ]
      - purl: "pkg:oci/acme-web-app@sha256:9b2a7c41e0d35f68a1c4e7b2d90f3a5c86e1b4d7f2a09c3e5b8d6f1a4c7e2b90?repository_url=ghcr.io/acme/web-app"
        tags: [ container ]

vulnerabilities:
  - id: CVE-2026-5678
    releases: [ 1.2.0 ]
    tags: [ container ]    # a statement for the image only

A vulnerability without tags, or whose tags no release purl carries, appears in no document, so it never suppresses a finding in an artifact nobody analysed.

Declare the purl your users' scanner matches:

Artifact Release purl Matched by

Container image

pkg:oci/<name>@sha256:<digest>?repository_url=<registry>/<repo>

Trivy, Grype

Library or plugin

The ecosystem purl, for example pkg:maven/com.acme/acme-lib@1.2.0

Trivy, also in the scans of your users

Application scanned as a project

The purl of the root module, for example pkg:maven/com.acme/acme-web-app@1.2.0

Trivy

Native binary, installer

pkg:generic/<name>@<version>

No scanner filters on it yet.

Every release purl carries the version of the artifact in that release. A purl without a version matches every version, so its statements would apply to every release you ever shipped; vulnlog validate warns about it. For a container image the version is the digest of the pushed image, because Trivy does not match an image tag in its place.

A release without purls is left out of the document and named in a warning: line; vulnlog validate warns about it as well.

What a statement says

Vulnlog writes one statement per vulnerability entry and release it applies to, naming the release purls it shares a tag with. An entry applies from the earliest release it lists through every later release in the file. From the release named by resolution.in on, it is fixed, even where releases lists it too.

Your entry OpenVEX statement

No verdict yet

under_investigation, with analysis as status_notes.

not affected

not_affected, with the justification and analysis as impact_statement.

affected

affected, with analysis as status_notes and an action_statement, see below.

resolution.in and later releases

fixed, without analysis: it describes the state before the fix.

The action_statement follows from disposition and resolution.in, never from analysis or the resolution note, which are written for your team:

disposition resolution.in action_statement

(absent) or will fix

a published release

Update to release <id>.

(absent) or will fix

a release without published_at

A fix is planned for release <id>.

(absent)

absent

No remediation is available yet.

will fix

absent

A fix is planned but not yet available.

wont fix

set

The risk is accepted for this release. A fix ships with release <id>.

wont fix

absent

The risk is accepted. No fix is planned.

Once you set published_at on the fix release, A fix is planned for release <id>. becomes Update to release <id>., and a run with --baseline issues a new version.

Each statement is dated by the day its status became true: resolution.at, else the fix release’s published_at, for fixed; analyzed_at for a verdict; the earliest reports[].at for an open investigation or a verdict without analyzed_at. Without any of these the release’s own published_at is used. Only a release named with --release can lack it; its statements are then dated by the run that first issues them. vulnlog validate warns about undated statements, because only a --baseline keeps such a time from one version to the next.

aliases, description and packages are written as well; packages become the subcomponents of each product. name, severity, comment and reports have no place in OpenVEX.

One document per release and artifact

Without scoping flags the document covers every published release that declares purls. That suits your own CI scan and a VEX hub, where one file serves every release.

A release without published_at is left out, because your users can only act on releases they can install. Name it with --release to write its document anyway, for example in the pipeline that publishes it.

To publish next to an artifact, write one document per release and artifact: --release picks the release, and --tag keeps the vulnerabilities and release purls carrying the tag.

vulnlog vex openvex vulnlog.yaml --release 1.2.0 --tag container -o vex-1.2.0-container.json

--release does not cut the range: an entry reported for an earlier release still applies, and a fix in a later release is still named in the action_statement.

Keep a published document stable

Consumers recognise later versions of a document by its @id. Without --baseline every run issues a new @id with version 1, even when the output file exists. With --baseline, the run continues the document you published:

vulnlog vex openvex vulnlog.yaml --baseline vex.json -o vex.json
  • @id stays, version counts up and timestamp is the time of the run.

  • A statement that did not change keeps its time.

  • When nothing but the clock would change, no new version is issued: writing back to the baseline prints Unchanged:, and any other output receives the baseline as it is.

  • A baseline written by another tool always counts as changed, because it carries fields Vulnlog does not write.

A baseline that is missing, no OpenVEX document, in another OpenVEX version than 0.2.0, or has an @id, timestamp or version that cannot be continued fails the run. Vulnlog never replaces it with a new identity, because consumers following the @id would lose track of your document.

Use the document with your scanner

Only not_affected and fixed statements remove a finding; affected and under_investigation keep it in the scan.

trivy image --vex vex.json ghcr.io/acme/web-app:1.2.0
trivy fs --vex vex.json --show-suppressed .
grype --vex vex.json ghcr.io/acme/web-app:1.2.0

Trivy matches a product against the scan target and every dependency in its tree, so a library’s document also filters the scans of its users. Grype matches the scanned image only and needs a pkg:oci purl.

Examples

See what the document leaves out
vulnlog -vv vex openvex full-example.vl.yaml
verbose: parsed full-example.vl.yaml: schema version 1, releases: 3, tags: 4, vulnerabilities: 7
verbose: releases without published_at are not part of the document: '8.1.2'
verbose: anchored on 2 releases with purls: '8.1.0' (2 purls), '8.1.1' (2 purls)
debug: skipped CVE-2020-099: no release purl in scope shares one of its tags
debug: skipped CVE-2022-21449: no release it applies to declares purls in scope
debug: skipped SNYK-2021-003: no release it applies to declares purls in scope
verbose: collected 4 statements: 1 affected, 2 not_affected, 1 under_investigation
Wrote: vex.json
verbose: wrote vex.json: openvex format, version 1, 4 statements
One statement of a written document
{
  "vulnerability": {
    "@id": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228",
    "name": "CVE-2021-44228",
    "description": "RCE vulnerability in Log4J via JNDI lookup",
    "aliases": [
      "GHSA-jfh8-c2jp-5v3q"
    ]
  },
  "timestamp": "2021-12-10T00:00:00Z",
  "products": [
    {
      "@id": "pkg:generic/demo-project@8.1.1",
      "identifiers": {
        "purl": "pkg:generic/demo-project@8.1.1"
      },
      "subcomponents": [
        {
          "@id": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
          "identifiers": {
            "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
          }
        }
      ]
    }
  ],
  "status": "affected",
  "action_statement": "A fix is planned for release 8.1.2.",
  "action_statement_timestamp": "2021-12-10T00:00:00Z",
  "status_notes": "Demo project utilizes Log4J and a PoC showed that the project is vulnerable to this RCE vulnerability.",
  "supplier": "Demo Project Inc"
}

When the command fails

  • The Vulnlog file fails validation: exit 2.

  • No statement applies, because no release declares purls, no published release declares them, no vulnerability shares a tag with the release purls, or the scope leaves nothing: exit 2, no file is written, since OpenVEX requires at least one statement.

  • An unknown --release or --tag, or a --baseline that cannot be continued: exit 5.

See Exit codes and messages for the full table.

See also