Security that happens once a year, in a document, after the code has shipped, is a report about the past. DevSecOps moves the checks into the place where code actually changes: the pull request, the build, the deploy. Done badly it is a wall of red findings that everyone learns to ignore. Done well it is a set of fast, precise gates that catch the specific bug classes that matter, plus a steady stream of low-severity findings handled like any other technical debt. This module shows which scanners belong at which stage, how to tune them so that a failing check means something, and how to measure whether the whole thing is working.
- Place SAST, secret scanning, dependency scanning, container scanning, IaC scanning and DAST at the right stage of the pipeline
- Configure gates that block on high-confidence, high-severity findings and report the rest
- Manage findings as work: triage, ownership, suppression with reasons, expiry
- Keep pipelines fast enough that security checks are not skipped
- Track the metrics that show whether the program reduces risk
The right check at the right stage
Each class of scanner sees a different kind of problem and costs a different amount of time, so each belongs at a specific point. Fast, precise checks run on every commit; slower or noisier ones run on merge or on a schedule; the slowest run against a deployed environment.
| Stage | Check | Catches |
|---|---|---|
| Pre-commit / PR | Secret scanning, linting, SAST on the diff | Leaked keys, injection patterns, insecure APIs |
| PR | Dependency audit, IaC scanning, licence check | Known-vulnerable packages, public buckets, open ports |
| Build on merge | Container scanning, SBOM, signing, provenance | Vulnerable base images, unsigned artifacts |
| Deploy | Admission policy, config validation | Unsigned or non-compliant workloads |
| Scheduled (nightly/weekly) | Full SAST, DAST against staging, re-scan stored SBOMs | Newly published vulnerabilities, runtime-only bugs |
SAST (static analysis) reads code for dangerous patterns: string-built queries, unsafe deserialisation, hard-coded credentials. DAST (dynamic analysis) attacks a running application over HTTP and finds what only shows at runtime: missing headers, reflected input, authentication issues. SCA (software composition analysis) is dependency scanning. IaC scanners read Terraform and Kubernetes manifests for misconfiguration. Each has blind spots; together they cover most of the OWASP categories before a human reviewer looks.
The DevOps and supply chain modules already put secret scanning, dependency audit, image scanning and signing into the pipeline. This module is about running all of it as a system without slowing delivery down.
Gates that mean something
A check that fails on every pull request trains people to click past it. Block only on findings that are both high severity and high confidence: a critical vulnerability with a fix available, a verified secret, a public bucket, a SQL injection pattern with a taint path from a request parameter. Everything else is reported in the pull request as a comment or in a dashboard, tracked as a ticket with an owner, and fixed on a schedule. Severity thresholds should be explicit in the pipeline configuration, not in someone's head.
name: security
on:
pull_request:
permissions:
contents: read
security-events: write
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Secrets (blocks)
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: SAST on OWASP rules (blocks on ERROR severity only)
run: |
pip install semgrep
semgrep scan --config p/owasp-top-ten --severity ERROR --error .
- name: Dependencies (blocks on fixable high/critical)
run: |
pip install pip-audit
pip-audit -r requirements.txt --strict --ignore-vuln GHSA-xxxx-accepted-until-2026-12
- name: IaC (blocks on the misconfiguration set we agreed)
run: |
pip install checkov
checkov -d infra/ --framework terraform --check CKV_AWS_18,CKV_AWS_19,CKV_AWS_23,CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56 --compact
report:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Full SAST, uploaded as code scanning alerts (does not block)
run: |
pip install semgrep
semgrep scan --config auto --sarif --output semgrep.sarif . || true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarifNote the split: the gate job fails the pull request; the report job never does, and its findings appear as code scanning alerts where they can be triaged. As the backlog shrinks, thresholds tighten. This is the ratchet: start permissive enough that the pipeline is green today, and never let it get looser.
Findings are work, not noise
Every finding gets one of three outcomes: fix (with a pull request), accept (with a reason, an owner and an expiry date), or false positive (with a suppression that names the rule and why). Suppressions live in code next to the finding, are reviewed like code, and expire so that they are reconsidered. A finding with no outcome after its SLA is a process failure that the metrics should show.
import subprocess
# nosemgrep: python.lang.security.audit.subprocess-shell-true
# Justification: `cmd` is a constant from config, never user input; reviewed by @security 2026-06.
result = subprocess.run(cmd, shell=True, check=True, capture_output=True)| Severity | Fix SLA | Where it blocks |
|---|---|---|
| Critical (exploitable, exposed) | 24 hours | PR gate and deploy |
| High | 7 days | PR gate if fix available |
| Medium | 30 days | Report only; tracked |
| Low / informational | Backlog | Report only |
Give every service a security owner and a dashboard of its open findings by age. The conversation changes from "security says" to "our service has three highs older than a week".
Keep it fast
Security checks compete with test suites for a developer's patience. Keep the blocking path under a few minutes: scan only the diff in SAST on pull requests and the whole codebase nightly; cache scanner databases and dependency downloads; run independent scanners in parallel jobs; scan container images once per build by digest rather than per environment. Put DAST, which takes long and needs a deployed target, on a schedule against staging or as a post-deploy check, never in the pull request path.
git fetch origin "$BASE_BRANCH" --depth=1
CHANGED=$(git diff --name-only "origin/$BASE_BRANCH"...HEAD -- '*.py' '*.js' '*.ts' '*.go')
if [ -n "$CHANGED" ]; then
# shellcheck disable=SC2086
semgrep scan --config p/owasp-top-ten --severity ERROR --error $CHANGED
else
echo "no source files changed"
fiDeveloper experience decides whether the program survives. Findings should appear where the developer already is (as pull request annotations, not in a separate portal), say what to change (a fix suggestion beats a CWE number), and be reproducible locally with the same command the pipeline runs. A pre-commit hook for the fastest checks catches most issues before a pull request exists.
Measure the program
Counting findings measures scanner output, not risk. Measure outcomes instead: mean time to remediate by severity, percentage of services with the full gate enabled, age of the oldest open critical, escaped vulnerabilities (found in production or by a pen test that the pipeline should have caught), and pipeline duration for the security stage. Rising remediation time or a growing pile of accepted risks are early warnings; a falling escape rate is the number that proves the investment.
- Coverage: which repositories and images have which checks. Gaps are usually the oldest, most important services.
- Signal quality: the ratio of findings marked false positive. Above a third, tune the rules or change tools.
- Ratchet progress: thresholds tightened per quarter, suppressions expired and re-evaluated.
- Escapes: every production vulnerability gets a retrospective question — which stage should have caught it?
The end state is unremarkable: security checks are simply part of the definition of done, the same as tests and linting, owned by the teams that own the code, with a small central group maintaining the tooling and the thresholds. That is what DevSecOps means when the buzzword is removed.