From 4e1de3c51062466a0679f44768e9e306800070a6 Mon Sep 17 00:00:00 2001
From: Alex <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 10:44:28 +0100
Subject: [PATCH 1/7] docs: add NVD API 2.0 compatibility examples and tests
Covers the NvdApi client added in 0.1.4: a README section with sync and async
examples that look up a CVE and page through a date range with cursor, plus
sync and async integration tests for all five endpoints and the cursor walk.
---
README.md | 83 +++++++++++++++++++++++++++++++++++
README.template | 25 +++++++++++
tests/integration_aio.py | 24 ++++++++++
tests/integration_test.py | 52 ++++++++++++++++++++++
tests/nvd.py | 28 ++++++++++++
tests/nvd_aio.py | 36 +++++++++++++++
tests/readme_examples_test.py | 2 +
7 files changed, 250 insertions(+)
create mode 100644 tests/nvd.py
create mode 100644 tests/nvd_aio.py
diff --git a/README.md b/README.md
index 74aaa20f..eff38289 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Bring the VulnCheck API to your Python applications.
- [CPE](#cpe)
- [Index](#index)
- [Indices](#indices)
+ - [NVD API 2.0 compatibility](#nvd-api-20-compatibility)
- [Pagination](#pagination)
- [PURL](#purl)
- [Proxy Support](#proxy-support)
@@ -626,6 +627,88 @@ if __name__ == "__main__":
```
+
+
+### NVD API 2.0 compatibility
+
+Query the NVD API 2.0 compatible endpoints under `/rest/json`. They take NVD's parameters and return NVD's response shapes, served from VulnCheck's data, and work with any VulnCheck API token, including community. `NvdApi` covers CVEs, CVE change history, CPEs, CPE match criteria and sources.
+
+Look up `CVE-2021-44228`, then page through a date range using `cursor`
+
+```python
+import vulncheck_sdk
+import os
+
+TOKEN = os.environ["VULNCHECK_API_TOKEN"]
+
+configuration = vulncheck_sdk.Configuration()
+configuration.api_key["Bearer"] = TOKEN
+
+with vulncheck_sdk.ApiClient(configuration) as api_client:
+ nvd_client = vulncheck_sdk.NvdApi(api_client)
+
+ # Look up a single CVE, shaped exactly as NVD's API returns it
+ api_response = nvd_client.nvd_cves_get(cve_id="CVE-2021-44228")
+ cve = api_response.vulnerabilities[0].cve
+ print(cve.id, cve.vuln_status)
+
+ # Walk every CVE modified in a date range. Sending an empty cursor opts in
+ # to cursor paging; each response then carries the cursor for the next page
+ cursor = ""
+ while cursor is not None:
+ api_response = nvd_client.nvd_cves_get(
+ last_mod_start_date="2026-09-01T00:00:00.000",
+ last_mod_end_date="2026-09-02T00:00:00.000",
+ results_per_page=500,
+ cursor=cursor,
+ )
+ print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ cursor = api_response.next_cursor
+```
+
+
+Click to View Async Implementation
+
+```python
+import asyncio
+import os
+import vulncheck_sdk.aio as vcaio
+
+TOKEN = os.environ.get("VULNCHECK_API_TOKEN")
+
+configuration = vcaio.Configuration()
+configuration.api_key["Bearer"] = TOKEN
+
+
+async def main():
+ async with vcaio.ApiClient(configuration) as api_client:
+ nvd_client = vcaio.NvdApi(api_client)
+
+ # Look up a single CVE, shaped exactly as NVD's API returns it
+ api_response = await nvd_client.nvd_cves_get(cve_id="CVE-2021-44228")
+ cve = api_response.vulnerabilities[0].cve
+ print(cve.id, cve.vuln_status)
+
+ # Walk every CVE modified in a date range. Sending an empty cursor opts
+ # in to cursor paging; each response then carries the cursor for the
+ # next page
+ cursor = ""
+ while cursor is not None:
+ api_response = await nvd_client.nvd_cves_get(
+ last_mod_start_date="2026-09-01T00:00:00.000",
+ last_mod_end_date="2026-09-02T00:00:00.000",
+ results_per_page=500,
+ cursor=cursor,
+ )
+ print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ cursor = api_response.next_cursor
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+
### Pagination
diff --git a/README.template b/README.template
index 57c1f4d5..03ccac0b 100644
--- a/README.template
+++ b/README.template
@@ -21,6 +21,7 @@ Bring the VulnCheck API to your Python applications.
- [CPE](#cpe)
- [Index](#index)
- [Indices](#indices)
+ - [NVD API 2.0 compatibility](#nvd-api-20-compatibility)
- [Pagination](#pagination)
- [PURL](#purl)
- [Proxy Support](#proxy-support)
@@ -199,6 +200,30 @@ with open("./tests/indicies_aio.py", 'r') as f:
+### NVD API 2.0 compatibility
+
+Query the NVD API 2.0 compatible endpoints under `/rest/json`. They take NVD's parameters and return NVD's response shapes, served from VulnCheck's data, and work with any VulnCheck API token, including community. `NvdApi` covers CVEs, CVE change history, CPEs, CPE match criteria and sources.
+
+Look up `CVE-2021-44228`, then page through a date range using `cursor`
+
+
+
+
+Click to View Async Implementation
+
+
+
+
+
+
### Pagination
Paginate over results for a query to VulnCheck-KEV using `cursor`
diff --git a/tests/integration_aio.py b/tests/integration_aio.py
index 33195e62..696f1e48 100644
--- a/tests/integration_aio.py
+++ b/tests/integration_aio.py
@@ -3,6 +3,7 @@
import vulncheck_sdk.aio as vcaio
from vulncheck_sdk.aio.api.endpoints_api import EndpointsApi
from vulncheck_sdk.aio.api.indices_api import IndicesApi
+from vulncheck_sdk.aio.api.nvd_api import NvdApi
from vulncheck_sdk.aio.exceptions import ApiException, UnauthorizedException
API_TOKEN = os.environ.get("VULNCHECK_API_TOKEN", "")
@@ -111,6 +112,28 @@ async def _check_all_indices(indices: IndicesApi) -> None:
# --- Orchestrator ---
+# --- NvdApi Tests ---
+
+
+async def _check_nvd(api: NvdApi) -> None:
+ print("Testing NvdApi")
+ cve_id = "CVE-2021-44228"
+
+ cves = await api.nvd_cves_get(cve_id=cve_id)
+ assert cves.total_results == 1
+ assert cves.vulnerabilities[0].cve.id == cve_id
+
+ first = await api.nvd_cves_get(cve_tag="disputed", results_per_page=5, cursor="")
+ assert first.next_cursor
+ second = await api.nvd_cves_get(cve_tag="disputed", results_per_page=5, cursor=first.next_cursor)
+ assert second.vulnerabilities[0].cve.id != first.vulnerabilities[0].cve.id
+
+ assert (await api.nvd_cve_history_get(cve_id=cve_id)).cve_changes
+ assert (await api.nvd_cpes_get(cpe_match_string="cpe:2.3:a:apache:log4j", results_per_page=5)).products
+ assert (await api.nvd_cpe_match_get(cve_id=cve_id, results_per_page=5)).match_strings
+ assert (await api.nvd_source_get(results_per_page=5)).sources
+
+
async def main() -> None:
if not API_TOKEN:
print("Warning: VULNCHECK_API_TOKEN is not set.")
@@ -137,6 +160,7 @@ async def main() -> None:
)
await _check_all_indices(indices_api)
+ await _check_nvd(vcaio.NvdApi(client))
# --- CRITICAL FIX FOR "UNCLOSED CONNECTOR" ---
# aiohttp requires a small window of time to allow the
diff --git a/tests/integration_test.py b/tests/integration_test.py
index e8b3a303..83ca7eab 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -2,6 +2,7 @@
import vulncheck_sdk
from vulncheck_sdk.api.endpoints_api import EndpointsApi
from vulncheck_sdk.api.indices_api import IndicesApi
+from vulncheck_sdk.api.nvd_api import NvdApi
from vulncheck_sdk.api_response import ApiResponse
from vulncheck_sdk.exceptions import ApiException, UnauthorizedException
@@ -101,6 +102,49 @@ def test_all_indicies():
assert status == 200
+"""
+NvdApi Tests
+"""
+
+NVD_CVE_ID = "CVE-2021-44228"
+
+
+def test_nvd_cves_get():
+ response = _get_nvd_instance(API_TOKEN).nvd_cves_get(cve_id=NVD_CVE_ID)
+ assert response.total_results == 1
+ assert response.vulnerabilities[0].cve.id == NVD_CVE_ID
+
+
+def test_nvd_cves_get_cursor():
+ nvd = _get_nvd_instance(API_TOKEN)
+ first = nvd.nvd_cves_get(cve_tag="disputed", results_per_page=5, cursor="")
+ assert first.next_cursor
+ second = nvd.nvd_cves_get(cve_tag="disputed", results_per_page=5, cursor=first.next_cursor)
+ assert second.vulnerabilities[0].cve.id != first.vulnerabilities[0].cve.id
+
+
+def test_nvd_cve_history_get():
+ response = _get_nvd_instance(API_TOKEN).nvd_cve_history_get(cve_id=NVD_CVE_ID)
+ assert response.cve_changes
+
+
+def test_nvd_cpes_get():
+ response = _get_nvd_instance(API_TOKEN).nvd_cpes_get(
+ cpe_match_string="cpe:2.3:a:apache:log4j", results_per_page=5
+ )
+ assert response.products
+
+
+def test_nvd_cpe_match_get():
+ response = _get_nvd_instance(API_TOKEN).nvd_cpe_match_get(cve_id=NVD_CVE_ID, results_per_page=5)
+ assert response.match_strings
+
+
+def test_nvd_source_get():
+ response = _get_nvd_instance(API_TOKEN).nvd_source_get(results_per_page=5)
+ assert response.sources
+
+
"""
Helpers
"""
@@ -138,3 +182,11 @@ def _get_indices_instance(token: str = "") -> IndicesApi:
config.api_key["Bearer"] = token
client = vulncheck_sdk.ApiClient(config)
return vulncheck_sdk.IndicesApi(client)
+
+
+def _get_nvd_instance(token: str = "") -> NvdApi:
+ config = vulncheck_sdk.Configuration()
+ if token != "":
+ config.api_key["Bearer"] = token
+ client = vulncheck_sdk.ApiClient(config)
+ return vulncheck_sdk.NvdApi(client)
diff --git a/tests/nvd.py b/tests/nvd.py
new file mode 100644
index 00000000..0dfb95f3
--- /dev/null
+++ b/tests/nvd.py
@@ -0,0 +1,28 @@
+import vulncheck_sdk
+import os
+
+TOKEN = os.environ["VULNCHECK_API_TOKEN"]
+
+configuration = vulncheck_sdk.Configuration()
+configuration.api_key["Bearer"] = TOKEN
+
+with vulncheck_sdk.ApiClient(configuration) as api_client:
+ nvd_client = vulncheck_sdk.NvdApi(api_client)
+
+ # Look up a single CVE, shaped exactly as NVD's API returns it
+ api_response = nvd_client.nvd_cves_get(cve_id="CVE-2021-44228")
+ cve = api_response.vulnerabilities[0].cve
+ print(cve.id, cve.vuln_status)
+
+ # Walk every CVE modified in a date range. Sending an empty cursor opts in
+ # to cursor paging; each response then carries the cursor for the next page
+ cursor = ""
+ while cursor is not None:
+ api_response = nvd_client.nvd_cves_get(
+ last_mod_start_date="2026-09-01T00:00:00.000",
+ last_mod_end_date="2026-09-02T00:00:00.000",
+ results_per_page=500,
+ cursor=cursor,
+ )
+ print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ cursor = api_response.next_cursor
diff --git a/tests/nvd_aio.py b/tests/nvd_aio.py
new file mode 100644
index 00000000..1f87f92d
--- /dev/null
+++ b/tests/nvd_aio.py
@@ -0,0 +1,36 @@
+import asyncio
+import os
+import vulncheck_sdk.aio as vcaio
+
+TOKEN = os.environ.get("VULNCHECK_API_TOKEN")
+
+configuration = vcaio.Configuration()
+configuration.api_key["Bearer"] = TOKEN
+
+
+async def main():
+ async with vcaio.ApiClient(configuration) as api_client:
+ nvd_client = vcaio.NvdApi(api_client)
+
+ # Look up a single CVE, shaped exactly as NVD's API returns it
+ api_response = await nvd_client.nvd_cves_get(cve_id="CVE-2021-44228")
+ cve = api_response.vulnerabilities[0].cve
+ print(cve.id, cve.vuln_status)
+
+ # Walk every CVE modified in a date range. Sending an empty cursor opts
+ # in to cursor paging; each response then carries the cursor for the
+ # next page
+ cursor = ""
+ while cursor is not None:
+ api_response = await nvd_client.nvd_cves_get(
+ last_mod_start_date="2026-09-01T00:00:00.000",
+ last_mod_end_date="2026-09-02T00:00:00.000",
+ results_per_page=500,
+ cursor=cursor,
+ )
+ print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ cursor = api_response.next_cursor
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/readme_examples_test.py b/tests/readme_examples_test.py
index 06d528e0..931beba8 100644
--- a/tests/readme_examples_test.py
+++ b/tests/readme_examples_test.py
@@ -25,6 +25,8 @@
"./tests/index_aio.py",
"./tests/indicies.py",
"./tests/indicies_aio.py",
+ "./tests/nvd.py",
+ "./tests/nvd_aio.py",
"./tests/pagination.py",
"./tests/pagination_aio.py",
"./tests/purl.py",
From 32e38fe685754a7e72f56036d65e5113e33a4fd7 Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:05:46 +0100
Subject: [PATCH 2/7] Update tests/nvd.py
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
tests/nvd.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/nvd.py b/tests/nvd.py
index 0dfb95f3..2117a24c 100644
--- a/tests/nvd.py
+++ b/tests/nvd.py
@@ -17,6 +17,7 @@
# Walk every CVE modified in a date range. Sending an empty cursor opts in
# to cursor paging; each response then carries the cursor for the next page
cursor = ""
+ count = 0
while cursor is not None:
api_response = nvd_client.nvd_cves_get(
last_mod_start_date="2026-09-01T00:00:00.000",
@@ -24,5 +25,6 @@
results_per_page=500,
cursor=cursor,
)
- print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ count += len(api_response.vulnerabilities)
+ print(f"Total Items: {count}/{api_response.total_results}")
cursor = api_response.next_cursor
From ff4f1d1d4d0c07add14f9da6a3b43342852a530e Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:05:55 +0100
Subject: [PATCH 3/7] Update README.md
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index eff38289..8e17638d 100644
--- a/README.md
+++ b/README.md
@@ -655,6 +655,7 @@ with vulncheck_sdk.ApiClient(configuration) as api_client:
# Walk every CVE modified in a date range. Sending an empty cursor opts in
# to cursor paging; each response then carries the cursor for the next page
cursor = ""
+ count = 0
while cursor is not None:
api_response = nvd_client.nvd_cves_get(
last_mod_start_date="2026-09-01T00:00:00.000",
@@ -662,7 +663,8 @@ with vulncheck_sdk.ApiClient(configuration) as api_client:
results_per_page=500,
cursor=cursor,
)
- print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ count += len(api_response.vulnerabilities)
+ print(f"Total Items: {count}/{api_response.total_results}")
cursor = api_response.next_cursor
```
From 23efb67da98a65e632cdc4a9ddd3f58fc0bce844 Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:06:08 +0100
Subject: [PATCH 4/7] Update tests/integration_aio.py
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
tests/integration_aio.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/tests/integration_aio.py b/tests/integration_aio.py
index 696f1e48..76385770 100644
--- a/tests/integration_aio.py
+++ b/tests/integration_aio.py
@@ -109,9 +109,6 @@ async def _check_all_indices(indices: IndicesApi) -> None:
assert status == 200
-# --- Orchestrator ---
-
-
# --- NvdApi Tests ---
From 804d8848ef8d2908c70a190afba7e791f2f43ff8 Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:06:16 +0100
Subject: [PATCH 5/7] Update tests/integration_aio.py
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
tests/integration_aio.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/integration_aio.py b/tests/integration_aio.py
index 76385770..df769423 100644
--- a/tests/integration_aio.py
+++ b/tests/integration_aio.py
@@ -131,6 +131,9 @@ async def _check_nvd(api: NvdApi) -> None:
assert (await api.nvd_source_get(results_per_page=5)).sources
+# --- Orchestrator ---
+
+
async def main() -> None:
if not API_TOKEN:
print("Warning: VULNCHECK_API_TOKEN is not set.")
From 8a51336f280d557c145ebc13cf757f986362adf0 Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:06:33 +0100
Subject: [PATCH 6/7] Update tests/nvd_aio.py
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
tests/nvd_aio.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/nvd_aio.py b/tests/nvd_aio.py
index 1f87f92d..cf2fed7f 100644
--- a/tests/nvd_aio.py
+++ b/tests/nvd_aio.py
@@ -21,6 +21,7 @@ async def main():
# in to cursor paging; each response then carries the cursor for the
# next page
cursor = ""
+ count = 0
while cursor is not None:
api_response = await nvd_client.nvd_cves_get(
last_mod_start_date="2026-09-01T00:00:00.000",
@@ -28,7 +29,8 @@ async def main():
results_per_page=500,
cursor=cursor,
)
- print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ count += len(api_response.vulnerabilities)
+ print(f"Total Items: {count}/{api_response.total_results}")
cursor = api_response.next_cursor
From 6bb7bf50066b275f683253df5bca3138f76aab20 Mon Sep 17 00:00:00 2001
From: alex-vulncheck <275749855+alex-vulncheck@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:06:44 +0100
Subject: [PATCH 7/7] Update README.md
Co-authored-by: robc-vulncheck <309769615+robc-vulncheck@users.noreply.github.com>
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8e17638d..989113d5 100644
--- a/README.md
+++ b/README.md
@@ -695,6 +695,7 @@ async def main():
# in to cursor paging; each response then carries the cursor for the
# next page
cursor = ""
+ count = 0
while cursor is not None:
api_response = await nvd_client.nvd_cves_get(
last_mod_start_date="2026-09-01T00:00:00.000",
@@ -702,7 +703,8 @@ async def main():
results_per_page=500,
cursor=cursor,
)
- print(len(api_response.vulnerabilities), "of", api_response.total_results)
+ count += len(api_response.vulnerabilities)
+ print(f"Total Items: {count}/{api_response.total_results}")
cursor = api_response.next_cursor