↓ Skip to main content
AI & Data

SSDLC when exploits are cheap: a working concept

A fire inspection once a year works when fires are rare. When sparks are falling every day, you need smoke detectors in every room, a sprinkler that fixes the small ones by itself, and a person who decides what to do about the big ones. That is what a secure development life cycle has to become.

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.

Four comic panels: a young man walks through an open gate into a yard, crawls through a hole cut in a chain-link fence, slides under a half-closed roll-up door, and only in the last panel padlocks the gate behind him
Locking the gate at the end: by the time the padlock goes on, the open gate, the cut fence and the gap under the door have all been used.

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 SDLCSSDLC as I would run it
When security happensGate before releaseEvery commit, every merge, every night, in production
Unit of outputReportPull request or ticket with an owner and a due date
PriorityCVSS severityExploited? Reachable? Likely? (KEV, exposure, EPSS)
Who fixes trivial issuesA developer, eventuallyA bot opens the PR; a developer reviews it
Shipped softwareDoneRescanned 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.

StageTriggerWhat runsTimeboxBlocks?
1. Commitgit commitSecrets scan (pre-commit hook)SecondsYes, locally
2. Pull requestPR opened or updatedSAST on the diff, dependency diff, IaC checks, unit testsUnder 10 minutesNew criticals and secrets only
3. Merge to mainPushFull SAST, SCA, container scan, SBOM, signed buildUnder 30 minutesNo; findings go to the store
4. NightlyScheduleRescan released SBOMs against new CVEs; DAST baseline on stagingOvernightNo
5. TriageNew finding in the storeEnrich with KEV, EPSS, exposure; assign tier and ownerSame dayn/a
6. FixTier due dateBot PR or human fixT0: 3 days · T1: 14 · T2: 30 · T3: next upgraden/a
7. VerifyFix mergedRescan; close the finding only when the scanner agreesNext pipeline runn/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
VectorIDCaught atWhy there
Credentials committed to the repositoryATT&CK T1552.001 Credentials In FilesLevel 1Once pushed, a secret is in history for good; block it before it leaves the laptop
Injection in new codeOWASP A05:2025 InjectionLevel 2Cheapest to fix while the author still has the context
Insecure configuration (IaC, containers)OWASP A02:2025 Security MisconfigurationLevel 2IaC diffs are small and reviewable in the PR
Vulnerable or malicious dependency addedOWASP A03:2025 Software Supply Chain Failures; ATT&CK T1195.001Level 2The lockfile diff shows exactly what is new
Vulnerable OS packages in the imageOWASP A03:2025Level 3Only exists after the image is built
Build tampered with between source and artefactOWASP A08:2025 Software or Data Integrity Failures; ATT&CK T1195.002Level 3Signed provenance (SLSA) proves what was built from what
New CVE in software already runningATT&CK T1190 Exploit Public-Facing ApplicationLevel 4The 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.

JobToolLicence
SecretsGitleaksMIT
SAST (your code)Semgrep Community EditionLGPL-2.1
SCA (dependencies)OSV-ScannerApache-2.0
Containers, IaC, SBOMTrivyApache-2.0
DAST (running app)ZAPApache-2.0
SBOM monitoringDependency-TrackApache-2.0
Findings storeDefectDojoBSD-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.com

The 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:

  1. 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.
  2. 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.
  3. 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 upgrade

Run 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 days

The 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:

LaneExamplesWho writes the fixWho merges
TrivialPatch or minor dependency bump with upgrade guidance; lint autofix; base-image refreshBot (Renovate-style or scanner fix PR)Human, after CI passes
AssistedParameterise a query; add output encoding; tighten a configLLM drafts a draft PR with explanation and testsCode owner
HumanAuthentication, authorisation, crypto, data model, major version upgradesDeveloper, LLM as a pairCode 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:

  1. 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.
  2. 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.
  3. 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.
  4. Make the secure path the easy path. Templates with the scans already wired in, base images that are already patched. People follow defaults.
  5. 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
#

Related on this site: From finding to fix, DAST and the AI in practice series.

Author
Samarthya
Engineer, learner, tinkerer. This is my trophy shelf of everyday work and leisure.

Related

From finding to fix: Black Duck pull requests on GitHub Enterprise Server

A vulnerability report that nobody reads is a smoke alarm with no battery. The goal is not more findings. The goal is a pull request that a developer can review and merge before lunch. Why this post # SentinelOne’s overview of GitHub vulnerability management describes the job as “discovering, triaging, and remediating security vulnerabilities in GitHub repositories, dependencies, and workflows”. It lists the usual native tools: Dependabot, code scanning, secret scanning and security advisories. It also admits the gaps: no asset inventory or risk scoring, and dependencies that Dependabot cannot track.

Security standards, plainly, part 4: CISA KEV and BOD 26-04

A hospital emergency room does not treat patients in the order they arrive, or by how loudly they shout. A triage nurse asks a few quick questions and sends the bleeding patient in first. CISA’s KEV catalog and directive BOD 26-04 are triage for vulnerabilities. The first three parts of this series, SSDF, SAMM and SLSA, share one gap: none of them says how fast a vulnerability must be fixed. This part is about the clock.

Security standards, plainly, part 3: SLSA v1.2

A sealed medicine bottle tells you two things: who made it, and that nobody has opened it since. You do not need to test the pills yourself. SLSA gives software the same seal and the same label. Part 1 (SSDF) listed what a secure process should do. Part 2 (SAMM) measured how well you do it. Both mostly look at the code. SLSA looks at the journey from code to the thing you ship, because attackers have learned that it is often easier to tamper with a build than to find a bug.