Gate CI with CISA KEV JSON: Ship Safer Builds
If you’re already generating SBOMs, you’re a 10-minute script away from turning CISA KEV JSON into a hard gate in CI/CD. The latest KEV additions—like the Chrome V8 type confusion vulnerability (CVE-2025-10585)—show how fast browser/JS engines move. Your pipeline should block risky versions on sight, not “note them for later.” (CISA added CVE-2025-10585 to KEV on Sept 23, 2025; use it as your working example.)
Explore more supply-chain security posts on the Cyber Rely blog → and our deep-dive on CVE-2025-10585.
What we’ll build
A practical CI policy that:
- Pulls CISA KEV JSON and builds a CVE allow/deny set
- Reads your CycloneDX SBOM (generated with Syft)
- Cross-checks components via the NVD 2.0 API using the
hasKev
filter - Fails builds with a clear override path (break-glass label)
- Auto-opens PRs to bump versions and notifies code owners
- Tracks Mean Time To Remediate (MTTR) across repos
Screenshot of our free Website Vulnerability Scanner homepage
1) Why KEV should be a hard gate—using V8 as a live example
JavaScript engines are relentlessly targeted. When V8 lands in KEV, assume real-world exploitation pressure and treat it as an immediate stop-ship for any artifact bundling that engine (or embedding a browser runtime). For engineering leaders, KEV is not a “nice-to-patch”—it’s the one list you must gate on. (CVE-2025-10585 is explicitly in KEV.)
For context and internal reading: our quick brief on Git CVE-2025-48384: Safe Submodules in Practice.
2) Pull the data: CISA KEV JSON + schema
CISA publishes the KEV Catalog as JSON (and a JSON schema). We’ll fetch that feed at build time and keep it cached for local dev.
scripts/kev_pull.py
#!/usr/bin/env python3
"""
Fetch CISA KEV JSON, build fast lookup sets, and write to disk.
Usage:
python scripts/kev_pull.py --out .cache/kev.json
"""
import argparse, json, os, sys, time, hashlib
from urllib.request import urlopen, Request
KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=".cache/kev.json")
args = ap.parse_args()
os.makedirs(os.path.dirname(args.out), exist_ok=True)
req = Request(KEV_URL, headers={"User-Agent":"CI-KEV-Gate/1.0"})
with urlopen(req, timeout=30) as r:
raw = r.read()
data = json.loads(raw)
kev_items = data.get("vulnerabilities", data.get("catalogItems", [])) # schema-tolerant
cve_set = {item.get("cveID") or item.get("cve_id") for item in kev_items}
cve_set = {c for c in cve_set if c}
out = {
"fetched_at": int(time.time()),
"kev_count": len(cve_set),
"cves": sorted(cve_set),
"hash": hashlib.sha256(raw).hexdigest()
}
with open(args.out, "w") as f:
json.dump(out, f, indent=2)
print(f"[KEV] saved {len(cve_set)} CVEs to {args.out}")
if __name__ == "__main__":
sys.exit(main())
3) Map to your inventory: generate a CycloneDX SBOM
We’ll use Syft to emit a CycloneDX JSON SBOM for your repo/container image.
Generate SBOM (Syft)
# Install Syft (Linux/macOS)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b ./bin
# For source code (package manifests)
./bin/syft dir:. -o cyclonedx-json > sbom.json
# For a container image
./bin/syft docker:ghcr.io/your/app:latest -o cyclonedx-json > sbom.json
Parse CycloneDX components in Python
# scripts/sbom_read.py
import json, re
def load_components(path="sbom.json"):
with open(path) as f:
sbom = json.load(f)
comps = sbom.get("components", [])
out = []
for c in comps:
out.append({
"name": c.get("name"),
"version": c.get("version"),
"type": c.get("type"),
"purl": c.get("purl"),
"cpe": (c.get("properties") or {}).get("cpe", None) # syft sometimes sets in properties
})
return out
4) Enrich and enforce: NVD API hasKev
filter
The NVD 2.0 CVE API exposes a hasKev
filter to return only CVEs that appear in CISA KEV—perfect for confirmation and details (CVSS, descriptions). We’ll query per component (prefer CPE name if available; else keyword fallback).
scripts/kev_gate.py
#!/usr/bin/env python3
"""
Cross-check CycloneDX components against KEV using NVD 2.0 API hasKev.
Fail (exit 1) on any match unless override requested.
"""
import os, sys, json, urllib.parse, time
from scripts.sbom_read import load_components
NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0"
NVD_KEY = os.getenv("NVD_API_KEY") # optional, improves rate limits
def nvd_qs(params):
qp = urllib.parse.urlencode(params, doseq=True)
return f"{NVD_BASE}?{qp}"
def fetch(url):
import urllib.request, json
req = urllib.request.Request(url, headers={
"User-Agent":"CI-KEV-Gate/1.0",
**({"apiKey":NVD_KEY} if NVD_KEY else {})
})
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def search_hasKev_for_comp(comp):
# Prefer CPE
if comp.get("cpe"):
url = nvd_qs({"hasKev": "", "cpeName": comp["cpe"], "noRejected": ""})
return fetch(url)
# Fallback: keyword search on name + version (may produce false positives)
kw = f'{comp["name"]} {comp.get("version","")}'.strip()
url = nvd_qs({"hasKev": "", "keywordSearch": kw, "noRejected": ""})
return fetch(url)
def main():
override = os.getenv("KEV_OVERRIDE", "false").lower() in ("1","true","yes")
comps = load_components("sbom.json")
matches = []
for c in comps:
if not c.get("name"):
continue
try:
data = search_hasKev_for_comp(c)
total = int(data.get("totalResults", 0))
if total > 0:
for item in data.get("vulnerabilities", []):
cve = item["cve"]["id"]
matches.append({
"component": c, "cve": cve,
"summary": item["cve"]["descriptions"][0]["value"]
})
except Exception as e:
print(f"[warn] NVD query failed for {c.get('name')}: {e}")
if matches:
print("=== KEV hits ===")
for m in matches:
print(f"- {m['cve']} on {m['component']['name']}@{m['component'].get('version')}: {m['summary'][:140]}")
if override:
print("[override] KEV hits found but KEV_OVERRIDE=true; continuing.")
sys.exit(0)
else:
print("[block] Failing build due to KEV matches. Set KEV_OVERRIDE=true to break-glass.")
sys.exit(1)
print("[ok] No KEV matches detected.")
return 0
if __name__ == "__main__":
sys.exit(main())
5) Wire it into GitHub Actions (with break-glass + owner notifications)
.github/workflows/kev-gate.yml
name: KEV Gate
on:
pull_request:
workflow_dispatch:
jobs:
gate:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b ./bin
echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
- name: Generate SBOM (CycloneDX)
run: syft dir:. -o cyclonedx-json > sbom.json
- name: Pull CISA KEV JSON
run: python scripts/kev_pull.py --out .cache/kev.json
- name: Check PR labels for break-glass
id: labels
if: github.event_name == 'pull_request'
run: |
echo "override=false" >> $GITHUB_OUTPUT
echo "${{ toJson(github.event.pull_request.labels) }}" | jq -r '.[].name' | grep -qi '^kev-override$' && echo "override=true" >> $GITHUB_OUTPUT || true
- name: Enforce KEV gate (NVD hasKev)
env:
KEV_OVERRIDE: ${{ steps.labels.outputs.override }}
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
run: python scripts/kev_gate.py
- name: Request review from CODEOWNERS on failure
if: failure() && github.event_name == 'pull_request'
run: |
echo "Requesting review from code owners..."
gh pr edit ${{ github.event.pull_request.number }} --add-reviewer @me --add-label "kev-blocked"
env:
GH_TOKEN: ${{ github.token }}
6) GitLab CI/CD variant
.gitlab-ci.yml
stages: [sbom, gate]
sbom:
stage: sbom
image: alpine:latest
script:
- apk add --no-cache curl bash jq python3
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b ./bin
- ./bin/syft dir:. -o cyclonedx-json > sbom.json
artifacts:
paths: [sbom.json]
kev_gate:
stage: gate
image: python:3.11-slim
variables:
NVD_API_KEY: $NVD_API_KEY
KEV_OVERRIDE: $KEV_OVERRIDE
script:
- pip install requests
- python scripts/kev_pull.py --out .cache/kev.json
- python scripts/kev_gate.py
7) Close the loop: auto-PRs, notify owners, track MTTR
When the gate fails, you can kick off an auto-remediation PR that bumps the offending dependency, tags the right owners, and files an issue to track MTTR.
Auto-open a PR (Node.js example)
# Add after the "Enforce KEV gate" step, but gated to run on failure
- name: Auto-bump risky packages and open PR
if: failure() && github.event_name == 'pull_request'
run: |
npm i -g npm-check-updates
ncu -u || true
npm install || true
BR="kev/fix-$(date +%s)"
git checkout -b "$BR"
git commit -am "chore(deps): bump to address CISA KEV JSON hits"
git push -u origin "$BR"
gh pr create --head "$BR" --title "Fix: bump vulnerable deps (KEV)" \
--body "Automated bump due to KEV-flagged CVEs found by CI. See workflow logs."
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Create an issue for MTTR tracking
gh issue create --title "KEV remediation: ${GITHUB_REPOSITORY}" \
--body "CI gate blocked due to CISA KEV JSON match. Track remediation here." \
--label "security,kev"
Notify code owners
gh pr edit $PR --add-reviewer "$(git log -1 --pretty=format:'%an')" --add-label "kev"
8) Worked example: catching CVE-2025-10585 (V8)
Suppose your SBOM includes a Chromium runtime for end-to-end tests or bundling. The gate will:
- Pull CISA KEV JSON (which includes CVE-2025-10585)
- Query NVD with
hasKev
+keywordSearch=chromium v8
(or a component CPE if present) - Match → fail → request review and open a bump PR
That’s the right default for a Known Exploited browser engine bug.
Sample vulnerability report by the free scanner to check Website Vulnerability
Where to go next (tools, services, and reading)
- Try our free Website Vulnerability Scanner to validate production targets after CI:
https://free.pentesttesting.com/
- Need help prioritizing and driving fixes across portfolios? Our Risk Assessment Services and Remediation Services turn scan noise into executive-ready action plans.
- More engineering-focused articles on the Cyber Rely blog, including our PyTorch Supply Chain Attack: Dev Guardrails post for your team brief.
Related reading on Cyber Rely
- CVE-2025-10585 Deep Dive (fast team brief)
- Browse all posts: https://www.cybersrely.com/blog/
Recommended next: npm ‘Shai-Hulud’ Worm: CI Fixes Now — a developer-first incident response guide to contain the npm supply chain attack 2025 with trusted publishing, provenance checks, and dependency freeze steps.
Read it here: https://www.cybersrely.com/npm-supply-chain-attack-2025/
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about Gate CI with CISA KEV JSON.