The lesson · rebuilt to teach
From a fingerprint to a defensible queue
Phase 3 is not “run a scanner and read the score.” It is a reasoning discipline: turn what you know about a device into a list of candidate weaknesses, test each one until the evidence is honest, then rank what survives by what an attacker can actually reach and break. Seven steps — each one runnable in the toolkit and console below.
00 Four words that keep you honest
Most bad vulnerability reports come from collapsing these four states into one. Hold them apart and the rest of the method follows on its own.
01Candidate
A weakness that might apply — a CPE match, a banner, a template hit. A lead, never a verdict.
02Confirmed
A candidate backed by direct or strongly corroborating evidence on the actual host.
03False positive
A candidate that does not survive triage — wrong version, backported patch, feature not enabled.
04Prioritized
A confirmed finding chosen for action now, under real exposure and consequence.
Every finding also carries five things you should be able to name on demand: observation (what a tool saw), state (source, method, confidence, validation status), decision (candidate / confirmed / false positive / accepted), guardrail (no promotion without applicable evidence), and an action handoff.
· Five assumptions you work under
- No single method is authoritative. A database match, an active probe, and a successful login each see something the others cannot.
- Zero CVEs is not zero risk. It can mean a weak CPE, thin database coverage, or a firmware nobody indexed — not a clean host.
- A high score is not an automatic first action. A 9.8 nobody is exploiting can wait behind a 7.5 that is already in the wild.
- An untested host is not a safe host. “Found nothing,” “never ran,” and “skipped by policy” are three different sentences.
- A working default login is your strongest single signal. It is direct proof, not a hypothesis — treat it as top of queue.
01 Turn identity into applicability — the CPE
A CPE 2.3 string is how you ask the world, “is this exact product known to be vulnerable?” Its precision decides everything downstream: too broad and you drown in unrelated advisories, too narrow and you miss adjacent builds.
| What raises CPE quality | Why it matters |
| Source strength | An ONVIF or TLS identity beats an HTTP header, which beats a generic OUI guess. |
| Version precision | A specific build sharpens the CVE set; a wildcard version blurs it into noise. |
| Layer clarity | Software, firmware, and hardware CPEs imply different fixes — keep them distinct. |
| Kept alternatives | When identity conflicts, preserve both candidates rather than averaging them away. |
One practical guardrail: do not feed OS-level CPEs (a broad linux or android match) into actionable CVE lookups — they pull thousands of advisories that have nothing to do with the device in front of you.
02 Ask the database — NVD, read carefully
Query NVD with the exact cpeName first; on a miss, fall back to a broader virtualMatchString prefix. Cache by CPE and rate-limit politely — identical CPEs across hosts should resolve to a single lookup. But the real skill is reading the record:
| Field | How to read it |
| Affected products | The field that matters most. It names the CPE criteria the CVE actually applies to — check your version against it. |
| Description | A public claim, often product-level not version-level. Necessary, never sufficient. |
| Severity + vector | A standardized estimate. Keep the vector; you will decompose it next. |
| CWE | A weakness-category hint, useful for grouping and generic mitigations. |
| References | Patch > Vendor Advisory > Mitigation tags give you the fix hierarchy. |
A recent modified date does not mean newly exploitable, and an old published date does not mean low priority. Dates organize; they do not rank.
03 Read CVSS as structure, not a number
A 9.8 means nothing until you can explain it. The base vector is a claim about the attack path and the impact — learn to read it metric by metric so two findings with the same score can still earn different priorities.
| Metric | Reads as |
| AV — Attack Vector | Network / Adjacent / Local / Physical — how close must the attacker be? |
| AC — Attack Complexity | Low / High — are special conditions required? |
| PR — Privileges Required | None / Low / High — authenticated or not? |
| UI — User Interaction | None / Required — must a victim do something? |
| S, C, I, A | Scope, then Confidentiality / Integrity / Availability impact — what is actually lost. |
Bands (0·1–3·9 Low, 4–6·9 Medium, 7–8·9 High, 9+ Critical) are general indicators, not rules. AV:N/AC:L/PR:N/UI:N means remote, trivial, unauthenticated, no victim action — that is the 9.8.
04 Gather evidence from four different angles
No method is authoritative, so you run several and compare. They trade breadth against directness, and passivity against intrusiveness:
| Method | What it observes | Posture |
| NVD / CVE | CPE-to-CVE association from a public database. | Passive, broad |
| OpenVAS | Network probe + vulnerability-test result. | Active |
| Nuclei | Template-driven check of target behavior. | Active, precise |
| Default creds | A successful authentication on the host. | Direct proof |
A CPE match is a hypothesis; a banner you pulled yourself, or a login that succeeded, is evidence. The closer a method gets to the host, the more it can confirm — and the more carefully you must run it. Active and credential checks stay off by default for a reason.
05 Correlate — without double-counting
Three things can happen when methods meet on one host. Name which one you are looking at before you write the finding.
Convergence
Different methods support the same claim (CPE match and an active template fires). The strongest case — but count it once.
Unique coverage
One method finds what no other can — a default-credential login with no CVE attached. Still fully actionable.
Contradiction
Methods disagree (CPE says vulnerable, the active probe fails). Do not average — record the conflict and resolve identity first.
Rank the strength of what you have on a confidence ladder — a scaffold for judgment, not a replacement for it:
| Evidence you hold | Confidence |
| Successful authentication | High |
| Active behavior check + matching identity | High |
| Single active tool, clear target behavior | Medium–high |
| Exact CPE + version, no active confirmation | Medium |
| Wildcard CPE, no version | Low–medium |
| Broad family match + conflicting identity | Low |
06 Triage the false positives
A candidate that does not apply is noise that costs trust. Most false positives come from one of five places — each with a cheap check that resolves it:
| Source | What happened | The check |
| CPE too broad | The product family is wider than this device. | Is the device in the CVE’s affected-CPE list? |
| Backported patch | The banner is stale; the fix is already in. | Vendor firmware / patch-branch history. |
| Stale banner | The service string is simply outdated. | A second, independent version source. |
| Config-dependent | Vulnerable only when a feature is enabled. | Is that setting actually present? |
| Misread probe | An active response was interpreted wrongly. | A safe second check. |
Suppress at the (cve_id, host) pair level, never globally — and write the reason. “False positive” is weak; “CVE applies to firmware <V5.5.800, device reports V5.7.1, vendor confirms patched branch” is reusable.
07 Prioritize into action bands
Rank by exploitation reality, not raw score. Five inputs decide the band: severity, evidence strength, exposure (can an attacker reach it?), device role (what it controls), and consequence (what breaks if it goes).
Act nowHigh consequence + strong evidence. Patch, isolate, rotate credentials, or disable the exposure today. A camera taking default RTSP creds on a user subnet lives here.
Validate nextSevere but uncertain. Gather one safe piece of confirming evidence before you commit. Broad-family CVEs on an isolated VLAN belong here.
ScheduleReal finding, lower urgency. Add it to the maintenance plan with an owner and a date.
MonitorWeak or externally dependent. Track it until more evidence arrives — e.g. a cloud-only device with sparse local signal.
AcceptKnown risk under policy. Record the owner, the reason, and a review date. Acceptance is a decision, not a shrug.
→ What you hand forward
The next phase (attack-graph analytics) is only as good as the packet you pass it. An edge built on a confirmed default login is worth far more than one built on a wildcard CPE — so the handoff carries provenance, not just a verdict:
- Identity + confidence for the host.
- CPE candidates with their precision.
- CVE candidates with source and affected-product evidence.
- CVSS base score + vector.
- Active findings (OpenVAS, Nuclei) with method provenance.
- Default-credential findings as direct authentication proof.
- Validation status — confirmed / false positive / accepted / unresolved.
- Priority band + rationale.
- Unresolved contradictions, stated plainly.
Python toolkit · copy & run
The assessment pipeline, in five short scripts
The console above shows the result. These scripts show the method — the same vulnerability-assessment, correlation and prioritization flow the lab asks you to build, written from scratch against public data sources so you can run each step and watch it work.
1 Fingerprint → candidate CVEs
Turn an asset fingerprint into a CPE 2.3 string, then ask NVD which CVEs are recorded against that exact product and version.
import requests def cpe_for(vendor, product, version): return f"cpe:2.3:a:{vendor}:{product}:{version}:*:*:*:*:*:*:*" def cves_for_cpe(cpe): r = requests.get( "https://services.nvd.nist.gov/rest/json/cves/2.0", params={"cpeName": cpe, "resultsPerPage": 50}, timeout=30, ) r.raise_for_status() vulns = r.json().get("vulnerabilities", []) return [v["cve"]["id"] for v in vulns] cpe = cpe_for("apache", "http_server", "2.4.49") print(cves_for_cpe(cpe)) # -> ['CVE-2021-41773', ...]
2 Decompose the CVSS vector
A score of 9.8 means nothing until you can explain it. Pull the base metrics out of the v3.1 vector so you can say why it is critical — remote, no privileges, no user action.
def parse_cvss(vector): body = vector.removeprefix("CVSS:3.1/") parts = dict(p.split(":") for p in body.split("/")) legend = { "AV": {"N": "Network", "A": "Adjacent", "L": "Local", "P": "Physical"}, "AC": {"L": "Low", "H": "High"}, "PR": {"N": "None", "L": "Low", "H": "High"}, "UI": {"N": "None", "R": "Required"}, "C": {"H": "High", "L": "Low", "N": "None"}, "I": {"H": "High", "L": "Low", "N": "None"}, "A": {"H": "High", "L": "Low", "N": "None"}, } return {k: legend.get(k, {}).get(v, v) for k, v in parts.items()} print(parse_cvss("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")) # {'AV': 'Network', 'AC': 'Low', 'PR': 'None', 'UI': 'None', ...}
3 Enrich with EPSS + CISA KEV
Severity is not likelihood. EPSS gives the 30-day exploitation probability; the CISA KEV catalog tells you what is already being exploited in the wild. Correlate both.
import csv, io, requests def epss_score(cve): r = requests.get("https://api.first.org/data/v1/epss", params={"cve": cve}, timeout=30) rows = r.json().get("data", []) return float(rows[0]["epss"]) if rows else 0.0 def kev_set(): url = ("https://www.cisa.gov/sites/default/files/csv/" "known_exploited_vulnerabilities.csv") text = requests.get(url, timeout=30).text return {row["cveID"] for row in csv.DictReader(io.StringIO(text))} kev = kev_set() for cve in ["CVE-2021-41773", "CVE-2019-10929"]: print(cve, f"epss={epss_score(cve):.3f}", "KEV" if cve in kev else "-")
4 Confirm with direct evidence
A CPE match is a hypothesis. Grab the live service banner and verify the version that is actually running before you raise a finding — this is what kills false positives.
import socket def banner(host, port, probe=b"HEAD / HTTP/1.0\r\n\r\n"): with socket.create_connection((host, port), timeout=5) as s: s.sendall(probe) return s.recv(2048).decode("latin-1", "replace") def confirms(host, port, needle): return needle.lower() in banner(host, port).lower() print(confirms("10.20.7.42", 80, "Apache/2.4.49")) # True -> CVE applies
5 Build the prioritized remediation queue
Finally, triage like an operator: anything in KEV jumps the line, then weight by EPSS likelihood and by CVSS scaled to how critical the affected asset is.
CRIT_WEIGHT = {"low": 1.0, "medium": 1.5, "high": 2.0, "critical": 3.0} def priority(f): score = f["cvss"] * CRIT_WEIGHT[f["asset_criticality"]] score += f["epss"] * 10 # exploitation likelihood if f["in_kev"]: score += 100 # actively exploited -> top of queue return score queue = sorted(findings, key=priority, reverse=True) for f in queue[:10]: print(f"{f['cve']:<16} {f['hostname']:<22} score={priority(f):6.1f}")
Sources: NVD CVE API, FIRST.org EPSS, and the CISA Known Exploited Vulnerabilities catalog — all public. This is original teaching code written for this site; it does not reproduce Dr. Mallarapu's graded assignment, only the public method it exercises.