diff --git a/pyvulnerabilitylookup/api.py b/pyvulnerabilitylookup/api.py index 2d447f6..9ded582 100644 --- a/pyvulnerabilitylookup/api.py +++ b/pyvulnerabilitylookup/api.py @@ -3,7 +3,7 @@ from __future__ import annotations from importlib.metadata import version -from pathlib import Path +from pathlib import PurePosixPath from typing import Any from urllib.parse import urljoin, urlparse @@ -46,5 +46,76 @@ class PyVulnerabilityLookup(): return r.json() def get_vulnerability(self, vulnerability_id: str) -> dict[str, Any]: - r = self.session.get(urljoin(self.root_url, str(Path('vulnerability', vulnerability_id)))) + '''Get a vulnerability + + :param vulnerability_id: The ID of the vulnerability to get (can be from any source, as long as it is a valid ID) + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('vulnerability', vulnerability_id)))) + return r.json() + + def get_info(self) -> dict[str, Any]: + '''Get more information about the current databases in use and when it was updated''' + r = self.session.get(urljoin(self.root_url, 'info')) + return r.json() + + def get_last(self, number: int | None=None, source: str | None = None) -> dict[str, Any]: + '''Get the last vulnerabilities + + :param number: The number of vulnerabilities to get + :param source: The source of the vulnerabilities + ''' + path = PurePosixPath('last') + if source: + path /= source + if number is not None: + path /= str(number) + r = self.session.get(urljoin(self.root_url, str(path))) + return r.json() + + def get_vendors(self) -> list[str]: + '''Get the list of known vendors''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'browse')))) + return r.json() + + def get_vendor_products(self, vendor: str) -> list[str]: + '''Get the known products for a vendor + + :params vendor: A vendor owning products (must be in the known vendor list) + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'browse', vendor)))) + return r.json() + + def get_vendor_product_vulnerabilities(self, vendor: str, product: str) -> list[str]: + '''Get the the vulnerabilities per vendor and a specific product + + :param vendor: A vendor owning products (must be in the known vendor list) + :param product: A product owned by that vendor + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'search', vendor, product)))) + return r.json() + + # NOTE: endpoints /api/cve/*, /api/dbInfo, /api/last are alises for backward compat. + + def get_comments(self, uuid: str | None = None, vuln_id: str | None = None, + author: str | None = None) -> dict[str, Any]: + '''Get comment(s) + + :param uuid: The UUID a specific comment + :param vuln_id: The vulnerability ID to get comments of + :param author: The author of the comment(s) + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'comment'))), + params={'uuid': uuid, 'vuln_id': vuln_id, 'author': author}) + return r.json() + + def get_bundles(self, uuid: str | None = None, vuln_id: str | None = None, + author: str | None = None) -> dict[str, Any]: + '''Get bundle(s) + + :param uuid: The UUID a specific bundle + :param vuln_id: The vulnerability ID to get bundles of + :param author: The author of the bundle(s) + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'bundle'))), + params={'uuid': uuid, 'vuln_id': vuln_id, 'author': author}) return r.json() diff --git a/tests/test_web.py b/tests/test_web.py index 8d9dea9..73d8098 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -6,10 +6,12 @@ import time from pyvulnerabilitylookup import PyVulnerabilityLookup -class TestBasic(unittest.TestCase): +class TestPublic(unittest.TestCase): def setUp(self) -> None: - self.client = PyVulnerabilityLookup(root_url="http://127.0.0.1:10001") + self.client = PyVulnerabilityLookup(root_url="https://vulnerability.circl.lu") + + # Test default def test_up(self) -> None: self.assertTrue(self.client.is_up) @@ -22,3 +24,102 @@ class TestBasic(unittest.TestCase): break print('waiting for pysec to be imported') time.sleep(1) + + def test_get_info(self) -> None: + info = self.client.get_info() + self.assertTrue(info['last_updates']) + self.assertTrue(info['db_sizes']) + + def test_get_last(self) -> None: + last = self.client.get_last() + self.assertTrue(last) + self.assertTrue(isinstance(last, list)) + last = self.client.get_last(number=1) + self.assertTrue(isinstance(last, list)) + self.assertEqual(len(last), 1) + last = self.client.get_last(source='pysec') + for vuln in last: + self.assertTrue(vuln['id'].startswith('PYSEC')) + last = self.client.get_last(source='pysec', number=1) + self.assertEqual(len(last), 1) + self.assertTrue(last[-1]['id'].startswith('PYSEC')) + + # TODO: POST Vulnerability / Delete vulnerability + # Test API + + def test_get_vendors(self) -> None: + vendors = self.client.get_vendors() + self.assertTrue(isinstance(vendors, list)) + + def test_get_vendor_products(self) -> None: + products = self.client.get_vendor_products('misp') + self.assertTrue(isinstance(products, list)) + self.assertTrue('misp' in products) + + def test_get_vendor_product_vulnerabilities(self) -> None: + vulns = self.client.get_vendor_product_vulnerabilities('misp', 'misp') + self.assertTrue(isinstance(vulns, dict)) + self.assertTrue('cvelistv5' in vulns) + + # Test comments + + def test_get_comments(self) -> None: + comments = self.client.get_comments() + self.assertTrue('metadata' in comments) + self.assertTrue('data' in comments) + self.assertTrue(len(comments['data']) > 0) + + comments = self.client.get_comments(uuid='a309d024-2714-4a81-a425-60f83f6d5740') + self.assertTrue(len(comments['data']) == 1) + self.assertEqual(comments['data'][0]['uuid'], 'a309d024-2714-4a81-a425-60f83f6d5740') + + comments = self.client.get_comments(vuln_id='CVE-2024-20401') + self.assertTrue(len(comments['data']) >= 1) + for comment in comments['data']: + self.assertEqual(comment['vulnerability'], 'CVE-2024-20401') + + comments = self.client.get_comments(author='admin') + self.assertTrue(len(comments['data']) >= 1) + for comment in comments['data']: + self.assertEqual(comment['author']['login'], 'admin') + + comments = self.client.get_comments(uuid='a309d024-2714-4a81-a425-60f83f6d5740', + vuln_id='CVE-2024-20401', + author='admin') + self.assertTrue(len(comments['data']) == 1) + self.assertEqual(comments['data'][0]['uuid'], 'a309d024-2714-4a81-a425-60f83f6d5740') + self.assertEqual(comments['data'][0]['vulnerability'], 'CVE-2024-20401') + self.assertEqual(comments['data'][0]['author']['login'], 'admin') + + # TODO: POST / Delete Comment + # TODO: POST / Get user + + # Test bundles + + def test_get_bundles(self) -> None: + bundles = self.client.get_bundles() + self.assertTrue('metadata' in bundles) + self.assertTrue('data' in bundles) + self.assertTrue(len(bundles['data']) > 0) + + bundles = self.client.get_bundles(uuid='a23cbcad-e890-4df8-8736-9332ed4c3d47') + self.assertTrue(len(bundles['data']) == 1) + self.assertEqual(bundles['data'][0]['uuid'], 'a23cbcad-e890-4df8-8736-9332ed4c3d47') + + bundles = self.client.get_bundles(vuln_id='CVE-2024-39573') + self.assertTrue(len(bundles['data']) >= 1) + for bundle in bundles['data']: + self.assertTrue('CVE-2024-39573' in bundle['related_vulnerabilities']) + + bundles = self.client.get_bundles(author='admin') + self.assertTrue(len(bundles['data']) >= 1) + for bundle in bundles['data']: + self.assertEqual(bundle['author']['login'], 'admin') + + bundles = self.client.get_bundles(uuid='a23cbcad-e890-4df8-8736-9332ed4c3d47', + vuln_id='CVE-2024-39573', + author='admin') + self.assertTrue(len(bundles['data']) == 1) + self.assertEqual(bundles['data'][0]['uuid'], 'a23cbcad-e890-4df8-8736-9332ed4c3d47') + self.assertTrue('CVE-2024-39573' in bundles['data'][0]['related_vulnerabilities']) + self.assertEqual(bundles['data'][0]['author']['login'], 'admin')