Authors and validates OpenVEX documents - produces `not_affected`, `affected`, `fixed`, and `under_investigation` statements with justification codes using `vexctl create`; attaches VEX assertions to container images; outputs `.openvex.json` files consumed on a downstream VEX-filter / vulnerability-prioritization path. Use when a scanner flags a CVE that analysis confirms is not exploitable in your deployment, and a machine-readable `not_affected` assertion is needed to suppress false positives without discarding the finding from the audit trail.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Per github.com/openvex/spec, OpenVEX is a minimal implementation of the Vulnerability Exploitability eXchange (VEX) standard. A VEX document carries statements that assert the exploitability status of a CVE against a specific product, allowing downstream consumers to suppress false positives while preserving the audit trail.
A downstream vulnerability-prioritization step reads a .openvex.json file
and moves findings with vex_status: not_affected to the Filtered-VEX
bucket: not blocking the build, but still surfaced in the report. A
not_affected assertion without a populated justification is rejected as
unverified.
Do not use VEX as a waiver mechanism for CVEs in CISA KEV; a conformant
prioritization step refuses not_affected on KEV entries regardless of
justification.
Per github.com/openvex/vexctl:
# Go
go install github.com/openvex/vexctl@latest
# Homebrew
brew install vexctlVerify:
vexctl versionPer github.com/openvex/spec/blob/main/OPENVEX-SPEC.md, an OpenVEX document wraps an array of statements. Each statement has:
| Field | Required | Notes |
|---|---|---|
vulnerability.name | yes | CVE-ID or GHSA-ID |
products[].@id | yes | Package URL (purl) identifying the component |
status | yes | not_affected / affected / fixed / under_investigation |
justification | yes for not_affected | One of five codes (see Step 3) |
impact_statement | alt for not_affected | Free-form prose if no justification code fits |
action_statement | optional for affected | Remediation description |
status_notes | optional | Supporting detail for any status |
Document-level required fields per vex-spec-md:
@context (https://openvex.dev/ns/v0.2.0), @id, author, timestamp,
version (integer, incremented on any content change), statements.
Per vex-spec-md, the five justification codes for not_affected:
| Code | When to use |
|---|---|
component_not_present | The component containing the vulnerable code is not included in the product at all |
vulnerable_code_not_present | The component is present but the vulnerable code was excluded via configuration or build flags |
vulnerable_code_not_in_execute_path | The vulnerable code exists but cannot be reached during normal execution |
vulnerable_code_cannot_be_controlled_by_adversary | The vulnerable code can run but adversary input cannot reach it |
inline_mitigations_already_exist | Built-in protections prevent exploitation (e.g. RELRO, stack canaries, vendor backport) |
If none of the five codes accurately describes the determination, omit
justification and supply impact_statement with a precise technical
explanation instead. The prioritization step accepts either field as evidence
of analysis.
not_affected assertionPer vexctl:
vexctl create \
--author="platform-team@example.com" \
--product="pkg:oci/my-app@sha256:abc123" \
--vuln="CVE-2024-9999" \
--status="not_affected" \
--justification="vulnerable_code_not_in_execute_path" \
> sbom.openvex.jsonThe generated document looks like:
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-...",
"author": "platform-team@example.com",
"timestamp": "2026-06-04T10:00:00Z",
"version": 1,
"statements": [
{
"vulnerability": { "name": "CVE-2024-9999" },
"products": [{ "@id": "pkg:oci/my-app@sha256:abc123" }],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
]
}Use a precise purl that matches the package value in the prioritization
step's ContainerFinding records; a mismatch on version or arch will cause the
affect.ref.endsWith(f['package']) check to miss.
Per vexctl, merge multiple single-statement files into one document:
# Author each assertion separately
vexctl create --product="pkg:oci/my-app@sha256:abc123" \
--vuln="CVE-2024-1111" \
--status="not_affected" \
--justification="component_not_present" \
> vex-cve-1111.json
vexctl create --product="pkg:oci/my-app@sha256:abc123" \
--vuln="CVE-2024-2222" \
--status="under_investigation" \
> vex-cve-2222.json
# Merge into a single document
vexctl merge --product="pkg:oci/my-app@sha256:abc123" \
vex-cve-1111.json vex-cve-2222.json \
> sbom.openvex.jsonvexctl merge re-timestamps the merged document and increments version.
Per vexctl, sign and attach the VEX document to the image manifest
(requires cosign credentials):
vexctl attest --attach --sign sbom.openvex.json \
my-registry.io/my-app@sha256:abc123Downstream consumers retrieve the attestation without a separate file transfer. The prioritization step can also read the VEX file directly from disk; image attachment is optional for local CI use.
Validation checklist:
@context is https://openvex.dev/ns/v0.2.0 (per vex-spec-md)not_affected statement has either justification or impact_statement populatedproducts[].@id purl matches the scanner's package identifier exactlyvulnerability.name is the canonical CVE-ID (not a GHSA alias) to match scanner outputversion integer has been incremented if the file was edited after initial generationnot_affected for a CVE in CISA KEV (will be rejected by the prioritization step)Quick structural check with jq:
jq '.statements[] | select(.status == "not_affected") |
select(.justification == null and .impact_statement == null) |
.vulnerability.name' sbom.openvex.json
# Returns CVE IDs that are missing justification - must be empty before handing offPer vexctl, apply a VEX document directly against a SARIF scan output to preview which findings would be suppressed:
vexctl filter scan_results.sarif.json sbom.openvex.jsonThe filtered output excludes findings whose CVE + product pair has a
not_affected statement. Use this to confirm suppression before committing
the file to the repository.
jobs:
vex-author:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: go install github.com/openvex/vexctl@latest
- name: Validate existing VEX document
run: |
jq '.statements[] |
select(.status == "not_affected") |
select(.justification == null and .impact_statement == null) |
.vulnerability.name' sbom.openvex.json \
| grep -q . && echo "FAIL: not_affected without justification" && exit 1 || true
- name: Attach to image
run: |
vexctl attest --attach --sign sbom.openvex.json \
${{ env.IMAGE_REF }}Pass sbom.openvex.json to the prioritization step via its vex_file input;
its VEX-filter path reads statements[].analysis.state mapped
from status.
Scenario: Grype reports CVE-2024-9999 against bash@5.1.16 in the image.
Analysis confirms the vulnerable SHELLOPTS=debug parser is never invoked -
SHELLOPTS is locked to privileged by the entrypoint script.
vexctl create \
--author="alice@example.com" \
--product="pkg:apk/alpine/bash@5.1.16-r2" \
--vuln="CVE-2024-9999" \
--status="not_affected" \
--justification="vulnerable_code_not_in_execute_path" \
> sbom.openvex.jsonPrioritization-step report section after ingestion:
### VEX-Filtered (surface for audit, not for action)
| CVE | Package | VEX status | Justification |
|---|---|---|---|
| CVE-2024-9999 | bash@5.1.16 | not_affected | vulnerable_code_not_in_execute_path |The finding is not in the Fix-Now or Fix-This-Sprint buckets and does not
block the build. It remains visible in the report for the audit trail.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
not_affected without justification or impact_statement | The prioritization step rejects unverified claims (Trust unverified VEX claims anti-pattern) | Add one of the five justification codes or write a precise impact_statement |
Asserting not_affected on a CISA KEV entry | The prioritization step hard-refuses; active exploitation cannot be hand-waved | Fix the vulnerability or apply a waiver per the step's waiver rules |
| Using a mismatched purl version | The affect.ref.endsWith() check will not match; finding stays in the fail bucket | Copy the exact package string from the scanner's ContainerFinding output |
Authoring VEX in a text editor without vexctl | Field names, timestamp format, and @context URL are easy to get wrong | Always generate with vexctl create and validate with jq (Step 7) |
| One monolithic VEX file maintained by one person | Merge conflicts; no ownership | Author per-team files, merge with vexctl merge before passing to CI |
not_affected claim is harder to detect than a false positive. Require
evidence (code path trace, test proof) before asserting, per the
prioritization step's own warning.vexctl create does not validate that the purl resolves to a real package;
typos in the --product flag produce silent mismatches. Cross-check against
Syft SBOM output.vexctl to a specific release in CI to avoid breaking changes.vexctl filter accepts SARIF and CSAF inputs; raw Grype JSON or Trivy JSON
require conversion before vexctl filter can process them. Use
the prioritization step's non-SARIF Python path for those formats.syft-generation - upstream SBOM generation
(produces the purl identifiers used in --product flags)