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.)

Gate CI with CISA KEV JSON: Ship Safer Builds

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:

  1. Pulls CISA KEV JSON and builds a CVE allow/deny set
  2. Reads your CycloneDX SBOM (generated with Syft)
  3. Cross-checks components via the NVD 2.0 API using the hasKev filter
  4. Fails builds with a clear override path (break-glass label)
  5. Auto-opens PRs to bump versions and notifies code owners
  6. Tracks Mean Time To Remediate (MTTR) across repos

Screenshot of our free Website Vulnerability Scanner homepage

Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.
Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.

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:

  1. Pull CISA KEV JSON (which includes CVE-2025-10585)
  2. Query NVD with hasKev + keywordSearch=chromium v8 (or a component CPE if present)
  3. 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

An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.
An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.

Where to go next (tools, services, and reading)


Related reading on Cyber Rely

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/


Free Consultation

If you have any questions or need expert assistance, feel free to schedule a Free consultation with one of our security engineers>>

🔐 Frequently Asked Questions (FAQs)

Find answers to commonly asked questions about Gate CI with CISA KEV JSON.

Get a Quote

Leave a Comment

Your email address will not be published. Required fields are marked *