Skip to content
Back to Quality Assurance

Dependency Scanning

Automatically find known vulnerabilities in your third-party dependencies on every change.

Why

Modern applications are 80-90% third-party code — your package.json pulls in 1000 transitive dependencies, each with its own CVEs. You did not write the vulnerable code, but you ship it, and you are responsible for it. The log4j vulnerability (CVE-2021-44228) let any unpatched server execute arbitrary code via a crafted log string, and it was present in thousands of applications whose developers had never heard of log4j — it came in transitively. Dependency scanning automates what is impossible to do manually: checking every dependency against the CVE database on every change, so a newly-disclosed vulnerability in a transitive package is caught within hours, not after a breach.

When

On every PR (block on new high/critical CVEs), nightly on the main branch (catch CVEs disclosed after the last merge), and before every release. Do not skip — a single missed scan is a single missed vulnerability.

How

1. Choose a scanner and wire it into CI
   - Snyk (broad language support, good remediation advice)
   - Dependabot (GitHub-native, free, creates PRs to bump versions)
   - npm audit / pnpm audit (built-in, fast, but less context)

2. Scan on every PR and nightly on main
   - PR: fail the build on new high/critical CVEs introduced by the change
   - Nightly: scan the current lockfile, open an issue for any new CVE
   - Do not fail on pre-existing CVEs (they were there before the PR) — track them separately

3. Triage each finding by exploitability, not just severity
   - A critical CVE in a dev-only dependency (e.g. a test runner) is not exploitable in prod
   - A moderate CVE in a prod dependency that processes user input may be critical in your context
   - Use the scanner's reachability analysis if available (Snyk Code, OSV-Scanner)

4. Fix by upgrading, not by suppressing
   - Upgrade to the fixed version; if a major upgrade is needed, schedule it
   - If no fix is available, use a patched fork, a virtual patch (Snyk), or isolate the dependency
   - Suppress a finding only with a documented reason and an expiry date

5. Keep the dependency list small and current
   - Remove unused dependencies (depcheck, knip) — fewer deps, fewer CVEs
   - Enable Renovate/Dependabot to keep versions current — stale deps accumulate CVEs
   - Pin transitive deps in the lockfile so a sub-dependency bump does not silently introduce a vuln

Example

# .github/workflows/dependency-scan.yml
name: Dependency Scan
on: [pull_request, schedule: [{cron: "0 2 * * *"}]]  # nightly at 2am

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high --fail-on=all
      # Fails the PR if a new high/critical CVE is introduced.

# Dependabot config: .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    # Dependabot opens PRs to bump versions. Review and merge promptly —
    # a PR that sits for 2 weeks is 2 weeks of exposure if the CVE is critical.

# Triage example:
# CVE-2024-1234 (critical) in lodash@4.17.20 — used in prod, processes user input
#   → fix immediately: npm i lodash@4.17.21
# CVE-2024-5678 (high) in jest@29 — dev-only, not in prod bundle
#   → schedule for next sprint, not a prod risk today

Common Problems

  • ⚠Failing the build on all CVEs including pre-existing ones paralyzes the team — the build has been red for months because of an unfixable CVE in a transitive dep, so new CVEs are ignored, and the signal is lost.
  • ⚠Scanning only direct dependencies and missing vulnerabilities in transitive deps — the log4j vulnerability was transitive in most affected applications, and a shallow scan would have missed it entirely.
  • ⚠Treating severity as the only triage factor — a critical CVE in a dev-only test framework is not a production risk, while a moderate prototype-pollution CVE in a prod JSON parser may be exploitable in your app.
  • ⚠Suppressing findings with no reason or expiry to make the scanner green — the suppressions accumulate, real vulnerabilities are hidden, and nobody reviews the suppress list until a breach happens.

Best Practices

  • ✓Fail the PR only on NEW high/critical CVEs introduced by the change, and track pre-existing CVEs in a separate backlog — this keeps the signal actionable instead of drowning the team in red builds.
  • ✓Use a scanner with reachability analysis (Snyk Code, OSV-Scanner) to determine whether the vulnerable function is actually called in your code — a CVE in an unreachable code path is not an immediate risk.
  • ✓Enable Renovate or Dependabot to keep dependencies current with automated PRs — the cheapest way to reduce CVE exposure is to not be 20 minor versions behind on every package.
  • ✓Review and prune the dependency list periodically (depcheck, knip) — removing unused dependencies permanently removes their CVEs and reduces the scan surface.