---
title: "VulnerabilityOps"
slug: vulnerabilityops
category: security
term_status: emerging
evidence_status: verified
summary: "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."
audience: [Vulnerability management analysts, Security engineers, Infrastructure and platform owners, CISOs and security leaders, Beginners]
last_verified: 2026-10-17
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."
related: [patchops, devsecops, secops, riskops, threatops, responseops, containerops, cloudsecops, soc-ops]
---

## What VulnerabilityOps is

### In 30 seconds

**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."[^csa-note] For analysts, security engineers, the owners who fix, and leaders.

::: note Term status: emerging. Read this first
"VulnOps" is in visible use but has no standard definition. Two emphases are documented:

1. **Operationalising AI-assisted vulnerability research.** In October 2025 Heather Adkins, Gadi Evron and Bruce Schneier wrote that
   "a new discipline might emerge: VulnOps" as research pipelines become repeatable enterprise operations[^aes-essay].
2. **Running vulnerability management as a flow.** CSA's May 2026 note calls VulnOps "an operating discipline rather than a product
   category"[^csa-note]; a SANS blog post announcing its August 2026 briefing describes "a
   continuous operation focused on reducing exploitability"[^sans-vulnops]. Both also present it as a response to AI-driven discovery
   volume, so the two meanings overlap. A vendor promotes a related "vulnerability operations center" model[^cp-voc].

This page teaches meaning 2. Its practices are long established as *vulnerability management* (NIST SP 800-40, CIS Control 7).
In the sources found for this page, "VulnOps" is the usual spelling; "VulnerabilityOps" appears rarely.
:::

### At a glance

| 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](#glossary-and-mental-model)), clocks, routing, verification, exception intake, disputes, flow metrics, feed quality |
| Out of scope | Patch rollout mechanics ([PatchOps](../patchops/)), pipeline gates ([DevSecOps](../devsecops/)), risk-acceptance policy ([RiskOps](../riskops/)), incident response ([ResponseOps](../responseops/)) |
| Prerequisites | Inventory with owners and exposure tags; a scanner; ticketing |
| Key outputs | Remediation policy, dated owner queues, verification evidence, monthly flow report |

### The problem it solves

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[^cve-metrics]. 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"[^p2p3]. 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[^mandiant-tte]. 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[^dbir2026].

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"[^sp800-40]. 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.

### What it is not

- **Not patch rollout.** VulnerabilityOps decides *what* must be fixed and *by when*; [PatchOps](../patchops/) decides *how* to roll it
  out safely.
- **Not pipeline security.** Blocking vulnerable code before release is [DevSecOps](../devsecops/); this page handles what is deployed.
- **Not risk-acceptance policy** ([RiskOps](../riskops/)) **or the owners-and-windows contract** ([SecOps](../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"[^psirt].
- **Not a tool.** CSA says VulnOps does not replace scanners or patch platforms; it reorganises how their output is used[^csa-note].

::: caution Editorial distinction
These boundaries are this Atlas's choice so that each topic is taught once; no standard draws them.
:::

## Where it comes from and where it sits

### Origins and evolution of the label

Vulnerability management is decades old: NIST's patch guidance reached its fourth revision in 2022[^sp800-40].
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)[^cvss-v1][^cvss-main]; the newer signals are the Exploit Prediction Scoring System (EPSS; since April 2021;
model v5 since 15 June 2026)[^epss-data], CISA's KEV catalogue (2021)[^bod2604] and Stakeholder-Specific Vulnerability
Categorization (SSVC) decision trees[^ssvc-cisa].

The label's documented history is short: the earliest use found here is the October 2025 essay[^aes-essay], 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[^csa-note]. 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[^bod2604].

### Glossary and mental model

| Term | Meaning on this page |
|---|---|
| Vulnerability | "a weakness in an information system … that could be exploited or triggered by a threat source" (NIST)[^nist-vuln] |
| CVE record, CNA | Common Vulnerabilities and Exposures: one public ID per publicly disclosed vulnerability, published by CVE Numbering Authorities (CNAs)[^cve-overview] |
| CWE | Common Weakness Enumeration, "a community-developed list of common software and hardware weaknesses": the *type* of flaw[^cwe] |
| CPE | Common Platform Enumeration: product identifiers that NVD enrichment adds and scanners depend on to match CVEs to software[^nvd-status][^csa-note] |
| 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[^cvss-spec] |
| EPSS | Exploit Prediction Scoring System: probability of exploitation activity in the next 30 days, with a percentile rank[^epss] |
| KEV | Known Exploited Vulnerabilities: CISA's catalogue of vulnerabilities with reliable evidence of exploitation[^kev-catalog] |
| 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"[^vulnrichment] |
| VEX | Vulnerability Exploitability eXchange: a supplier's machine-readable statement that a product is or is not affected[^vex-min] |
| 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[^bod2604] |
| Clock, saturation | Clock: maximum time to remediate from a trigger such as KEV listing or detection[^bod2604]. Saturation: arrivals ÷ remediation throughput (CSA)[^csa-note]; 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[^little]. Four levers: **turn down the tap** (fewer vulnerable components),
**widen the drain** (automation, change windows), **choose which water matters** (prioritisation), **check the drain** (verification).

### How VulnerabilityOps relates to other Ops

| Neighbour | What the neighbour owns | Handoff with VulnerabilityOps |
|---|---|---|
| [SecOps](../secops/) | Security–IT contract: owners, maintenance groups, exception register | Owners and windows in; joint scorecard out |
| [PatchOps](../patchops/) | Rings, windows, rollback, reboots | Dated, prioritised item out |
| [DevSecOps](../devsecops/) | Build-time gates, SBOM production | SBOMs in; fixes for shipped code out |
| [ContainerOps](../containerops/) | Image builds, base-image updates | Rebuild request out |
| [ThreatOps](../threatops/) | Intelligence and hunts | Exploitation evidence in |
| [SOC Ops](../soc-ops/), [ResponseOps](../responseops/) | Triage, forensics, incidents | Forensic-triage request out |
| [RiskOps](../riskops/) | Who may accept which risk | Exception request out |
| [CloudSecOps](../cloudsecops/), [KubernetesSecOps](../kubernetessecops/), [EndpointOps](../endpointops/) | Posture, admission, device agents | Exposure and coverage facts in |

::: figure Where VulnerabilityOps sits in the security operations flow
```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).
:::

## Principles and operating model

### Charter and principles

::: decision Atlas proposal: a one-paragraph VulnerabilityOps charter
*We reduce exploitable exposure faster than it arrives. We decide urgency from evidence under a written policy, give every finding an
owner and a date, accept nothing as fixed without proof, and report openly what we have chosen not to fix and why.*
:::

1. **Ration on purpose.** Focus on the highest risk, not all vulnerabilities equally, as BOD 26-04 does[^bod2604].
2. **Severity is an input, not a decision** (see [CVSS and its limits](#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"[^epss-using].
4. **The clock follows the facts.** "The timelines for each asset are dynamic as facts change"[^bod2604].
5. **No owner, no fix.** Every finding goes to the team that can change the asset, via the queue agreed in [SecOps](../secops/).
6. **Closed means verified** (see [Lifecycle](#lifecycle)).
7. **Measure the flow, not the pile.**
8. **Remediation is engineering:** "treating remediation as code"[^csa-note].

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

### Roles, ownership and RACI

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](../secops/) role.

### Lifecycle

- **Day 0, build:** inventory and exposure tags, discovery sources, policy v1 with defaults, routing agreed with
  [SecOps](../secops/) and [PatchOps](../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[^sp800-40].

### Operating model and governance

| 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"[^cis7].

## Architecture and workflow

### Reference architecture and components

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 Reference architecture: from discovery to verified fix
```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[^bod2604] |
| 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 |

### End-to-end workflow

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

::: figure Handoffs when a KEV entry hits two exposed gateways
```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"[^bod2604-ig].

**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"[^ssvc-deployer].
- *Owner cannot meet the clock:* exception request with compensating control and expiry ([RiskOps](../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"[^bod2604-ig]; 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](#core-practices-and-playbooks).
- *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[^vex-min].
- *Compromise found:* the incident moves to [ResponseOps](../responseops/).

## 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 and its limits

CVSS v4.0 scores a flaw's intrinsic characteristics, labelled CVSS-B, -BT, -BE or -BTE by the metric groups used[^cvss-spec]. 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"[^cvss-ug].

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[^nist-nvd-2026]. 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: exploitation probability

EPSS "estimates the probability that a published CVE will be exploited in the wild in the next 30 days", daily, with a percentile
rank[^epss]. 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[^epss-using].
- **It is not about your environment.** Localise it by presence, reachability and consequence[^epss-using]; FIRST lists "Observation
  bias in training labels" among its limits[^epss-how].
- **Never combine it arithmetically with CVSS.** "Do not multiply an EPSS score by an ordinal (such as CVSS)"[^epss-using].

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[^epss-how]. Model v5 replaced v4 on 15 June 2026, so time series shift there[^epss-data].

### KEV: evidence of exploitation

CISA calls KEV "the authoritative source of vulnerabilities that have been exploited in the wild"[^kev-catalog].

- **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"[^bod2604-ig]. Exploitability alone does not qualify; attempted
  exploitation, even against a honeypot, does[^kev-criteria].
- **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[^kev-feed]. 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"[^bod2604-ig]. 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[^bod2604-ig].

### 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[^ssvc-guide]; the CERT/CC deployer model
ends in Defer, Scheduled, Out-of-Cycle or Immediate from Exploitation, System Exposure, Automatable and Human Impact[^ssvc-deployer].
CISA publishes three decision points per CVE (Exploitation, Automatable, Technical Impact) as Vulnrichment data in the CVE
record[^vulnrichment].

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"[^bod2604][^bod2604-ig]. Table 1, transcribed from the directive's image
(calendar days; "triage" = forensic triage within the same three days)[^bod2604]:

| 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[^bod2604][^bod2604-ig].

### Combining the signals

::: figure Atlas proposal: combining presence, KEV, exposure and EPSS
```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"[^bod2604-ig]; SSVC
carries it as Human Impact or Mission Prevalence[^ssvc-deployer][^ssvc-guide]. 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"[^bod2604-ig].

### The data-quality limits of public feeds

- **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"[^nist-nvd-2026][^nvd-status]. 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)[^nvd-dash]. 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[^csa-note]; ENISA's European Vulnerability Database is operational under NIS2[^euvd].

## Practices, playbooks and tools

### Core practices and playbooks

| 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[^redhat-backport]. 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"[^csa-note]. 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"[^sp800-40]. CIS Control 7 cadences: internal scans at least quarterly (authenticated and unauthenticated),
external scans and remediation at least monthly[^cis7].

### Tooling landscape

Examples verified 2026-10-16.

| Tool category | Job | Examples | Watch-outs |
|---|---|---|---|
| Network and host scanners | Missing patches, weak configurations | Tenable Nessus; Qualys[^qualys-scores]; Greenbone OpenVAS (open source)[^greenbone] | Unauthenticated scans miss local flaws[^tenable-cred]; credential failures look like progress |
| Image and SCA scanners | Match components to advisories | Trivy[^trivy-cli]; Grype (EPSS, KEV and OpenVEX support)[^grype] | Third-party code with access to your source[^aqua-trivy] |
| 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[^qualys-scores] |
| 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.

### Automation and AI

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"[^sans-vulnops]. 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.

## VulnerabilityOps in practice

### Reference implementation

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._

```yaml
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[^certcc-bod]. "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[^bod2604-ig].

The second artefact, the **hand-off record** sent with each finding to the owner or [PatchOps](../patchops/), freezes the signals
behind the decision for the auditor.

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

```json
{
  "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"}
}
```

### Hands-on labs

**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

**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"[^epss-data]; 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._

```python
#!/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")
```

3. Run `python3 enrich.py cves.txt`.

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

```text
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[^vulnrichment]. 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

**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._

```text
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
```

2. Save the script as `policy.py`:

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

```python
#!/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]))
```

3. Run `python3 policy.py enriched.json findings.csv 2026-10-16`.

**Expected output:**

```text
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._

```python
#!/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.

#### Lab 3 (advanced): measure the flow

**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._

```python
#!/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())
```

2. Run `python3 flow.py` (or `python3 flow.py export.csv` for your own data).

**Expected output:**

```text
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.

### Use cases and scenarios

| 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[^csrb-log4j]. CSA's bar: a programme "should be able to answer "where is this dependency in our
stack?" within minutes"[^csa-note]. 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[^cisa-citrixbleed]. After installing fixed builds, NetScaler
advised: "we also recommend killing all active and persistent sessions"[^netscaler]. 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[^sp800-82].

## Measure, govern, pay

### Metrics and KPIs

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"[^csa-note]. 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 λ[^little] | 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](../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 The remediation flow and its four levers
```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](#worked-runbook-the-dashboard-turned-green-overnight)). 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.

### Quality, security and compliance

- **Scanner credentials are privileged.** Reliable Windows patch checks need local administrator rights[^tenable-cred]. 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[^aqua-trivy]. Pin scanners by digest, verify checksums and keep them away from unrelated secrets ([DevSecOps](../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[^bod2604-ig].
  Since 11 September 2026 the EU Cyber Resilience Act requires *manufacturers* to report actively exploited vulnerabilities (early
  warning within 24 hours)[^cra]. It is supplier-side but adds advisories to ingest.
- **Evidence.** Keep policy version, signal snapshot, decision and verification per finding ([ComplianceOps](../complianceops/)).

### Cost, capacity and sustainability

The main cost is fixing time, not licences; the 2019 Kenna/Cyentia study found top performers closed roughly 2.5 times more than
others[^p2p3]. 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.

### Assurance of the operating model

- 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.

## When things go wrong

### Common failures and troubleshooting

| 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[^tenable-cred] | 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](../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[^gao-equifax].

### 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 Runbook: separating real progress from lost coverage
```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[^tenable-19506], 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[^tenable-cred].
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.

### Anti-patterns and myths

- **"CVSS 9+ first" as the whole policy.** Severity is not risk ([EPSS](#epss-exploitation-probability)). *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[^epss-using].
  *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[^epss-how].

## Deciding and adopting

### When to use and when not to use

**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](../itops/) and [SecOps](../secops/).
- **The estate is small and patches itself.** Ten SaaS-heavy laptops need patch compliance ([EndpointOps](../endpointops/),
  [PatchOps](../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[^bod2604-ig], plus endpoints.
- **The bottleneck is change approval.** Faster triage only lengthens the change-board queue; fix that first ([ITOps](../itops/)).

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

### Decision framework

::: figure Should you run vulnerability management as VulnerabilityOps?
```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](#a-judgement-call-replace-cvss-bands-with-exposure-and-exploitation-clocks) compares CVSS bands with Table
1-style clocks; a commercial platform is a third option (less engineering, proprietary scores).

### Maturity model

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 |

### Adoption roadmap: 30/60/90 days

| 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](../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 |

## Toolkit

### Checklists

**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](../patchops/) and [ITOps](../itops/)
- [ ] Forensic triage request agreed with the SOC; evidence before fix where possible[^bod2604-ig]
- [ ] 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

### Templates

**One-page remediation policy summary:**

```text
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](../riskops/)):

```text
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:**

```text
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: _______________________________________________________
```

### Deep comparisons

| 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[^cp-voc]) | Attack path, incl. misconfigurations and identities | What can an attacker reach? | Mature programmes adding validation |

### FAQ

**Is VulnerabilityOps just vulnerability management renamed?** Partly: same tasks, plus explicit rationing, verified closure and flow
metrics[^csa-note].

**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"[^bod2604-ig].

**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](#ssvc-and-bod-26-04-decisions-instead-of-scores)),
add a second source for the unenriched tail and track the gap. NIST will consider enrichment on request, as resources
allow[^nist-nvd-2026].

**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.

## Going further

### Advanced topics and emerging direction

- **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"[^aes-essay]; 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[^cswp41].
- **VEX and reachability at scale (emerging; reachability: [DevSecOps](../devsecops/)).** Each VEX statement is its author's
  assertion[^vex-min]; 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[^csa-note].

### Learning path

| Stage | What to do |
|---|---|
| Prerequisites | [SecOps](../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"[^cvss-ug], "Using EPSS"[^epss-using], KEV criteria[^kev-criteria], BOD 26-04 Table 1[^bod2604]; 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](../patchops/) |
| Specialisation | CVSS-BTE enrichment[^cvss-impl]; custom SSVC trees[^ssvc-deployer]; VEX ingestion; LEV[^cswp41] |
| 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](#decision-framework) and the
[judgement call](#a-judgement-call-replace-cvss-bands-with-exposure-and-exploitation-clocks).

## Wrap-up

### Executive recap and cheat sheet

| 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?

**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[^epss-using], 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.

### Learning objectives and mastery check

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.

::: answer 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.
:::

::: answer 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.
:::

::: answer 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.
:::

::: answer 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.
:::

::: answer 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.
:::

::: answer 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.
:::

### Conclusion

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.**

## Sources and further reading

[^csa-note]: [VulnOps: Vulnerability Management in the Age of AI](https://labs.cloudsecurityalliance.org/research/csa-research-note-vulnops-age-of-ai-20260504/) — Cloud Security Alliance, 2026-05-04. Checked 2026-10-17.
[^aes-essay]: [Autonomous AI Hacking and the Future of Cybersecurity](https://www.schneier.com/blog/archives/2025/10/autonomous-ai-hacking-and-the-future-of-cybersecurity.html) — Adkins, Evron, Schneier (CSO essay, reposted), 2025-10-10. Checked 2026-10-16.
[^sans-vulnops]: [VulnOps: A Guide to Starting Implementation From A CISO](https://www.sans.org/blog/vulnops-cisos-guide-to-starting-implementation) — SANS Institute blog (briefing announcement), 2026-08-05. Checked 2026-10-17.
[^cp-voc]: [The Case for a Vulnerability Operations Center](https://blog.checkpoint.com/exposure-management/the-case-for-a-vulnerability-operations-center/) — Check Point (vendor blog), 2026-05-15. Checked 2026-10-17.
[^cve-metrics]: [Metrics](https://www.cve.org/About/Metrics) — CVE Program. Checked 2026-10-16.
[^cve-overview]: [Overview](https://www.cve.org/About/Overview) — CVE Program. Checked 2026-10-16.
[^nist-vuln]: [Vulnerability](https://csrc.nist.gov/glossary/term/vulnerability) — NIST CSRC Glossary. Checked 2026-10-17.
[^cwe]: [About CWE](https://cwe.mitre.org/about/index.html) — MITRE. Checked 2026-10-16.
[^p2p3]: [Prioritization to Prediction, Volume 3](https://www.cyentia.com/wp-content/uploads/2026/05/Kenna-Prioritization_to_Prediction-v3.pdf) — Cyentia Institute for Kenna Security, 2019. Checked 2026-10-16.
[^mandiant-tte]: [How Low Can You Go? An Analysis of 2023 Time-to-Exploit Trends](https://cloud.google.com/blog/topics/threat-intelligence/time-to-exploit-trends-2023) — Mandiant, Google Cloud, 2024-10-16. Checked 2026-10-17.
[^dbir2026]: [2026 Data Breach Investigations Report: Executive Summary](https://www.verizon.com/business/resources/executivebriefs/2026-dbir-executive-summary.pdf) — Verizon, 2026. Checked 2026-10-17.
[^sp800-40]: [NIST SP 800-40 Rev. 4: Guide to Enterprise Patch Management Planning](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-40r4.pdf) — NIST, April 2022. Checked 2026-10-16.
[^psirt]: [PSIRT Services Framework v1.1](https://www.first.org/standards/frameworks/psirts/psirt_services_framework_v1.1) — FIRST. Checked 2026-10-16.
[^cis7]: [Control 7: Continuous Vulnerability Management](https://cas.docs.cisecurity.org/en/latest/source/Controls7/) — CIS Controls Assessment Specification v8.1. Checked 2026-10-16.
[^cvss-v1]: [CVSS v1 Archive](https://www.first.org/cvss/v1/) — FIRST ("FIRST to host CVSS", 14 April 2005). Checked 2026-10-17.
[^cvss-main]: [Common Vulnerability Scoring System](https://www.first.org/cvss/) — FIRST. Checked 2026-10-16.
[^cvss-spec]: [CVSS v4.0 Specification Document](https://www.first.org/cvss/v4.0/specification-document) — FIRST, document version 1.2. Checked 2026-10-16.
[^cvss-ug]: [CVSS v4.0 User Guide](https://www.first.org/cvss/v4.0/user-guide) — FIRST. Checked 2026-10-17.
[^cvss-impl]: [CVSS v4.0 Consumer Implementation Guide](https://www.first.org/cvss/v4.0/implementation-guide) — FIRST, 2026-01-16. Checked 2026-10-16.
[^epss]: [Exploit Prediction Scoring System](https://www.first.org/epss/) — FIRST. Checked 2026-10-16.
[^epss-data]: [EPSS: Get the Data](https://www.first.org/epss/data) — FIRST. Checked 2026-10-17.
[^epss-how]: [How EPSS Works](https://www.first.org/epss/how-it-works) — FIRST. Checked 2026-10-16.
[^epss-using]: [Using EPSS](https://www.first.org/epss/using-epss) — FIRST. Checked 2026-10-17.
[^ssvc-cisa]: [Stakeholder-Specific Vulnerability Categorization](https://www.cisa.gov/resources-tools/resources/stakeholder-specific-vulnerability-categorization-ssvc) — CISA. Checked 2026-10-16.
[^ssvc-guide]: [CISA SSVC Guide](https://www.cisa.gov/sites/default/files/publications/cisa-ssvc-guide%20508c.pdf) — CISA, November 2022. Checked 2026-10-16.
[^ssvc-deployer]: [Prioritizing Patch Deployment](https://certcc.github.io/SSVC/howto/deployer_tree/) — CERT/CC SSVC documentation. Checked 2026-10-16.
[^vulnrichment]: [CISA Vulnrichment](https://github.com/cisagov/vulnrichment) — CISA. Checked 2026-10-17.
[^certcc-bod]: [CISA BOD 26-04 decision table, v1.0.0 (JSON)](https://github.com/CERTCC/SSVC/blob/675fda1e1c7c7d97763ff57ce549183da61f2fba/data/json/decision_tables/cisa/cisa_bod_26_04_1_0_0.json) — CERT/CC SSVC repository, pinned commit. Checked 2026-10-17.
[^kev-catalog]: [Known Exploited Vulnerabilities Catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) — CISA. Checked 2026-10-17.
[^kev-feed]: [KEV JSON feed](https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json) — CISA, version 2026.10.16; counts by the Atlas. Checked 2026-10-17.
[^kev-criteria]: [Reducing the Significant Risk of Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities) — CISA. Checked 2026-10-17.
[^bod2604]: [BOD 26-04: Prioritizing Security Updates Based on Risk](https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk) — CISA, 2026-06-10. Checked 2026-10-16.
[^bod2604-ig]: [BOD 26-04: Implementation Guidance](https://www.cisa.gov/news-events/directives/bod-26-04-implementation-guidance-prioritizing-security-updates-based-risk) — CISA, 2026-06-10. Checked 2026-10-17.
[^nist-nvd-2026]: [NIST Updates NVD Operations to Address Record CVE Growth](https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth) — NIST, 2026-04-15. Checked 2026-10-17.
[^nvd-status]: [Vulnerability Status](https://nvd.nist.gov/vuln/vulnerability-status) — NIST NVD, updated 2026-04-14. Checked 2026-10-17.
[^nvd-dash]: [NVD Dashboard](https://nvd.nist.gov/general/nvd-dashboard) — NIST NVD, live. Checked 2026-10-17.
[^euvd]: [Consult the European Vulnerability Database](https://www.enisa.europa.eu/news/consult-the-european-vulnerability-database-to-enhance-your-digital-security) — ENISA, 2025-05-13. Checked 2026-10-16.
[^qualys-scores]: [Finding-Level Scores: QVS, QDS, and QVSS](https://docs.qualys.com/en/etm/latest/appendix/finding_scores.htm) — Qualys, updated September 2026. Checked 2026-10-17.
[^vex-min]: [Minimum Requirements for VEX](https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex-508c.pdf) — CISA, April 2023. Checked 2026-10-16.
[^little]: [Little's Law as Viewed on Its 50th Anniversary](https://people.cs.umass.edu/~emery/classes/cmpsci691st/readings/OS/Littles-Law-50-Years-Later.pdf) — John D. C. Little, Operations Research, 2011. Checked 2026-10-16.
[^tenable-cred]: [Tenable Nessus Credentialed Checks](https://docs.tenable.com/nessus/Content/NessusCredentialedChecks.htm) — Tenable. Checked 2026-10-17.
[^tenable-19506]: [Nessus Scan Information (plugin 19506)](https://www.tenable.com/plugins/nessus/19506) — Tenable. Checked 2026-10-17.
[^redhat-backport]: [Security Backporting Practice](https://access.redhat.com/security/updates/backporting) — Red Hat. Checked 2026-10-17.
[^greenbone]: [Greenbone Community Edition: Background](https://greenbone.github.io/docs/latest/background.html) — Greenbone. Checked 2026-10-16.
[^trivy-cli]: [trivy image](https://trivy.dev/docs/latest/references/configuration/cli/trivy_image/) — Trivy documentation. Checked 2026-10-16.
[^grype]: [anchore/grype](https://github.com/anchore/grype) — Anchore. Checked 2026-10-16.
[^aqua-trivy]: [Update: Ongoing Investigation and Continued Remediation](https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/) — Aqua Security (Trivy incident post), 2026-03-22, updated 2026-04. Checked 2026-10-17.
[^gao-equifax]: [Data Protection: Actions Taken by Equifax and Federal Agencies (GAO-18-559)](https://www.gao.gov/products/gao-18-559) — US GAO, 2018-08-30. Checked 2026-10-17.
[^csrb-log4j]: [Review of the December 2021 Log4j Event](https://www.cisa.gov/sites/default/files/publications/CSRB-Report-on-Log4-July-11-2022_508.pdf) — Cyber Safety Review Board, 2022-07-11. Checked 2026-10-16.
[^cisa-citrixbleed]: [AA23-325A: LockBit 3.0 Affiliates Exploit CVE 2023-4966 Citrix Bleed](https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-325a) — CISA and partners, 2023-11-21. Checked 2026-10-16.
[^netscaler]: [CVE-2023-4966: Critical security update now available](https://www.netscaler.com/blogs/2023-10/cve-2023-4966-critical-security-update-now-available-for-netscaler-adc-and-netscaler-gateway) — NetScaler, October 2023. Checked 2026-10-16.
[^cra]: [Cyber Resilience Act: Reporting obligations](https://digital-strategy.ec.europa.eu/en/policies/cra-reporting) — European Commission. Checked 2026-10-16.
[^sp800-82]: [NIST SP 800-82 Rev. 3: Guide to Operational Technology Security](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-82r3.pdf) — NIST, September 2023 (§6.3.2.4; Appendix E.2.3). Checked 2026-10-17.
[^cswp41]: [NIST CSWP 41: Likely Exploited Vulnerabilities](https://csrc.nist.gov/pubs/cswp/41/likely-exploited-vulnerabilities-a-proposed-metric/final) — NIST, 2025-05-19. Checked 2026-10-16.

**Further reading:** [EPSS FAQ](https://www.first.org/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.

