Back to glossary

Vulnerability Scanning

Vulnerability scanning is the automated matching of the software you run against databases of known vulnerabilities such as CVE, the NVD, and OSV.

Also known as Vulnerability scanner, Vuln scanning, Vulnerability assessment scanning

Vulnerability scanning, also called a vulnerability scan, is the automated process of inventorying the software on a system, identifying each component's name and version, and matching that inventory against known-vulnerability databases such as CVE, the NVD, and OSV. It finds published flaws only. It cannot see unreported vulnerabilities, and it sees malware only after someone files an advisory.

Every vulnerability scanner performs the same join, whether it probes a network port or reads a lockfile. The inventory sits on one side, advisory records sit on the other, and the join key is a product identifier plus a version range. Network and host scanners key on Common Platform Enumeration (CPE) names from the National Vulnerability Database. Dependency scanners key on ecosystem package names from OSV and the GitHub Advisory Database. When the key is missing or wrong, the scanner is wrong without saying so. Only 57.6% of the 48,185 CVEs published in 2025 carried CPE identifiers, according to Jerry Gamblin's analysis of 2025 CVE data. Since April 15, 2026, NIST enriches only prioritized CVEs and leaves the rest unscheduled, a change we covered when NIST officially stopped enriching most CVEs.

Our position: vulnerability scanning is necessary hygiene and a poor security boundary. It answers one question, whether you run a version that someone has publicly reported as flawed, and it answers late and noisily. Malicious releases reach installs before any advisory exists. Most matched CVEs are never reached by your code. The scanner itself runs with your CI secrets. Use vulnerability scanning for patch hygiene, put malware detection in front of the install, and let reachability drive triage.

What is vulnerability scanning?

#

Vulnerability scanning is the detection stage of vulnerability management: an automated, repeatable check of what you run against what the world has disclosed. The term covers several kinds of tools that look at different layers of the stack. They differ in where they get the inventory and which identifier they match on.

Scanner typeWhat it inspectsHow it identifies softwareMain data sourceBlind spot
Network scanner, unauthenticatedOpen ports, from outside the hostService banners and protocol probes, translated to CPENVD CPE configurationsBackported fixes; anything not listening on a port
Host scanner, authenticated or agentThe OS package database (dpkg, rpm)Distro package name and full distro versionDistro security trackers (Debian, Ubuntu, Red Hat)Software installed outside the package manager
Container image scannerImage layers: OS packages and language manifestsOS packages plus lockfiles found in the imageDistro trackers plus OSV and GHSABinaries copied in without package metadata
Dependency scanner (SCA)Manifests and lockfiles in a repositoryEcosystem, package name, and version, as a package URL (PURL)OSV, GHSA, registry advisory feedsVendored code; tools run by npx; malware before an advisory
Web application scanner (DAST)A running app's HTTP surfaceNeeds no versions; sends payloads and reads responsesIts own library of checksCode paths it cannot reach; business logic

Dependency scanning is the core of software composition analysis, and it is the kind of scanning most open source supply chain work depends on. Dynamic application security testing is the odd one out in the table. A DAST scanner tests behavior by sending payloads and observing responses. Every other scanner in the table matches an inventory against advisory records, and the rest of this article is about that matching.

How does vulnerability scanning work?

#

Vulnerability scanning works in four steps: build an inventory, turn each item into an identifier, match identifiers against advisory records, and rank what matched. The first three steps decide whether a finding is correct. The fourth decides whether anyone acts on it.

The figure traces both kinds of scanner through the four steps, using the real artifacts from the commands below. Step 1, inventory: the network scanner reads the service banner Server: SimpleHTTP/0.6 Python/3.13.16, and the dependency scanner reads the lockfile entry requests==2.31.0. This step breaks when code is vendored, run by npx, or served without a banner. Step 2, identify: nmap converts the banner to the CPE cpe:/a:python:simplehttpserver:0.6, leaving Python 3.13.16 as free text, while the dependency scanner forms the package URL pkg:pypi/requests@2.31.0. This step breaks when a distro backports a fix or a CPE names the wrong product. Step 3, match: the network scanner looks up NVD CPE configurations, which only 57.6% of 2025 CVEs had, and the dependency scanner looks up OSV and GHSA version ranges, which return three CVEs as six records. This step breaks when no identifier exists, aliases are not merged, or no advisory has been published yet. Step 4, rank: network findings are ranked by CVSS, KEV listing, and exposure, and dependency findings by reachability, KEV listing, and exploit data. This step breaks when severity is used without context.

A network scanner builds its inventory from what services announce about themselves. We started Python's built-in web server on a test machine and ran nmap's version detection against it:

Bash
$ curl -sI http://127.0.0.1:8088/ | grep Server
Server: SimpleHTTP/0.6 Python/3.13.16

$ nmap -sV -p 8088 -oX - 127.0.0.1 | grep -E '<service|<cpe>'
<service name="http" product="SimpleHTTPServer" version="0.6" extrainfo="Python 3.13.16" method="probed" conf="10">
<cpe>cpe:/a:python:simplehttpserver:0.6</cpe>

nmap turned the banner into one CPE, cpe:/a:python:simplehttpserver:0.6, written in the older CPE 2.2 URI form. The interpreter version, the part with a real vulnerability history, survived only as free text in extrainfo. A scanner that joins on the CPE looks up a product named simplehttpserver and never considers Python 3.13.16. Unauthenticated network scanning is inference from whatever a service chooses to print.

A dependency scanner starts from better data, because a lockfile states names and versions exactly. pip-audit, by default, sends each pinned package to PyPI's JSON API, which returns the advisories OSV has mapped to that exact release:

Bash
$ curl -s https://pypi.org/pypi/requests/2.31.0/json \
    | jq -c '.vulnerabilities[] | {id, aliases, fixed_in}'
{"id":"GHSA-9wx4-h78v-vm56","aliases":["CVE-2024-35195","PYSEC-2026-1873"],"fixed_in":["2.32.0"]}
{"id":"GHSA-9hjg-9r4m-mvj7","aliases":["CVE-2024-47081","PYSEC-2026-1872"],"fixed_in":["2.32.4"]}
{"id":"GHSA-gc5v-m9x4-r6x2","aliases":["CVE-2026-25645","PYSEC-2026-2275"],"fixed_in":["2.33.0"]}
{"id":"PYSEC-2026-1873","aliases":["CVE-2024-35195","GHSA-9wx4-h78v-vm56"],"fixed_in":["2.32.0"]}
{"id":"PYSEC-2026-1872","aliases":["CVE-2024-47081","GHSA-9hjg-9r4m-mvj7"],"fixed_in":["2.32.4"]}
{"id":"PYSEC-2026-2275","aliases":["CVE-2026-25645","GHSA-gc5v-m9x4-r6x2"],"fixed_in":["2.33.0"]}

The registry did the join. The scanner's remaining job is to collapse aliases, because the response holds six records for three CVEs. pip-audit 2.10.1 did not collapse them when we ran it on October 19, 2026:

Plain text
$ echo "requests==2.31.0" > requirements.txt
$ pip-audit -r requirements.txt --no-deps --disable-pip
Found 6 known vulnerabilities in 1 package
Name     Version ID              Fix Versions
-------- ------- --------------- ------------
requests 2.31.0  PYSEC-2026-1873 2.32.0
requests 2.31.0  PYSEC-2026-1872 2.32.4
requests 2.31.0  PYSEC-2026-2275 2.33.0
requests 2.31.0  PYSEC-2026-1873 2.32.0
requests 2.31.0  PYSEC-2026-1872 2.32.4
requests 2.31.0  PYSEC-2026-2275 2.33.0

Three vulnerabilities became six findings. Duplicate counting is a small bug with a large effect, because finding counts feed dashboards, remediation deadlines, and compliance reports.

One CVE, four records

#

Vulnerability scanners disagree about the same CVE because each one reads a different record of the same vulnerability, keyed on a different identifier. CVE-2024-35195, a TLS verification bug in Python's requests library fixed in 2.32.0, shows how far apart those records can drift. GitHub was the CVE Numbering Authority (CNA) that issued it.

RecordIdentifier for the affected softwareVersion dataWhat a scanner does with it
CNA record (GitHub)vendor psf, product requestsThe string "< 2.32.0", with no version typePeople can read it; a machine cannot parse it as a range
CISA ADP enrichmentcpe:2.3:a:request_project:request:2.32.0:*:*:*:*:node.js:*:*version 0, lessThan 2.32.0A CPE scanner matches the Node.js request package and misses Python requests
NVDNone: API status Deferred, not scheduled for enrichment, no configurations blockNoneA scanner that reads NVD CPE configurations finds nothing
OSV via PyPI (GHSA-9wx4-h78v-vm56)pkg:pypi/requestsfixed_in 2.32.0A package scanner matches requests 2.31.0 correctly

The CVE Services record carries an enrichment from CISA's Vulnrichment program, last updated on July 31, 2025. That enrichment attached a CPE for request_project:request with a node.js target, which names the deprecated npm package request, not the Python library. The NVD record shows that CPE but has no NVD configurations of its own. Its API status is Deferred, and the NVD website labels it as not scheduled for enrichment. A scanner keyed on CPE either finds nothing or flags the wrong ecosystem. A scanner keyed on the PyPI package name gets it right. For open source dependencies, package identifiers beat CPE.

Where vulnerability scanning goes wrong

#

Vulnerability scanning errors fall into two groups. False positives report a flaw that is already fixed or cannot be triggered. False negatives miss software the scanner never inventoried or never identified. Both groups come from the join, not only from late vulnerability data.

Failure modeDirectionWhat happensExample
Backported fixFalse positiveThe distro patches the flaw but keeps the upstream version numberUbuntu 24.04's openssl reports 3.0.13 and carries 44 CVE fixes
Missing or wrong identifierFalse negativeNo CPE, or a CPE for a different productCVE-2024-35195 has no NVD configurations
Duplicate aliasesInflated countGHSA and PYSEC records for one CVE are counted twicepip-audit reports 6 findings for 3 CVEs
Vendored or bundled codeFalse negativeThe code ships inside another package and is absent from any manifestA package shipped a tampered axios inside its own node_modules
Code outside the lockfileFalse negativenpx and global installs resolve dependencies at run timeCI tools run by npx during the axios compromise
Unreachable vulnerable codeNoiseThe vulnerable function is never calledCVE-2026-25645 affects only callers of one helper function
Bad feed recordFalse positiveA wrong advisory propagates to every consumerOSV withdrew 157 malware reports on May 26, 2026

The backport case is the oldest failure mode, and it is easy to reproduce. On the Ubuntu 24.04 machine we used for this article:

Bash
$ openssl version
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)

$ dpkg-query -W -f='${Version}\n' openssl
3.0.13-0ubuntu3.16

$ zcat /usr/share/doc/openssl/changelog.Debian.gz \
    | sed -n '/^openssl (3.0.13-0ubuntu3)/q;p' \
    | grep -oE 'CVE-[0-9]{4}-[0-9]+' | sort -u | wc -l
44

Ubuntu's 13 package revisions since 3.0.13-0ubuntu3 patch 44 distinct CVEs. One of them is CVE-2024-6119, which upstream OpenSSL fixed only in 3.0.15, according to its OSV record. A scanner comparing "3.0.13" against upstream ranges reports dozens of vulnerabilities that are already fixed. A correct scanner reads the full package version, 3.0.13-0ubuntu3.16, and matches it against Ubuntu's own advisory data using dpkg version ordering. Authenticated host scanning exists to get this right.

Vendored code fails in the opposite direction. During the axios compromise, we found a package that shipped a tampered copy of axios 1.14.1 inside its own node_modules directory, with the malicious dependency injected. That copy appears in no manifest the consumer controls, so a lockfile-based scanner has nothing to match. The same blind spot covers tools executed with npx or installed globally. As we wrote after that incident, code resolved at execution time never enters your project's lockfile, so a clean lockfile scan says nothing about it.

Can vulnerability scanning detect malware?

#

Vulnerability scanning detects malware only by lookup, after someone has published an advisory for the specific malicious version. A malicious package is not a vulnerability. It is working code doing what its author intended. Malware rarely receives a CVE, so CPE-keyed scanners do not see it. Package scanners see it once GitHub or OpenSSF publishes a malware record, which usually happens after the first installs have already run the payload.

The Qix maintainer compromise in September 2025 shows the gap. The npm registry records chalk@5.6.1 as published at 13:13:05 UTC on September 8. The matching OpenSSF record, MAL-2025-46969, was published at 17:11:19 UTC. For just under four hours, every fresh install of chalk 5.6.1 pulled the payload, and no advisory existed for a scanner to match.

The axios compromise in March 2026 moved faster on both sides. The malicious dependency plain-crypto-js@4.2.1 was published at 23:59:12 UTC on March 30. Socket's automated malware detection, which analyzes the package code itself, flagged it at 00:05:41 UTC, six minutes later. The npm registry records axios@1.14.1, the release that pulled it in, as published at 00:21:58 UTC, 16 minutes after that flag. The dropper ran from a postinstall hook and then replaced its own package.json with a clean copy, so a scan of node_modules afterward finds an ordinary crypto-js clone.

Today the advisory exists, and it is easy to miss. We asked the npm registry's bulk advisory endpoint, the endpoint npm audit calls, about the malicious release:

Bash
$ curl -s -X POST -H 'content-type: application/json' \
    https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -d '{"axios":["1.14.1"]}' > axios.json

$ jq '.axios | length' axios.json
35

$ jq -r '.axios[] | [.severity, (.cwe|join(",")), .title[0:44]] | @tsv' axios.json \
    | sort | head -4
critical	CWE-506	Malware in axios
high	CWE-113,CWE-1321	Axios: Header Injection via Prototype Pollut
high	CWE-1321	Axios has prototype pollution read-side gadg
high	CWE-1321	Axios: Prototype Pollution Gadgets - Respons

As of October 19, 2026, the endpoint returns 35 advisories for axios 1.14.1: 1 critical, 12 high, 21 moderate, and 1 low. The critical one says the release installs malware. The other 34 are ordinary vulnerability advisories. A vulnerability scanner presents all 35 as rows of the same kind, distinguished only by CWE-506 in one field. Malware advisory feeds also inherit their sources' errors: OSV withdrew 157 malware reports on May 26, 2026, after automated detections flagged legitimate npm and PyPI packages, including FastAPI.

Catching a malicious release in time requires analyzing the published code before install: install scripts, network access, obfuscation, and what changed from the previous version. That is a different mechanism from vulnerability scanning, not a faster version of it.

The vulnerability scanner is part of your attack surface

#

A vulnerability scanner in CI runs with read access to source code and often with cloud and registry credentials. That makes the scanner a target. In the Trivy GitHub Actions compromise in March 2026, an attacker holding a stolen credential force-pushed 75 of the 76 version tags of aquasecurity/trivy-action, an action referenced by more than 10,000 workflow files on GitHub. Each poisoned tag ran an infostealer first and the real Trivy scan second, so the scan output looked normal. On GitHub-hosted runners, the payload dumped the runner's process memory to extract secrets. The attack was one step in TeamPCP's campaign against security tools.

The controls are ordinary and often skipped. Pin third-party actions to full commit SHAs, not tags. Give the scan job no secrets it does not need. Treat a scanner update like any other dependency update.

How should you prioritize vulnerability scan results?

#

Prioritize vulnerability scan results by evidence of exploitability in your context, not by CVSS severity alone. Severity describes the flaw in the abstract. Whether it matters depends on whether your code reaches the vulnerable function, whether attackers exploit it in the wild, and whether the component is exposed.

CVE-2026-25645 in requests shows why. A scanner flags every project that uses requests below 2.33.0. The maintainers' advisory, GHSA-gc5v-m9x4-r6x2, narrows that sharply: "Standard usage of the Requests library is not affected by this vulnerability." Only applications that call requests.utils.extract_zipped_paths() directly are affected. The NVD scored the CVE 5.5, and GitHub, as the issuing CNA, scored it 4.4. Neither number tells you whether your code calls that function.

Reachability analysis answers that question by building a call graph from your code into each dependency and checking whether any path reaches the vulnerable function. Socket's Full Application Reachability, formerly called Tier 1, typically filters out about 80% of CVE findings as unreachable, and more than 90% in some ecosystems, according to our launch post. The analysis over-approximates by design. When it cannot prove that no path exists, it reports the finding as reachable or unknown.

Exploitation data narrows the list further. NIST now enriches CVEs in CISA's Known Exploited Vulnerabilities (KEV) catalog first, and explained why in its April 2026 announcement: "This change is driven by a surge in CVE submissions, which increased 263% between 2020 and 2025." A queue ordered by reachability, then known exploitation, then exposure is shorter and closer to real risk than one ordered by CVSS. Vulnerability prioritization covers the scoring systems in detail.

Choosing a vulnerability scanning tool

#

The best vulnerability scanning tool is the one whose inventory matches what you ship. No scanner covers every layer, and the identifiers they match on are not interchangeable.

What you need to knowUseWhat it will not tell you
Which hosts expose vulnerable servicesAuthenticated host scanning, plus unauthenticated network scanning for the external viewAnything about application dependencies
Which OS packages in an image are vulnerableAn image scanner that reads distro advisories, not upstream version rangesVendored binaries; malicious packages
Which dependencies have known CVEsA lockfile scanner backed by OSV or GHSA, with alias deduplicationReachability; malware before an advisory; code run by npx
Which of those CVEs matterReachability analysis plus KEV and exploit dataNew vulnerabilities nobody has reported
Whether a new dependency version is maliciousBehavioral analysis of the package code before installKnown CVEs in older versions
Whether the running app is exploitable over HTTPDAST against a staging environmentDependencies and code paths it cannot reach

Our verdict: run a package-native dependency scanner on every lockfile, and an authenticated scanner that reads distro data on every host and image. Do not use CPE-based matching for open source dependencies. Do not treat a clean vulnerability scan as evidence that no malicious code was installed.

The hard problem left is identity. The CVE record format added a packageURL field in version 5.2.0, but nothing requires a CNA to fill it in. The 2026 record for CVE-2026-25645 uses that format version, and its CNA entry still names only vendor psf and product requests. Until advisories ship with package identifiers by default, every vulnerability scanner depends on someone downstream translating a prose description into a matchable record. For CVE-2024-35195, that translation named the wrong language.