Skip to main content

nvd-client

Typed Python client for the National Vulnerability Database CVE and CPE Match APIs (version 2.0).

PyPI Python 3.10–3.14 License: MIT CI Documentation

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.

Contents

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. Use datetime.now(timezone.utc).
  • Named zones on Windows need the tzdata package. timezone.utc does not.
  • One end without the other, an end before the start, or a span longer than 120 days raises InvalidParametersError before any request is sent.
  • A non-datetime value raises InvalidDateFormatError.

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_results for an empty match. Do not check for None.
  • Date arguments must be datetime values. Strings are rejected. Naive values are UTC. Ranges longer than 120 days are rejected locally.
  • pytz is not a dependency. The old import existed so the package could build an unused Asia/Tehran zone, and a fresh pip install failed because pytz was never declared in install_requires.
  • per_page above NVD's maximum raises InvalidParametersError.
  • A proxy passed 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. Pass by_alias=True when 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.

Further reading

Maintained by Ahur4.

Metadata

Release files for nvd-client 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nvd-client 0.2.2
File Size Uploaded Provenance Actions
nvd_client-0.2.2.tar.gz 34.5 kB Without provenance Details for nvd_client-0.2.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for nvd-client 0.2.2
File Interpreter ABI Platform Actions
nvd_client-0.2.2-py3-none-any.whl Python 3 none any Details for nvd_client-0.2.2-py3-none-any.whl

Total release size: 58.0 kB

Release files / nvd_client-0.2.2.tar.gz

Download URL nvd_client-0.2.2.tar.gz
Size 34.5 kB
Tags Source
SHA-256 checksum
How to use checksums
38f60ab874787d57c9ddd491432554e50df35a495c4c8a56138cf37526dbc59e
BLAKE2b-256 checksum
How to use checksums
f1a9d89c35c3317af94da3cbf45778d6ea7487a188d8827869ce85c1c4c05781
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.2-py3-none-any.whl

Download URL nvd_client-0.2.2-py3-none-any.whl
Size 23.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0ede2af6f3bca40718098ef2d13c13037b3d28a20c62549295b0616a5eda8f77
BLAKE2b-256 checksum
How to use checksums
21f12d4f8fcc7d812648d06d65e6ddd7d34f00dc85645233903ac644a2f4d7c1
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.2

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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page