If your security testing happens after code ships, it is too late. By the time a penetration test finds a SQL injection vulnerability, the code has been in production for weeks, the developer has moved on to other work, and the fix requires context-switching back to a feature they have already mentally closed out.
CI/CD pipelines are the natural place for automated security testing. Every code change already passes through the pipeline. Adding security checks at the right points ensures continuous coverage without requiring developers to change their workflow.
But there is an art to doing this well. Add too many checks, and builds take 30 minutes and developers abandon the pipeline. Add the wrong checks, and you drown in false positives. Here is how to get it right.
The Security Testing Stack
A comprehensive CI/CD security pipeline includes four categories:
- SAST (Static Application Security Testing) - Analyzes source code for vulnerability patterns.
- SCA (Software Composition Analysis) - Checks dependencies for known vulnerabilities and license issues.
- Secret Scanning - Detects accidentally committed credentials.
- Container Scanning - Analyzes container images for OS-level and application-level vulnerabilities.
You do not need all four on day one. Start with secret scanning and SCA (highest signal-to-noise ratio), then add SAST and container scanning as your pipeline matures.
Pipeline Architecture
The key design principle is: fail fast on high-confidence issues, report on everything else.
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐
│ Secret │ │ SAST │ │ SCA │ │ Container │
│ Scanning │ │ (Fast) │ │ Scan │ │ Scan │
│ [BLOCK] │ │ [REPORT] │ │ [BLOCK] │ │ [BLOCK] │
└─────────┘ └──────────┘ └──────────┘ └───────────┘
│ │ │ │
└──────────────┴───────────────┴────────────────┘
│
┌────────┐
│ Deploy │
└────────┘
Secret scanning blocks immediately because there is no acceptable scenario for committing credentials. SCA blocks on critical/high vulnerabilities. SAST reports findings for review but does not block (too many false positives in most tools). Container scanning blocks on critical issues in the final image.
Secret Scanning
This should be your first addition. It is fast, has near-zero false positives for well-configured tools, and catches a category of issue that has no acceptable risk tolerance.
GitHub Actions example with Gitleaks:
name: Security Pipeline
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Gitleaks scans the git history for patterns matching API keys, passwords, private keys, and tokens. It runs in seconds and catches issues that would otherwise reach production.
Configuration tips:
- Scan the full git history on the first run, then scan only new commits incrementally.
- Maintain an allowlist file for false positives (test fixtures, example values).
- Use
.gitleaksignorefor legitimate exclusions rather than disabling rules.
Software Composition Analysis
SCA is the second highest-value addition. It checks your dependency manifests and lock files against vulnerability databases.
GitHub Actions example with Grype:
sca-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
format: cyclonedx-json
output-file: sbom.json
- name: Vulnerability scan
uses: anchore/scan-action@v3
with:
sbom: sbom.json
fail-build: true
severity-cutoff: high
output-format: sarif
- name: Upload results
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif
Key decisions:
- Severity cutoff: Start with blocking on critical only. Once teams are comfortable, lower to high. Blocking on medium from day one creates too much noise.
- Lock file vs. manifest: Always scan lock files for accurate version detection. Manifest-only scans miss transitive dependencies.
- New vs. existing vulnerabilities: Consider only blocking on newly introduced vulnerabilities in PRs, while tracking existing ones separately.
Static Analysis (SAST)
SAST is valuable but noisy. Semgrep is my recommendation for most teams because it has a good balance of detection capability and false positive rate, and the rules are readable and customizable.
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/security-audit
p/owasp-top-ten
generateSarif: true
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
Start with targeted rulesets. Do not enable every rule available. Start with the OWASP Top 10 ruleset and security-audit, which cover the most impactful issues. Add more rulesets as your team builds comfort with the findings.
Run SAST in advisory mode initially. Let it post findings as PR comments without blocking the merge. After a month, review the findings, tune out false positives, and then consider making it blocking for high-confidence rules.
Write custom rules for your codebase. Every organization has patterns specific to their frameworks. A Semgrep rule that detects your internal ORM being used without authorization middleware is more valuable than any generic rule.
Container Scanning
If you deploy containers, scan the final built image before pushing to the registry.
container-scan:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1
- name: Push if clean
if: success()
run: |
docker tag myapp:${{ github.sha }} myregistry.io/myapp:${{ github.sha }}
docker push myregistry.io/myapp:${{ github.sha }}
Scan the final image, not intermediate build stages. The final image is what runs in production.
Performance Optimization
Security checks should add minutes to your pipeline, not tens of minutes.
Run scans in parallel. Secret scanning, SCA, SAST, and container scanning are independent. Run them concurrently.
jobs:
secret-scan:
runs-on: ubuntu-latest
# ...
sca-scan:
runs-on: ubuntu-latest
# ...
sast-scan:
runs-on: ubuntu-latest
# ...
container-scan:
runs-on: ubuntu-latest
needs: [build]
# ...
deploy:
needs: [secret-scan, sca-scan, sast-scan, container-scan]
# ...
Cache vulnerability databases. Tools like Trivy and Grype download vulnerability databases on each run. Cache these between builds.
Scan incrementally where possible. SAST tools like Semgrep support differential scanning that only analyzes changed files.
Set timeouts. If a scan hangs, it should not block your pipeline forever. Set reasonable timeouts (5-10 minutes) and treat timeouts as soft failures that notify security without blocking deployment.
Managing Results
Centralize findings. Use SARIF format to upload results to GitHub Security tab, or pipe them to a vulnerability management platform. Scattered results across tool-specific dashboards are useless.
Deduplicate across tools. Multiple tools may flag the same issue. Your results pipeline should group them.
Track trends, not just counts. The number of findings per scan is less useful than trends: are new issues being introduced faster than old ones are fixed?
How Safeguard.sh Helps
Safeguard.sh provides the SCA layer of your CI/CD security pipeline with minimal setup. It scans dependencies, correlates findings with vulnerability intelligence, enforces severity-based policies through configurable gates, and tracks remediation across your entire project portfolio. Instead of assembling and maintaining multiple open-source scanning tools, Safeguard.sh gives you a single integration point that covers dependency security, SBOM generation, and policy enforcement in your pipeline.