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 |
|---|---|
|
Write the document for this release only, even one without |
|
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. |
|
The OpenVEX document you published before.
Its |
|
Output file path, or |
|
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 |
|
Trivy, Grype |
Library or plugin |
The ecosystem purl, for example |
Trivy, also in the scans of your users |
Application scanned as a project |
The purl of the root module, for example |
Trivy |
Native binary, installer |
|
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 |
|
|
|
|
|
|
|
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 |
a published release |
|
(absent) or |
a release without |
|
(absent) |
absent |
|
|
absent |
|
|
set |
|
|
absent |
|
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
-
@idstays,versioncounts up andtimestampis 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
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
{
"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: exit2, no file is written, since OpenVEX requires at least one statement. -
An unknown
--releaseor--tag, or a--baselinethat cannot be continued: exit5.
See Exit codes and messages for the full table.
See also
-
Project and releases for declaring release purls
-
Verdicts and justifications for the justification vocabulary
-
vulnlog suppress for scanners that read ignore files instead of VEX