These are my notes for a question I keep coming back to: how do you keep shipping software when new vulnerabilities arrive faster than you can fix them, and attackers can use an LLM to turn a CVE description into a working exploit? There is no single standard answer. What follows is the concept I would build, with the evidence behind each choice and the parts where I am still unsure.
Prefer a walk-through? The same ideas are in a 15-slide deck: SSDLC when exploits are cheap, the slides.
Three clocks that changed#
The volume clock. 2025 had 48,185 published CVEs, up 20.6% on 2024. That is about 132 a day. About 39% were rated high or critical. No team can read them all, let alone fix them all.
The exploit clock. VulnCheck’s State of Exploitation 2026 counted 884 CVEs exploited for the first time in 2025. 28.96% of them were exploited on or before the day the CVE was published, up from 23.6% in 2024. For almost a third of the vulnerabilities that matter, there is no grace period.
The AI clock. In 2024, researchers showed that an LLM agent of 91 lines could exploit 87% of a set of 15 real one-day vulnerabilities when given the CVE description, and only 7% without it. The sample is small, but the lesson is clear: the CVE text itself is the attacker’s instructions. Defenders have the same tools. At DARPA’s AI Cyber Challenge final in August 2025, automated systems found 86% of the planted vulnerabilities and patched 68% of them, in about 45 minutes per patch on average.
CISA now writes this into policy. Its June 2026 directive BOD 26-04 says threat actors’ “use of AI may further narrow the time defenders have to react”.
Why “SDLC plus a scan at the end” fails#
The classic SDLC (software development life cycle: plan, design, build, test, release, operate) treats security as a gate before release. That made sense with quarterly releases and a few CVEs a week.

Today it fails in three ways:
- The gate is too late. A finding at release time competes with a deadline, and the deadline usually wins.
- The gate is a snapshot. Code that was clean on Tuesday is vulnerable on Wednesday because a dependency got a CVE. Nothing in the code changed.
- The gate produces a report, not a fix. A PDF of 400 findings is a to-do list nobody owns.
An SSDLC (secure software development life cycle) makes security part of every stage, and keeps scanning software after it ships. The second part is the one most teams miss.
| Classic SDLC | SSDLC as I would run it | |
|---|---|---|
| When security happens | Gate before release | Every commit, every merge, every night, in production |
| Unit of output | Report | Pull request or ticket with an owner and a due date |
| Priority | CVSS severity | Exploited? Reachable? Likely? (KEV, exposure, EPSS) |
| Who fixes trivial issues | A developer, eventually | A bot opens the PR; a developer reviews it |
| Shipped software | Done | Rescanned daily against new CVEs |
There are standards, but they say what, not how fast#
“There is no standard recommendation” is half true. The frameworks exist; they just leave out the clock. If these names are new to you, I explain each one in plain words, with what it is worth, in a short series: SSDF, SAMM, SLSA and KEV with BOD 26-04.
- NIST SSDF (SP 800-218, version 1.1) groups practices into Prepare the Organization, Protect the Software, Produce Well-Secured Software and Respond to Vulnerabilities. Version 1.2 was published as a draft in December 2025 and is not final as I write this.
- OWASP SAMM (v2.0) gives five business functions (Governance, Design, Implementation, Verification, Operations) and lets you measure maturity. It is where Education & Guidance lives.
- SLSA (v1.2) covers build integrity: signed provenance from a hosted, hardened build platform.
- CISA BOD 26-04 is the closest thing to a timebox. It binds US federal civilian agencies only, but its logic carries over. It scores each vulnerability on four questions: is the asset publicly exposed, is the CVE in the Known Exploited Vulnerabilities (KEV) catalog, can the exploit be automated, and does it give partial or total control? The worst combinations get three days. CISA reports that at one large agency only 1% of vulnerability instances fell into that category, and over 60% could wait for the next planned upgrade.
That last number matters for morale. Risk-based triage does not mean “fix everything faster”. It means fix a small set very fast, and stop pretending the rest is urgent.
The loop and its timeboxes#
Here is the rhythm I would aim for. The numbers are my proposal, not a standard. They are chosen so that each stage stays fast enough that developers do not route around it.
| Stage | Trigger | What runs | Timebox | Blocks? |
|---|---|---|---|---|
| 1. Commit | git commit | Secrets scan (pre-commit hook) | Seconds | Yes, locally |
| 2. Pull request | PR opened or updated | SAST on the diff, dependency diff, IaC checks, unit tests | Under 10 minutes | New criticals and secrets only |
| 3. Merge to main | Push | Full SAST, SCA, container scan, SBOM, signed build | Under 30 minutes | No; findings go to the store |
| 4. Nightly | Schedule | Rescan released SBOMs against new CVEs; DAST baseline on staging | Overnight | No |
| 5. Triage | New finding in the store | Enrich with KEV, EPSS, exposure; assign tier and owner | Same day | n/a |
| 6. Fix | Tier due date | Bot PR or human fix | T0: 3 days · T1: 14 · T2: 30 · T3: next upgrade | n/a |
| 7. Verify | Fix merged | Rescan; close the finding only when the scanner agrees | Next pipeline run | n/a |
Two rules keep this honest:
- Block only on what is new. Blocking a PR for a 3-year-old finding in another module teaches people to hate the scanner. Block new criticals and secrets; track everything else.
- Stage 4 is the important one. Most new risk is not in code you write this week. It is in code you shipped months ago and a CVE published today.
What each level is fed, and which vector it guards#
A stage is only as good as what you feed it. A secrets scanner that sees only the diff cannot find a token
committed last year; a nightly rescan that has no SBOM of what is running has nothing to rescan. So for each
level I write down three things: the feed (input), the check (tool), and the vector (the attack path
it closes). The vectors use two public vocabularies so they can be discussed without ambiguity: the
OWASP Top 10:2025 categories (A0x) and MITRE ATT&CK
technique IDs (T1xxx). If either is new to you, each has a short introduction with cheat cards:
OWASP Top 10:2025, plainly and
MITRE ATT&CK, plainly. The chart is dense; use Open full
size to read it.
flowchart TB
subgraph L1["Level 1 · Commit · seconds"]
direction LR
F1[/"Feed: staged diff"/] --> C1["Gitleaks"] --> O1(["Block the commit"])
O1 -.- V1{{"Leaked credentials
T1552.001"}}:::vec
end
subgraph L2["Level 2 · Pull request · under 10 min"]
direction LR
F2[/"Feed: code diff,
lockfile diff, IaC diff"/] --> C2["Semgrep · OSV-Scanner
Trivy config"] --> O2(["PR comments + SARIF"])
O2 -.- V2{{"Injection A05
Misconfiguration A02
Bad dependency A03 · T1195.001"}}:::vec
end
subgraph L3["Level 3 · Merge and build · under 30 min"]
direction LR
F3[/"Feed: full repo,
built image, build steps"/] --> C3["Trivy image · SBOM
signed provenance"] --> O3(["SBOM + SARIF to store"])
O3 -.- V3{{"Vulnerable base image A03
Tampered build A08 · T1195.002"}}:::vec
end
subgraph L4["Level 4 · Nightly · overnight"]
direction LR
F4[/"Feed: released SBOMs,
new CVEs, staging URL"/] --> C4["Dependency-Track
ZAP baseline"] --> O4(["New findings on
shipped code"])
O4 -.- V4{{"New CVE in running app
T1190"}}:::vec
end
subgraph L5["Level 5 · Triage · same day"]
direction LR
F5[/"Feed: findings + KEV
+ EPSS + exposure + owner"/] --> C5["triage.py
policy as code"] --> O5(["Tier T0 to T3
+ due date"]):::tier
end
subgraph L6["Level 6 · Fix and verify · 3 / 14 / 30 days"]
direction LR
F6[/"Feed: tiered finding,
code context, tests"/] --> C6["Bot or LLM
draft PR"] --> O6(["Human merges,
rescan closes"])
end
L1 --> L2 --> L3 --> L4 --> L5 --> L6
L6 -. "every fix re-enters at level 2" .-> L2
classDef vec fill:#fee2e2,stroke:#ef4444,color:#7f1d1d
classDef tier fill:#ef4444,stroke:#7f1d1d,color:#fff
| Vector | ID | Caught at | Why there |
|---|---|---|---|
| Credentials committed to the repository | ATT&CK T1552.001 Credentials In Files | Level 1 | Once pushed, a secret is in history for good; block it before it leaves the laptop |
| Injection in new code | OWASP A05:2025 Injection | Level 2 | Cheapest to fix while the author still has the context |
| Insecure configuration (IaC, containers) | OWASP A02:2025 Security Misconfiguration | Level 2 | IaC diffs are small and reviewable in the PR |
| Vulnerable or malicious dependency added | OWASP A03:2025 Software Supply Chain Failures; ATT&CK T1195.001 | Level 2 | The lockfile diff shows exactly what is new |
| Vulnerable OS packages in the image | OWASP A03:2025 | Level 3 | Only exists after the image is built |
| Build tampered with between source and artefact | OWASP A08:2025 Software or Data Integrity Failures; ATT&CK T1195.002 | Level 3 | Signed provenance (SLSA) proves what was built from what |
| New CVE in software already running | ATT&CK T1190 Exploit Public-Facing Application | Level 4 | The code did not change; the world did. Only a rescan of the released SBOM sees it |
Two vectors deliberately do not appear: broken access control (A01) and insecure design (A06). Scanners are poor at both. They belong to threat modelling at design time and to the human-only lane further down.
An open-source toolchain#
Every tool here has an OSI licence and can write SARIF (the JSON format for static analysis results) or an SBOM (software bill of materials, a list of every component in a build), so results can go into one store.
| Job | Tool | Licence |
|---|---|---|
| Secrets | Gitleaks | MIT |
| SAST (your code) | Semgrep Community Edition | LGPL-2.1 |
| SCA (dependencies) | OSV-Scanner | Apache-2.0 |
| Containers, IaC, SBOM | Trivy | Apache-2.0 |
| DAST (running app) | ZAP | Apache-2.0 |
| SBOM monitoring | Dependency-Track | Apache-2.0 |
| Findings store | DefectDojo | BSD-3-Clause |
The PR and merge stages, as commands:
# Stage 1-2: secrets and static analysis, SARIF out
gitleaks git --report-format sarif --report-path gitleaks.sarif
semgrep scan --config p/ci --sarif-output=semgrep.sarif --error
# Stage 3: dependencies, containers, SBOM
osv-scanner scan source -r .
trivy image --format sarif -o trivy.sarif my-app:1.4.2
trivy image --format cyclonedx -o sbom.cdx.json my-app:1.4.2
# Stage 4: passive DAST against staging (it does not attack)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t https://staging.example.comThe flags come from each tool’s current documentation: Semgrep’s CLI reference, Trivy’s reporting docs and the OSV-Scanner README (v2). Pin versions in CI.
The common data lake#
Seven tools produce seven reports. Without one place to merge them, nobody can answer “what is our riskiest open issue?” The “data lake” does not need to be large. It needs three things:
- One schema for findings. SARIF for code issues and CycloneDX SBOMs for components cover most tools. DefectDojo imports scanner output and deduplicates it; Dependency-Track keeps watching every SBOM you upload, so a CVE published tonight shows up against a release you shipped last month.
- Context that scanners do not have. Is the service internet-facing? Who owns it? Is the vulnerable function reachable? This comes from your service catalogue, not from the scanner.
- Threat data, refreshed daily. The KEV catalog (exploited now) and EPSS, FIRST’s daily probability that a CVE will be exploited in the next 30 days.
The triage step from the loop is a small script, now its own project:
samarthya/ssdlc-triage. It needs Python 3.12 and nothing else
(standard library only). It reads CVE IDs from the command line, a file or a scanner’s output, fetches the two
public feeds, and can fail a CI job with --fail-on T0. The README covers piping in OSV-Scanner and Trivy
output and running it on an air-gapped GHES runner.
The part that matters is one function. Everything else is plumbing:
def tier(in_kev: bool, epss: float, exposed: bool, threshold: float = 0.10) -> tuple[str, Days]:
"""Loosely modelled on CISA BOD 26-04: exploited and reachable comes first. First match wins."""
likely = epss >= threshold
if in_kev and exposed:
return "T0", 3 # known exploited and reachable from the internet: drop everything
if in_kev or (exposed and likely):
return "T1", 14 # exploited somewhere (but not reachable), or likely and reachable
if likely or exposed:
return "T2", 30 # one risk factor, not both
return "T3", None # none of the above: fix with the next planned upgradeRun against today’s feeds, the same CVE lands in different tiers depending on where it runs:
$ python3.12 triage.py --exposed CVE-2021-44228 CVE-2022-3602
CVE-2021-44228 kev=True epss=1.000 T0 fix within 3 days
CVE-2022-3602 kev=False epss=0.908 T1 fix within 14 days
$ python3.12 triage.py CVE-2021-44228 CVE-2022-3602
CVE-2021-44228 kev=True epss=1.000 T1 fix within 14 days
CVE-2022-3602 kev=False epss=0.908 T2 fix within 30 daysThe tier() function is the part to argue about with your team. It is policy written as code, and that is the
point: anyone can read it and anyone can challenge it. As a picture:
flowchart LR
F([New finding]) --> K{In CISA KEV?}
K -- yes --> E1{Internet-exposed?}
E1 -- yes --> T0["T0: fix in 3 days"]:::t0
E1 -- no --> T1a["T1: fix in 14 days"]:::t1
K -- no --> E2{Internet-exposed?}
E2 -- yes --> P1{EPSS at least 0.10?}
P1 -- yes --> T1b["T1: fix in 14 days"]:::t1
P1 -- no --> T2a["T2: fix in 30 days"]:::t2
E2 -- no --> P2{EPSS at least 0.10?}
P2 -- yes --> T2b["T2: fix in 30 days"]:::t2
P2 -- no --> T3["T3: next planned upgrade"]:::t3
classDef t0 fill:#ef4444,stroke:#7f1d1d,color:#fff
classDef t1 fill:#ea580c,stroke:#7c2d12,color:#fff
classDef t2 fill:#ca8a04,stroke:#713f12,color:#fff
classDef t3 fill:#64748b,stroke:#1e293b,color:#fff
The LLM in the loop, and the human who stays#
LLMs fit in two places: explaining a finding in the developer’s context (“this query is built from
request.args, here is the parameterised version”), and drafting the fix. What they must not do is decide on
their own that a fix is correct.
Google DeepMind’s CodeMender shows the pattern I would copy. The agent checks its own patch with static analysis, fuzzing and a critique step, surfaces only patches that pass, and “all patches generated by CodeMender are reviewed by human researchers before they’re submitted upstream”. It upstreamed 72 fixes in six months that way.
I would split work into three lanes:
| Lane | Examples | Who writes the fix | Who merges |
|---|---|---|---|
| Trivial | Patch or minor dependency bump with upgrade guidance; lint autofix; base-image refresh | Bot (Renovate-style or scanner fix PR) | Human, after CI passes |
| Assisted | Parameterise a query; add output encoding; tighten a config | LLM drafts a draft PR with explanation and tests | Code owner |
| Human | Authentication, authorisation, crypto, data model, major version upgrades | Developer, LLM as a pair | Code owner + security review |
The rules that keep the human in the loop (HITL) meaningful:
- The bot never has write access to
main. Branch protection applies to bot PRs exactly as to human ones. - Tests prove, the LLM proposes. A fix PR without a failing-then-passing test, or at least a clean rescan, is a draft, not a fix.
- Rescan before closing. The finding closes when the scanner stops reporting it, not when the PR merges.
- Choose the model by the data. Proprietary code and findings are sensitive. A local open-weight model (see my LangChain and Ollama demo) keeps them in-house; a hosted model may be stronger. Decide per lane, not once for everything.
I covered how to raise the PRs themselves on a self-hosted GitHub in From finding to fix.
Still delivering#
Security work that has no capacity reserved for it becomes overtime, and then it stops. Three things help:
- A standing budget. A fixed share of each sprint goes to the security queue, the same way on-call does. The exact share is a team decision; the important part is that it is planned, not borrowed.
- Measure the flow, not the pile. Track time to fix per tier, the share of bot PRs merged within a week, and the age of the oldest T0/T1 item. The total count of findings mostly measures how many scanners you run.
- Treat a missed T0 like an incident. A short, blameless review of why it took longer than three days.
Teaching it#
The hardest part is not the tools. It is making people care without scaring them into paralysis. What I would try:
- Show, don’t lecture. Take a CVE from your own dependency tree, in a sandbox, and walk through how its public description leads to an exploit. Ten minutes of that beats an hour of slides.
- Teach triage, not fear. Explain KEV, EPSS and exposure so developers understand why this finding is due in three days and that one can wait. Clear rules reduce arguments.
- Security champions. One person per team with a little extra training and time, as SAMM’s Education & Guidance practice suggests. They review bot PRs first and bring patterns back to the team.
- Make the secure path the easy path. Templates with the scans already wired in, base images that are already patched. People follow defaults.
- Share near misses. A short note when a bot PR caught something real builds more trust than any metric.
What I am still unsure about#
- How much to trust an LLM fix in the assisted lane. CodeMender and AIxCC used heavy validation (fuzzing, differential testing). Most teams have unit tests. I do not yet know where the bar should sit.
- Reachability. Knowing whether vulnerable code is actually called would cut noise sharply. Open-source options for this are still uneven across languages.
- My tier thresholds. EPSS 0.10 as a cut-off is a starting guess. The right value comes from watching your own data for a quarter.
Takeaways#
- The scale changed: about 132 CVEs a day, nearly a third of exploited ones attacked on or before publication day, and LLM agents that can turn a CVE description into an exploit.
- SSDLC means security in every stage and daily rescans of shipped software; the nightly SBOM rescan matters most.
- Timebox by risk, not severity: exploited + exposed gets days, most of the rest can wait for the next upgrade. BOD 26-04 is a usable model even outside government.
- Put every scanner’s output into one store with KEV, EPSS and ownership data, and write the triage policy as code.
- Let bots and LLMs write the trivial and assisted fixes as PRs; keep humans as the ones who merge, and close findings only after a rescan.
References#
- samarthya/ssdlc-triage: the triage script from this post, with README, tests and CI examples (Python 3.12, MIT)
- BOD 26-04: Prioritizing Security Updates Based on Risk: CISA, the four criteria, three-day timeline, AI statement, scope
- Patch Smarter, Not Harder: CISA, rationale and the 1% / 60% figures
- Known Exploited Vulnerabilities Catalog: CISA, KEV catalogue and JSON feed
- BOD 26-04 FAQ: Tenable, summary of the directive’s timeline tiers (secondary)
- Secure Software Development Framework: NIST, SSDF 1.1 practice groups
- SP 800-218 Rev. 1 (initial public draft): NIST, SSDF version 1.2 status
- OWASP SAMM model: OWASP, business functions and practices
- OWASP Top 10:2025: OWASP, the A01–A10 categories used as vector labels
- MITRE ATT&CK: T1552.001 Credentials In Files, T1195 Supply Chain Compromise and T1190 Exploit Public-Facing Application: MITRE, technique IDs used as vector labels
- SLSA build track basics (v1.2): OpenSSF, build levels
- Exploit Prediction Scoring System and EPSS API: FIRST, definition and API used by the script
- LLM Agents can Autonomously Exploit One-day Vulnerabilities: Fang, Bindu, Gupta, Kang (arXiv, 2024), the 87% / 7% result
- AIxCC results: DARPA, automated find and patch rates
- Introducing CodeMender: Google DeepMind, validated AI patches with human review
- State of Exploitation 2026: VulnCheck, 2025 exploitation figures
- 2025 CVE Data Review: Jerry Gamblin, 2025 CVE count and severity split
- Gitleaks, Semgrep and Semgrep CLI reference, OSV-Scanner, Trivy and Trivy reporting, ZAP baseline scan, Dependency-Track, DefectDojo: project pages, licences and commands
Related on this site: From finding to fix, DAST and the AI in practice series.



