Skip to content
Codeloom
CI/CD

CI/CD Pipeline Security Scanning

Integrate security scanning into CI/CD pipelines: SAST, SCA, container scanning, secrets detection, and policies that block vulnerable code before it reaches production.

·6 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • The four types of security scanning and where each fits in a pipeline
  • Integrating SAST tools like Semgrep and CodeQL into pull requests
  • Using SCA to catch vulnerable dependencies before they ship
  • Container image scanning with Trivy and policy enforcement
  • Secrets detection to prevent credentials from entering the codebase

Prerequisites

None — this post is self-contained.

Security scanning in CI/CD pipelines catches vulnerabilities before they reach production. The idea is simple: run automated security tools at every pull request and block merges that introduce known risks. In practice, the challenge is integrating multiple types of scanning without making the pipeline so slow or noisy that developers ignore the results. This guide covers the four main scanning types and how to wire them into a practical pipeline.

Types of Security Scanning

Each scanning type targets a different attack surface:

SAST (Static Application Security Testing) analyzes your source code for patterns that indicate vulnerabilities: SQL injection, cross-site scripting, path traversal, and other CWE categories. It runs without executing the code.

SCA (Software Composition Analysis) checks your dependency tree against vulnerability databases like the National Vulnerability Database (NVD) and GitHub Advisory Database. It catches known CVEs in third-party packages.

Container Scanning inspects Docker images for vulnerable OS packages, misconfigured permissions, and known CVEs in the base image layers.

Secrets Detection scans code and commit history for accidentally committed credentials: API keys, passwords, tokens, and private keys.

SAST with Semgrep

Semgrep is a fast, open-source SAST tool that supports custom rules alongside its community rulesets. Add it to a GitHub Actions workflow:

name: Security
on: [pull_request]

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/default
            p/owasp-top-ten
            p/javascript
          generateSarif: true
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

The SARIF upload integrates findings directly into GitHub’s Security tab and annotates pull request diffs with inline comments on the affected lines.

For larger codebases, CodeQL provides deeper analysis at the cost of longer scan times:

  codeql:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript, python
      - uses: github/codeql-action/analyze@v3

CodeQL builds a database of your code’s data flow and queries it for vulnerability patterns. It catches issues that pattern-based tools miss, like tainted data flowing through multiple function calls.

SCA for Dependency Vulnerabilities

Dependency scanning is the highest-value, lowest-effort security check. Most breaches exploit known CVEs in unpatched dependencies, not zero-days.

Use npm audit or pip audit for language-specific scanning, or a dedicated tool like Trivy for broader coverage:

  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy for dependencies
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: 1
          format: table

Setting exit-code: 1 makes the job fail when high or critical vulnerabilities are found. This blocks the PR from being merged until the dependency is updated or the finding is explicitly suppressed.

Handling False Positives

Not every CVE applies to your usage of a dependency. Create a .trivyignore file to suppress specific findings with justification:

# CVE-2024-12345: Affects XML parsing, which we don't use
CVE-2024-12345

# CVE-2024-67890: Fixed in our fork, pending upstream merge
CVE-2024-67890

Review suppressions periodically. A suppressed CVE that was irrelevant six months ago may become relevant after a code change.

Container Image Scanning

Container scanning goes beyond dependency scanning by checking the OS packages and configuration of your Docker images:

  container-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Scan image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          severity: HIGH,CRITICAL
          exit-code: 1
          format: sarif
          output: trivy-image.sarif
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-image.sarif

Common findings include outdated base images with known CVEs, packages installed by apt-get that have patches available, and running containers as root. Fix these in your Dockerfile:

# Use a specific, regularly updated base image
FROM node:22-slim AS base

# Create a non-root user
RUN groupadd -r app && useradd -r -g app app

# Install dependencies as root, then switch
COPY package*.json ./
RUN npm ci --omit=dev

COPY --chown=app:app . .
USER app

CMD ["node", "server.js"]

Secrets Detection

Secrets in source code are among the most damaging security issues because they provide direct access to production systems. Scan for them before commits land:

  secrets-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for scanning commits
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Gitleaks scans both the current code and the commit history. The full history scan catches secrets that were committed and then deleted, which are still accessible in Git history.

For pre-commit prevention, add Gitleaks as a client-side hook:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

Building a Complete Security Pipeline

Combine all four scanning types into a single workflow that runs on pull requests:

name: Security Pipeline
on:
  pull_request:
    branches: [main]

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: semgrep/semgrep-action@v1
        with:
          config: p/default

  sca:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          severity: HIGH,CRITICAL
          exit-code: 1

  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  container:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:scan .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:scan
          severity: CRITICAL
          exit-code: 1

All four jobs run in parallel. The PR cannot be merged until all pass. Set branch protection rules to require these status checks.

Reducing Noise

The biggest risk with security scanning is alert fatigue. If every PR triggers dozens of low-severity findings, developers learn to ignore the results. Manage noise by starting with high and critical severity only, expanding to medium once the backlog is clear. Suppress false positives with documented justification. Send findings to a triage queue rather than blocking PRs for informational findings. And track metrics on mean time to remediate so you know whether the pipeline is actually reducing risk.

Key Takeaways

A CI/CD security pipeline combines SAST for code-level vulnerabilities, SCA for dependency CVEs, container scanning for image issues, and secrets detection for leaked credentials. Run all four in parallel on every pull request. Start with high and critical severity thresholds to avoid alert fatigue, and enforce results through branch protection rules. The goal is catching known vulnerabilities automatically so that security reviews can focus on design-level issues that tools cannot detect.