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.