Diagrams — Module S08: SDLC Gate Harnesses

All Mermaid validated in Mermaid Live Editor. n8n JSON is structurally valid and importable.


Diagram 1 — The SDLC Gate Harness (n8n)

Type: n8n workflow (importable JSON) Purpose: The primary visual — the full gate harness from PR input through parallel multi-scanner orchestration, aggregation, risk scoring, and gate decision.

{
  "name": "S08 — SDLC Gate Harness",
  "nodes": [
    { "name": "PR Input", "type": "n8n-nodes-base.manualTrigger", "position": [200, 300], "notes": "PR diff + repo snapshot + dependency manifests + IaC files" },
    { "name": "Parallel Scanners", "type": "n8n-nodes-base.set", "position": [440, 300], "notes": "Fan-out to 4 scanner types simultaneously" },
    { "name": "SAST (Semgrep+CodeQL)", "type": "n8n-nodes-base.executeCommand", "position": [680, 150], "notes": "Code weaknesses. ~90s." },
    { "name": "SCA (Snyk+DepCheck)", "type": "n8n-nodes-base.executeCommand", "position": [680, 280], "notes": "Vulnerable dependencies. ~30s." },
    { "name": "Secrets (Gitleaks+Truffle)", "type": "n8n-nodes-base.executeCommand", "position": [680, 380], "notes": "Committed credentials. ~10s." },
    { "name": "IaC (Checkov+OPA)", "type": "n8n-nodes-base.executeCommand", "position": [680, 470], "notes": "Insecure infra declarations. ~15s." },
    { "name": "Aggregation + Dedup", "type": "n8n-nodes-base.set", "position": [920, 300], "notes": "Normalize to unified schema. Cross-scanner dedup via dedup_key. Severity reconciliation." },
    { "name": "Cross-Scanner Triage (LLM)", "type": "n8n-nodes-base.set", "position": [1160, 300], "notes": "Correlate related findings across scanners. Upgrade/downgrade severity. Token-budgeted." },
    { "name": "Risk Score + Trend", "type": "n8n-nodes-base.set", "position": [1400, 300], "notes": "Score from finding counts + build-history trend. Single number 0-100." },
    { "name": "Gate Decision", "type": "n8n-nodes-base.set", "position": [1640, 300], "notes": "Apply hard-gate/soft-gate matrix. Block, warn, or pass." },
    { "name": "Build History Store", "type": "n8n-nodes-base.set", "position": [1400, 470], "notes": "Finding counts per build. Trend detection over N builds." }
  ],
  "connections": {
    "PR Input": { "main": [[{ "node": "Parallel Scanners", "type": "main", "index": 0 }]] },
    "Parallel Scanners": { "main": [[{ "node": "SAST (Semgrep+CodeQL)", "type": "main", "index": 0 }], [{ "node": "SCA (Snyk+DepCheck)", "type": "main", "index": 0 }], [{ "node": "Secrets (Gitleaks+Truffle)", "type": "main", "index": 0 }], [{ "node": "IaC (Checkov+OPA)", "type": "main", "index": 0 }]] },
    "SAST (Semgrep+CodeQL)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "SCA (Snyk+DepCheck)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "Secrets (Gitleaks+Truffle)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "IaC (Checkov+OPA)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "Aggregation + Dedup": { "main": [[{ "node": "Cross-Scanner Triage (LLM)", "type": "main", "index": 0 }]] },
    "Cross-Scanner Triage (LLM)": { "main": [[{ "node": "Risk Score + Trend", "type": "main", "index": 0 }]] },
    "Build History Store": { "main": [[{ "node": "Risk Score + Trend", "type": "main", "index": 0 }]] },
    "Risk Score + Trend": { "main": [[{ "node": "Gate Decision", "type": "main", "index": 0 }]] }
  }
}

Reading the diagram: A PR enters and fans out to four scanner types in parallel — SAST, SCA, secrets, IaC. Parallel execution bounds wall-clock time to the slowest scanner (~90s), not the sum. Results aggregate into a unified schema with cross-scanner dedup. The LLM triage layer correlates related findings across scanners (e.g. an SCA vulnerability upgraded because the vulnerable path is reachable per SAST), under a token budget prioritizing high-severity findings. The risk score combines finding counts with the build-history trend into a single number. The gate decision applies the hard-gate/soft-gate matrix: block, warn, or pass. The build history store holds finding counts per build for trend detection.


Diagram 2 — The Control Plane Shift

Type: Mermaid (flowchart) Purpose: Shows the structural difference between review-time checks (old) and creation-time guardrails (new), and why the shift is forced by AI coding agents.

flowchart LR
    subgraph OLD["Review-time check (old)"]
        C1["Commit"] --> SCAN["Scan overnight"]
        SCAN --> DASH["Dashboard"]
        DASH --> TICKET["Ticket, days later"]
        TICKET --> PROD["Already in production"]
    end
    subgraph NEW["Creation-time guardrail (new)"]
        C2["Developer creates"] --> GUARD["Guardrail runs<br/>synchronously"]
        GUARD -->|"insecure"| BLOCK["Block + inline feedback<br/>(seconds)"]
        GUARD -->|"secure"| MERGE["Proceed"]
    end
    AGENT["AI coding agent<br/>creates code/deps/IaC<br/>in seconds"]
    AGENT -.->|"too fast for review-time"| OLD
    AGENT -.->|"defense must match speed"| NEW

    style OLD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style PROD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style NEW fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style BLOCK fill:#14141f,stroke:#5eead4,color:#5eead4
    style AGENT fill:#2a1810,stroke:#a04000,color:#f0a868

Reading the diagram: The review-time model (left) commits, scans overnight, files a dashboard finding, opens a ticket days later — by which point the code is in production. Feedback latency is hours to days. This was tolerable when developers wrote code at human pace. AI coding agents (top) create code, dependencies, and infrastructure in seconds — far faster than review-time checks can process. The defense must match the speed: creation-time guardrails (right) run synchronously in the developer's (or agent's) path, blocking insecure artifacts with inline feedback in seconds. The shift is structural — the gate moves from after to before.


Diagram 3 — Hard Gate vs Soft Gate Decision Matrix

Type: Mermaid (flowchart) Purpose: Shows the decision logic for when to block vs warn, per scan type and finding characteristic.

flowchart TD
    FIND["Finding (normalized)"]
    TYPE{"Scan type +<br/>characteristic?"}

    SECRET["Secrets<br/>(validated, high conf)"]
    SAST_C["SAST critical<br/>(high conf)"]
    SCA_KEV["SCA: CISA KEV<br/>or EPSS > 0.7"]
    IAC_PUB["IaC: public exposure<br/>0.0.0.0/0, public S3"]
    SAST_ML["SAST medium/low"]
    SCA_LOW["SCA low EPSS,<br/>fix available"]
    LIC["License<br/>unknown/copyleft"]

    FIND --> TYPE
    TYPE --> SECRET
    TYPE --> SAST_C
    TYPE --> SCA_KEV
    TYPE --> IAC_PUB
    TYPE --> SAST_ML
    TYPE --> SCA_LOW
    TYPE --> LIC

    SECRET --> HARD["HARD BLOCK<br/>cannot merge"]
    SAST_C --> HARD
    SCA_KEV --> HARD
    IAC_PUB --> HARD
    SAST_ML --> SOFT["SOFT WARN<br/>proceed, surface for review"]
    SCA_LOW --> SOFT
    LIC --> SOFT

    style HARD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style SOFT fill:#2a1810,stroke:#a04000,color:#f0a868
    style TYPE fill:#14141f,stroke:#5eead4,color:#5eead4

Reading the diagram: The gate decision depends on scan type and finding characteristic. Hard block: validated secrets (active breach path), SAST critical with high confidence (exploit path ships), SCA matching CISA KEV or EPSS above 0.7 (known/likely exploited), IaC public exposure (immediate risk). Soft warn: SAST medium/low (surface for review, don't block), SCA with low EPSS and a fix available (triage and schedule), license issues (legal review). The matrix is the calibration — over-blocking trains developers to override without reading; under-blocking ships vulnerabilities. Block the immediately dangerous; warn the rest.


Diagram 4 — Risk Score and Trend

Type: Mermaid (flowchart) Purpose: Shows how build history feeds risk scoring and trend detection, producing a single number that gates PR approval.

flowchart LR
    HIST["Build history<br/>(finding counts per build,<br/>last N builds)"]
    CURRENT["Current PR<br/>(new findings introduced)"]
    NET["Net direction<br/>(new introduced −<br/>existing fixed)"]
    TREND{"Trend over<br/>last 10 builds?"}
    SCORE["Risk score 0-100<br/>severity-weighted<br/>+ trend modifier"]
    GATE1["Score < 30:<br/>pass"]
    GATE2["Score 30-60:<br/>requires security<br/>reviewer approval"]
    GATE3["Score > 60:<br/>BLOCKED until<br/>findings resolved"]

    HIST --> NET
    CURRENT --> NET
    NET --> SCORE
    HIST --> TREND
    TREND -->|"rising → ×1.2"| SCORE
    SCORE --> GATE1
    SCORE --> GATE2
    SCORE --> GATE3

    style SCORE fill:#14141f,stroke:#5eead4,color:#5eead4
    style GATE3 fill:#2a0d0d,stroke:#a00000,color:#f08080
    style GATE2 fill:#2a1810,stroke:#a04000,color:#f0a868
    style GATE1 fill:#0d2818,stroke:#1e8449,color:#82e0aa

Reading the diagram: The risk score combines the current PR's new findings (severity-weighted: critical=25, high=10, medium=3, low=1) with net direction (new introduced minus existing fixed) and a trend modifier (rising trend over 10 builds multiplies the score by 1.2). The single number gates approval: below 30, pass; 30-60, requires a security reviewer's explicit approval; above 60, blocked until findings are resolved. The thresholds are policy; the score is computed, consistent, and auditable — not a gut call. Trend matters: a PR that introduces 5 criticals but fixes 7 is net-positive; a PR that introduces 5 and fixes 0 is net-negative.


Diagram 5 — Vulnerability Triage at Scale (CVE → priority queue)

Type: Mermaid (flowchart) Purpose: Shows the pipeline from CVE feed ingestion to a prioritized remediation queue, using EPSS and CISA KEV.

flowchart TD
    NVD["NVD CVE feed<br/>(thousands/year)"]
    MATCH["Dependency matching<br/>(vs package.json,<br/>requirements.txt, go.mod)"]
    EPSS["EPSS scoring<br/>(probability of exploit<br/>in 30 days, 0-1)"]
    REACH{"Environment<br/>relevance?<br/>(vulnerable path reachable?)"}
    QUEUE["Prioritized remediation queue"]

    KEV["CISA KEV match"]
    HIGH["EPSS > 0.7,<br/>reachable"]
    VERIFY["EPSS > 0.7,<br/>not confirmed"]
    BACKLOG["High CVSS,<br/>low EPSS"]
    ACCEPT["Low CVSS,<br/>low EPSS"]

    NVD --> MATCH --> EPSS --> REACH --> QUEUE
    QUEUE --> KEV
    QUEUE --> HIGH
    QUEUE --> VERIFY
    QUEUE --> BACKLOG
    QUEUE --> ACCEPT

    KEV_FIX["Fix: HOURS<br/>(active exploitation)"]
    HIGH_FIX["Fix: DAYS"]
    VERIFY_FIX["Verify reachability,<br/>then fix"]
    BACKLOG_FIX["Schedule in backlog"]
    ACCEPT_FIX["Accept risk / batch-fix"]

    KEV --> KEV_FIX
    HIGH --> HIGH_FIX
    VERIFY --> VERIFY_FIX
    BACKLOG --> BACKLOG_FIX
    ACCEPT --> ACCEPT_FIX

    style NVD fill:#2a1810,stroke:#a04000,color:#f0a868
    style EPSS fill:#14141f,stroke:#5eead4,color:#5eead4
    style KEV fill:#2a0d0d,stroke:#a00000,color:#f08080
    style KEV_FIX fill:#2a0d0d,stroke:#a00000,color:#f08080
    style HIGH fill:#2a1810,stroke:#a04000,color:#f0a868
    style ACCEPT fill:#0d2818,stroke:#1e8449,color:#82e0aa

Reading the diagram: The NVD feed publishes thousands of CVEs per year. Dependency matching narrows to CVEs that affect the application's actual dependencies. EPSS scoring adds exploitation probability. Environment-relevance filtering removes CVEs in unreachable code paths. The output is a prioritized queue: CISA KEV matches first (confirmed active exploitation — fix in hours), then EPSS above 0.7 and reachable (fix in days), then EPSS above 0.7 but not confirmed reachable (verify first), then high-CVSS/low-EPSS (backlog), then low-CVSS/low-EPSS (accept risk or batch-fix). The pipeline turns hundreds of raw CVEs into three things to fix this week. EPSS and KEV are the signals that prevent CVSS-only prioritization — a CVSS 9.8 with EPSS 0.01 is less urgent than a CVSS 7.5 with EPSS 0.8.

# Diagrams — Module S08: SDLC Gate Harnesses

> All Mermaid validated in Mermaid Live Editor. n8n JSON is structurally valid and importable.

---

## Diagram 1 — The SDLC Gate Harness (n8n)

**Type**: n8n workflow (importable JSON)
**Purpose**: The primary visual — the full gate harness from PR input through parallel multi-scanner orchestration, aggregation, risk scoring, and gate decision.

```json
{
  "name": "S08 — SDLC Gate Harness",
  "nodes": [
    { "name": "PR Input", "type": "n8n-nodes-base.manualTrigger", "position": [200, 300], "notes": "PR diff + repo snapshot + dependency manifests + IaC files" },
    { "name": "Parallel Scanners", "type": "n8n-nodes-base.set", "position": [440, 300], "notes": "Fan-out to 4 scanner types simultaneously" },
    { "name": "SAST (Semgrep+CodeQL)", "type": "n8n-nodes-base.executeCommand", "position": [680, 150], "notes": "Code weaknesses. ~90s." },
    { "name": "SCA (Snyk+DepCheck)", "type": "n8n-nodes-base.executeCommand", "position": [680, 280], "notes": "Vulnerable dependencies. ~30s." },
    { "name": "Secrets (Gitleaks+Truffle)", "type": "n8n-nodes-base.executeCommand", "position": [680, 380], "notes": "Committed credentials. ~10s." },
    { "name": "IaC (Checkov+OPA)", "type": "n8n-nodes-base.executeCommand", "position": [680, 470], "notes": "Insecure infra declarations. ~15s." },
    { "name": "Aggregation + Dedup", "type": "n8n-nodes-base.set", "position": [920, 300], "notes": "Normalize to unified schema. Cross-scanner dedup via dedup_key. Severity reconciliation." },
    { "name": "Cross-Scanner Triage (LLM)", "type": "n8n-nodes-base.set", "position": [1160, 300], "notes": "Correlate related findings across scanners. Upgrade/downgrade severity. Token-budgeted." },
    { "name": "Risk Score + Trend", "type": "n8n-nodes-base.set", "position": [1400, 300], "notes": "Score from finding counts + build-history trend. Single number 0-100." },
    { "name": "Gate Decision", "type": "n8n-nodes-base.set", "position": [1640, 300], "notes": "Apply hard-gate/soft-gate matrix. Block, warn, or pass." },
    { "name": "Build History Store", "type": "n8n-nodes-base.set", "position": [1400, 470], "notes": "Finding counts per build. Trend detection over N builds." }
  ],
  "connections": {
    "PR Input": { "main": [[{ "node": "Parallel Scanners", "type": "main", "index": 0 }]] },
    "Parallel Scanners": { "main": [[{ "node": "SAST (Semgrep+CodeQL)", "type": "main", "index": 0 }], [{ "node": "SCA (Snyk+DepCheck)", "type": "main", "index": 0 }], [{ "node": "Secrets (Gitleaks+Truffle)", "type": "main", "index": 0 }], [{ "node": "IaC (Checkov+OPA)", "type": "main", "index": 0 }]] },
    "SAST (Semgrep+CodeQL)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "SCA (Snyk+DepCheck)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "Secrets (Gitleaks+Truffle)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "IaC (Checkov+OPA)": { "main": [[{ "node": "Aggregation + Dedup", "type": "main", "index": 0 }]] },
    "Aggregation + Dedup": { "main": [[{ "node": "Cross-Scanner Triage (LLM)", "type": "main", "index": 0 }]] },
    "Cross-Scanner Triage (LLM)": { "main": [[{ "node": "Risk Score + Trend", "type": "main", "index": 0 }]] },
    "Build History Store": { "main": [[{ "node": "Risk Score + Trend", "type": "main", "index": 0 }]] },
    "Risk Score + Trend": { "main": [[{ "node": "Gate Decision", "type": "main", "index": 0 }]] }
  }
}
```

**Reading the diagram**: A PR enters and fans out to four scanner types in parallel — SAST, SCA, secrets, IaC. Parallel execution bounds wall-clock time to the slowest scanner (~90s), not the sum. Results aggregate into a unified schema with cross-scanner dedup. The LLM triage layer correlates related findings across scanners (e.g. an SCA vulnerability upgraded because the vulnerable path is reachable per SAST), under a token budget prioritizing high-severity findings. The risk score combines finding counts with the build-history trend into a single number. The gate decision applies the hard-gate/soft-gate matrix: block, warn, or pass. The build history store holds finding counts per build for trend detection.

---

## Diagram 2 — The Control Plane Shift

**Type**: Mermaid (flowchart)
**Purpose**: Shows the structural difference between review-time checks (old) and creation-time guardrails (new), and why the shift is forced by AI coding agents.

```mermaid
flowchart LR
    subgraph OLD["Review-time check (old)"]
        C1["Commit"] --> SCAN["Scan overnight"]
        SCAN --> DASH["Dashboard"]
        DASH --> TICKET["Ticket, days later"]
        TICKET --> PROD["Already in production"]
    end
    subgraph NEW["Creation-time guardrail (new)"]
        C2["Developer creates"] --> GUARD["Guardrail runs<br/>synchronously"]
        GUARD -->|"insecure"| BLOCK["Block + inline feedback<br/>(seconds)"]
        GUARD -->|"secure"| MERGE["Proceed"]
    end
    AGENT["AI coding agent<br/>creates code/deps/IaC<br/>in seconds"]
    AGENT -.->|"too fast for review-time"| OLD
    AGENT -.->|"defense must match speed"| NEW

    style OLD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style PROD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style NEW fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style BLOCK fill:#14141f,stroke:#5eead4,color:#5eead4
    style AGENT fill:#2a1810,stroke:#a04000,color:#f0a868
```

**Reading the diagram**: The review-time model (left) commits, scans overnight, files a dashboard finding, opens a ticket days later — by which point the code is in production. Feedback latency is hours to days. This was tolerable when developers wrote code at human pace. AI coding agents (top) create code, dependencies, and infrastructure in seconds — far faster than review-time checks can process. The defense must match the speed: creation-time guardrails (right) run synchronously in the developer's (or agent's) path, blocking insecure artifacts with inline feedback in seconds. The shift is structural — the gate moves from after to before.

---

## Diagram 3 — Hard Gate vs Soft Gate Decision Matrix

**Type**: Mermaid (flowchart)
**Purpose**: Shows the decision logic for when to block vs warn, per scan type and finding characteristic.

```mermaid
flowchart TD
    FIND["Finding (normalized)"]
    TYPE{"Scan type +<br/>characteristic?"}

    SECRET["Secrets<br/>(validated, high conf)"]
    SAST_C["SAST critical<br/>(high conf)"]
    SCA_KEV["SCA: CISA KEV<br/>or EPSS > 0.7"]
    IAC_PUB["IaC: public exposure<br/>0.0.0.0/0, public S3"]
    SAST_ML["SAST medium/low"]
    SCA_LOW["SCA low EPSS,<br/>fix available"]
    LIC["License<br/>unknown/copyleft"]

    FIND --> TYPE
    TYPE --> SECRET
    TYPE --> SAST_C
    TYPE --> SCA_KEV
    TYPE --> IAC_PUB
    TYPE --> SAST_ML
    TYPE --> SCA_LOW
    TYPE --> LIC

    SECRET --> HARD["HARD BLOCK<br/>cannot merge"]
    SAST_C --> HARD
    SCA_KEV --> HARD
    IAC_PUB --> HARD
    SAST_ML --> SOFT["SOFT WARN<br/>proceed, surface for review"]
    SCA_LOW --> SOFT
    LIC --> SOFT

    style HARD fill:#2a0d0d,stroke:#a00000,color:#f08080
    style SOFT fill:#2a1810,stroke:#a04000,color:#f0a868
    style TYPE fill:#14141f,stroke:#5eead4,color:#5eead4
```

**Reading the diagram**: The gate decision depends on scan type and finding characteristic. Hard block: validated secrets (active breach path), SAST critical with high confidence (exploit path ships), SCA matching CISA KEV or EPSS above 0.7 (known/likely exploited), IaC public exposure (immediate risk). Soft warn: SAST medium/low (surface for review, don't block), SCA with low EPSS and a fix available (triage and schedule), license issues (legal review). The matrix is the calibration — over-blocking trains developers to override without reading; under-blocking ships vulnerabilities. Block the immediately dangerous; warn the rest.

---

## Diagram 4 — Risk Score and Trend

**Type**: Mermaid (flowchart)
**Purpose**: Shows how build history feeds risk scoring and trend detection, producing a single number that gates PR approval.

```mermaid
flowchart LR
    HIST["Build history<br/>(finding counts per build,<br/>last N builds)"]
    CURRENT["Current PR<br/>(new findings introduced)"]
    NET["Net direction<br/>(new introduced −<br/>existing fixed)"]
    TREND{"Trend over<br/>last 10 builds?"}
    SCORE["Risk score 0-100<br/>severity-weighted<br/>+ trend modifier"]
    GATE1["Score < 30:<br/>pass"]
    GATE2["Score 30-60:<br/>requires security<br/>reviewer approval"]
    GATE3["Score > 60:<br/>BLOCKED until<br/>findings resolved"]

    HIST --> NET
    CURRENT --> NET
    NET --> SCORE
    HIST --> TREND
    TREND -->|"rising → ×1.2"| SCORE
    SCORE --> GATE1
    SCORE --> GATE2
    SCORE --> GATE3

    style SCORE fill:#14141f,stroke:#5eead4,color:#5eead4
    style GATE3 fill:#2a0d0d,stroke:#a00000,color:#f08080
    style GATE2 fill:#2a1810,stroke:#a04000,color:#f0a868
    style GATE1 fill:#0d2818,stroke:#1e8449,color:#82e0aa
```

**Reading the diagram**: The risk score combines the current PR's new findings (severity-weighted: critical=25, high=10, medium=3, low=1) with net direction (new introduced minus existing fixed) and a trend modifier (rising trend over 10 builds multiplies the score by 1.2). The single number gates approval: below 30, pass; 30-60, requires a security reviewer's explicit approval; above 60, blocked until findings are resolved. The thresholds are policy; the score is computed, consistent, and auditable — not a gut call. Trend matters: a PR that introduces 5 criticals but fixes 7 is net-positive; a PR that introduces 5 and fixes 0 is net-negative.

---

## Diagram 5 — Vulnerability Triage at Scale (CVE → priority queue)

**Type**: Mermaid (flowchart)
**Purpose**: Shows the pipeline from CVE feed ingestion to a prioritized remediation queue, using EPSS and CISA KEV.

```mermaid
flowchart TD
    NVD["NVD CVE feed<br/>(thousands/year)"]
    MATCH["Dependency matching<br/>(vs package.json,<br/>requirements.txt, go.mod)"]
    EPSS["EPSS scoring<br/>(probability of exploit<br/>in 30 days, 0-1)"]
    REACH{"Environment<br/>relevance?<br/>(vulnerable path reachable?)"}
    QUEUE["Prioritized remediation queue"]

    KEV["CISA KEV match"]
    HIGH["EPSS > 0.7,<br/>reachable"]
    VERIFY["EPSS > 0.7,<br/>not confirmed"]
    BACKLOG["High CVSS,<br/>low EPSS"]
    ACCEPT["Low CVSS,<br/>low EPSS"]

    NVD --> MATCH --> EPSS --> REACH --> QUEUE
    QUEUE --> KEV
    QUEUE --> HIGH
    QUEUE --> VERIFY
    QUEUE --> BACKLOG
    QUEUE --> ACCEPT

    KEV_FIX["Fix: HOURS<br/>(active exploitation)"]
    HIGH_FIX["Fix: DAYS"]
    VERIFY_FIX["Verify reachability,<br/>then fix"]
    BACKLOG_FIX["Schedule in backlog"]
    ACCEPT_FIX["Accept risk / batch-fix"]

    KEV --> KEV_FIX
    HIGH --> HIGH_FIX
    VERIFY --> VERIFY_FIX
    BACKLOG --> BACKLOG_FIX
    ACCEPT --> ACCEPT_FIX

    style NVD fill:#2a1810,stroke:#a04000,color:#f0a868
    style EPSS fill:#14141f,stroke:#5eead4,color:#5eead4
    style KEV fill:#2a0d0d,stroke:#a00000,color:#f08080
    style KEV_FIX fill:#2a0d0d,stroke:#a00000,color:#f08080
    style HIGH fill:#2a1810,stroke:#a04000,color:#f0a868
    style ACCEPT fill:#0d2818,stroke:#1e8449,color:#82e0aa
```

**Reading the diagram**: The NVD feed publishes thousands of CVEs per year. Dependency matching narrows to CVEs that affect the application's actual dependencies. EPSS scoring adds exploitation probability. Environment-relevance filtering removes CVEs in unreachable code paths. The output is a prioritized queue: CISA KEV matches first (confirmed active exploitation — fix in hours), then EPSS above 0.7 and reachable (fix in days), then EPSS above 0.7 but not confirmed reachable (verify first), then high-CVSS/low-EPSS (backlog), then low-CVSS/low-EPSS (accept risk or batch-fix). The pipeline turns hundreds of raw CVEs into three things to fix this week. EPSS and KEV are the signals that prevent CVSS-only prioritization — a CVSS 9.8 with EPSS 0.01 is less urgent than a CVSS 7.5 with EPSS 0.8.