Skip to content

Prioritize a backlog of 300 CVEs

Who this is for: security engineers staring at a massive CVE report and asking "which ones do I patch first?". By the end of this page: you have a ranked list of the CVEs that actually matter for your system, and a suppressed view of the rest.

This is the most common use case for Vens. It takes ~3 minutes end to end.


The problem

A typical Trivy scan of a production image returns 150–500 CVEs. Most of them:

  • Are not reachable in your runtime
  • Exploit features you don't use
  • Are mitigated by your existing controls (WAF, network segmentation, read-only filesystem…)
  • Touch low-sensitivity data

Without context, every CVE looks equally urgent. You waste engineering time patching things that don't matter, and you miss the few that really do.


The workflow

┌────────────┐   ┌─────────┐   ┌──────────┐   ┌────────────┐   ┌──────────┐
│  trivy     │──▶│  vens   │──▶│  jq      │──▶│  patch     │──▶│  trivy   │
│  scan      │   │ generate│   │  sort    │   │  top N     │   │  --vex   │
└────────────┘   └─────────┘   └──────────┘   └────────────┘   └──────────┘

Step 1 — Scan

trivy image my-registry/my-app:v1.2.3 \
  --format json \
  --severity MEDIUM,HIGH,CRITICAL \
  --output report.json

Tip

Include MEDIUM severity. Vens will upgrade some of them to high if your context warrants it — this is where hidden risk often lives.


Step 2 — Describe your system once

Create config.yaml with the real characteristics of the workload:

project:
  name: "checkout-api"
  description: "Public-facing checkout service, handles payment card data"

context:
  exposure: "internet"
  data_sensitivity: "critical"       # payment card data
  business_criticality: "critical"   # revenue-critical
  availability_requirement: "high"
  compliance_requirements:
    - "PCI-DSS"
    - "GDPR"
  controls:
    waf: true
    ddos_protection: true
    segmentation: true
    siem: true
  notes: "Behind AWS ALB, WAF with OWASP ruleset, deployed across 3 AZs"

This file lives in your repo. You write it once, version it with the service, and re-use it on every scan. See Describe your context for every field.


Step 3 — Generate the VEX

SBOM_UUID="urn:uuid:$(uuidgen | tr '[:upper:]' '[:lower:]')"

vens generate \
  --config-file config.yaml \
  --sbom-serial-number "$SBOM_UUID" \
  report.json \
  output.vex.json

--sbom-serial-number is required — it feeds the BOM-Link references in the VEX. Store the UUID alongside the service if you want stable BOM-Links across runs.

Runtime: ~1 minute for 300 CVEs with gpt-4o.


Step 4 — Get your top 10

jq '[.vulnerabilities[] | {
       id,
       score: .ratings[0].score,
       severity: .ratings[0].severity,
       vector: .ratings[0].vector
     }]
    | sort_by(-.score)
    | .[0:10]' output.vex.json

Example output:

[
  {
    "id": "CVE-XXXX-YYYY",
    "score": 64,
    "severity": "critical",
    "vector": "SL:8/M:8/O:8/S:8/ED:7/EE:7/A:7/ID:3/LC:9/LI:9/LAV:9/LAC:9/FD:9/RD:9/NC:9/PV:9"
  },
  {
    "id": "CVE-2025-4477",
    "score": 56,
    "severity": "high",
    "vector": "SL:7/M:7/O:7/S:7/ED:7/EE:7/A:7/ID:3/LC:8/LI:8/LAV:8/LAC:8/FD:8/RD:8/NC:8/PV:8"
  }
]

These are the CVEs you patch first. Everything below a threshold (e.g. score < 20) can be deferred or suppressed.

Where is the reasoning?

Vens prints the LLM's per-CVE reasoning to stderr as it runs, and dumps every prompt and response to disk if you pass --debug-dir ./debug. The reasoning is intentionally not embedded in the VEX file — VEX stays a pure CycloneDX document. Use --debug-dir for audits.


Step 5 — Feed the VEX back to your scanner / platform

Point Trivy at the VEX so CVEs are displayed with their contextual OWASP rating alongside the native scanner severity:

trivy image my-registry/my-app:v1.2.3 \
  --vex output.vex.json \
  --show-suppressed

Grype supports --vex as well. Dependency-Track can ingest the VEX file directly.

Note

Vens emits OWASP ratings on every CVE — not a not_affected analysis state. To suppress CVEs below a contextual risk threshold, filter the VEX with jq as shown in Put it in CI below, or pre-process the VEX in your dashboard to drop low-score entries.


Put it in CI

For a turnkey Action, see GitHub Actions integration. The manual approach below works too:

- name: Install Vens
  run: go install github.com/venslabs/vens/cmd/vens@v0.3.2

- name: Scan image
  run: trivy image $IMAGE --format json --output report.json

- name: Generate contextual VEX
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
    OPENAI_MODEL: gpt-4o               # required alongside OPENAI_API_KEY
    # Stable per-service UUID so BOM-Links don't churn between builds.
    # Store it as a repo variable instead of hardcoding once you have more than one service.
    SBOM_UUID: urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79
  run: |
    vens generate \
      --config-file .vens/config.yaml \
      --sbom-serial-number "$SBOM_UUID" \
      report.json vex.json

- name: Fail on high contextual risk
  run: |
    HIGH_COUNT=$(jq '[.vulnerabilities[] | select(.ratings[0].score >= 40)] | length' vex.json)
    if [ "$HIGH_COUNT" -gt 0 ]; then
      echo "::error::$HIGH_COUNT CVEs above contextual threshold"
      jq '[.vulnerabilities[] | select(.ratings[0].score >= 40) | {id, score: .ratings[0].score}]' vex.json
      exit 1
    fi

- name: Upload VEX
  uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
  with:
    name: vex
    path: vex.json

Air-gapped CI

Swap the Install Vens step for a prebuilt binary download (see Installation) and replace the OpenAI env vars with OLLAMA_MODEL + OLLAMA_HOST pointing at your internal Ollama server. Nothing else in the pipeline changes.

This fails the build only when there is real risk, not when the scanner finds a CVSS 7 in a component you don't execute.


What this changes

Without Vens With Vens
Engineering time per release hours triaging 300 CVEs minutes reviewing top 10
CI failures flaky (every new CVE blocks) meaningful (only real risk blocks)
Backlog always growing stays manageable
Developer trust "the scanner is noise" "this score means something"

Next