From 0867fac80f772878278c90790db9b9644beba88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bonhomme?= Date: Wed, 27 Nov 2024 11:31:49 +0100 Subject: [PATCH] chg: [website] The API now offers the possibility to search for vulnerabilities per vendor and product, and to browse the vendors Improved API documentation. Related to #78. --- website/web/api/v1/__init__.py | 31 ++++----- website/web/api/v1/bundle.py | 4 +- website/web/api/v1/comment.py | 4 +- website/web/api/v1/epss.py | 2 +- website/web/api/v1/sighting.py | 6 +- website/web/api/v1/stats.py | 6 +- website/web/api/v1/system.py | 8 +-- website/web/api/v1/user.py | 8 +-- website/web/api/v1/vulnerability.py | 97 ++++++++++------------------- website/web/templates/search.html | 2 +- 10 files changed, 70 insertions(+), 98 deletions(-) diff --git a/website/web/api/v1/__init__.py b/website/web/api/v1/__init__.py index 893bc208..771208c5 100644 --- a/website/web/api/v1/__init__.py +++ b/website/web/api/v1/__init__.py @@ -11,7 +11,7 @@ apiv1_blueprint = Blueprint( - "apiv1", __name__, url_prefix="" + "apiv1", __name__, url_prefix="/api" ) # should we put the version in the URL ? csrf.exempt(apiv1_blueprint) @@ -29,14 +29,15 @@ def setup_api(application: Any) -> Api: apiv1_blueprint, title="Vulnerability-Lookup API", version=version("vulnerabilitylookup"), - description="" + description="" "
" - "API to query Vulnerability Lookup" + "API to query Vulnerability-Lookup. " + "A project from CIRCL." "

Back to the main page" - "

Official documentation", + "

Official documentation of the project.", license="GNU Affero General Public License version 3", license_url="https://www.gnu.org/licenses/agpl-3.0.html", - doc="/doc", + doc="/", security="apikey", authorizations=authorizations, contact_email=application.config.get("ADMIN_EMAIL", "info@circl.lu"), @@ -49,13 +50,14 @@ def custom_ui() -> str: return render_template( "swagger-ui.html", title=api.title, - specs_url="{}://{}/swagger.json".format( + specs_url="{}://{}/api/swagger.json".format( http_schema, get_config("generic", "public_domain") ), ) from website.web.api.v1 import system from website.web.api.v1 import vulnerability + from website.web.api.v1 import browse from website.web.api.v1 import comment from website.web.api.v1 import user from website.web.api.v1 import bundle @@ -63,16 +65,17 @@ def custom_ui() -> str: from website.web.api.v1 import sighting from website.web.api.v1 import stats - api.add_namespace(system.system_ns, path="/api") - api.add_namespace(vulnerability.vulnerability_ns, path="/api") + api.add_namespace(system.system_ns, path="/system") + api.add_namespace(vulnerability.vulnerability_ns, path="/vulnerability") api.add_namespace(vulnerability.legacy_ns, path="/") - api.add_namespace(stats.stats_ns, path="/api") + api.add_namespace(browse.browse_ns, path="/browse") + api.add_namespace(stats.stats_ns, path="/stats") if get_config("generic", "user_accounts"): - api.add_namespace(comment.comment_ns, path="/api") - api.add_namespace(user.user_ns, path="/api") - api.add_namespace(bundle.bundle_ns, path="/api") - api.add_namespace(sighting.sighting_ns, path="/api") - api.add_namespace(epss.epss_ns, path="/api") + api.add_namespace(comment.comment_ns, path="/comment") + api.add_namespace(user.user_ns, path="/user") + api.add_namespace(bundle.bundle_ns, path="/bundle") + api.add_namespace(sighting.sighting_ns, path="/sighting") + api.add_namespace(epss.epss_ns, path="/epss") return api diff --git a/website/web/api/v1/bundle.py b/website/web/api/v1/bundle.py index ceeac285..4ff97cdc 100644 --- a/website/web/api/v1/bundle.py +++ b/website/web/api/v1/bundle.py @@ -100,7 +100,7 @@ ) -@bundle_ns.route("/bundle/") +@bundle_ns.route("/") class BundleItem(Resource): # type: ignore[misc] @bundle_ns.doc(description="Get a bundle with its UUID.") # type: ignore[misc] @bundle_ns.doc( @@ -138,7 +138,7 @@ def delete(self, bundle_uuid: str) -> Tuple[dict[Any, Any], int]: return {"message": "Bundle not found."}, 404 -@bundle_ns.route("/bundle/") +@bundle_ns.route("/") class BundlesList(Resource): # type: ignore[misc] @bundle_ns.doc("list_bundles") # type: ignore[misc] @bundle_ns.expect(parser) # type: ignore[misc] diff --git a/website/web/api/v1/comment.py b/website/web/api/v1/comment.py index b6a2cc82..b59cbb7c 100644 --- a/website/web/api/v1/comment.py +++ b/website/web/api/v1/comment.py @@ -105,7 +105,7 @@ ) -@comment_ns.route("/comment/") +@comment_ns.route("/") class CommentItem(Resource): # type: ignore[misc] @comment_ns.doc(description="Get a comment with its UUID.") # type: ignore[misc] @comment_ns.doc( @@ -145,7 +145,7 @@ def delete(self, comment_uuid: str) -> Tuple[dict[Any, Any], int]: return {"message": "Comment not found."}, 404 -@comment_ns.route("/comment/") +@comment_ns.route("/") class CommentsList(Resource): # type: ignore[misc] @comment_ns.doc("list_comments") # type: ignore[misc] @comment_ns.expect(parser) # type: ignore[misc] diff --git a/website/web/api/v1/epss.py b/website/web/api/v1/epss.py index ed04e5a3..db506ca6 100644 --- a/website/web/api/v1/epss.py +++ b/website/web/api/v1/epss.py @@ -11,7 +11,7 @@ epss_ns = Namespace("epss", description="EPSS related operations.") -@epss_ns.route("/epss/") +@epss_ns.route("/") class EPSSItem(Resource): # type: ignore[misc] @epss_ns.doc(description="Experimental - Get the EPSS score of a vulnerability.") # type: ignore[misc] @epss_ns.doc( diff --git a/website/web/api/v1/sighting.py b/website/web/api/v1/sighting.py index f25ac21e..3faca0dd 100644 --- a/website/web/api/v1/sighting.py +++ b/website/web/api/v1/sighting.py @@ -119,7 +119,7 @@ ) -@sighting_ns.route("/sighting/") +@sighting_ns.route("/") class SightingItem(Resource): # type: ignore[misc] @sighting_ns.doc(description="Get a sighting with its UUID.") # type: ignore[misc] @sighting_ns.doc( @@ -136,8 +136,8 @@ def get(self, sighting_uuid: str) -> Tuple[dict[Any, Any], int]: return result, 200 -@sighting_ns.route("/sighting") -@sighting_ns.route("/sighting/") +@sighting_ns.route("") +@sighting_ns.route("/") class SightingsList(Resource): # type: ignore[misc] @sighting_ns.doc("list_sightings") # type: ignore[misc] @sighting_ns.expect(parser) # type: ignore[misc] diff --git a/website/web/api/v1/stats.py b/website/web/api/v1/stats.py index caa93bf1..3b37d586 100644 --- a/website/web/api/v1/stats.py +++ b/website/web/api/v1/stats.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) -stats_ns = Namespace("stats", description="Endpoint to get various stats.") +stats_ns = Namespace("stats", description="Endpoint for retrieving various statistics.") # Argument Parsing @@ -112,7 +112,7 @@ def generate_markdown_table(data: list[dict[str, Any]]) -> str: return markdown_table -@stats_ns.route("/stats/vulnerability/most_sighted") +@stats_ns.route("/vulnerability/most_sighted") class VulnerabilityMostSighted(Resource): # type: ignore[misc] @stats_ns.doc(description="Returns the most sighted vulnerabilities.") # type: ignore[misc] @stats_ns.expect(parser_sighting) # type: ignore[misc] @@ -163,7 +163,7 @@ def get(self) -> Union[list[dict[str, Any]], str]: return result -@stats_ns.route("/stats/vulnerability/most_commented") +@stats_ns.route("/vulnerability/most_commented") class VulnerabilityMostCommented(Resource): # type: ignore[misc] @stats_ns.doc(description="Returns the most commented vulnerabilities.") # type: ignore[misc] @stats_ns.expect(parser) # type: ignore[misc] diff --git a/website/web/api/v1/system.py b/website/web/api/v1/system.py index d2592198..c6491ebd 100644 --- a/website/web/api/v1/system.py +++ b/website/web/api/v1/system.py @@ -15,10 +15,10 @@ local_instance_name = get_config("generic", "local_instance_name").lower() local_instance_vulnid_pattern = get_config("generic", "local_instance_vulnid_pattern") -system_ns = Namespace("system", description="Get the status of the system.") +system_ns = Namespace("system", description="Endpoints for retrieving various information about the system's status.") -@system_ns.route("/system/redis_up") +@system_ns.route("/redis_up") @system_ns.route( "/redis_up", doc={ @@ -32,7 +32,7 @@ def get(self) -> bool: return vulnerabilitylookup.check_redis_up() -@system_ns.route("/system/dbInfo") +@system_ns.route("/dbInfo") @system_ns.route( "/dbInfo", doc={ @@ -55,7 +55,7 @@ def get(self) -> dict[str, Any]: return vulnerabilitylookup.get_info() -@system_ns.route("/system/configInfo") +@system_ns.route("/configInfo") @system_ns.doc( description="Get non-sensitive information about the configuration of the system." ) diff --git a/website/web/api/v1/user.py b/website/web/api/v1/user.py index 37ef2e5e..fdfa6f1d 100644 --- a/website/web/api/v1/user.py +++ b/website/web/api/v1/user.py @@ -76,7 +76,7 @@ ) -@user_ns.route("/user/me") +@user_ns.route("/me") class UserSelf(Resource): # type: ignore[misc] @user_ns.doc(description="Get information about the currently authenticated user.") # type: ignore[misc] @user_ns.doc( @@ -99,7 +99,7 @@ def get(self) -> Tuple[dict[Any, Any], int]: return me, 200 -@user_ns.route("/user/api_key") +@user_ns.route("/api_key") class UserNewAPIKey(Resource): # type: ignore[misc] @user_ns.doc(description="Regenerating the API key of the authenticated user with the current API key.") # type: ignore[misc] @user_ns.doc( @@ -140,7 +140,7 @@ def post(self) -> Tuple[dict[Any, Any], int]: return user_obj, 200 -@user_ns.route("/user/") +@user_ns.route("/") class UserItem(Resource): # type: ignore[misc] @user_ns.doc(description="Delete a user.") # type: ignore[misc] @user_ns.doc( @@ -177,7 +177,7 @@ def delete(self, user_id: int) -> Tuple[dict[Any, Any], int]: return {}, 204 -@user_ns.route("/user/") +@user_ns.route("/") class UsersList(Resource): # type: ignore[misc] @user_ns.doc("list_users") # type: ignore[misc] @user_ns.doc( diff --git a/website/web/api/v1/vulnerability.py b/website/web/api/v1/vulnerability.py index 8195f01f..a7edcfbf 100644 --- a/website/web/api/v1/vulnerability.py +++ b/website/web/api/v1/vulnerability.py @@ -29,7 +29,7 @@ vulnerability_ns = Namespace( "vulnerability", description="Vulnerability related operations." ) -legacy_ns = Namespace("legacy", description="legacy endpoints") +legacy_ns = Namespace("legacy", description="Legacy endpoints for vulnerabilities.") storage = Redis( host=get_config("generic", "storage_db_hostname"), @@ -55,16 +55,21 @@ ) -@vulnerability_ns.route("/vulnerability/") -@vulnerability_ns.route( - "/cve/", +@vulnerability_ns.route("/") +@legacy_ns.route( + "cve/", + doc={ + "description": "Alias for /api/vulnerability/", doc={ "description": "Alias for /api/vulnerability/") class Vulnerability(Resource): # type: ignore[misc] @vulnerability_ns.doc(description="Get a vulnerability.") # type: ignore[misc] @vulnerability_ns.expect(parser) # type: ignore[misc] @@ -102,7 +107,7 @@ def delete(self, vulnerability_id: str) -> Tuple[dict[Any, Any], int]: return {}, 204 -@vulnerability_ns.route("/vulnerability/") +@vulnerability_ns.route("/") class VulnerabilitiesList(Resource): # type: ignore[misc] @vulnerability_ns.doc(description="Create a vulnerability with the CVE version 5 format.") # type: ignore[misc] @vulnerability_ns.doc( @@ -205,10 +210,10 @@ def post(self) -> Tuple[Dict[Any, Any], int]: return vuln, 200 -@vulnerability_ns.route("/vulnerability/last") -@vulnerability_ns.route("/vulnerability/last/") -@vulnerability_ns.route("/vulnerability/last/") -@vulnerability_ns.route("/vulnerability/last//") +@vulnerability_ns.route("/last") +@vulnerability_ns.route("/last/") +@vulnerability_ns.route("/last/") +@vulnerability_ns.route("/last//") @vulnerability_ns.doc(description="Get the last vulnerabilities") @vulnerability_ns.route( "/last", @@ -226,22 +231,6 @@ def post(self) -> Tuple[Dict[Any, Any], int]: "doc": False, }, ) -@vulnerability_ns.route( - "/last/", - doc={ - "description": "Alias for /api/vulnerability/last/", - "deprecated": True, - "doc": False, - }, -) -@vulnerability_ns.route( - "/last//", - doc={ - "description": "Alias for /api/vulnerability/last//", - "deprecated": True, - "doc": False, - }, -) class Last(Resource): # type: ignore[misc] def get( self, source: str | None = None, number: int | None = 30 @@ -249,43 +238,10 @@ def get( return [entry for v_id, entry in vulnerabilitylookup.get_last(source, number)] -@vulnerability_ns.route("/vulnerability/browse") -@vulnerability_ns.route( - "/browse", - doc={ - "description": "Alias for /api/vulnerability/browse", - "deprecated": True, - "doc": False, - }, -) -@vulnerability_ns.doc(description="Get the known vendors") -class Vendors(Resource): # type: ignore[misc] - def get(self) -> list[str]: - vendor = request.args.get("vendor", "").lower() - vendors = list(vulnerabilitylookup.get_vendors()) - if vendor and len(vendor) >= 3: - vendors = [elem for elem in vendors if vendor in elem] - return vendors - -@vulnerability_ns.route("/vulnerability/browse/") -@vulnerability_ns.route( - "/browse/", - doc={ - "description": "Alias for /api/vulnerability/browse/", - "deprecated": True, - "doc": False, - }, -) -@vulnerability_ns.doc(description="Get the known products for a vendor") -class VendorProducts(Resource): # type: ignore[misc] - def get(self, vendor: str) -> list[str]: - return list(vulnerabilitylookup.get_vendor_products(vendor)) - - -@vulnerability_ns.route("/vulnerability/search//") -@vulnerability_ns.route( - "/search//", +@vulnerability_ns.route("/search//") +@legacy_ns.route( + "search//", doc={ "description": "Alias for /api/vulnerability/search//", "deprecated": True, @@ -293,10 +249,23 @@ def get(self, vendor: str) -> list[str]: }, ) @vulnerability_ns.doc( - description="Get the the vulnerabilities per vendor and a specific product" + description="Returns a list of vulnerabilities related to the product." ) class VendorProductVulnerabilities(Resource): # type: ignore[misc] def get( self, vendor: str, product: str ) -> dict[str, list[tuple[str, dict[str, Any]]]]: + """Returns a list of vulnerabilities related to the product.""" return vulnerabilitylookup.get_vendor_product_vulnerabilities(vendor, product) + + +@vulnerability_ns.route("/browse/") +@vulnerability_ns.doc(description="Get the known vendors.") +class Vendors(Resource): # type: ignore[misc] + def get(self) -> list[str]: + """Get the known vendors.""" + vendor = request.args.get("vendor", "").lower() + vendors = list(vulnerabilitylookup.get_vendors()) + if vendor and len(vendor) >= 3: + vendors = [elem for elem in vendors if vendor in elem] + return vendors diff --git a/website/web/templates/search.html b/website/web/templates/search.html index 798c3329..59d2ff1e 100644 --- a/website/web/templates/search.html +++ b/website/web/templates/search.html @@ -263,7 +263,7 @@
All the vulnerabilites related to {{vendor}} - {{product}}
document.getElementById("freetext_search").oninput = function(event) { var text = document.getElementById("freetext_search").value; if (text.length >= 3) { - fetch("{{ url_for('apiv1.vulnerability_vendors') }}?vendor="+text) + fetch("{{ url_for('apiv1.browse_vendors') }}?vendor="+text) .then(response => response.json()) .then(vendors => { var options = '';