Dependency Scanning and SCA — A Practical Guide

Look at any modern service and count the lines of code you actually wrote versus the lines you pulled in. A typical Node or Python app ships with hundreds of transitive dependencies, and most of the code running in production came from a registry, not your team. That inversion is the whole story: your dependencies are the largest, least-audited part of your attack surface, and they change every time someone runs an install.

Why dependencies dominate your attack surface #

You review your own pull requests. You do not review the twelfth-level transitive package that a logging library pulled in three years ago. Attackers know this. The supply chain is where the leverage is, because one compromised or vulnerable package fans out across thousands of projects. Software Composition Analysis (SCA) exists to give you visibility into that layer.

What SCA actually does #

SCA scans your dependency manifests and lockfiles against vulnerability databases. It resolves the exact versions you ship (this is why the lockfile matters, not just the manifest), matches them against known CVEs and advisories, and reports what is vulnerable and what fixes exist.

The tooling landscape is broad and mostly overlapping:

Dependabot: built into GitHub, opens update PRs and surfaces alerts against the GitHub Advisory Database.

Snyk: commercial, strong prioritization and fix guidance, deep language support.

Trivy: open source, scans dependencies, containers, and IaC in one binary. A common default in CI.

OWASP Dependency-Check: open source, long-standing, maps components to CVEs via the NVD.

npm audit / pip-audit: language-native, already installed, good for a quick gate.

The cheapest possible check is the one your package manager ships. Here is a GitHub Actions step that fails a build on high-severity npm advisories:

yaml.yaml

- name: Audit dependencies
run: npm audit --audit-level=high

For broader coverage, including containers and lockfiles across ecosystems, Trivy is a solid default:

yaml.yaml

- name: Trivy dependency scan
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: '1'
ignore-unfixed: true

Note ignore-unfixed. There is no point failing a build over a CVE that has no available patch. Let it report, but do not block on something you cannot act on today.

The noise problem, and reachability #

Run SCA on a real repo and the first result is discouraging: dozens or hundreds of findings, most of them CRITICAL by CVSS score. If you treat every one as a blocker, engineers learn to ignore the scanner. That is the failure mode you are trying to avoid.

The key insight is that a CVE in a dependency is only exploitable if your code actually reaches the vulnerable function. A parsing bug in a code path you never call is not a live risk to you. Modern tools lean into this.

Reachability analysis: newer SCA engines build a call graph and check whether the vulnerable symbol is reachable from your code. Snyk, Semgrep's SCA, and others use this to demote unreachable findings.

EPSS and KEV: prioritize by real-world exploitation. EPSS scores the probability a CVE gets exploited; CISA's KEV catalog lists what is being exploited right now. A medium-CVSS bug that is in KEV outranks a critical one nobody has ever weaponized.

The practical rule: block the pipeline on findings that are fixable, reachable, and high-severity. Everything else goes to a tracked backlog, not a red build.

Keeping dependencies current #

Most vulnerabilities are fixed by an upgrade you have not applied yet. The lowest-effort defense is staying close to current, which is why automated update PRs matter. Renovate and Dependabot both open pull requests as new versions land, so upgrades become small, reviewable, and continuous instead of a terrifying annual bump.

A minimal Dependabot config that groups minor and patch updates to keep PR volume sane:

yaml.yaml

# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
minor-and-patch:
update-types:
- minor
- patch

Pair this with committed lockfiles and pinned versions. Lockfiles guarantee that CI, staging, and production resolve the exact same tree, so a scan result is meaningful. Floating ranges (^1.2.0) without a lockfile mean the thing you scanned is not necessarily the thing you shipped.

SCA, SAST, DAST, IAST — who scans what #

SCA is one of four scan types, and confusion between them is common. They look at different things and run at different points.

SCA scans your dependencies. It answers "am I shipping a known-vulnerable component?" Runs early, on every commit and in the dependency-update PR.

SAST (Static Application Security Testing) scans your own source code without running it, looking for injection, hardcoded secrets, unsafe deserialization, and similar patterns. Tools like Semgrep and CodeQL run in CI on pull requests. Fast, good coverage, prone to false positives on custom rules.

DAST (Dynamic Application Security Testing) tests the running application from the outside, like an attacker probing HTTP endpoints. OWASP ZAP is the standard open-source option. It catches auth and configuration issues that only appear at runtime, but needs a deployed environment, so it runs later, against staging.

IAST (Interactive) instruments the running app during tests and observes real execution to confirm whether a flaw is genuinely exploitable. Lower false positives, at the cost of runtime instrumentation. It sits between SAST and DAST.

A reasonable pipeline placement:

Commit / PR: SCA + SAST (fast, fail-fast).

Build: SCA on the container image, SBOM generation.

Staging deploy: DAST, optionally IAST during integration tests.

None of these replaces another. SCA will never find the SQL injection you wrote by hand, and SAST will never tell you a third-party parser has a CVE.

SBOMs tie it together #

While you are scanning dependencies, generate a Software Bill of Materials. An SBOM (CycloneDX or SPDX) is the machine-readable inventory of everything you ship, and it is what lets you answer "are we affected?" within minutes the next time a Log4Shell-class bug drops, instead of grepping repos for a week. Most SCA tools, including Trivy, emit an SBOM as a side output. For the full picture on generating, signing, and verifying them, see our guide on supply-chain security with SBOMs and attestation.

The call we'd make #

Start with the free, native tools and a hard gate on what is actually actionable. Run npm audit or Trivy in CI, fail only on high or critical findings that are fixable and reachable, and route the rest to a backlog so the signal stays trustworthy. Turn on Dependabot or Renovate day one, group the noisy updates, and commit your lockfiles so scans reflect reality. Layer SAST (Semgrep or CodeQL) on pull requests and DAST (ZAP) against staging, but do not expect any one scanner to cover another's blind spot. Generate an SBOM as part of the build so your next zero-day response is a query, not a fire drill. For how these pieces fit into a broader program, see our application security best practices pillar.