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.