This project was originally designed to compared CVSS scores with EPSS data via a simple command-line interface
Download the scanner and convert it to executable:
wget https://raw.githubusercontent.com/ndouglas-cloudsmith/exploit-check/refs/heads/main/exploit-check.sh
chmod +x exploit-check.sh
If you need to update the scanner (EPSS records are refreshed daily), run the below command:
./exploit-check.sh update
To query a specific CVE (for example, CVE-2021-44228), run the below command:
./exploit-check.sh query CVE-2021-44228
For a full output of KEV findings, use the --full flag
./exploit-check.sh query CVE-2021-44228 --full
List all KEV CVEs (short form):
./exploit-check.sh list
List all KEV CVEs with details:
./exploit-check.sh list --full
| CVE ID | CVSS Severity | CVSS Score | EPSS Percentage | KEV | ExploitDB | OSV |
|---|---|---|---|---|---|---|
CVE-2021-45786 |
CRITICAL | 9.8 | 0.41% | ❌ | ❌ | ❌ |
CVE-2024-0646 |
HIGH | 7.0 | 0.02% | ❌ | ❌ | ✅ |
CVE-2024-25062 |
HIGH | 7.5 | 0.11% | ❌ | ❌ | ✅ |
CVE-2021-44228 |
CRITICAL | 10.0 | 94.47% | ✅ | ✅ | ✅ |
CVE-2024-38285 |
❌ | ❌ | 0.08% | ❌ | ❌ | ❌ |
CVE-2017-0144 |
HIGH | 8.8 | 94.42% | ✅ | ✅ | ❌ |
CVE-2024-20024 |
MEDIUM | 6.0 | 0.02% | ❌ | ❌ | ❌ |
CVE-2014-0160 |
HIGH | 7.5 | 94.45% | ✅ | ✅ | ✅ |
CVE-2024-9482 |
MEDIUM | 5.1 | 0.03% | ❌ | ❌ | ❌ |
CVE-2017-5638 |
CRITICAL | 9.8 | 94.27% | ✅ | ✅ | N/A |
CVE-2024-28085 |
LOW | 3.3 | 9.83% | ❌ | ❌ | ✅ |
CVE-2024-50302 |
MEDIUM | 5.5 | 0.30% | ✅ | ❌ | ✅ |
CVE-2025-47273 |
HIGH | 8.8 | 0.16% | ❌ | ❌ | ✅ |
CVE-2024-6345 |
❌ | ❌ | 4.36% | ❌ | ❌ | ✅ |
CVE-2016-5195 |
HIGH | 7.0 | 94.18% | ✅ | ✅ | ✅ |
CVE-2022-48477 |
MEDIUM | 4.1 | 0.00% | ❌ | ❌ | ❌ |
If you get a CVE ID from Trivy or similar tooling, you get a short-hand response of all the above data for a CVE:
./exploit-check.sh query CVE-2024-6345
If you want to get the associated package-level insights from OSV.dev, you can add the --package-info flag:
./exploit-check.sh query CVE-2024-6345 --package-info
| Ecosystem Name | Description | Example Package Name | Exploit Check Query |
|---|---|---|---|
pypi |
Python packages from the Python Package Index. | requests | ./exploit-check.sh pkg pypi requests |
npm |
JavaScript/TypeScript packages from the Node Package Manager registry. | react | ./exploit-check.sh pkg npm react |
maven |
Java and other JVM language packages. Use the GroupId:ArtifactId format. |
log4j | ./exploit-check.sh pkg maven log4j |
rubygems |
Ruby packages (gems). | rails | ./exploit-check.sh pkg rubygems rails |
pub |
Dart and Flutter packages from the Pub repository. | http | ./exploit-check.sh pkg pub http |
debian |
Debian Linux packages. Includes different versions (eg: debian:11). |
openssl | ./exploit-check.sh pkg debian openssl |
conancenter |
C/C++ packages from ConanCenter. | openssl | ./exploit-check.sh pkg conancenter openssl |
hex |
Elixir and Erlang packages. | phoenix | ./exploit-check.sh pkg hex phoenix |
alpine |
Alpine Linux packages. Includes different versions (eg: alpine:v3.16). |
curl | ./exploit-check.sh pkg alpine curl |
You can use the pkg package command to query the packages of a specific ecosystem like PyPi:
./exploit-check.sh pkg pypi requests
Finally, run the --follow-cves flag to find mapped CVEs found in OSV aliases for these records:
./exploit-check.sh pkg pypi requests --follow-cves
We can scan a public-facing package to understand if it contains a vulnerability:
./exploit-check.sh pkg Alpine:v3.22 setuptools
Users can then scan the associated CVE ID in the same tooling to understand what the realistic risk is associated with that vulnerability:
./exploit-check.sh query CVE-2024-6345 --package-info
Using Github Search I was able to search examples of packages published with the sole purpose of typosquatting within OpenSSF's Malicious Packages project:
Type: TYPOSQUATTING
./exploit-check.sh query MAL-2023-8358
Type: NETWORK_ACTIVITY
./exploit-check.sh query MAL-2025-5829
Type: TYPOSQUATTING
./exploit-check.sh query MAL-2025-2549
Type: TYPOSQUATTING
./exploit-check.sh query MAL-2023-8360
Github Search can also be used to filter for all instances of public-facing Python requirements.txt file that container typosquatted dependencies. This Github Search at least returned one instance of a typosquatted dependency being used in a Python application. Problem is, this process is tedious. I instead need to define a list of typosquatted package names that are already listed in OSSF Malicious Packages project so that I can query those through Github Search to prove that organisations are being impacted by either typosquatting or hallucinated LLM slopsquatting.
./exploit-check.sh pkg npm eslint-plugin-react_editor
If you want to also run query_cve on any discovered CVEs (this will not run query_cve on MAL-* items because they are not CVEs), add --follow-cves:
./exploit-check.sh pkg npm eslint-plugin-react_editor --follow-cves
An explicit example where a single advisory record contains both a CVE and a MAL alias is shown in OSV / GitHub advisory data: the advisory for [email protected] lists CVE-2025-59140 and MAL-2025-46968 as aliases (i.e., the same incident/advisory is tagged with both kinds of IDs).
./exploit-check.sh pkg npm backslash --follow-cves
Why this happens: different authorities/databases label incidents differently — some tracks mark an event as “malicious package” (MAL-...) while vulnerability databases assign CVE identifiers for the same underlying problem (or for related issues discovered in the same package/advisory). OSV/GitHub advisory database often aggregates those aliases into one record when they refer to the same incident.
GHSA-53mq-f4w3-f7qv: [email protected] contains malware after npm account takeover
CVE-2025-59140: "Bug not found, but the following aliases were: GHSA-53mq-f4w3-f7qv GHSA-m2xf-jp99-f298 MAL-2025-46968"
CVE-2021-44228: "aliases": "GHSA-jfh8-c2jp-5v3q"
Users can now query a known advistory to better understand the relationship between the mapped identifiers (CVE-, GHSA-, MAL-) to better understand the risky packages:
./exploit-check.sh query GHSA-53mq-f4w3-f7qv --package-info
OSV’s API accepts a JSON body with package and (optionally) version. If you supply version OSV will return only vulnerabilities that actually match that specific version (or none if it doesn’t match).
# replace 0.2.1 and backslash with whatever you need
curl -s -X POST https://api.osv.dev/v1/query -d '{
"package": {
"ecosystem":"npm",
"name":"backslash"
},
"version":"0.2.1"
}' -H 'Content-Type: application/json' | jq .
Sometimes advisories list affected ranges rather than exact enumerated versions. In that case you can ask OSV for all vulnerabilities for the package and then check whether your version is contained in any affected range (tools/logic required). For example:
# get all vuln records for the package
curl -s -X POST https://api.osv.dev/v1/query -d '{
"package": {
"ecosystem":"npm",
"name":"backslash"
}
}' -H 'Content-Type: application/json' | jq .
Look at each affected[].ranges entry (they use semver ranges or git commit ranges). You can script a semver check (node, python semver library, or use OSV’s own matching logic) to test whether 0.2.1 falls into any range. OSV docs/discussions show this approach when matching exact versions is necessary.
Correct OSV query for an exact version:
curl -s -X POST https://api.osv.dev/v1/query -d '{"package":{"ecosystem":"npm","name":"backslash"},"version":"0.2.1"}' -H 'Content-Type: application/json' | jq
- If that returns nothing, query without
versionand inspectaffected[].rangesto see whether0.2.1falls in any range. - Don’t pass
[email protected]as the package name — split name and version separately. I'm working on this distinction within the code.
Or query the malware finding directly:
curl -s "https://api.osv.dev/v1/vulns/MAL-2023-8358" | jq
Check a single CVE in FIRST's EPSS API & optional compare this against the exploit checker:
./exploit-check.sh query CVE-2022-48477 --full --package-info
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2022-48477" | jq
or query multiple CVEs at the same time:
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2022-48477,CVE-2024-9916" | jq
Using kubectl, we can create custom-columns to understand which container images are running in our Kubernetes cluster.
kubectl get pods -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGES:.spec.containers[*].image'
Haven't built a case for this yet, but eventually we will need to compare capabilities against the actually CVE conditions to be exploited:
kubectl get pods -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,IMAGES:.spec.containers[*].image,CAPS_ADD:.spec.containers[*].securityContext.capabilities.add,CAPS_DROP:.spec.containers[*].securityContext.capabilities.drop'
The goal is to automatically pipe those images into the Trivy scan so we can understand which HIGH or CRITICAL vulnerabilities are in-use.
kubectl get pods -A -o jsonpath='{.items[*].spec.containers[*].image}' \
| tr ' ' '\n' \
| sort -u \
| xargs -n1 trivy image --scanners vuln --severity HIGH,CRITICAL
A much more efficient way to find all CVEs in an active cluster namespace is with the below command:
./exploit-check.sh kubernetes namespace default --severity high,critical
Note: This works on a per-namespace basis and requires Trivy as a prerequisite to perform this automated scan.
We can now find all the in-use container images within your Kubernetes with a simple plain-text search flag:
./exploit-check.sh images namespace -A --source cloudsmith
Do all packages have a signed author? In short, NO. PyPI has:
No cryptographic verification of author/maintainer in metadata.
The only “real” source of truth is the PyPI account(s) listed as maintainers (visible on the web UI, not in the JSON API).
That’s why typosquatting works: the JSON metadata is weak, and the fields can be blank or misleading.
curl https://pypi.org/pypi/fabric/json | jq '.info.name, .info.author, .info.home_page'
You might also want to track the official docs/issues associated with a software package to confirm its validity.
Pro tip: If you do this a lot, you could wrap it into a shell function like:
pypi_urls () {
curl -s https://pypi.org/pypi/$1/json | jq -r '..|strings?|select(test("\\.com|\\.io"))' | grep --color=always -E '\.com|\.io'
}
Then you can run:
pypi_urls requests
If you want/need to delete the local data sources, and remove the Gatekeeper admission controller, you can run the below command:
rm -v -- exploitdb_exploits.csv epss_scores-current.csv epss_scores-current.csv.gz known_exploited_vulnerabilities.json && kubectl delete -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.13/deploy/gatekeeper.yaml
Those sources are pulled down again when you run the update command:
./exploit-check.sh update
Demo: Just making kubectl outputs a bit more readable:
brew install kubecolor
alias kubectl=kubecolor
If you want a neat list of 2025 CVEs with key info:
curl -sL https://kubernetes.io/docs/reference/issues-security/official-cve-feed/index.json \
| jq '.items
| map(select(.date_published | startswith("2025-")))
| sort_by(.date_published)
| reverse
| map({id, date_published, summary})'
Or for a simple table output:
curl -sL https://kubernetes.io/docs/reference/issues-security/official-cve-feed/index.json \
| jq -r '.items
| map(select(.date_published | startswith("2025-")))
| sort_by(.date_published)
| reverse[]
| "\(.date_published)\t\(.id)\t\(.summary)"'