# 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

Source: https://socket.dev/glossary/vulnerability-scanning

Last updated: 2026-10-19

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](https://socket.dev/glossary/common-vulnerabilities-and-exposures-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](https://socket.dev/glossary/national-vulnerability-database-nvd). Dependency scanners key on ecosystem package names from [OSV](https://socket.dev/glossary/osv) and the [GitHub Advisory Database](https://socket.dev/glossary/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](https://socket.dev/blog/cve-volume-surges-past-48k-in-2025). 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](https://socket.dev/blog/nist-officially-stops-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 type | What it inspects | How it identifies software | Main data source | Blind spot |
| --- | --- | --- | --- | --- |
| Network scanner, unauthenticated | Open ports, from outside the host | Service banners and protocol probes, translated to CPE | NVD CPE configurations | Backported fixes; anything not listening on a port |
| Host scanner, authenticated or agent | The OS package database (dpkg, rpm) | Distro package name and full distro version | Distro security trackers (Debian, Ubuntu, Red Hat) | Software installed outside the package manager |
| Container image scanner | Image layers: OS packages and language manifests | OS packages plus lockfiles found in the image | Distro trackers plus OSV and GHSA | Binaries copied in without package metadata |
| Dependency scanner (SCA) | Manifests and lockfiles in a repository | Ecosystem, package name, and version, as a package URL (PURL) | OSV, GHSA, registry advisory feeds | Vendored code; tools run by npx; malware before an advisory |
| Web application scanner (DAST) | A running app's HTTP surface | Needs no versions; sends payloads and reads responses | Its own library of checks | Code paths it cannot reach; business logic |

Dependency scanning is the core of [software composition analysis](https://socket.dev/glossary/software-composition-analysis), and it is the kind of scanning most open source supply chain work depends on. [Dynamic application security testing](https://socket.dev/glossary/dynamic-application-security-testing-dast) 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 four steps of vulnerability scanning for a network scanner and a dependency scanner, with the artifacts each produces and where each step breaks

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:

```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.

> Four records for CVE-2024-35195 in Python requests, and what a scanner can do with each

| Record | Identifier for the affected software | Version data | What a scanner does with it |
| --- | --- | --- | --- |
| CNA record (GitHub) | vendor psf, product requests | The string "\< 2.32.0", with no version type | People can read it; a machine cannot parse it as a range |
| CISA ADP enrichment | cpe:2.3:a:request\_project:request:2.32.0:\*:\*:\*:\*:node.js:\*:\* | version 0, lessThan 2.32.0 | A CPE scanner matches the Node.js request package and misses Python requests |
| NVD | None: API status Deferred, not scheduled for enrichment, no configurations block | None | A scanner that reads NVD CPE configurations finds nothing |
| OSV via PyPI (GHSA-9wx4-h78v-vm56) | pkg:pypi/requests | fixed\_in 2.32.0 | A package scanner matches requests 2.31.0 correctly |

The [CVE Services record](https://cveawg.mitre.org/api/cve/CVE-2024-35195) carries an enrichment from [CISA's Vulnrichment program](https://socket.dev/blog/cisa-launches-vulnrichment-project-as-nvd-backlog-hits-10-000), 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](https://nvd.nist.gov/vuln/detail/CVE-2024-35195) 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 mode | Direction | What happens | Example |
| --- | --- | --- | --- |
| Backported fix | False positive | The distro patches the flaw but keeps the upstream version number | Ubuntu 24.04's openssl reports 3.0.13 and carries 44 CVE fixes |
| Missing or wrong identifier | False negative | No CPE, or a CPE for a different product | CVE-2024-35195 has no NVD configurations |
| Duplicate aliases | Inflated count | GHSA and PYSEC records for one CVE are counted twice | pip-audit reports 6 findings for 3 CVEs |
| Vendored or bundled code | False negative | The code ships inside another package and is absent from any manifest | A package shipped a tampered axios inside its own node\_modules |
| Code outside the lockfile | False negative | npx and global installs resolve dependencies at run time | CI tools run by npx during the axios compromise |
| Unreachable vulnerable code | Noise | The vulnerable function is never called | CVE-2026-25645 affects only callers of one helper function |
| Bad feed record | False positive | A wrong advisory propagates to every consumer | OSV 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](https://osv.dev/vulnerability/CVE-2024-6119). 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](https://socket.dev/blog/axios-npm-package-compromised), 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](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise), 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](https://socket.dev/glossary/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](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack) 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](https://osv.dev/vulnerability/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](https://socket.dev/blog/osv-withdraws-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](https://socket.dev/supply-chain-attacks/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](https://socket.dev/blog/teampcp-targeting-security-tools-across-oss-ecosystem).

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](https://github.com/advisories/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](https://socket.dev/glossary/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](https://socket.dev/blog/introducing-tier-1-reachability). 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](https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth): "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](https://socket.dev/glossary/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 know | Use | What it will not tell you |
| --- | --- | --- |
| Which hosts expose vulnerable services | Authenticated host scanning, plus unauthenticated network scanning for the external view | Anything about application dependencies |
| Which OS packages in an image are vulnerable | An image scanner that reads distro advisories, not upstream version ranges | Vendored binaries; malicious packages |
| Which dependencies have known CVEs | A lockfile scanner backed by OSV or GHSA, with alias deduplication | Reachability; malware before an advisory; code run by npx |
| Which of those CVEs matter | Reachability analysis plus KEV and exploit data | New vulnerabilities nobody has reported |
| Whether a new dependency version is malicious | Behavioral analysis of the package code before install | Known CVEs in older versions |
| Whether the running app is exploitable over HTTP | DAST against a staging environment | Dependencies 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.
