The Best Approach to Dependency Vulnerability Scanning in CI Pipelines

Dependency vulnerabilities are the silent killers of modern software projects. A single outdated package can expose your entire application to attacks, and with the average project pulling in hundreds of transitive dependencies, manual tracking is impossible.

After years of implementing security scanning across Python, TypeScript, and React projects, I've learned that the "best" approach isn't about choosing a single tool—it's about building a multi-layered strategy that catches vulnerabilities early, fails fast when necessary, and doesn't bring your development velocity to a grinding halt.

Let's cut through the noise and build a practical scanning strategy.

The Two-Stage Scanning Philosophy

Most teams make a critical mistake: they treat all vulnerabilities equally and fail every build that has any vulnerability whatsoever. This sounds secure in theory but becomes unworkable in practice. Within weeks, developers start bypassing the checks or creating "temporary" exceptions that become permanent.

The better approach? Implement two distinct scanning stages:

Stage 1: Fail on Critical/High Vulnerabilities

This is your hard gate. Any critical or high-severity vulnerability with a known exploit blocks the build. No exceptions, no debate.

Stage 2: Report on Medium/Low Vulnerabilities

These get logged, tracked, and addressed during regular sprint planning. They don't block deployments but create visibility and accountability.

This pragmatic approach maintains security without sacrificing developer productivity.

Python Projects: Combining pip-audit and Safety

For Python projects, I recommend running both pip-audit and safety in your CI pipeline. Here's why: pip-audit uses the PyPI Advisory Database (which is comprehensive and well-maintained), while safety pulls from Safety DB (which sometimes catches things others miss).

Here's a GitHub Actions workflow that implements the two-stage philosophy:

yaml
name: Security Scan

on: [push, pull_request]

jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

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

  - name: Install dependencies
    run: |
      pip install pip-audit safety
      pip install -r requirements.txt

  - name: Run pip-audit (fail on critical/high)
    run: |
      pip-audit --desc --vulnerability-service osv \
        --severity-threshold high

  - name: Run safety check (report only)
    continue-on-error: true
    run: |
      safety check --json --output safety-report.json
      safety check --short-report

  - name: Upload safety report
    if: always()
    uses: actions/upload-artifact@v3
    with:
      name: safety-report
      path: safety-report.json

Enter fullscreen mode Exit fullscreen mode

The key detail: pip-audit fails the build on high/critical issues, while safety runs with continue-on-error: true, ensuring visibility without blocking.

TypeScript and React: Leveraging npm audit and Snyk

The JavaScript ecosystem moves fast, which means vulnerabilities appear frequently. The built-in npm audit is your first line of defense, but it's not enough on its own.

Here's my recommended workflow for TypeScript/React projects:

yaml
name: Security Scan

on: [push, pull_request]

jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

  - name: Setup Node.js
    uses: actions/setup-node@v3
    with:
      node-version: '18'

  - name: Install dependencies
    run: npm ci

  - name: Run npm audit (critical/high only)
    run: |
      npm audit --audit-level=high

  - name: Run Snyk test
    uses: snyk/actions/node@master
    continue-on-error: true
    env:
      SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
    with:
      args: --severity-threshold=medium --json-file-output=snyk-report.json

  - name: Upload Snyk report
    if: always()
    uses: actions/upload-artifact@v3
    with:
      name: snyk-report
      path: snyk-report.json

Enter fullscreen mode Exit fullscreen mode

Snyk excels at finding vulnerabilities in React components and frontend dependencies that other tools miss. The free tier is generous enough for most small-to-medium projects.

The Practical Details That Matter

1. Suppress Strategically, Not Habitually

Every scanning tool supports suppression, but use it carefully. When you suppress a vulnerability:

  • Document why ("No fix available, but we don't use the affected function")
  • Set a review date ("Re-evaluate in Q2 2025")
  • Track suppressions in code, not in CI configuration

For pip-audit, use a .pip-audit-ignore file. For Snyk, use .snyk policy files. This keeps your security decisions version-controlled and reviewable.

2. Pin Your Scanning Tools

Nothing breaks CI quite like a scanner that suddenly gets stricter. Pin your tool versions:

yaml

  • name: Install pip-audit run: pip install pip-audit==2.6.1

Update these dependencies deliberately during sprint planning, not accidentally when a build breaks on Friday afternoon.

3. Differentiate Between Direct and Transitive Dependencies

A vulnerability in a direct dependency is your problem. A vulnerability buried five levels deep in a transitive dependency might not even affect your code path.

Most modern tools (pip-audit, Snyk, npm audit) show this distinction. Use it to prioritize. Fix direct dependencies immediately. Investigate transitive dependencies based on actual code usage.

4. Schedule Comprehensive Scans

Run your strict scans on every PR. But also schedule a comprehensive, everything-included scan weekly:

yaml
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM

This catches new vulnerabilities in old dependencies and ensures nothing slips through the cracks.

What About Container Scanning?

If you're shipping containers (and you probably should be), add Trivy to your pipeline:

yaml

  • name: Run Trivy scanner uses: aquasecurity/trivy-action@master with: image-ref: 'myapp:${{ github.sha }}' severity: 'CRITICAL,HIGH' exit-code: '1'

Trivy scans both your application dependencies AND the base OS packages in your container image. It's fast, accurate, and catches an entirely different class of vulnerabilities.

The Reality Check: This Isn't Set-and-Forget

Here's what nobody tells you: dependency scanning is high-maintenance. New vulnerabilities appear constantly. Tools update their databases. Risk profiles change.

Budget 2-4 hours per month to:

  • Review vulnerability reports
  • Update dependencies
  • Refine your suppression rules
  • Tune severity thresholds

Treat this like technical debt management, because that's exactly what it is.

Conclusion: Build Defense in Depth

The best approach to dependency vulnerability scanning isn't a single tool or technique—it's a layered defense:

  1. Fast, strict scanning on every commit for critical vulnerabilities
  2. Comprehensive scanning that reports (but doesn't block) on lower-severity issues
  3. Regular review cycles to keep your suppressions and policies current
  4. Container scanning for production environments

Start with the workflows above. Adjust severity thresholds based on your risk tolerance. Most importantly, make security scanning a first-class part of your development process, not an afterthought that gets bypassed when deadlines loom.

Your future self—and your security team—will thank you.


📚 Recommended Reading

Want to go deeper on CI?? These are worth it:

These are affiliate links — if you buy through them I earn a small commission at no extra cost to you.