Tens of thousands of vulnerabilities are published every year. A scanner will happily list a thousand of them across your estate, and most will never be exploited by anyone. Vulnerability management is the discipline of finding the ones that matter to you, fixing them within a time that reflects their real risk, and being able to prove it. It sits between the scanning tools of the earlier modules and the incident response of the next one: done well, the emergency patch at 2 a.m. is rare, because the exposed, exploitable things were fixed on Tuesday afternoon. This module covers how vulnerabilities are identified and scored, how to prioritise with exploitability rather than raw severity, what patch SLAs look like in practice, and how to verify that what you think is patched actually is.
- Read a CVE record and a CVSS vector, and explain what the score does and does not tell you
- Prioritise with exploitability (EPSS, known-exploited lists), exposure and asset value, not severity alone
- Run an inventory-driven scanning program across hosts, containers, code dependencies and cloud configuration
- Set and meet remediation SLAs with owners, exceptions and escalation
- Verify remediation and report coverage honestly
CVE, CVSS and what a score means
A CVE (Common Vulnerabilities and Exposures) identifier names one vulnerability in one product: CVE-2024-3094 is the xz backdoor, always and everywhere. Details live in the National Vulnerability Database and the vendor's advisory: affected versions, fixed versions, a description, and a CVSS score. CVSS rates the vulnerability's intrinsic severity from 0 to 10 using a vector of properties: attack vector (network, adjacent, local, physical), complexity, privileges required, user interaction, and impact on confidentiality, integrity and availability.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H -> 9.8 Critical
AV:N attack vector: network (reachable remotely)
AC:L attack complexity: low (reliable exploit)
PR:N privileges required: none (unauthenticated)
UI:N user interaction: none (no click needed)
S:U scope: unchanged
C:H/I:H/A:H full impact on confidentiality, integrity, availabilityCVSS describes the worst case for the vulnerability in the abstract. It does not know whether the affected code path is reachable in your deployment, whether the service is on the internet, whether an exploit exists, or whether anyone is using it. Two 9.8s can be a five-alarm fire and a non-event. That is why severity alone is a poor sorting key.
Scanners also report vulnerabilities from vendor advisories and distribution trackers that are not (yet) CVEs, and distributions often backport fixes without changing the upstream version number, which confuses version-matching scanners. Prefer scanners that understand your distribution's security tracker.
Prioritise by risk, not by score
Risk is likelihood times impact, in your environment. For likelihood, use EPSS (Exploit Prediction Scoring System), a daily-updated probability that a CVE will be exploited in the next thirty days, and the Known Exploited Vulnerabilities catalogue maintained by CISA, which lists what is being exploited right now. For exposure, use your own inventory: is the vulnerable component reachable from the internet, does it face untrusted users, what would its compromise give an attacker. For impact, use asset classification: the customer database versus a build-cache server.
| Signal | Question it answers | Source |
|---|---|---|
| CVSS | How bad is it in the worst case? | NVD, vendor advisory |
| EPSS | How likely is exploitation soon? | FIRST, updated daily |
| KEV list | Is it being exploited now? | CISA catalogue |
| Exposure | Can an attacker reach it in our estate? | Your inventory, network scans |
| Asset value | What does compromise cost us? | Your data classification |
def priority(cvss: float, epss: float, on_kev: bool, internet_exposed: bool, asset_tier: int) -> float:
"""asset_tier: 1 = crown jewels ... 3 = low value. Higher result = fix sooner."""
likelihood = 1.0 if on_kev else max(epss, 0.01) # KEV means it is happening now
exposure = 1.0 if internet_exposed else 0.4
impact = cvss / 10 * {1: 1.0, 2: 0.6, 3: 0.3}[asset_tier]
return round(likelihood * exposure * impact, 3)
# a 9.8 on an internal build server with negligible EPSS ranks below
# a 7.5 on the internet-facing API that is on the KEV list
print(priority(9.8, 0.02, False, False, 3)) # 0.002
print(priority(7.5, 0.60, True, True, 1)) # 0.75Whatever formula you use, publish it. Engineers accept "fix this first" when they can see why, and auditors accept a documented risk-based process far more readily than a pile of unfixed criticals with no rationale.
Inventory and scanning across every layer
You cannot prioritise what you have not found, and you cannot find vulnerabilities in assets you do not know about. The inventory comes first: every host, container image, function, code repository, cloud account and SaaS integration, with an owner. Then scan each layer with the tool that understands it, on a schedule and on change.
- Hosts and VMs: agent-based or authenticated network scanning for OS and installed packages (OpenVAS, cloud-native inspectors, commercial scanners).
- Container images: scan in the registry continuously, not just at build; a clean image last month has new findings today (Trivy, Grype, registry-native scanning).
- Code dependencies: SCA on every repository, stored SBOMs re-scanned daily so new advisories match existing builds.
- Cloud configuration: misconfiguration is a vulnerability class; benchmark scanning from the cloud module.
- Web applications: DAST on a schedule against staging, plus periodic penetration tests for what tools miss.
# nightly: scan every stored SBOM and emit JSON for the tracker
for f in sboms/*.cdx.json; do
grype "sbom:$f" --output json --add-cpes-if-none > "results/$(basename "$f" .cdx.json).json"
done
# which images carry a specific CVE today?
grep -l 'CVE-2025-12345' results/*.jsonFeed every scanner into one tracker (a purpose-built vulnerability management system, or at minimum a deduplicated database) so a vulnerability is one record with many affected assets, one owner, one status. Twelve scanner dashboards are how findings fall between the cracks.
SLAs, exceptions and the patch process
A remediation SLA turns priority into a commitment. Typical targets: known-exploited or critical-and-exposed within days, high within weeks, medium within a quarter, low in the normal maintenance cycle. The numbers matter less than three properties: they are agreed with the teams that must do the work, they are measured, and missing them escalates to someone who can reallocate time. Emergency out-of-band patching for actively exploited internet-facing vulnerabilities is a separate, faster path with its own playbook.
| Priority | Example | Target |
|---|---|---|
| P1: exploited or critical + exposed | KEV-listed bug in the public API's framework | 72 hours, emergency change |
| P2: high, reachable | Auth bypass in an internal admin tool | 14 days |
| P3: medium or unreachable high | Local privilege escalation on a locked-down host | 90 days |
| P4: low | Informational library advisory | Next scheduled update |
Some findings cannot be fixed on time: the vendor has no patch, the fix breaks a dependency, the system is being retired. An exception records the risk, the compensating control (network isolation, WAF rule, monitoring), the approver and an expiry date, after which it is reviewed again. Exceptions without expiry are how a temporary decision becomes a permanent hole. Make patching itself boring: automatic OS updates in maintenance windows, image rebuilds on a cadence, dependency bots opening pull requests, and immutable infrastructure so that the fix is a redeploy rather than a login.
Verify, measure, report
"We patched it" is a claim; a rescan is evidence. Verify every P1 and P2 fix by rescanning the specific asset, and confirm the vulnerable process actually restarted — a library updated on disk while the old version stays loaded in memory is the most common false fix. Track the metrics that describe the program's health rather than its busyness: time to remediate by priority, SLA compliance by team, age of the oldest open P1, coverage (percentage of inventory scanned in the last week), and exception count and age.
# processes still using deleted (replaced) shared libraries need a restart
sudo lsof -nP +c 15 2>/dev/null | grep -E 'DEL|\(deleted\)' | awk '{print $1, $2}' | sort -u
# Debian/Ubuntu: which services need restarting after the last apt upgrade
sudo needrestart -r lReport the picture honestly: findings you know about and their ages, the inventory you cover and the gaps you do not, exceptions and their expiry. A dashboard that shows zero criticals because scanning covers a third of the estate is worse than none. The program is working when the emergency patch is rare, the backlog is small and known, and the next disclosed critical is a routine morning's work instead of a week of surprises.