Security Scanning in CI Pipelines

Integrate security scanning into your CI pipeline to catch vulnerabilities early. This CI/CD foundations lesson covers core concepts, hands-on steps, comparisons, and troubleshooting.

Focus: integrate security scanning into ci pipeline

Sponsored

You've automated tests, built artifacts, and pushed to registries — but your pipeline is still shipping vulnerabilities straight to production. Every merge is an unguarded door. Integrating security scanning into your CI pipeline takes what used to be a quarterly audit and turns it into an automated gate that rejects dangerous code before it reaches your users. This lesson shows you exactly how to weave static analysis, dependency checks, and secret detection into your existing workflow — and why doing it now saves you from a headline later.

The problem this lesson solves

Security on a traditional team is a phase — a checkpoint near the end of the release train, often a checklist item that gets skipped when deadlines squeeze. In a CI/CD world, that approach is a time bomb. You push code every hour, but a vulnerability scan that runs once a month means a flaw can live in production for weeks. Worse, when security is a manual step, it becomes the first thing dropped when things get busy.

Here's the core pain this lesson addresses: security is too slow, too manual, and too late. You need a way to catch vulnerabilities at the same speed your pipeline ships code. Without automation, your team is forever playing catch-up — cleaning up breaches instead of preventing them. The fix is to embed scanning directly into the CI pipeline, where every commit, every pull request, and every merge runs through the same checks automatically. No reminders, no tickets, no human forgetfulness.

Core concept / mental model

Think of your CI pipeline as a factory assembly line. At each station, a worker inspects the product — a unit test checks the gears, a linter checks the finish. A security scan is simply another quality-control station. But instead of testing what your code does, it tests what your code is made of.

Three kinds of stations make up a complete security line:

  • Static Application Security Testing (SAST) — scans your source code for patterns that lead to vulnerabilities (SQL injection, XSS, insecure deserialization). It's a code review with a library of known bad patterns.
  • Software Composition Analysis (SCA) — examines your dependencies and their transitive dependencies against public vulnerability databases (like the GitHub Advisory Database or the National Vulnerability Database). It answers: Do any of the packages I pull in have known CVE?
  • Secret scanning / secret detection — looks for hard-coded credentials, API keys, and tokens that accidentally ended up in your repo. It's the seatbelt that prevents data leaks.

This isn't a replacement for a dedicated security team — it's the first line of defense. A mental model that helps teams adopt this: security shifts left, meaning you move security considerations as early as possible in the development lifecycle. The CI pipeline is the perfect place because it's the choke point every change must pass through.

How it works step by step

Integrating security scanning isn't a one-click magic wand; it's a deliberate sequence you bolt onto your existing pipeline. Here's the logical flow:

  1. Choose your scanners. Based on your tech stack, pick a SAST tool (e.g., Bandit for Python, Semgrep for multi-language), an SCA tool (e.g., pip-audit, Trivy), and a secret scanner (e.g., gitleaks, trufflehog).
  2. Create a dedicated job or step for each scanner in your pipeline configuration. In GitHub Actions, this is a separate step within a job, or even a separate job in the same workflow.
  3. Configure fail thresholds. Decide what severity causes a build failure — critical or high severity should almost always block the merge. Medium/Low can be warnings.
  4. Run on every pull request (event: pull_request) and on push to main. The pull request run gives developers feedback before code merges.
  5. Upload results as artifacts (e.g., SARIF format) so they appear in the GitHub Security tab and are available for later review.
  6. Set up break-glass exceptions — a documented process to allow a vulnerability through with a risk owner and a ticket, rather than an open door.

Cause and effect is clear: every scanner is a gate. If a scanner fails, the pipeline stops, and the developer gets immediate feedback on what to fix. This is the difference between security at the end and security at every step.

Hands-on walkthrough

Let's build a complete security-scanning workflow in GitHub Actions for a small Python project. We'll use three tools: Bandit for SAST, pip-audit for SCA, and gitleaks for secret detection.

1. The workflow file (.github/workflows/security.yml)

name: Security Scans

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      # --- SAST: Bandit ---
      - name: Install Bandit
        run: pip install bandit

      - name: Run Bandit
        run: bandit -r . -f sarif -o bandit.sarif

      # --- SCA: pip-audit ---
      - name: Install pip-audit
        run: pip install pip-audit

      - name: Scan dependencies
        run: pip-audit -r requirements.txt

      # --- Secret scanning: Gitleaks ---
      - name: Install gitleaks
        run: |
          wget https://github.com/gitleaks/gitleaks/releases/download/v8.18.1/gitleaks_8.18.1_linux_x64.tar.gz
          tar -xzf gitleaks_8.18.1_linux_x64.tar.gz
          sudo install gitleaks /usr/local/bin/

      - name: Run gitleaks
        run: gitleaks detect --source . --report-format sarif --report-path gitleaks.sarif

      # --- Upload results as artifacts ---
      - uses: actions/upload-artifact@v4
        with:
          name: security-results
          path: '*.sarif'

Expected output: After a push or pull request, the Security Scans workflow runs. The Run Bandit step will exit non-zero if it finds a high-severity issue (e.g., B605 for SQL injection). Similarly, pip-audit fails if a package has a known vulnerability of any severity, and gitleaks fails if a secret is detected. The upload-artifact step saves the SARIF files for later viewing in the GitHub Security tab.

2. Failing a build — actually try it

# Clone a demo repo (or use your own)
git clone https://github.com/your-name/demo-app.git
cd demo-app

# Add a deliberately bad line to a file
echo "import subprocess; subprocess.call(['ls', '-l'])" >> utils.py

# Commit and push to trigger the pipeline
git add .
git commit -m "test bad code"
git push origin main

Go to the Actions tab in GitHub. You'll see the Security Scans job fail. Click on the failed step (Run Bandit) and read the output: it will show Issue: [subprocess_popen_with_shell_equals_true] with a line number. This is your feedback loop — the pipeline caught the risk before it ever merged.

Pro tip: Start scanners as non-blocking (warnings) in the first sprint so your team can get used to the noise. Then escalate to blocking over the next two weeks. This avoids a sudden wall of failed pipelines while you're still tuning false-positive rates.

3. Making scans non-blocking initially

# run bandit and always continue even if it finds issues
- name: Run Bandit (non-blocking)
  run: bandit -r . -f sarif -o bandit.sarif || true

While you tune rules, || true prevents the pipeline from stopping. Replace it with a proper continue-on-error: true directive when using prebuilt actions.

Compare options / when to choose what

Which scanner should you pick? Here's a quick comparison of the three categories:

Tool Category Best For Pros Cons
Bandit SAST (Python) Python-only projects Fast, simple, zero config, tight security community Only Python, can miss some framework aliases
Semgrep SAST (multi-language) Polyglot teams, custom rules Supports 30+ languages, rule library, can be used as linter too Steeper learning curve for custom rules, heavier runtime
pip-audit SCA (Python) Python dependencies Focused, uses PyPI advisory DB, supports requirements.txt and lockfiles Only Python (use npm audit for Node, trivy for containers)
Trivy SCA/container scanning Container images, IaC Scans images, filesystems, and repos; broad coverage More setup, slower on large repos
Gitleaks Secret scanning Any git-based repo Fast, low false positives, native SARIF output Requires config for custom patterns
GitHub Secret Scanning Secret scanning Native to GitHub Zero setup, alerts on public/private repos Limited to known provider patterns

When to choose what:

  • SaaS only → Use GitHub's built-in dependency graph + Dependabot alerts; add pip-audit for local verification.
  • Self-hosted / on-prem → Choose self-contained tools like bandit + pip-audit + gitleaks running in your runner.
  • Container-heavy → Add trivy as a final step before pushing the image to a registry.

Troubleshooting & edge cases

Even the best pipelines hit snags. Here are common issues and how to fix them.

Bandit fails on code you didn't write (false positive)

Symptom: Bandit flags a function that's actually safe, e.g., subprocess with a constant argument.

Fix: Add a # nosec comment on the exact line with a short reason:

subprocess.call(['ls', '-l'], shell=False)  # nosec B603

If you find yourself adding # nosec everywhere, your scanner config is too aggressive. Exclude noisy paths in the Bandit config file rather than silencing individual lines.

pip-audit fails because the lockfile is out of date

Symptom: The pipeline fails with "requirements.txt is not pinned" or a resolved version has a vulnerability.

Fix: Commit a pip freeze-generated requirements.txt and run pip-audit -r requirements.txt with the -l flag to limit to known fixes. Alternatively, use a full lockfile like pip-tools or poetry.lock for deterministic audit results.

Gitleaks detects secrets in your repository's entire history

Symptom: A secret was committed last month, and now every pipeline fails.

Fix: First, use gitleaks detect --history to see all leaks. For GitHub, enable push protection and rescans. For self-hosted, you must rewrite history (git filter-repo) or accept a one-time cleanup process. Then add a .gitleaks.toml to allowlist known test patterns.

Scanner takes too long and blocks the build

Symptom: The security job adds 10 minutes to every PR.

Fix: Split the security job into its own workflow (the example above), so it runs in parallel to your test job. Or only scan changed files rather than the whole repo:

bandit -r $(git diff --name-only origin/main...HEAD | xargs -I {} echo "{}" | grep '\.py$')

Pipeline is green but no scans ever run

Symptom: The workflow appears, but the log shows "No jobs were run" or "skipped."

Fix: Check your on: triggers. If you only have push on main, pull requests from forks to your repo won't trigger scans. Add pull_request_target for fork PRs — but be careful: pull_request_target runs on the target repo's secrets, so only use it with reviewed actions.

What you learned & what's next

By now, you should be able to explain the core idea behind integrating security scanning into your CI pipeline — it's about embedding automated gates for SAST, SCA, and secret detection at every change. You also completed a practical exercise: you built a GitHub Actions workflow that runs Bandit, pip-audit, and gitleaks, you saw how a deliberately bad commit fails the build, and you learned how to handle common edge cases like false positives and loooong runtimes.

Security scanning is the final guard before your artifact ships, but it's not the last step. Next in this track, you'll learn how to promote artifacts between environments — taking that signed, scanned build and moving it from staging to production with confidence. The gate you just built feeds directly into that, because only artifacts that pass every security check should ever be promoted. Ready to move to the next stage? Let's go.

Practice recap

Take the workflow from this lesson and add a 'medium severity' warning threshold using continue-on-error. Then introduce a deliberately vulnerable dependency (e.g., an older requests version) and observe how pip-audit flags it. Finally, change the trigger to include pull_request_target for fork PRs — and test what happens with a benign change from a fork.

Common mistakes

  • Forgetting to scan transitive dependencies — you fix the direct package but a sub-dependency still has a CVE.
  • Making scanners blocking from day one and drowning the team in failures; tune thresholds first.
  • Scanning only on the main branch and ignoring pull request events, so vulnerabilities slip in during PRs.
  • Using only one scanner type (e.g., SAST) and thinking you're covered — you still need dependency and secret checks.
  • Not committing a lockfile, so pip-audit can't reproduce the exact dependency set and may miss issues.

Variations

  1. Use a pre-built GitHub Action like gitleaks/gitleaks-action or anchore/scan-action instead of installing tools manually.
  2. Adopt a unified security platform like Snyk or SonarQube that combines SAST and SCA in one interface.
  3. Scan container images with Trivy or Grype after building them, as an additional layer beyond source-level scanning.

Real-world use cases

  • A startup catches a critical CVE in its requests dependency during CI, and blocks the release before it goes live.
  • A fintech team integrates gitleaks to prevent hard-coded API keys from leaking into a public monorepo, averting a data breach.
  • An e-commerce platform uses Bandit plus pip-audit on every PR to comply with OWASP ASVS requirements and pass security audits.

Key takeaways

  • Security scanning in CI is a set of automated gates for SAST, SCA, and secret detection that run on every change.
  • A mental model for success: treat scanners like quality-control stations on an assembly line — non-negotiable and parallel.
  • The step-by-step flow is: choose tools → add a security job → set fail thresholds → trigger on PRs and pushes → upload results.
  • Hands-on, you built a GitHub Actions workflow with Bandit, pip-audit, and gitleaks, and saw it fail the build on a vulnerable commit.
  • Tool choice depends on language and platform; GitHub's native features work for small teams, and dedicated tools give more control.
  • Troubleshooting false positives, slow scans, and secret history is a real part of the job — use nosec comments, parallel jobs, and history rewriting carefully.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.