Skip to content

Commit

Permalink
chg: [website] The API now offers the possibility to search for vulne…
Browse files Browse the repository at this point in the history
…rabilities per vendor and product, and to browse the vendors Improved API documentation. Related to #78.
  • Loading branch information
cedricbonhomme committed Nov 27, 2024
1 parent f71da45 commit 0867fac
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 98 deletions.
31 changes: 17 additions & 14 deletions website/web/api/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -29,14 +29,15 @@ def setup_api(application: Any) -> Api:
apiv1_blueprint,
title="Vulnerability-Lookup API",
version=version("vulnerabilitylookup"),
description="<a href='https://www.circl.lu' rel='noreferrer' target='_blank'>"
description="<a href='https://github.com/cve-search/vulnerability-lookup' rel='noreferrer' target='_blank'>"
"<img src='https://vulnerability.circl.lu/static/img/VL-hori-coul.png' width='500px' /></a><br />"
"API to query <a href='https://github.com/cve-search/vulnerability-lookup' rel='noreferrer' target='_blank'>Vulnerability Lookup</a>"
"API to query <a href='https://github.com/cve-search/vulnerability-lookup' rel='noreferrer' target='_blank'>Vulnerability-Lookup</a>. "
"A project from <a href='https://www.circl.lu' target='_blank'>CIRCL</a>."
"<br /><br />Back to the <a href='/'>main page</a>"
"<br /><br /><a href='https://vulnerability.circl.lu/documentation' target='_blank'>Official documentation</a>",
"<br /><br /><a href='https://vulnerability.circl.lu/documentation' target='_blank'>Official documentation</a> 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"),
Expand All @@ -49,30 +50,32 @@ 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
from website.web.api.v1 import epss
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

Expand Down
4 changes: 2 additions & 2 deletions website/web/api/v1/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
)


@bundle_ns.route("/bundle/<string:bundle_uuid>")
@bundle_ns.route("/<string:bundle_uuid>")
class BundleItem(Resource): # type: ignore[misc]
@bundle_ns.doc(description="Get a bundle with its UUID.") # type: ignore[misc]
@bundle_ns.doc(
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions website/web/api/v1/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
)


@comment_ns.route("/comment/<string:comment_uuid>")
@comment_ns.route("/<string:comment_uuid>")
class CommentItem(Resource): # type: ignore[misc]
@comment_ns.doc(description="Get a comment with its UUID.") # type: ignore[misc]
@comment_ns.doc(
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion website/web/api/v1/epss.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
epss_ns = Namespace("epss", description="EPSS related operations.")


@epss_ns.route("/epss/<string:vulnerability_id>")
@epss_ns.route("/<string:vulnerability_id>")
class EPSSItem(Resource): # type: ignore[misc]
@epss_ns.doc(description="Experimental - Get the EPSS score of a vulnerability.") # type: ignore[misc]
@epss_ns.doc(
Expand Down
6 changes: 3 additions & 3 deletions website/web/api/v1/sighting.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
)


@sighting_ns.route("/sighting/<uuid:sighting_uuid>")
@sighting_ns.route("/<uuid:sighting_uuid>")
class SightingItem(Resource): # type: ignore[misc]
@sighting_ns.doc(description="Get a sighting with its UUID.") # type: ignore[misc]
@sighting_ns.doc(
Expand All @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions website/web/api/v1/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
8 changes: 4 additions & 4 deletions website/web/api/v1/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand All @@ -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={
Expand All @@ -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."
)
Expand Down
8 changes: 4 additions & 4 deletions website/web/api/v1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -140,7 +140,7 @@ def post(self) -> Tuple[dict[Any, Any], int]:
return user_obj, 200


@user_ns.route("/user/<int:user_id>")
@user_ns.route("/<int:user_id>")
class UserItem(Resource): # type: ignore[misc]
@user_ns.doc(description="Delete a user.") # type: ignore[misc]
@user_ns.doc(
Expand Down Expand Up @@ -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(
Expand Down
97 changes: 33 additions & 64 deletions website/web/api/v1/vulnerability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -55,16 +55,21 @@
)


@vulnerability_ns.route("/vulnerability/<string:vulnerability_id>")
@vulnerability_ns.route(
"/cve/<string:vulnerability_id>",
@vulnerability_ns.route("/<string:vulnerability_id>")
@legacy_ns.route(
"cve/<string:vulnerability_id>",
doc={
"description": "Alias for /api/vulnerability/<string:vulnerability_id",
"deprecated": True,
},
)
@legacy_ns.route(
"vulnerability/<string:vulnerability_id>",
doc={
"description": "Alias for /api/vulnerability/<string:vulnerability_id",
"deprecated": True,
"doc": False,
},
)
@legacy_ns.route("vulnerability/<string:vulnerability_id>")
class Vulnerability(Resource): # type: ignore[misc]
@vulnerability_ns.doc(description="Get a vulnerability.") # type: ignore[misc]
@vulnerability_ns.expect(parser) # type: ignore[misc]
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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/<int:number>")
@vulnerability_ns.route("/vulnerability/last/<string:source>")
@vulnerability_ns.route("/vulnerability/last/<string:source>/<int:number>")
@vulnerability_ns.route("/last")
@vulnerability_ns.route("/last/<int:number>")
@vulnerability_ns.route("/last/<string:source>")
@vulnerability_ns.route("/last/<string:source>/<int:number>")
@vulnerability_ns.doc(description="Get the last vulnerabilities")
@vulnerability_ns.route(
"/last",
Expand All @@ -226,77 +231,41 @@ def post(self) -> Tuple[Dict[Any, Any], int]:
"doc": False,
},
)
@vulnerability_ns.route(
"/last/<string:source>",
doc={
"description": "Alias for /api/vulnerability/last/<string:source>",
"deprecated": True,
"doc": False,
},
)
@vulnerability_ns.route(
"/last/<string:source>/<int:number>",
doc={
"description": "Alias for /api/vulnerability/last/<string:source>/<int:number>",
"deprecated": True,
"doc": False,
},
)
class Last(Resource): # type: ignore[misc]
def get(
self, source: str | None = None, number: int | None = 30
) -> list[dict[str, Any]]:
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/<string:vendor>")
@vulnerability_ns.route(
"/browse/<string:vendor>",
doc={
"description": "Alias for /api/vulnerability/browse/<string:vendor>",
"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/<string:vendor>/<string:product>")
@vulnerability_ns.route(
"/search/<string:vendor>/<string:product>",
@vulnerability_ns.route("/search/<string:vendor>/<string:product>")
@legacy_ns.route(
"search/<string:vendor>/<string:product>",
doc={
"description": "Alias for /api/vulnerability/search/<string:vendor>/<string:product>",
"deprecated": True,
"doc": False,
},
)
@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
2 changes: 1 addition & 1 deletion website/web/templates/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ <h5>All the vulnerabilites related to {{vendor}} - {{product}}</h5>
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 = '';
Expand Down

0 comments on commit 0867fac

Please sign in to comment.