How-To Guide

Automated Security Testing in CI/CD Pipelines

A hands-on guide to embedding SAST, SCA, secret scanning, and container analysis into your CI/CD pipeline without making builds unbearably slow.

Bob
Cloud Security Engineer
6 min read

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:

  1. SAST (Static Application Security Testing) - Analyzes source code for vulnerability patterns.
  2. SCA (Software Composition Analysis) - Checks dependencies for known vulnerabilities and license issues.
  3. Secret Scanning - Detects accidentally committed credentials.
  4. 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 .gitleaksignore for 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.