The Ops Atlas xOps School

Security & Compliance Emerging practice Sources verified

VulnerabilityOps

VulnerabilityOps (usually "VulnOps") runs vulnerability management as a continuous flow: decide what must be fixed by when from exploitation evidence and asset exposure, hand dated work to owners, verify every fix, and keep remediation capacity ahead of arrivals.

Key takeaways

  • "VulnOps" is an emerging label for an established practice. The Cloud Security Alliance calls it "an operating discipline rather than a product category"; no standards body defines it, and early uses differ in emphasis.
  • You cannot fix everything. A 2019 vendor-commissioned study found a typical organisation could remediate about one in ten open vulnerabilities a month, so the core skill is choosing what to leave and saying so.
  • CVSS severity scores are not risk. Combine exploitation evidence (CISA's Known Exploited Vulnerabilities catalogue, KEV), exploitation probability (EPSS) and your own exposure and impact; never multiply EPSS by CVSS.
  • CISA's BOD 26-04 (June 2026) replaced CVSS bands with clocks from four facts: public exposure, KEV status, automatability and technical impact. It binds only US federal civilian agencies, but it is a detailed, public model to adapt.
  • Public enrichment is now partial: since April 2026 NIST fully enriches only prioritised CVEs. Pipelines must tolerate missing severity scores and product identifiers (CPE) and use explicit defaults.
  • Manage the flow: compare arrivals with verified closures (saturation), watch backlog age, and close a finding only when a rescan or test proves the fix.

VulnerabilityOps is vulnerability management run as a continuous operation. Findings from scanners and software inventories arrive daily. A written policy sets each one’s urgency from exploitation evidence, the asset’s exposure and the possible damage; the finding goes to the asset’s owner with a due date, closes only when a rescan or test proves the fix, and the team watches whether fixes keep up with arrivals. The Cloud Security Alliance (CSA): “VulnOps reframes vulnerability management as a continuous flow problem rather than a queue problem.”[1] For analysts, security engineers, the owners who fix, and leaders.

Question Answer
Goal Exploitable exposure falls faster than new exposure arrives
Users Vulnerability analysts, security engineers, asset and platform owners, CISOs
In scope Coverage, CVE/CWE, CVSS limits, EPSS, KEV, SSVC (glossary), clocks, routing, verification, exception intake, disputes, flow metrics, feed quality
Out of scope Patch rollout mechanics (PatchOps), pipeline gates (DevSecOps), risk-acceptance policy (RiskOps), incident response (ResponseOps)
Prerequisites Inventory with owners and exposure tags; a scanner; ticketing
Key outputs Remediation policy, dated owner queues, verification evidence, monthly flow report

The inflow grows: the CVE (Common Vulnerabilities and Exposures) Program published 18,375 records in 2020, 48,244 in 2025 and 35,872 in the first half of 2026 alone[5]. Capacity is limited: a 2019 study of about 300 organisations, by the Cyentia Institute for the vendor Kenna Security, found that “A typical organization, regardless of asset complexity, will have the capacity to remediate about one out of every 10 vulnerabilities in their environment within a given month”[6]. And exploited flaws go fast: for the 138 vulnerabilities disclosed in 2023 that Mandiant tracked as exploited in the wild (70% as zero-days), average time-to-exploit was five days after removing outliers (47 with them), down from 63 days in 2018–2019[7]. Few flaws are exploited, but those that are go fast, so exploitation evidence and exposure should set the clock. In Verizon’s 2026 report, exploitation was the top initial access vector in its breach dataset (31%), and organisations fully remediated only 26% of vulnerabilities in the Known Exploited Vulnerabilities (KEV) catalogue of CISA, the US Cybersecurity and Infrastructure Security Agency, in 2025 (38% in 2024), with a median of 43 days[8].

Without an operating model, programmes rank by Common Vulnerability Scoring System (CVSS) severity, ticket everything above a threshold and miss most dates. NIST, the US standards body, warns that “Overly simplistic metrics, such as counting the number of vulnerabilities … are not actionable”[9]. VulnerabilityOps replaces the ranked list with a policy that rations capacity on purpose, a pipeline that proves fixes, and metrics that show whether the system keeps up.

  • Not patch rollout. VulnerabilityOps decides what must be fixed and by when; PatchOps decides how to roll it out safely.
  • Not pipeline security. Blocking vulnerable code before release is DevSecOps; this page handles what is deployed.
  • Not risk-acceptance policy (RiskOps) or the owners-and-windows contract (SecOps).
  • Not the supplier side. Flaws in products you sell are product security incident response (PSIRT) work, about vulnerabilities “within the products … which an organization produces and/or sells”[10].
  • Not a tool. CSA says VulnOps does not replace scanners or patch platforms; it reorganises how their output is used[1].

Vulnerability management is decades old: NIST’s patch guidance reached its fourth revision in 2022[9]. CVSS has been the default severity signal since FIRST (the Forum of Incident Response and Security Teams) became its custodian in April 2005 (v4.0: November 2023)[11][12]; the newer signals are the Exploit Prediction Scoring System (EPSS; since April 2021; model v5 since 15 June 2026)[13], CISA’s KEV catalogue (2021)[14] and Stakeholder-Specific Vulnerability Categorization (SSVC) decision trees[15].

The label’s documented history is short: the earliest use found here is the October 2025 essay[2], though CSA says it “has been circulating in security operations communities for several years”. CSA’s May 2026 note names AI-driven discovery and NIST’s April 2026 triage of the National Vulnerability Database (NVD) among the forces behind it[1]. Independently, CISA’s Binding Operational Directive (BOD) 26-04 (June 2026) moved US federal policy the same way, replacing severity bands with exposure- and exploitation-based clocks[14].

Term Meaning on this page
Vulnerability “a weakness in an information system … that could be exploited or triggered by a threat source” (NIST)[16]
CVE record, CNA Common Vulnerabilities and Exposures: one public ID per publicly disclosed vulnerability, published by CVE Numbering Authorities (CNAs)[17]
CWE Common Weakness Enumeration, “a community-developed list of common software and hardware weaknesses”: the type of flaw[18]
CPE Common Platform Enumeration: product identifiers that NVD enrichment adds and scanners depend on to match CVEs to software[19][1]
Finding (instance) One vulnerability on one asset or image: the unit counted, routed and closed
CVSS; -B, -BT, -BE, -BTE Common Vulnerability Scoring System; v4.0 scores are labelled by metric groups used: Base, Threat, Environmental[20]
EPSS Exploit Prediction Scoring System: probability of exploitation activity in the next 30 days, with a percentile rank[21]
KEV Known Exploited Vulnerabilities: CISA’s catalogue of vulnerabilities with reliable evidence of exploitation[22]
SSVC, ADP, Vulnrichment Stakeholder-Specific Vulnerability Categorization: decision trees that output an action. CISA adds SSVC points to CVE records in its Authorized Data Publisher (ADP) container: “Vulnrichment”[23]
VEX Vulnerability Exploitability eXchange: a supplier’s machine-readable statement that a product is or is not affected[24]
SBOM, SCA Software bill of materials (component list); software composition analysis (matching dependencies to advisories)
FCEB Federal Civilian Executive Branch: the US agencies BOD 26-04 binds[14]
Clock, saturation Clock: maximum time to remediate from a trigger such as KEV listing or detection[14]. Saturation: arrivals ÷ remediation throughput (CSA)[1]; here, verified closures only
Verified fix Closure backed by a rescan, configuration test, new image digest or upheld dispute, not by ticket status

Mental model: a sink you will never empty. The tap is the arrival rate of findings, the drain the rate of verified fixes. Little’s Law: average items in a queuing system equal their arrival rate times the average time each spends in it[25]. Four levers: turn down the tap (fewer vulnerable components), widen the drain (automation, change windows), choose which water matters (prioritisation), check the drain (verification).

Neighbour What the neighbour owns Handoff with VulnerabilityOps
SecOps Security–IT contract: owners, maintenance groups, exception register Owners and windows in; joint scorecard out
PatchOps Rings, windows, rollback, reboots Dated, prioritised item out
DevSecOps Build-time gates, SBOM production SBOMs in; fixes for shipped code out
ContainerOps Image builds, base-image updates Rebuild request out
ThreatOps Intelligence and hunts Exploitation evidence in
SOC Ops, ResponseOps Triage, forensics, incidents Forensic-triage request out
RiskOps Who may accept which risk Exception request out
CloudSecOps, KubernetesSecOps, EndpointOps Posture, admission, device agents Exposure and coverage facts in
Figure 1. Where VulnerabilityOps sits in the security operations flow
ThreatOps: intelligenceDevSecOps: SBOMsInventory: owners, exposureSecOps: owners, windowsVulnerabilityOps: what, bywhen, verifiedPatchOps: rolloutContainerOps: rebuildsRiskOps: acceptanceSOC and ResponseOps:triage
Diagram source (Mermaid)
flowchart LR
  TI["ThreatOps: intelligence"]
  DS["DevSecOps: SBOMs"]
  INV["Inventory: owners, exposure"]
  SEC["SecOps: owners, windows"]
  V["VulnerabilityOps: what, by when, verified"]
  PO["PatchOps: rollout"]
  CO["ContainerOps: rebuilds"]
  RO["RiskOps: acceptance"]
  SO["SOC and ResponseOps: triage"]
  TI --> V
  DS --> V
  INV --> V
  SEC -.-> V
  V --> PO
  V --> CO
  V --> RO
  V --> SO
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class V core
  class TI,DS,INV,SEC,PO,CO,RO,SO neutral

Text equivalent: ThreatOps intelligence, DevSecOps SBOMs, the owner-and-exposure inventory and SecOps windows (dotted) feed VulnerabilityOps, which hands work to PatchOps (rollout), ContainerOps (rebuilds), RiskOps (acceptance) and SOC Ops and ResponseOps (triage).

  1. Ration on purpose. Focus on the highest risk, not all vulnerabilities equally, as BOD 26-04 does[14].
  2. Severity is an input, not a decision (see CVSS and its limits).
  3. Evidence beats prediction; prediction beats guessing. FIRST’s own advice: when a vulnerability appears on KEV, “treat it as actively exploited and prioritize accordingly, regardless of EPSS score”[26].
  4. The clock follows the facts. “The timelines for each asset are dynamic as facts change”[14].
  5. No owner, no fix. Every finding goes to the team that can change the asset, via the queue agreed in SecOps.
  6. Closed means verified (see Lifecycle).
  7. Measure the flow, not the pile.
  8. Remediation is engineering: “treating remediation as code”[1].

Non-goals: zero findings, scanner-score league tables, replacing the change process.

R = responsible, A = accountable, C = consulted, I = informed. Atlas proposal; adapt the names.

Activity VulnerabilityOps Asset owner PatchOps / platform Risk owner SOC
Discovery and coverage A/R C C I I
Remediation policy (versioned) A/R C C C C
Tier and due date per finding A/R I I I I
Fix or mitigate C A R I I
Verify and close A/R I C I I
Exception R (intake) R (request) C A I
Forensic triage of exploited assets C C C I A/R

Analysts need systems basics, signal literacy, SQL or Python and diplomacy; small firms make it a part-time SecOps role.

  • Day 0, build: inventory and exposure tags, discovery sources, policy v1 with defaults, routing agreed with SecOps and PatchOps, verification rules.
  • Day 1, start the clocks: tier the backlog (KEV first; do not try to clear it), open owner queues, publish a flow baseline.
  • Day 2, run: the cadences below; automate the high-volume tail.

NIST’s life cycle is the same: know, plan, then prepare, implement, verify and continuously monitor[9].

Cadence Forum Output
Continuous Pipeline (automated) Tiered, dated findings in owner queues
Daily KEV and emergency-advisory check Tier-1 items, forensic-triage requests
Weekly Owner sync per large owner group Commitments and escalations
Monthly Flow review with security leadership Capacity or policy decision
Quarterly Policy review Policy version N+1

Escalation (Atlas proposal): a tier-1 item at half its clock without a plan goes to the owner’s manager; at the clock, to the CISO and the risk owner. CIS Safeguard 7.2 asks for a risk-based remediation process “with monthly, or more frequent, reviews”[27].

A pipeline with a policy engine in the middle; a commercial platform, open-source tools plus a database, or a spreadsheet can play these roles.

Figure 2. Reference architecture: from discovery to verified fix
still presentfixedexpiredHost scansSCA and image scansAttack surfaceNormalise: asset x CVESignals: KEV, EPSS, SSVC,VEXContext: owner, exposurePolicy engine: tier, due dateOwner queuesAutomated fix jobException to RiskOpsVerify: rescan or testFlow metrics
Diagram source (Mermaid)
flowchart TB
  S1["Host scans"]
  S2["SCA and image scans"]
  S3["Attack surface"]
  N["Normalise: asset x CVE"]
  P["Signals: KEV, EPSS, SSVC, VEX"]
  A["Context: owner, exposure"]
  E["Policy engine: tier, due date"]
  Q["Owner queues"]
  AU["Automated fix job"]
  X["Exception to RiskOps"]
  VF["Verify: rescan or test"]
  M["Flow metrics"]
  S1 --> N
  S2 --> N
  S3 --> N
  N --> E
  P --> E
  A --> E
  E --> Q
  E --> AU
  E --> X
  Q --> VF
  AU --> VF
  VF -- "still present" --> Q
  VF -- "fixed" --> M
  X --> M
  X -- "expired" --> Q
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class N,E,VF core
  class P,A decision
  class S1,S2,S3,Q,AU,X,M neutral

Text equivalent: Host, composition, image and attack-surface scans feed a normalisation step (one record per asset and CVE). A policy engine adds public signals and asset context and assigns a tier and due date. Work goes to owner queues, an automated fix job or an exception to RiskOps. Failed verifications and expired exceptions return to the owner queue; verified fixes and exceptions feed the flow metrics.

Component Design note
Normalisation One record per asset × vulnerability × component; keep the first-seen date or every rescan resets the clock
Asset context The weakest link; BOD 26-04 asks agencies to tag organisation, environment, exposure and asset type[14]
Signal store Keep the daily snapshot used for each decision so an audit sees what you knew then
Policy engine Deterministic, versioned like code, with explicit defaults for missing data
Verification Authenticated scans or tests; ticket status alone never closes

Happy path. A new KEV entry matches two internet-facing gateways:

Figure 3. Handoffs when a KEV entry hits two exposed gateways
PatchOps rolloutAsset ownerVulnerabilityOpsSOC and ResponseOpsKEV lists a CVE: exposed, total controlClock: 3 days plus forensic triageMatch inventory: 2 exposed gatewaysForensic triage requestVolatile evidence collectedTier 1 ticket, due in 3 daysEmergency changeFixed build, sessions resetMarked fixedAuthenticated rescan: not detectedNo evidence of compromiseClose with evidence
Diagram source (Mermaid)
sequenceDiagram
  participant SOC as SOC and ResponseOps
  participant V as VulnerabilityOps
  participant O as Asset owner
  participant P as PatchOps rollout
  Note over V: KEV lists a CVE: exposed, total control
  Note over V: Clock: 3 days plus forensic triage
  V->>V: Match inventory: 2 exposed gateways
  V->>SOC: Forensic triage request
  SOC-->>V: Volatile evidence collected
  V->>O: Tier 1 ticket, due in 3 days
  O->>P: Emergency change
  P-->>O: Fixed build, sessions reset
  O-->>V: Marked fixed
  V->>V: Authenticated rescan: not detected
  SOC-->>V: No evidence of compromise
  V->>V: Close with evidence

Text equivalent: A KEV entry giving total control of exposed assets sets a three-day clock with triage. VulnerabilityOps matches two gateways, asks the SOC to triage, then raises a tier-1 ticket. The owner’s emergency change applies the fix and resets sessions. A rescan no longer detects the flaw, the SOC finds no compromise, and the finding closes with its evidence.

Evidence comes first because CISA’s forensic triage steps say not to “alter or remediate systems prior to evidence/artifact collection when possible”[28].

Exception paths.

  • No fix exists: mitigate and re-tier. SSVC notes that “Applying mitigations may change the value of decision points”, but a firewall rule is “only valid until someone changes the rule”[29].
  • Owner cannot meet the clock: exception request with compensating control and expiry (RiskOps). BOD 26-04 allows none (“CISA does not issue waivers or exceptions”), and for KEV entries, apart from removing the asset, CISA calls the catalogue’s required actions “the only known technical mitigation”[28]; a control that leaves the service reachable does not count. The Atlas policy allows exceptions as a declared local change, but for KEV and T1 items only removal or isolation, for at most 7 days.
  • Owner disputes the finding: see the false-positive practice in Core practices.
  • Verification fails: back to the owner with evidence; the clock keeps running.
  • Not affected: a justified supplier VEX not_affected statement, or proof the component is absent, closes it[24].
  • Compromise found: the incident moves to ResponseOps.

Prioritisation methods, standards and frameworks

Link to section “Prioritisation methods, standards and frameworks”

Rule of thumb: use each signal for the question it answers.

Signal Question it answers Publisher Main limit
CVSS-B How bad is the flaw in the worst reasonable case? CNA or NVD (FIRST standard) Not risk; often missing now
EPSS How likely is exploitation activity in 30 days, across everyone? FIRST EPSS SIG Population-level
KEV Has it been exploited, with a clear fix? CISA Lags; US-government lens
SSVC points Automatable? How much control? CISA Vulnrichment, your analysts Gaps; snapshots
Exposure, criticality Can an attacker reach it here, and does it matter? You Only as good as your inventory

CVSS v4.0 scores a flaw’s intrinsic characteristics, labelled CVSS-B, -BT, -BE or -BTE by the metric groups used[20]. The User Guide says the Threat group adjusts the “reasonable worst case” Base score using threat intelligence. Base scores “should not be used alone to assess risk”[30].

Use CVSS as a coarse severity filter and a source of technical impact. Expect gaps: since April 2026 NIST no longer routinely scores where the CNA has[31]. Many records carry only v3.1 scores (every scored record in Lab 1 does); use their Base score the same way, but the -B/-BT labels apply to v4.0 only.

EPSS “estimates the probability that a published CVE will be exploited in the wild in the next 30 days”, daily, with a percentile rank[21]. Three facts from FIRST matter:

  • The base rate is low. In any 30-day period FIRST sees exploitation activity for “anywhere from 1.5% to 3% of published vulnerabilities”, so a 50% threshold selects almost nothing. FIRST equates the effort of a “CVSS Critical” policy with roughly the 90th percentile (a score near 0.04), and “CVSS High and above (approximately the top 48% of published vulnerabilities)” with about 0.008[26].
  • It is not about your environment. Localise it by presence, reachability and consequence[26]; FIRST lists “Observation bias in training labels” among its limits[32].
  • Never combine it arithmetically with CVSS. “Do not multiply an EPSS score by an ordinal (such as CVSS)”[26].

FIRST frames the trade-off as coverage (share of exploited vulnerabilities you acted on) versus efficiency (share of your actions that hit exploited ones): recall and precision[32]. Model v5 replaced v4 on 15 June 2026, so time series shift there[13].

CISA calls KEV “the authoritative source of vulnerabilities that have been exploited in the wild”[22].

  • Entry rules. A CVE ID, reliable evidence of active exploitation, clear remediation and significant risk to US government systems; CISA “aims to update the KEV within 24 hours”[28]. Exploitability alone does not qualify; attempted exploitation, even against a honeypot, does[33].
  • The feed. On 16 October 2026 the catalogue held 1,748 entries. Each JSON entry carries a forensicTriage flag beside dueDate. Of the 131 entries added since BOD 26-04, 108 were due three days after listing (87 with triage) and 23 after fourteen days[34]. Counts change with each catalogue update; recount from the feed.
  • dueDate is CISA’s default, computed from Table 1 with the evidence of public exposure it has; “the final determination for asset exposure is determined by the agency”[28]. Compute your own clock per asset (Lab 2).
  • Cautions. An old CVE newly added “does not necessarily indicate that CISA is currently observing active exploitation”, and KEV is not the only list to track[28].

SSVC and BOD 26-04: decisions instead of scores

Link to section “SSVC and BOD 26-04: decisions instead of scores”

SSVC walks a decision tree to an action: CISA’s version ends in Track, Track*, Attend or Act[35]; the CERT/CC deployer model ends in Defer, Scheduled, Out-of-Cycle or Immediate from Exploitation, System Exposure, Automatable and Human Impact[29]. CISA publishes three decision points per CVE (Exploitation, Automatable, Technical Impact) as Vulnrichment data in the CVE record[23].

BOD 26-04 turns those points plus public exposure into clocks. It revoked BOD 19-02 and BOD 22-01, and its guidance notes that “the FCEB no longer requires CVSS use for vulnerability prioritization”[14][28]. Table 1, transcribed from the directive’s image (calendar days; “triage” = forensic triage within the same three days)[14]:

Publicly exposed In KEV Automatable Total control Partial control
Yes Yes Yes 3 days + triage 3 days
Yes Yes No 3 days + triage 14 days
Yes No Yes 3 days 14 days
Yes No No 14 days 60 days
No Yes Yes 3 days + triage 14 days
No Yes No 14 days 14 days
No No Yes 60 days 60 days
No No No Fix on system upgrade Fix on system upgrade

The clock starts at KEV listing or detection on an asset, “whichever event occurs first”; unknown Automatable and Technical Impact for a non-KEV CVE count as “no” and “total”; credential leaks count as total control; timelines are maximums[14][28].

Figure 4. Atlas proposal: combining presence, KEV, exposure and EPSS
No: absent or VEXnot_affectedYesYesNoYesNoYesNoYesNoFinding: asset x CVEPresent and affected?Close with evidenceIn KEV?Table 1 clock: 3 or 14 daysPublicly exposed?Table 1 clock: 3, 14 or 60daysAutomatable?60 daysEPSS in the top 10%?Weekly review: may move to60 daysFix on next upgrade
Diagram source (Mermaid)
flowchart TB
  F["Finding: asset x CVE"] --> P{"Present and affected?"}
  P -- "No: absent or VEX not_affected" --> C1["Close with evidence"]
  P -- "Yes" --> K{"In KEV?"}
  K -- "Yes" --> T1["Table 1 clock: 3 or 14 days"]
  K -- "No" --> X{"Publicly exposed?"}
  X -- "Yes" --> T2["Table 1 clock: 3, 14 or 60 days"]
  X -- "No" --> A{"Automatable?"}
  A -- "Yes" --> T3["60 days"]
  A -- "No" --> E{"EPSS in the top 10%?"}
  E -- "Yes" --> R["Weekly review: may move to 60 days"]
  E -- "No" --> U["Fix on next upgrade"]
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class P,K,X,A,E decision
  class T1,T2 risk
  class C1,T3,R,U neutral

Text equivalent: Absent or “not affected” findings close with evidence. KEV findings get a Table 1 clock of 3 or 14 days; non-KEV findings on exposed assets 3, 14 or 60 days; internal automatable ones 60 days. Internal non-automatable findings wait for the next upgrade unless EPSS is in the top 10% (90th percentile or above), which triggers a weekly review (the Atlas’s addition).

Table 1 has no asset-criticality input. Its four facts say nothing about what the asset does, so an internal, non-automatable flaw on a crown-jewel database waits for an upgrade, as on a test box. CISA handles this outside the table: agencies must document “explicit, transparent risk decisions that weigh mission needs—especially availability and integrity of high value assets (HVAs) and mission critical functions—against potential impacts of exploitation”[28]; SSVC carries it as Human Impact or Mission Prevalence[29][35]. The Atlas policy adds a criticality tag as a local change: T4 findings on critical assets become T3, and promotions on them need no review.

Exploitation confirmed by a vendor or national CERT before CISA lists it can count as KEV by local policy; record the source. Flaws without CVE IDs (misconfigurations, custom web-application flaws) still need tiers: “The CVE program is not a comprehensive list of cybersecurity vulnerabilities”[28].

  • NVD enrichment is partial by policy. Since 15 April 2026 NIST prioritises CVEs in KEV, software used by the federal government and EO 14028 critical software; the rest are “lowest priority”, and backlogged CVEs published before 1 March 2026 moved to “Not Scheduled”[31][19]. On 17 October 2026 the NVD dashboard listed 57,074 CVEs as Deferred (the API name for Not Scheduled) and 7,563 awaiting enrichment (live counts; re-read the dashboard)[36]. Tools that rely on NVD CPE data will miss matches.
  • Use more than one source. CSA notes that the CVE Program “has weathered repeated funding scares” and recommends pairing NVD with a source for the unenriched tail[1]; ENISA’s European Vulnerability Database is operational under NIS2[37].
Practice Trigger Output Verification
Coverage reconciliation Weekly Inventory assets without a recent authenticated scan Coverage metric
KEV-day playbook New KEV entry Matched assets, clocks, triage requests Rescan within the clock
Advisory triage Vendor emergency advisory Provisional tier before KEV or EPSS move Re-tier when data arrives
Owner routing New tiered finding Ticket in the owner’s queue Acknowledgement
Rebuild instead of patch Image or immutable-host finding New image digest deployed Old digest gone from runtime
Exception intake Clock cannot be met Time-limited exception Expiry check and rescan
False-positive dispute Owner rejects a finding with evidence Package-aware or authenticated recheck; internal “not affected” with reason and expiry Rescan at expiry; dispute rate per plugin
Verification sweep Ticket marked fixed Closed with evidence, or reopened Authenticated rescan or test (configuration review on OT)

Two variants matter. Disputes: Red Hat warns that “just looking at the version number of a package will not tell them if they are vulnerable or not”; scanners that decide by version alone report backported fixes as false positives[38]. Without a dispute path such findings never close under a rescan rule, and owners stop trusting the queue. Rebuild-as-remediation: CSA argues that a vulnerable container should default to “an automated rebuild against a patched base image, with a tested rollback path, rather than a human ticket”[1]. For real flaws, NIST’s point stands: patching or upgrading are “the only forms of risk response that can completely eliminate the vulnerabilities without removing functionality”[9]. CIS Control 7 cadences: internal scans at least quarterly (authenticated and unauthenticated), external scans and remediation at least monthly[27].

Examples verified 2026-10-16.

Tool category Job Examples Watch-outs
Network and host scanners Missing patches, weak configurations Tenable Nessus; Qualys[39]; Greenbone OpenVAS (open source)[40] Unauthenticated scans miss local flaws[41]; credential failures look like progress
Image and SCA scanners Match components to advisories Trivy[42]; Grype (EPSS, KEV and OpenVEX support)[43] Third-party code with access to your source[44]
Prioritisation platforms Deduplicate, enrich, score, route Commercial risk-based platforms; scanner vendors’ scores Partly opaque: Qualys lists a CVE score’s inputs, not their weights[39]
Ticketing and IT service management (ITSM) Owner queues, change records Your existing ITSM “Resolved” is not “verified”

Choose on authenticated and appliance coverage, package-aware detection, VEX input, export APIs and transparent rules.

Automate the deterministic steps first: ingestion, deduplication, enrichment, tiering, routing, rebuilds, rescans and reports. Keep people on policy changes, exceptions, disputes and risky changes to critical systems; SANS describes routing each system to “automatic patching, test before deployment, or manual review, with a human owning the final call”[3]. Language models can summarise advisories and draft tickets, but a wrong product or version match silently opens or closes findings: require a source link for every model claim and keep the policy engine deterministic.

The core artefact is a remediation policy written as data, versioned and read by the policy engine. This Atlas proposal adapts Table 1 for a non-federal organisation and lists every local change; the risk owner signs it.

Example status: syntax-checked (YAML), not deployed; values are illustrative.

policy: remediation-clocks
version: 3
effective: 2026-10-01
owner: vuln-mgmt@example.com
approved_by: risk-committee            # RiskOps owns acceptance authority
basis: "CISA BOD 26-04 Table 1, adapted; local changes listed below"
inputs:
  exposure: inventory.tags.exposure                 # public | internal
  kev: cisa_kev_feed                                # daily snapshot
  automatable: vulnrichment.ssvc.automatable        # missing, non-KEV -> "no"
  technical_impact: vulnrichment.ssvc.technical_impact   # missing -> "total"
  epss_percentile: first_epss_daily_file            # daily snapshot
  criticality: inventory.tags.criticality           # critical | standard; not in Table 1
clock:
  starts: earliest_of_kev_added_or_first_detected
  unit: calendar_days
  recompute_when: [exposure_changes, kev_added, mitigation_verified]
tiers:                           # "+t" in the decision table adds forensic triage
  T1: {days: 3}
  T2: {days: 14}
  T3: {days: 60}
  T4: {days: null, rule: next_scheduled_upgrade}
decision_table:                  # Table 1 as tiers: [total control, partial control]
  public:
    kev_automatable: [T1+t, T1]
    kev_not_automatable: [T1+t, T2]
    automatable: [T1, T2]
    not_automatable: [T2, T3]
  internal:
    kev_automatable: [T1+t, T2]
    kev_not_automatable: [T2, T2]
    automatable: [T3, T3]
    not_automatable: [T4, T4]
local_changes:
  - "Exceptions are allowed (BOD 26-04 allows none); risk acceptance follows RiskOps."
  - "Assets without an exposure tag are treated as public until tagged."
  - "Past its clock at first detection: T1, due 3 days after detection, reported overdue."
  - "Internal, non-automatable findings at EPSS percentile >= 0.90 get a weekly review."
  - "Analysts may promote, never demote, a tier; the reason is recorded."
  - "Criticality (absent from Table 1): T4 on a critical asset is T3; promotions there need no review."
verification:
  closes_only_with: [authenticated_rescan, config_test, image_digest_change,
                     upheld_dispute]
  ticket_status_alone: false
exceptions:
  max_days: {T1: 7, T2: 30, T3: 90}
  requires: [compensating_control, expiry_date, rescan_plan, risk_owner]
  kev_or_T1:                           # CISA: required action or removal, nothing else
    accepted_controls: [remove_from_network, isolate_vulnerable_service]
    sign_off: [ciso, risk_owner]
reporting:
  monthly: [saturation_by_tier, overdue_by_tier, coverage, dispute_rate,
            exceptions_past_expiry]

The engine reads decision_table, which matches Table 1 and CERT/CC’s machine-readable BOD 26-04 table[45]. “Untagged = public” is conservative and can flood the 3-day tier in a poorly tagged estate. CISA’s cheaper fallback: public if the asset has a publicly routable IP address or the asset type “network”, or, for a KEV CVE, if the software is generally public-facing; otherwise internal[28].

The second artefact, the hand-off record sent with each finding to the owner or PatchOps, freezes the signals behind the decision for the auditor.

Example status: syntax-checked (JSON); identifiers are fictitious.

{
  "finding_id": "VF-2026-10-004211",
  "asset": {"id": "vpn-gw-01", "owner_group": "network-ops",
            "exposure": "public", "maintenance_group": "edge-appliances"},
  "vulnerability": {"cve": "CVE-2023-4966", "advisory": "<vendor advisory URL>"},
  "signals_snapshot": {"taken": "2026-10-16T09:00:00Z", "kev": true,
                       "kev_added": "2023-10-18", "epss": 0.99999,
                       "automatable": "yes", "technical_impact": "total"},
  "decision": {"tier": "T1", "policy_version": 3, "clock_start": "2023-10-18",
               "first_detected": "2026-10-14", "overdue_on_detection": true,
               "due": "2026-10-17", "forensic_triage": true},
  "required_actions": ["preserve volatile evidence", "install fixed build",
                       "kill active and persistent sessions"],
  "verification": {"method": "authenticated_rescan",
                   "evidence": "credentialed checks: yes"},
  "handoff": {"to": "patchops", "change_type": "emergency"}
}

All three labs were executed on 2026-10-16 and re-run on 2026-10-17 (Ubuntu 20.04, Python 3.8.10); outputs are real. Lab 1’s live numbers change daily.

Lab 1 (beginner): enrich CVEs with KEV, EPSS and Vulnrichment

Link to section “Lab 1 (beginner): enrich CVEs with KEV, EPSS and Vulnrichment”

Objective. See what each public signal says about the same CVEs, and where data is missing. Prerequisites. Python 3.8+, outbound HTTPS to cisa.gov, api.first.org and cveawg.mitre.org. Cost and risk. Free and read-only. In production, pull the daily EPSS file and the KEV JSON in bulk, which FIRST calls “more efficient than querying the API per CVE”[13]; per-CVE queries also reveal what you run.

  1. Create cves.txt with one ID per line: CVE-2023-4966, CVE-2021-44228, CVE-2024-3094, CVE-2020-14343, CVE-2023-38545, CVE-2024-6387.
  2. Save the script as enrich.py:

Example status: executed on 2026-10-17 against the live KEV feed (catalogVersion 2026.10.16), the EPSS API and the CVE Services API.

#!/usr/bin/env python3
"""Enrich CVE IDs with three free public signals: KEV, EPSS and Vulnrichment (SSVC)."""
import json
import sys
import urllib.request

KEV_URL = ("https://www.cisa.gov/sites/default/files/feeds/"
           "known_exploited_vulnerabilities.json")
EPSS_URL = "https://api.first.org/data/v1/epss?cve="
CVE_URL = "https://cveawg.mitre.org/api/cve/"
HEADERS = {"User-Agent": "atlas-vulnops-lab/1.0 (contact: you@example.com)"}


def get_json(url):
    req = urllib.request.Request(url, headers=HEADERS)
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.load(resp)


def ssvc_and_cvss(record):
    """Return CISA ADP SSVC options and the first CVSS base score (CNA, then ADP)."""
    ssvc, cvss = {}, None
    containers = [record["containers"]["cna"]] + record["containers"].get("adp", [])
    for container in containers:
        for metric in container.get("metrics", []):
            other = metric.get("other", {})
            if other.get("type") == "ssvc":
                for option in other["content"]["options"]:
                    ssvc.update(option)
                # The SSVC timestamp is not a reliable assessment date; the ADP
                # container's dateUpdated says when CISA last changed its data.
                ssvc["ts"] = other["content"].get("timestamp", "")[:10]
                meta = container.get("providerMetadata", {})
                ssvc["updated"] = meta.get("dateUpdated", "")[:10]
            for key in ("cvssV4_0", "cvssV3_1", "cvssV3_0"):
                if cvss is None and key in metric:
                    cvss = "%s %s" % (key[4:].replace("_", "."), metric[key]["baseScore"])
    return ssvc, cvss


def main(path):
    cves = [line.strip() for line in open(path) if line.strip()]
    kev = {v["cveID"]: v for v in get_json(KEV_URL)["vulnerabilities"]}
    epss = {row["cve"]: row for row in get_json(EPSS_URL + ",".join(cves))["data"]}
    rows = []
    for cve in cves:
        ssvc, cvss = ssvc_and_cvss(get_json(CVE_URL + cve))
        k = kev.get(cve, {})
        e = epss.get(cve, {})
        rows.append({
            "cve": cve,
            "kev": bool(k),
            "kev_added": k.get("dateAdded", ""),
            "ransomware": k.get("knownRansomwareCampaignUse", ""),
            "epss": float(e.get("epss", 0.0)),
            "percentile": float(e.get("percentile", 0.0)),
            "exploitation": ssvc.get("Exploitation", "n/a"),
            "automatable": ssvc.get("Automatable", "n/a"),
            "technical_impact": ssvc.get("Technical Impact", "n/a"),
            "ssvc_ts": ssvc.get("ts", ""),
            "adp_updated": ssvc.get("updated", ""),
            "cvss": cvss or "none",
        })
    print("%-14s %-3s %-10s %-7s %5s %5s %-6s %-4s %-7s %-10s %s" % (
        "cve", "kev", "kev_added", "ransom", "epss", "pctl", "exploit", "auto", "impact",
        "adp_upd", "cvss"))
    for r in rows:
        print("%-14s %-3s %-10s %-7s %5.3f %5.3f %-6s %-4s %-7s %-10s %s" % (
            r["cve"], "yes" if r["kev"] else "no", r["kev_added"] or "-",
            r["ransomware"] or "-",
            r["epss"], r["percentile"], r["exploitation"], r["automatable"],
            r["technical_impact"], r["adp_updated"] or "-", r["cvss"]))
    with open("enriched.json", "w") as out:
        json.dump(rows, out, indent=1)


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "cves.txt")
  1. Run python3 enrich.py cves.txt.

Expected output (17 October 2026; EPSS values rounded to three decimals):

cve            kev kev_added  ransom   epss  pctl exploit auto impact  adp_upd    cvss
CVE-2023-4966  yes 2023-10-18 Known   1.000 1.000 active yes  total   2026-07-31 V3.1 9.4
CVE-2021-44228 yes 2021-12-10 Known   1.000 1.000 active yes  total   2025-10-21 V3.1 10
CVE-2024-3094  no  -          -       0.909 0.998 none   yes  total   2024-07-30 V3.1 10
CVE-2020-14343 no  -          -       0.060 0.932 n/a    n/a  n/a     -          none
CVE-2023-38545 no  -          -       0.787 0.996 poc    no   total   2025-08-27 V3.1 8.8
CVE-2024-6387  no  -          -       0.995 0.999 poc    no   total   2024-07-02 V3.1 8.1

What to notice. CVE-2020-14343 has no CVSS score or SSVC data: the enrichment gap your policy must default. exploit is CISA’s view as of its ADP container’s last update (adp_upd): CVE-2024-3094 reads none from July 2024 while its EPSS is high today. The SSVC timestamp (ssvc_ts in the JSON) is no assessment date: CVE-2023-4966’s reads 2023-10-10, before the ADP programme began in 2024[23]. A score of 0.060 sits at the 93rd percentile because the base rate is so low. Validation. Spot-check one row on the KEV website and the EPSS API. Troubleshooting. HTTP 403 or 429: slow down, keep a descriptive User-Agent; HTTP Error 404: the ID is reserved or unassigned (no record yet); a rejected record (cveMetadata.state REJECTED) prints a row of n/a and none. Teardown. Keep enriched.json for Lab 2, then delete it.

Lab 2 (intermediate): turn the policy into due dates

Link to section “Lab 2 (intermediate): turn the policy into due dates”

Objective. Apply Table 1, CISA’s defaults and the Atlas’s EPSS review rule to findings. Prerequisites. Lab 1’s enriched.json; PyYAML for the table check. Cost and risk. None; synthetic assets.

  1. Save the findings as findings.csv:

Example status: executed on 2026-10-17 as input to the script below.

asset,cve,exposed,detected
vpn-gw-01,CVE-2023-4966,yes,2026-10-14
app-17,CVE-2021-44228,no,2026-10-15
build-04,CVE-2024-3094,no,2026-10-12
etl-02,CVE-2020-14343,no,2026-09-20
web-09,CVE-2023-38545,yes,2026-10-01
bastion-1,CVE-2024-6387,yes,2026-10-10
db-03,CVE-2024-6387,no,2026-10-10
  1. Save the script as policy.py:

Example status: executed on 2026-10-17 (Python 3.8.10); the built-in self-test passed.

#!/usr/bin/env python3
"""Compute remediation due dates from BOD 26-04 Table 1 plus local asset exposure.

Usage: python3 policy.py enriched.json findings.csv 2026-10-16
"""
import csv
import json
import sys
from datetime import date, timedelta

# BOD 26-04 Table 1 (CISA, 2026-06-10), keyed by
# (publicly exposed, in KEV, automatable, technical impact) -> calendar days.
# None means "fix on system upgrade"; "triage" adds a forensic triage.
TABLE_1 = {
    (True, True, True, "total"): (3, "triage"),
    (True, True, True, "partial"): (3, ""),
    (True, True, False, "total"): (3, "triage"),
    (True, True, False, "partial"): (14, ""),
    (True, False, True, "total"): (3, ""),
    (True, False, True, "partial"): (14, ""),
    (True, False, False, "total"): (14, ""),
    (True, False, False, "partial"): (60, ""),
    (False, True, True, "total"): (3, "triage"),
    (False, True, True, "partial"): (14, ""),
    (False, True, False, "total"): (14, ""),
    (False, True, False, "partial"): (14, ""),
    (False, False, True, "total"): (60, ""),
    (False, False, True, "partial"): (60, ""),
    (False, False, False, "total"): (None, ""),
    (False, False, False, "partial"): (None, ""),
}


def decision_inputs(signal):
    """Defaults for missing Vulnrichment data: CISA's for non-KEV CVEs ("no", "total");
    for a KEV CVE (CISA says it always enriches these) assume "yes", "total"."""
    auto = signal["automatable"]
    impact = signal["technical_impact"]
    if auto not in ("yes", "no"):
        auto = "yes" if signal["kev"] else "no"
    if impact not in ("total", "partial"):
        impact = "total"
    return auto == "yes", impact


def main(signals_path, findings_path, today):
    signals = {s["cve"]: s for s in json.load(open(signals_path))}
    print("%-9s %-14s %-3s %-3s %-4s %-7s %-10s %-10s %s" % (
        "asset", "cve", "exp", "kev", "auto", "impact", "start", "due", "status"))
    for f in csv.DictReader(open(findings_path)):
        s = signals[f["cve"]]
        exposed = f["exposed"] == "yes"
        auto, impact = decision_inputs(s)
        days, extra = TABLE_1[(exposed, s["kev"], auto, impact)]
        detected = date.fromisoformat(f["detected"])
        start = detected
        if s["kev"]:
            start = min(detected, date.fromisoformat(s["kev_added"]))
        if days is None:
            due, status = "upgrade", "-"
            if s["percentile"] >= 0.90:
                status = "pctl %.3f: review" % s["percentile"]
        else:
            due_day = start + timedelta(days=days)
            due = due_day.isoformat()
            late = (today - due_day).days
            status = "LATE %dd" % late if late > 0 else "due in %dd" % -late
            if extra:
                status += " +triage"
        print("%-9s %-14s %-3s %-3s %-4s %-7s %-10s %-10s %s" % (
            f["asset"], f["cve"], "Y" if exposed else "N", "Y" if s["kev"] else "N",
            "Y" if auto else "N", impact, start.isoformat(), due, status))


def self_test():
    assert len(TABLE_1) == 16
    assert TABLE_1[(True, True, True, "total")] == (3, "triage")
    assert TABLE_1[(True, False, False, "partial")] == (60, "")
    assert TABLE_1[(False, False, False, "total")] == (None, "")
    unknown = {"kev": False, "automatable": "n/a", "technical_impact": "n/a"}
    assert decision_inputs(unknown) == (False, "total")
    assert decision_inputs(dict(unknown, kev=True)) == (True, "total")


if __name__ == "__main__":
    self_test()
    main(sys.argv[1], sys.argv[2], date.fromisoformat(sys.argv[3]))
  1. Run python3 policy.py enriched.json findings.csv 2026-10-16.

Expected output:

asset     cve            exp kev auto impact  start      due        status
vpn-gw-01 CVE-2023-4966  Y   Y   Y    total   2023-10-18 2023-10-21 LATE 1091d +triage
app-17    CVE-2021-44228 N   Y   Y    total   2021-12-10 2021-12-13 LATE 1768d +triage
build-04  CVE-2024-3094  N   N   Y    total   2026-10-12 2026-12-11 due in 56d
etl-02    CVE-2020-14343 N   N   N    total   2026-09-20 upgrade    pctl 0.932: review
web-09    CVE-2023-38545 Y   N   N    total   2026-10-01 2026-10-15 LATE 1d
bastion-1 CVE-2024-6387  Y   N   N    total   2026-10-10 2026-10-24 due in 8d
db-03     CVE-2024-6387  N   N   N    total   2026-10-10 upgrade    pctl 0.999: review

What to notice. Under the “whichever event occurs first” rule, an old KEV vulnerability found today is overdue on discovery; your policy must say what that means (the Atlas policy: tier 1, due three days after detection, reported overdue). pctl is the EPSS percentile, not a probability. The xz backdoor (CVE-2024-3094) gets 60 days on an internal build host, neither in KEV nor exposed: where criticality tags and promotion earn their place. The same CVE gets different clocks on bastion-1 and db-03: exposure sets the clock. Validation. Set db-03 to exposed=yes and rerun; the row should read due in 8d. Then save the reference policy as policy.yaml and check with check_table.py (needs PyYAML) that the hand-written TABLE_1 matches its decision_table:

Example status: executed on 2026-10-17 (PyYAML 5.3.1): rows: 16, mismatches: 0; a copy with one changed cell exits 1.

#!/usr/bin/env python3
"""Rebuild TABLE_1 from the policy file's decision_table and compare (needs PyYAML).

Usage: python3 check_table.py policy.yaml   (in the folder that holds policy.py)
"""
import sys

import yaml

from policy import TABLE_1

ROWS = {"kev_automatable": (True, True), "kev_not_automatable": (True, False),
        "automatable": (False, True), "not_automatable": (False, False)}


def table_from_policy(path):
    policy = yaml.safe_load(open(path))
    days = {tier: spec["days"] for tier, spec in policy["tiers"].items()}
    table = {}
    for exposure, rows in policy["decision_table"].items():
        for row, cells in rows.items():
            kev, auto = ROWS[row]
            for impact, cell in zip(("total", "partial"), cells):
                tier, _, triage = cell.partition("+")
                table[(exposure == "public", kev, auto, impact)] = (
                    days[tier], "triage" if triage else "")
    return table


if __name__ == "__main__":
    table = table_from_policy(sys.argv[1])
    wrong = [key for key in TABLE_1 if table.get(key) != TABLE_1[key]]
    print("rows: %d, mismatches: %d" % (len(table), len(wrong)))
    for key in wrong:
        print("mismatch", key, "policy:", table.get(key), "TABLE_1:", TABLE_1[key])
    sys.exit(1 if wrong else 0)

Troubleshooting. KeyError on a CVE: rerun Lab 1 with it. Teardown. None.

Objective. Compute saturation, backlog, age and a Little’s Law estimate, and see how each misleads when read alone. Prerequisites. Python 3.8+. Cost and risk. None; the default run generates a synthetic 24-week log (seed 7). Your own export needs id,opened_week,closed_week (blank = open), with closed_week the week the fix was verified.

  1. Save the script as flow.py:

Example status: executed on 2026-10-17 (Python 3.8.10), synthetic mode and a four-row CSV.

#!/usr/bin/env python3
"""Flow metrics for a remediation pipeline: arrivals, verified closures, saturation, age.

Usage: python3 flow.py              (synthetic 24-week log, seed 7)
       python3 flow.py export.csv   (columns: id,opened_week,closed_week; blank = open)
"""
import csv
import math
import random
import sys

WEEKS = 24


def poisson(rng, mean):
    limit, k, p = math.exp(-mean), 0, 1.0
    while True:
        p *= rng.random()
        if p <= limit:
            return k
        k += 1


def synthetic(seed=7):
    """500 items already open; arrivals step from 100 to 150 a week at week 13;
    capacity near 120 verified closures a week; nothing is fixed in its first week;
    70% of capacity goes to the newest items (urgent work jumps the queue)."""
    rng = random.Random(seed)
    items = [{"id": n, "opened": -rng.randrange(26), "closed": None} for n in range(500)]
    for week in range(1, WEEKS + 1):
        for _ in range(poisson(rng, 100 if week <= 12 else 150)):
            items.append({"id": len(items), "opened": week, "closed": None})
        ready = [i for i in items if i["closed"] is None and i["opened"] < week]
        ready.sort(key=lambda i: -i["opened"])
        capacity = poisson(rng, 120)
        newest, rest = ready[:int(0.7 * capacity)], ready[int(0.7 * capacity):]
        rng.shuffle(rest)
        for item in newest + rest[:capacity - len(newest)]:
            item["closed"] = week
    return items


def load(path):
    rows = csv.DictReader(open(path))
    return [{"id": r["id"], "opened": int(r["opened_week"]),
             "closed": int(r["closed_week"]) if r["closed_week"] else None} for r in rows]


def pct(values, q):
    values = sorted(values)
    return values[min(len(values) - 1, int(q * len(values)))] if values else 0


def report(items, window=4):
    weeks = max(max(i["opened"], i["closed"] or 0) for i in items)
    print("weeks  arrive  close    rho  backlog  age_p50  age_p90  L/lambda")
    for end in range(window, weeks + 1, window):
        start = end - window + 1
        arrive = sum(1 for i in items if start <= i["opened"] <= end)
        close = sum(1 for i in items if i["closed"] and start <= i["closed"] <= end)
        still_open = [i for i in items if i["opened"] <= end
                      and (i["closed"] is None or i["closed"] > end)]
        ages = [end - i["opened"] for i in still_open]
        lam = arrive / window
        print("%2d-%-2d  %6d  %5d  %5.2f  %7d  %7d  %7d  %8.1f" % (
            start, end, arrive, close, arrive / max(close, 1), len(still_open),
            pct(ages, 0.5), pct(ages, 0.9), len(still_open) / lam if lam else 0))
    still_open = [i for i in items if i["closed"] is None]
    stale = [i for i in still_open if weeks - i["opened"] > 8]
    print("open longer than 8 weeks: %d of %d" % (len(stale), len(still_open)))
    last = [i for i in items if weeks - window < i["opened"] <= weeks]
    shut = [i for i in items if i["closed"] and weeks - window < i["closed"] <= weeks]
    print("backlog growth now: %+.1f items/week" % ((len(last) - len(shut)) / window))


if __name__ == "__main__":
    report(load(sys.argv[1]) if len(sys.argv) > 1 else synthetic())
  1. Run python3 flow.py (or python3 flow.py export.csv for your own data).

Expected output:

weeks  arrive  close    rho  backlog  age_p50  age_p90  L/lambda
 1-4      366    484   0.76      382       15       26       4.2
 5-8      420    453   0.93      349        4       29       3.3
 9-12     353    485   0.73      217        4       30       2.5
13-16     619    466   1.33      370        1       12       2.4
17-20     613    505   1.21      478        2        7       3.1
21-24     607    507   1.20      578        3       10       3.8
open longer than 8 weeks: 88 of 578
backlog growth now: +25.0 items/week

What to notice. While saturation (rho) stays below 1 the backlog shrinks; after arrivals step up in week 13 it stays above 1 and the backlog grows by about 25 items a week. The trap: the 90th-percentile age falls from 30 to 12 weeks just as things get worse, because new items dilute the old ones. Never report age without saturation and backlog. L/lambda (average weeks in the system) is reliable only when arrivals and closures roughly balance. Validation. Recompute one window by hand. Troubleshooting. Zero arrivals print L/lambda as 0; partial windows at the end are skipped. Teardown. None.

Scenario Source Core problem VulnerabilityOps move Success measure
Log4Shell, December 2021 Sourced Where is the component? Estate-wide dependency queries Time to locate every instance
Citrix Bleed, October 2023 Sourced “Patched” was not “safe” Fix definition includes session kill and hunt No sessions predating the fix
Plant with unpatchable controllers Illustrative No fix for years Mitigate, re-tier, expiring exception Mitigation re-verified quarterly

Log4Shell (sourced). The Cyber Safety Review Board (2022) reported that one federal cabinet department spent 33,000 hours on Log4j response, called Log4j an “endemic vulnerability”, and found that organisations using SBOMs reported none had used them to find vulnerable Log4j deployments[46]. CSA’s bar: a programme “should be able to answer “where is this dependency in our stack?” within minutes”[1]. Build that query (SBOM index, agent package inventory, image metadata) before the next crisis; an index of your own builds misses appliances and commercial software.

Citrix Bleed (sourced). CVE-2023-4966 let attackers “bypass password requirements and multifactor authentication (MFA), leading to successful session hijacking”, and LockBit 3.0 affiliates exploited it[47]. After installing fixed builds, NetScaler advised: “we also recommend killing all active and persistent sessions”[48]. Closing on a version change left stolen sessions alive: the fix definition must list every action (patch, kill sessions, rotate credentials, hunt), and verification must check each.

Plant with unpatchable controllers (illustrative scenario). Firmware is certified once a year. Isolate the controllers behind a monitored gateway, re-tier, issue an expiring exception owned by the plant manager, and re-verify the isolation quarterly by configuration review. Do not point active scanners at the controllers: NIST warns that active scans “may cause device instability” in operational technology (OT); use passive discovery and test scanners offline first[49].

CSA recommends to “instrument the inbound rate of vulnerability findings against the throughput of remediation, treat the ratio as a saturation metric, and report it to security leadership monthly”[1]. Formulas are the Atlas’s unless marked.

Metric Formula Pitfall Cadence
Saturation ρ (CSA; “verified” is the Atlas’s) Arrivals ÷ verified closures, same window, per tier Read with backlog age Monthly
Backlog growth Arrivals − verified closures per week Deduplication changes counts Weekly
Time in system Little’s Law: W ≈ backlog L ÷ arrival rate λ[25] Valid only near balance Monthly
On-time verified fixes Verified by due date ÷ due in window; open overdue count as misses Measure at verification Monthly per tier
Credentialed coverage In-scope inventory assets with a good authenticated or agent scan in 7 days ÷ in-scope assets Denominator from the scanner Weekly
Enrichment gap Open findings missing exposure, SSVC or CVSS inputs ÷ open findings Defaults hide it Weekly
Recurrence Reopened within 90 days of a verified fix ÷ verified fixes Stale golden images Monthly
Remediation change failure Remediation changes causing incident or rollback ÷ remediation changes Shared with PatchOps Monthly
Dispute rate Upheld disputes ÷ findings, per scanner and plugin Owners who stop disputing and ignore Monthly
Exception debt Exceptions past expiry; open exception-days Exceptions outside the register Monthly
Figure 5. The remediation flow and its four levers
flow countsMonthly review: saturation,age, coverageReduce inflowRation with tiersRaise capacityVerify every fixArrivals: CVEs, assets,regressionsOpen backlogVerified closures
Diagram source (Mermaid)
flowchart TB
  M["Monthly review: saturation, age, coverage"]
  L1["Reduce inflow"]
  L3["Ration with tiers"]
  L2["Raise capacity"]
  L4["Verify every fix"]
  IN["Arrivals: CVEs, assets, regressions"]
  B["Open backlog"]
  OUT["Verified closures"]
  M --> L1
  M --> L3
  M --> L2
  M --> L4
  L1 -.-> IN
  L3 -.-> B
  L2 -.-> OUT
  L4 -.-> OUT
  IN --> B
  B --> OUT
  OUT -- "flow counts" --> M
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class IN,B,OUT core
  class M decision
  class L1,L2,L3,L4 neutral

Text equivalent: A monthly review of saturation, age and coverage pulls four levers: reduce inflow (acts on arrivals of CVEs, assets and regressions), ration with tiers (the open backlog), raise capacity and verify every fix (verified closures). Arrivals pass through the backlog to closures; a “flow counts” edge from closures back to the review stands for all three counts.

Worked calculation (illustrative numbers). September arrivals and verified closures by tier: T1 12 and 11, T2 180 and 190, T3 600 and 520 (T4 is tracked separately). Saturation: T1 1.09 (one item: review the item, not the ratio), T2 0.95, T3 1.15. The T3 backlog is 1,300 with 600 arrivals a month, so Little’s Law gives 1,300 ÷ 600 ≈ 2.2 months, about 65 days: the average T3 item takes longer than its 60-day clock, and with saturation above 1 the real figure is rising. Coverage is 1,820 of 1,900 in-scope assets, 95.8%; the 80 missing hosts lost scan credentials in a key rotation (see the runbook). Decision: automate the two largest T3 sources, restore coverage, and report T3 saturation until it stays below 1.

Targets. Baseline for two or three months, then set per tier: saturation below 1 over a quarter, zero overdue T1, coverage above a floor. Never reward closed-ticket counts.

  • Scanner credentials are privileged. Reliable Windows patch checks need local administrator rights[41]. Vault the credentials, scope them per segment, rotate them through change control and alert on use outside scan windows.
  • Scanners are third-party code. In March 2026 attackers redirected Trivy’s GitHub Action tags and published a malicious Trivy v0.69.4[44]. Pin scanners by digest, verify checksums and keep them away from unrelated secrets (DevSecOps).
  • Findings are an attack map. Restrict and log access to the findings database and exports.
  • Rules. FedRAMP will require its new Vulnerability Detection and Response and Vulnerability Evaluation and Reporting rules by 7 December 2026 to align with BOD 26-04[28]. Since 11 September 2026 the EU Cyber Resilience Act requires manufacturers to report actively exploited vulnerabilities (early warning within 24 hours)[50]. It is supplier-side but adds advisories to ingest.
  • Evidence. Keep policy version, signal snapshot, decision and verification per finding (ComplianceOps).

The main cost is fixing time, not licences; the 2019 Kenna/Cyentia study found top performers closed roughly 2.5 times more than others[6]. Plan with verified fixes per engineer-week by tier and the share automated. Cheapest first: reduce inflow (unused packages, standard base images, decommissioning), then automate high-volume fixes, then buy tools.

  • Each month, rescan a random sample of closed findings with a different method or account.
  • Reconcile inventory and scan results weekly, and your exposed-asset list with an external attack-surface scan.
  • Alert when the KEV snapshot, EPSS date or advisory feed is more than a day old.
Symptom Likely cause Diagnosis Fix and prevention
Findings drop sharply; dashboard turns green Scan credential or path failure Authenticated coverage by segment; Nessus plugin 21745[41] Restore access, rescan; coverage alert
Same finding reopens monthly Golden image or baseline reintroduces it Recurrence by image or template Fix the source; rebuild pipeline
Clocks reset on every rescan Deduplication keyed on scan ID Compare first-seen dates Key on asset × CVE × component; fixtures
Serious CVE gets a low tier Missing CVSS/CPE data, weak defaults Enrichment-gap report Explicit defaults; analyst promotion
Owners ignore or reject tickets No agreed routing or capacity; version-only false positives (backports) Overdue by owner group; dispute rate by plugin; package changelog or vendor advisory Escalate; agree windows in SecOps; package-aware checks and a dispute path
Exploited although “fixed” Incomplete fix definition Post-incident review List every action in the hand-off record
Scan missed a vulnerable system Scope or method gap Inventory versus scan scope Agent or authenticated scan; reconciliation

A precedent for the last row: Equifax told GAO that a scan a week after the Apache Struts announcement “did not detect the vulnerability on the online dispute portal”, and the alert had gone to an out-of-date recipient list[51].

Worked runbook: the dashboard turned green overnight

Link to section “Worked runbook: the dashboard turned green overnight”

Scenario (illustrative). Monday’s report shows open findings down 18% and tier-2 saturation at 0.4, with no patch campaign.

Figure 6. Runbook: separating real progress from lost coverage
YesCredential rotated or expiredFirewall or agent changeNoYesNoFindings drop sharplyCredentialed coveragedropped too?Check failed-login results persegmentCause found?Restore vaulted credential,rescanRestore scan path, rescanMark the gap windowlow-confidenceMatching fixes in changerecords?Re-verify a sample, acceptthe dropCheck deduplication andscanner versionAdd coverage alert; rotate viachange process
Diagram source (Mermaid)
flowchart TB
  S["Findings drop sharply"] --> Q1{"Credentialed coverage dropped too?"}
  Q1 -- "Yes" --> C1["Check failed-login results per segment"]
  C1 --> C2{"Cause found?"}
  C2 -- "Credential rotated or expired" --> F1["Restore vaulted credential, rescan"]
  C2 -- "Firewall or agent change" --> F2["Restore scan path, rescan"]
  F1 --> R["Mark the gap window low-confidence"]
  F2 --> R
  Q1 -- "No" --> Q2{"Matching fixes in change records?"}
  Q2 -- "Yes" --> V["Re-verify a sample, accept the drop"]
  Q2 -- "No" --> D["Check deduplication and scanner version"]
  R --> P["Add coverage alert; rotate via change process"]
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  class S risk
  class Q1,C2,Q2 decision
  class F1,F2,R,V,D,P core

Text equivalent: If findings drop sharply, check whether credentialed coverage dropped too. If so, read failed-login results per segment, restore the credential or scan path, rescan, mark the gap window low-confidence, then add a coverage alert and rotate credentials through change control. If coverage held and change records show matching fixes, re-verify a sample and accept the drop; otherwise check deduplication and scanner version.

  1. Triage (first hour). Compare credentialed coverage by segment with last week. Here plugin 19506, which reports per host whether credentialed checks are possible[52], no longer shows Credentialed checks : yes for 80 Linux hosts in two segments, and plugin 21745 reports failed logins; on Linux that status also needs the package inventory[41].
  2. Diagnose. A job rotated the scan account’s SSH key on Friday without updating the scanner’s vault entry.
  3. Remediate. Update the vault entry, rescan both segments, confirm the open count returns to about last week’s level (undoing the 18% drop).
  4. Communicate. Annotate the report (lost coverage, not progress) and recompute saturation for those weeks.
  5. Prevent. Alert when a segment’s authenticated coverage falls 5 points in a week; add the scan account to the rotation process.

Exit criteria: coverage restored, report corrected, alert live, rotation process updated.

  • “CVSS 9+ first” as the whole policy. Severity is not risk (EPSS). Instead: exposure and exploitation clocks with CVSS as one input.
  • What FIRST calls “Score Laundering”. Multiplying EPSS by CVSS, or a weighted “risk score” nobody can explain[26]. Instead: a traceable decision table.
  • “Patch everything.” A wish, not a plan; it guarantees silent rationing by whoever shouts loudest.
  • Myth: low EPSS means safe. A 5% score means about 1 in 20 such vulnerabilities see exploitation activity within 30 days[32].

Use the model when arrivals outrun verified fixes for months, tier-1 clocks are missed, scanners overlap, or nobody can answer “are we getting better?”

Do not start here when:

  • There is no inventory with owners. Build it first with ITOps and SecOps.
  • The estate is small and patches itself. Ten SaaS-heavy laptops need patch compliance (EndpointOps, PatchOps) and a KEV watch, not a pipeline.
  • You run mainly SaaS. The provider fixes the service; you need contractual timelines and status reports (CISA’s advice to agencies) and a process for what stays yours: access, authentication, data and audit logging[28], plus endpoints.
  • The bottleneck is change approval. Faster triage only lengthens the change-board queue; fix that first (ITOps).

Hidden costs: tagging, a policy owner, analyst time for promotions, exceptions and disputes, and the politics of publishing what you will not fix.

Figure 7. Should you run vulnerability management as VulnerabilityOps?
NoYesNoYesDecidingFixingBothInventory with owners andexposure tags?Build inventory firstSaturation above 1 or tier-1clocks missed?Keep the current process;add KEV and EPSS signalsMain bottleneck?Risk-based policy and flowmetricsRemediation as code withplatform teamsFull VulnerabilityOps: onepipeline, one policyMonthly saturation review
Diagram source (Mermaid)
flowchart TB
  A{"Inventory with owners and exposure tags?"}
  A -- "No" --> B["Build inventory first"]
  A -- "Yes" --> C{"Saturation above 1 or tier-1 clocks missed?"}
  C -- "No" --> D["Keep the current process; add KEV and EPSS signals"]
  C -- "Yes" --> E{"Main bottleneck?"}
  E -- "Deciding" --> F["Risk-based policy and flow metrics"]
  E -- "Fixing" --> G["Remediation as code with platform teams"]
  E -- "Both" --> H["Full VulnerabilityOps: one pipeline, one policy"]
  F --> I["Monthly saturation review"]
  G --> I
  H --> I
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class A,C,E decision
  class F,G,H,I core
  class B,D neutral

Text equivalent: Without an inventory of owners and exposure tags, build it first. Then, if saturation is at or below 1 and tier-1 clocks are met, keep the current process and add KEV and EPSS signals. Otherwise find the bottleneck: deciding (risk-based policy and flow metrics), fixing (remediation as code with platform teams) or both (full VulnerabilityOps). Every path ends in a monthly saturation review.

The judgement call compares CVSS bands with Table 1-style clocks; a commercial platform is a third option (less engineering, proprietary scores).

This is the Atlas’s model, not an industry standard; exit criteria are examples.

Level Observable behaviour Exit criterion to next level
1. Scan and react Periodic scans, CVSS-ranked spreadsheets Inventory with owners for in-scope assets
2. Owned queues Findings routed to owners with dates; KEV watched Written tiered policy; authenticated coverage measured
3. Risk-based policy Exposure and exploitation clocks, explicit defaults, expiring exceptions Closure requires verification; saturation reported monthly
4. Flow-managed Saturation, age, coverage and recurrence drive capacity decisions High-volume classes automated; no overdue T1 for a quarter
5. Adaptive Remediation as code by default; VEX and reachability cut noise; policy reviewed against outcomes Sustain; review yearly
Days Actions Milestone
1–30 Inventory gap list; tag internet-facing assets; daily KEV match; baseline arrivals, closures, coverage Baseline report; KEV matches routed within one business day
31–60 Policy v1 with defaults; hand-off record; rescan-based closure; exception intake with RiskOps Policy signed; on-time T1 measured
61–90 Automate one high-volume class (e.g. base-image rebuilds); first monthly flow review; coverage alert Saturation per tier reported; one class automated

Quick start (first two weeks)

  • In-scope assets exported with owner and exposure; a written default for untagged assets
  • Daily KEV match against the inventory, routed within one business day
  • EPSS and Vulnrichment pulled for open CVEs (Lab 1)
  • Baseline: arrivals, verified closures, backlog, credentialed coverage

KEV-day readiness (preflight)

  • On-call contact per owner group of internet-facing assets
  • Emergency change route agreed with PatchOps and ITOps
  • Forensic triage request agreed with the SOC; evidence before fix where possible[28]
  • Out-of-band channel for suspected compromise; rescan job that targets named assets within hours

Monthly flow review

  • Saturation per tier, with backlog and age beside it
  • Overdue tier-1 and tier-2 items by owner, with reasons
  • Coverage, enrichment-gap and dispute-rate trends; exceptions past expiry
  • One decision recorded: capacity, policy or inflow

Governance audit (quarterly)

  • Policy version, approver and changelog current
  • 20 sampled closures each have verification evidence; sampled tiers match the policy in force
  • Scanner credentials vaulted, scoped, rotated through change control; findings access reviewed

Continuous improvement

  • Top three recurring findings fixed at their source; one manual remediation class automated per quarter

One-page remediation policy summary:

REMEDIATION POLICY v3 - summary                          effective 2026-10-01
Owner: Vulnerability management     Approved by: Risk committee
Purpose: reduce exploitable exposure faster than it arrives.
Clocks (calendar days, from KEV listing or first detection, whichever is first):
  T1  3 days (+ forensic triage if in KEV and total control)
  T2  14 days     T3  60 days     T4  next scheduled upgrade
Inputs: public exposure, KEV, automatable, technical impact, EPSS percentile,
  asset criticality (local: T4 on a critical asset = T3).
Defaults: untagged asset = public; missing automatable = no; missing impact = total.
Found already past its clock: T1, due 3 days after detection, reported overdue.
Closure: authenticated rescan, configuration test, new image digest or upheld dispute.
Exceptions (a local change; BOD 26-04 allows none): max T1 7, T2 30, T3 90 days;
  compensating control, expiry and risk owner signature. KEV and T1 items: removal
  or isolation only, signed by the CISO and the risk owner.
Promotion: analysts may raise a tier with a recorded reason, never lower one.
Review: monthly flow review; policy review every quarter.

Exception request (VulnerabilityOps intake; the approval policy belongs to RiskOps):

EXCEPTION REQUEST                                    ID: EXC-____  Policy: v__
Finding(s): ______________________  Assets: _______________  Tier/clock: ____
Why the clock cannot be met: ____________________________________________
Compensating control: __________________________________________________
  (KEV or T1: removal or isolation only - segmentation, software-defined perimeter, proxy)
Which decision point it changes (exposure / automatable / impact): ________
How the control is verified, and how often: ______________________________
Requested expiry (max T1 7, T2 30, T3 90 days): ________  Rescan on: _______
Requested by (asset owner): ___________  Approved by (risk owner): ________
KEV or T1 only - CISO sign-off: ______________________

Monthly flow review record:

FLOW REVIEW  Month: ________   Attendees: _____________________________
Tier | Arrivals | Verified closures | Saturation | Backlog | Oldest open
T1   |          |                   |            |         |
T2   |          |                   |            |         |
T3   |          |                   |            |         |
Coverage (credentialed): ____%   Enrichment gap: ____%   Recurrence: ____%
Exceptions past expiry: ___      Remediation change failures: ___   Disputes upheld: ___
Decision taken (capacity / policy / inflow): ______________________________
Owner and date: _______________________________________________________
Approach Unit of work Primary question Choose when
Traditional vulnerability management Scan report What did the scanner find? Small estates that patch automatically
VulnerabilityOps (this page) Finding instance in a flow Are we fixing what matters faster than it arrives? Saturated programmes, many sources
Exposure management (continuous threat exposure management, CTEM; “VOC” in vendor material[4]) Attack path, incl. misconfigurations and identities What can an attacker reach? Mature programmes adding validation

Is VulnerabilityOps just vulnerability management renamed? Partly: same tasks, plus explicit rationing, verified closure and flow metrics[1].

Should we stop using CVSS? No; use it for severity and technical impact, not as clocks. CISA notes that labels such as “critical” or “high” “do not prescribe specific actions”[28].

We are not a US federal agency. Does BOD 26-04 apply? Not legally, unless a contract says so. Adapt Table 1 and write down where you differ, as the reference policy does.

What if a CVE has no CVSS score or product data? Apply CISA’s explicit defaults (see Table 1), add a second source for the unenriched tail and track the gap. NIST will consider enrichment on request, as resources allow[31].

An owner says a finding is a false positive. Who decides? The analyst, with a package-aware or authenticated check. An upheld dispute closes the finding with its evidence and an expiry; nothing closes silently.

  • More inflow from AI-assisted discovery (emerging). The 2025 essay expects AI vulnerability research to become available “at scale, repeatable, and integrated into enterprise operations”[2]; plan capacity for rising arrivals, not last year’s.
  • Measuring what KEV misses (research). NIST CSWP 41 (May 2025) proposes a “Likely Exploited Vulnerabilities” metric that may augment EPSS and measure KEV’s comprehensiveness[53].
  • VEX and reachability at scale (emerging; reachability: DevSecOps). Each VEX statement is its author’s assertion[24]; never let a team approve VEX relief for its own service.
  • New defect classes. CSA notes that prompt-injection chains and tool-poisoning of agent integrations do not map cleanly onto CVE taxonomies but are still vulnerabilities in the operational sense[1].
Stage What to do
Prerequisites SecOps; basic Linux, Windows and networking; reading a vendor advisory
1-day primer The CVSS User Guide’s “CVSS Base Score (CVSS-B) Measures Severity, not Risk”[30], “Using EPSS”[26], KEV criteria[33], BOD 26-04 Table 1[14]; Lab 1
1-week practice Labs 2 and 3 on your own scanner export; policy v1 in YAML
30 days One monthly flow review; coverage reconciliation; hand-off record agreed with PatchOps
Specialisation CVSS-BTE enrichment[54]; custom SSVC trees[29]; VEX ingestion; LEV[53]
Portfolio Open-source enrichment script; anonymised comparison of CVSS bands versus Table 1; coverage dashboard

Beginners stop after the primer; leaders read the decision framework and the judgement call.

Topic Remember
Definition Vulnerability management run as a flow: tier, route, verify, measure
Decide with KEV (evidence) > EPSS (probability) > CVSS (severity), localised by exposure and impact
Model to adapt BOD 26-04 Table 1: exposure, KEV, automatable, impact → 3, 14, 60 days or next upgrade
Never Multiply EPSS by CVSS; close on ticket status; grant exceptions without expiry or for KEV items without isolation
Metrics Saturation per tier, backlog and age, on-time verified fixes, coverage, enrichment gap
Watch-outs Partial NVD enrichment; credential failures that look like progress; scanners as supply-chain risk
Next actions Tag exposure; match KEV daily; write policy v1; require rescans; review saturation monthly

A judgement call: replace CVSS bands with exposure-and-exploitation clocks?

Link to section “A judgement call: replace CVSS bands with exposure-and-exploitation clocks?”

Situation (illustrative). A 4,000-asset company fixes CVSS 9.0+ in 15 days and 7.0–8.9 in 30. About 1,300 high or critical finding instances arrive a month against capacity for about 1,000 verified fixes: saturation 1.3, on-time near 60%.

Option A: adopt Table 1-style clocks. A shadow run on last quarter’s data puts about 150 instances a month in the 3- and 14-day tiers, 500 in the 60-day tier and the rest at “next upgrade”. Assumptions: exposure tags are accurate, CISA’s defaults are applied, capacity stays at 1,000.

Option B: keep the bands and add “any KEV item: 7 days”.

Why B is weaker than it looks. The bands still mark roughly half of published CVEs as urgent[26], so saturation stays above 1, due dates become fiction, and the KEV override competes with hundreds of “critical” internal items no attacker can reach.

Why A can fail. It moves risk into the inventory: an internet-facing host tagged internal waits 60 days or for an upgrade, and without a criticality input crown-jewel internal systems wait too. Default “total” impact pushes items up, and auditors may ask why “critical” items wait.

Decision process. Run both policies in shadow for 60 days; fix exposure tags first; let analysts promote any CVSS 9.0+ internal item after review. Adopt A if tier-1 and tier-2 saturation stays below 1 in the shadow period and an external attack-surface scan finds no public asset tagged internal. Uncertainty: one quarter of data; arrivals may rise. If tags cannot be trusted within 60 days, B with honest capacity reporting is the safer interim choice.

After this page you can explain why CVSS alone is not a policy, apply Table 1 and EPSS, design a policy, hand-off record and verification rule, operate a flow review and evaluate whether you need the model.

1. Tier-2 arrivals were 240 in a month and verified closures 200. What is saturation, and what follows?

240 ÷ 200 = 1.2. The tier-2 backlog grows by about 40 a month, so time in system rises and on-time performance falls unless inflow drops or capacity rises.

2. A non-KEV, automatable CVE with partial control sits on an internet-facing web server. What does Table 1 say?

14 days. If the server is taken off the internet, the row becomes “not exposed, automatable”: 60 days. The clock follows the facts.

3. Why not multiply EPSS by CVSS to get a risk score?

EPSS is a calibrated probability; CVSS is an uncalibrated ordinal, so the product has no meaning (FIRST’s “Score Laundering”). Use a decision table in which each signal answers its own question.

4. The T3 backlog holds 900 items and about 450 arrive each month. What does Little’s Law suggest?

W ≈ 900 ÷ 450 = 2 months, about 60 days (valid only near balance; a growing backlog means longer). The mean equals the clock, so a large share of items will be late; aim for a mean well below the clock.

5. Open findings fell 18% in a week with no patch campaign. What do you check first?

Credentialed coverage by segment. Failed logins or broken scan paths make findings vanish; a credentialed result needs a login and a package inventory. If coverage fell, the improvement is not real.

6. A leader asks for “zero critical vulnerabilities by year end”. How do you respond?

The goal measures the pile and is unreachable while arrivals exceed capacity. Offer instead: no overdue tier-1 items, saturation below 1 for tiers 1–3, coverage above an agreed floor and every exception within expiry, in the monthly flow report.

VulnerabilityOps is a new name for an old job done in a better order: start from capacity, decide openly from evidence rather than severity alone, give each item an owner and a date, prove every fix and watch the flow. The label may fade; CISA’s clocks, FIRST’s probabilities and Little’s arithmetic will still apply. If you remember one thing: you will never fix everything, so make sure what you leave unfixed is a decision, not an accident.

Further reading: EPSS FAQ. Evidence ledger: research/vulnerabilityops-ledger.md.

Changelog: 2026-10-16 r0: first draft, labs executed. 2026-10-17 r1: review fixes (origins, study populations, exceptions, disputes, acronyms, decision table, OT scanning); labs re-run. 2026-10-17 r2: criticality input added to the policy; Lab 1 troubleshooting corrected and re-tested; Figure 3 labels shortened; related list, runbook units and User Guide reference fixed. 2026-10-17, post-score polish: NIST definition; Qualys precision; Figure 5 uncrossed; KEV and CVSS prose split; live-count notes; Lab 2 policy-table check; DevSecOps link; repetitions cut. Checker fixes: KEV note, Figure 5 text, Lab 2 prerequisites.


  1. VulnOps: Vulnerability Management in the Age of AI — Cloud Security Alliance, 2026-05-04. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Autonomous AI Hacking and the Future of Cybersecurity — Adkins, Evron, Schneier (CSO essay, reposted), 2025-10-10. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  3. VulnOps: A Guide to Starting Implementation From A CISO — SANS Institute blog (briefing announcement), 2026-08-05. Checked 2026-10-17. ↩︎ ↩︎

  4. The Case for a Vulnerability Operations Center — Check Point (vendor blog), 2026-05-15. Checked 2026-10-17. ↩︎ ↩︎

  5. Metrics — CVE Program. Checked 2026-10-16. ↩︎

  6. Prioritization to Prediction, Volume 3 — Cyentia Institute for Kenna Security, 2019. Checked 2026-10-16. ↩︎ ↩︎

  7. How Low Can You Go? An Analysis of 2023 Time-to-Exploit Trends — Mandiant, Google Cloud, 2024-10-16. Checked 2026-10-17. ↩︎

  8. 2026 Data Breach Investigations Report: Executive Summary — Verizon, 2026. Checked 2026-10-17. ↩︎

  9. NIST SP 800-40 Rev. 4: Guide to Enterprise Patch Management Planning — NIST, April 2022. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  10. PSIRT Services Framework v1.1 — FIRST. Checked 2026-10-16. ↩︎

  11. CVSS v1 Archive — FIRST (“FIRST to host CVSS”, 14 April 2005). Checked 2026-10-17. ↩︎

  12. Common Vulnerability Scoring System — FIRST. Checked 2026-10-16. ↩︎

  13. EPSS: Get the Data — FIRST. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  14. BOD 26-04: Prioritizing Security Updates Based on Risk — CISA, 2026-06-10. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  15. Stakeholder-Specific Vulnerability Categorization — CISA. Checked 2026-10-16. ↩︎

  16. Vulnerability — NIST CSRC Glossary. Checked 2026-10-17. ↩︎

  17. Overview — CVE Program. Checked 2026-10-16. ↩︎

  18. About CWE — MITRE. Checked 2026-10-16. ↩︎

  19. Vulnerability Status — NIST NVD, updated 2026-04-14. Checked 2026-10-17. ↩︎ ↩︎

  20. CVSS v4.0 Specification Document — FIRST, document version 1.2. Checked 2026-10-16. ↩︎ ↩︎

  21. Exploit Prediction Scoring System — FIRST. Checked 2026-10-16. ↩︎ ↩︎

  22. Known Exploited Vulnerabilities Catalog — CISA. Checked 2026-10-17. ↩︎ ↩︎

  23. CISA Vulnrichment — CISA. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  24. Minimum Requirements for VEX — CISA, April 2023. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  25. Little’s Law as Viewed on Its 50th Anniversary — John D. C. Little, Operations Research, 2011. Checked 2026-10-16. ↩︎ ↩︎

  26. Using EPSS — FIRST. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  27. Control 7: Continuous Vulnerability Management — CIS Controls Assessment Specification v8.1. Checked 2026-10-16. ↩︎ ↩︎

  28. BOD 26-04: Implementation Guidance — CISA, 2026-06-10. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  29. Prioritizing Patch Deployment — CERT/CC SSVC documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  30. CVSS v4.0 User Guide — FIRST. Checked 2026-10-17. ↩︎ ↩︎

  31. NIST Updates NVD Operations to Address Record CVE Growth — NIST, 2026-04-15. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  32. How EPSS Works — FIRST. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  33. Reducing the Significant Risk of Known Exploited Vulnerabilities — CISA. Checked 2026-10-17. ↩︎ ↩︎

  34. KEV JSON feed — CISA, version 2026.10.16; counts by the Atlas. Checked 2026-10-17. ↩︎

  35. CISA SSVC Guide — CISA, November 2022. Checked 2026-10-16. ↩︎ ↩︎

  36. NVD Dashboard — NIST NVD, live. Checked 2026-10-17. ↩︎

  37. Consult the European Vulnerability Database — ENISA, 2025-05-13. Checked 2026-10-16. ↩︎

  38. Security Backporting Practice — Red Hat. Checked 2026-10-17. ↩︎

  39. Finding-Level Scores: QVS, QDS, and QVSS — Qualys, updated September 2026. Checked 2026-10-17. ↩︎ ↩︎

  40. Greenbone Community Edition: Background — Greenbone. Checked 2026-10-16. ↩︎

  41. Tenable Nessus Credentialed Checks — Tenable. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  42. trivy image — Trivy documentation. Checked 2026-10-16. ↩︎

  43. anchore/grype — Anchore. Checked 2026-10-16. ↩︎

  44. Update: Ongoing Investigation and Continued Remediation — Aqua Security (Trivy incident post), 2026-03-22, updated 2026-04. Checked 2026-10-17. ↩︎ ↩︎

  45. CISA BOD 26-04 decision table, v1.0.0 (JSON) — CERT/CC SSVC repository, pinned commit. Checked 2026-10-17. ↩︎

  46. Review of the December 2021 Log4j Event — Cyber Safety Review Board, 2022-07-11. Checked 2026-10-16. ↩︎

  47. AA23-325A: LockBit 3.0 Affiliates Exploit CVE 2023-4966 Citrix Bleed — CISA and partners, 2023-11-21. Checked 2026-10-16. ↩︎

  48. CVE-2023-4966: Critical security update now available — NetScaler, October 2023. Checked 2026-10-16. ↩︎

  49. NIST SP 800-82 Rev. 3: Guide to Operational Technology Security — NIST, September 2023 (§6.3.2.4; Appendix E.2.3). Checked 2026-10-17. ↩︎

  50. Cyber Resilience Act: Reporting obligations — European Commission. Checked 2026-10-16. ↩︎

  51. Data Protection: Actions Taken by Equifax and Federal Agencies (GAO-18-559) — US GAO, 2018-08-30. Checked 2026-10-17. ↩︎

  52. Nessus Scan Information (plugin 19506) — Tenable. Checked 2026-10-17. ↩︎

  53. NIST CSWP 41: Likely Exploited Vulnerabilities — NIST, 2025-05-19. Checked 2026-10-16. ↩︎ ↩︎

  54. CVSS v4.0 Consumer Implementation Guide — FIRST, 2026-01-16. Checked 2026-10-16. ↩︎

Related Ops

PatchOps

PatchOps is an informal name for patch management run as an operation: getting vendor fixes onto every asset through rings, maintenance windows, reboots and rollback, then proving they took effect. The established practice is enterprise patch management.

DevSecOps

DevSecOps is the practice of building security into the software delivery pipeline and into the teams that run it, so that every change is checked, traceable and safe to ship without a separate security phase that stalls delivery.

SecOps

SecOps is an ambiguous label. Here it means the working contract between security and IT operations: agreed hardening baselines, one owned remediation queue with deadlines, governed exceptions and joint metrics. Others use it for SOC work or a Google product.

RiskOps

Operational and technology risk management: risk = likelihood x impact, risk registers, risk appetite and tolerance, inherent vs residual risk, treatment options (avoi…

In preparation

ThreatOps

ThreatOps is an unstandardised label. This page uses it for running threat intelligence and threat hunting as one operation: priority intelligence requirements steer what is collected and analysed, and testable hypotheses steer hunts whose findings change defences.

ResponseOps

In this Atlas, ResponseOps is security incident response run as an operational capability: contain an active adversary, preserve evidence, evict and recover cleanly, and meet notification duties. The label itself is not a standard term.

ContainerOps

ContainerOps is this Atlas's name for operating single containers below the orchestrator: building small, traceable OCI images, managing tags, digests and registries, and running containers predictably on a host's runtime, namespaces and cgroups.

CloudSecOps

CloudSecOps is running security for a cloud estate at the provider-API layer: guardrails that block unsafe configuration, continuous posture checks, complete audit logs, and detection and response for control-plane activity. The label is emerging and loosely defined.

Practise and get certified

From xOps School

Practise VulnerabilityOps and get certified

The free VulnerabilityOps track on xOps School has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-17 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)