"Shift left" means running security checks earlier in the development lifecycle — during coding and code review rather than after deployment. The economic argument is straightforward: a vulnerability found during a pull request review costs a code change; the same vulnerability found in production costs an incident response, a patch, a deployment, customer communication, and potentially regulatory notification.
This post covers six categories of security scanning, where each fits in your CI/CD pipeline, how to manage false positives without training developers to ignore findings, and how to measure whether your security pipeline is actually working.
The Six Scanning Categories
1. SAST — Static Application Security Testing
SAST tools analyze source code without executing it. They identify patterns that match known vulnerability classes: SQL injection, cross-site scripting, insecure deserialization, hardcoded credentials, path traversal.
Tool comparison:
| Tool | Language Support | CI Integration | Custom Rules | License |
|---|---|---|---|---|
| Semgrep | 30+ languages | GitHub Actions, GitLab CI, CLI | YAML-based (accessible) | OSS + commercial |
| SonarQube | 25+ languages | Plugins for most CI systems | Java-based | Community + commercial |
| CodeQL | 10+ languages | Native GitHub integration | QL query language (steep learning curve) | Free for OSS, paid for private |
Semgrep has become the default choice for most teams because of its rule syntax. A custom Semgrep rule is a YAML file that most developers can read and write:
rules:
- id: hardcoded-database-password
patterns:
- pattern: |
$DB_CONFIG = {
...,
password: "...",
...
}
message: "Database password is hardcoded. Use environment variables or a secrets manager."
severity: ERROR
languages: [javascript, typescript]
metadata:
category: security
cwe: "CWE-798"
compliance: [pci-dss, soc2]
False positive rates: SAST tools produce the highest false positive rates of any scanning category. Semgrep averages 10–20% false positives with its default rulesets; SonarQube can reach 30–40% without tuning. This is the single biggest factor in developer adoption — if developers learn that most findings are noise, they stop reading the reports.
2. DAST — Dynamic Application Security Testing
DAST tools test running applications by sending crafted HTTP requests and analyzing responses. They find vulnerabilities that SAST cannot: server misconfigurations, authentication flaws, runtime injection vulnerabilities.
Key tools:
- ✓OWASP ZAP: open-source, scriptable, headless mode for CI. The baseline scan runs in 2–5 minutes; the full active scan can take 30+ minutes.
- ✓Burp Suite: commercial, more comprehensive scanning engine, CI integration via Burp Suite Enterprise.
DAST requires a running application, which means it fits later in the pipeline — typically against a staging or preview environment after deployment.
# ZAP baseline scan in GitHub Actions
- name: DAST scan with ZAP
uses: zaproxy/[email protected]
with:
target: "https://staging.example.com"
rules_file_name: "zap-rules.tsv"
fail_action: "warn"
allow_issue_writing: false
The rules_file_name parameter points to a TSV file that configures which alerts cause failures vs. warnings. This is how you manage false positives in DAST — by tuning rule severities rather than suppressing individual findings.
3. Secret Scanning
Secret scanning detects credentials, API keys, tokens, and private keys committed to version control.
Tools:
- ✓Gitleaks: scans git history and current files, configurable via TOML, works as a pre-commit hook and CI check.
- ✓TruffleHog: scans git history with entropy-based detection plus regex patterns, supports scanning multiple VCS providers.
- ✓GitHub Secret Scanning: native integration for GitHub repositories, automatic alerts for partner patterns (AWS keys, Stripe keys, etc.).
The pre-commit hook is the first line of defense — catching secrets before they enter git history:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
But pre-commit hooks run locally and can be bypassed (developers can use --no-verify). The CI check is the enforcement layer:
# GitHub Actions secret scanning
- name: Gitleaks scan
uses: gitleaks/gitleaks-action@v2
with:
args: "--verbose --redact"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_COMMENTS: true
Important: if a secret is detected in git history, rotating the secret is mandatory — removing the commit (via git filter-branch or BFG Repo-Cleaner) is not sufficient because the secret may have been cloned, cached, or logged.
4. SCA — Software Composition Analysis
SCA tools analyze your dependency tree for known vulnerabilities (CVEs). The critical nuance is transitive dependencies — your application might directly depend on 50 packages, but the full dependency tree includes 500+ packages, and vulnerabilities in transitive dependencies are just as exploitable.
Tools:
- ✓Snyk: commercial, covers npm, pip, Maven, Go, container images. Good at suggesting fix versions.
- ✓Dependabot: native GitHub integration, automatic PRs for vulnerable dependencies.
- ✓npm audit / pip-audit: built-in package manager tools, limited to their respective ecosystems.
# Snyk in CI with severity threshold
- name: Snyk dependency test
uses: snyk/actions/node@master
with:
args: --severity-threshold=high --fail-on=upgradable
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
The --fail-on=upgradable flag is significant: it only fails the build when there is a known fix available. Failing on vulnerabilities with no available patch creates build failures that developers cannot resolve, which degrades trust in the pipeline.
5. Container Image Scanning
Container scanners analyze OS packages and application dependencies within container images.
Tools:
- ✓Trivy: fast, covers OS packages + application dependencies, supports multiple output formats.
- ✓Grype: from Anchore, similar coverage to Trivy, good SBOM (Software Bill of Materials) integration.
# Trivy scan in CI
- name: Build Docker image
run: docker build -t app:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: "app:${{ github.sha }}"
exit-code: 1
severity: "CRITICAL,HIGH"
format: "sarif"
output: "trivy-results.sarif"
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: "trivy-results.sarif"
Uploading results in SARIF format integrates findings directly into GitHub's Security tab, providing a unified view of vulnerabilities alongside code scanning results.
6. Infrastructure as Code Scanning
IaC scanning catches cloud misconfigurations before terraform apply or kubectl apply runs.
Tools:
- ✓Checkov: covers Terraform, CloudFormation, Kubernetes, Helm, Dockerfile. 1000+ built-in checks.
- ✓tfsec (now part of Trivy): Terraform-focused, fast, good IDE integration.
# Checkov in CI
- name: Checkov IaC scan
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
framework: terraform
check: CKV_AWS_18,CKV_AWS_19,CKV_AWS_145
soft_fail: false
Pipeline Architecture: Where Each Scan Fits
Not every scan belongs at every stage. The goal is to catch issues as early as possible while keeping the pipeline fast enough that developers don't bypass it.
┌─────────────────────────────────────────────────────────────┐
│ PRE-COMMIT (developer machine) │
│ • Gitleaks (secret scanning) — 2-5 seconds │
│ • Semgrep quick rules (top 20 patterns) — 5-10 seconds │
├─────────────────────────────────────────────────────────────┤
│ PR CHECK (CI, runs on every push to PR) │
│ • Full Semgrep SAST scan — 30-90 seconds │
│ • Snyk/npm audit dependency scan — 20-60 seconds │
│ • Gitleaks (full repo scan) — 10-30 seconds │
│ • Checkov/tfsec IaC scan — 15-45 seconds │
│ • Unit/integration tests — varies │
├─────────────────────────────────────────────────────────────┤
│ MERGE GATE (CI, runs before merge to main) │
│ • All PR checks must pass │
│ • Container image build + Trivy scan — 2-5 minutes │
│ • DAST baseline scan against preview env — 3-5 minutes │
├─────────────────────────────────────────────────────────────┤
│ PRE-DEPLOY (CI, after merge to main) │
│ • Container image signing (cosign) — 30 seconds │
│ • Image digest verification — 10 seconds │
│ • Final vulnerability gate check — 30 seconds │
├─────────────────────────────────────────────────────────────┤
│ POST-DEPLOY (production) │
│ • Runtime security monitoring (Falco/Sysdig) │
│ • Continuous DAST scanning (scheduled) │
│ • Log-based anomaly detection │
└─────────────────────────────────────────────────────────────┘
Key principle: pre-commit and PR checks must be fast. If your security checks add more than 3 minutes to a PR build, developers will find ways to avoid them. Move heavyweight scans (full DAST, comprehensive container scanning) to merge gates where they run less frequently.
False Positive Management
False positives are the primary reason security pipelines fail in practice. A scan that produces 50 findings where 40 are false positives trains developers to ignore all findings.
Strategies:
1. Severity thresholds. Only fail builds on HIGH and CRITICAL findings. Report MEDIUM and LOW as informational.
2. Suppression files with expiration dates. When a finding is reviewed and determined to be a false positive or accepted risk, document the decision:
# .semgrep/ignore.yml
rules:
- id: javascript.express.security.audit.xss.mustache-escape
paths:
- src/templates/admin-panel.js
reason: "Admin panel is internal-only, behind VPN and SSO. Risk accepted by security team on 2025-09-15."
expires: 2026-03-15
approved_by: "security-team"
3. Incremental scanning. On PR checks, only scan changed files. This reduces both scan time and noise. Semgrep supports this with --baseline-commit:
semgrep --config=auto --baseline-commit=$(git merge-base HEAD origin/main)
4. Centralized triage. Route all findings to a security channel (Slack, Jira) rather than blocking individual PRs. Reserve build-breaking for CRITICAL severity findings only.
Developer Experience
The security pipeline must be designed for the developer workflow, not against it.
Speed: total security check time per PR should be under 3 minutes. Parallelize scans that don't depend on each other:
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semgrep
uses: returntocorp/semgrep-action@v1
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Snyk test
uses: snyk/actions/node@master
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
iac:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'infrastructure')
steps:
- uses: actions/checkout@v4
- name: Checkov
uses: bridgecrewio/checkov-action@master
Clarity: when a scan blocks a PR, the developer must understand what the finding is, why it matters, and how to fix it. Semgrep and Snyk both provide fix suggestions in their PR comments. If your tool doesn't, configure it to link to remediation documentation.
Escape hatches: provide a documented process for overriding a finding when there's a legitimate business reason. Require security team approval for overrides and track them centrally.
Metrics: Measuring Security Pipeline Effectiveness
Track these metrics monthly:
| Metric | What It Measures | Target |
|---|---|---|
| Mean Time to Remediate (MTTR) | Average time from vulnerability detection to fix merged | Critical: < 7 days, High: < 30 days |
| Vulnerability Escape Rate | Percentage of production vulnerabilities not caught by CI scans | < 5% |
| Scan Coverage | Percentage of repositories with security scanning enabled | 100% |
| False Positive Rate | Percentage of findings marked as false positive after triage | < 15% |
| Developer Bypass Rate | Percentage of deployments that skipped security checks | 0% |
| Detection by Stage | Distribution of findings across pipeline stages | 70%+ caught at PR stage |
The detection-by-stage metric is the most informative. If most vulnerabilities are caught at the DAST or post-deploy stage, your SAST rules need improvement. If most are caught by pre-commit hooks, your pipeline is working as designed.
Case Study: Fintech API Platform — 5-Stage Security Pipeline
Background
A fintech API platform processing card transactions (PCI-DSS scope) needed to demonstrate a comprehensive security pipeline to their QSA (Qualified Security Assessor). The platform comprised 12 microservices in a Kubernetes cluster, with 40+ deployments per week. The Stripe Systems team designed and implemented a 5-stage security pipeline.
Pipeline Architecture
Stage 1: Pre-Commit
Gitleaks and Semgrep quick rules ran on the developer's machine. The Semgrep configuration targeted the 15 highest-confidence PCI-relevant patterns:
# .semgrep/pci-quick.yml
rules:
- id: pci-hardcoded-card-number
patterns:
- pattern-regex: '\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b'
message: "Possible hardcoded card number detected. Card data must never be stored in source code."
severity: ERROR
languages: [generic]
- id: pci-unencrypted-sensitive-field
patterns:
- pattern: |
$MODEL = {
...,
$FIELD: { type: String, ... },
...
}
- metavariable-regex:
metavariable: $FIELD
regex: (cardNumber|cvv|pan|accountNumber|ssn)
message: "Sensitive field '$FIELD' stored without encryption. Use application-level encryption for PCI-scoped data."
severity: ERROR
languages: [javascript, typescript]
- id: pci-logging-sensitive-data
patterns:
- pattern: |
console.log(..., $DATA, ...)
- metavariable-regex:
metavariable: $DATA
regex: .*(card|pan|cvv|ssn|account).*
message: "Potentially logging sensitive data. PCI-DSS Requirement 3.4 prohibits displaying full PAN in logs."
severity: WARNING
languages: [javascript, typescript]
Stage 2: PR Check
The full Semgrep ruleset (OWASP Top 10 + PCI-specific rules + custom rules) plus Trivy for any Dockerfile changes. All four scans ran in parallel:
# .github/workflows/pr-security.yml
name: PR Security Checks
on:
pull_request:
branches: [main, release/*]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v4
- run: |
semgrep --config=p/owasp-top-ten \
--config=p/secrets \
--config=.semgrep/ \
--baseline-commit=${{ github.event.pull_request.base.sha }} \
--sarif --output=semgrep.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
trivy-fs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
exit-code: 1
severity: "CRITICAL,HIGH"
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- uses: snyk/actions/node@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Stage 3: Merge Gate (DAST)
After PR approval, a merge-triggered workflow deployed the branch to a preview environment and ran a ZAP baseline scan:
dast:
runs-on: ubuntu-latest
needs: [deploy-preview]
steps:
- uses: zaproxy/[email protected]
with:
target: "https://preview-${{ github.event.pull_request.number }}.staging.example.com"
rules_file_name: ".zap/rules.tsv"
cmd_options: >-
-j
-z "-config api.disablekey=true
-config spider.maxDuration=2
-config scanner.maxScanDuration=5"
fail_action: "warn"
- name: Parse ZAP results
run: |
HIGHS=$(cat zap-report.json | jq '[.site[].alerts[] | select(.riskcode == "3")] | length')
if [ "$HIGHS" -gt 0 ]; then
echo "::error::ZAP found $HIGHS high-severity alerts"
exit 1
fi
Stage 4: Pre-Deploy (Image Signing)
After merge, the production image was built, scanned, and signed with cosign:
sign-image:
runs-on: ubuntu-latest
needs: [build, trivy-image]
steps:
- name: Sign container image
uses: sigstore/cosign-installer@v3
- run: |
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
The Kubernetes deployment was configured to verify image signatures via a policy controller, preventing unsigned images from running in production.
Stage 5: Post-Deploy (Runtime Scanning)
Falco monitored runtime behavior in the production cluster:
# falco-rules.yaml
- rule: PCI Sensitive File Access
desc: Detect access to files containing card data
condition: >
open_read and
(fd.name startswith /data/cards or
fd.name startswith /data/transactions) and
not proc.name in (payment-service, encryption-service)
output: >
Unauthorized access to PCI-scoped file
(user=%user.name command=%proc.cmdline file=%fd.name container=%container.name)
priority: CRITICAL
tags: [pci, filesystem]
Metrics Dashboard
After 3 months of operation, the security pipeline produced these results:
| Metric | Value |
|---|---|
| Total vulnerabilities detected | 247 |
| Caught at pre-commit | 31 (12.5%) |
| Caught at PR check | 168 (68.0%) |
| Caught at merge gate (DAST) | 29 (11.7%) |
| Caught at pre-deploy | 12 (4.9%) |
| Caught post-deploy | 7 (2.8%) |
| False positive rate | 11.3% |
| Mean time to remediate (Critical) | 3.2 days |
| Mean time to remediate (High) | 18.7 days |
| Developer bypass rate | 0% |
The 68% detection rate at the PR check stage validated the pipeline architecture — developers received findings in their PR context where remediation was cheapest. The 2.8% escape rate to post-deploy (7 findings) consisted of 4 runtime-specific issues that SAST/DAST couldn't detect and 3 findings in third-party components disclosed after the scan.
The QSA accepted the pipeline documentation, scan results, and metrics dashboard as evidence satisfying PCI-DSS Requirements 6.3 (security vulnerabilities management), 6.5 (secure development), and 11.3 (penetration testing and vulnerability scanning).
Key Implementation Decisions
Why Semgrep over SonarQube: the PCI-specific custom rules were faster to write in Semgrep's YAML format. The team had 15 custom rules operational within 2 days. Equivalent SonarQube custom rules would have required Java development and plugin packaging.
Why baseline scanning for PRs: running Semgrep with --baseline-commit limited findings to code changed in the PR. Without this, every PR in a legacy codebase would inherit hundreds of pre-existing findings, making the reports unusable.
Why ZAP at merge gate, not PR check: the ZAP baseline scan added 3–5 minutes. At the PR stage, this delay would apply to every push. At the merge gate, it runs once per PR — an acceptable tradeoff for DAST coverage.
Why image signing: the fintech platform's PCI assessor specifically asked how they prevent unauthorized container images from running in production. Cosign with a Kubernetes admission controller provided a verifiable chain: the image was built by CI, scanned by Trivy, and signed before deployment. No unsigned image could be scheduled.
Summary
A security pipeline is a layered detection system. No single tool catches every vulnerability. The goal is to construct overlapping layers where each tool's blind spots are covered by another, and the pipeline is fast enough that developers treat it as a natural part of their workflow rather than an obstacle to work around.
Start with secret scanning and SAST at the PR stage — these have the best effort-to-value ratio. Add SCA and container scanning once the first two are stable. DAST and runtime scanning are the final layers, providing defense against vulnerability classes that static analysis cannot reach.
Ready to discuss your project?
Get in Touch →