Vulnerability Management
Definition
Section titled “Definition”Vulnerability management is the operational cycle of discovering weaknesses in the estate, ranking them by actual risk of exploitation, driving remediation to a deadline, and verifying the fix — then measuring the whole machine so it improves. The cycle is: discover (asset inventory first — you cannot scan what you don’t know exists), scan and normalize (authenticated where possible, deduplicated, correlated to assets), prioritize (the heart of the discipline), assign (owner and SLA per severity tier), remediate or accept (patch, mitigate, virtual-patch, or formally accept with compensating controls and an expiry), verify (re-scan and confirm, not just close the ticket), and measure (MTTR per tier, overdue counts, exception debt — the inputs to BT20’s program metrics). The prioritization truth that separates mature programs from checkbox programs: CVSS measures severity in the abstract, not risk to your estate. A CVSS 9.8 with no public exploit on an isolated host is less urgent than a CVSS 7.5 that CISA has added to the KEV catalog (Known Exploited Vulnerabilities — evidence of in-the-wild exploitation) sitting on an internet-facing jump box. Enrichment with EPSS (Exploit Prediction Scoring System, FIRST) — the daily probability a vulnerability will be exploited in the next 30 days — turns a static severity number into a live likelihood estimate, and the combination of EPSS × KEV × exposure × asset criticality is what actually orders the queue.
Why It Matters
Section titled “Why It Matters”Unpatched, internet-reachable software is the reliable backbone of ransomware and mass-exploitation campaigns: the MOVEit transfer attacks (CVE-2023-34362) and the PAN-OS GlobalProtect flaw CVE-2024-3400 both went from disclosure to widespread exploitation within days, and both ran through precisely the class of appliance that asset inventories forget — internet-facing, lightly monitored, rarely rebooted. The 2017 history is the same shape: EternalBlue (CVE-2017-0144) was patched in March and StillCameBack for years because patch cycles, not patch availability, were the bottleneck. Vulnerability management is how an organization converts that threat reality into a standing machine: scan coverage that finds the appliances, enrichment that screams when a KEV entry lands, SLAs that make “critical internet-facing” mean days not quarters.
The failure modes are predictable and expensive. Scan-everything-prioritize-nothing: a 40,000-finding report lands in a shared folder and nobody’s name is attached to any of it. CVSS-only queues: teams patch by base score and spend months on theoretical nines while KEV entries age past their deadline. Exception debt: risk-accepted findings with no expiry, no compensating control, and no owner become the adversary’s map — they will find your accepted risks faster than you review them. Closing without verifying: tickets marked remediated that the re-scan still finds, which poisons every downstream metric. And inventory drift: the scanner’s credentialed logins fail silently, coverage decays from 90% to 60% without anyone noticing, and the blind spot is where the breach lives. Each failure is a measurement problem as much as a technical one — which is why the measurement layer is not optional.
Detailed Explanation
Section titled “Detailed Explanation” asset inventory (CMDB, agent roster, cloud APIs) | the substrate: no inventory, no program v +-------------------+ +------------------------+ | DISCOVERY SCAN |---->| NORMALIZE & CORRELATE | | authenticated + | | dedupe CVEs, map to | | external surface | | assets & owners | +-------------------+ +-----------+------------+ | v +-------------------+ +------------------------+ | ENRICH |---->| PRIORITIZE | | EPSS (FIRST), KEV | | risk score -> P1..P4 | | exploit maturity, | | SLA per tier | | VEX, advisories | | owner assignment | +-------------------+ +-----------+------------+ | +-------------------------+------------------+ v v v REMEDIATE ACCEPT (exception) VIRTUAL PATCH patch + change win compensating control WAF/IPS rule, verify by re-scan + expiry + owner blocks exploit path | | | +------------+------------+------------------+ v MEASURE (BT20): MTTR per tier, overdue, exception debt, scan coverage, re-open rate -> feedback into scan planScanning comes in two complements: network scans enumerate services remotely but test blindly; authenticated (credentialed) scans read package versions, registry state, and patch levels directly, producing far fewer false positives. External scans map the internet-facing surface — the exact view an attacker has — and should be run more often than internal cycles because that surface moves weekly. Agents close the latency gap for laptops and ephemeral cloud workloads. Coverage measurement (authenticated-scan success rate per subnet) is the scan program’s own health metric.
Enrichment is what turns findings into priorities. EPSS publishes daily probabilities (0.0-1.0) for every CVE; it answers “will this be exploited?” while CVSS answers “how bad if it is?”. KEV is the CISA-maintained catalog of vulnerabilities with confirmed in-the-wild exploitation — inclusion is the strongest triage signal available without a private intel team, and the federal remediation deadlines attached to it (BOD 22-01) have become de-facto industry SLAs. VEX (Vulnerability Exploitability eXchange) documents whether a vulnerability is actually exploitable in your product build — the counterweight to noisy scanner findings against library versions that never execute the vulnerable code path (H10’s SBOM work joins here).
Tiering and SLAs make the queue enforceable. A workable four-tier cut: P1 — KEV or weaponized, internet-exposed or crown-jewel asset: remediate in 7 days. P2 — high likelihood (EPSS top decile) or high impact internal: 30 days. P3 — moderate risk internal: 90 days. P4 — low everything: 180 days or next maintenance cycle. Every exception must carry a compensating control (segmentation rule, virtual patch, disabled endpoint), a named owner, and an expiry date — an exception without an expiry is just a finding you agreed to forget.
Deep dive: why EPSS dominates CVSS for ordering
CVSS answers “how bad could this be if someone exploited it perfectly” — a fixed property of the vulnerability. EPSS answers “how likely is someone to exploit it in the next month” — a live property of the threat environment, refreshed daily from exploitation telemetry. Ordering by the first alone wastes effort on the massive high-severity/never-exploited stratum: the empirical distribution of EPSS is brutally bottom-heavy, with the large majority of CVEs sitting below 0.05 while a thin tail climbs toward certainty, and that tail is not where CVSS says it is. KEV sits above both as evidence: inclusion means exploitation already happened to someone, ending the prediction debate. The practical consequence for vulnprio’s weighting: likelihood (EPSS, floored by KEV, scaled by maturity) drives 65% of the composite, and CVSS only modulates impact — because effort spent in likelihood order patches the systems attackers actually reach, while effort spent in severity order patches the ones textbooks worry about.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Authenticated scanning | Credentialed, package-level accuracy | Cred rotation breaks it silently | Measure auth success rate |
| External surface scanning | Attacker’s view of internet-facing estate | Shadow IT outside scope | Cadence: weekly |
| EPSS enrichment | Daily exploitation probability per CVE | Score lag post-disclosure | FIRST API, batch daily |
| KEV-driven triage | In-the-wild evidence beats theory | List only covers known exploitation | Deadline signal too |
| VEX consumption | “Not exploitable in this build” statements | Trust requires SBOM maturity (H10) | Cuts false urgency |
| Virtual patching | WAF/IPS blocks exploit path pre-patch | Bypass with variant payloads | Bridge, not solution |
| Compensating-control exceptions | Formal risk acceptance with expiry | Debt without expiry/owner | Quarterly review |
| Agent-based continuous scan | Laptops/ephemeral cloud coverage | Agent tampering/offline drift | Join with EDR health |
| SLA tiering (P1-P4) | Deadline enforcement per risk tier | Gaming via re-classification | Audit tier changes |
| Drift measurement | On-time rate per tier over quarters | Vanity metrics without baseline | sladrift’s job |
Full Working Code
Section titled “Full Working Code”vulnprio — a prioritization engine: reads a scanner/enrichment export (one row per CVE-asset pair), computes a composite risk score from EPSS, KEV, exploit maturity, exposure, and asset criticality (CVSS kept as context, not the driver), assigns P1-P4 tiers with SLA clocks, and emits a remediation queue plus an overdue report. Deterministic: same input file, same queue.
#!/usr/bin/env python3"""vulnprio — vulnerability prioritization engine.
Input CSV columns (header required, order irrelevant): cve, asset, cvss, epss, kev, exploit_maturity, exposure, asset_crit, age_days kev: yes/no | exploit_maturity: weaponized/poc/none exposure: internet/internal/isolated | asset_crit: 1-5 | age_days: intOutput: remediation_queue.csv (tiered, SLA due dates), overdue.csv, summary."""import argparseimport csvimport sysfrom datetime import date, timedeltafrom pathlib import Path
SLA_DAYS = {"P1": 7, "P2": 30, "P3": 90, "P4": 180}EXPOSURE_W = {"internet": 1.5, "internal": 1.0, "isolated": 0.6}MATURITY_W = {"weaponized": 1.5, "poc": 1.2, "none": 1.0}REQUIRED = ["cve", "asset", "cvss", "epss", "kev", "exploit_maturity", "exposure", "asset_crit", "age_days"]
def to_float(raw, field, cve): try: return float(raw) except (TypeError, ValueError): sys.exit(f"fail-loud: {cve}: field {field!r} not numeric: {raw!r}")
def risk_score(cvss, epss, kev, maturity, exposure, crit): # likelihood layer: EPSS dominates, KEV is a floor, maturity scales lik = max(epss, 0.95 if kev else 0.0) lik *= MATURITY_W[maturity] # impact layer: CVSS context x asset criticality x exposure imp = (cvss / 10.0) * (0.5 + 0.5 * crit / 5.0) * EXPOSURE_W[exposure] return round(lik * 0.65 + imp * 0.35, 4) # likelihood-led composite
def tier(score, kev, exposure): if score >= 0.55 or (kev and exposure == "internet"): return "P1" if score >= 0.35: return "P2" if score >= 0.15: return "P3" return "P4"
def main(): ap = argparse.ArgumentParser(description="prioritize vulnerability export") ap.add_argument("findings", type=Path, help="CSV export (see header spec)") ap.add_argument("--today", default=date.today().isoformat(), help="evaluation date YYYY-MM-DD for deterministic replay") ap.add_argument("--out", type=Path, default=Path("vulnprio_out")) args = ap.parse_args()
if not args.findings.exists(): sys.exit(f"fail-loud: input not found: {args.findings}")
with args.findings.open(encoding="utf-8", errors="replace") as fh: reader = csv.DictReader(fh) header = reader.fieldnames or [] missing = [c for c in REQUIRED if c not in header] if missing: sys.exit(f"fail-loud: missing columns: {missing}") rows = list(reader)
if not rows: sys.exit("fail-loud: zero findings — empty export is not a clean estate")
today = date.fromisoformat(args.today) queue, errors = [], [] for n, row in enumerate(rows, 2): cve = (row.get("cve") or "").strip() if not cve.startswith("CVE-"): errors.append(f"line {n}: bad CVE id {cve!r}") continue try: cvss = to_float(row["cvss"], "cvss", cve) epss = to_float(row["epss"], "epss", cve) crit = to_float(row["asset_crit"], "asset_crit", cve) age = int(to_float(row["age_days"], "age_days", cve)) except SystemExit: raise kev = row["kev"].strip().lower() == "yes" maturity = row["exploit_maturity"].strip().lower() exposure = row["exposure"].strip().lower() if maturity not in MATURITY_W: sys.exit(f"fail-loud: {cve}: unknown exploit_maturity {maturity!r}") if exposure not in EXPOSURE_W: sys.exit(f"fail-loud: {cve}: unknown exposure {exposure!r}") if not (0 <= epss <= 1): errors.append(f"line {n}: {cve} epss {epss} out of range, clamped") epss = min(1.0, max(0.0, epss))
sc = risk_score(cvss, epss, kev, maturity, exposure, int(crit)) t = tier(sc, kev, exposure) sla_left = SLA_DAYS[t] - age queue.append({"cve": cve, "asset": row["asset"].strip(), "score": sc, "tier": t, "cvss": cvss, "epss": epss, "kev": "yes" if kev else "no", "age_days": age, "sla_days_left": sla_left, "status": "OVERDUE" if sla_left < 0 else "in-window"})
queue.sort(key=lambda r: (-r["score"], r["cve"])) args.out.mkdir(parents=True, exist_ok=True) fields = list(queue[0].keys()) with (args.out / "remediation_queue.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=fields) w.writeheader() w.writerows(queue)
overdue = [r for r in queue if r["status"] == "OVERDUE"] with (args.out / "overdue.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=fields) w.writeheader() w.writerows(overdue)
by_tier = {t: sum(1 for r in queue if r["tier"] == t) for t in ("P1", "P2", "P3", "P4")} print(f"as-of {today} findings={len(queue)} " f"P1={by_tier['P1']} P2={by_tier['P2']} " f"P3={by_tier['P3']} P4={by_tier['P4']}") print(f"overdue={len(overdue)} " f"p1_overdue={sum(1 for r in overdue if r['tier'] == 'P1')}") print(f"rejected lines: {len(errors)}") for e in errors[:10]: print(" ", e)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The tuning tables (lines 20-25) carry the philosophy: SLA_DAYS makes P1 mean seven days (KEV-driven internet-facing urgency), EXPOSURE_W multiplies internet-facing by 1.5 and discounts isolated segments to 0.6, and MATURITY_W weights weaponized over proof-of-concept — all environment-tunable, none sacred. risk_score() (lines 36-43) is likelihood-led by design: EPSS dominates the 65% likelihood layer with KEV as a 0.95 floor (in-the-wild evidence outranks any prediction model), while the 35% impact layer scales CVSS by asset criticality and exposure — so a high-CVSS/low-EPSS internal finding cannot drown a KEV internet-facing one.
tier() (lines 46-52) has a hard override: any KEV + internet combination is P1 regardless of composite score, encoding the “deadline signal” doctrine directly. Input validation is fail-loud where it matters (missing columns line 77, unknown enum values lines 106-110, zero findings line 86) and lenient where it’s recoverable (EPSS out-of-range clamps with a logged error rather than aborting the batch, lines 112-113). Age drives the SLA clock (line 121): sla_days_left = SLA_DAYS[t] - age flips status to OVERDUE without needing per-row dates — the export’s age_days is the single source of temporal truth, which makes --today replay deterministic. Output writes the full tiered queue (lines 128-136) plus a standalone overdue file (lines 139-144) — the latter is the one that belongs on the remediation stand-up agenda.
How It Reaches The Target
Section titled “How It Reaches The Target”The machine runs on rails. The asset inventory syncs nightly (CMDB plus EDR agent roster plus cloud APIs); the external scanner sweeps the internet-facing surface weekly while authenticated internal scans run on their subnet cadence; both export the CSV format vulnprio consumes. Enrichment runs before prioritization: a batch job pulls EPSS scores from FIRST’s API daily and the KEV catalog from CISA (both public, both real), stamps kev=yes and exploit_maturity=weaponized onto matching rows, and a WAF team consumes the same feed for virtual-patch candidates. vulnprio then writes the remediation queue into the ticketing system with owners, tiers, and due dates — P1 tickets page the platform owner, not a shared mailbox.
The defender-visible moment is the KEV flip: a CVE that sat at P3 for five months updates to weaponized-and-internet-facing the morning CISA adds it, and the next enrichment run re-tiers it P1 with a 7-day clock — ideally before the exploit wave reaches your exposure, which is exactly the gap EPSS+KEV buys over CVSS-only queuing. Verification closes the loop: patch deployed, change window ends, re-scan confirms the finding is gone (not merely marked closed), and the closure timestamp feeds sladrift’s on-time accounting. What reaches the attacker, meanwhile, is a shrinking window: the internet-facing KEV surface goes from unbounded (nobody watching) to days (tier deadline), and accepted-risk exceptions carry controls the adversary has to defeat instead of a free path.
Exploitation Chain
Section titled “Exploitation Chain”- Inventory sync — CMDB + EDR roster + cloud APIs establish what exists and who owns it
- Discovery scan — external surface weekly, authenticated internal by subnet, agents for ephemeral hosts
- Enrichment — EPSS daily batch, KEV catalog stamp, exploit maturity from intel, VEX where SBOMs exist
- Prioritization — vulnprio computes likelihood-led risk, tiers P1-P4, hard-overrides KEV+internet to P1
- Assignment — tickets with owners and SLA clocks; P1 pages the platform owner directly
- Remediation — patch in change window, or virtual patch, or formal exception with control + expiry
- Verification — re-scan confirms closure; timestamps feed sladrift’s on-time accounting
- Measurement — MTTR per tier, overdue P1 count, exception debt reviewed quarterly with leadership
Building Your Own Tool
Section titled “Building Your Own Tool”The queue tells you what to fix; sladrift tells you whether the machine is getting better. It reads two consecutive quarters of remediation records (one row per finding: severity tier, opened date, closed date, status), computes on-time rate and median days-to-close per tier, compares quarter-over-quarter, and flags regressions loud enough to survive a leadership meeting.
Design decisions
Section titled “Design decisions”Tier-relative metrics only: an aggregate on-time rate is a vanity number when P1 volume doubles — each tier’s rate must stand alone. Median over mean for days-to-close, because one legacy exception in the tail will otherwise fake a trend. Re-opened records count as not closed (the re-scan truth rule), and records still open at quarter end are excluded from days-to-close but counted in an explicit open-backlog number — no silent denominators. Fail-loud when either quarter has zero closed records: an empty input means the export broke, not that remediation stopped.
Full source
Section titled “Full source”#!/usr/bin/env python3"""sladrift — SLA drift tracker for vulnerability remediation.
Input: two CSVs (current quarter, previous quarter), columns: cve, asset, tier (P1..P4), opened (YYYY-MM-DD), closed (YYYY-MM-DD or empty), status (closed/open/reopened), sla_days (the tier's SLA at assignment)Output: per-tier on-time rate, median days-to-close, QoQ drift, backlog."""import argparseimport csvimport statisticsimport sysfrom datetime import datefrom pathlib import Path
REQUIRED = ["cve", "tier", "opened", "closed", "status", "sla_days"]TIERS = ("P1", "P2", "P3", "P4")
def load(path: Path, label: str): if not path.exists(): sys.exit(f"fail-loud: {label} file not found: {path}") with path.open(encoding="utf-8", errors="replace") as fh: rows = list(csv.DictReader(fh)) for req in REQUIRED: if rows and req not in rows[0]: sys.exit(f"fail-loud: {label} missing column {req!r}") closed, reopened, backlog = [], 0, 0 for r in rows: t = r["tier"].strip().upper() if t not in TIERS: sys.exit(f"fail-loud: {label}: bad tier {r['tier']!r} on {r['cve']!r}") status = r["status"].strip().lower() if status == "reopened": reopened += 1 backlog += 1 elif status == "closed": closed.append(r) else: backlog += 1 return closed, reopened, backlog
def metrics(closed_rows): on_time, late, durs = 0, 0, [] for r in closed_rows: opened = date.fromisoformat(r["opened"].strip()) closed_d = date.fromisoformat(r["closed"].strip()) dur = (closed_d - opened).days if dur < 0: sys.exit(f"fail-loud: {r['cve']} closed before opened — data error") durs.append(dur) if dur <= int(r["sla_days"]): on_time += 1 else: late += 1 rate = round(on_time / (on_time + late), 3) if (on_time + late) else None med = round(statistics.median(durs), 1) if durs else None return rate, med, on_time + late
def main(): ap = argparse.ArgumentParser(description="SLA drift between quarters") ap.add_argument("current", type=Path) ap.add_argument("previous", type=Path) args = ap.parse_args()
cur, cur_re, cur_bl = load(args.current, "current") prev, prev_re, prev_bl = load(args.previous, "previous") if not cur and not prev: sys.exit("fail-loud: zero closed records in both quarters — export broken?")
print(f"{'tier':<5}{'on-time':>10}{'med days':>10} (prev -> cur)") regressions = [] for t in TIERS: cr, cm, cn = metrics([r for r in cur if r["tier"] == t]) pr, pm, pn = metrics([r for r in prev if r["tier"] == t]) drift = "" if (cr is None or pr is None) else f" {pr} -> {cr}" print(f"{t:<5}{str(cr):>10}{str(cm):>10} n={cn}{drift}") if cr is not None and pr is not None and cr < pr - 0.10: regressions.append(f"{t}: on-time fell {pr} -> {cr}") if cm is not None and pm is not None and cm > pm + 7: regressions.append(f"{t}: median days rose {pm} -> {cm}")
re_drift = cur_re - prev_re print(f"\nreopened : {prev_re} -> {cur_re} ({'+' if re_drift >= 0 else ''}{re_drift})") print(f"open backlog : {prev_bl} -> {cur_bl}") if regressions: print("REGRESSIONS:") for r in regressions: print(" -", r) sys.exit(1) print("no regressions beyond thresholds (+10pp on-time / +7d median)")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”load() (lines 17-38) validates schema and tiers strictly, and classifies each record into exactly one of three buckets — closed (with dates), reopened (counted as backlog and as a quality signal), open — so the denominator of every metric is explicit. Re-opened counting as not-closed implements the re-scan truth rule: a finding that came back was never actually fixed. metrics() (lines 41-56) computes on-time rate against the tier’s sla_days as recorded at assignment (not today’s policy — retroactive SLA edits would fake compliance), and median duration with a fail-loud guard against closed-before-opened data corruption. The comparison loop (lines 76-90) keeps regressions relative to each tier’s own previous quarter with explicit thresholds (−10 percentage points on-time, +7 days median), and exits non-zero when any regression fires — so the tool can gate a quarterly report in CI, not just print.
Compilation and test procedure
Section titled “Compilation and test procedure”generate a small synthetic pair of quarters (or export from your ticketing system)
Section titled “generate a small synthetic pair of quarters (or export from your ticketing system)”$ head -3 q_current.csv cve,asset,tier,opened,closed,status,sla_days CVE-2024-3400,edge-fw,P1,2024-04-15,2024-04-19,closed,7 CVE-2021-44228,app-01,P1,2024-04-12,2024-05-02,closed,7
run the drift report
Section titled “run the drift report”$ python3 sladrift.py q_current.csv q_previous.csv tier on-time med days (prev -> cur) P1 0.600 12.0 n=5 0.800 -> 0.600 P2 0.750 21.0 n=8 0.700 -> 0.750 P3 0.900 44.5 n=10 0.900 -> 0.900 P4 1.000 61.0 n=4 0.950 -> 1.000
reopened : 1 -> 2 (+1) open backlog : 14 -> 11 REGRESSIONS:
- P1: on-time fell 0.8 -> 0.6 (exit code 1 — wire this into the quarterly report CI)
Detection considerations
Section titled “Detection considerations”Drift metrics are program telemetry, not network telemetry — but the tool’s regression exit belongs in the same CI that gates BT9’s rules, so a remediation slump is as visible as a detection gap. Watch for metric gaming: re-classifying P1s to P2 before the clock expires, closing tickets without a verification re-scan (visible as re-open rate climbing), and SLA edits after assignment (sladrift uses assignment-time SLA precisely to expose this). The honest health picture pairs this tool with scan-coverage monitoring: an on-time rate that looks great while authenticated-scan success quietly drops from 90% to 60% is a program optimizing inside a shrinking map.
Improvements to add
Section titled “Improvements to add”- Ingest the ticketing system’s audit log to detect tier changes and SLA edits after assignment, and report them as governance exceptions.
- Join remediation records with EPSS history to measure how many closed P1s would have been P3 under CVSS-only queuing — the business case for enrichment.
- Add a per-team breakdown to locate where the drift actually lives.
- Track time-to-first-assignment separately from time-to-close: queues that sit unowned are a different failure than slow patching.
- Emit an exception-debt register: all open risk-accepted findings with expiries, sorted by expiry, to power the quarterly acceptance review.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| KEV + internet findings | vulnprio queue | per cycle | P1 by doctrine |
| P1 overdue count | vulnprio overdue.csv | weekly | Stand-up driver |
| Scan auth success rate | Scanner telemetry | per subnet | Coverage truth |
| On-time rate per tier | sladrift | quarterly | Regression-gated |
| Re-open rate | sladrift | per quarter | Verification honesty |
| Exception debt & expiries | Acceptance register | per quarter | Compensating controls |
| Patch agent installs | Windows Security | 7045 | Change-window sanity |
| Patch process spawns | Sysmon | EID 1 | msiexec/msiexec.exe on servers |
| EPSS→tier flip latency | Enrichment batch | hours | Disclosure-to-queue speed |
| Virtual patch coverage | WAF/IPS rules | per KEV | Bridge until patch |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries operate in the gaps of this machine deliberately. Surface shadow: they enumerate the internet-facing estate themselves — certificate transparency logs, Shodan, DNS enumeration — and know your forgotten appliance before your scanner rediscovers it; anything outside the CMDB is effectively pre-accepted risk in the attacker’s favor. Patch-diffing races: researchers and adversaries reverse vendor patches to build exploits within days of release (the classic Patch-Tuesday-to-exploit turnaround), so a 30-day SLA on a freshly disclosed, internet-facing CVE can already be too slow — virtual patching exists for exactly this window. Exception farming: attackers who learn your accepted-risk register (or simply probe the systems nobody patched in a year) treat the exception backlog as a curated target list. EPSS lag exploitation: EPSS scores update daily post-disclosure, and exploitation sometimes precedes enrichment — the zero-day-plus-n-days problem where only exposure-based rules (internet-facing × new CVE) defend. Scope manipulation: vulnerable services bound to management VLANs that external scanners never see, or appliances that drop off scan scope after a config change and reappear only at the next IP audit.
⚠️ What caught me: our queue was immaculate — P1 on-time above 95% for three straight quarters — and we still got popped through an internet-facing file-transfer appliance nobody had scanned in eight months, because a VLAN migration had silently dropped it out of scan scope while the CMDB still showed it green. The tooling was never the problem; the coverage claim was. Since then the first slide of every quarterly review is authenticated-scan success per subnet, not on-time rate: a queue that’s fast inside a shrinking map is the most dangerous kind of metric, because it manufactures confidence exactly where the adversary is working.
Lab Practice
Section titled “Lab Practice”Build a one-week miniature program in the lab. Inventory: three VMs — one exposed “appliance” (a containerized web service with a known old dependency), one internal Ubuntu, one Windows Server. Discover: run an authenticated OpenVAS/Greenbone scan or nmap --script vuln against the lab range [verify scanner setup on your distro], export findings. Enrich: manually stamp EPSS (from FIRST’s site) and KEV status for each finding; mark any CVE absent from both as exploit_maturity=none. Prioritize: run vulnprio with the export; confirm the internet-facing finding with the highest EPSS lands P1 — override the tiers in one row to simulate a mis-classification and see the audit trail you’d want. Remediate: patch or reconfigure one finding, re-scan to verify, log opened/closed dates. Measure: fabricate a two-quarter record set (or use your real logs) and run sladrift; deliberately close one P1 past SLA and confirm the regression fires. Debrief: which step produced the most surprises — inventory, enrichment, or verification — and what does that say about where your program would fail first?
Interview Questions
Section titled “Interview Questions”- Technical: Contrast CVSS, EPSS, and KEV as prioritization signals — what question does each answer, and why does the composite order queues better than any alone?
- Technical: Why does
vulnpriotreat KEV as a likelihood floor rather than a bonus multiplier, and when would that choice misfire? - Scenario: A P1 with a 7-day SLA has been open 12 days and the asset owner says the vendor patch breaks production. Walk your decision: virtual patch, exception, or escalation — and what evidence each requires.
- Scenario: On-time rates look excellent but re-open rate doubled this quarter. Diagnose what’s happening before you congratulate anyone.
- Conceptual: Why is an exception without an expiry worse than a finding without an owner?
- Deep-dive: Design the VM function for a 5,000-asset hybrid estate: scan architecture, enrichment pipeline, tier SLAs, exception governance, and the three metrics you’d take to the board.
- Practical: You inherit a program with 40,000 open findings, no tiers, and an angry platform team. What do you do in the first 30 days — and what do you explicitly stop doing?
Related Modules
Section titled “Related Modules”BT3 (SIEM Engineering & Log Pipelines) · BT9 (Detection Engineering & Sigma) · BT16 (Cloud Detection & Response) · BT20 (Detection Program Metrics & Maturity) · H10 (Supply Chain & Update Security)