The Ops Atlas xOps School

Security & Compliance Established term Sources verified

DevSecOps

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.

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

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.[1] 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.

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, SecOps); estate-wide vulnerability prioritisation (VulnerabilityOps); admission and runtime controls in clusters (KubernetesSecOps); cloud posture (CloudSecOps); audit evidence programmes (ComplianceOps)
Prerequisites 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,[1][2] a NIST NCCoE project,[3] DoD guidance[4] and OWASP projects;[5][6] no single normative definition

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,[7] 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). 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”.[8] These are survey correlations: direction, not a business case.

  • 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, ResponseOps and 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.
  • 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).[9] “DevSecOps engineer” is a job title with no standard scope (DevOps discusses such titles).

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)”,[10] 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.[9]
  • 2012: Gartner published a report titled DevOpsSec: Creating the Agile Triangle, as reported at the time.[11]
  • 2022: NIST published the Secure Software Development Framework (SSDF) v1.1 as SP 800-218 (February), mapped to US Executive Order 14028,[12][13] and SP 800-204C (March) on DevSecOps for microservices.[1] DORA made software supply-chain security a primary theme of its 2022 report.[8]
  • 2023–2024: SLSA v1.0, its first stable version, appeared on 19 April 2023.[14] NIST SP 800-204D (February 2024) covered supply-chain security in CI/CD pipelines.[2] The US DoD updated its DoD Enterprise DevSecOps Fundamentals in October 2024 (version 2.5 according to the file name of the linked PDF).[4]
  • 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;[15] in January 2026 the US OMB rescinded M-22-18, the memo behind the federal software attestation form.[16] The OWASP Top 10:2025 widened its supply-chain category, A03, to “all supply chain failures”;[17] SLSA v1.2 added a Source track;[18] CISA and 17 co-authoring organisations replaced the 2021 SBOM minimum elements in July 2026.[19]

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”[20] 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[21][22]
SLSA Supply-chain Levels for Software Artifacts (“salsa”): a specification with a Build track (L0–L3) and a Source track (L1–L4)[23][24] “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[25] 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[26] 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[27] “Not affected: the vulnerable function is never called”
KEV, EPSS CISA’s catalogue of vulnerabilities known to be exploited;[28] FIRST’s estimated probability that a CVE is exploited in the next 30 days[29] Inputs for prioritising known vulnerabilities
Effective false positive A finding the developer chooses not to act on, whatever the tool author thinks[30] 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[31] Reviews threat models for their squad

DevSecOps sits on top of 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 1. Where DevSecOps sits among neighbouring Atlas pages
pipelines to secureimages to scan, signvulnerable components thatshippedsignatures, SBOMs,provenance to verifyIaC rules shared with posturecheckscontrol evidencepipeline-compromiseincidentsDevOpsContainerOpsDevSecOpsVulnerabilityOpsKubernetesSecOpsCloudSecOpsComplianceOpsSOC Ops, SecOps
Diagram source (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 CI/CD, automation, culture Which security controls sit in the pipeline, and how they gate The delivery system and its metrics
VulnerabilityOps SCA findings, CVSS, KEV, EPSS Preventing new vulnerable code Prioritising every vulnerability across the estate to a verified fix
KubernetesSecOps Image signatures, policies Producing signatures, SBOMs and provenance at build time Verifying them at admission; runtime controls
ContainerOps Images Scanning, signing and attesting images Building small, reproducible images
CloudSecOps IaC misconfigurations Catching them in the pull request The deployed cloud control plane and drift
ComplianceOps Evidence Control evidence as a by-product Mapping evidence to frameworks and auditors
SecOps, SOC Ops Security incidents Pipeline compromise and secret leaks Runtime detection and response
ReleaseOps, GitLabOps Release decisions; scanners in one product Security inputs; which findings block The release decision; configuring GitLab’s scanners

Published frameworks and this Atlas’s charter

Link to section “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”.[32] Version 1.2 is a draft (as of October 2026).[33][34]
  • Community manifestos. DevSecOps.org values “Data & Security Science” over fear, uncertainty and doubt;[35] the Threat Modeling Manifesto values “a culture of finding and fixing design issues” over checkbox compliance.[36]

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.

DevSecOps controls fall into seven families. The last column names the SSDF practice each family supports.[32]

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[37]) and a gate policy saying which results block, warn or are recorded.

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 owns. In a pipeline compromise the IncidentOps lead coordinates and ResponseOps takes over if attacker activity is found (worked runbook).

Champions. OWASP’s Security Champions Manifesto lists ten principles, including “Nominate a dedicated captain”, “Reward responsibility” and “Anticipate personnel changes”;[38] OWASP’s Developer Guide says champions “can be the first point of contact for information security teams”.[31] 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), not a central team triaging every finding.

  • 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”).[32]

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). Exceptions above the risk threshold escalate to AppSec; disputes about risk acceptance go to the system owner (RiskOps owns appetite).

Figure 2. The life of a security finding
scanner reportsowner assignedreal and fixablereal, not fixable noweffective false positiverescan shows it goneexpiry date reachedpattern repeatsNewTriagedFixingExceptedDismissedVerifiedRuleTuned
Diagram source (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.

Reference architecture: where each control sits

Link to section “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 shows it on one CI system.

Figure 3. Where each control sits between the editor and runtime
commitsreviewed mergedigest, SBOM, attestationstested release candidateverified digest onlynew rules and tickets1. Editor and pre-commit:secret scan, fast lint rules2. Pull request: SAST onchanged lines, dependencydiff, IaC policy, review3. Build on main: full SCA,image scan, SBOM, sign,provenance4. Test environment: DASTbaseline, API tests5. Deploy: verify signatureand provenance, policy check6. Runtime: admission,monitoring, responseFeedback: incidents and newCVEs
Diagram source (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”[30]
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[39]
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, 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

Link to section “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).[40] 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 4. Trust zones: which job can reach which credential
read-only token, no secretsregistry writedigestSBOM; read-only tokenOIDC signing identity,attestationsattestations checked in thejobOIDC role bound to theenvironmentZone 1, untrusted input:pull-request checks,scanners, third-party stepsFindings (SARIF) and gateresultZone 2, main: build jobpushes the imageZone 2, main: scan job runsthird-party scannersZone 2, main: attest job runsGitHub-owned actions onlyRegistry: image andattestationsZone 3, approvedenvironment: deploy jobverifies, then deploysProduction account
Diagram source (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.[41] 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”),[41] 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.”[42] Zone 1 holds no secrets only if none exist at repository or organisation level.

Rules that follow (each applied in the 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 shows why). The one deliberate exception is deploy’s aws-actions/configure-aws-credentials, from GitHub’s AWS guide[43] 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,[44] 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.[42]
  • 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”.[42] 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”.[45]
  • Replace stored cloud keys with workload identity. With OIDC there are no stored cloud secrets to steal;[25] 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”.[42]

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 5. Happy path from pull request to verified deployment
Deploy jobRegistryCI jobsGit platformDeveloperapprovedverify identityOIDC role,deploypull requestchecks, no secretsfindings, gatefix, review, mergebuild, scan, attestimage, attestationsfetch attestationsbundles
Diagram source (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.
  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).

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?”[36]

  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).[46] 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”);[47] restrict bypass to reviewers (delegated bypass) for production repositories. Plan limits (October 2026): repository push protection “Requires GitHub Secret Protection to be enabled”,[47] available for GitHub Team and Enterprise Cloud, with some features free for public repositories;[48] push protection for users, on by default, covers pushes to public repositories only.[47] 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.[30]
  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.[30]
  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[49]). Newly disclosed vulnerabilities in shipped components are prioritised by 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,[50] names an attacker can register.

Playbook 5: harden the pipeline against the OWASP CI/CD Top 10.[40]

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); alerts on new admins, runners, disabled protections

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

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.[26] For GitHub Actions the exact values are:[52]

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. In test, ZAP’s API scan imports an OpenAPI, SOAP or GraphQL definition and runs an active scan against the URLs it finds,[53] 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.[54] 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.[55]

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

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.

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);[64] GitHub push protection[47] 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”);[65] Semgrep Code[49] 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);[66] Trivy;[67] Semgrep Supply Chain[49] 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)[68] 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)[69] Testable policies, shared libraries Policy sprawl without tests
DAST Test a running app ZAP (“Free and open source”, branded “ZAP by Checkmarx”)[70] Auth handling, API support, CI mode Slow; flaky against unstable test environments
Signing and provenance Integrity evidence Sigstore cosign;[26] GitHub artifact attestations (Build L2 by itself, L3 with reusable workflows)[71] 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;[72] check before scanning confidential code.

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;[73] 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).[50] NIST SP 800-218A adds SSDF practices for developing generative-AI models;[34] securing LLM applications is covered on 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.

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[74]
production environment: required reviewers, self-review prevented, no administrator bypass Unreviewed or self-approved deploys Enterprise Cloud (others: public repositories only)[74]
Artifact attestations Unsigned provenance and SBOMs Enterprise Cloud (others: public repositories only)[75]
Ruleset on main: code-owner review; checks required, from GitHub Actions Merging past a red gate; unreviewed pipeline edits Pro, Team, Enterprise Cloud[76]
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).

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”.[42] 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.[77] 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.[52] 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.[71][78] 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.[79]
  • 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).[80][64][81] 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).[76] 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.[42]
  • Gitleaks scans only the pull request’s commits and exits 0 so the gate, not each tool, decides (it otherwise exits 1 on leaks).[64] 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.[82] The image scan on main is recorded, not gated: the change is already merged.
  • actions/attest follows GitHub’s example,[75] plus artifact-metadata: write, which its README lists for registry storage records;[79] alone it gives SLSA Build Level 2.[71] 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.[78]

Example status: illustrative file; not executed.

# .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.[83]

{
  "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.[83] Its AWS guide also shows a StringLike wildcard such as repo:octo-org/octo-repo:*;[43] 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”,[74] 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).[74]
  • A pull_request_target job passes a main-only limit: since December 2025 GitHub evaluates its environment rules against the default branch.[84] 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”.[74] 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.[83]

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,[69] plus OPA’s object.get built-in.

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

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

Link to section “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.
  5. For each job, ask the four questions of the Threat Modeling Manifesto[36] and check it against the OWASP CI/CD Top 10 (Playbook 5).[40]

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

Link to section “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.[63][37]
  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.

#!/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:

{
  "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
}
{"fingerprints": ["example-sast:sast-c3", "example-sca:sca-e5"]}
{
  "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):

{
  "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}}}]}
      ]
    }
  ]
}
{
  "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.

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

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

Link to section “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.[64][68][66]

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

#!/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.[64] trivy.sarif lists vulnerabilities for the old PyYAML, which should include CVE-2020-14343 (critical below 5.4 in GitHub’s advisory database[85]). 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

Link to section “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, the GitHub CLI, Docker. Side effects: in a public repository, attestations go to a public transparency log[71] whose entries “cannot be removed later”;[86] private repositories need GitHub Enterprise Cloud for attestations.[75] 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[78]) and verify the positive case (replace the placeholders):

    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).[78]

  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.

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 follows its advice).[87] Method: pin actions by full SHA or lockfile,[42] 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”.[88] 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.[7] 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 and rolled out via PatchOps or an image rebuild.

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 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[30] 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[51] 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.

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

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), never a repository wildcard.[83][43] 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.[83]

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;[17] SLSA’s Source L4 is “Two-party review” of changes to protected branches.[56] Recorded peer review in the pull request usually meets this without a separate approval board (evidence on change approval: DevOps).

Privacy side effects of signing. Keyless signing writes events to Rekor “so that the events are publicly auditable”.[26] Email-based identities put the user’s email in the certificate,[52] which cosign warns “will be stored in public transparency logs and cannot be removed later”.[86] Sign from CI with workload identity, not personal accounts; GitHub’s private-repository Sigstore instance has no transparency log.[71]

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

Evidence retention. SSDF PS.3 is “Archive and Protect Each Software Release”, including integrity and provenance data.[32] 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.[60]

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.[15] In the US, the SSDF maps its practices to Executive Order 14028 §4e,[13] 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.[16] This is not legal advice; scope and evidence mapping belong to ComplianceOps.

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.

  • 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 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.[40]
  • 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).
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[63] 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[47] 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[66] Make SBOM generation part of the main-branch pipeline

Worked runbook: a compromised pipeline dependency

Link to section “Worked runbook: a compromised pipeline dependency”

Scenario (pattern of the 2025 and 2026 incidents in use cases): 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 lead coordinates, and ResponseOps takes over the security workstream if attacker activity is found.

Figure 6. Response flow for a compromised third-party CI step
advisory SHA in loglog expiredyesnoAdvisory: step X maliciousbetween T1 and T2Contain: block step X, pausedeploys from affected reposScope: which runs used X inthe window, and whichcommitUnknown run: log gone,never counted as unaffectedCould those jobs read secretsor roles?Rotate every reachablesecret, revoke tokensHunt: audit logs, role use,artefacts built in windowRecover: pin a reviewed SHAor remove X, rebuild artefactsLearn: zones, allow-list,lockfile, drill
Diagram source (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),[89] so a run whose log is gone counts as unknown, never as unaffected. A date-filtered run list returns at most 1,000 runs,[90] 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.

    #!/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.[87]

  4. Rotate (same day). Rotate every reachable secret whether or not you see misuse: CISA said such secrets “should be considered compromised”.[87] 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.[45]

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

  • 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.[29][28]
  • “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.[8]
  • 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,[7] 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).

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 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 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 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 7. Decision tree: rule-level confidence first, then per-finding questions
10% or moreunder 10%, not near zeronear zeronoyesnoyesyesnoRule: effectivefalse-positive rate?Record only; tunethe ruleWarn as a reviewcommentFinding: author canfix it in thischange?Ticket with ownerand due dateNew, and highseverity or asecret?Warn as a reviewcommentFast enough forthe pull request?Block the mergeRun on main ornightly
Diagram source (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;[30] 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[28]), 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.

This is this Atlas’s maturity model, not an official one. OWASP SAMM (three maturity levels per practice),[58] BSIMM (descriptive)[59] and OWASP DSOMM[6] are established external models; use one of them if you need comparability with other organisations.

Figure 8. Maturity levels of this Atlas’s DevSecOps model
inventory; secret scanningeverywhere80% on the template; falsepositives measuredoverrides under 5% for aquarter95% provenance coverage;drillsLevel 1: Ad hoc. Manualreviews, central scans, keysin CILevel 2: Visible. Report-onlyscanners, one findings store,SBOMsLevel 3: Proportionate. Gatepolicy, baselines, expiringexceptionsLevel 4: Verified. OIDC,pinned steps, provenancechecked at deployLevel 5: Learning. Escapesfeed rules and threat models2 to 3 often stalls: no AppSectime to tune rules3 to 4 often stalls: legacypipelines without workloadidentity
Diagram source (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[91] 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
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) and an owner for CI identities. Longer horizon: SLSA Build L3 for release builds, Source-track controls, CRA readiness where relevant.

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)
  • 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

Exception request (one per finding)

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

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

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
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
Runtime-first security Detection and response in production Sees real attacks Does not stop vulnerable code shipping Always, alongside (SOC Ops, 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)
Signing and provenance Who built what, from which source Whether the code is benign

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

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 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.[7] Generate it from the built artefact, store it with the release, and check the minimum fields.[19]

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.[39] The Source track has its own levels.[56] Ask which track and which artefacts.

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

  • Source integrity (approved specification, early adoption). SLSA v1.2’s Source track runs from version control to two-party review.[56] GitHub’s CI dependency lockfiles are a vendor feature in preview.[45]
  • SBOM quality, not presence (guidance). The 2026 minimum elements add fields such as component hashes, licences, SBOM author signature and generation context;[19] expect (speculation) SBOM consumers to check them.
  • VEX and reachability (established formats, maturing practice). Both cut noise;[61][49] 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.[21][22]
  • Prerequisites: Git, CI/CD and trunk-based development (DevOps); basic web security (OWASP Top 10:2025); containers (ContainerOps).
  • One-day primer: read this page’s first three parts; do Lab 1 on your own pipeline; read the SSDF practice list.[12]
  • 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 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.
  • 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.
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?

Link to section “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”,[30] so a warning without an owner and a tracked due date is weak.

Decision process. Apply the decision tree. 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.[28]

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.

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?

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?

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.

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

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?

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[92]), plus provenance naming the expected builder. For this page’s example:

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?

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.

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

Further reading

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.

  1. SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh — NIST, March 2022. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  2. SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines — NIST, February 2024. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  3. SP 1800-44 (Initial Preliminary Draft), Secure Software Development, Security, and Operations (DevSecOps) Practices — NIST NCCoE, July 2025 (obsoleted by a later draft on 2026-03-24). Checked 2026-10-16. ↩︎ ↩︎

  4. Pentagon to Advance DevSecOps Adoption With New Guidance — ExecutiveGov, 2024-10-28 (secondary report; the DoD PDF returned HTTP 403). Checked 2026-10-16. ↩︎ ↩︎

  5. OWASP DevSecOps Guideline — OWASP Foundation (Incubator project). Checked 2026-10-16. ↩︎

  6. OWASP DevSecOps Maturity Model — OWASP Foundation (Lab project). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  7. Reported Supply Chain Compromise Affecting XZ Utils Data Compression Library, CVE-2024-3094 — CISA, 2024-03-29. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  8. Accelerate State of DevOps Report 2022 — DORA / Google Cloud, v. 2022-12. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  9. Rugged Software — Rugged Software project, undated. Checked 2026-10-16. ↩︎ ↩︎

  10. Microsoft Security Development Lifecycle (SDL) — Microsoft, undated page. Checked 2026-10-16. ↩︎

  11. 3 Hot Themes in DevOps This Week — SiliconANGLE (Klint Finley), 2012-04-04. Checked 2026-10-16. ↩︎

  12. SP 800-218, Secure Software Development Framework (SSDF) Version 1.1 — NIST, February 2022. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  13. Secure Software Development Framework project — NIST CSRC, undated page. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  14. SLSA v1.0 is now final! — SLSA blog, 2023-04-19. Checked 2026-10-16. ↩︎

  15. Cyber Resilience Act — European Commission. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  16. 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. ↩︎ ↩︎

  17. A03:2025 Software Supply Chain Failures — OWASP Foundation, 2025. Checked 2026-10-16. ↩︎ ↩︎

  18. What’s new in SLSA v1.2 — SLSA. Checked 2026-10-16. ↩︎

  19. 2026 Minimum Elements for a Software Bill of Materials (SBOM) (PDF) — CISA and 17 co-authoring organisations, 2026-07-29 (copy hosted by NCSC NZ). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  20. Software Bill of Materials (SBOM) — CISA. Checked 2026-10-16. ↩︎

  21. SLSA v1.2 Build: provenance — SLSA. Checked 2026-10-16. ↩︎ ↩︎

  22. in-toto — in-toto project (CNCF graduated). Checked 2026-10-16. ↩︎ ↩︎

  23. SLSA — SLSA / OpenSSF (home page; expands the name). Checked 2026-10-16. ↩︎

  24. SLSA specification — SLSA / OpenSSF, version 1.2 (Approved). Checked 2026-10-16. ↩︎ ↩︎

  25. OpenID Connect — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎

  26. Signing with cosign: overview — Sigstore. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  27. Vulnerability Exploitability eXchange (VEX) — OWASP CycloneDX. Checked 2026-10-16. ↩︎

  28. Known Exploited Vulnerabilities Catalog — CISA. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  29. Exploit Prediction Scoring System (EPSS) — FIRST. Checked 2026-10-16. ↩︎ ↩︎

  30. Tricorder: Building a Program Analysis Ecosystem — Sadowski et al., ICSE 2015 (full text: PDF copy). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  31. Security champions guide — OWASP Developer Guide. Checked 2026-10-16. ↩︎ ↩︎

  32. NIST SP 800-218 (PDF) — NIST, February 2022. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  33. SP 800-218 Rev. 1 (Initial Public Draft), SSDF Version 1.2 — NIST, 2025-12-17 (comments closed 2026-01-30). Checked 2026-10-16. ↩︎ ↩︎

  34. SSDF publications — NIST CSRC. Checked 2026-10-16. ↩︎ ↩︎

  35. DevSecOps manifesto — DevSecOps.org, site © 2012–2023. Checked 2026-10-16. ↩︎

  36. Threat Modeling Manifesto — Threat Modeling Manifesto working group, undated. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  37. Static Analysis Results Interchange Format (SARIF) Version 2.1.0 Plus Errata 01 — OASIS, 2023-08-28. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  38. Security Champions Manifesto — OWASP Security Champions Guide project, undated. Checked 2026-10-16. ↩︎

  39. SLSA v1.2 Build: requirements for producing artifacts — SLSA. Checked 2026-10-16. ↩︎ ↩︎

  40. OWASP Top 10 CI/CD Security Risks — OWASP Foundation (Lab project). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  41. Events that trigger workflows — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎

  42. Security hardening for GitHub Actions — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  43. Configuring OpenID Connect in Amazon Web Services — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  44. assume-role-with-web-identity — AWS CLI Command Reference. Checked 2026-10-17. ↩︎

  45. Workflow dependency lockfiles — GitHub Docs (public preview). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  46. Threats – Microsoft Threat Modeling Tool — Microsoft Learn, updated 2026-03-04. Checked 2026-10-16. ↩︎

  47. About push protection — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  48. About GitHub Advanced Security — GitHub Docs. Checked 2026-10-16. ↩︎

  49. Semgrep documentation — Semgrep. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  50. We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs — Spracklen et al., USENIX Security 2025 (arXiv v3 2025-03-02). Checked 2026-10-16. ↩︎ ↩︎

  51. OpenSSF Scorecard — OpenSSF. Checked 2026-10-16. ↩︎ ↩︎

  52. OIDC usage in Fulcio — Sigstore. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  53. ZAP API scan — ZAP project. Checked 2026-10-16. ↩︎

  54. OWASP Top 10 API Security Risks – 2023 — OWASP API Security Project, 2023. Checked 2026-10-16. ↩︎

  55. OWASP Top 10:2025 — OWASP Foundation, 2025 edition. Checked 2026-10-16. ↩︎ ↩︎

  56. SLSA v1.2 Source track requirements — SLSA. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  57. OWASP Application Security Verification Standard — OWASP Foundation, 5.0.0 listed. Checked 2026-10-16. ↩︎

  58. About OWASP SAMM — OWASP SAMM. Checked 2026-10-16. ↩︎ ↩︎

  59. BSIMM maturity model — Black Duck, BSIMM16 (undated page). Checked 2026-10-16. ↩︎ ↩︎

  60. Regulation (EU) 2024/2847 (Cyber Resilience Act), Annex I Part II — EUR-Lex, 2024. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  61. CycloneDX specification overview — OWASP CycloneDX (ECMA-424; version 1.7, 2025-10-21). Checked 2026-10-16. ↩︎ ↩︎

  62. SPDX overview — SPDX / Linux Foundation, updated 2024-09-13. Checked 2026-10-16. ↩︎

  63. SARIF support for code scanning — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  64. gitleaks/gitleaks README — Gitleaks project. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  65. About code scanning with CodeQL — GitHub Docs. Checked 2026-10-16. ↩︎

  66. anchore/syft README — Anchore. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  67. aquasecurity/trivy README — Aqua Security. Checked 2026-10-16. ↩︎

  68. trivy image CLI reference — Trivy documentation. Checked 2026-10-16. ↩︎ ↩︎

  69. Conftest — Open Policy Agent project. Checked 2026-10-16. ↩︎ ↩︎

  70. ZAP — ZAP project (“ZAP by Checkmarx”). Checked 2026-10-16. ↩︎

  71. Artifact attestations — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  72. Semgrep CLI reference — Semgrep. Checked 2026-10-16. ↩︎

  73. Asleep at the Keyboard? Assessing the Security of GitHub Copilot’s Code Contributions — Pearce et al., arXiv, 2021 (v3 2021-12-16). Checked 2026-10-16. ↩︎

  74. Deployments and environments — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  75. Using artifact attestations to establish provenance for builds — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  76. Available rules for rulesets — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎

  77. Private registries — Anchore open-source documentation, updated 2026-10-16. Checked 2026-10-16. ↩︎

  78. gh attestation verify — GitHub CLI manual. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  79. actions/attest README — GitHub. Checked 2026-10-16. ↩︎ ↩︎

  80. Ignore files, folders, and code — Semgrep. Checked 2026-10-17. ↩︎

  81. trivy filesystem CLI reference — Trivy documentation. Checked 2026-10-17. ↩︎

  82. Options — Open Policy Agent project. Checked 2026-10-17. ↩︎

  83. OpenID Connect reference — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  84. Actions pull_request_target and environment branch protections changes — GitHub Changelog, 2025-11-07 (effective 2025-12-08). Checked 2026-10-17. ↩︎

  85. GHSA-8q59-q68h-6hv4 (CVE-2020-14343), PyYAML — GitHub Advisory Database. Checked 2026-10-16. ↩︎

  86. sigstore/cosign README — Sigstore project. Checked 2026-10-16. ↩︎ ↩︎

  87. Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup@v1 (CVE-2025-30154) — CISA, 2025-03-18 (updated 2025-03-26). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  88. Trivy supply chain attack: what you need to know — Aqua Security, 2026-03-22 (updated 2026-04-01). Checked 2026-10-16. ↩︎

  89. Configuring the retention period for GitHub Actions artifacts and logs in your organization — GitHub Docs. Checked 2026-10-17. ↩︎

  90. REST API endpoints for workflow runs — GitHub Docs. Checked 2026-10-17. ↩︎

  91. SLSA v1.2 Build track basics — SLSA. Checked 2026-10-16. ↩︎

  92. Verifying signatures — Sigstore. Checked 2026-10-16. ↩︎

Related Ops

DevOps

DevOps is a way of organising software delivery so that the people who build a service and the people who run it share goals, tools and feedback, letting changes reach users quickly, safely and repeatedly. It is judged by delivery outcomes, not job titles.

VulnerabilityOps

VulnerabilityOps (usually "VulnOps") runs vulnerability management as a continuous flow: decide what must be fixed by when from exploitation evidence and asset exposure, hand dated work to owners, verify every fix, and keep remediation capacity ahead of arrivals.

KubernetesSecOps

KubernetesSecOps is a rare name for Kubernetes security operations: running the controls that decide who may change a cluster, what may run in it, what may talk to what, and how an attack in progress is noticed and contained.

CloudSecOps

CloudSecOps is running security for a cloud estate at the provider-API layer: guardrails that block unsafe configuration, continuous posture checks, complete audit logs, and detection and response for control-plane activity. The label is emerging and loosely defined.

ComplianceOps

ComplianceOps runs regulatory and contractual compliance as a continuous operation: one library of controls mapped to every framework in scope, evidence collected and tested on a schedule, exceptions with expiry dates, and audits served from records that already exist.

SecOps

SecOps is an ambiguous label. Here it means the working contract between security and IT operations: agreed hardening baselines, one owned remediation queue with deadlines, governed exceptions and joint metrics. Others use it for SOC work or a Google product.

ContainerOps

ContainerOps is this Atlas's name for operating single containers below the orchestrator: building small, traceable OCI images, managing tags, digests and registries, and running containers predictably on a host's runtime, namespaces and cgroups.

ReleaseOps

ReleaseOps is an informal label for turning deployed code into a deliberate, reversible change for users: versioning, cadence, progressive exposure, feature flags, rollback and release records. The exact name is also a ServiceNow product.

Practise and get certified

From xOps School

Practise DevSecOps and get certified

The free DevSecOps track on xOps School has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-17 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)