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.
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.
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.
| Choice | Buys you | Costs you |
|---|---|---|
| Fail the build on high/critical | Vulnerable versions never reach prod | A fresh CVE with no patch can block every deploy |
| Warn-only, triage later | Builds keep flowing; you batch fixes | Easy to ignore; flagged versions ship anyway |
| Pinned lockfile vs floating range | You scan exactly what installs; reproducible | Ranges can silently re-introduce a fixed lib on the next install |
| Direct upgrade vs transitive override | Upgrade is clean and durable | Override/pin patches the tree but can drift from upstream |
^1.2.0 can silently pull a vulnerable version back in on a later install unless the lockfile pins it.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.
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.