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
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:
- Choose your scanners. Based on your tech stack, pick a SAST tool (e.g.,
Banditfor Python,Semgrepfor multi-language), an SCA tool (e.g.,pip-audit,Trivy), and a secret scanner (e.g.,gitleaks,trufflehog). - 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.
- 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.
- Run on every pull request (event:
pull_request) and on push to main. The pull request run gives developers feedback before code merges. - Upload results as artifacts (e.g., SARIF format) so they appear in the GitHub Security tab and are available for later review.
- 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-auditfor local verification. - Self-hosted / on-prem → Choose self-contained tools like
bandit+pip-audit+gitleaksrunning in your runner. - Container-heavy → Add
trivyas 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
# noseceverywhere, 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-auditcan't reproduce the exact dependency set and may miss issues.
Variations
- Use a pre-built GitHub Action like
gitleaks/gitleaks-actionoranchore/scan-actioninstead of installing tools manually. - Adopt a unified security platform like Snyk or SonarQube that combines SAST and SCA in one interface.
- 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
requestsdependency 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
noseccomments, parallel jobs, and history rewriting carefully.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.