Vulnerability management · Beginner
Scan, prioritise by real risk, and fix
Why it matters
Vulnerability management appeared in 60% of the Cybersecurity Engineer job descriptions we read. Every scanner produces long lists; sorting by severity alone buries the few issues attackers actually use. Prioritising by real-world risk, then getting fixes shipped, is the job.
The concept
CVE: a public identifier for a vulnerability. The same flaw often also has other IDs (GitHub's GHSA, PyPI's PYSEC), so deduplicate.
CVSS: a 0 to 10 severity score describing how bad exploitation would be. It does not say how likely exploitation is.
EPSS (Exploit Prediction Scoring System, from FIRST): the estimated probability that a CVE will be exploited in the next 30 days, updated daily.
CISA KEV: the US government's catalogue of vulnerabilities known to be exploited in the wild. Anything in it goes to the top.
Context: is the vulnerable code reachable, internet-facing, handling sensitive data? A critical bug in an unused function matters less than a medium one on your login page.
Remediation: upgrade, patch, or mitigate (configuration, WAF rule) with an owner, a deadline based on risk, and a verification scan.
Worked example: an old Python service
requests==2.25.0
urllib3==1.26.4
Jinja2==2.11.2
PyYAML==5.3"""Prioritise pip-audit findings with EPSS (likelihood of exploitation) and CISA KEV (known exploited)."""
import json
import subprocess
import urllib.request
audit = json.loads(subprocess.run(
["pip-audit", "-r", "requirements.txt", "--format", "json", "--progress-spinner", "off"],
capture_output=True, text=True).stdout)
findings, seen = [], set()
for dep in audit["dependencies"]:
for vuln in dep.get("vulns", []):
cves = [a for a in [vuln["id"], *vuln.get("aliases", [])] if a.startswith("CVE-")]
key = (dep["name"], cves[0] if cves else vuln["id"])
if key in seen: # the same flaw is often listed under several IDs (PYSEC, GHSA, CVE)
continue
seen.add(key)
findings.append({"package": f"{dep['name']} {dep['version']}", "id": vuln["id"],
"cve": cves[0] if cves else None, "fix": ", ".join(vuln.get("fix_versions", [])[:1]) or "none"})
cves = sorted({f["cve"] for f in findings if f["cve"]})
with urllib.request.urlopen("https://api.first.org/data/v1/epss?cve=" + ",".join(cves), timeout=30) as r:
epss_data = json.load(r)
epss = {row["cve"]: float(row["epss"]) for row in epss_data["data"]}
epss_date = epss_data["data"][0]["date"] if epss_data["data"] else "n/a"
with urllib.request.urlopen("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", timeout=60) as r:
kev = {v["cveID"] for v in json.load(r)["vulnerabilities"]}
for f in findings:
f["epss"] = epss.get(f["cve"], 0.0)
f["kev"] = f["cve"] in kev
findings.sort(key=lambda f: (f["kev"], f["epss"]), reverse=True)
print(f"{len(findings)} findings in {len({f['package'] for f in findings})} packages; EPSS scores dated {epss_date}")
print(f"{'package':18} {'CVE':16} {'EPSS':>6} {'KEV':>4} fixed in")
for f in findings[:6]:
print(f"{f['package']:18} {str(f['cve']):16} {f['epss']:6.3f} {'yes' if f['kev'] else 'no':>4} {f['fix']}")
print("findings with EPSS below 0.01:", sum(f["epss"] < 0.01 for f in findings))
print("in CISA KEV:", sum(f["kev"] for f in findings))
from packaging.version import Version
plan = {}
for f in findings:
if f["fix"] != "none":
name = f["package"].split()[0]
plan[name] = max(plan.get(name, f["fix"]), f["fix"], key=Version)
print("upgrade plan:", ", ".join(f"{k}>={v}" for k, v in sorted(plan.items())))Run on 30 September 2026 with pip-audit 2.10:
$ pip install pip-audit packaging
$ python3 prioritise.py
22 findings in 5 packages; EPSS scores dated 2026-09-29
package CVE EPSS KEV fixed in
pyyaml 5.3 CVE-2020-14343 0.060 no 5.4
pyyaml 5.3 CVE-2020-1747 0.054 no 5.3.1
jinja2 2.11.2 CVE-2020-28493 0.035 no 2.11.3
urllib3 1.26.4 CVE-2021-33503 0.033 no 1.26.5
requests 2.25.0 CVE-2023-32681 0.030 no 2.31.0
urllib3 1.26.4 CVE-2026-21441 0.030 no 2.6.3
findings with EPSS below 0.01: 13
in CISA KEV: 0
upgrade plan: idna>=3.15, jinja2>=3.1.6, pyyaml>=5.4, requests>=2.33.0, urllib3>=2.7.0pip-audit reported 44 entries; deduplicating the same flaw listed under several IDs leaves 23 (the idna findings come from a dependency pulled in by requests). None is in the KEV catalogue, and more than half have an EPSS below 1%, so this is not an emergency, but the two PyYAML issues (including unsafe loading that can run code) lead the list. The whole backlog collapses into five upgrades. After applying them:
$ cat fixed.txt
requests==2.33.0
urllib3==2.7.0
idna==3.15
Jinja2==3.1.6
PyYAML==6.0.2
$ pip-audit -r fixed.txt
No known vulnerabilities foundThe same approach works for container images (Trivy, Grype) and hosts (Nessus, Qualys, OpenVAS): scan, deduplicate, enrich with EPSS and KEV, add your own context, group into fixes, verify.
Common mistakes
Sorting by CVSS only. Severity is not likelihood. Add EPSS, KEV and your context.
Counting findings instead of fixes. Report "five upgrades clear 23 findings", not "23 open vulnerabilities".
Upgrading without testing. Major version jumps (urllib3 1.x to 2.x, Jinja2 2.x to 3.x) can break code; run the test suite.
Scanning once. New CVEs appear daily against code that has not changed. Scan in CI and on a schedule.
Interview angle
"You have 5,000 open vulnerabilities. Where do you start?" (known exploited first, then high EPSS on internet-facing and sensitive systems, grouped into fixes by owner, with SLAs by risk). "CVSS versus EPSS?" "How do you handle a vulnerability with no fix?" (mitigate, isolate, monitor, and document the accepted risk with an expiry date).
Practice task
Add CVSS base scores from the OSV API to the table and find a case where CVSS and EPSS disagree.
Scan a container image you use with Trivy or Grype and apply the same prioritisation.