A tool to create, transform and attest VEX metadata
Go
213
1,089 commits
updated Sep 23, 2026
vexctl is a tool to create, apply, and attest VEX (Vulnerability Exploitability
eXchange) data. Its purpose is to help with the creation and management of
VEX documents that allow "turning off" security scanner alerts of vulnerabilities
known not to affect a product.
VEX can be thought of as a "negative security advisory". Using VEX, software authors can communicate to their users that an otherwise vulnerable component has no security implications for their product.
Every release ships signed binaries for Linux, macOS and Windows on the
releases page. Download the
binary for your platform, make it executable and put it in your PATH.
Each binary comes with a sigstore bundle (*.sigstore.json) you can verify
with cosign before running it:
cosign verify-blob \
--bundle vexctl-linux-amd64.sigstore.json \
--certificate-identity-regexp='^https://github.com/openvex/vexctl/' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
vexctl-linux-amd64
If you use Homebrew, you can install the latest tagged version of vexctl using:
brew install vexctl
vexctl tracks recent Go releases. The Go version it requires is the go
directive in go.mod. With Go 1.21 or later, go install downloads
the required toolchain automatically, so this is enough:
go install github.com/openvex/vexctl@latest
With an older Go, install a current toolchain from go.dev/dl first.
To achieve its mission, vexctl has three main modes of operation:
VEX data can be created to a file on disk, or it can be captured in a signed attestation that can be attached to a container image.
The easiest way to create a VEX document is using the vexctl create command:
vexctl create --product="pkg:apk/wolfi/git@2.38.1-r0?arch=x86_64" \
--vuln="CVE-2014-123456" \
--status="not_affected" \
--justification="inline_mitigations_already_exist"
The previous invocations creates a VEX document with a single statement asserting
that the WolfiOS package git-2.38.1-r0 is not affected by CVE-2014-123456 because
it has already been mitigated in the distribution.
This is the resulting document:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-adc52fe6c8d2ba0feee7f4343f9b40c90e8cdb077817f880a6650502aece82bc",
"author": "Unknown Author",
"timestamp": "2023-10-07T23:32:07.620932-08:00",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "CVE-2014-123456"
},
"timestamp": "2023-10-07T23:32:07.620932-08:00",
"products": [
{
"@id": "pkg:apk/wolfi/git@2.38.1-r0?arch=x86_64"
}
],
"status": "not_affected",
"justification": "inline_mitigations_already_exist"
}
]
}
vexctl can create VEX documents from three different sources:
The data is generated from a known rule set (the Golden Data) which is reused and reapplied to new releases of the same project.
When more than one stakeholder is issuing VEX metadata about a piece of software, vexctl can merge the documents to get the most up-to-date impact assessment of a vulnerability. The following example can be run using the test documents found in this repository:
vexctl merge --product=pkg:apk/wolfi/bash@1.0.0 \
examples/openvex/document1.vex.json \
examples/openvex/document2.vex.json
The resulting document combines the VEX statements that express data about
bash@1.0.0 into a single document that tells the whole story of how CVE-2014-123456
was under_investigation and then fixed four hours later:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "merged-vex-077a7a26ee6f351b86fba3206d39e1872cb726f955ce18535b2e890cc20a8bf6",
"author": "Unknown Author",
"timestamp": "2023-10-07T23:33:45.966496-08:00",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "CVE-1234-5678"
},
"timestamp": "2022-12-22T16:36:43-05:00",
"products": [
{
"@id": "pkg:apk/wolfi/bash@1.0.0"
}
],
"status": "under_investigation"
},
{
"vulnerability": {
"name": "CVE-1234-5678"
},
"timestamp": "2022-12-22T20:56:05-05:00",
"products": [
{
"@id": "pkg:apk/wolfi/bash@1.0.0"
}
],
"status": "fixed"
}
]
}
The vexctl validate subcommand checks OpenVEX documents for conformance with
the specification, whatever wrote them: another tool, a pipeline, or you. It
matters because readers are forgiving — vexctl, like most consumers, ignores
data it does not recognize, so a misspelled field name is not rejected but
quietly dropped, and the statement ends up saying less than whoever wrote it
meant. validate reports every problem it finds rather than stopping at the
first:
$ vexctl validate vex.json
vex.json: 3 errors, 2 warnings
warning [iri] @id: the document @id "my-vex-doc" is not an IRI; @id fields should be globally unique identifiers such as a URL or a package URL
error [status] statements[0]: either justification or impact statement must be defined when using status "not_affected"
warning [unknown-field] statements[0].justifcation: "justifcation" is not an OpenVEX v0.2.0 field and is ignored when the document is read
error [purl] statements[1].products[0].@id: "pkg:not a purl" is not a valid package URL: purl is missing type or name
error [hash] statements[1].products[0].hashes.sha-256: a sha-256 hash is 64 hexadecimal characters long, "abc123" has 6
1 document checked, 0 valid, 1 invalid (3 errors, 2 warnings)
Findings come in two severities. An error means the document breaks the OpenVEX spec, and tools reading it may reject it or read it differently than intended. A warning means the document parses, but carries data that is ignored, redundant or no longer part of the spec.
Among other things, validate checks that the file holds a single OpenVEX
document; that every field is one the spec defines and holds a value of the
right type; that the document has an @id, an author, a timestamp and a
version; that every statement names a vulnerability, a status and at least one
product, and can be placed in time; that statuses, justifications, action
statements and impact statements are used in the combinations the spec allows;
and that package URLs parse, CPEs are well formed and hashes match the length
of the algorithm naming them.
vexctl exits with a non-zero status when any document has errors. Pass
--strict to fail on warnings too, which is what you want in CI:
# Check every document in a directory, failing on warnings too
vexctl validate --strict .openvex/*.json
# Report the findings as JSON, for another tool to read
vexctl validate --format=json vex.json
Note that validate checks documents written against OpenVEX v0.2.0. Documents
declaring an older spec version are reported as such and left alone; running
them through vexctl merge rewrites them in the current version.
# Attest and attach VEX statements in mydata.vex.json to a container image:
vexctl attest --attach --sign mydata.vex.json cgr.dev/image@sha256:e4cf37d568d195b4..
Signed attestations are written as sigstore bundles
and attached to images through the OCI referrers API, the layout used by
cosign v3. Use --format dsse to write the bare DSSE envelope instead and
--attach-method legacy to attach attestations in the cosign tag layout
(the .att tag next to the image) read by older tooling.
Using statements in a VEX document or from an attestation, vexctl will filter
security scanner results to remove VEX'ed out entries.
# From a VEX file:
vexctl filter scan_results.sarif.json vex_data.csaf
# From a stored VEX attestation:
vexctl filter scan_results.sarif.json cgr.dev/image@sha256:e4cf37d568d195b4b5af4c36a...
The output from both examples will be the same: the SARIF result data, but without the vulnerabilities that were stated as not exploitable:
{
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
"runs": [
{
"tool": {
"driver": {
"fullName": "Trivy Vulnerability Scanner",
"informationUri": "https://github.com/aquasecurity/trivy",
"name": "Trivy",
"rules": [
We support results files in SARIF for now. We plan to add support for the proprietary formats of the most popular scanners.
Assessing impact is process that takes time. VEX is designed to communicate with users as time progresses. An example timeline may look like this:
CVE-2014-123456, associated with one of its components.under_investigation to
inform their users they are aware of the CVE but are checking what impact it has.not_affected and using
the vulnerable_code_not_in_execute_path justification.vexctl will read all the documents in chronological order and "replay" the
known impacts statuses the order they were found, effectively computing the
not_affected status.
If a SARIF report is VEX'ed with vexctl any entries alerting of CVE-2014-123456
will be filtered out.
To build vexctl, clone this repository and run make.
$ git clone https://github.com/openvex/vexctl.git
$ cd vex
$ make
$ ./vexctl version
_ _ _____ __ __ _____ _____ _
| | | || ___|\ \ / // __ \|_ _|| |
| | | || |__ \ V / | / \/ | | | |
| | | || __| / \ | | | | | |
\ \_/ /| |___ / /^\ \| \__/\ | | | |____
\___/ \____/ \/ \/ \____/ \_/ \_____/
vexctl: A tool for working with VEX data
GitVersion: v0.1.0-21-g769ba3f-dirty
GitCommit: 769ba3f0c638003b6c5e3c41ae88f4cdc63555ab
GitTreeState: dirty
BuildDate: 2023-01-18T00:19:24Z
GoVersion: go1.19.4
Compiler: gc
Platform: darwin/arm64
Go
96.1%
Shell
2.3%
Makefile
1.6%
A tool to create, transform and attest VEX metadata
Go
213
1,089 commits
updated Sep 23, 2026
vexctl is a tool to create, apply, and attest VEX (Vulnerability Exploitability
eXchange) data. Its purpose is to help with the creation and management of
VEX documents that allow "turning off" security scanner alerts of vulnerabilities
known not to affect a product.
VEX can be thought of as a "negative security advisory". Using VEX, software authors can communicate to their users that an otherwise vulnerable component has no security implications for their product.
Every release ships signed binaries for Linux, macOS and Windows on the
releases page. Download the
binary for your platform, make it executable and put it in your PATH.
Each binary comes with a sigstore bundle (*.sigstore.json) you can verify
with cosign before running it:
cosign verify-blob \
--bundle vexctl-linux-amd64.sigstore.json \
--certificate-identity-regexp='^https://github.com/openvex/vexctl/' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
vexctl-linux-amd64
If you use Homebrew, you can install the latest tagged version of vexctl using:
brew install vexctl
vexctl tracks recent Go releases. The Go version it requires is the go
directive in go.mod. With Go 1.21 or later, go install downloads
the required toolchain automatically, so this is enough:
go install github.com/openvex/vexctl@latest
With an older Go, install a current toolchain from go.dev/dl first.
To achieve its mission, vexctl has three main modes of operation:
VEX data can be created to a file on disk, or it can be captured in a signed attestation that can be attached to a container image.
The easiest way to create a VEX document is using the vexctl create command:
vexctl create --product="pkg:apk/wolfi/git@2.38.1-r0?arch=x86_64" \
--vuln="CVE-2014-123456" \
--status="not_affected" \
--justification="inline_mitigations_already_exist"
The previous invocations creates a VEX document with a single statement asserting
that the WolfiOS package git-2.38.1-r0 is not affected by CVE-2014-123456 because
it has already been mitigated in the distribution.
This is the resulting document:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-adc52fe6c8d2ba0feee7f4343f9b40c90e8cdb077817f880a6650502aece82bc",
"author": "Unknown Author",
"timestamp": "2023-10-07T23:32:07.620932-08:00",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "CVE-2014-123456"
},
"timestamp": "2023-10-07T23:32:07.620932-08:00",
"products": [
{
"@id": "pkg:apk/wolfi/git@2.38.1-r0?arch=x86_64"
}
],
"status": "not_affected",
"justification": "inline_mitigations_already_exist"
}
]
}
vexctl can create VEX documents from three different sources:
The data is generated from a known rule set (the Golden Data) which is reused and reapplied to new releases of the same project.
When more than one stakeholder is issuing VEX metadata about a piece of software, vexctl can merge the documents to get the most up-to-date impact assessment of a vulnerability. The following example can be run using the test documents found in this repository:
vexctl merge --product=pkg:apk/wolfi/bash@1.0.0 \
examples/openvex/document1.vex.json \
examples/openvex/document2.vex.json
The resulting document combines the VEX statements that express data about
bash@1.0.0 into a single document that tells the whole story of how CVE-2014-123456
was under_investigation and then fixed four hours later:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "merged-vex-077a7a26ee6f351b86fba3206d39e1872cb726f955ce18535b2e890cc20a8bf6",
"author": "Unknown Author",
"timestamp": "2023-10-07T23:33:45.966496-08:00",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "CVE-1234-5678"
},
"timestamp": "2022-12-22T16:36:43-05:00",
"products": [
{
"@id": "pkg:apk/wolfi/bash@1.0.0"
}
],
"status": "under_investigation"
},
{
"vulnerability": {
"name": "CVE-1234-5678"
},
"timestamp": "2022-12-22T20:56:05-05:00",
"products": [
{
"@id": "pkg:apk/wolfi/bash@1.0.0"
}
],
"status": "fixed"
}
]
}
The vexctl validate subcommand checks OpenVEX documents for conformance with
the specification, whatever wrote them: another tool, a pipeline, or you. It
matters because readers are forgiving — vexctl, like most consumers, ignores
data it does not recognize, so a misspelled field name is not rejected but
quietly dropped, and the statement ends up saying less than whoever wrote it
meant. validate reports every problem it finds rather than stopping at the
first:
$ vexctl validate vex.json
vex.json: 3 errors, 2 warnings
warning [iri] @id: the document @id "my-vex-doc" is not an IRI; @id fields should be globally unique identifiers such as a URL or a package URL
error [status] statements[0]: either justification or impact statement must be defined when using status "not_affected"
warning [unknown-field] statements[0].justifcation: "justifcation" is not an OpenVEX v0.2.0 field and is ignored when the document is read
error [purl] statements[1].products[0].@id: "pkg:not a purl" is not a valid package URL: purl is missing type or name
error [hash] statements[1].products[0].hashes.sha-256: a sha-256 hash is 64 hexadecimal characters long, "abc123" has 6
1 document checked, 0 valid, 1 invalid (3 errors, 2 warnings)
Findings come in two severities. An error means the document breaks the OpenVEX spec, and tools reading it may reject it or read it differently than intended. A warning means the document parses, but carries data that is ignored, redundant or no longer part of the spec.
Among other things, validate checks that the file holds a single OpenVEX
document; that every field is one the spec defines and holds a value of the
right type; that the document has an @id, an author, a timestamp and a
version; that every statement names a vulnerability, a status and at least one
product, and can be placed in time; that statuses, justifications, action
statements and impact statements are used in the combinations the spec allows;
and that package URLs parse, CPEs are well formed and hashes match the length
of the algorithm naming them.
vexctl exits with a non-zero status when any document has errors. Pass
--strict to fail on warnings too, which is what you want in CI:
# Check every document in a directory, failing on warnings too
vexctl validate --strict .openvex/*.json
# Report the findings as JSON, for another tool to read
vexctl validate --format=json vex.json
Note that validate checks documents written against OpenVEX v0.2.0. Documents
declaring an older spec version are reported as such and left alone; running
them through vexctl merge rewrites them in the current version.
# Attest and attach VEX statements in mydata.vex.json to a container image:
vexctl attest --attach --sign mydata.vex.json cgr.dev/image@sha256:e4cf37d568d195b4..
Signed attestations are written as sigstore bundles
and attached to images through the OCI referrers API, the layout used by
cosign v3. Use --format dsse to write the bare DSSE envelope instead and
--attach-method legacy to attach attestations in the cosign tag layout
(the .att tag next to the image) read by older tooling.
Using statements in a VEX document or from an attestation, vexctl will filter
security scanner results to remove VEX'ed out entries.
# From a VEX file:
vexctl filter scan_results.sarif.json vex_data.csaf
# From a stored VEX attestation:
vexctl filter scan_results.sarif.json cgr.dev/image@sha256:e4cf37d568d195b4b5af4c36a...
The output from both examples will be the same: the SARIF result data, but without the vulnerabilities that were stated as not exploitable:
{
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
"runs": [
{
"tool": {
"driver": {
"fullName": "Trivy Vulnerability Scanner",
"informationUri": "https://github.com/aquasecurity/trivy",
"name": "Trivy",
"rules": [
We support results files in SARIF for now. We plan to add support for the proprietary formats of the most popular scanners.
Assessing impact is process that takes time. VEX is designed to communicate with users as time progresses. An example timeline may look like this:
CVE-2014-123456, associated with one of its components.under_investigation to
inform their users they are aware of the CVE but are checking what impact it has.not_affected and using
the vulnerable_code_not_in_execute_path justification.vexctl will read all the documents in chronological order and "replay" the
known impacts statuses the order they were found, effectively computing the
not_affected status.
If a SARIF report is VEX'ed with vexctl any entries alerting of CVE-2014-123456
will be filtered out.
To build vexctl, clone this repository and run make.
$ git clone https://github.com/openvex/vexctl.git
$ cd vex
$ make
$ ./vexctl version
_ _ _____ __ __ _____ _____ _
| | | || ___|\ \ / // __ \|_ _|| |
| | | || |__ \ V / | / \/ | | | |
| | | || __| / \ | | | | | |
\ \_/ /| |___ / /^\ \| \__/\ | | | |____
\___/ \____/ \/ \/ \____/ \_/ \_____/
vexctl: A tool for working with VEX data
GitVersion: v0.1.0-21-g769ba3f-dirty
GitCommit: 769ba3f0c638003b6c5e3c41ae88f4cdc63555ab
GitTreeState: dirty
BuildDate: 2023-01-18T00:19:24Z
GoVersion: go1.19.4
Compiler: gc
Platform: darwin/arm64
Go
96.1%
Shell
2.3%
Makefile
1.6%