---
title: "DevSecOps"
slug: devsecops
category: security
term_status: established
evidence_status: verified
summary: "DevSecOps is the practice of building security into the software delivery pipeline and into the teams that run it, so that every change is checked, traceable and safe to ship without a separate security phase that stalls delivery."
audience: [Developers, AppSec engineers, CI/CD and platform owners, Security champions, Architects, Engineering leaders]
last_verified: 2026-10-17
key_takeaways:
  - "DevSecOps makes security a property of the pipeline: put each control where its finding is cheapest to fix and its false positives cost least."
  - "Block a merge only on high-confidence findings the author can fix now; send everything else to tracked work with an owner and a due date."
  - "The pipeline is an attack surface: pin third-party steps, give each job the narrowest identity, and keep deploy credentials in branch-limited environments, out of reach of scanners and pull requests."
  - "Provenance answers what is running and how it was built: SBOMs, signatures and SLSA (Supply-chain Levels for Software Artifacts) attestations, verified by the deploy job itself."
  - "DORA's 2022 survey found culture, not tooling, was the biggest predictor of security-practice adoption."
  - "NIST's SSDF lists the outcomes; SLSA, OWASP SAMM and the OWASP CI/CD Top 10 supply detail. None of them certifies 'DevSecOps'."
related: [devops, vulnerabilityops, kubernetessecops, cloudsecops, complianceops, secops, containerops, releaseops]
---

## What DevSecOps is

### In 30 seconds

**DevSecOps is the practice of building security into the software delivery pipeline and into the teams that run it.** Security
was often one review near release, by a separate team; with many small changes a day, that review became a bottleneck or a rubber
stamp. DevSecOps replaces it with small, automated, proportionate controls between the editor and production, each placed where its
finding is cheapest to act on, plus supply-chain evidence (bills of materials, signatures, build provenance) that proves what
shipped. Developers own most of their code's security; specialists set policy and coach. It is judged by outcomes: fewer exploitable
defects reach production, those that do are fixed fast, and delivery does not slow down.

There is no single normative definition: NIST's SP 800-204C expands the acronym ("Development, Security, and Operations") and calls
DevSecOps "a facilitating paradigm" built on CI/CD pipelines.[^nist-204c] The definition above is this Atlas's. This page is for
developers, application-security (AppSec) engineers, platform owners, architects and engineering leaders. Last verified 2026-10-17.

### At a glance

| Aspect | Summary |
|---|---|
| Goal | Ship changes that are secure enough for their risk, with evidence, at delivery speed |
| Main users | Developers, AppSec engineers, CI/CD and platform owners, security champions, release approvers |
| In scope | Threat modelling of changes; secret, SAST, SCA, IaC and image scanning; DAST and API tests; policy as code; SBOMs, signing, provenance; hardening CI/CD itself; champions; gates and exceptions |
| Out of scope (linked) | Runtime detection and response ([SOC Ops](../soc-ops/), [SecOps](../secops/)); estate-wide vulnerability prioritisation ([VulnerabilityOps](../vulnerabilityops/)); admission and runtime controls in clusters ([KubernetesSecOps](../kubernetessecops/)); cloud posture ([CloudSecOps](../cloudsecops/)); audit evidence programmes ([ComplianceOps](../complianceops/)) |
| Prerequisites | [DevOps](../devops/) basics: version control, CI/CD, trunk-based development |
| Key outputs | A pipeline with placed controls, a gate policy, an exceptions register, SBOMs and attestations per release, security metrics |
| Term status | **Established**: in the titles of NIST publications,[^nist-204c][^nist-204d] a NIST NCCoE project,[^nccoe-1800-44] DoD guidance[^dod-execgov] and OWASP projects;[^owasp-dsg][^owasp-dsomm] no single normative definition |

### The problem it solves

**What fails without it.** Security that lives only at the end of delivery fails in three predictable ways:

1. **Late findings get waived.** A pre-launch penetration test finds an injection flaw written six weeks earlier; the author has
   moved on, the launch date wins, and the finding becomes an accepted risk nobody revisits.
2. **Volume overwhelms reviewers.** With dozens of deployments a day, a manual sign-off queues work for days or becomes a
   click-through.
3. **Nobody can answer "are we affected?"** When a widely used component is found to be compromised, as XZ Utils versions 5.6.0 and
   5.6.1 were in March 2024,[^cisa-xz] teams without an inventory of what is inside each deployed artefact have to inspect
   images one by one.

A fourth failure is newer: **the delivery system itself becomes the target.** In 2025 a widely used GitHub Action exposed the
secrets of workflows that ran it; in 2026 a *security scanner's* official GitHub Actions were hijacked
([use cases](#use-cases-and-scenarios)). CI/CD holds production credentials.

**Before and after (illustrative).**

| | Before: end-of-line security | After: DevSecOps |
|---|---|---|
| When issues are found | Pre-release review or pen test | In the editor, pull request and build, minutes after writing |
| Who fixes them | A ticket for whoever is free | The author, in the same change, with a suggested fix |
| What blocks a release | Whatever the reviewer decides that week | A written gate policy |
| Third-party code | Unknown; searched for during incidents | SBOM per release; dependency updates continuous |
| Proof of what shipped | Release notes | Signed artefacts with build provenance, verified before deploy |
| Pipeline credentials | Long-lived keys in CI variables | Short-lived identities per job and environment |
| Security team role | Gatekeeper | Policy owner, coach, incident partner |

**Measured value.** The best independent evidence is survey-based. DORA's 2022 report found that teams with low levels of security
practices had 1.4 times greater odds of high burnout, and that SLSA-related practices predicted better organisational and delivery
performance, an effect that "needs strong continuous integration capabilities in place to fully emerge".[^dora-2022] These are
survey correlations: direction, not a business case.

### What DevSecOps is not

- **Not a tool purchase.** A "DevSecOps platform" is a vendor's product category. Scanners without a gate policy, an exceptions
  process and owners for findings produce dashboards of unread results.
- **Not "the security team does DevOps".** Delivery teams own security outcomes for their services; specialists set policy and
  build paved roads.
- **Not shift-left only.** Moving checks earlier is half the idea; the other half is evidence that survives to production
  (provenance, SBOMs, signatures) and feedback from production into the pipeline.
- **Not runtime security operations.** Attacks on running systems belong to [SOC Ops](../soc-ops/),
  [ResponseOps](../responseops/) and [SecOps](../secops/); DevSecOps owns security before and at deployment, and pipeline compromise.
- **Not a compliance programme.** Pipeline controls produce audit evidence; deciding which obligations apply and managing auditors
  is [ComplianceOps](../complianceops/).
- **Not "block everything above medium".** A gate that fails builds on low-confidence findings teaches developers to bypass gates.
  Proportionality is the core skill this page teaches.

**Naming confusion.** "DevSecOps", "SecDevOps" and "DevOpsSec" are used interchangeably; the word order carries no agreed meaning.
"Rugged DevOps" echoes the Rugged Software initiative (started 2010).[^rugged] "DevSecOps engineer" is a job title with no standard
scope ([DevOps](../devops/) discusses such titles).

## Where it comes from and where it sits

### Origins and evolution

The idea predates the word. Secure development lifecycles such as Microsoft's SDL, now described by Microsoft as its approach "to
integrate security into DevOps processes (sometimes called a DevSecOps approach)",[^ms-sdl] put security activities into each phase;
DevOps made phases minutes long, so activities became automated checks. Milestones that can be traced to sources:

- **2010:** the Rugged Software initiative (Joshua Corman, David Rice, Jeff Williams) asks developers to own their code's
  resilience; a 2012 Rugged Handbook group included Gene Kim.[^rugged]
- **2012:** Gartner published a report titled *DevOpsSec: Creating the Agile Triangle*, as reported at the time.[^siliconangle-2012]
- **2022:** NIST published the Secure Software Development Framework (SSDF) v1.1 as SP 800-218 (February), mapped to US Executive
  Order 14028,[^ssdf][^ssdf-project] and SP 800-204C (March) on DevSecOps for microservices.[^nist-204c] DORA made software
  supply-chain security a primary theme of its 2022 report.[^dora-2022]
- **2023–2024:** SLSA v1.0, its first stable version, appeared on 19 April 2023.[^slsa-v1] NIST SP 800-204D (February 2024)
  covered supply-chain security in CI/CD pipelines.[^nist-204d] The US DoD updated its *DoD Enterprise DevSecOps
  Fundamentals* in October 2024 (version 2.5 according to the file name of the linked PDF).[^dod-execgov]
- **2024–2026:** regulation arrived, and some was withdrawn. The EU Cyber Resilience Act entered into force in December 2024, with
  obligations staged to December 2027;[^eu-cra] in January 2026 the US OMB rescinded M-22-18, the memo behind the federal software
  attestation form.[^omb-m2605] The OWASP Top 10:2025 widened its supply-chain category, A03, to "all supply chain
  failures";[^owasp-a03] SLSA v1.2 added a Source track;[^slsa-whatsnew] CISA and 17 co-authoring organisations replaced the 2021
  SBOM minimum elements in July 2026.[^sbom-2026]

### Glossary and mental model

**The mental model: placed controls plus a chain of evidence.** Picture the path from editor to production as a conveyor lined with
*controls*. Each has a **position** (how early it runs), a **confidence** (how often its findings are real *to the developer*), a
**cost** (minutes and triage hours) and an **action** (block, warn or record). Alongside runs a *chain of evidence*: each artefact
carries its bill of materials, signature and provenance. DevSecOps design is choosing position, confidence threshold and action per
control, and keeping the chain unbroken.

| Term | Meaning | Example |
|---|---|---|
| Shift left | Run a check earlier, where the fix is cheaper | Pre-commit secret scanning, not a quarterly audit |
| SAST | Static application security testing: analyses source without running it | A rule finds user input flowing into a SQL string |
| SCA | Software composition analysis: inventories third-party components and matches them to known vulnerabilities | A dependency at a version listed in a CVE |
| DAST | Dynamic testing of a running application from the outside | A baseline scan of a test deployment for missing security headers |
| IaC scanning / policy as code | Checking infrastructure and deployment definitions against written rules | A rule rejects an unencrypted storage bucket |
| SBOM | Software bill of materials: "a nested inventory, a list of ingredients that make up software components"[^cisa-sbom] | A CycloneDX or SPDX file per image |
| Provenance, attestation | A signed statement (attestation) of how and from what an artefact was built; attestations can also carry SBOMs | An in-toto statement naming the builder and source commit[^slsa-prov][^in-toto] |
| SLSA | Supply-chain Levels for Software Artifacts ("salsa"): a specification with a Build track (L0–L3) and a Source track (L1–L4)[^slsa-home][^slsa-spec] | "Build L2": provenance signed by a hosted build platform |
| OIDC, workload identity | OpenID Connect: a CI job proves where it runs (repository, branch, environment) with a short-lived signed token and exchanges it for temporary cloud credentials[^gh-oidc-concept] | A deploy job assumes a cloud role with no stored key |
| Transparency log | A public log of signing events, so they can be audited; Sigstore's is Rekor[^sigstore-sign] | Anyone can see that your workflow signed an image |
| VEX | Vulnerability Exploitability eXchange: states whether a vulnerable component is exploitable in the product that uses it[^cdx-vex] | "Not affected: the vulnerable function is never called" |
| KEV, EPSS | CISA's catalogue of vulnerabilities known to be exploited;[^kev] FIRST's estimated probability that a CVE is exploited in the next 30 days[^epss] | Inputs for prioritising known vulnerabilities |
| Effective false positive | A finding the developer chooses not to act on, whatever the tool author thinks[^tricorder] | A "SQL injection" in a test fixture nobody will fix |
| Baseline | The set of findings that already existed before a change | Old findings tracked as backlog rather than blocking new work |
| Security champion | A developer with extra security skills and time, often the team's first point of contact for security[^devguide-champions] | Reviews threat models for their squad |

### How DevSecOps relates to other Ops

DevSecOps sits on top of [DevOps](../devops/), which owns the delivery system, CI/CD and the DORA delivery metrics, and feeds several
security pages. The boundaries below are **editorial distinctions** chosen by this Atlas; no standard defines them.

::: figure Where DevSecOps sits among neighbouring Atlas pages
```mermaid
flowchart LR
  DEV["DevOps"]:::neutral
  CON["ContainerOps"]:::neutral
  DSO["DevSecOps"]:::core
  VUL["VulnerabilityOps"]:::neutral
  KSO["KubernetesSecOps"]:::neutral
  CSO["CloudSecOps"]:::neutral
  CMP["ComplianceOps"]:::neutral
  SOC["SOC Ops, SecOps"]:::neutral
  DEV -->|"pipelines to secure"| DSO
  CON -->|"images to scan, sign"| DSO
  DSO -->|"vulnerable components that shipped"| VUL
  DSO -->|"signatures, SBOMs, provenance to verify"| KSO
  DSO -->|"IaC rules shared with posture checks"| CSO
  DSO -->|"control evidence"| CMP
  DSO -->|"pipeline-compromise incidents"| SOC
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
```
Text equivalent: DevOps hands DevSecOps pipelines to secure; ContainerOps, images to scan and sign. DevSecOps hands VulnerabilityOps
the vulnerable components that shipped; KubernetesSecOps signatures, SBOMs and provenance to verify; CloudSecOps the IaC rules it
reuses on deployed resources (posture is CloudSecOps's); ComplianceOps control evidence; SOC Ops and SecOps pipeline-compromise
incidents.
:::

| Neighbouring page | Shared scope | DevSecOps focus | The neighbour's focus |
|---|---|---|---|
| [DevOps](../devops/) | CI/CD, automation, culture | Which security controls sit in the pipeline, and how they gate | The delivery system and its metrics |
| [VulnerabilityOps](../vulnerabilityops/) | SCA findings, CVSS, KEV, EPSS | Preventing *new* vulnerable code | Prioritising every vulnerability across the estate to a verified fix |
| [KubernetesSecOps](../kubernetessecops/) | Image signatures, policies | Producing signatures, SBOMs and provenance at build time | Verifying them at admission; runtime controls |
| [ContainerOps](../containerops/) | Images | Scanning, signing and attesting images | Building small, reproducible images |
| [CloudSecOps](../cloudsecops/) | IaC misconfigurations | Catching them in the pull request | The deployed cloud control plane and drift |
| [ComplianceOps](../complianceops/) | Evidence | Control evidence as a by-product | Mapping evidence to frameworks and auditors |
| [SecOps](../secops/), [SOC Ops](../soc-ops/) | Security incidents | Pipeline compromise and secret leaks | Runtime detection and response |
| [ReleaseOps](../releaseops/), [GitLabOps](../gitlabops/) | Release decisions; scanners in one product | Security inputs; which findings block | The release decision; configuring GitLab's scanners |

## Principles and operating model

### Published frameworks and this Atlas's charter

Two kinds of published material exist; neither is "the official DevSecOps standard":

- **Outcome frameworks.** NIST's SSDF v1.1 has four practice groups: Prepare the Organization (PO), Protect the Software (PS),
  Produce Well-Secured Software (PW) and Respond to Vulnerabilities (RV). "The SSDF does not prescribe how to implement each
  practice. The focus is on the outcomes".[^ssdf-pdf] Version 1.2 is a draft (as of October 2026).[^ssdf-r1][^ssdf-pubs]
- **Community manifestos.** DevSecOps.org values "Data & Security Science" over fear, uncertainty and doubt;[^devsecops-org] the
  Threat Modeling Manifesto values "a culture of finding and fixing design issues" over checkbox compliance.[^tm-manifesto]

**This Atlas's proposed charter** (a proposal, not an industry standard):

> **Mission:** every change that reaches production is as secure as its risk requires, and we can prove what it contains and how it
> was built, without making delivery slower than the risk justifies.
>
> **Principles:**
> 1. *Place, don't pile.* Each control runs at the earliest point where its finding is actionable by the person who can fix it.
> 2. *Block on confidence, not on fear.* A gate may stop work only when its findings are usually real, severe and fixable now.
> 3. *Every finding has an owner and a clock.* Fixed, excepted with an expiry, or tuned away; never silently ignored.
> 4. *The pipeline is production.* CI/CD identities, runners and third-party steps get production-grade controls.
> 5. *Evidence is a by-product.* SBOMs, signatures and provenance are generated by the pipeline, not assembled for auditors.
> 6. *Security is a team skill.* Delivery teams own their services' security; specialists coach, set policy and build paved roads.
>
> **Non-goals:** zero findings; scanning everything with every tool; replacing design review with scanners; owning runtime detection.

**Invariants and trade-offs.** Three things must stay true: artefacts are immutable and identified by digest; nobody, human or job,
holds a production credential it does not need; every exception expires. Trade-offs: earlier checks see less context (a pre-commit
scan cannot know whether a library is reachable), stricter gates cost time and trust, richer evidence costs storage.

### Fundamentals and building blocks

DevSecOps controls fall into seven families. The last column names the SSDF practice each family supports.[^ssdf-pdf]

| Family | What it checks | Typical position | SSDF outcome |
|---|---|---|---|
| Design and threat modelling | Data flows, trust boundaries, abuse cases | Start of a change | PW.1 design to meet security requirements |
| Code analysis (SAST, secret scanning) | Source code and history | Editor, pre-commit, pull request | PW.7 review and analyse human-readable code |
| Composition (SCA, licences, SBOM) | Third-party components | Pull request (diff), build (full), continuous | PW.4 reuse well-secured software; PS.3 archive releases |
| Configuration (IaC, Dockerfile, Kubernetes manifests) | Infrastructure and deployment files | Pull request, plan time | PW.7, as configuration-as-code is code (PS.1.1) |
| Dynamic testing (DAST, API tests, fuzzing) | A running build in a test environment | After deploy to test | PW.8 test executable code |
| Integrity (signing, provenance, attestations) | Artefacts and their build | Build and deploy | PS.1 protect code; PS.2 integrity verification |
| Platform hardening (CI/CD, identities, runners) | The pipeline itself | Continuous | PO.5 secure development environments |

Two cross-cutting blocks hold the families together: a **findings store** that de-duplicates results across tools (SARIF is the
OASIS standard format for static-analysis results[^sarif]) and a **gate policy** saying which results block, warn or are recorded.

### Roles, ownership and RACI

| Activity | Developer | Security champion | AppSec team | Platform / CI owner | Engineering manager |
|---|---|---|---|---|---|
| Threat model for a significant change | R | A | C | C | I |
| Fix findings in own change | A/R | C | C | I | I |
| Gate policy (what blocks) | C | C | A/R | C | C |
| Scanner rules and tuning | I | C | A/R | R | I |
| Pipeline identities, runners, third-party steps | I | I | C | A/R | I |
| Exceptions (approve, track expiry) | R (requests) | C | A (above a risk threshold) | I | A (below threshold) |
| SBOM, signing and provenance | I | I | C | A/R | I |
| Pipeline compromise response | C | C | R | A/R | I |

R = responsible, A = accountable, C = consulted, I = informed. This RACI is an Atlas proposal; adapt the exception threshold to your
risk appetite, which [RiskOps](../riskops/) owns. In a pipeline compromise the [IncidentOps](../incidentops/) lead coordinates and
[ResponseOps](../responseops/) takes over if attacker activity is found ([worked runbook](#worked-runbook-a-compromised-pipeline-dependency)).

**Champions.** OWASP's Security Champions Manifesto lists ten principles, including "Nominate a dedicated captain", "Reward
responsibility" and "Anticipate personnel changes";[^champions] OWASP's Developer Guide says champions "can be the first point of
contact for information security teams".[^devguide-champions] Neither sets a time budget; this Atlas suggests 10–20%, protected in planning.

**Team topology.** What scales is an AppSec *enabling* team (policy, rules, coaching) plus a platform team that ships security in the
paved road ([PlatformOps](../platformops/)), not a central team triaging every finding.

### Lifecycle: Day 0, Day 1, Day 2

- **Day 0, design.** Agree the gate policy and exception rules; choose a findings format (SARIF) and store; decide the identity
  model for CI/CD jobs; threat-model the pipeline itself (Lab 1).
- **Day 1, rollout.** Run controls *report-only* on a pilot service; measure effective false positives and added minutes; promote
  only rules that pass the bar; generate SBOMs and provenance from day one, because evidence is cheap to start and hard to backfill.
- **Day 2, operate and improve.** Review metrics monthly; retire noisy rules; renew or close exceptions; narrow pipeline identities;
  rehearse the pipeline-compromise runbook; feed incidents back into threat models and rules (SSDF RV.3: "Analyze Vulnerabilities to
  Identify Their Root Causes").[^ssdf-pdf]

### Operating model and governance

Governance is a small set of written artefacts, versioned in Git and changed by pull request like code:

1. **Gate policy** (machine-readable): severities and tags that block, warn or record; whether only *new* findings block.
2. **Exceptions register**: one entry per excepted finding with owner, reason, ticket, grant date and expiry (maximum 90 days in this
   Atlas's template; renewal needs a new decision).
3. **Pipeline security baseline**: allowed third-party steps, pinning, token permissions, runner types per trust level.
4. **Rule catalogue**: rules enabled at each position, with measured effective false-positive rates.

Cadence (Atlas proposal): champions fortnightly; metrics and exceptions monthly; pipeline baseline, identities and a one-page
leadership scorecard quarterly ([Templates](#templates)). Exceptions above the risk threshold escalate to AppSec; disputes about risk
acceptance go to the system owner ([RiskOps](../riskops/) owns appetite).

::: figure The life of a security finding
```mermaid
stateDiagram-v2
  [*] --> New: scanner reports
  New --> Triaged: owner assigned
  Triaged --> Fixing: real and fixable
  Triaged --> Excepted: real, not fixable now
  Triaged --> Dismissed: effective false positive
  Fixing --> Verified: rescan shows it gone
  Excepted --> Triaged: expiry date reached
  Dismissed --> RuleTuned: pattern repeats
  Verified --> [*]
  RuleTuned --> [*]
```
Text equivalent: a scanner reports a new finding, which is triaged and given an owner. A real, fixable finding moves to fixing and,
once a rescan shows it gone, to verified and closed. A real finding that cannot be fixed now becomes an exception, which returns to
triage when its expiry date is reached. A finding the developer will not act on is dismissed as an effective false positive; when the
same pattern repeats, the rule is tuned.
:::

## Architecture and workflow

### Reference architecture: where each control sits

The reference architecture is a sequence of *positions*, each with the controls actionable there. The design question for every
control: who sees the result, how soon, and can they fix it? The pattern is vendor-neutral; the
[reference implementation](#reference-implementation) shows it on one CI system.

::: figure Where each control sits between the editor and runtime
```mermaid
flowchart TB
  P1["1. Editor and pre-commit: secret scan, fast lint rules"]:::core
  P2["2. Pull request: SAST on changed lines, dependency diff, IaC policy, review"]:::core
  P3["3. Build on main: full SCA, image scan, SBOM, sign, provenance"]:::core
  P4["4. Test environment: DAST baseline, API tests"]:::core
  P5["5. Deploy: verify signature and provenance, policy check"]:::decision
  P6["6. Runtime: admission, monitoring, response"]:::neutral
  FB["Feedback: incidents and new CVEs"]:::risk
  P1 -->|"commits"| P2
  P2 -->|"reviewed merge"| P3
  P3 -->|"digest, SBOM, attestations"| P4
  P4 -->|"tested release candidate"| P5
  P5 -->|"verified digest only"| P6
  P6 --> FB
  FB -->|"new rules and tickets"| P2
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
```
Text equivalent: six positions from top to bottom, each listed with its controls in the table below: editor and pre-commit, pull
request, build on main, test environment, deploy, runtime. Between them pass commits, a reviewed merge, an image digest with its
SBOM and attestations, a tested release candidate and, into runtime, only a verified digest. Incidents and new CVEs feed back into
the pull-request stage as new rules and tickets.
:::

| Position | Controls | Typical action | Why here |
|---|---|---|---|
| 1. Editor, pre-commit | Secret scan, fast SAST rules | Block the commit locally (bypassable) | A secret never pushed never needs rotating |
| 2. Pull request | SAST on changed lines, SCA diff, IaC and manifest policy, server-side secret scan, review | Block merge on high-confidence, fixable, severe *new* findings | The author still has context; Tricorder showed results at code review, "on changed lines by default"[^tricorder] |
| 3. Build (main) | Full SCA, image scan, licences, SBOM, signing, provenance | Fail on policy; record the rest | Needs the artefact; provenance must come from the build platform[^slsa-req] |
| 4. Test environment | DAST baseline, API security tests, fuzzing | Warn; block release on confirmed severe issues | Needs a running system |
| 5. Deploy | Verify signature, provenance, SBOM presence, policy | Refuse unverified artefacts | Last cheap, deterministic point before production |
| 6. Runtime | Admission, detection | Hand off | [KubernetesSecOps](../kubernetessecops/), [SOC Ops](../soc-ops/) |

**Alternatives and trade-offs.** *Centralised scanning* (a security team scans every repository nightly) starts fast but yields late,
ownerless findings; keep it as a backstop. *Scanners in shared pipeline templates* scale best but need their own versioning.
*All-in-one platforms* cut integration work but make one weak scanner hard to swap.

### Trust boundaries and identities in the pipeline

A pipeline is a set of jobs that run code (yours and third parties') with identities that can change production. The OWASP Top 10
CI/CD Security Risks name the failure modes, from "Inadequate Identity and Access Management" (CICD-SEC-2) to "Poisoned Pipeline
Execution (PPE)" (CICD-SEC-4) and "Insufficient Credential Hygiene" (CICD-SEC-6).[^owasp-cicd] The defensive pattern is to split
jobs by trust level and give each the narrowest identity. Permissions apply to a whole job, so one compromised step gets everything
its job holds.

::: figure Trust zones: which job can reach which credential
```mermaid
flowchart TB
  PR["Zone 1, untrusted input: pull-request checks, scanners, third-party steps"]:::risk
  RES["Findings (SARIF) and gate result"]:::neutral
  BLD["Zone 2, main: build job pushes the image"]:::core
  SCN["Zone 2, main: scan job runs third-party scanners"]:::risk
  ATT["Zone 2, main: attest job runs GitHub-owned actions only"]:::core
  REG["Registry: image and attestations"]:::neutral
  DEP["Zone 3, approved environment: deploy job verifies, then deploys"]:::decision
  CLOUD["Production account"]:::neutral
  PR -->|"read-only token, no secrets"| RES
  BLD -->|"registry write"| REG
  BLD -->|"digest"| SCN
  SCN -->|"SBOM; read-only token"| ATT
  ATT -->|"OIDC signing identity, attestations"| REG
  REG -->|"attestations checked in the job"| DEP
  DEP -->|"OIDC role bound to the environment"| CLOUD
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
```
Text equivalent: in Zone 1 (untrusted input), the pull-request checks job runs scanners and third-party steps with a read-only token
and no secrets, producing findings and a gate result. In Zone 2 (main), build pushes the image; scan runs third-party scanners with
a read-only token and passes the SBOM on; attest, running only GitHub-owned actions, signs with an OIDC identity and pushes
attestations. In Zone 3 (an approved environment), deploy checks the attestations, then assumes a cloud role bound to it.
:::

**Who controls a pull-request job?** A pull request from a fork gets no secrets except a read-only `GITHUB_TOKEN`.[^gh-events] One
from a branch of the *same* repository runs in the context of its merge commit (unlike `pull_request_target`, which runs on the
default branch "rather than in the context of the merge commit"),[^gh-events] so the author's edited workflow file runs and can
reference any secret the repository can reach: "Any user with write access to your repository has read access to all secrets
configured in your repository."[^gh-hardening] Zone 1 holds no secrets only if none exist at repository or organisation level.

Rules that follow (each applied in the [reference implementation](#reference-implementation)):

- **Third-party code never shares a job with a write token, a secret or a signing identity, unless the job cannot work without
  it.** Scanners run in Zone 1 and the read-only scan job; the attest job runs only GitHub-owned actions (Case 2 in
  [use cases](#use-cases-and-scenarios) shows why). The one deliberate exception is deploy's
  `aws-actions/configure-aws-credentials`, from GitHub's AWS guide[^gh-oidc-aws] and pinned to a reviewed SHA; compromised, it
  would reach the production role. The AWS CLI's `assume-role-with-web-identity`, which needs no stored AWS
  credentials,[^aws-awwi] replaces it with a few lines of shell.
- **Deploy credentials live only in environments limited to the protected branch**, and pipeline files need code-owner
  review.[^gh-hardening]
- **Pin every third-party step to an immutable reference.** GitHub says to pin actions to a full commit SHA because a tag "can be
  moved or deleted if a bad actor gains access to the repository storing the action".[^gh-hardening] GitHub's workflow dependency
  lockfiles (`.github/workflows/actions.lock`) also pin transitive action dependencies; as of October 2026 they are "in public preview
  and subject to change".[^gh-lockfiles]
- **Replace stored cloud keys with workload identity.** With OIDC there are no stored cloud secrets to steal;[^gh-oidc-concept] the
  trust policy must name *which* jobs may assume the role.
- **Never run untrusted code with privileged triggers.** GitHub warns that workflows using `pull_request_target` "must not explicitly
  check out untrusted code, including from pull request forks".[^gh-hardening]

### End-to-end workflow

**Trigger:** a pull request. **Actors:** author, reviewer, CI, AppSec (by exception), release approver. **Output:** a deployed,
attested artefact; findings fixed, excepted or tuned. **Feedback:** gate-outcome and escape metrics.

::: figure Happy path from pull request to verified deployment
```mermaid
%%{init: {"sequence": {"width": 100, "actorMargin": 40, "messageMargin": 30, "noteMargin": 6, "diagramMarginX": 6, "wrap": true, "wrapPadding": 6}}}%%
sequenceDiagram
  participant Dev as Developer
  participant Git as Git platform
  participant CI as CI jobs
  participant Reg as Registry
  participant Dep as Deploy job
  Dev->>Git: pull request
  Git->>CI: checks, no secrets
  CI-->>Git: findings, gate
  Dev->>Git: fix, review, merge
  Git->>CI: build, scan, attest
  CI->>Reg: image, attestations
  Note over Dep: approved
  Dep->>Reg: fetch attestations
  Reg-->>Dep: bundles
  Note over Dep: verify identity
  Note over Dep: OIDC role, deploy
```
Text equivalent: the developer opens a pull request; check jobs without secrets return findings and the gate result. The developer
fixes them and, after review, merges. On main, CI builds, scans and attests the image and pushes it with its attestations to the
registry. A reviewer approves the production environment. The deploy job fetches the attestation bundles for the digest and verifies
them itself (signer identity, issuer, predicate type, runner type), then exchanges its OIDC token for the production role and
deploys by digest. The registry stores evidence; it does not decide.
:::

**Exception paths.**

1. *The author believes a blocking finding is wrong:* they record a reason, the champion confirms, and the dismissal counts against
   the rule; three in a month trigger a tuning review.
2. *A real finding cannot be fixed in this change:* an exception with owner, reason, ticket and expiry, in its own pull request.
3. *Verification fails at deploy:* the deploy stops and nobody overrides it; rebuild from the protected branch, and if the signature
   or provenance is unexpected, follow the [worked runbook](#worked-runbook-a-compromised-pipeline-dependency).
4. *An emergency fix:* a recorded break-glass merge with a second approver, never a skipped build or unsigned artefact, reviewed
   within two working days (Atlas proposal).

## Practices, frameworks and tools

### Core practices and playbooks

Six playbooks cover the practices that most often go wrong, followed by a note on API testing.

**Playbook 1: threat-model a change in 45 minutes.** Use the four questions of the Threat Modeling Manifesto: "What are we working
on?", "What can go wrong?", "What are we going to do about it?", "Did we do a good enough job?"[^tm-manifesto]

1. *Trigger:* a change that adds a data store, an external integration, a trust boundary or a new class of user data.
2. Draw the data flow on one page: actors, processes, stores, flows and the trust boundaries they cross.
3. For each flow across a boundary, walk STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service,
   Elevation of privilege).[^ms-stride] Write threats as "an attacker who … can … because …".
4. Decide per threat: mitigate, eliminate, transfer or accept; mitigations become tickets or tests.
5. *Acceptance:* every threat has a decision and an owner. *Contingency:* if the design cannot be secured in time, scope the release
   down.

**Playbook 2: keep secrets out of Git.** Use a pre-commit secret scanner (fast, local, bypassable) *and* server-side protection
(authoritative). GitHub's push protection blocks pushes containing detected secrets, but by default anyone with write access can bypass
it with a reason ("It's used in tests", "It's a false positive", "I'll fix it later");[^gh-push] restrict bypass to reviewers
(delegated bypass) for production repositories. Plan limits (October 2026): repository push protection "Requires GitHub Secret
Protection to be enabled",[^gh-push] available for GitHub Team and Enterprise Cloud, with some features free for public
repositories;[^gh-ghas] push protection for users, on by default, covers pushes to public repositories only.[^gh-push] When a secret
is pushed, **rotate first, clean history second**: rewriting history does not un-leak a credential that was cloned or logged.

**Playbook 3: introduce SAST without losing developers' trust.**

1. Run the scanner report-only on main for two weeks; export SARIF.
2. Sample 30 findings per rule and ask the owning developers whether they would fix each. The share they would not is the rule's
   *effective false-positive rate*, the measure Google's Tricorder team used.[^tricorder]
3. Make *blocking* only rules whose rate is "essentially zero", Tricorder's bar for build-breaking checks; make *review comments*
   only rules under 10%, its bar for code-review analyses; leave the rest in a backlog report.[^tricorder]
4. Show results on changed lines only, and baseline existing findings.
5. *Acceptance:* blocking-rule override rate under 5% a month (Atlas target). *Rollback:* demote a rule the day its overrides spike.

**Playbook 4: dependency hygiene.** Keep dependencies few and current rather than chasing CVEs: commit lockfiles; let an update bot
propose grouped weekly updates; check the dependency *diff* in pull requests and the full image on main; prefer reachability-aware
SCA where available (Semgrep documents reachability for several languages[^semgrep]). Newly disclosed vulnerabilities in shipped
components are prioritised by [VulnerabilityOps](../vulnerabilityops/) with EPSS and KEV. Beware typosquatted and *hallucinated*
package names: in one study, LLM-generated code referenced non-existent packages at an average of at least 5.2% for commercial and
21.7% for open-source models,[^spracklen] names an attacker can register.

**Playbook 5: harden the pipeline against the OWASP CI/CD Top 10.**[^owasp-cicd]

| Risk | Control that addresses it |
|---|---|
| CICD-SEC-1 Insufficient Flow Control Mechanisms | Protected branches with required review and status checks; code owners for pipeline files; deploy only from the protected branch |
| CICD-SEC-2 Inadequate Identity and Access Management | Quarterly review of who can change pipelines, runners and environments |
| CICD-SEC-3 Dependency Chain Abuse | Lockfiles, internal proxy registries, scoped package names, review of new dependencies |
| CICD-SEC-4 Poisoned Pipeline Execution (PPE) | Same-repository PRs run their own workflow file: secrets only in branch-limited environments; gate rules from the base commit |
| CICD-SEC-5 Insufficient PBAC (Pipeline-Based Access Controls) | Per-job permissions; separate runners for untrusted and deploy jobs |
| CICD-SEC-6 Insufficient Credential Hygiene | OIDC instead of stored keys; masked logs; rotation runbook |
| CICD-SEC-7 Insecure System Configuration | Patch CI servers and runners; disable unused features; baseline per system |
| CICD-SEC-8 Ungoverned Usage of 3rd Party Services | Allow-list of actions, apps and integrations; pin by SHA or lockfile; review permissions they request |
| CICD-SEC-9 Improper Artifact Integrity Validation | Sign artefacts, generate provenance, verify before deploy |
| CICD-SEC-10 Insufficient Logging and Visibility | CI audit logs to the security data platform ([SecDataOps](../secdataops/)); alerts on new admins, runners, disabled protections |

OpenSSF Scorecard automates several of these checks (Pinned-Dependencies, Token-Permissions, Dangerous-Workflow).[^scorecard]

**Playbook 6: sign, attest and verify.** On every protected-branch build: build once, generate the SBOM from the artefact, sign,
attach provenance, push by digest. At deploy, verify the expected signer and builder; refuse anything else. Keyless signing with
Sigstore "associates identities, rather than keys, with an artifact signature": Fulcio issues a short-lived certificate for an OIDC
identity, and the event is logged in Rekor.[^sigstore-sign] For GitHub Actions the exact values are:[^fulcio-oidc]

```text
certificate identity:  https://github.com/{job_workflow_ref}
OIDC issuer:           https://token.actions.githubusercontent.com
```

Verifying that identity, not "any valid signature", is what makes the check meaningful.

**API security testing.** Linting and diffing the API contract in the pull request belongs to [API Ops](../api-ops/). In test,
ZAP's API scan imports an OpenAPI, SOAP or GraphQL definition and runs an active scan against the URLs it finds,[^zap-api] so point
it only at test systems. Scanners rarely find authorisation flaws, yet three of the top five risks in the OWASP API Security Top 10
(2023, the latest edition listed in October 2026) are authorisation failures: API1, API3 and API5.[^owasp-api] Write them as tests
(Atlas recommendation): call each endpoint that takes an object ID with a second user's token and expect a refusal; call admin
functions as an ordinary user. For web applications, the OWASP Top 10:2025 guides SAST rules and champion training.[^owasp-top10-2025]

### Methods, standards and frameworks

| Framework | What it is | Status (October 2026) | Mandatory? |
|---|---|---|---|
| NIST SP 800-218 SSDF v1.1 | Outcome-based secure development practices (PO, PS, PW, RV)[^ssdf] | Final (Feb 2022); v1.2 draft since Dec 2025[^ssdf-r1] | Voluntary; mapped to EO 14028 §4e;[^ssdf-project] US attestation policy changed in 2026 ([Quality](#quality-security-privacy-and-compliance)) |
| NIST SP 800-204C, 800-204D | DevSecOps for microservices; supply-chain security in CI/CD[^nist-204c][^nist-204d] | Final (2022, 2024) | Guidance |
| NIST SP 1800-44 (NCCoE) | DevSecOps practices aligned with the SSDF[^nccoe-1800-44] | Preliminary drafts | Guidance in progress |
| SLSA v1.2 | Build track L0–L3 and Source track L1–L4 for supply-chain integrity[^slsa-spec][^slsa-source] | Approved | Voluntary |
| OWASP Top 10:2025 | Awareness list of web-application risk categories[^owasp-top10-2025] | Current edition | Awareness only |
| OWASP ASVS | Requirements for testing web application security controls[^owasp-asvs] | 5.0.0 latest listed | Voluntary |
| OWASP SAMM v2 | Maturity model: 5 business functions, 15 practices, 3 maturity levels each[^samm-about] | v2.0 | Self-assessment |
| BSIMM | Descriptive model of observed practices across 100+ organisations[^bsimm] | BSIMM16 (vendor page) | Benchmark |
| OWASP DSOMM | DevSecOps maturity model, an OWASP Lab project[^owasp-dsomm] | Lab | Self-assessment |
| SBOM minimum elements | 17 minimum data fields for an SBOM[^sbom-2026] | 2026 edition (July 2026) | Guidance, not regulation |
| EU Cyber Resilience Act | Product cybersecurity regulation; SBOM covering at least top-level dependencies[^eu-cra-text] | In force; reporting from 2026-09-11, main obligations from 2027-12-11[^eu-cra] | **Legally binding** for in-scope products on the EU market |
| CycloneDX, SPDX | SBOM formats: CycloneDX is ECMA-424; SPDX is ISO/IEC 5962:2021[^cyclonedx][^spdx] | CycloneDX 1.7; SPDX 3.0 | Choose per consumer |
| SARIF 2.1.0 | Interchange format for static-analysis results[^sarif] | OASIS Standard | Accepted by GitHub code scanning[^gh-sarif] |

**Normative versus recommended.** Only the CRA imposes legal duties, on in-scope products; the rest become mandatory when a customer,
regulator or contract cites them.

### Tooling landscape

Choose the *job* first, then a tool; prices are omitted.

| Tool category | Job | Representative tools (checked 2026-10-16) | Selection criteria | Risks |
|---|---|---|---|---|
| Secret scanning | Find credentials in code and history | Gitleaks (CLI, pre-commit; "feature complete", security patches only, maintainer moving to Betterleaks);[^gitleaks] GitHub push protection[^gh-push] | Pre-push blocking, history scan, custom rules, baseline | Bypass culture; secrets in logs and images not covered |
| SAST | Find code flaws | CodeQL ("the code analysis engine developed by GitHub");[^gh-codeql] Semgrep Code[^semgrep] | Languages, rule quality, diff-aware scans, SARIF output | Noise; long scan times on large repos |
| SCA and SBOM | Inventory and match components | Syft (SBOM generator);[^syft] Trivy;[^trivy] Semgrep Supply Chain[^semgrep] | Ecosystems covered, reachability, SBOM formats, VEX support | Duplicate findings across tools; stale databases |
| Container and IaC scanning | Images, Dockerfiles, Terraform, Kubernetes manifests | Trivy (`vuln`, `misconfig`, `secret`, `license` scanners)[^trivy-cli] | Targets, policy language, offline use | Base-image noise without `--ignore-unfixed`-style filtering |
| Policy as code | Custom rules for config | Conftest (`deny`, `warn`, `violation` rules in Rego)[^conftest] | Testable policies, shared libraries | Policy sprawl without tests |
| DAST | Test a running app | ZAP ("Free and open source", branded "ZAP by Checkmarx")[^zap] | Auth handling, API support, CI mode | Slow; flaky against unstable test environments |
| Signing and provenance | Integrity evidence | Sigstore cosign;[^sigstore-sign] GitHub artifact attestations (Build L2 by itself, L3 with reusable workflows)[^gh-attest-concept] | Keyless support, verification tooling, private-repo handling | Weak verification policies; public log side effects |

**Some tools phone home**: Semgrep's `--config auto` logs in to the Semgrep Registry
with the project URL and sends pseudonymous metrics by default;[^semgrep-cli] check before scanning confidential code.

### Automation and AI

**What to automate.** Deterministic checks (secret patterns, known-vulnerable versions, policy rules, signature verification).
Judgement (threat modelling, risk acceptance, exploitability in context) stays with people.

**AI as a source of risk.** A 2021 study of an early GitHub Copilot model generated 1,689 programs from 89 security-relevant scenarios
and found approximately 40% vulnerable;[^pearce] read this as a reason to gate generated code like any other, not as a current rate. Package hallucination is a newer supply-chain vector (Playbook 4).[^spracklen] NIST SP 800-218A
adds SSDF practices for developing generative-AI models;[^ssdf-pubs] securing LLM *applications* is covered on [LLMOps](../llmops/).

**AI as a helper.** An AI-proposed fix is a code change: same review, same gates. Never let an assistant dismiss findings without a
recorded human decision, or the effective false-positive metric starts measuring the model.
Evaluating AI inside security operations is covered on [AIOps for Security](../aiops-for-security/).

## DevSecOps in practice

### Reference implementation

**Design.** One service (`orders-service`), one private repository in a GitHub Enterprise Cloud organisation, GitHub Actions, images
in GitHub Container Registry, deployment to AWS through OIDC. The structure maps to any CI system: jobs split by trust, a gate that
applies a written policy, verification before deploy. **Assumptions:** GitHub-hosted runners; scanners installed from pinned,
checksum-verified releases by a repository script. **Constraints:** replace `<FULL_LENGTH_COMMIT_SHA>` with the full SHA of the
release you reviewed; account IDs and names are fake. The YAML is only as strong as these settings (availability for a **private**
repository, October 2026):

| Setting | What it prevents | Plan |
|---|---|---|
| `production` environment: deployment branches limited to `main` | Other branches and `pull_request` jobs getting its OIDC subject (not `pull_request_target` jobs: see below) | Pro, Team, Enterprise Cloud[^gh-envs] |
| `production` environment: required reviewers, self-review prevented, no administrator bypass | Unreviewed or self-approved deploys | Enterprise Cloud (others: public repositories only)[^gh-envs] |
| Artifact attestations | Unsigned provenance and SBOMs | Enterprise Cloud (others: public repositories only)[^gh-attest-howto] |
| Ruleset on `main`: code-owner review; `checks` required, from GitHub Actions | Merging past a red gate; unreviewed pipeline edits | Pro, Team, Enterprise Cloud[^gh-rulesets] |
| No repository- or organisation-level deploy secrets | Same-repository pull requests reading them | All plans |

**1. The pipeline.**

_Example status: YAML syntax-checked; not executed (requires a GitHub repository, a registry and an AWS account)._

```yaml
name: ci
on:
  pull_request:
  push:
    branches: [main]

permissions: {}            # nothing by default; each job asks for what it needs

jobs:
  checks:                  # Zone 1: untrusted input, no secrets; a required status check
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      BASE_SHA: ${{ github.event.pull_request.base.sha }}
    steps:
      - uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>   # pinned; note the release
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Gate, scanner rules and installer from the base commit, not from this PR
        run: |
          mkdir -p "$RUNNER_TEMP/base"
          git archive "$BASE_SHA" .ci | tar -x -C "$RUNNER_TEMP/base"
          echo "CI_DIR=$RUNNER_TEMP/base/.ci" >> "$GITHUB_ENV"
      - name: Install pinned scanners (checksums verified)
        run: bash "$CI_DIR/install-scanners.sh"
      - name: Secret scan of this pull request's commits
        run: >
          gitleaks git --log-opts="$BASE_SHA..HEAD" --redact --exit-code 0
          --report-format sarif --report-path gitleaks.sarif
      - name: SAST, new findings relative to the base commit
        run: >
          semgrep scan --config "$CI_DIR/semgrep/" --baseline-commit "$BASE_SHA"
          --sarif --output semgrep.sarif
      - name: Dependencies, secrets and IaC misconfiguration (whole tree)
        run: >
          trivy fs --scanners vuln,secret,misconfig
          --format sarif --output trivy.sarif .
      - name: Proportionate gate
        run: >
          python3 "$CI_DIR/security_gate.py"
          --policy "$CI_DIR/gate-policy.json"
          --baseline "$CI_DIR/baseline.json"
          --exceptions "$CI_DIR/exceptions.json"
          gitleaks.sarif semgrep.sarif trivy.sarif
      - name: Kubernetes manifest policy (a deliberate hard gate)
        if: ${{ !cancelled() }}  # report it even when the gate has already blocked
        run: conftest test deploy/ --policy "$CI_DIR/policy"

  build:                   # Zone 2: main only; Docker and the reviewed Dockerfile
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>
        with:
          persist-credentials: false
      - name: Build and push by digest
        id: push
        env:
          IMAGE: ghcr.io/example-org/orders-service
          REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "$REGISTRY_TOKEN" \
            | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
          docker build -t "$IMAGE:$GITHUB_SHA" .
          docker push "$IMAGE:$GITHUB_SHA"
          ref="$(docker inspect --format '{{index .RepoDigests 0}}' "$IMAGE:$GITHUB_SHA")"
          echo "digest=${ref#*@}" >> "$GITHUB_OUTPUT"

  scan:                    # Zone 2: third-party scanners with a read-only token
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: read
    env:
      REF: ghcr.io/example-org/orders-service@${{ needs.build.outputs.digest }}
    steps:
      - uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>
        with:
          persist-credentials: false
      - name: Install pinned scanners (checksums verified)
        run: ./.ci/install-scanners.sh
      - name: Full image scan (recorded) and SBOM from the pushed image
        env:
          REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "$REGISTRY_TOKEN" \
            | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
          trivy image --format sarif --output image.sarif "$REF"
          syft "$REF" -o cyclonedx-json=sbom.cdx.json
      - uses: actions/upload-artifact@<FULL_LENGTH_COMMIT_SHA>
        with:
          name: scan-results
          path: |
            sbom.cdx.json
            image.sarif

  attest:                  # Zone 2: signing identity; GitHub-owned actions only
    needs: [build, scan]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write      # push attestations to the registry
      id-token: write
      attestations: write
      artifact-metadata: write
    env:
      IMAGE: ghcr.io/example-org/orders-service
    steps:
      - name: Registry login for push-to-registry
        env:
          REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "$REGISTRY_TOKEN" \
            | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
      - uses: actions/download-artifact@<FULL_LENGTH_COMMIT_SHA>
        with:
          name: scan-results
      - name: Attest build provenance
        uses: actions/attest@<FULL_LENGTH_COMMIT_SHA>     # v4
        with:
          subject-name: ${{ env.IMAGE }}
          subject-digest: ${{ needs.build.outputs.digest }}
          push-to-registry: true
      - name: Attest the SBOM
        uses: actions/attest@<FULL_LENGTH_COMMIT_SHA>     # v4
        with:
          subject-name: ${{ env.IMAGE }}
          subject-digest: ${{ needs.build.outputs.digest }}
          sbom-path: sbom.cdx.json
          push-to-registry: true

  deploy:                  # Zone 3: approved environment, verify before deploy
    needs: [build, attest]
    runs-on: ubuntu-latest
    environment: production   # main only, reviewers, no administrator bypass
    permissions:
      contents: read
      packages: read
      attestations: read
      id-token: write
    env:
      REF: ghcr.io/example-org/orders-service@${{ needs.build.outputs.digest }}
    steps:
      - uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>   # for deploy/deploy.sh
        with:
          persist-credentials: false
      - name: Verify provenance and signer identity
        env:
          GH_TOKEN: ${{ github.token }}
          WORKFLOW: ${{ github.repository }}/.github/workflows/ci.yml
        run: |
          echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
          gh attestation verify "oci://$REF" --bundle-from-oci \
            --repo "$GITHUB_REPOSITORY" \
            --cert-identity "https://github.com/$WORKFLOW@refs/heads/main" \
            --deny-self-hosted-runners
      - name: Assume the production deploy role (OIDC, no stored keys)
        uses: aws-actions/configure-aws-credentials@<FULL_LENGTH_COMMIT_SHA>
        with:
          role-to-assume: arn:aws:iam::111122223333:role/orders-deploy-production
          aws-region: eu-west-1
      - name: Deploy by digest
        run: ./deploy/deploy.sh "$REF"
```

Why it is built this way:

- `permissions: {}` plus per-job grants follow GitHub's advice to give `GITHUB_TOKEN` "the minimum required
  permissions".[^gh-hardening] Third-party scanners run only where the token can at most read (`checks`, `scan`); that matters
  because Syft, for one, reads the registry credential that `docker login` stores.[^anchore-registries] `build` can push but runs
  only Docker and the reviewed Dockerfile.
- **Residual risks.** First, every job with `id-token: write` (`attest`, and `deploy` with its AWS action) could obtain a Sigstore
  certificate with the same identity, `ci.yml@refs/heads/main`.[^fulcio-oidc] The deploy job cannot publish an attestation (no
  `attestations: write` or `packages: write`). To give signing its own identity, move the attest steps into a reusable workflow and
  pin it at verification with `--signer-workflow`, also GitHub's route to SLSA Build Level 3.[^gh-attest-concept][^gh-attest-verify]
  Second, an attestation vouches for who signed the SBOM, not for the generator's honesty: a compromised Syft in the scan job could
  hand `attest` a falsified SBOM. Pin and checksum-verify the generator, and weigh the SBOM attestation below the build provenance,
  which `attest` generates itself.[^attest-readme]
- The gate script, policy, baseline and exceptions, the Semgrep rules, the Conftest policy and the scanner installer all come from
  `.ci/` at the base commit, so a pull request cannot loosen them; an exception lands in its own reviewed pull request first.
  Bootstrap: `git archive` fails while the base commit has no `.ci/`, so merge `.ci/` in a reviewed pull request before making
  `checks` required. A pull
  request can still change what scanners report: by editing `ci.yml`, with inline suppressions (`nosemgrep`, `gitleaks:allow`), or
  with ignore and config files read from the checkout (`.semgrepignore`, `.gitleaksignore`, `.gitleaks.toml`, `.trivyignore`,
  `trivy.yaml`).[^semgrep-ignore][^gitleaks][^trivy-fs] So the ruleset requires code-owner review (CODEOWNERS below covers those
  files; reviewers treat a new inline suppression as an exception request) and makes `checks` a required status check whose
  source is GitHub Actions (any writer "can set the state of any status check" otherwise).[^gh-rulesets]
  CODEOWNERS cannot express a risk threshold, so AppSec approves every exception here, stricter than the RACI.
- Untrusted values reach shell commands through environment variables, GitHub's pattern against script injection.[^gh-hardening]
- Gitleaks scans only the pull request's commits and exits 0 so the gate, not each tool, decides (it otherwise exits 1 on
  leaks).[^gitleaks] Trivy scans the whole tree, quick for lockfiles; the baseline stops old findings blocking. Conftest is the
  deliberate exception, a hard gate: its few rules are deterministic and fixable in the same change. If they need baselines or
  exceptions, write SARIF (`--output sarif`) and pass it to the gate.[^conftest-opts] The image scan on main is recorded, not
  gated: the change is already merged.
- `actions/attest` follows GitHub's example,[^gh-attest-howto] plus `artifact-metadata: write`, which its README lists for registry
  storage records;[^attest-readme] alone it gives SLSA Build Level 2.[^gh-attest-concept] `gh attestation verify` enforces the
  `https://slsa.dev/provenance/v1` predicate by default, `--cert-identity` pins the exact certificate SubjectAlternativeName,
  `--deny-self-hosted-runners` rejects self-hosted builds, and `--bundle-from-oci` reads the bundle from the registry, which needs a
  registry login.[^gh-attest-verify]

_Example status: illustrative file; not executed._

```text
# .github/CODEOWNERS (enforced by the ruleset's "Require review from Code Owners")
/.github/CODEOWNERS   @example-org/appsec
/.github/workflows/   @example-org/appsec
/.ci/                 @example-org/appsec
# Files that change what scanners report, in any directory
.gitleaks.toml        @example-org/appsec
.gitleaksignore       @example-org/appsec
.semgrepignore        @example-org/appsec
.trivyignore          @example-org/appsec
trivy.yaml            @example-org/appsec
```

**2. The cloud trust policy.** The role trusts only tokens from jobs that reference the `production` environment of this repository.

_Example status: JSON syntax-checked; not executed. Repositories created after 2026-07-15 use an immutable subject format that
includes owner and repository IDs; read the `sub` claim from a real token before writing the condition._[^gh-oidc-ref]

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated":
          "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub":
            "repo:example-org/orders-service:environment:production"
        }
      }
    }
  ]
}
```

GitHub documents the `environment` subject format `repo:ORG-NAME/REPO-NAME:environment:ENVIRONMENT-NAME`.[^gh-oidc-ref] Its AWS guide
also shows a `StringLike` wildcard such as `repo:octo-org/octo-repo:*`;[^gh-oidc-aws] that form lets *any* branch, tag or pull-request
job in the repository assume the role, so never use it for a production role.

**The environment subject is only as strong as the environment's settings** (table above). Protection rules must pass "before a
job referencing the environment can proceed",[^gh-envs] so:

- With deployment branches at "No restriction", a job on any branch, a same-repository pull-request branch included, can declare
  `environment: production` and receive this subject ("Protected branches only" lets every branch deploy if no branch has
  protection rules).[^gh-envs]
- A `pull_request_target` job passes a `main`-only limit: since December 2025 GitHub evaluates its environment rules against the
  default branch.[^gh-prt-env] Keep such workflows away from deploy environments.
- On Free, Pro and Team, required reviewers "are only available for public repositories", and "By default, administrators can
  bypass the protection rules".[^gh-envs] Without reviewers, anyone who can merge to `main` can deploy.

To bind the role to one workflow file too, add `job_workflow_ref` to the subject template; the new format replaces the default for
the repository, so update every dependent trust policy.[^gh-oidc-ref]

**3. The gate policy, baseline and exceptions.** Three JSON files under `.ci/`, read from the base commit; format in Lab 2. The
policy blocks *new* high or critical findings, warns on medium, and always blocks a new secret.

**4. A manifest policy (Conftest).**

_Example status: not executed; Rego v1 syntax as in Conftest's documentation,[^conftest] plus OPA's `object.get` built-in._

```rego
package main

deny contains msg if {
  input.kind == "Deployment"
  some c in input.spec.template.spec.containers
  not contains(c.image, "@sha256:")
  msg := sprintf("container %s must reference its image by digest", [c.name])
}

deny contains msg if {
  input.kind == "Deployment"
  pod := input.spec.template.spec
  some c in pod.containers
  not non_root(c, pod)
  msg := sprintf("container %s must run with runAsNonRoot: true", [c.name])
}

# A container-level setting wins; without one, the pod-level setting applies.
non_root(c, _) if c.securityContext.runAsNonRoot == true

non_root(c, pod) if {
  object.get(c, ["securityContext", "runAsNonRoot"], null) == null
  pod.securityContext.runAsNonRoot == true
}
```

This catches the problem in the pull request. Enforcing the same rules at cluster admission, so nothing bypasses the pipeline, is
[KubernetesSecOps](../kubernetessecops/) territory.

### Hands-on labs

Lab 1 needs only a whiteboard; Lab 2 was **executed** for this page; Labs 3 and 4 were **not executed** here, so their expected
results are patterns.

#### Lab 1 (beginner): threat-model your own pipeline

**Objective:** find which pipeline jobs can reach which credentials, and the three changes that most reduce risk. **Prerequisites:**
read access to one repository's CI configuration and its settings (secrets, environments, runners); 60–90 minutes; no cost or risk.

1. List every job: trigger (push, pull request, schedule, manual), runner type (hosted or self-hosted), token permissions.
2. For each job, list every secret and cloud role it can reach, including organisation-level secrets inherited by the repository.
3. List every third-party step and how it is referenced: tag, branch or full commit SHA.
4. Place each job in a trust zone from the [trust-zone figure](#trust-boundaries-and-identities-in-the-pipeline).
5. For each job, ask the four questions of the Threat Modeling Manifesto[^tm-manifesto] and check it against the OWASP CI/CD Top 10
   (Playbook 5).[^owasp-cicd]

**Expected output:** a table of jobs with trigger, zone, reachable secrets, unpinned steps and a risk note, plus three ranked actions.
**Validation:** no job triggered by untrusted input can reach a secret (count repository- and organisation-level secrets too); no
job holding a secret or write token runs an unpinned third-party step; every cloud role names a protected branch or environment. **Troubleshooting:** if you cannot tell which secrets a
job can read, that is your first finding. **Teardown:** none; file the actions as tickets.

#### Lab 2 (intermediate): build and test a proportionate gate

**Objective:** turn a written gate policy into code and watch how baselines, severities, secrets and expiring exceptions change the
decision. **Prerequisites:** Python 3.8 or later; no network, cloud account or cost; 45 minutes. **Status:** executed (output
below).

1. Save the script below as `security_gate.py`.
2. Save the five JSON files that follow as `gate-policy.json`, `baseline.json`, `exceptions.json`, `sast.sarif` and `sca.sarif`:
   minimal hand-written SARIF 2.1.0, with GitHub's `security-severity` bands ("over 9.0 is `critical`", 7.0–8.9 high, 4.0–6.9
   medium) and `partialFingerprints` to match results across runs.[^gh-sarif][^sarif]
3. Run the gate for 16 October 2026.
4. Run it again "on 1 December 2026", after the exception has expired.
5. Simulate fixing the SQL injection and removing the secret, and run again.

_Example status: executed with Python 3.8.10 (re-run on 2026-10-17 after line wrapping); output below is captured._

```python
#!/usr/bin/env python3
"""Proportionate security gate: decide BLOCK / WARN / PASS from SARIF results.

Usage: python3 security_gate.py --policy gate-policy.json --baseline baseline.json \
           --exceptions exceptions.json [--today YYYY-MM-DD] \
           results1.sarif [results2.sarif ...]
Exit codes: 0 = pass (warnings allowed), 1 = at least one BLOCK, 2 = invalid input.
Standard library only (Python 3.8+).
"""
import argparse
import datetime as dt
import hashlib
import json
import sys

RANK = {"none": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
LEVEL_TO_SEVERITY = {"error": "high", "warning": "medium", "note": "low", "none": "none"}


def severity_from_score(score):
    # GitHub code scanning bands: over 9.0 critical, 7.0-8.9 high, 4.0-6.9 medium,
    # 0.1-3.9 low.
    if score > 9.0:
        return "critical"
    if score >= 7.0:
        return "high"
    if score >= 4.0:
        return "medium"
    return "low" if score > 0 else "none"


def fingerprint(tool, result):
    fps = result.get("partialFingerprints") or {}
    if fps:
        key = sorted(fps)[0]
        return "%s:%s" % (tool, fps[key])
    # Fallback: rule + file + line. Line numbers move when code is edited,
    # so prefer tools that emit fingerprints.
    loc = (result.get("locations") or [{}])[0].get("physicalLocation", {})
    uri = loc.get("artifactLocation", {}).get("uri")
    line = loc.get("region", {}).get("startLine")
    raw = "%s|%s|%s|%s" % (tool, result.get("ruleId"), uri, line)
    return "%s:sha1:%s" % (tool, hashlib.sha1(raw.encode()).hexdigest()[:16])


def load_json(path):
    with open(path, encoding="utf-8") as fh:
        return json.load(fh)


def load_findings(path):
    sarif = load_json(path)
    findings = []
    for run in sarif.get("runs", []):
        tool = run["tool"]["driver"]["name"]
        rules = {r["id"]: r for r in run["tool"]["driver"].get("rules", [])}
        for res in run.get("results", []):
            rule = rules.get(res.get("ruleId"), {})
            props = rule.get("properties", {})
            if "security-severity" in props:
                sev = severity_from_score(float(props["security-severity"]))
            else:
                default = rule.get("defaultConfiguration", {}).get("level", "warning")
                level = res.get("level") or default
                sev = LEVEL_TO_SEVERITY.get(level, "medium")
            loc = (res.get("locations") or [{}])[0].get("physicalLocation", {})
            findings.append({
                "tool": tool,
                "rule": res.get("ruleId", "?"),
                "severity": sev,
                "tags": set(props.get("tags", [])),
                "where": "%s:%s" % (loc.get("artifactLocation", {}).get("uri", "?"),
                                    loc.get("region", {}).get("startLine", "?")),
                "fp": fingerprint(tool, res),
            })
    return findings


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--policy", required=True)
    ap.add_argument("--baseline", required=True)
    ap.add_argument("--exceptions", required=True)
    ap.add_argument("--today", default=dt.date.today().isoformat())
    ap.add_argument("sarif", nargs="+")
    a = ap.parse_args()
    try:
        policy = load_json(a.policy)
        baseline = set(load_json(a.baseline)["fingerprints"])
        exceptions = load_json(a.exceptions)["exceptions"]
        today = dt.date.fromisoformat(a.today)
        findings = [f for p in a.sarif for f in load_findings(p)]
    except (OSError, ValueError, KeyError) as err:
        print("INVALID INPUT: %s" % err)
        return 2

    block_at = RANK[policy["block_at_or_above"]]
    warn_at = RANK[policy["warn_at_or_above"]]
    always = set(policy.get("always_block_tags", []))
    hard_tools = {t.lower() for t in policy.get("always_block_tools", [])}
    max_days = int(policy.get("max_exception_days", 90))

    active, problems = {}, []
    for ex in exceptions:
        expires = dt.date.fromisoformat(ex["expires"])
        granted = dt.date.fromisoformat(ex["granted"])
        if (expires - granted).days > max_days:
            problems.append("exception %s exceeds %d days"
                            % (ex["fingerprint"], max_days))
        elif not ex.get("owner") or not ex.get("reason"):
            problems.append("exception %s lacks owner or reason" % ex["fingerprint"])
        elif expires < today:
            problems.append("exception %s expired on %s"
                            % (ex["fingerprint"], ex["expires"]))
        else:
            active[ex["fingerprint"]] = ex

    counts = {"BLOCK": 0, "WARN": 0, "EXCEPTED": 0, "BASELINE": 0, "INFO": 0}
    for f in sorted(findings, key=lambda x: (-RANK[x["severity"]], x["tool"], x["rule"])):
        rank, is_new = RANK[f["severity"]], f["fp"] not in baseline
        hard = bool(f["tags"] & always) or f["tool"].lower() in hard_tools
        if f["fp"] in active and not hard:
            decision = "EXCEPTED"
        elif hard and is_new:
            decision = "BLOCK"
        elif not is_new and policy.get("block_only_new", True):
            decision = "BASELINE"
        elif rank >= block_at:
            decision = "BLOCK"
        elif rank >= warn_at:
            decision = "WARN"
        else:
            decision = "INFO"
        counts[decision] += 1
        print("%-8s %-8s %-15s %-14s %s"
              % (decision, f["severity"], f["tool"], f["rule"], f["where"]))

    for p in problems:
        print("NOTE     %s" % p)
    print("SUMMARY  " + " ".join("%s=%d" % kv for kv in counts.items()))
    return 1 if counts["BLOCK"] else 0


if __name__ == "__main__":
    sys.exit(main())
```

`gate-policy.json`, `baseline.json` and `exceptions.json`:

```json
{
  "version": 1,
  "block_at_or_above": "high",
  "warn_at_or_above": "medium",
  "block_only_new": true,
  "always_block_tags": ["secret"],
  "always_block_tools": ["gitleaks"],
  "max_exception_days": 90
}
```

```json
{"fingerprints": ["example-sast:sast-c3", "example-sca:sca-e5"]}
```

```json
{
  "exceptions": [
    {
      "fingerprint": "example-sca:sca-f6",
      "reason": "Vulnerable function not called; upgrade needs framework 5.x migration",
      "owner": "team-orders",
      "ticket": "SEC-123",
      "granted": "2026-10-01",
      "expires": "2026-11-30"
    }
  ]
}
```

`sast.sarif` (a SAST run and a secrets run) and `sca.sarif` (fake CVE identifiers in the reserved-looking 2099 range):

```json
{
  "version": "2.1.0",
  "runs": [
    {
      "tool": {"driver": {"name": "example-sast", "rules": [
        {"id": "SQLI-001",
         "properties": {"security-severity": "8.8", "tags": ["security"]}},
        {"id": "XSS-002",
         "properties": {"security-severity": "6.1", "tags": ["security"]}},
        {"id": "WEAKHASH-003",
         "properties": {"security-severity": "5.3", "tags": ["security"]}}
      ]}},
      "results": [
        {"ruleId": "SQLI-001", "level": "error",
         "message": {"text": "User input reaches SQL query"},
         "partialFingerprints": {"primaryLocationLineHash": "sast-a1"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "app/orders.py"},
           "region": {"startLine": 42}}}]},
        {"ruleId": "XSS-002", "level": "warning",
         "message": {"text": "Unescaped output in template"},
         "partialFingerprints": {"primaryLocationLineHash": "sast-b2"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "web/cart.html"},
           "region": {"startLine": 17}}}]},
        {"ruleId": "WEAKHASH-003", "level": "warning",
         "message": {"text": "MD5 used for a token"},
         "partialFingerprints": {"primaryLocationLineHash": "sast-c3"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "app/legacy.py"},
           "region": {"startLine": 88}}}]}
      ]
    },
    {
      "tool": {"driver": {"name": "example-secrets", "rules": [
        {"id": "aws-access-key",
         "properties": {"tags": ["security", "secret"]}}
      ]}},
      "results": [
        {"ruleId": "aws-access-key", "level": "warning",
         "message": {"text": "Possible access key"},
         "partialFingerprints": {"primaryLocationLineHash": "sec-d4"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "config/dev.env"},
           "region": {"startLine": 3}}}]}
      ]
    }
  ]
}
```

```json
{
  "version": "2.1.0",
  "runs": [
    {
      "tool": {"driver": {"name": "example-sca", "rules": [
        {"id": "CVE-2099-0001",
         "properties": {"security-severity": "9.8", "tags": ["security"]}},
        {"id": "CVE-2099-0002",
         "properties": {"security-severity": "7.5", "tags": ["security"]}},
        {"id": "CVE-2099-0003",
         "properties": {"security-severity": "3.1", "tags": ["security"]}}
      ]}},
      "results": [
        {"ruleId": "CVE-2099-0001",
         "message": {"text": "examplelib 1.2.0 is affected"},
         "partialFingerprints": {"primaryLocationLineHash": "sca-e5"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "requirements.txt"},
           "region": {"startLine": 12}}}]},
        {"ruleId": "CVE-2099-0002",
         "message": {"text": "parserlib 3.0.1 is affected"},
         "partialFingerprints": {"primaryLocationLineHash": "sca-f6"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "requirements.txt"},
           "region": {"startLine": 19}}}]},
        {"ruleId": "CVE-2099-0003",
         "message": {"text": "colourlib 0.9.0 is affected"},
         "partialFingerprints": {"primaryLocationLineHash": "sca-g7"},
         "locations": [{"physicalLocation": {
           "artifactLocation": {"uri": "requirements.txt"},
           "region": {"startLine": 23}}}]}
      ]
    }
  ]
}
```

_Example status: syntax-checked with `bash -n`; the commands were executed and printed the output shown below._

```bash
python3 security_gate.py --policy gate-policy.json --baseline baseline.json \
  --exceptions exceptions.json --today 2026-10-16 sast.sarif sca.sarif; echo "exit=$?"
python3 security_gate.py --policy gate-policy.json --baseline baseline.json \
  --exceptions exceptions.json --today 2026-12-01 sast.sarif sca.sarif; echo "exit=$?"
```

**Expected output** (step 3, captured):

```text
BASELINE critical example-sca     CVE-2099-0001  requirements.txt:12
BLOCK    high     example-sast    SQLI-001       app/orders.py:42
EXCEPTED high     example-sca     CVE-2099-0002  requirements.txt:19
BASELINE medium   example-sast    WEAKHASH-003   app/legacy.py:88
WARN     medium   example-sast    XSS-002        web/cart.html:17
BLOCK    medium   example-secrets aws-access-key config/dev.env:3
INFO     low      example-sca     CVE-2099-0003  requirements.txt:23
SUMMARY  BLOCK=2 WARN=1 EXCEPTED=1 BASELINE=2 INFO=1
exit=1
```

Step 4 prints `BLOCK high example-sca CVE-2099-0002`, a line `NOTE exception example-sca:sca-f6 expired on 2026-11-30` and
`SUMMARY BLOCK=3 … EXCEPTED=0`, exit 1. Step 5 (remove the `SQLI-001` result and the `example-secrets` run from `sast.sarif`) prints
`SUMMARY BLOCK=0 WARN=1 EXCEPTED=1 BASELINE=2 INFO=1` and exit 0. A missing input file prints `INVALID INPUT: …` and exits 2; an
exception longer than 90 days prints `NOTE exception … exceeds 90 days` and is not applied.

**Validation.** Explain each line: why critical CVE-2099-0001 does *not* block (baseline debt), why a medium-severity secret *does*
(the `secret` tag), and why the exception stops applying after expiry. **Troubleshooting.** `KeyError: 'fingerprints'`: the baseline
lacks that key. Everything blocks on an existing repository: generate a baseline from main first. A finding reappears as new after a
refactor: its fingerprint changed; prefer tools with stable `partialFingerprints`.
**Teardown:** delete the directory.

#### Lab 3 (intermediate): scan a throwaway repository locally

**Objective:** produce SARIF and CycloneDX output from secret scanning, dependency scanning and SBOM generation, and feed it to Lab 2.
**Prerequisites:** Gitleaks, Trivy and Syft from their official releases (verify checksums); Git; Python 3; network access for
Trivy's database; 30 minutes. **Risk:** none if you never push this repository. **Status:** not executed; flags from each tool's
documentation, 2026-10-16.[^gitleaks][^trivy-cli][^syft]

_Example status: syntax-checked with `bash -n`; not executed (the tools are not installed in the authoring environment)._

```bash
#!/usr/bin/env bash
set -euo pipefail
mkdir lab3 && cd lab3 && git init -q
# A fake, token-shaped string generated locally; it grants nothing.
FAKE="ghp_$(python3 -c 'import random,string
print("".join(random.choices(string.ascii_letters + string.digits, k=36)))')"
printf 'GITHUB_TOKEN=%s\n' "$FAKE" > settings.env
printf 'pyyaml==5.3\n' > requirements.txt          # deliberately old version
git add . && git commit -qm "lab 3 seed"

gitleaks git --report-format sarif --report-path gitleaks.sarif --redact \
  || echo "gitleaks exit=$?"
trivy fs --scanners vuln,secret --format sarif --output trivy.sarif . \
  || echo "trivy exit=$?"
syft . -o cyclonedx-json=sbom.cdx.json
python3 -c 'import json; d = json.load(open("sbom.cdx.json"))
print(d["bomFormat"], len(d.get("components", [])))'
```

**Expected output (pattern, not captured).** Gitleaks reports one leak in `settings.env` and exits 1.[^gitleaks] `trivy.sarif`
lists vulnerabilities for the old PyYAML, which should include CVE-2020-14343 (critical below 5.4 in GitHub's advisory
database[^ghsa-pyyaml]). The last line prints `CycloneDX` and a component count. **Validation:** feed both SARIF files to Lab 2's
script with an empty baseline (`{"fingerprints": []}`): the secret must block (check the tool name and tags the SARIF carries).
**Troubleshooting:** Trivy cannot fetch its database behind a proxy: set proxy variables; Gitleaks finds nothing: was the file
committed? **Teardown:** `cd .. && rm -rf lab3`; never push it.

#### Lab 4 (advanced): sign, attest and verify in a sandbox repository

**Objective:** attest provenance and an SBOM for an image, and prove verification rejects the wrong signer. **Prerequisites:** a
throwaway repository with the `build`, `scan` and `attest` jobs from the [reference implementation](#reference-implementation), the
GitHub CLI, Docker. **Side effects:** in a **public** repository, attestations go to a public transparency log[^gh-attest-concept]
whose entries "cannot be removed later";[^cosign-readme] private repositories need GitHub Enterprise Cloud for
attestations.[^gh-attest-howto] **Status:** not executed here.

1. Push the workflow and let the jobs run on `main`; note the digest the build job prints.
2. Log in to the registry (verifying an OCI URL requires it[^gh-attest-verify]) and verify the positive case (replace the
   placeholders):

   ```bash
   docker login ghcr.io   # a token that can read the package
   gh attestation verify "oci://ghcr.io/OWNER/REPO@DIGEST" --repo "OWNER/REPO" \
     --cert-identity \
     "https://github.com/OWNER/REPO/.github/workflows/ci.yml@refs/heads/main"
   ```

3. Verify the SBOM attestation by adding `--predicate-type` with the SBOM's predicate type (the default is the SLSA provenance
   type).[^gh-attest-verify]
4. Negative test: repeat step 2 with `@refs/heads/other` in `--cert-identity`. Verification must fail.
5. Tamper test: build a different image locally, push it under the same tag, and verify by tag. Verification must fail because no
   attestation exists for the new digest.

**Expected output (pattern):** step 2 succeeds and names the workflow; steps 4 and 5 exit non-zero. **Validation:** your deploy job
runs the same check and stops on failure. **Troubleshooting:** "no attestations found": the tag now points at another digest, or
(with `--bundle-from-oci`) `push-to-registry` was off; an authentication error: no registry login. **Teardown:** delete the package
versions and repository; public log entries remain.

### Use cases and scenarios

Three sourced cases, two labelled illustrations.

**Case 1 (sourced): a CI dependency leaks secrets into logs.** *Problem:* in March 2025 the GitHub Action tj-actions/changed-files
was compromised; CISA said the compromise allowed "information disclosure of secrets" and was "potentially enabled by a compromise of
another GitHub Action, reviewdog/action-setup@v1". Affected runs fell between 12 and 15 March 2025, and CISA told users to rotate
the exposed secrets (the [worked runbook](#worked-runbook-a-compromised-pipeline-dependency) follows its advice).[^cisa-tj] *Method:* pin
actions by full SHA or lockfile,[^gh-hardening] give jobs that run third-party steps no secrets, use OIDC instead of stored keys,
keep CI logs private. *Trade-off:* pinned steps need an update bot proposing reviewed SHA bumps. *Success criteria:* within two
hours you can list which runs used which commit and which secrets they could read. *Pitfall:* rotating secrets without checking
whether they were used meanwhile.

**Case 2 (sourced): the security scanner is the attack vector.** *Problem:* on 19 March 2026 an attacker force-pushed tags in the
`trivy-action` and `setup-trivy` repositories and published a malicious Trivy v0.69.4; Aqua contained it the same evening, and
malicious Docker Hub images followed on 22 March. Almost all `trivy-action` version tags were rewritten; the advisory gives two
reasons why one was not (it already pointed at the commit the attacker built on; immutable releases) and warns that GitHub's
"Immutable" badge "does not prevent tag force-pushing". Its instruction: "all secrets accessible to affected pipelines must be
treated as exposed and rotated immediately".[^aqua-trivy] *Method:* as Case 1, and treat security tools as untrusted code.
*Trade-off:* security teams like to give their own tools broad access; this case argues the opposite. *Success criteria:* a
compromised scanner can read source but reach no secret, write token or signing identity.

**Case 3 (sourced): "are we running the backdoored version?"** *Problem:* on 29 March 2024 CISA reported malicious code in XZ Utils
5.6.0 and 5.6.1 (CVE-2024-3094) and advised downgrading to an uncompromised version such as 5.4.6.[^cisa-xz] *Method:* with an SBOM per
deployed artefact, the question becomes a query across stored SBOMs; without them, teams inspect images by hand.
*Trade-off:* SBOMs did not *prevent* this: a malicious release is not a known vulnerability until disclosed. Their value is
response speed. *Success criteria:* the list of affected deployed artefacts within hours; the
fix tracked by [VulnerabilityOps](../vulnerabilityops/) and rolled out via [PatchOps](../patchops/) or an image rebuild.

::: example Illustrative scenario: a payments team adopts proportionate gates
A payments team of 12 engineers turned on four scanners in blocking mode at once; within three weeks most blocks were overridden.
They switched to report-only, measured each rule's effective false-positive rate, re-enabled six rules as blocking and twenty as
review comments, and baselined old findings. Success criteria: overrides under 5% a month, median added time under 4 minutes, no
growth in the high-severity backlog. Numbers invented; the method is Playbook 3.
:::

::: example Illustrative scenario: an EU device maker prepares for the Cyber Resilience Act
A connected-device maker selling into the EU needs, from 11 December 2027, "a software bill of materials in a commonly used and
machine-readable format covering at the very least the top-level dependencies".[^eu-cra-text][^eu-cra] The team generates, attests
and archives one with every firmware build (SSDF PS.3)[^ssdf-pdf] and checks the 2026 minimum fields.[^sbom-2026] Scope is for
[ComplianceOps](../complianceops/) and counsel; this is not legal advice.
:::

## Measure, govern, pay

### Metrics and KPIs

No standards body defines DevSecOps metrics. This Atlas proposes the set below around two questions: are fewer exploitable defects
reaching production, and is the security machinery trusted and cheap enough to keep? Watch [DevOps](../devops/) delivery metrics
alongside them.

| KPI | Definition and formula | Data source | Pitfall | Review |
|---|---|---|---|---|
| Effective false-positive rate (per rule) | Findings dismissed or overridden without a fix ÷ findings triaged, last 30 days[^tricorder] | Findings store | Bulk dismissals by scripts | Monthly, per rule |
| Gate override rate | Blocked merges that were bypassed or broke glass ÷ blocked merges | CI and Git platform audit log | Overrides hidden as "re-run until green" | Monthly |
| New-finding fix time | Median and 90th percentile, first detection to verified fix, for findings introduced by a change | Findings store | Mixing in baseline debt | Monthly |
| Escape rate | Post-release security defects a pipeline control class could have caught ÷ all post-release security defects | Root-cause tags on incidents, pen tests, bug bounty (SSDF RV.3) | Fewer escapes can mean less testing | Quarterly |
| Provenance coverage | Production deploys whose artefact passed signature and provenance verification ÷ all production deploys | Deploy logs | Counting "signed" without "verified" | Monthly |
| Pipeline security overhead | Minutes added to the critical path by security steps (median, 90th percentile) | CI timings | Ignoring queue time | Monthly |
| Exception health | Expired-but-open exceptions; exceptions renewed more than once | Exceptions register | Renewals rubber-stamped | Monthly |
| Pinned third-party references | Third-party steps pinned to a full SHA or lockfile ÷ all third-party steps | Workflow files; OpenSSF Scorecard Pinned-Dependencies[^scorecard] | Pinned but never updated | Quarterly |
| Secret exposure and rotation | Secrets that reached a remote per month; median time from detection to rotation | Secret-scanning alerts | Counting detections, not rotations | Monthly |

**Leading and lagging.** Override rate, effective false positives, pinning and overhead lead; escape rate and fix time lag. The
*baseline* backlog belongs to [VulnerabilityOps](../vulnerabilityops/).

**Worked calculation (illustrative numbers for one month at one organisation).**

| Input | Value |
|---|---|
| Pull requests merged | 1,240 |
| Pull requests blocked by the gate | 96 |
| Blocked pull requests overridden | 22 |
| Findings triaged for SAST rule `X-17` | 140, of which 61 dismissed without a fix |
| Production deployments / verified before deploy | 412 / 398 |
| Post-release security defects / of a class a pipeline control should catch | 9 / 3 |

- Gate override rate = 22 ÷ 96 = **22.9%**. Far above a 5% target: the gate is not trusted.
- Effective false-positive rate of `X-17` = 61 ÷ 140 = **43.6%**. Far above the 10% bar for review comments (Playbook 3), so it
  should neither block nor comment; demote it to a backlog report and tune it.
- Provenance coverage = 398 ÷ 412 = **96.6%**. The 14 unverified deployments came from one legacy pipeline: a named, fixable gap.
- Escape rate = 3 ÷ 9 = **33.3%**. Root-cause the three: was the control absent, mis-tuned, or overridden?

Together: the override rate and one noisy rule explain most of the friction; the fix is tuning, not more tools.

**Target setting.** Compare each service with its own history; use [Playbook 3](#core-practices-and-playbooks)'s thresholds for
rule confidence; agree overhead budgets with teams (for example, five added minutes at the 90th percentile).

**Vanity metrics and traps.** Scans run, findings found and "repositories with a scanner" rise with effort, not outcomes; "zero
critical vulnerabilities" rewards suppressing or not looking.

**Data limitations.** Unstable fingerprints inflate "new" findings; dismissal reasons depend on culture; escapes depend on how hard
you look after release. State these limits on every dashboard.

### Quality, security, privacy and compliance

**Least privilege for pipeline identities.** Bind every cloud role to the narrowest OIDC subject, a protected branch or a protected
environment (settings in the [reference implementation](#reference-implementation)), never a repository
wildcard.[^gh-oidc-ref][^gh-oidc-aws] GitHub repositories
created after 15 July 2026 use an immutable default subject that includes owner and repository IDs
(`repo:OWNER@OWNER-ID/REPO@REPO-ID:…`); older repositories keep the previous format unless they opt in (which changes the expected `sub`), and GitHub Enterprise Server does not offer it.[^gh-oidc-ref]

**Separation of duties.** OWASP's A03:2025 guidance says no single person should be able to write code and push it to production
without oversight;[^owasp-a03] SLSA's Source L4 is "Two-party review" of changes to protected branches.[^slsa-source] Recorded peer
review in the pull request usually meets this without a separate approval board (evidence on change approval: [DevOps](../devops/)).

**Privacy side effects of signing.** Keyless signing writes events to Rekor "so that the events are publicly
auditable".[^sigstore-sign] Email-based identities put the user's email in the certificate,[^fulcio-oidc] which cosign warns "will be
stored in public transparency logs and cannot be removed later".[^cosign-readme] Sign from CI with workload identity, not personal
accounts; GitHub's private-repository Sigstore instance has no transparency log.[^gh-attest-concept]

**Secrets in security output.** Secret-scanning reports contain the secrets they found: redact (Gitleaks `--redact`)[^gitleaks] and
treat SARIF artefacts as confidential.

**Evidence retention.** SSDF PS.3 is "Archive and Protect Each Software Release", including integrity and provenance data.[^ssdf-pdf]
Keep SBOMs, attestations and gate decisions for at least each release's supported life. Under the EU CRA the SBOM need not be
published, but market-surveillance authorities can request it.[^eu-cra-text]

**Regulation and jurisdiction.** The CRA is binding EU law for in-scope products with digital elements: reporting obligations have
applied since 11 September 2026; the main obligations, including the SBOM, apply from 11 December 2027.[^eu-cra] In the US, the SSDF
maps its practices to Executive Order 14028 §4e,[^ssdf-project] but OMB's M-26-05 (January 2026) rescinded M-22-18, the 2022
memo behind the federal attestation form, and its companion M-23-16; agencies now set risk-based requirements and may still ask for
the form or an SBOM, so read your contracts.[^omb-m2605] This is not legal advice; scope and evidence mapping belong to
[ComplianceOps](../complianceops/).

### Cost, capacity and sustainability

**Cost drivers.** Developer triage time (dominant when rules are noisy), runner minutes, licences, AppSec and champion time,
evidence storage, and the cost of delay when gates block.

**Worked example (illustrative).** In the metrics example, rule `X-17`'s 61 dismissed findings at 15 minutes each cost about 15
developer-hours a month and changed nothing. Security steps adding a median 3.2 minutes to 1,240 pull requests use about 3,968
runner-minutes (66 hours) a month; whether that matters depends on whether people wait. Tuning `X-17` is the cheapest improvement.

**Capacity.** Size AppSec by the rules and teams it supports, not by findings: a team that triages every finding grows its workload
with every developer hired.

**Efficiency and sustainability.** Scan only what changed in pull requests where the tool supports it; run full image scans on
main and on a schedule; cache vulnerability databases; skip documentation-only changes. Fewer, better rules use less compute and
attention.

### Assurance of the operating model

- **Quarterly trace audit.** Trace ten random production deployments from the running digest back to provenance, SBOM, gate
  decision, review and commit. Any break in the chain is a finding.
- **Exception audit.** Sample ten exceptions: owner, a reason a peer would accept, ticket, expiry, and was the expiry honoured?
- **Pipeline compromise drill.** Run the [worked runbook](#worked-runbook-a-compromised-pipeline-dependency) as a tabletop twice a year.
- **Adversarial test.** In a sandbox, try poisoned pipeline execution (a same-repository pull request that edits the workflow, a
  build script or an ignore file) and check that it reaches no secret and cannot pass the gate unreviewed.[^owasp-cicd]
- **Risk indicators to watch:** rising override rate; exceptions renewed repeatedly; new unpinned third-party steps; new
  organisation-level secrets; scanners silently failing (zero findings for weeks is a signal, not a success).

## When things go wrong

### Common failures and troubleshooting

| Symptom | Likely cause | Diagnosis | Remediation | Prevention |
|---|---|---|---|---|
| Developers routinely bypass the gate | Noisy rules block merges | Override rate per rule | Demote rules above the confidence bar; baseline old findings | Measure effective false positives before blocking |
| Every pull request fails on old findings | No baseline, or unstable fingerprints | Compare fingerprints across two runs of main | Baseline main; prefer stable `partialFingerprints`[^gh-sarif] | Baseline at rollout |
| Builds fail overnight with no code change | New CVE for an existing dependency; gate blocks old findings | Is the finding new to this change? | Block only findings the change introduced; queue the rest | Separate "new in diff" from "newly disclosed" |
| Deploy verification fails after a CI refactor | Workflow file renamed, so the certificate identity changed | Compare the expected and actual SAN | Update the expected identity through a reviewed change | Keep expected identities in versioned config |
| A secret leaked despite push protection | Bypass with "I'll fix it later", or the secret was in logs or an image layer | Open push-protection alerts; image history | Rotate first[^gh-push] | Delegated bypass; scan images and logs |
| A cloud role was assumed from an unexpected branch | Wildcard `sub`, or an environment with no branch limit | Cloud audit log for web-identity role assumptions | Narrow the condition; limit the environment to `main` | Lint trust policies and environment settings |
| The SBOM misses components | Generated from source manifests, not the built image | Compare component counts with an image-based SBOM | Generate from the artefact after build[^syft] | Make SBOM generation part of the main-branch pipeline |

### Worked runbook: a compromised pipeline dependency

**Scenario (pattern of the 2025 and 2026 incidents in [use cases](#use-cases-and-scenarios)):** an advisory says a third-party CI
step used by your organisation ran malicious code between two timestamps. Owner: platform on-call, with AppSec; the
[IncidentOps](../incidentops/) lead coordinates, and [ResponseOps](../responseops/) takes over the security workstream if attacker
activity is found.

::: figure Response flow for a compromised third-party CI step
```mermaid
flowchart TB
  A["Advisory: step X malicious between T1 and T2"]:::risk
  B["Contain: block step X, pause deploys from affected repos"]:::core
  C["Scope: which runs used X in the window, and which commit"]:::core
  U["Unknown run: log gone, never counted as unaffected"]:::risk
  D{"Could those jobs read secrets or roles?"}:::decision
  E["Rotate every reachable secret, revoke tokens"]:::core
  F["Hunt: audit logs, role use, artefacts built in window"]:::core
  G["Recover: pin a reviewed SHA or remove X, rebuild artefacts"]:::core
  H["Learn: zones, allow-list, lockfile, drill"]:::neutral
  A --> B --> C
  C -->|"advisory SHA in log"| D
  C -->|"log expired"| U
  U --> D
  D -->|"yes"| E --> F --> G
  D -->|"no"| G
  G --> H
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
```
Text equivalent: after an advisory that step X was malicious between two times, contain (block X, pause deploys), then scope which
runs used X and at which commit. Runs whose log contains the advisory's SHA, and unknown runs whose log has expired, lead to one
question: could those jobs read secrets or roles? If so, rotate every reachable secret and hunt through audit logs, role use and
artefacts from the window; otherwise go straight to recovery (pin a reviewed commit or remove X, rebuild). Then learn.
:::

1. **Contain (first 30 minutes).** Block the step organisation-wide, disable affected workflows, pause production deploys from
   affected repositories. Keep logs: they are evidence.
2. **Scope (first 2 hours).** Find every reference to the step, then every run in the window and the action commit it ran. The
   API's `head_sha` is *your* commit, not the action's, so search each run's log for the malicious commit SHA the advisory names.
   Logs are kept for 90 days by default (up to 400 in private repositories),[^gh-retention] so a run whose log is gone counts as
   *unknown*, never as unaffected. A date-filtered run list returns at most 1,000 runs,[^gh-runs-api] so the script asks one day at a
   time and stops at the cap.

   _Example status: syntax-checked with `bash -n`; executed against a stub `gh` (large logs, one missing log, a day at the cap),
   not against GitHub. Replace `example-org`, the repository, the days and the SHA._

   ```bash
   #!/usr/bin/env bash
   set -euo pipefail
   ORG="example-org"
   REPO="$ORG/orders-service"
   STEP="third-party/compromised-action"
   BAD_SHA="<malicious commit SHA from the advisory>"
   gh search code "$STEP" --owner "$ORG" --json repository,path --limit 200 \
     > references.json
   # For each affected repository: every run created in the advisory window (all pages),
   # one day at a time, because a filtered list stops at 1,000 runs.
   FIELDS='.workflow_runs[] | [.id, .name, .head_branch, .event, .created_at] | @tsv'
   : > runs.tsv
   for DAY in 2026-03-19 2026-03-20; do
     gh api --paginate "repos/$REPO/actions/runs?created=$DAY..$DAY&per_page=100" \
       --jq "$FIELDS" > day.tsv
     if [ "$(wc -l < day.tsv)" -ge 1000 ]; then
       echo "$DAY reached the 1,000-run cap: split it into hours" >&2
       exit 1
     fi
     cat day.tsv >> runs.tsv
   done
   # Save each log, then search the file. Do not pipe `gh ... --log` into `grep -q`:
   # grep exits at the first match, gh dies on SIGPIPE, and pipefail reports a miss.
   mkdir -p logs
   : > affected-runs.txt
   : > unknown-runs.txt
   for run in $(cut -f1 runs.tsv); do
     if ! gh run view "$run" --repo "$REPO" --log > "logs/$run.log"; then
       echo "$run" >> unknown-runs.txt       # log expired or unavailable
     elif grep -qF "$BAD_SHA" "logs/$run.log"; then
       echo "$run" >> affected-runs.txt
     fi
   done
   wc -l references.json runs.tsv affected-runs.txt unknown-runs.txt
   ```

3. **Decide reach.** For each affected or unknown run, list the secrets and roles its job could reach (Lab 1's inventory makes this a lookup).
   In a public repository assume anything in logs is public; CISA noted leaked secrets could appear as a double-encoded base64
   payload.[^cisa-tj]
4. **Rotate (same day).** Rotate every reachable secret whether or not you see misuse: CISA said such secrets "should be considered
   compromised".[^cisa-tj] Revoke tokens and deploy keys those jobs used.
5. **Hunt.** Check cloud audit logs for role use in the window and registry pushes by affected runs; rebuild the window's artefacts
   in a clean pipeline and re-verify them.
6. **Recover.** Re-enable workflows with the step pinned to a reviewed commit, or replaced.
7. **Learn.** Blameless review: which zone rule would have kept secrets out of reach? Adopt a lockfile where available.[^gh-lockfiles]

**Exit criteria:** every reachable secret rotated; no unexplained role use; the window's artefacts rebuilt and verified; the step
pinned or removed; actions owned.

### Anti-patterns and myths

- **Blocking on severity alone.** A critical CVSS score in an unreachable code path blocks a merge the author cannot fix. *Instead:*
  block on confidence, severity, fixability *and* newness; route the rest to tracked work with EPSS and KEV as inputs.[^epss][^kev]
- **"Shift left" as "developers do security alone".** Moving checks earlier without coaching, paved roads and tuned rules moves the
  burden, not the capability; DORA's 2022 data points to culture as the biggest predictor of adoption.[^dora-2022]
- **Exceptions without expiry.** They accumulate into an invisible risk register. *Instead:* maximum duration, owner, ticket, review.
- **"Signed means safe."** A signature proves who built an artefact and from what, not that the code is benign: the malicious code in
  XZ Utils shipped inside versions 5.6.0 and 5.6.1 themselves,[^cisa-xz] and a signature would only have confirmed who published them.
  *Instead:* combine provenance with review and behaviour checks.
- **The unfunded champion.** A title without protected time tends to lapse (Atlas view). *Instead:* protected time and recognition
  ([roles](#roles-ownership-and-raci)).

## Deciding and adopting

### When to use and when not to use

**Use DevSecOps practices** whenever you ship software through an automated pipeline, especially software that handles money,
personal data or safety functions, when customers or regulators ask for SBOMs or attestations, or when many teams share CI/CD.

**When not to (or not yet):**

- **No CI/CD yet.** Pipeline-placed controls need a pipeline. Start with [DevOps](../devops/) basics; add secret scanning and
  dependency updates from day one, the rest once changes flow through automation.
- **Throwaway prototypes.** Secret prevention and dependency updates are enough; a full gate policy costs more than the prototype.
- **As a replacement for expert review.** High-risk designs (authentication, payments, cryptography, safety) still need human design
  review and penetration testing. Scanners find classes of bugs, not design flaws.
- **When you build nothing.** Organisations that only consume SaaS need supplier assurance (contracts, SBOM and attestation
  requests), owned by [ComplianceOps](../complianceops/) and procurement.

**Hidden responsibilities you take on:** owning rules and their tuning; running an exceptions register; governing CI identities and
runners; retaining evidence; keeping scanners themselves patched and pinned; responding when the pipeline is compromised.

### Decision framework

**Decision 1: should this check block a merge?** The tree has two levels. The first question is about the *rule*, measured over
its recent findings (not about the tool); the rest are asked of each *finding* a qualifying rule produces.

::: figure Decision tree: rule-level confidence first, then per-finding questions
```mermaid
%%{init: {"flowchart": {"wrappingWidth": 130, "nodeSpacing": 16, "rankSpacing": 28, "padding": 6}}}%%
flowchart TB
  Q1{"Rule: effective false-positive rate?"}:::decision
  R1["Record only; tune the rule"]:::neutral
  R2["Warn as a review comment"]:::core
  Q3{"Finding: author can fix it in this change?"}:::decision
  R3["Ticket with owner and due date"]:::core
  Q4{"New, and high severity or a secret?"}:::decision
  R6["Warn as a review comment"]:::core
  Q5{"Fast enough for the pull request?"}:::decision
  R4["Block the merge"]:::risk
  R5["Run on main or nightly"]:::neutral
  Q1 -->|"10% or more"| R1
  Q1 -->|"under 10%, not near zero"| R2
  Q1 -->|"near zero"| Q3
  Q3 -->|"no"| R3
  Q3 -->|"yes"| Q4
  Q4 -->|"no"| R6
  Q4 -->|"yes"| Q5
  Q5 -->|"yes"| R4
  Q5 -->|"no"| R5
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
```
Text equivalent: first, for the rule, ask its effective false-positive rate. At 10 percent or more: record only and tune the rule.
Under 10 percent but not near zero: its findings become review comments. Near zero: ask of each finding whether the author can fix it
in this change; if not, open a ticket with an owner and due date. If the author can, and the finding is not both new and high severity (or a secret), warn as a review comment.
If it is, block the merge when the check is fast enough for the pull request; otherwise run it on main or nightly.
:::

The thresholds come from Tricorder;[^tricorder] the rest of the tree is this Atlas's proposal. Consequences: a newly disclosed
vulnerability in an existing dependency never blocks an unrelated change (it becomes a ticket, escalated if listed in KEV[^kev]), and
a check too slow for a pull request moves later instead of being skipped.

**Decision 2: which control to add first.** An Atlas worked example for a web-service team with no OIDC yet, third-party CI steps
referenced by tag, no SBOMs and an existing SAST licence. Scores 1–5 (higher is better); weights sum to 100. Re-score for yours.

| Option | Risk reduced (30) | Confidence (25) | Low effort (20) | Evidence value (15) | Low friction (10) | Weighted score |
|---|---|---|---|---|---|---|
| Pipeline hardening (pinning, token permissions, OIDC) | 5 | 5 | 3 | 3 | 4 | **4.20** |
| Signing, provenance and deploy verification | 3 | 5 | 3 | 5 | 5 | **4.00** |
| Secret prevention (pre-commit and push protection) | 4 | 4 | 5 | 2 | 4 | **3.90** |
| Dependency updates and SCA diff | 4 | 4 | 4 | 3 | 3 | **3.75** |
| DAST baseline in test | 3 | 3 | 2 | 3 | 4 | **2.90** |
| SAST in blocking mode | 3 | 2 | 3 | 3 | 2 | **2.65** |

Weighted score = Σ(weight × score) ÷ 100; for example pipeline hardening = (30×5 + 25×5 + 20×3 + 15×3 + 10×4) ÷ 100 = 4.20. Run
SAST report-only from week one, but *blocking* SAST is usually a poor first investment; hardening the pipeline that holds production
credentials is a good one.

### Maturity model

This is **this Atlas's** maturity model, not an official one. OWASP SAMM (three maturity levels per practice),[^samm-about] BSIMM
(descriptive)[^bsimm] and OWASP DSOMM[^owasp-dsomm] are established external models; use one of them if you need comparability with
other organisations.

::: figure Maturity levels of this Atlas's DevSecOps model
```mermaid
flowchart TB
  L1["Level 1: Ad hoc. Manual reviews, central scans, keys in CI"]:::risk
  L2["Level 2: Visible. Report-only scanners, one findings store, SBOMs"]:::neutral
  L3["Level 3: Proportionate. Gate policy, baselines, expiring exceptions"]:::core
  L4["Level 4: Verified. OIDC, pinned steps, provenance checked at deploy"]:::core
  L5["Level 5: Learning. Escapes feed rules and threat models"]:::core
  S3["2 to 3 often stalls: no AppSec time to tune rules"]:::decision
  S4["3 to 4 often stalls: legacy pipelines without workload identity"]:::decision
  L1 -->|"inventory; secret scanning everywhere"| L2
  L2 -->|"80% on the template; false positives measured"| L3
  L3 -->|"overrides under 5% for a quarter"| L4
  L4 -->|"95% provenance coverage; drills"| L5
  L2 -.- S3
  L3 -.- S4
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
```
Text equivalent: five levels in sequence: 1 ad hoc, 2 visible, 3 proportionate, 4 verified, 5 learning; each arrow carries the main
exit criterion from the table below. Two notes mark typical stalls: from Level 2 to 3, no AppSec time to tune rules; from 3 to 4,
legacy pipelines without workload identity.
:::

| Level | Observable behaviour | Evidence | Exit criteria (to next level) |
|---|---|---|---|
| 1 Ad hoc | Reviews before big releases; central scans of some repositories; cloud keys stored in CI | Spreadsheets of findings | Inventory of repositories, pipelines and CI secrets; secret scanning everywhere |
| 2 Visible | Scanners in report-only mode on the paved-road template; SARIF into one store; SBOM per release | Findings store; SBOM archive | 80% of active repositories on the template; effective false positives measured per rule |
| 3 Proportionate | Written gate policy blocks new, high-confidence, severe findings; baselines; expiring exceptions; champions in most teams | Gate decisions; exceptions register | Override rate under 5% for a quarter; no exception overdue by 30 days |
| 4 Verified | OIDC instead of stored keys; third-party steps pinned; provenance and signer identity verified at deploy; SLSA Build L3 for critical artefacts[^slsa-build] | Deploy verification logs; trust policies | Provenance coverage of 95% or more; pipeline-compromise drill run twice a year |
| 5 Learning | Escapes root-caused and turned into rules or design patterns; rules retired on data; cost per merged change tracked | Post-incident actions; rule changelog | Sustained: escape rate and fix time improve year on year |

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

| Window | Actions | Owner | Milestone and metric |
|---|---|---|---|
| Days 1–30 | Lab 1 inventory of the ten most important repositories; push protection and pre-commit secret scanning; read-only default tokens; deploy secrets moved into branch-limited environments; pinned third-party steps where secrets are held; SBOMs on main; a findings store | Platform + AppSec | Inventory done; pinned-reference ratio measured; first SBOMs archived |
| Days 31–60 | Report-only scanners in the shared template; measure effective false positives; baseline main branches; gate policy and exceptions register; OIDC for two services; champion pilot; threat-model two significant changes | AppSec + champions | Rule catalogue with measured confidence; two services without stored keys |
| Days 61–90 | Promote passing rules to blocking; attest and verify at deploy for pilot services; pipeline-compromise tabletop; first monthly scorecard; plan source controls (two-party review) | AppSec + platform + engineering managers | Override rate under 10% (heading to 5%); provenance coverage for pilots 100%; scorecard reviewed by leadership |

Quick wins: read-only default tokens, pinning, push protection. Dependencies: a shared pipeline template (see
[PlatformOps](../platformops/)) and an owner for CI identities. Longer horizon: SLSA Build L3 for release builds, Source-track
controls, CRA readiness where relevant.

## Toolkit

### Checklists

**Quick start (first week)**

- [ ] Push protection on every repository; bypass limited for production
- [ ] Default CI token permissions set to read-only; each job requests what it needs
- [ ] Third-party steps in secret-holding workflows pinned to full SHAs or a lockfile
- [ ] An inventory of CI secrets and the jobs that can read them
- [ ] SBOM generated for each release build and archived with it

**Preflight for a new service pipeline**

- [ ] Uses the shared pipeline template, at a pinned version
- [ ] No repository- or organisation-level deploy secrets; credentials only in environments
- [ ] Production environment: `main` only, reviewers, self-review prevented, no administrator bypass, no `pull_request_target` job
- [ ] Cloud access by OIDC with a trust policy naming that environment or a protected branch
- [ ] Third-party tools only in read-only jobs (deploy: one pinned credentials action at most); signing only in a job running first-party actions
- [ ] Gate and scanner rules read from the base commit; AppSec code owners for `.ci/`, `.github/workflows/` and scanner ignore files; gate is a required check
- [ ] Threat model done if the service adds a trust boundary or sensitive data

**Release readiness**

- [ ] Artefact built once on the protected branch and identified by digest
- [ ] SBOM and provenance attested; deploy job verifies signer identity and builder
- [ ] No open blocking findings; exceptions valid and unexpired
- [ ] DAST baseline run against the release candidate; confirmed findings triaged

**Routine review (monthly and quarterly)**

- [ ] Effective false-positive rate per rule; noisy rules demoted or tuned
- [ ] Override rate and exception health reviewed; expired exceptions closed or re-decided
- [ ] Pinned-reference ratio and new third-party steps reviewed (quarterly)
- [ ] CI identities, runner registrations and organisation secrets reviewed (quarterly)
- [ ] Every post-release security defect root-caused (SSDF RV.3) and mapped to a control class

**Security and governance audit**

- [ ] Trace audit and exception sample done ([assurance](#assurance-of-the-operating-model))
- [ ] CI audit logs reach the security data platform, with alerts on admin and protection changes
- [ ] Evidence (SBOMs, attestations, gate decisions) retained for the release's supported life

**Pipeline incident readiness**

- [ ] Runbook for a compromised third-party step tested in the last six months
- [ ] Rotation procedure documented for every CI secret, with an owner
- [ ] Ability to list all runs that used a given step and commit within two hours

### Templates

**Exception request (one per finding)**

```text
Finding:        <tool>:<fingerprint>  (<rule id>, <file:line>, severity <x>)
Requested by:   <name>, team <team>          Date: YYYY-MM-DD
Why not fix now: <no fixed version | needs migration | not reachable because ...>
Compensating control: <WAF rule, feature flag off, input validation at ...>
Owner:          <named person>               Ticket: <link>
Expiry:         YYYY-MM-DD (maximum 90 days from grant)
Risk decision:  <approved by champion | approved by AppSec (above threshold)>
Review on expiry: fix, re-decide with new evidence, or escalate to the system owner
```

**Architecture decision record: change to the gate policy**

```text
Title:     Promote rule <id> from warn to block (or demote)
Status:    proposed | accepted | superseded       Date: YYYY-MM-DD
Context:   effective false-positive rate <x%> over <n> triaged findings (period);
           median fix time <t>; findings introduced by changes per month <n>
Decision:  <block | warn | record> at <pull request | build | deploy>
Consequences: expected blocked merges per month <n>; added minutes <m>;
           rollback trigger: override rate above <y%> for one week
Owner:     <AppSec engineer>   Consulted: <champions of affected teams>
```

**Threat model one-pager**

```text
System / change: <name>          Date: YYYY-MM-DD       Participants: <names>
1. What are we working on?      <data-flow sketch link; trust boundaries crossed>
2. What can go wrong?           <one threat per line, with its STRIDE category:
                                 "an attacker who ... can ... because ...">
3. What are we going to do about it?
                                <per threat: mitigate | eliminate | transfer | accept,
                                 with a ticket or an exception>
4. Did we do a good enough job? <review date; open questions; who checked the model>
```

**Quarterly DevSecOps scorecard (one page)**

| Measure | Last quarter | This quarter | Target | Comment |
|---|---|---|---|---|
| Gate override rate | | | under 5% | |
| Rules above 10% effective false positives | | | 0 blocking | |
| New-finding fix time, median / 90th percentile | | | team-set | |
| Provenance coverage of production deploys | | | 95% or more | |
| Expired-but-open exceptions | | | 0 | |
| Pinned third-party references | | | 100% in secret-holding workflows | |
| Post-release security defects / escape rate | | | improving | |
| Security steps, added minutes (90th percentile) | | | team-set | |

### Deep comparisons

| Approach | Where security happens | Strength | Weakness | Choose when |
|---|---|---|---|---|
| Traditional AppSec gate | One review or pen test before release | Expert attention | Late; does not scale | Rare, high-risk releases, alongside DevSecOps |
| DevSecOps (this page) | Placed controls along the pipeline, plus evidence | Fast feedback; scales; evidence | Needs tuning, owners, pipeline hardening | Any team shipping through CI/CD |
| Compliance-driven security | Controls chosen to satisfy an audit | Clear obligations | "Compliant" is not "secure" | Regulated scope, with [ComplianceOps](../complianceops/) |
| Runtime-first security | Detection and response in production | Sees real attacks | Does not stop vulnerable code shipping | Always, alongside ([SOC Ops](../soc-ops/), [SecOps](../secops/)) |

| Control type | Sees | Misses |
|---|---|---|
| SAST | Data flows in source, insecure APIs | Configuration, runtime behaviour, third-party code |
| SCA | Known-vulnerable and unwanted components, licences | Malicious releases until disclosed; your own code |
| DAST | Runtime behaviour, headers, some injections | Code paths not reached by the scan |
| Secret scanning | Credential patterns in code and history | Secrets in images, logs, tickets |
| IaC and policy as code | Misconfiguration before deployment | Drift after deployment ([CloudSecOps](../cloudsecops/)) |
| Signing and provenance | Who built what, from which source | Whether the code is benign |

### FAQ

**Is DevSecOps a role, a team or a practice?** A practice; "DevSecOps engineer" titles have no standard scope. This page recommends
an AppSec enabling team, a platform team and champions ([roles](#roles-ownership-and-raci)).

**We already have a SAST licence. Why not block on everything it finds?** Because developers decide what a false positive is.
Measure each rule against the two bars in [Playbook 3](#core-practices-and-playbooks) before promoting it.

**Does an SBOM make us secure?** No. It makes you *answerable*: after an XZ-style advisory you can list affected artefacts
quickly.[^cisa-xz] Generate it from the built artefact, store it with the release, and check the minimum fields.[^sbom-2026]

**What does "SLSA Level 3" actually mean?** Usually the *Build* track: a hardened build platform where concurrent builds cannot
influence each other and signing material is not accessible to user-defined build steps.[^slsa-req] The *Source* track has its own
levels.[^slsa-source] Ask which track and which artefacts.

**How do DevSecOps and GitOps interact?** GitOps reconciles a cluster to the state in Git ([GitOps](../gitops/)). DevSecOps makes sure
what Git points to is a verified artefact: digests in manifests, signature checks before or at admission
([KubernetesSecOps](../kubernetessecops/)).

## Going further

### Advanced topics and emerging direction

- **Source integrity (approved specification, early adoption).** SLSA v1.2's Source track runs from version control to two-party
  review.[^slsa-source] GitHub's CI dependency lockfiles are a vendor feature in preview.[^gh-lockfiles]
- **SBOM quality, not presence (guidance).** The 2026 minimum elements add fields such as component hashes, licences, SBOM author
  signature and generation context;[^sbom-2026] expect (speculation) SBOM consumers to check them.
- **VEX and reachability (established formats, maturing practice).** Both cut noise;[^cyclonedx][^semgrep] quality varies by
  language and vendor, so verify before using them in gates.
- **AI in the loop (fast-moving, thin independent evidence).** Keep humans accountable for dismissals.
- **Attestations beyond the build (emerging).** Test results, reviews and model provenance as signed in-toto attestations, the
  framework SLSA provenance already uses.[^slsa-prov][^in-toto]

### Learning path

- **Prerequisites:** Git, CI/CD and trunk-based development ([DevOps](../devops/)); basic web security (OWASP Top 10:2025);
  containers ([ContainerOps](../containerops/)).
- **One-day primer:** read this page's first three parts; do Lab 1 on your own pipeline; read the SSDF practice list.[^ssdf]
- **One week:** Labs 2 and 3; threat-model one real change; pin every third-party step in one repository.
- **30 days:** the first 30 days of the [roadmap](#adoption-roadmap-306090-days) on one team; effective false positives for your SAST
  rules; OIDC for one deployment; Lab 4.
- **Advanced specialisation:** supply-chain integrity (SLSA, in-toto, Sigstore); CI/CD attack and defence; policy as code at
  scale; admission control with [KubernetesSecOps](../kubernetessecops/).
- **Portfolio projects:** a sample repository at SLSA Build L3 with deploy-time verification; a tested gate-policy library; a
  before-and-after pipeline threat model.

## Wrap-up

### Executive recap and cheat sheet

| Decide | Do | Measure | Watch out for |
|---|---|---|---|
| What blocks, in writing (gate policy) | Place controls where the fix is cheapest; block only new, high-confidence, severe, fixable findings | Effective false positives; override rate | Blocking on severity alone |
| Who owns findings | Every finding fixed, excepted with expiry, or tuned away | Fix time; exception health | Exceptions without expiry |
| How the pipeline is trusted | Jobs split by trust; no reachable secrets for PR jobs; OIDC bound to branch-limited environments; pinned steps | Pinned-reference ratio; secret rotation time | Tags, wildcards, scanners with super-user access |
| What evidence ships with each release | SBOM from the artefact; provenance; verification at deploy | Provenance coverage | "Signed" without "verified" |
| How you learn | Root-cause escapes; drill the pipeline-compromise runbook | Escape rate | Counting scans, not outcomes |

Next actions: run Lab 1 this week; pin and de-privilege before you buy anything.

### A judgement call: block on new high-severity dependency findings, or warn with a deadline?

**Situation (illustrative).** A product group of eight teams has just rolled out SCA in pull requests. The CISO proposes: "Block any
pull request that introduces a dependency with a known high or critical vulnerability." The engineering director proposes: "Warn,
and fix within 14 days." Both are sensible.

**Assumptions.** Version matching is accurate; there is no reachability analysis for the main language; about one pull request in
six touches dependencies; a fixed version exists for most, not all, flagged vulnerabilities.

**Option A, block.** For: it stops *new* vulnerable components, exactly the debt a gate should stop, and the author who chose the
dependency can usually pick another version. Against: with no fixed version the author cannot comply, so each such block becomes an
exception request; without reachability, some blocks concern code never called.

**Option B, warn with a 14-day deadline.** For: no blocked work. Against: introduced debt is still debt, and deadlines slip.
Google's Tricorder team observed that "warnings shown when building are often ignored",[^tricorder] so a warning without an owner and
a tracked due date is weak.

**Decision process.** Apply the [decision tree](#decision-framework). Version matching passes the rule-level question only if
developers act on its findings even in code never called, the ones they may dismiss without reachability analysis: measure that
first (Playbook 3). If it passes, each finding is high severity, so the open question is fixability. So: **block when a fixed
version exists and the finding is new in this change; otherwise warn, open a ticket with a due date, and require an exception with an owner if the pull request merges.** Escalate to a release block if the vulnerability appears in CISA's KEV catalogue.[^kev]

**Why the attractive choice can be wrong.** "Block everything high" looks strict but turns unfixable findings into routine
overrides, the culture DevSecOps exists to prevent; "warn only" lets the dependency tree rot. **Uncertainty:** the split depends on how
often fixed versions exist in your ecosystem; measure the share of blocks that needed an exception in month one, then revisit.

### Learning objectives and mastery check

After this page you should be able to: **explain** what DevSecOps is and is not, and where it hands off to neighbouring practices;
**select** the position and action (block, warn, record) for a given control; **design** a pipeline with trust zones, workload
identity and deploy-time verification; **operate** a gate with baselines and expiring exceptions; and **evaluate** a DevSecOps
programme with outcome and trust metrics.

**1. A SAST rule finds 50 issues in a month; developers dismiss 9 without fixing. Should it block merges?**

::: answer Show answer
Its effective false-positive rate is 9 ÷ 50 = 18%: above the "essentially zero" bar for blocking and the 10% bar for review
comments (Playbook 3). Record it, sample the dismissals and tune the rule; dismissals, not the tool author, define false positives.
:::

**2. Your pull-request job runs a third-party scanner and has access to a cloud deploy key "just for convenience". What is wrong, and
what is the fix?**

::: answer Show answer
The job runs untrusted input and third-party code, so it belongs in the no-secrets zone; a malicious scanner or pull request exposes
the key, as in the tj-actions and Trivy incidents. Removing it from the job is not enough for a repository or organisation secret:
a same-repository pull request can edit the workflow to read it. Delete the key; deploy from a separate job via OIDC bound to a
protected environment; pin the scanner.
:::

**3. A colleague writes the AWS trust condition `repo:example-org/orders-service:*` "so all our workflows can deploy". Explain the
risk in one sentence and give a better condition.**

::: answer Show answer
The wildcard lets any job in the repository, including pull-request jobs and any branch, assume the production role; use
`StringEquals` on `repo:example-org/orders-service:environment:production` (or the immutable-ID form for repositories created after
2026-07-15), with the environment settings from the [reference implementation](#reference-implementation), so only approved jobs
from `main` qualify.
:::

**4. A pull request adds an exception for its own blocked finding to `.ci/exceptions.json`. Why does the gate still block?**

::: answer Show answer
The reference gate reads its rules from the base commit, so a change cannot approve itself. The exception goes in its own pull
request, which AppSec must approve as code owner of `.ci/`; once it is merged, update the original branch from `main` so the gate
runs against a base that includes it.
:::

**5. Your deploy job verifies that the image "has a valid Sigstore signature". An attacker signs a malicious image with their own
GitHub account. Does verification stop it?**

::: answer Show answer
Not as described: any valid signature passes, an attacker's included. Verification must pin the expected identity and issuer
(`cosign verify` takes `--certificate-identity` and `--certificate-oidc-issuer`[^sigstore-verify]), plus provenance naming the
expected builder. For this page's example:

```text
certificate identity:
  https://github.com/example-org/orders-service/.github/workflows/ci.yml@refs/heads/main
OIDC issuer:
  https://token.actions.githubusercontent.com
```
:::

**6. Scenario: you have 90 days and one AppSec engineer for 15 teams. What do you do first, and what do you deliberately not do?**

::: answer Show answer
First: inventory pipelines and secrets, read-only default tokens, pinned third-party steps, push protection, OIDC for one or two
deployments, SBOMs on main. Then report-only scanners in a shared template, promoting only measured, high-confidence rules. Do not
triage findings centrally for 15 teams, block on every rule, or buy a platform before a gate policy exists. Build champions to scale.
:::

### Conclusion

DevSecOps is easy to describe and easy to get wrong, and the failures recur: gates that cry wolf until people stop listening,
exceptions that never expire, and pipelines that hold the keys to production while running other people's code.

The durable version of the practice has three parts. **Placement:** each control sits where its finding reaches the person who can fix
it, and blocks only when it is usually right, severe and fixable now. **Evidence:** every artefact carries a bill of materials and a
verifiable record of how it was built, checked before it runs. **A pipeline treated as production:** narrow identities, pinned
dependencies, separate trust zones and a rehearsed plan for the day a trusted tool turns hostile. Judge a programme less by the number of
its scanners than by whether its developers still trust the gate.

## Sources and further reading

Sources were first opened on 2026-10-16; each footnote gives the date it was last checked. Details and caveats are in the evidence
ledger (`research/devsecops-ledger.md`).

[^nist-204c]: [SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh](https://csrc.nist.gov/pubs/sp/800/204/c/final) — NIST, March 2022. Checked 2026-10-16.
[^nist-204d]: [SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines](https://csrc.nist.gov/pubs/sp/800/204/d/final) — NIST, February 2024. Checked 2026-10-16.
[^nccoe-1800-44]: [SP 1800-44 (Initial Preliminary Draft), Secure Software Development, Security, and Operations (DevSecOps) Practices](https://csrc.nist.gov/pubs/sp/1800/44/iprd) — NIST NCCoE, July 2025 (obsoleted by a later draft on 2026-03-24). Checked 2026-10-16.
[^dod-execgov]: [Pentagon to Advance DevSecOps Adoption With New Guidance](https://www.executivegov.com/2024/10/dod-devsecops-guidance/) — ExecutiveGov, 2024-10-28 (secondary report; the DoD PDF returned HTTP 403). Checked 2026-10-16.
[^ms-sdl]: [Microsoft Security Development Lifecycle (SDL)](https://www.microsoft.com/en-us/securityengineering/sdl) — Microsoft, undated page. Checked 2026-10-16.
[^siliconangle-2012]: [3 Hot Themes in DevOps This Week](https://siliconangle.com/?p=98866) — SiliconANGLE (Klint Finley), 2012-04-04. Checked 2026-10-16.
[^rugged]: [Rugged Software](https://ruggedsoftware.org/) — Rugged Software project, undated. Checked 2026-10-16.
[^devsecops-org]: [DevSecOps manifesto](https://www.devsecops.org/) — DevSecOps.org, site © 2012–2023. Checked 2026-10-16.
[^ssdf]: [SP 800-218, Secure Software Development Framework (SSDF) Version 1.1](https://csrc.nist.gov/pubs/sp/800/218/final) — NIST, February 2022. Checked 2026-10-16.
[^ssdf-pdf]: [NIST SP 800-218 (PDF)](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-218.pdf) — NIST, February 2022. Checked 2026-10-16.
[^ssdf-project]: [Secure Software Development Framework project](https://csrc.nist.gov/projects/ssdf) — NIST CSRC, undated page. Checked 2026-10-16.
[^ssdf-r1]: [SP 800-218 Rev. 1 (Initial Public Draft), SSDF Version 1.2](https://csrc.nist.gov/pubs/sp/800/218/r1/ipd) — NIST, 2025-12-17 (comments closed 2026-01-30). Checked 2026-10-16.
[^ssdf-pubs]: [SSDF publications](https://csrc.nist.gov/Projects/ssdf/publications) — NIST CSRC. Checked 2026-10-16.
[^omb-m2605]: [M-26-05, Adopting a Risk-based Approach to Software and Hardware Security (PDF)](https://www.whitehouse.gov/wp-content/uploads/2026/01/M-26-05-Adopting-a-Risk-based-Approach-to-Software-and-Hardware-Security.pdf) — US Office of Management and Budget, 2026-01-23. Checked 2026-10-16.
[^slsa-spec]: [SLSA specification](https://slsa.dev/spec/) — SLSA / OpenSSF, version 1.2 (Approved). Checked 2026-10-16.
[^slsa-home]: [SLSA](https://slsa.dev/) — SLSA / OpenSSF (home page; expands the name). Checked 2026-10-16.
[^slsa-v1]: [SLSA v1.0 is now final!](https://slsa.dev/blog/2023/04/slsa-v1-final) — SLSA blog, 2023-04-19. Checked 2026-10-16.
[^slsa-whatsnew]: [What's new in SLSA v1.2](https://slsa.dev/spec/v1.2/whats-new) — SLSA. Checked 2026-10-16.
[^slsa-build]: [SLSA v1.2 Build track basics](https://slsa.dev/spec/v1.2/build-track-basics) — SLSA. Checked 2026-10-16.
[^slsa-req]: [SLSA v1.2 Build: requirements for producing artifacts](https://slsa.dev/spec/v1.2/build-requirements) — SLSA. Checked 2026-10-16.
[^slsa-source]: [SLSA v1.2 Source track requirements](https://slsa.dev/spec/v1.2/source-requirements) — SLSA. Checked 2026-10-16.
[^slsa-prov]: [SLSA v1.2 Build: provenance](https://slsa.dev/spec/v1.2/build-provenance) — SLSA. Checked 2026-10-16.
[^in-toto]: [in-toto](https://in-toto.io/) — in-toto project (CNCF graduated). Checked 2026-10-16.
[^owasp-top10-2025]: [OWASP Top 10:2025](https://top10.owasp.org/2025) — OWASP Foundation, 2025 edition. Checked 2026-10-16.
[^owasp-a03]: [A03:2025 Software Supply Chain Failures](https://top10.owasp.org/2025/A03_2025-Software_Supply_Chain_Failures/) — OWASP Foundation, 2025. Checked 2026-10-16.
[^owasp-api]: [OWASP Top 10 API Security Risks – 2023](https://api-security.owasp.org/editions/2023/en/0x11-t10) — OWASP API Security Project, 2023. Checked 2026-10-16.
[^owasp-cicd]: [OWASP Top 10 CI/CD Security Risks](https://owasp.org/www-project-top-10-ci-cd-security-risks/) — OWASP Foundation (Lab project). Checked 2026-10-16.
[^owasp-dsg]: [OWASP DevSecOps Guideline](https://owasp.org/www-project-devsecops-guideline/) — OWASP Foundation (Incubator project). Checked 2026-10-16.
[^owasp-dsomm]: [OWASP DevSecOps Maturity Model](https://owasp.org/www-project-devsecops-maturity-model/) — OWASP Foundation (Lab project). Checked 2026-10-16.
[^samm-about]: [About OWASP SAMM](https://owaspsamm.org/about/) — OWASP SAMM. Checked 2026-10-16.
[^bsimm]: [BSIMM maturity model](https://www.blackduck.com/services/security-program/bsimm-maturity-model.html) — Black Duck, BSIMM16 (undated page). Checked 2026-10-16.
[^owasp-asvs]: [OWASP Application Security Verification Standard](https://owasp.org/www-project-application-security-verification-standard/) — OWASP Foundation, 5.0.0 listed. Checked 2026-10-16.
[^tm-manifesto]: [Threat Modeling Manifesto](https://www.threatmodelingmanifesto.org/) — Threat Modeling Manifesto working group, undated. Checked 2026-10-16.
[^ms-stride]: [Threats – Microsoft Threat Modeling Tool](https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats) — Microsoft Learn, updated 2026-03-04. Checked 2026-10-16.
[^champions]: [Security Champions Manifesto](https://securitychampions.owasp.org/manifesto/) — OWASP Security Champions Guide project, undated. Checked 2026-10-16.
[^devguide-champions]: [Security champions guide](https://devguide.owasp.org/en/08-culture-process/02-security-champions/02-security-champions-guide/) — OWASP Developer Guide. Checked 2026-10-16.
[^sarif]: [Static Analysis Results Interchange Format (SARIF) Version 2.1.0 Plus Errata 01](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) — OASIS, 2023-08-28. Checked 2026-10-16.
[^gh-sarif]: [SARIF support for code scanning](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) — GitHub Docs. Checked 2026-10-16.
[^cyclonedx]: [CycloneDX specification overview](https://cyclonedx.org/specification/overview/) — OWASP CycloneDX (ECMA-424; version 1.7, 2025-10-21). Checked 2026-10-16.
[^cdx-vex]: [Vulnerability Exploitability eXchange (VEX)](https://cyclonedx.org/capabilities/vex/) — OWASP CycloneDX. Checked 2026-10-16.
[^spdx]: [SPDX overview](https://spdx.dev/about/overview/) — SPDX / Linux Foundation, updated 2024-09-13. Checked 2026-10-16.
[^sigstore-sign]: [Signing with cosign: overview](https://docs.sigstore.dev/cosign/signing/overview/) — Sigstore. Checked 2026-10-16.
[^sigstore-verify]: [Verifying signatures](https://docs.sigstore.dev/cosign/verifying/verify/) — Sigstore. Checked 2026-10-16.
[^fulcio-oidc]: [OIDC usage in Fulcio](https://docs.sigstore.dev/certificate_authority/oidc-in-fulcio/) — Sigstore. Checked 2026-10-17.
[^cosign-readme]: [sigstore/cosign README](https://github.com/sigstore/cosign) — Sigstore project. Checked 2026-10-16.
[^gh-attest-concept]: [Artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations) — GitHub Docs. Checked 2026-10-16.
[^gh-attest-howto]: [Using artifact attestations to establish provenance for builds](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations) — GitHub Docs. Checked 2026-10-16.
[^gh-attest-verify]: [gh attestation verify](https://cli.github.com/manual/gh_attestation_verify) — GitHub CLI manual. Checked 2026-10-16.
[^attest-readme]: [actions/attest README](https://github.com/actions/attest) — GitHub. Checked 2026-10-16.
[^cisa-sbom]: [Software Bill of Materials (SBOM)](https://www.cisa.gov/sbom) — CISA. Checked 2026-10-16.
[^sbom-2026]: [2026 Minimum Elements for a Software Bill of Materials (SBOM) (PDF)](https://www.ncsc.govt.nz/assets/guidance/Documents/2026_cisa_sbom_minimum_elements_508c.pdf) — CISA and 17 co-authoring organisations, 2026-07-29 (copy hosted by NCSC NZ). Checked 2026-10-16.
[^eu-cra]: [Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) — European Commission. Checked 2026-10-16.
[^eu-cra-text]: [Regulation (EU) 2024/2847 (Cyber Resilience Act), Annex I Part II](https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng) — EUR-Lex, 2024. Checked 2026-10-16.
[^gh-hardening]: [Security hardening for GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions) — GitHub Docs. Checked 2026-10-16.
[^gh-events]: [Events that trigger workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows) — GitHub Docs. Checked 2026-10-17.
[^gh-envs]: [Deployments and environments](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments) — GitHub Docs. Checked 2026-10-17.
[^gh-prt-env]: [Actions pull_request_target and environment branch protections changes](https://github.blog/changelog/2025-11-07-actions-pull_request_target-and-environment-branch-protections-changes/) — GitHub Changelog, 2025-11-07 (effective 2025-12-08). Checked 2026-10-17.
[^gh-rulesets]: [Available rules for rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets) — GitHub Docs. Checked 2026-10-16.
[^gh-lockfiles]: [Workflow dependency lockfiles](https://docs.github.com/en/actions/concepts/security/workflow-dependency-lockfiles) — GitHub Docs (public preview). Checked 2026-10-16.
[^gh-oidc-concept]: [OpenID Connect](https://docs.github.com/en/actions/concepts/security/openid-connect) — GitHub Docs. Checked 2026-10-16.
[^gh-oidc-ref]: [OpenID Connect reference](https://docs.github.com/en/actions/reference/security/oidc) — GitHub Docs. Checked 2026-10-16.
[^gh-oidc-aws]: [Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws) — GitHub Docs. Checked 2026-10-16.
[^aws-awwi]: [assume-role-with-web-identity](https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html) — AWS CLI Command Reference. Checked 2026-10-17.
[^gh-push]: [About push protection](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection) — GitHub Docs. Checked 2026-10-16.
[^gh-retention]: [Configuring the retention period for GitHub Actions artifacts and logs in your organization](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization) — GitHub Docs. Checked 2026-10-17.
[^gh-runs-api]: [REST API endpoints for workflow runs](https://docs.github.com/en/rest/actions/workflow-runs) — GitHub Docs. Checked 2026-10-17.
[^gh-ghas]: [About GitHub Advanced Security](https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security) — GitHub Docs. Checked 2026-10-16.
[^gh-codeql]: [About code scanning with CodeQL](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) — GitHub Docs. Checked 2026-10-16.
[^cisa-tj]: [Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup@v1 (CVE-2025-30154)](https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction) — CISA, 2025-03-18 (updated 2025-03-26). Checked 2026-10-16.
[^aqua-trivy]: [Trivy supply chain attack: what you need to know](https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know) — Aqua Security, 2026-03-22 (updated 2026-04-01). Checked 2026-10-16.
[^cisa-xz]: [Reported Supply Chain Compromise Affecting XZ Utils Data Compression Library, CVE-2024-3094](https://www.cisa.gov/news-events/alerts/2024/03/29/reported-supply-chain-compromise-affecting-xz-utils-data-compression-library-cve-2024-3094) — CISA, 2024-03-29. Checked 2026-10-16.
[^ghsa-pyyaml]: [GHSA-8q59-q68h-6hv4 (CVE-2020-14343), PyYAML](https://github.com/advisories/GHSA-8q59-q68h-6hv4) — GitHub Advisory Database. Checked 2026-10-16.
[^dora-2022]: [Accelerate State of DevOps Report 2022](https://dora.dev/research/2022/dora-report/) — DORA / Google Cloud, v. 2022-12. Checked 2026-10-16.
[^tricorder]: [Tricorder: Building a Program Analysis Ecosystem](https://research.google/pubs/tricorder-building-a-program-analysis-ecosystem/) — Sadowski et al., ICSE 2015 (full text: [PDF copy](https://www.cs.umd.edu/class/spring2019/cmsc414/papers/tricorder-building-a-program-analysis-ecosystem.pdf)). Checked 2026-10-17.
[^pearce]: [Asleep at the Keyboard? Assessing the Security of GitHub Copilot's Code Contributions](https://arxiv.org/abs/2108.09293) — Pearce et al., arXiv, 2021 (v3 2021-12-16). Checked 2026-10-16.
[^spracklen]: [We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs](https://arxiv.org/abs/2406.10279) — Spracklen et al., USENIX Security 2025 (arXiv v3 2025-03-02). Checked 2026-10-16.
[^trivy]: [aquasecurity/trivy README](https://github.com/aquasecurity/trivy) — Aqua Security. Checked 2026-10-16.
[^trivy-cli]: [trivy image CLI reference](https://trivy.dev/docs/latest/references/configuration/cli/trivy_image/) — Trivy documentation. Checked 2026-10-16.
[^trivy-fs]: [trivy filesystem CLI reference](https://trivy.dev/docs/latest/references/configuration/cli/trivy_filesystem/) — Trivy documentation. Checked 2026-10-17.
[^syft]: [anchore/syft README](https://github.com/anchore/syft) — Anchore. Checked 2026-10-16.
[^anchore-registries]: [Private registries](https://oss.anchore.com/docs/guides/private-registries/) — Anchore open-source documentation, updated 2026-10-16. Checked 2026-10-16.
[^gitleaks]: [gitleaks/gitleaks README](https://github.com/gitleaks/gitleaks) — Gitleaks project. Checked 2026-10-16.
[^semgrep]: [Semgrep documentation](https://docs.semgrep.dev/) — Semgrep. Checked 2026-10-16.
[^semgrep-cli]: [Semgrep CLI reference](https://docs.semgrep.dev/cli-reference) — Semgrep. Checked 2026-10-16.
[^semgrep-ignore]: [Ignore files, folders, and code](https://docs.semgrep.dev/ignoring-files-folders-code) — Semgrep. Checked 2026-10-17.
[^zap]: [ZAP](https://www.zaproxy.org/) — ZAP project ("ZAP by Checkmarx"). Checked 2026-10-16.
[^zap-api]: [ZAP API scan](https://www.zaproxy.org/docs/docker/api-scan/) — ZAP project. Checked 2026-10-16.
[^conftest]: [Conftest](https://www.conftest.dev/) — Open Policy Agent project. Checked 2026-10-16.
[^conftest-opts]: [Options](https://www.conftest.dev/options/) — Open Policy Agent project. Checked 2026-10-17.
[^scorecard]: [OpenSSF Scorecard](https://scorecard.dev/) — OpenSSF. Checked 2026-10-16.
[^kev]: [Known Exploited Vulnerabilities Catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) — CISA. Checked 2026-10-16.
[^epss]: [Exploit Prediction Scoring System (EPSS)](https://www.first.org/epss/) — FIRST. Checked 2026-10-16.

**Further reading**

- [Cloud native security and Kubernetes](https://kubernetes.io/docs/concepts/security/cloud-native-security/) — the Develop,
  Distribute, Deploy and Runtime phases; continue on [KubernetesSecOps](../kubernetessecops/).

**Changelog**

- 2026-10-16 — First edition (research, outline against Blueprint v2, draft, diagrams; Lab 2 executed; validator and browser QA runs).
- 2026-10-17 — Round 1 (`reviews/devsecops-r1.md`): environment OIDC preconditions and plan tiers; fork vs same-repository pull requests; build, scan and attest jobs split; deploy-time verification fixed; gate rules from the base commit; four figures redrawn; API testing; OMB M-26-05 and other fixes.
- 2026-10-17 — Round 2 (`reviews/devsecops-score.md`): runbook log search fixed and tested; deploy exception to the zone rule; all of `.ci/` from the base commit, suppression files code-owned; SBOM-generator and Conftest notes; three figures redrawn; repetition cut.
- 2026-10-17, post-score polish (`reviews/devsecops-polish.md`): `pull_request_target` precondition; runbook stops at the 1,000-run cap; `.ci/` bootstrap; judgement-call assumption; issuer value; figures 6 and 7 redrawn.
