Dependency CVEs

Your app is mostly other people's code. When a vulnerability is published against one of those libraries, you're exposed — even if you've never heard of the library that pulled it in.

The idea

A modern app declares a handful of direct dependencies, but each of those drags in its own dependencies, and so on — a deep tree of third-party code you mostly never read. A CVE is a publicly disclosed vulnerability with a severity (usually a CVSS score). When one is published against a specific library version, every app whose tree contains that version is at risk.

The catch: most of that risk hides in transitive dependencies — packages you never typed into your manifest. The defense is a dependency scanner: it resolves your full graph, matches each package@version against a vulnerability database, flags what's vulnerable, and you remediate by upgrading to a patched version, then re-scan to confirm the graph is clean.

app your manifest web-framework @4.3.0 http-client @2.0.1 logger @1.8.4 json-parser @1.2.0 CVE high · 8.1 patched transitive — not in your manifest
The resolver expands your manifest into the full dependency graph — direct deps and everything they pull in.

How it works

A software composition analysis (SCA) scanner doesn't read your code for bugs — it reads your resolved graph (the lockfile) and looks each package up in a vulnerability feed. The work is graph traversal plus a database join, not magic.

def scan(graph, vuln_db):
    flagged = []
    for pkg in graph.walk():                  # every node, direct and transitive
        for cve in vuln_db.match(pkg.name, pkg.version):
            flagged.append((pkg, cve.id, cve.severity))   # e.g. CVSS 8.1, high
    return flagged

def remediate(graph, flagged, vuln_db):
    for pkg, cve_id, sev in flagged:
        fix = vuln_db.first_patched_version(pkg.name, after=pkg.version)
        if fix:
            graph.upgrade(pkg.name, fix)      # bump to a non-vulnerable version
        else:
            graph.override(pkg.name)          # no patch yet: pin / override / mitigate
    return scan(graph, vuln_db)               # re-scan: confirm the tree is clean

Note what the scanner needs: the resolved graph, not the manifest. The manifest says web-framework@^4; the lockfile says exactly which json-parser got installed underneath it. Scanning the manifest alone would miss the node that's actually exposed.

Signals

ChoiceBuys youCosts you
Fail the build on high/criticalVulnerable versions never reach prodA fresh CVE with no patch can block every deploy
Warn-only, triage laterBuilds keep flowing; you batch fixesEasy to ignore; flagged versions ship anyway
Pinned lockfile vs floating rangeYou scan exactly what installs; reproducibleRanges can silently re-introduce a fixed lib on the next install
Direct upgrade vs transitive overrideUpgrade is clean and durableOverride/pin patches the tree but can drift from upstream

Watch out for

Worked example

Your manifest lists web-framework, http-client, and logger — none of them json-parser. The scanner resolves the graph and finds json-parser@1.2.0 sitting under web-framework, and the vuln DB matches it to a high-severity CVE (CVSS 8.1). It's flagged, with the path traced as app → web-framework → json-parser. The DB says 1.2.1 is the first patched version, so you bump it (upgrade the framework, or pin the transitive directly). The node turns clean, you re-scan, and the graph reports zero vulnerable packages. Flagged, then patched — exactly the loop.

Check yourself

Your manifest lists no vulnerable package, but the scan still flags one. How is that possible?

Coach note: the manifest only lists what you asked for; the resolved graph holds everything those asks dragged in. Take another pass if the transitive idea feels slippery — it's where most real exposure lives.