CVE Patch Pipeline: Nuclei + KEV in 12 Steps [2026]
![CVE Patch Pipeline: Nuclei + KEV in 12 Steps [2026]](https://trendintech.com/wp-content/uploads/2026/10/wpshim-2551573-cve-patch-pipeline-nuclei-cisa-kev-tutorial-2026.webp)
Patch Tuesday in September 2026 landed 973 CVEs on security teams’ desks in a single release, 119 of them rated critical, according to Ivanti’s monthly breakdown. Two were already being exploited before Microsoft shipped the fix. That is the reality most IT teams live in now: too many vulnerabilities, too little time, and attackers who move faster than monthly patch cycles. The fix is not scanning more. It is scanning smarter, by cross-referencing what is actually exploitable against what attackers are actually exploiting right now.
This tutorial walks through building a patch-prioritization pipeline that merges two free data sources: the CISA Known Exploited Vulnerabilities (KEV) catalog and a live Nuclei scan of your own infrastructure. By the end, you will have a working Python and Bash pipeline that pulls the KEV feed, scans your assets with Nuclei templates, cross-matches the two, and spits out a ranked patch list, before ransomware operators get there first. Google Cloud’s 2026 M-Trends report found that prior compromise accounted for 30% of observed ransomware initial-infection vectors, which means a huge share of breaches trace back to a vulnerability that was already public knowledge and already weaponized.
What makes this approach different from the “run a scanner, read a PDF report” workflow most teams default to is that it treats vulnerability data as a stream, not a snapshot. CISA refreshes the KEV catalog multiple times a week. A pipeline that checks it once a quarter during an audit cycle is, by definition, always working from stale intelligence. Building this as an automated daily job rather than a manual task is the actual point of the exercise, not an optional nice-to-have tacked on at the end.
Don't miss new tech stories on Google
Add TrendinTech once in the Google app and our stories appear in your news suggestions.
Why CISA KEV plus Nuclei beats generic vulnerability scanning
Most vulnerability scanners generate noise. They flag thousands of CVEs ranked purely by CVSS score, and CVSS measures theoretical severity, not real-world exploitation. A 9.8 CVSS bug sitting unexploited in the wild for two years is less urgent than a 7.2 bug being actively weaponized by ransomware affiliates this week. CISA built the KEV catalog specifically to correct that mismatch: it only lists vulnerabilities with confirmed evidence of active exploitation, not theoretical risk. As CISA puts it, “the KEV catalog sends a clear message to all organizations to prioritize remediation efforts on the subset of vulnerabilities that are causing immediate harm based on adversary activity.”
Nuclei, the open source scanning engine from ProjectDiscovery, fills the other half of the gap. KEV tells you what is dangerous in theory; Nuclei tells you what is actually reachable in your environment. ProjectDiscovery describes Nuclei as “the application security testing framework” built for exactly this kind of fast, template-driven checking, and its community template repository on GitHub tracks new KEV entries within days of disclosure. Combine the two and you get a short list: vulnerabilities that are (a) confirmed under active attack and (b) confirmed present on your network. That is the list worth losing sleep over, and it is usually a fraction the size of a raw CVE export.
Where this fits next to NVD and vendor advisories
None of this replaces the National Vulnerability Database, maintained by NIST, which remains the canonical record of a CVE’s technical details, CVSS vectors, and affected configurations. The NVD is where you go to understand a vulnerability in depth once you know it matters. KEV is where you go to decide which vulnerabilities matter in the first place. Think of NVD as the encyclopedia and KEV as the breaking-news alert sitting on top of it, pointing at the handful of encyclopedia entries that are actively on fire right now.
Vendor advisories sit in between the two. Citrix, Cisco, and Fortinet all publish their own bulletins the moment a fix ships, often with more granular build-number detail than either NVD or KEV carries. A mature pipeline pulls from all three: KEV for urgency triage, NVD for technical depth, and vendor bulletins for the exact patched build number your change-management ticket needs to reference.
Recent weeks make the case for this approach bluntly. Citrix confirmed two critical NetScaler remote code execution flaws, CVE-2026-88771 and CVE-2026-88772, were being exploited in the wild before patches landed widely, a pattern tracked in near-real time by outlets like The Hacker News. A separate NetScaler bug, CVE-2026-88779, carries a CVSS of 8.7 and was added to KEV on October 4, 2026. Cisco’s Catalyst SD-WAN Manager vulnerability, CVE-2026-76504, scored a maximum-severity 9.8 and reportedly let an unauthenticated remote attacker drive the Manager API as if they were an admin, with no workaround available at disclosure. None of these needed a zero-day budget to catch, just a pipeline watching the KEV feed and checking it against live infrastructure.
What ties these three examples together is speed, not sophistication. In every case, the gap that mattered was not how clever the exploit was, it was how long the vulnerable system sat exposed between public disclosure and an organization actually confirming, on its own network, whether it was affected. A scanner that runs once a quarter cannot close that gap. A scanner that runs nightly against a target list cross-matched with yesterday’s KEV update can.
Prerequisites and environment versions
This build uses only free, open source tooling. You do not need an enterprise vulnerability management license to run it, though the same logic scales into one if you later adopt a commercial platform. Here is what to install before starting, with the versions this tutorial was tested against as of October 2026.
| Tool | Version used | Purpose | Install command |
|---|---|---|---|
| Nuclei | v3.5.x (latest stable) | Template-driven vulnerability scanner | go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest |
| Go | 1.23 or newer | Required to build Nuclei from source | Download from go.dev/dl |
| Python | 3.11 or newer | KEV fetch, cross-match, and reporting scripts | Pre-installed on most Linux distros |
| requests, pandas | latest via pip | HTTP calls and CSV/JSON handling | pip install requests pandas |
| jq | 1.7.x | Quick JSON inspection in the terminal | apt install jq / brew install jq |
| cron or systemd timer | n/a (OS-native) | Daily automated pipeline runs | Built into Linux |
You will also need read access to a list of your own externally-facing hosts (a simple text file of domains or IPs is fine to start), and permission to actively scan them. Nuclei sends real HTTP requests and protocol probes; running it against infrastructure you do not own or lack authorization to test is a policy and, in many jurisdictions, a legal problem. Treat this exactly like any other authorized security assessment: get sign-off, scope the targets, and log what you scan.
Step 1: Install and verify Nuclei
Nuclei ships as a single Go binary, which makes it painless to install on a scanner box, a CI runner, or even a laptop. If you already have Go configured, the install is one line.
# Install Nuclei directly via Go
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Confirm it installed and check the version
nuclei -version
# Pull the latest community templates (includes fresh KEV-tagged checks)
nuclei -update-templates
Expect output similar to this once the binary resolves correctly:
[INF] Nuclei Engine Version: v3.5.2
[INF] Nuclei Config Directory: /home/user/.config/nuclei
[INF] Templates Directory: /home/user/nuclei-templates
[INF] Successfully updated 7853 templates
If `nuclei` is not found after install, your Go bin path likely is not on your shell’s PATH. Add `export PATH=$PATH:$(go env GOPATH)/bin` to your shell profile and reopen the terminal.
Step 2: Build the CISA KEV fetcher script
CISA publishes the full KEV catalog as a public JSON feed, refreshed as new entries are added, with no API key or authentication required. This script pulls the current catalog and saves it locally so the rest of the pipeline can reference it without hitting the network every run.
# fetch_kev.py
import json
import requests
from datetime import datetime, timezone
KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
def fetch_kev_catalog():
headers = {"User-Agent": "Mozilla/5.0 (patch-pipeline-tutorial)"}
response = requests.get(KEV_URL, headers=headers, timeout=20)
response.raise_for_status()
data = response.json()
catalog = data.get("vulnerabilities", [])
print(f"Fetched {len(catalog)} entries from CISA KEV catalog")
print(f"Catalog version: {data.get('catalogVersion')}")
out_path = f"kev_catalog_{datetime.now(timezone.utc).strftime('%Y%m%d')}.json"
with open(out_path, "w") as f:
json.dump(catalog, f, indent=2)
return catalog, out_path
if __name__ == "__main__":
catalog, path = fetch_kev_catalog()
print(f"Saved to {path}")
Run it and check the output:
$ python3 fetch_kev.py
Fetched 1428 entries from CISA KEV catalog
Catalog version: 2026.10.08
Saved to kev_catalog_20261009.json
Each entry in that JSON file includes the CVE ID, vendor, product, a short description, the date CISA added it, and the required remediation deadline for U.S. federal agencies under Binding Operational Directive 22-01. Even if you are not a federal agency, that remediation deadline column is a useful de facto SLA benchmark: if CISA gave federal agencies three weeks to patch something, that is a reasonable ceiling for your own team too.
Understanding the fields inside a KEV catalog entry
Before writing any matching logic, it is worth knowing exactly what each KEV record contains, since the downstream scripts in this tutorial all key off specific JSON fields. Skipping this step is the single most common reason people’s cross-match scripts in Step 6 silently return zero results: they guess at field names instead of inspecting the actual payload.
Core identification fields
Every entry starts with `cveID`, the canonical identifier you will match against Nuclei template IDs. `vendorProject` and `product` identify the affected software in plain English, which is what the vendor-watch script in Step 8 filters on. `vulnerabilityName` gives a short human-readable label, and `shortDescription` expands on it with a sentence or two describing the flaw class, whether it is a buffer overflow, an authentication bypass, or a path traversal issue, for example.
Remediation urgency fields
`dateAdded` records when CISA confirmed active exploitation and added the entry, which is the timestamp the watch script diffs against. `dueDate` sets the remediation deadline, originally scoped to U.S. federal civilian agencies under Binding Operational Directive 22-01, but widely adopted across the industry as a practical SLA benchmark regardless of sector. `knownRansomwareCampaignUse` flags whether the CVE has documented ties to ransomware operations specifically, the field that drives the top-priority weighting in Step 10. `requiredAction` spells out, in CISA’s own words, exactly what remediation looks like, apply a vendor patch, implement mitigations, or discontinue use of the product entirely in rare cases.
| Field name | Type | Used by which script | Example value |
|---|---|---|---|
| cveID | string | prioritize.py, watch_kev.py | CVE-2026-88771 |
| vendorProject | string | watch_kev.py | Citrix |
| product | string | watch_kev.py, prioritize.py | NetScaler ADC and Gateway |
| dateAdded | date (YYYY-MM-DD) | watch_kev.py | 2026-10-02 |
| dueDate | date (YYYY-MM-DD) | risk_score.py | 2026-10-23 |
| knownRansomwareCampaignUse | string (Known/Unknown) | risk_score.py | Known |
| requiredAction | string | create_tickets.py (description field) | Apply updates per vendor instructions |
Step 3: Map KEV entries to Nuclei templates
Nuclei’s community template repository tags templates by CVE ID, which makes cross-referencing straightforward. Clone the templates repo if you have not already (the `-update-templates` command from Step 1 handles this into `~/nuclei-templates`), then grep for CVE coverage.
# Search installed Nuclei templates for a specific CVE
grep -rl "CVE-2026-88771" ~/nuclei-templates/
# Count how many CVEs in your KEV export have a matching Nuclei template
python3 - <<'EOF'
import json, subprocess, os
with open("kev_catalog_20261009.json") as f:
kev = json.load(f)
template_dir = os.path.expanduser("~/nuclei-templates")
matched = 0
unmatched = []
for entry in kev:
cve = entry["cveID"]
result = subprocess.run(
["grep", "-rl", cve, template_dir],
capture_output=True, text=True
)
if result.stdout.strip():
matched += 1
else:
unmatched.append(cve)
print(f"KEV entries with a Nuclei template: {matched} / {len(kev)}")
print(f"First 10 unmatched (may need custom templates): {unmatched[:10]}")
EOF
Typical coverage runs 25-40% of the full KEV catalog, since the catalog spans everything from decade-old Adobe Flash bugs to brand-new edge-device zero-days, and Nuclei's community focuses template effort on internet-facing software most likely to show up in real assessments. That gap is exactly why Step 9 covers writing a custom template for anything business-critical that is missing.
Step 4: Define your scan scope
Create a plain text target file listing every host, subdomain, or IP range you are authorized to scan. One target per line, no trailing slashes.
# targets.txt
https://app.yourcompany.com
https://vpn.yourcompany.com
https://mail.yourcompany.com
10.20.30.0/24
Keep this file under version control, separately from the rest of the pipeline, since it is effectively your attack surface inventory. Review it monthly; forgotten subdomains and shadow IT assets are consistently among the easiest entry points attackers find.
Step 5: Run a KEV-tagged Nuclei scan
Rather than running Nuclei's entire template library (which takes hours and produces a lot you do not need), filter to templates tagged with CVEs that appear in your fresh KEV export. Nuclei supports tag-based filtering natively.
# Run only KEV-relevant CVE templates against your target list
nuclei -l targets.txt \
-tags cve \
-severity critical,high \
-json-export scan_results.json \
-rate-limit 50 \
-timeout 10
# Narrow further to a specific set of CVE IDs pulled from your KEV match
nuclei -l targets.txt \
-template-id "CVE-2026-88771,CVE-2026-88772,CVE-2026-76504" \
-json-export priority_scan.json
A clean, no-findings run looks like this:
[INF] Current nuclei version: v3.5.2
[INF] Current nuclei-templates version: v10.2.1
[INF] New templates added: 42
[INF] Templates loaded for current scan: 214
[INF] Targets loaded for current scan: 4
[INF] Executing 214 signed templates against 4 host(s)
[INF] No results found. Better luck next time.
A match looks like this, and it means stop reading and go patch:
[CVE-2026-88771] [http] [critical] https://vpn.yourcompany.com [CVE-2026-88771]
matched-at: https://vpn.yourcompany.com/vpn/../vpns/cfg/smb.conf
extracted-results: ["NetScaler build 14.1-xx.xx"]
Step 6: Cross-match scan results with KEV deadlines
Now join the two JSON outputs: what Nuclei found live on your network, and what KEV says about urgency and remediation timelines for each CVE. This script produces the actual priority list.
# prioritize.py
import json
import pandas as pd
def load_nuclei_results(path):
findings = []
with open(path) as f:
for line in f:
if line.strip():
findings.append(json.loads(line))
return findings
def load_kev(path):
with open(path) as f:
return json.load(f)
def build_priority_list(nuclei_path, kev_path):
findings = load_nuclei_results(nuclei_path)
kev = load_kev(kev_path)
kev_by_cve = {entry["cveID"]: entry for entry in kev}
rows = []
for finding in findings:
template_id = finding.get("template-id", "")
host = finding.get("host", "")
cve = next((p for p in template_id.split("-") if p.startswith("CVE")), None)
cve_id = template_id if template_id.startswith("CVE-") else None
kev_entry = kev_by_cve.get(cve_id)
if kev_entry:
rows.append({
"host": host,
"cve": cve_id,
"vendor": kev_entry.get("vendorProject"),
"product": kev_entry.get("product"),
"ransomware_use": kev_entry.get("knownRansomwareCampaignUse", "Unknown"),
"due_date": kev_entry.get("dueDate"),
"severity": finding.get("info", {}).get("severity"),
})
df = pd.DataFrame(rows)
if not df.empty:
df = df.sort_values(by=["ransomware_use", "due_date"], ascending=[False, True])
return df
if __name__ == "__main__":
df = build_priority_list("priority_scan.json", "kev_catalog_20261009.json")
df.to_csv("patch_priority_list.csv", index=False)
print(df.to_string(index=False))
The output sorts confirmed-ransomware-linked CVEs to the top, then orders the rest by how close the KEV due date is. That single CSV becomes your patching team's daily to-do list, replacing the usual spreadsheet of a few thousand undifferentiated CVSS scores.
Step 7: Automate the pipeline with a daily cron job
CISA adds new entries to KEV multiple times a week, sometimes multiple times a day during active incident response. A pipeline that only runs manually will miss the window that matters most, the first 48 hours after a CVE gets weaponized. Wire the whole thing into cron.
# run_pipeline.sh
#!/bin/bash
set -e
cd /opt/patch-pipeline
echo "[$(date)] Fetching latest KEV catalog..."
python3 fetch_kev.py
echo "[$(date)] Running Nuclei scan against targets.txt..."
nuclei -l targets.txt -tags cve -severity critical,high \
-json-export priority_scan.json -rate-limit 50
echo "[$(date)] Cross-matching results..."
python3 prioritize.py
echo "[$(date)] Pipeline complete. Results in patch_priority_list.csv"
# Add to crontab (runs daily at 6:00 AM)
crontab -e
# Add this line:
0 6 * * * /opt/patch-pipeline/run_pipeline.sh >> /var/log/patch-pipeline.log 2>&1
Check `/var/log/patch-pipeline.log` each morning, or better, pipe the final CSV into whatever chat tool your team already watches (Slack webhook, Teams webhook, or a simple email via `mail` or `sendmail`) so the list lands in front of humans without anyone needing to remember to look.
Step 8: Alert on new KEV entries matching your stack
Beyond the daily scan, you want an immediate alert the moment CISA adds a CVE for software you actually run, even before Nuclei has a template for it. This script diffs today's KEV catalog against yesterday's and flags new entries matching a vendor list you maintain.
# watch_kev.py
import json
import glob
YOUR_VENDORS = ["Citrix", "Fortinet", "Cisco", "Microsoft", "Google", "Ivanti", "Adobe"]
def diff_kev_catalogs():
files = sorted(glob.glob("kev_catalog_*.json"))
if len(files) < 2:
print("Need at least two days of catalog snapshots to diff.")
return
with open(files[-2]) as f:
yesterday = {e["cveID"] for e in json.load(f)}
with open(files[-1]) as f:
today = json.load(f)
new_entries = [e for e in today if e["cveID"] not in yesterday]
relevant = [e for e in new_entries if e["vendorProject"] in YOUR_VENDORS]
if relevant:
print(f"ALERT: {len(relevant)} new KEV entries affect your tracked vendors:")
for e in relevant:
print(f" - {e['cveID']} ({e['vendorProject']} {e['product']}) due {e['dueDate']}")
else:
print(f"{len(new_entries)} new KEV entries today, none match tracked vendors.")
if __name__ == "__main__":
diff_kev_catalogs()
Run this right after `fetch_kev.py` in your cron job, before the Nuclei scan. If it fires an alert, you already know to prioritize that vendor's assets in today's scan rather than waiting for the full nightly run.
Step 9: Write a custom Nuclei template for uncovered CVEs
When a KEV entry has no existing community template, usually because it is brand new or affects niche internal software, you can write a minimal YAML template yourself. Here is a basic HTTP-based template checking for a vulnerable version string in a response header, a common pattern for confirming unpatched software without attempting exploitation.
# custom-templates/cve-2026-check.yaml
id: CVE-2026-XXXXX-version-check
info:
name: Vulnerable Version Detection
author: your-security-team
severity: critical
description: Flags hosts running an unpatched version susceptible to CVE-2026-XXXXX
tags: cve,kev,custom
http:
- method: GET
path:
- "{{BaseURL}}/"
matchers-condition: and
matchers:
- type: word
part: header
words:
- "Server: VulnApp/2.3"
case-insensitive: true
- type: status
status:
- 200
# Validate the template syntax before running it
nuclei -validate -t custom-templates/cve-2026-check.yaml
# Run it against your target list
nuclei -l targets.txt -t custom-templates/cve-2026-check.yaml
Keep custom templates in version control alongside the pipeline, and contribute genuinely useful ones back to the Nuclei templates project so the wider community benefits the next time that CVE shows up elsewhere.
Step 10: Build the ransomware-risk weighting layer
Not every KEV entry carries equal weight. The catalog includes a `knownRansomwareCampaignUse` field flagging CVEs with documented links to ransomware operations, which deserves to outrank everything else regardless of CVSS. Extend the prioritization script to assign numeric risk scores instead of simple sorting, so you can feed the output into a ticketing system with meaningful priority labels.
# risk_score.py
import pandas as pd
from datetime import datetime
def calculate_risk_score(row):
score = 0
if str(row.get("ransomware_use", "")).lower() == "known":
score += 100
severity_weights = {"critical": 40, "high": 25, "medium": 10, "low": 5}
score += severity_weights.get(str(row.get("severity", "")).lower(), 0)
try:
due = datetime.strptime(row["due_date"], "%Y-%m-%d")
days_left = (due - datetime.now()).days
if days_left < 0:
score += 50
elif days_left < 7:
score += 30
elif days_left < 21:
score += 15
except (ValueError, TypeError):
pass
return score
df = pd.read_csv("patch_priority_list.csv")
df["risk_score"] = df.apply(calculate_risk_score, axis=1)
df = df.sort_values(by="risk_score", ascending=False)
df.to_csv("patch_priority_ranked.csv", index=False)
print(df[["cve", "host", "risk_score", "ransomware_use", "due_date"]].to_string(index=False))
A finding with confirmed ransomware use, critical severity, and a due date already in the past scores 190+, which should trip an emergency-patch SLA rather than sitting in a normal sprint backlog. That three-tier logic, ransomware linkage first, severity second, deadline urgency third, mirrors how CISA itself frames KEV usage for federal agencies and translates well to private-sector patch SLAs.
Step 11: Wire results into your ticketing system
A CSV on a scanner box does nothing if nobody opens a ticket. Most ticketing platforms (Jira, ServiceNow, Linear) expose a REST API that accepts issue creation. Here is a generic example posting high-risk findings to a webhook-style endpoint; adapt the payload shape to your specific platform's API docs.
# create_tickets.py
import pandas as pd
import requests
import os
TICKET_API_URL = os.environ["TICKET_API_URL"]
TICKET_API_TOKEN = os.environ["TICKET_API_TOKEN"]
df = pd.read_csv("patch_priority_ranked.csv")
high_risk = df[df["risk_score"] >= 80]
for _, row in high_risk.iterrows():
payload = {
"title": f"[PATCH] {row['cve']} on {row['host']} - risk {row['risk_score']}",
"description": (
f"Vendor: {row['vendor']} / {row['product']}\n"
f"Ransomware use confirmed: {row['ransomware_use']}\n"
f"KEV due date: {row['due_date']}\n"
f"Detected via Nuclei + CISA KEV pipeline."
),
"priority": "urgent" if row["risk_score"] >= 130 else "high",
"labels": ["security", "kev", "patch-required"],
}
resp = requests.post(
TICKET_API_URL,
json=payload,
headers={"Authorization": f"Bearer {TICKET_API_TOKEN}"},
timeout=15,
)
print(f"{row['cve']} -> ticket created, status {resp.status_code}")
Store credentials as environment variables or in a secrets manager, never hardcoded in the script. This is also the point where a lot of pipelines quietly fail: an expired token means tickets silently stop being created while the cron job still reports "success" because the scan itself ran fine.
Step 12: Validate remediation with a rescan
Closing a ticket is not the same as confirming a fix. Build a verification step that re-runs the specific Nuclei template against the specific host once a patch ticket is marked resolved, and only then marks the finding as closed in your tracking sheet.
# verify_patch.sh
#!/bin/bash
CVE=$1
HOST=$2
echo "Re-scanning $HOST for $CVE..."
nuclei -u "$HOST" -template-id "$CVE" -json-export "verify_${CVE}.json"
if [ -s "verify_${CVE}.json" ]; then
echo "STILL VULNERABLE: $CVE on $HOST - do not close ticket"
exit 1
else
echo "CONFIRMED PATCHED: $CVE on $HOST"
exit 0
fi
Run this as a required check before a ticket can transition to "closed" in your workflow, not as an optional afterthought. Patches fail silently more often than most teams expect, config rollbacks, load balancers serving a mix of patched and unpatched instances, and CDN caching all produce false confidence that something is fixed when it is not.
Common pitfalls when building this pipeline
A handful of mistakes show up repeatedly in home-grown vulnerability pipelines. Most of them are not exotic engineering failures, they are the same small oversights that cause problems in any scheduled automation: an unchecked edge case, a credential nobody rotated, a path that only exists on the engineer's own laptop. Catching them early saves a lot of debugging later, and several of them are the direct reason a pipeline quietly stops working weeks after it was first built and tested.
- Scanning without authorization. Nuclei actively probes targets; running it against infrastructure outside your documented scope, including third-party SaaS tools your company merely uses, can violate terms of service or the law. Keep targets.txt scoped to assets you own or have written permission to test.
- Treating CVSS and KEV presence as the same signal. A CVE can be CVSS 9.8 and never appear in KEV, meaning no confirmed active exploitation. Conflating the two inflates your priority list with noise and defeats the purpose of this pipeline.
- Skipping rate limiting. Running Nuclei at full speed against production systems can trigger WAF blocks, degrade service, or get your scanner IP blacklisted. The `-rate-limit` flag exists for a reason; use it.
- Letting the KEV catalog snapshot go stale. If `fetch_kev.py` fails silently (network timeout, changed URL, rate limiting from CISA's CDN) and nobody notices, you are scanning against yesterday's threat list while believing it is current. Add a freshness check that alerts if the catalog file is more than 24 hours old.
- Assuming template coverage equals full coverage. As Step 3 showed, only a fraction of KEV entries have ready-made Nuclei templates. A clean scan result does not mean you are not vulnerable to the untested majority; it means Nuclei did not check for them.
- Hardcoding target lists that never get reviewed. Infrastructure changes faster than most target.txt files get updated. Decommissioned hosts waste scan time; new hosts go unscanned entirely. Tie target discovery into your asset inventory or CMDB if you have one.
- No rescan-to-verify step. As covered in Step 12, a closed ticket is not proof of a fix. Pipelines that stop at "ticket created" rather than "ticket verified closed" tend to accumulate silent regressions.
Troubleshooting guide
Here are the issues that come up most often once this pipeline runs on a schedule rather than as a one-off experiment.
| Symptom | Likely cause | Fix |
|---|---|---|
| Nuclei command not found after install | Go bin directory not on PATH | Add `export PATH=$PATH:$(go env GOPATH)/bin` to your shell profile |
| fetch_kev.py returns HTTP 403 | Missing or blocked User-Agent header, or CDN-level bot filtering | Set a realistic User-Agent string; add retry with backoff; confirm your egress IP is not on a shared blocklist |
| Nuclei scan hangs or runs for hours | No rate limit set, large target list, or slow/unresponsive hosts | Add `-rate-limit 50 -timeout 10` and split large target lists into batches |
| Cross-match script shows zero matches despite known vulnerable hosts | Template ID in Nuclei output does not match the CVE ID format used in KEV JSON | Print both ID formats and normalize casing/prefixes before comparing |
| Cron job runs but produces no output | Script references relative paths that do not resolve outside an interactive shell | Use absolute paths everywhere and set an explicit `cd` at the top of the shell script |
| Nuclei flags a false positive on a patched host | Template matches a version string in a header that was not fully updated, or a CDN/proxy caching an old response | Add a cache-busting query parameter and manually confirm the live software version |
| Ticket creation script returns 401 Unauthorized | API token expired or was rotated without updating the environment variable | Rotate and re-store the token in your secrets manager; add a token-expiry alert |
| KEV catalog file size suddenly drops to near zero | CISA restructured the feed schema or temporarily serves a partial response | Add a sanity check requiring at least 500 entries before overwriting yesterday's snapshot |
| Custom template fails validation | YAML indentation error or missing required `matchers-condition` field | Run `nuclei -validate -t yourfile.yaml` and fix the specific line it reports |
Advanced tips for scaling the pipeline
Once the basic pipeline runs reliably, a few extensions make it genuinely useful at larger scale. First, feed EPSS scores alongside KEV status. The Exploit Prediction Scoring System, maintained by FIRST.org, assigns a probability that a given CVE will be exploited in the next 30 days, which helps you triage the huge majority of CVEs that never make it into KEV at all but still carry meaningful risk. Pulling the EPSS API into your risk_score.py logic from Step 10 turns a binary KEV/not-KEV signal into a continuous risk gradient, and it is free to query with no authentication required, much like the KEV feed itself.
Second, containerize the pipeline. Running `fetch_kev.py`, Nuclei, and the cross-match scripts inside a scheduled container (Kubernetes CronJob, or a simple Docker Compose service with a cron sidecar) makes the whole thing portable across environments and easier to hand off to a platform team instead of living on one engineer's laptop.
Third, track mean time to remediate (MTTR) per severity tier over time. Once you have a few months of patch_priority_ranked.csv snapshots, you can measure whether your team is actually closing KEV-listed findings faster than the CISA-recommended deadlines, a number worth reporting to leadership and auditors alike. Fourth, extend the Nuclei scan beyond web-facing HTTP checks into network-level protocol templates for exposed RDP, SSH misconfigurations, and database ports, since a meaningful share of KEV entries affect non-HTTP services like VPN appliances and network gear rather than web applications specifically.
Finally, build a quarterly tabletop review of what the pipeline caught versus what got through. If an incident ever does occur, check whether the exploited CVE was in KEV at the time, and if so, how long it sat unpatched after your pipeline flagged it. That retrospective loop is what turns a scanning script into an actual security program.
Complete working project structure
Here is how all the pieces from this tutorial fit together as a single deployable project. Create this directory structure on your scanner host or CI runner.
patch-pipeline/
├── fetch_kev.py # Step 2: pulls CISA KEV catalog JSON
├── watch_kev.py # Step 8: diffs catalogs, alerts on new vendor matches
├── prioritize.py # Step 6: cross-matches Nuclei results with KEV
├── risk_score.py # Step 10: numeric risk weighting
├── create_tickets.py # Step 11: pushes high-risk findings to your ticketing API
├── verify_patch.sh # Step 12: rescans to confirm a fix actually worked
├── run_pipeline.sh # Step 7: orchestrates the daily run, called by cron
├── targets.txt # Step 4: your authorized scan scope
├── custom-templates/
│ └── cve-2026-check.yaml # Step 9: templates for CVEs with no community coverage
└── logs/
└── patch-pipeline.log
Clone this structure, fill in your own targets.txt and ticketing API credentials, point cron at run_pipeline.sh, and you have a working, scheduled patch-prioritization system built entirely on free tooling. The total daily runtime depends on target count, but a scan of a few dozen hosts with reasonable rate limiting typically finishes in under 20 minutes, well within a nightly maintenance window.
Treat this project the way you would treat any other piece of production infrastructure, not a one-off script you wrote during a slow week. Put it in version control. Give it a README explaining what each file does and who owns it. Make sure more than one person on the team knows how to restart it if the scanner host gets rebuilt. Vulnerability pipelines have an unfortunate habit of becoming invisible exactly because they work quietly in the background, right up until a dependency breaks, a credential expires, or the one engineer who understood it leaves the company. None of that is a reason to skip building it, it is a reason to document it properly from day one.
For context on how fast the threat landscape this pipeline tracks actually moves, see trendintech.com's ongoing cybersecurity threats coverage for 2026, which tracks new ransomware campaigns, breach disclosures, and KEV-listed CVEs as they emerge.
Frequently asked questions
Is the CISA KEV catalog free to use?
Yes. CISA publishes the Known Exploited Vulnerabilities catalog as a public JSON feed with no API key, license, or registration required. It updates whenever CISA confirms new active exploitation.
Do I need an enterprise license for Nuclei?
No. The core Nuclei scanning engine and the community template library from ProjectDiscovery are open source and free. ProjectDiscovery sells a hosted platform (Nuclei Cloud) with additional orchestration features, but this tutorial's pipeline runs entirely on the free CLI tool.
How is KEV different from the CVSS score on a vulnerability?
CVSS measures theoretical severity based on factors like attack complexity and impact. KEV only lists CVEs with confirmed evidence of real-world active exploitation. A high CVSS score does not guarantee KEV listing, and a lower CVSS score can still appear in KEV if attackers are actively using it.
How often should this pipeline run?
Daily at minimum, since CISA adds new KEV entries multiple times a week. For organizations managing internet-facing infrastructure in high-risk sectors, running the watch_kev.py vendor-match check (Step 8) on a shorter interval, every few hours, catches emerging threats faster than a once-daily full scan alone.
Is it legal to run Nuclei against my own company's infrastructure?
Scanning assets you own or have explicit written authorization to test is standard security practice. Scanning third-party infrastructure, including vendor SaaS platforms, without permission can violate terms of service and, in some jurisdictions, computer fraud statutes. Always confirm scope and authorization before scanning anything outside infrastructure you directly control.
What percentage of KEV entries actually have ready-made Nuclei templates?
Coverage varies and changes constantly as the community adds templates, but in practice it typically lands in the 25-40% range of the full historical KEV catalog. Internet-facing enterprise software (VPNs, web servers, CMS platforms) tends to get templates fastest; older or niche entries often require a custom template, as covered in Step 9.
Can this pipeline replace a commercial vulnerability management platform?
For small to mid-sized environments, this free pipeline covers the core workflow: fetch threat intel, scan, prioritize, ticket, verify. Larger enterprises with thousands of assets typically still benefit from commercial platforms for asset discovery at scale, compliance reporting, and broader protocol coverage beyond what Nuclei's HTTP-centric templates handle, but the KEV-first prioritization logic in this tutorial applies regardless of which scanning engine sits underneath it.
What should I do if a scan finds a CVE with no patch available yet?
Check the KEV entry and vendor advisory for recommended mitigations, which sometimes include configuration changes, WAF rules, or temporary feature disabling when no official patch exists yet. Cisco's Catalyst SD-WAN Manager vulnerability, CVE-2026-76504, is a recent example where no workaround was available at disclosure, which is precisely the scenario where isolating or restricting network access to the affected system becomes the interim control until a vendor fix ships.
Rachel Kowalski
Rachel Kowalski is the Cybersecurity Analyst at TrendinTech, where she covers vulnerabilities, ransomware, supply chain attacks, nation-state operations and the security rules shaping organizations in the United States and Europe. Before moving into journalism she worked as a threat intelligence analyst at a security vendor and later reported for CyberScoop and the security desk at Wired, covering the SolarWinds compromise and the ransomware wave that hit hospitals and pipelines. Rachel holds a Master of Science in Cybersecurity from the Georgia Institute of Technology and attends Black Hat and DEF CON in Las Vegas each year, where she follows research from the security community. She writes to make risks and countermeasures understandable to people who are not specialists.
All stories by Rachel Kowalski (215)