From 51bcb96d612973b48cafc40439ce2210a2e6839d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 19 Nov 2024 13:15:36 +0100 Subject: [PATCH] new: Add all missing endpoints, update existing ones. --- pyvulnerabilitylookup/api.py | 167 ++++++++++++++++++++++++++++++----- tests/test_web.py | 9 +- 2 files changed, 150 insertions(+), 26 deletions(-) diff --git a/pyvulnerabilitylookup/api.py b/pyvulnerabilitylookup/api.py index 4d0b20c..756cc39 100644 --- a/pyvulnerabilitylookup/api.py +++ b/pyvulnerabilitylookup/api.py @@ -65,6 +65,20 @@ class PyVulnerabilityLookup(): r = self.session.get(urljoin(self.root_url, 'redis_up')) return r.json() + # #### DB status #### + + 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_config_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, 'configInfo')) + return r.json() + + # #### Vulnerabilities #### + def get_vulnerability(self, vulnerability_id: str) -> dict[str, Any]: '''Get a vulnerability @@ -90,16 +104,6 @@ class PyVulnerabilityLookup(): r = self.session.delete(urljoin(self.root_url, str(PurePosixPath('vulnerability', vulnerability_id)))) return r.status_code - 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_config_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, 'configInfo')) - return r.json() - def get_last(self, number: int | None=None, source: str | None = None) -> list[dict[str, Any]]: '''Get the last vulnerabilities @@ -115,7 +119,7 @@ class PyVulnerabilityLookup(): return r.json() def get_vendors(self) -> list[str]: - '''Get the list of known vendors''' + '''Get the known vendors''' r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'browse')))) return r.json() @@ -136,15 +140,42 @@ class PyVulnerabilityLookup(): 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. + # #### Comments #### - def create_comment(self, comment: dict[str, Any]) -> dict[str, Any]: + def create_comment(self, /, *, comment: dict[str, Any] | None=None, description: str | None=None, + description_format: str | None = None, meta: dict[str, str] | None = None, + related_vulnerabilities: list[str] | None=None, title: str | None=None, + uuid: str | None=None, vulnerability: str | None = None) -> dict[str, Any]: '''Create a comment. :param comment: The comment + :param description: The description of the comment + :param description_format: Description format (markdown or text). + :param meta: Zero or more meta-fields. + :param related_vulnerabilities: Zero or more related vulnerabilities. + :param title: The title of the comment + :param uuid: The UUID of the comment + :param vulnerability: The vulnerability ID of the comment ''' - r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'comment'))), - json=comment) + + if not comment: + comment = {} + if description: + comment['description'] = description + if description_format: + comment['description_format'] = description_format + if meta: + comment['meta'] = meta + if related_vulnerabilities: + comment['related_vulnerabilities'] = related_vulnerabilities + if title: + comment['title'] = title + if uuid: + comment['uuid'] = uuid + if vulnerability: + comment['vulnerability'] = vulnerability + + r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'comment'))), json=comment) return r.json() def get_comments(self, uuid: str | None = None, vuln_id: str | None = None, @@ -159,6 +190,15 @@ class PyVulnerabilityLookup(): params={'uuid': uuid, 'vuln_id': vuln_id, 'author': author}) return r.json() + def get_comment(self, comment_uuid: str) -> dict[str, Any]: + '''Get a comment + + :param comment_uuid: The UUID of the comment + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'comment', comment_uuid))) + ) + return r.json() + def delete_comment(self, comment_uuid: str) -> int: '''Delete a comment. @@ -167,25 +207,65 @@ class PyVulnerabilityLookup(): r = self.session.delete(urljoin(self.root_url, str(PurePosixPath('api', 'comment', comment_uuid)))) return r.status_code - def create_bundle(self, bundle: dict[str, Any]) -> dict[str, Any]: + # #### Bundles #### + + def create_bundle(self, /, *, bundle: dict[str, Any] | None=None, description: str | None=None, + meta: dict[str, str] | None=None, name: str | None=None, related_vulnerabilities: list[str] | None=None, + uuid: str | None=None) -> dict[str, Any]: '''Create a bundle. :param bundle: The bundle ''' + + if not bundle: + bundle = {} + if description: + bundle['description'] = description + if meta: + bundle['meta'] = meta + if name: + bundle['name'] = name + if related_vulnerabilities: + bundle['related_vulnerabilities'] = related_vulnerabilities + if uuid: + bundle['uuid'] = uuid + r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'bundle'))), json=bundle) return r.json() def get_bundles(self, uuid: str | None = None, vuln_id: str | None = None, - author: str | None = None) -> dict[str, Any]: + author: str | None = None, per_page: int | None=None, + meta: list[dict[str, 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) + :param per_page: The number of bundles to get per page + :param meta: Query for the meta JSON field. Example: meta=[{‘tags’: [‘tcp’]}] ''' - r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'bundle'))), - params={'uuid': uuid, 'vuln_id': vuln_id, 'author': author}) + params: dict[str, Any] = {} + if uuid: + params['uuid'] = uuid + if vuln_id: + params['vuln_id'] = vuln_id + if author: + params['author'] = author + if per_page is not None: + params['per_page'] = per_page + if meta: + params['meta'] = meta + + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'bundle'))), params=params) + return r.json() + + def get_bundle(self, bundle_uuid: str) -> dict[str, Any]: + '''Get a bundle + + :param bundle_uuid: The UUID of the bundle + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'bundle', bundle_uuid)))) return r.json() def delete_bundle(self, bundle_uuid: str) -> int: @@ -196,7 +276,11 @@ class PyVulnerabilityLookup(): r = self.session.delete(urljoin(self.root_url, str(PurePosixPath('api', 'bundle', bundle_uuid)))) return r.status_code - def create_user(self, login: str, name: str, organisation: str, email: str) -> dict[str, Any]: + # #### Users #### + + def create_user(self, /, *, user: dict[str, Any] | None=None, + login: str | None=None, name: str | None=None, + organisation: str | None=None, email: str | None=None) -> dict[str, Any]: '''Create a user. :param login: The login of the user @@ -204,11 +288,26 @@ class PyVulnerabilityLookup(): :param organisation: The organisation of the user :param email: The email of the user ''' - r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'user'))), - json={'login': login, 'name': name, 'organisation': organisation, 'email': email}) + + if not user: + user = {} + if login: + user['login'] = login + if name: + user['name'] = name + if organisation: + user['organisation'] = organisation + if email: + user['email'] = email + + r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'user'))), json=user) return r.json() def list_users(self) -> dict[str, Any]: + # Alias this one to get_users for consistency + return self.get_users() + + def get_users(self) -> dict[str, Any]: '''List users''' r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'user')))) return r.json() @@ -218,6 +317,20 @@ class PyVulnerabilityLookup(): r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'user', 'me')))) return r.json() + def reset_api_key(self) -> dict[str, Any]: + '''Reset the API key''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'user', 'api_key')))) + return r.json() + + def delete_user(self, user_id: str) -> int: + '''Delete a user. + + :param user_id: The user ID + ''' + r = self.session.delete(urljoin(self.root_url, str(PurePosixPath('api', 'user', user_id))) + ) + return r.status_code + # #### Sightings #### def get_sighting(self, sighting_uuid: str) -> dict[str, Any]: @@ -298,3 +411,13 @@ class PyVulnerabilityLookup(): r = self.session.post(urljoin(self.root_url, str(PurePosixPath('api', 'sighting'))), json=sighting) return r.json() + + # #### EPSS #### + + def get_epss(self, vulnerability: str) -> dict[str, Any]: + '''Get the EPSS for a vulnerability + + :param vulnerability: The vulnerability ID + ''' + r = self.session.get(urljoin(self.root_url, str(PurePosixPath('api', 'epss', vulnerability)))) + return r.json() diff --git a/tests/test_web.py b/tests/test_web.py index 0716335..7d82294 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -127,7 +127,7 @@ class TestPublic(unittest.TestCase): "vulnerability": "CVE-2024-20401", "related_vulnerabilities": ["ghsa-4rcj-fmjg-q9fv"], } - comments = self.client.create_comment(comment) + comments = self.client.create_comment(comment=comment) self.assertTrue(len(comments["data"]) == 1) self.assertEqual( comments["data"][0]["uuid"], "a309d024-2714-4a81-a425-60f83f6d5740" @@ -215,7 +215,7 @@ class TestPublic(unittest.TestCase): "description_format": "markdown", "related_vulnerabilities": ["ghsa-4rcj-fmjg-q9fv", "CVE-2024-39573"], } - bundles = self.client.create_bundle(bundle) + bundles = self.client.create_bundle(bundle=bundle) self.assertTrue(len(bundles["data"]) == 1) self.assertEqual( bundles["data"][0]["uuid"], "a23cbcad-e890-4df8-8736-9332ed4c3d47" @@ -293,7 +293,8 @@ class TestPublic(unittest.TestCase): if not instance_config.get('registration'): return None - user = self.client.create_user('test Name', 'test Login', 'test Organization', 'test@testorg.lu') + user = self.client.create_user(name='test Name', login='test Login', + organisation='test Organization', email='test@testorg.lu') self.assertTrue(user) self.assertTrue('login' in user, user) self.assertTrue('apikey' in user, user) @@ -303,7 +304,7 @@ class TestPublic(unittest.TestCase): comment = {'title': 'test', 'description': 'test', 'vulnerability': 'CVE-2024-20401', 'related_vulnerabilities': ['CVE-2024-20402']} - created_comment = self.client.create_comment(comment) + created_comment = self.client.create_comment(comment=comment) new_comment_uuid = created_comment['data'][0]['uuid'] comments = self.client.get_comments(uuid=new_comment_uuid) self.assertTrue(len(comments['data']) == 0, comments)