Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Advanced5 min readUpdated
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

Cheapest and fastest checks earliest. A secret caught pre-commit costs nothing; the same secret found after deploy costs a rotation and an investigation.

Secret scanning

.pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
.github/workflows/security.yml
- 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 --verbose

The 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

pom.xml
<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-project.properties
sonar.qualitygate.wait=true
sonar.newCode.referenceBranch=main

Dependency scanning

pom.xml
<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>
dependency-check-suppressions.xml
<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

scanning steps
- 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: false

Checkov 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

zap-scan.yml
- 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: openapi

DAST 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 existsignore-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:

falco-rule.yaml
- 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: WARNING

Continuous 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?
Secret scanning, comfortably. A leaked credential is immediately exploitable and the tools have a very low false-positive rate. Dependency scanning is second — most real exploitation happens through known CVEs in libraries, not through novel bugs in your code.
How do I stop scanners becoming noise?
Gate on new findings only, and only where a fix exists. A legacy codebase with 400 pre-existing findings blocks every build if you gate on the total; gating on the delta means the codebase improves without stopping delivery. Track the backlog separately with an owner.
What if a secret is committed?
Rotate it immediately — that is the only step that matters. Removing it from history with filter-repo is worth doing but secondary; assume anything pushed was captured. Then check the logs for use of that credential during the exposure window.

Related tutorials