Dependency Vulnerability Scanning: Managing Supply Chain Risk in 2026

Dependency Vulnerability Scanning: Managing Supply Chain Risk in 2026

The average Node.js application has over 1,000 transitive dependencies. The average Python project has hundreds. Most of those packages are maintained by a single developer with no security review process, no SLA, and no obligation to notify users when vulnerabilities are found. When Log4Shell dropped in December 2021, organizations scrambled to find out which of their applications included log4j — many couldn't answer that question in under a week.

Dependency vulnerability scanning is how you maintain a continuous, accurate picture of your supply chain risk. Done well, it's a background process that surfaces actionable findings without drowning your team in noise. Done poorly, it's a wall of CVE alerts that everyone learns to ignore.

The Supply Chain Attack Surface

Understanding what you're defending against shapes how you build your scanning program.

Direct dependencies — packages you explicitly declare in package.json, requirements.txt, pom.xml. These are what most developers think about.

Transitive dependencies — packages your dependencies depend on. A single express installation pulls in 50+ packages you never explicitly chose. These are where most vulnerabilities live in practice.

Dev dependencies — packages only used during development or CI. These matter less for production risk but can still be attack vectors (see: the event-stream attack in 2018, where a malicious package was added to a developer tool and used to steal cryptocurrency).

Supply chain attacks — malicious code injected upstream (typosquatting, compromised maintainer accounts, malicious contributions). CVE databases don't help here; behavioral analysis tools and source verification do.

This guide focuses on known-vulnerability scanning — the CVE-based approach. It catches the vast majority of real-world supply chain risk.

npm audit: Start Here

For JavaScript projects, npm audit is built in and free:

# Basic audit
npm audit

# JSON output for processing
npm audit --json

# Only show vulnerabilities of a specific severity or above
npm audit --audit-level=high

# Fix automatically (where possible)
npm audit fix

# Fix including breaking changes (test carefully)
npm audit fix --force

The output structure:

{
  "vulnerabilities": {
    "lodash": {
      "name": "lodash",
      "severity": "high",
      "isDirect": false,
      "via": ["lodash"],
      "effects": ["your-direct-dependency"],
      "range": "<4.17.21",
      "nodes": ["node_modules/your-dep/node_modules/lodash"],
      "fixAvailable": {
        "name": "your-direct-dependency",
        "version": "2.3.0",
        "isSemVerMajor": false
      }
    }
  },
  "metadata": {
    "vulnerabilities": {
      "info": 0,
      "low": 2,
      "moderate": 5,
      "high": 3,
      "critical": 1,
      "total": 11
    }
  }
}

npm audit in CI:

# GitHub Actions
- name: Security audit
  run: |
    npm audit --audit-level=high --json > audit-results.json || true
    
    CRITICAL=$(jq '.metadata.vulnerabilities.critical' audit-results.json)
    HIGH=$(jq '.metadata.vulnerabilities.high' audit-results.json)
    
    if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
      echo "FAIL: $CRITICAL critical, $HIGH high vulnerabilities"
      npm audit --audit-level=high
      exit 1
    fi

Limitation: npm audit only checks your JavaScript dependencies against the npm advisory database. It won't scan your Dockerfile, Python requirements, or system packages.

OWASP Dependency-Check: Multi-Ecosystem Scanning

OWASP Dependency-Check works across Java, .NET, JavaScript, Ruby, Python, and more. It correlates package names and versions against the National Vulnerability Database (NVD).

# Docker
docker run --rm \
  -v $(pwd):/src \
  -v $(pwd)/reports:/report \
  owasp/dependency-check \
  --scan /src \
  --format HTML \
  --format JSON \
  --out /report \
  --project "My Application" \
  --nvdApiKey $NVD_API_KEY

# Standalone JAR
dependency-check.sh \
  --scan /path/to/project \
  --format JSON \
  --out ./reports \
  --project "My App" \
  --nvdApiKey $NVD_API_KEY

Configuring suppression for false positives:

<!-- dependency-check-suppression.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
  
  <!-- This CVE applies to the server component, not the client library we use -->
  <suppress>
    <notes>CVE-2023-XXXX: Only affects server deployments, we use client library</notes>
    <cve>CVE-2023-XXXX</cve>
    <packageUrl regex="true">pkg:npm/some-package@.*</packageUrl>
    <until>2026-12-31</until>  <!-- Revisit if still unpatched -->
  </suppress>
  
  <!-- False positive: our vendored copy is patched -->
  <suppress>
    <notes>Vendored copy manually patched 2026-04-15</notes>
    <filePath regex="true">.*vendor/old-library.*</filePath>
    <cve>CVE-2025-YYYY</cve>
  </suppress>
  
</suppressions>

Use it:

dependency-check.sh \
  --scan /path/to/project \
  --suppression dependency-check-suppression.xml \
  --format JSON \
  --out ./reports \
  --failOnCVSS 7.0  # Fail on HIGH and above

Snyk: Developer-First Vulnerability Management

Snyk occupies the middle ground between "run a one-off scan" and "full enterprise ASPM platform." It integrates at every level — CLI, IDE, CI, and pull request checks.

# Install
npm install -g snyk

# Authenticate
snyk auth

# Test current project
snyk test

# Test with specific severity threshold
snyk test --severity-threshold=high

# Monitor a project (continuous tracking)
snyk monitor --project-name="my-app-production"

# Test a Docker image
snyk container test your-image:tag --severity-threshold=critical

# Test Infrastructure as Code (Terraform, Helm, K8s manifests)
snyk iac test terraform/

The Snyk CI integration with detailed output:

snyk test --json 2>/dev/null | jq '{
  total: .vulnerabilities | length,
  critical: [.vulnerabilities[] | select(.severity == "critical")] | length,
  high: [.vulnerabilities[] | select(.severity == "high")] | length,
  findings: [.vulnerabilities[] | select(.severity | IN("critical", "high")) | {
    id: .id,
    package: .packageName,
    version: .version,
    fixedIn: .fixedIn[0],
    title: .title,
    cvss: .cvssScore
  }]
}'

Snyk in GitHub Actions:

name: Snyk Security Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  snyk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Snyk to check for vulnerabilities
        uses: snyk/actions/node@master
        continue-on-error: true
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high --sarif-file-output=snyk.sarif
      
      - name: Upload result to GitHub Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif

Dependabot: Automated Dependency Updates

GitHub's Dependabot doesn't just scan — it creates pull requests to fix vulnerabilities automatically. This is the only approach that scales to large dependency graphs.

Configure with .github/dependabot.yml:

version: 2
updates:
  # npm
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
    open-pull-requests-limit: 10
    reviewers:
      - "security-team"
    labels:
      - "dependencies"
      - "security"
    # Only open security PRs immediately, others weekly
    groups:
      production-dependencies:
        dependency-type: "production"
        update-types:
          - "patch"
          - "minor"
  
  # Docker
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    reviewers:
      - "platform-team"
  
  # GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
  
  # Python
  - package-ecosystem: "pip"
    directory: "/services/ml-service"
    schedule:
      interval: "weekly"
    ignore:
      # Pin major versions for ML stability
      - dependency-name: "torch"
        update-types: ["version-update:semver-major"]

Handling the Dependabot PR flood: A naive Dependabot setup creates dozens of PRs simultaneously. Use grouping:

updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      # Group all patch updates into one PR
      patch-updates:
        update-types:
          - "patch"
      # Group all minor non-breaking updates
      minor-updates:
        update-types:
          - "minor"
      # Security fixes always get individual PRs (default behavior)

SBOM Generation: Know What You Ship

A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in your software. In 2021, the US Executive Order on Cybersecurity made SBOMs mandatory for federal software vendors. In 2026, enterprise customers and regulated industries routinely require them.

Two formats dominate: SPDX (Linux Foundation) and CycloneDX (OWASP).

Generate with Syft:

# Install
brew install syft

# Generate SBOM for current directory
syft . -o spdx-json=sbom.spdx.json
syft . -o cyclonedx-json=sbom.cdx.json

# Generate for a Docker image
syft your-image:tag -o spdx-json=image-sbom.spdx.json

# Scan the SBOM for vulnerabilities with Grype
grype sbom:sbom.spdx.json
grype sbom:sbom.spdx.json --fail-on high

Attaching SBOM to container images:

# Using cosign to attach SBOM as an OCI artifact
cosign attach sbom --sbom sbom.spdx.json your-image:tag

# Verify and retrieve later
cosign verify-attestation --type spdx your-image:tag

Generating SBOM in CI:

- name: Generate SBOM
  uses: anchore/sbom-action@v0
  with:
    path: ./
    format: spdx-json
    output-file: sbom.spdx.json

- name: Scan SBOM for vulnerabilities
  uses: anchore/scan-action@v3
  with:
    sbom: sbom.spdx.json
    fail-build: true
    severity-cutoff: high
    
- name: Upload SBOM as artifact
  uses: actions/upload-artifact@v4
  with:
    name: sbom
    path: sbom.spdx.json
    retention-days: 90

CVE Triage Process

The raw output of dependency scanners is overwhelming without a triage process. Not all CVEs require immediate action.

Triage dimensions:

  1. CVSS Score — but don't use this alone. A CVSS 9.8 in a package used for internal tooling is less critical than a CVSS 7.5 in your public API.
  2. Exploitability — Is exploit code publicly available? (Check ExploitDB, Metasploit modules.) Is this being actively exploited in the wild? (Check CISA KEV catalog at cisa.gov/known-exploited-vulnerabilities-catalog.)
  3. Reachability — Is the vulnerable code path actually called in your application? Some tools (Snyk, Semgrep) can analyze reachability. A vulnerable function in a package that you use but don't call the vulnerable code path of is much lower priority.
  4. Fix availability — Is there a patched version? How much upgrade effort is involved?
# Check CISA KEV for active exploitation
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json | \
  jq --arg cve "CVE-2021-44228" '.vulnerabilities[] | select(.cveID == $cve)'

# Check for exploits in ExploitDB (via searchsploit if installed)
searchsploit "log4j 2.14"

Priority matrix:

CVSS Active Exploit Fix Available Priority
≥ 9.0 Yes Yes CRITICAL — fix within 24h
≥ 9.0 Yes No CRITICAL — mitigate within 24h
≥ 7.0 Yes Yes HIGH — fix within 7 days
≥ 7.0 No Yes MEDIUM — fix in next sprint
< 7.0 No Yes LOW — schedule for batch update
Any No No WATCH — track for fix release

Automating the triage:

#!/usr/bin/env python3
# triage-cves.py
import json
import requests

CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"

def get_actively_exploited_cves():
    resp = requests.get(CISA_KEV_URL, timeout=30)
    return {v["cveID"] for v in resp.json()["vulnerabilities"]}

def triage_finding(vuln, exploited_cves):
    cvss = float(vuln.get("cvssScore", 0))
    cve_id = vuln.get("id", "")
    is_exploited = cve_id in exploited_cves
    has_fix = bool(vuln.get("fixedIn"))
    
    if cvss >= 9.0 or (cvss >= 7.0 and is_exploited):
        priority = "CRITICAL" if is_exploited else "HIGH"
    elif cvss >= 7.0:
        priority = "MEDIUM"
    else:
        priority = "LOW"
    
    return {
        "id": cve_id,
        "package": vuln.get("packageName"),
        "cvss": cvss,
        "actively_exploited": is_exploited,
        "has_fix": has_fix,
        "priority": priority,
    }

# Load Snyk output
with open("snyk-results.json") as f:
    results = json.load(f)

exploited = get_actively_exploited_cves()
triaged = [triage_finding(v, exploited) for v in results.get("vulnerabilities", [])]
triaged.sort(key=lambda x: ["CRITICAL", "HIGH", "MEDIUM", "LOW"].index(x["priority"]))

for finding in triaged:
    print(f"[{finding['priority']}] {finding['id']} in {finding['package']} "
          f"(CVSS: {finding['cvss']}, Exploited: {finding['actively_exploited']})")

For teams that have CI pipeline monitoring set up through HelpMeTest, tracking your vulnerability scan job results over time gives you trend visibility — whether your total open CVE count is trending up or down, and whether your fix rate is keeping pace with newly discovered vulnerabilities.

The Complete Scanning Stack

Recommended setup by ecosystem:

JavaScript/TypeScript: npm audit (baseline) + Snyk (depth + PRs) + Dependabot (automation)

Python: pip-audit + Snyk + Dependabot

Java/JVM: OWASP Dependency-Check + Snyk

Containers: Trivy or Grype for images + Syft for SBOM generation

Multi-ecosystem: OWASP Dependency-Check as the single-pane-of-glass report, supplemented by ecosystem-specific tools

The key is automating the fix path. Scanning that produces a report that goes into a ticket queue that nobody works is theater. Scanning + Dependabot automation + a triage process with SLAs is a real security program.

Read more

Start now free