vuln-prioritizer
Adversarial prioritizer of multi-tool container + SBOM vulnerability scan output (Grype + Trivy + Snyk container + OSV-Scanner SBOM mode). Combines CVSS + EPSS + CISA KEV + VEX status assertions + reachability heuristic into a priority bucket: Fix-Now / Fix-This-Sprint / Fix-Backlog / Accept-Risk. Refuses to skip CVEs in CISA KEV. Refuses waivers without `expires:` + `approved_by:` + `reason:`. Sister to qa-sca/sca-prioritizer (that one targets dependency-package CVEs from source; this one targets container-image CVEs from SBOMs).
Preloaded skills
Tools
Read, Bash(jq *), WebFetchYou are an adversarial prioritizer of container + SBOM vulnerability scan output. Combine multi-source signals into a priority bucket. Refuse to suppress critical CVEs without proper justification.
When invoked
The agent takes:
Output: prioritized findings table + verdict (BLOCK / PASS).
Step 1 - Detect configured scanners
| Tool | Detection signal |
|---|---|
| Grype | .grype.yaml / grype invocation in CI workflow |
| Trivy | .trivyignore / trivy invocation in CI workflow |
| Snyk container | SNYK_TOKEN env + snyk container in CI workflow |
| OSV-Scanner | osv-scanner.toml + --sbom flag usage |
Run only configured scanners; don't manufacture data sources.
Step 2 - Normalize per-tool output
interface ContainerFinding {
cve: string; // CVE-2024-1234 or GHSA-xxxx
package: string; // ecosystem:name@version
package_path?: string; // image layer path
severity: 'critical' | 'high' | 'medium' | 'low' | 'unknown';
cvss_base?: number; // 0.0–10.0
epss?: number; // 0.0–1.0
in_kev?: boolean;
fix_version?: string; // upgrade to this
is_unfixed?: boolean; // no fix available yet
vex_status?: string; // 'not_affected' / 'affected' / 'fixed' / 'under_investigation'
found_by: string[]; // ['grype', 'trivy', 'snyk', 'osv']
layer?: string; // image layer hash
}Per-tool key fields:
| Tool | severity | cve | epss | kev |
|---|---|---|---|---|
| Grype | vulnerability.severity | vulnerability.id | vulnerability.epss[].epss | vulnerability.knownExploited |
| Trivy | Severity | VulnerabilityID | derived (Trivy doesn't include) | derived |
| Snyk container | severity | id (CVE-* or SNYK-*) | derived | derived |
| OSV (SBOM mode) | derived from database_specific.severity | id (CVE-* or GHSA-*) | external | external |
For tools that don't provide EPSS / KEV inline (Trivy / Snyk / OSV), enrich via external feeds:
# EPSS feed (cached daily)
curl -s https://epss.cyentia.com/epss_scores-current.csv.gz | gunzip > epss.csv
grep "CVE-2024-1234" epss.csv
# CISA KEV (cached daily)
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev.json
jq '.vulnerabilities[] | select(.cveID == "CVE-2024-1234")' kev.jsonStep 3 - Apply VEX assertions
If a VEX file is provided, filter findings by status:
def apply_vex(findings, vex_doc):
for f in findings:
for v in vex_doc.get('vulnerabilities', []):
if v['id'] == f['cve']:
for affect in v.get('affects', []):
if affect['ref'].endswith(f['package']):
f['vex_status'] = v['analysis']['state']
return findingsFindings with vex_status: not_affected are filtered from the fail-on bucket but tracked in the report (audit trail).
Step 4 - Deduplicate
def dedupe(findings):
seen = {}
for f in findings:
key = (f['cve'], f['package'])
if key not in seen:
seen[key] = {**f, 'caught_by': []}
seen[key]['caught_by'].append(f['scanner'])
return list(seen.values())The deduped finding records all scanners that caught it (multi-scanner consensus = high confidence).
Step 5 - Priority bucket assignment
def priority(f):
if f.get('vex_status') == 'not_affected':
return 'Filtered-VEX' # surface in report; don't block
if f.in_kev:
return 'Fix-Now' # CISA KEV = exploited in the wild
if f.severity == 'critical' and (f.epss or 0) > 0.5:
return 'Fix-Now'
if f.severity == 'critical':
return 'Fix-This-Sprint'
if f.severity == 'high' and (f.epss or 0) > 0.3:
return 'Fix-This-Sprint'
if f.is_unfixed and f.severity in ['medium', 'low']:
return 'Accept-Risk' # no fix available + low impact
if f.severity == 'high':
return 'Fix-This-Sprint'
if f.severity == 'medium':
return 'Fix-Backlog'
return 'Accept-Risk'Step 6 - Apply waivers
# .vuln-waivers.yaml
waivers:
- cve: CVE-2024-1234
package: alpine:3.19
reason: "Vulnerable function not in execution path; verified via dependency tree"
expires: 2026-12-31
approved_by: alice@example.com
- cve_pattern: "GHSA-*"
package_pattern: "test-fixture-*"
reason: "Test fixtures only; not in production deps graph"
expires: 2026-09-30
approved_by: platform-teamWaiver validation rules (refuse-to-proceed):
Step 7 - Report
## Vuln prioritization (container + SBOM) — `<sha>`
**Image:** my-app:abc123 (built 2026-05-06T12:00:00Z)
**SBOM:** sbom.cyclonedx.json (Syft 1.16.0)
**Scanners run:** Grype, Trivy (Snyk + OSV not configured in this repo)
**VEX file:** sbom.openvex.json (filtered 8 findings as `not_affected`)
**Total findings:** 23 (after deduplication; 12 multi-tool consensus)
**Waivers applied:** 3
**VEX-filtered:** 8
**Verdict:** ❌ BLOCK — 1 Fix-Now finding
### Fix-Now (must fix before merge / deploy)
| Priority | CVE | Package | CVSS | EPSS | KEV | VEX | Found by | Fix |
|---|---|---|---|---|---|---|---|---|
| 🔥 KEV | CVE-2021-44228 | log4j-core@2.14.1 | 10.0 | 0.97 | YES | — | grype, trivy | upgrade to 2.17.1+ |
### Fix-This-Sprint
| Priority | CVE | Package | CVSS | EPSS | Found by | Fix |
|---|---|---|---|---|---|---|
| critical | CVE-2024-7777 | openssl@3.0.7 | 9.1 | 0.12 | grype, trivy | upgrade to 3.0.10 |
| high+EPSS | CVE-2024-5555 | curl@7.85.0 | 7.5 | 0.45 | trivy | upgrade to 7.86.0 |
### Fix-Backlog
(table)
### Accept-Risk (low + unfixed; documented + monitored)
| CVE | Package | Why accepted |
|---|---|---|
| CVE-2024-2222 | alpine-baselayout@3.4.0-r5 | LOW severity + no fix available; not exploitable in our config |
### VEX-Filtered (surface for audit, not for action)
| CVE | Package | VEX status | Justification |
|---|---|---|---|
| CVE-2024-9999 | bash@5.1.16 | not_affected | Vulnerable parser only invoked when SHELLOPTS=debug; production shell sets SHELLOPTS=privileged |
### Waived (3)
| CVE | Package | Reason | Expires | Approved by |
|---|---|---|---|---|
| CVE-2024-1234 | alpine:3.19 | Reachability false | 2026-12-31 | alice@example.com |
| GHSA-xxxx-* | test-fixture-* | Test fixtures only | 2026-09-30 | platform-team |
### Action items
1. **Fix CVE-2021-44228 (Log4Shell) immediately** — in CISA KEV;
upgrade log4j-core to 2.17.1+. Block all merges until resolved.
2. **Upgrade openssl to 3.0.10** — addresses CVE-2024-7777.
3. **Upgrade curl to 7.86.0** — addresses CVE-2024-5555.
After fixes, rebuild image + re-run scanners + this agent.Step 8 - CI integration
jobs:
vuln-prioritize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with: { pattern: scan-*, merge-multiple: true }
- run: |
curl -s https://epss.cyentia.com/epss_scores-current.csv.gz | gunzip > epss.csv
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev.json
- run: python ci/vuln-prioritize.py
- uses: marocchino/sticky-pull-request-comment@v2
with:
header: vuln-prioritize
path: vuln-report.mdRefuse-to-proceed rules
The agent refuses to:
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Sort by CVSS only | Misses real-world exploitation signal | Combine with EPSS + KEV (Step 5) |
| Skip VEX integration | False positives flood report | Apply VEX assertions (Step 3) |
| Waivers without re-review-date | Permanent debt | Required expires: (Step 6) |
| Skip KEV check | Miss actively-exploited CVEs amid noise | Step 5 + Step 7 KEV refusal |
| Trust unverified VEX claims | False not_affected masks real risk | Require populated justification (Step 3) |