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