diff --git a/README.md b/README.md index 74aaa20f..989113d5 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,92 @@ 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 = "" + count = 0 + 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, + ) + count += len(api_response.vulnerabilities) + print(f"Total Items: {count}/{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 = "" + 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", + last_mod_end_date="2026-09-02T00:00:00.000", + results_per_page=500, + cursor=cursor, + ) + count += len(api_response.vulnerabilities) + print(f"Total Items: {count}/{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..df769423 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", "") @@ -108,6 +109,28 @@ async def _check_all_indices(indices: IndicesApi) -> None: assert status == 200 +# --- 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 + + # --- Orchestrator --- @@ -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..2117a24c --- /dev/null +++ b/tests/nvd.py @@ -0,0 +1,30 @@ +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 = "" + count = 0 + 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, + ) + count += len(api_response.vulnerabilities) + print(f"Total Items: {count}/{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..cf2fed7f --- /dev/null +++ b/tests/nvd_aio.py @@ -0,0 +1,38 @@ +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 = "" + 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", + last_mod_end_date="2026-09-02T00:00:00.000", + results_per_page=500, + cursor=cursor, + ) + count += len(api_response.vulnerabilities) + print(f"Total Items: {count}/{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",