nvd-client
Typed Python client for the National Vulnerability Database CVE and CPE Match APIs (version 2.0).
Documentation: https://ahur4.github.io/nvd-client/
The National Vulnerability Database is NIST's repository of publicly disclosed vulnerabilities. Each record is a CVE, with descriptions, references, CVSS scores, weaknesses, and the CPE configurations that say which products are affected. nvd-client calls the NVD REST API 2.0 and returns those documents as pydantic models.
Use it when a vulnerability scanner, asset inventory, or research notebook needs CVE or CPE match data and should not parse raw JSON by hand. It is a read-only client. It does not submit data to NVD.
This page describes 0.2.1. The client behaves the same as 0.2.0; this release adds the documentation site and the PyPI project links. The PyPI badge tracks the release that is published today. Until 0.2.1 is published, pip install nvd-client installs 0.2.0.
Contents
- Documentation
- Installation
- Quickstart
- Configuration
- Rate limits and retries
- Return values
- Errors
- Migrating from 0.1.x
- Development
- Further reading
Installation
Python 3.10 or newer.
pip install nvd-client
HTTP and HTTPS proxies work with the base install. SOCKS proxies need the extra:
pip install "nvd-client[socks]"
From a checkout, including the linters and test runner:
pip install -e ".[dev]"
Quickstart
Pass an API key from the environment when you have one. The same calls work without a key; they then use NVD's public rate (see Rate limits and retries).
This program looks up one CVE, one page of CVEs published in the last seven days, and the CPE match strings for that CVE. Seven days is inside NVD's 120-day maximum. The datetimes are timezone-aware UTC.
import os
from datetime import datetime, timedelta, timezone
from nvd_client import NvdApi, NvdConfig
config = NvdConfig(
api_key=os.environ.get("NVD_API_KEY"),
results_per_page=100,
)
end = datetime.now(timezone.utc)
start = end - timedelta(days=7)
with NvdApi(config=config) as api:
record = api.get_cve_by_id("CVE-2024-30078")
print(record.id, record.vuln_status)
print(record.description())
print(record.cwe_ids)
for metric in record.metrics.cvss_metric_v31:
data = metric.cvss_data
print(metric.type, data.base_score, data.vector_string)
page = api.get_cve_by_date(
publish_start_date=start,
publish_end_date=end,
per_page=50,
)
print(f"{len(page.cves)} of {page.total_results}")
for cve in page.cves:
print(cve.id, cve.published)
found = api.get_cpes_by_cve("CVE-2024-30078", per_page=20)
for item in found.matches:
print(item.criteria, item.status, len(item.matches))
get_cve_by_id returns a CveRecord. A missing id raises NvdNotFoundError. get_cve_by_date returns one page, a CveListResponse: total_results is the size of the whole match, and cves is only this page. get_cpes_by_cve calls the CPE Match API (/cpematch/2.0) and returns a CpeMatchResponse. A page with total_results == 0 matched nothing. It is not an error.
get_cve_by_cpe goes the other way: CVEs whose applicability statement matches a CPE name. NVD requires a real part, vendor, product, and version in that name (* is not enough for those components). The same publish and modify date arguments are accepted.
from nvd_client import NvdApi
with NvdApi() as api:
windows = api.get_cve_by_cpe(
"cpe:2.3:o:microsoft:windows_10:1607:*:*:*:*:*:*:*",
per_page=20,
)
for cve in windows.cves:
print(cve.id)
modify_start_date and modify_end_date filter on lastModified. A publish range and a modify range may be sent in one call. Each pair still has to be complete and no longer than 120 days.
To read past the first page, advance offset by the page size. There is no automatic pager.
import os
from datetime import datetime, timedelta, timezone
from nvd_client import NvdApi, NvdConfig
config = NvdConfig(api_key=os.environ.get("NVD_API_KEY"))
end = datetime.now(timezone.utc)
start = end - timedelta(days=7)
offset = 0
with NvdApi(config=config) as api:
while True:
page = api.get_cve_by_date(
per_page=200,
offset=offset,
publish_start_date=start,
publish_end_date=end,
)
for cve in page.cves:
print(cve.id)
offset = page.start_index + page.results_per_page
if not page.cves or offset >= page.total_results:
break
Dates
The client sends YYYY-MM-DDTHH:MM:SS.sss+00:00. requests percent-encodes the + as %2B, which is what NVD requires.
- An aware datetime, including one built with
zoneinfo.ZoneInfo, is converted to UTC. - A naive datetime is treated as UTC, not as the machine's local zone.
datetime.now()is naive. Usedatetime.now(timezone.utc). - Named zones on Windows need the
tzdatapackage.timezone.utcdoes not. - One end without the other, an end before the start, or a span longer than 120 days raises
InvalidParametersErrorbefore any request is sent. - A non-
datetimevalue raisesInvalidDateFormatError.
Timestamps in NVD responses are parsed as datetime. Values without an offset stay naive. Values with Z or a numeric offset are aware.
Configuration
NvdConfig is frozen. Build a new config and a new client to change it. NvdApi(api_key) and NvdApi(api_key, proxy) still work. A non-None api_key or proxy argument overrides the same field on config. An empty or whitespace api_key is stored as None.
repr(client) reports whether a key is set. It does not include the key.
| Field | Default | What it does |
|---|---|---|
base_url |
https://services.nvd.nist.gov/rest/json |
CVE calls use {base_url}/cves/2.0. CPE match calls use {base_url}/cpematch/2.0. Must be http or https. |
api_key |
None |
Sent as the apiKey header. Raises the NVD rate limit from 5 to 50 requests per 30 seconds. |
timeout |
30 |
Seconds, or a (connect, read) pair. Passed through to requests. Each part must be greater than 0. |
results_per_page |
2000 |
CVE page size when a call omits per_page. NVD's maximum is 2000. |
cpe_results_per_page |
500 |
CPE Match page size when a call omits per_page. NVD's maximum is 500. |
rate_limit_delay |
None |
Seconds to wait between HTTP calls. None selects 6.0 without a key and 0.6 with one. 0 disables the wait. |
max_retries |
4 |
Extra attempts after the first request. |
backoff_factor |
1.0 |
Wait backoff_factor * 2**attempt seconds between retries, unless Retry-After is set. |
max_backoff |
60 |
Cap on that computed wait. A Retry-After value is not capped. |
user_agent |
nvd-client/<version> (+https://github.com/ahur4/nvd-client) |
User-Agent header. |
proxy |
None |
Used for both http and https. A proxy= argument on a single call replaces it for that call. |
per_page=None on a method uses the config default. per_page outside 1..2000 (CVE) or 1..500 (CPE match), or a negative offset, raises InvalidParametersError.
Invalid config values raise ValueError when the config is constructed: a non-HTTP base URL, a non-positive timeout, a page size outside NVD's maximum, or a negative retry or backoff setting.
When proxy is left unset, requests still honors HTTP_PROXY and HTTPS_PROXY. A socks5:// or socks5h:// URL needs the socks extra.
import os
from nvd_client import NvdApi, NvdConfig
config = NvdConfig(
api_key=os.environ.get("NVD_API_KEY"),
timeout=(5.0, 60.0),
results_per_page=100,
user_agent="research-scanner/1.0",
)
with NvdApi(config=config) as api:
page = api.get_all_cves()
Request a key from NIST: https://nvd.nist.gov/developers/request-an-api-key.
Use the client as a context manager, or call close(), so the connection pool is released. One client may be shared across threads. Calls take turns on the rate-limit clock.
Rate limits and retries
NVD's published windows, from the API start page:
| Credential | Limit | Default delay |
|---|---|---|
| No API key | 5 requests in a rolling 30 seconds | 6.0 seconds |
| API key | 50 requests in a rolling 30 seconds | 0.6 seconds |
The default delay is the window divided by the number of allowed requests, so a single client stays inside the limit. The first call does not wait. NIST also recommends sleeping for several seconds between requests so bursts are not rejected. Set rate_limit_delay=6 to do that even when a key is configured.
The client retries connection errors and HTTP 403, 429, 500, 502, 503, and 504. NVD uses 403 both for rate limits and for a rejected key. A 403 whose message says the API key is invalid is not retried. Any other 4xx, including 400 and 404, fails immediately.
With the defaults, a failure is followed by waits of 1, 2, 4, and 8 seconds (backoff_factor * 2**attempt), then the error is raised. max_backoff caps that series. If the response has Retry-After (a number of seconds or an HTTP date), that value replaces the computed wait.
After the retries are used up, a 403 or 429 raises NvdRateLimitError. Other exhausted failures raise NvdApiError.
Return values
Every successful call returns a pydantic model. Nothing returns a dict or None.
| Method | Endpoint | Returns |
|---|---|---|
get_all_cves(per_page=None, offset=0, proxy=None) |
/cves/2.0 |
CveListResponse |
get_cve_by_id(cve_id, proxy=None) |
/cves/2.0 |
CveRecord |
get_cve_by_date(...) |
/cves/2.0 |
CveListResponse |
get_cve_by_cpe(cpe_name, ...) |
/cves/2.0 |
CveListResponse |
get_cpes_by_cve(cve_id, ...) |
/cpematch/2.0 |
CpeMatchResponse |
get_cpes_by_criteria(criteria, ...) |
/cpematch/2.0 |
CpeMatchResponse |
get_cpes_by_criteria_name_id(criteria_name_id, ...) |
/cpematch/2.0 |
CpeMatchResponse |
get_cve_by_date and get_cve_by_cpe also take per_page, offset, publish_start_date, publish_end_date, modify_start_date, modify_end_date, and proxy. The CPE match methods take per_page, offset, and proxy.
Serialization
Attributes are snake_case. The NVD names are aliases, so both forms validate.
record.model_dump()
record.model_dump(by_alias=True)
record.model_dump(by_alias=True, mode="json", exclude_none=True)
record.model_dump_json(by_alias=True, indent=2)
model_dump() uses Python names (last_modified) and leaves datetime objects in place. by_alias=True uses the NVD names (lastModified). mode="json" and model_dump_json() render datetimes as ISO strings.
Unknown JSON keys are kept (extra="allow") and show up in model_dump(). A field NVD adds later does not drop the rest of the document and is not discarded. Metric groups that NVD omits are empty lists, not None.
CveListResponse
| Attribute | NVD name | Meaning |
|---|---|---|
results_per_page |
resultsPerPage |
Records in this response |
start_index |
startIndex |
Offset NVD used |
total_results |
totalResults |
Size of the full match |
format |
format |
Response format name |
version |
version |
API version string |
timestamp |
timestamp |
When NVD generated the page |
vulnerabilities |
vulnerabilities |
CveItem wrappers |
cves |
CveRecord objects on this page |
CveRecord
description(lang="en") returns that language's text, or None. cwe_ids is the weakness description values (CWE-787, NVD-CWE-noinfo, and similar).
| Attribute | NVD name | Notes |
|---|---|---|
id |
id |
CVE-YYYY-NNNN |
source_identifier |
sourceIdentifier |
|
vuln_status |
vulnStatus |
|
published |
published |
datetime |
last_modified |
lastModified |
datetime |
descriptions |
descriptions |
LangString (lang, value) |
references |
references |
url, optional source and tags |
metrics |
metrics |
CVSS v2, v3.0, v3.1, v4.0, and SSVC |
weaknesses |
weaknesses |
Weakness: source, type, description |
configurations |
configurations |
CPE applicability tree |
affected |
affected |
Affected: source, affected_data |
cve_tags |
cveTags |
CveTag: source_identifier, tags |
vendor_comments |
vendorComments |
organization, comment, last_modified |
evaluator_comment |
evaluatorComment |
|
evaluator_solution |
evaluatorSolution |
|
evaluator_impact |
evaluatorImpact |
|
cisa_exploit_add |
cisaExploitAdd |
date, when the CVE is in the KEV catalog |
cisa_action_due |
cisaActionDue |
date |
cisa_required_action |
cisaRequiredAction |
|
cisa_vulnerability_name |
cisaVulnerabilityName |
Metrics holds cvss_metric_v2, cvss_metric_v30, cvss_metric_v31, cvss_metric_v40, and ssvc_v203. Each CVSS metric has source, type (Primary or Secondary), and cvss_data (version, vector_string, base_score, plus that version's base metrics). v2, v3.0, and v3.1 wrappers also carry exploitability_score and impact_score when NVD sends them. v2 adds base_severity and the privilege flags. v4 scores that NVD attaches to the vector (threat, environmental) live on cvss_data.
A configuration node (ConfigNode) has operator, negate, and cpe_match. Each CpeMatch has vulnerable, criteria, match_criteria_id, and the optional version bounds. AffectedProduct carries vendor, product, versions, and the other product fields from the NVD affected schema.
The full field list is nvd_client.models.
CpeMatchResponse
| Attribute | NVD name | Meaning |
|---|---|---|
results_per_page |
resultsPerPage |
Match strings in this response |
start_index |
startIndex |
Offset NVD used |
total_results |
totalResults |
Size of the full match |
format |
format |
|
version |
version |
|
timestamp |
timestamp |
|
match_strings |
matchStrings |
Wrappers around CpeMatchString |
matches |
The CpeMatchString objects on this page |
CpeMatchString has criteria, match_criteria_id, created, last_modified, status, optional version bounds, cpe_last_modified, and matches (CpeNameMatch: cpe_name, cpe_name_id).
Errors
NvdClientError
├── InvalidDateFormatError
├── InvalidParametersError
├── InvalidCVEIDError
├── NvdApiError
│ ├── NvdRateLimitError
│ └── NvdNotFoundError
└── NvdResponseError
| Exception | When it is raised |
|---|---|
InvalidCVEIDError |
The id is not CVE-YYYY-NNNN with at least four digits in the sequence. No request is sent. |
InvalidDateFormatError |
A date argument is not a datetime. |
InvalidParametersError |
A date range is incomplete, reversed, or longer than 120 days. per_page or offset is out of range. A required CPE name, criteria, or match-criteria id is blank. |
ValueError |
NvdConfig itself is invalid (base URL, timeout, page-size bounds, negative retry settings). |
NvdNotFoundError |
HTTP 404, or get_cve_by_id received an empty page. An empty id lookup is still HTTP 200, so status_code is 200. Subclass of NvdApiError. |
NvdRateLimitError |
HTTP 403 or 429 after retries. Subclass of NvdApiError. |
NvdApiError |
Any other HTTP error, including a rejected API key, and connection or timeout failures after retries. |
NvdResponseError |
The body was not JSON, or it did not match the pydantic model. |
NvdApiError (and its subclasses) expose status_code and url. status_code is None when the failure happened before a response, for example a timeout.
from nvd_client import NvdApi, NvdNotFoundError, NvdRateLimitError
with NvdApi() as api:
try:
record = api.get_cve_by_id("CVE-2024-30078")
except NvdNotFoundError as exc:
print(exc.status_code, exc)
except NvdRateLimitError:
raise
Migrating from 0.1.x
0.2.0 changes the return types and stops hiding failed requests. The method names are the same.
# 0.1.x returned a dict, or None when the request failed.
raw = api.get_cve_by_id("CVE-2024-30078")
if raw is None:
...
else:
cve_id = raw["vulnerabilities"][0]["cve"]["id"]
# 0.2.0 returns a CveRecord and raises when the CVE is missing.
from nvd_client import NvdNotFoundError
try:
record = api.get_cve_by_id("CVE-2024-30078")
except NvdNotFoundError:
...
else:
cve_id = record.id
document = record.model_dump(by_alias=True, mode="json")
Collection calls (get_all_cves, get_cve_by_date, get_cve_by_cpe) return CveListResponse. Read page.cves, not page["vulnerabilities"]. CPE match calls return CpeMatchResponse.
Other breaks:
- Python 3.10 or newer is required.
- Failures raise the exceptions in Errors. Check
total_resultsfor an empty match. Do not check forNone. - Date arguments must be
datetimevalues. Strings are rejected. Naive values are UTC. Ranges longer than 120 days are rejected locally. pytzis not a dependency. The old import existed so the package could build an unusedAsia/Tehranzone, and a freshpip installfailed becausepytzwas never declared ininstall_requires.per_pageabove NVD's maximum raisesInvalidParametersError.- A
proxypassed to one call overrides the client proxy. In 0.1.x the client proxy won and the argument was ignored. str(client)describes the base URL and whether a key is set.model_dump()is the replacement for treating the response as a dictionary. Passby_alias=Truewhen you need NVD's camelCase keys.
See CHANGELOG.md for the dependency changes that went with this release.
Development
pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy --strict nvd_client
pytest
Documentation site:
pip install -e ".[docs]"
mkdocs build --strict
mkdocs serve
Tests mock HTTP. They do not call NVD. GitHub Actions runs the same checks on Python 3.10 through 3.14, and runs mkdocs build --strict on pull requests. Pushes to main publish the site to GitHub Pages. The Pages source in the repository settings has to be GitHub Actions before that first deploy can succeed.
Further reading
- Documentation
- NVD API quick start, including the rate limits
- CVE API
- CPE and CPE Match APIs
- Request an API key
- Changelog
- MIT license
There is no CONTRIBUTING guide in this repository.
Maintained by Ahur4.
Metadata
Release files for nvd-client 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| nvd_client-0.2.1.tar.gz | 34.8 kB | Without provenance | Details for nvd_client-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| nvd_client-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for nvd_client-0.2.1-py3-none-any.whl |
Total release size: 58.5 kB
Release files / nvd_client-0.2.1.tar.gz
| Download URL | nvd_client-0.2.1.tar.gz |
|---|---|
| Size | 34.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d433cd5138b1412f2150e99b54d191f79adaac1d7d09d61c97d7c6f4a99ad0c
|
|
BLAKE2b-256 checksum How to use checksums |
4c83c0bb7873b084a7a1fb4f1a79bbb66dfb3b9bc50e5417be998fb916323c33
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / nvd_client-0.2.1-py3-none-any.whl
| Download URL | nvd_client-0.2.1-py3-none-any.whl |
|---|---|
| Size | 23.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fdb02d616f61dc3e2806dec6b602da906b46ccc3ad7a9551e25dd206f4bd2872
|
|
BLAKE2b-256 checksum How to use checksums |
1b2a1cbf8ea21c77bab0600c5db18a04610c44da9aea05005ac238643f20031d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Security for nvd-client 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.