DevSecOps — Securing the Pipeline
Security gates that catch real problems without blocking delivery: pre-commit secret scanning, SAST with FindSecBugs, dependency and container scanning, DAST, and tuning out the noise.
On this page
DevSecOps means the pipeline enforces security rather than a review at the end. The engineering problem is choosing gates that catch real issues without producing so much noise that people route around them.
Key Takeaways
- Secret scanning has the highest return — a leaked credential is immediately exploitable.
- SCA before SAST: most real exploitation comes through known CVEs in dependencies.
- Gate on new findings with an available fix, not on the total.
- DAST runs against a deployed environment and finds what static analysis structurally cannot.
- A noisy gate gets disabled. Tune it or remove it.
Where the gates go
Secret scanning
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks- name: Scan history for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
with:
# Scan the full history on the default branch, not just the diff —
# a secret committed six months ago is still a live credential.
args: detect --redact --verboseThe pre-commit hook is the one that actually prevents leaks; the CI job is the backstop for anyone who skipped it. Both are worth having.
When a secret does reach a remote, rotate first. Rewriting history is worthwhile but secondary — assume anything pushed was captured by a scanner within minutes, because it was.
Static analysis
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
<includeFilterFile>spotbugs-security-include.xml</includeFilterFile>
</configuration>
<executions>
<execution><goals><goal>check</goal></goals></execution>
</executions>
</plugin>FindSecBugs detects the patterns that matter in Java: SQL injection through concatenation, command
injection, path traversal, weak cryptography, hard-coded credentials, XXE, and readObject on
untrusted input. Those are real findings with low false-positive rates, unlike generic style rules.
SonarQube adds a quality gate on new code, which is the setting that makes it usable on an existing codebase:
sonar.qualitygate.wait=true
sonar.newCode.referenceBranch=mainDependency scanning
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<configuration>
<failBuildOnCVSS>7.0</failBuildOnCVSS>
<suppressionFiles>
<suppressionFile>dependency-check-suppressions.xml</suppressionFile>
</suppressionFiles>
<nvdApiKey>${env.NVD_API_KEY}</nvdApiKey>
</configuration>
</plugin><suppress until="2026-10-01Z">
<notes>
CVE-2026-XXXX affects the servlet integration, which we do not use.
Reviewed by security 2026-07-26. Revisit when a patched release ships.
</notes>
<cve>CVE-2026-XXXX</cve>
</suppress>Suppressions must have a reason and an expiry. A suppression file with no dates becomes permanent,
and the CVE nobody re-examined is the one that gets exploited. The until attribute makes it come
back automatically.
This is the highest-value scanner after secrets, because most real-world compromise happens through a known vulnerability in a dependency rather than a novel bug in application code.
Container and IaC scanning
- name: Scan the image
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/acme/order-service:${{ github.sha }}
severity: 'HIGH,CRITICAL'
exit-code: '1'
# Do not block on CVEs with no available fix — that teaches teams to
# disable the scanner, which is worse than the CVE.
ignore-unfixed: true
- name: Scan Terraform and Kubernetes manifests
uses: bridgecrewio/checkov-action@master
with:
directory: infrastructure/
framework: terraform,kubernetes
soft_fail: falseCheckov catches the configuration mistakes that cause breaches: a public storage bucket, an unencrypted volume, a security group open to the world, a container running as root. These are cheap to fix at review time and expensive afterwards.
DAST
- name: ZAP baseline scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.acme.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
- name: ZAP API scan against the OpenAPI spec
uses: zaproxy/action-api-scan@v0.9.0
with:
target: 'https://staging.acme.com/v3/api-docs'
format: openapiDAST runs against a deployed application, so it finds things static analysis structurally cannot: missing security headers, cookies without flags, verbose error pages, endpoints present in the deployment but absent from the code you scanned.
The API scan driven by your OpenAPI spec is the more useful of the two, because it exercises every declared endpoint rather than whatever a crawler happens to reach.
Run DAST nightly rather than per-commit — it takes minutes and needs a deployed environment.
Keeping gates usable
The failure mode is predictable: a scanner produces 300 findings, the team cannot triage them, someone
adds continue-on-error: true, and the gate is decorative.
Three practices prevent it. Gate on the delta — new findings fail the build, existing ones go to a
tracked backlog with an owner. Gate only where a fix exists — ignore-unfixed on container scans,
suppressions with expiry for unpatchable CVEs. And tune the rules — turn off checks that do not
apply to your stack rather than letting people learn to ignore output.
Measure the gates themselves. If a scanner has never caught a real issue in six months, it is costing build minutes and attention for nothing.
Runtime
Pipeline gates check what you ship. Runtime detection catches what happens after:
- rule: Unexpected outbound connection from a Java service
desc: A JVM connecting somewhere outside the expected allowlist
condition: >
outbound and container and proc.name = java
and not fd.sip in (allowed_destinations)
output: >
Unexpected connection (container=%container.name dest=%fd.sip:%fd.sport)
priority: WARNINGContinuous dependency monitoring matters as much. A library that was clean when you shipped it develops a CVE later, and only a service that re-scans deployed artefacts will tell you.
What to take away
Put secret scanning at pre-commit and dependency scanning at pull request — those two catch the majority of real risk. Gate on new findings that have a fix, give suppressions an expiry, and run DAST nightly against staging. Then watch whether each gate ever catches anything, and remove the ones that do not.
Frequently Asked Questions
Which scanner gives the best return?
How do I stop scanners becoming noise?
What if a secret is committed?
Related tutorials
- Penetration Testing for Java AppsA structured approach to testing your own application: reconnaissance, authentication and authorisation testing, injection, business logic flaws, and the tools that help.
- Threat ModellingFinding design flaws before they ship: drawing data flow diagrams, applying STRIDE per element, prioritising with DREAD, and running a session that produces actionable work.
- Testing Spring SecurityWriting security tests that catch real gaps: @WithMockUser and @WithUserDetails, MockMvc request post-processors, testing method security, mock JWTs, and the negative tests that matter.
- Secrets Management & Key SecurityGetting secrets out of configuration: taking an inventory, Vault KV and dynamic database credentials, Kubernetes auth, the External Secrets Operator, and rotation that works.