From e5c42cdd30a948440d8d6b3dd777e006644a4843 Mon Sep 17 00:00:00 2001 From: Matthias Veit Date: Wed, 27 Sep 2023 08:50:53 +0200 Subject: [PATCH] [resotocore][feat] List available benchmarks (#1783) --- resotocore/resotocore/static/api-doc.yaml | 141 ++++++++++++++++---- resotocore/resotocore/web/api.py | 27 ++++ resotocore/tests/resotocore/web/api_test.py | 15 +++ 3 files changed, 156 insertions(+), 27 deletions(-) diff --git a/resotocore/resotocore/static/api-doc.yaml b/resotocore/resotocore/static/api-doc.yaml index d4fdb06006..23e065e00c 100644 --- a/resotocore/resotocore/static/api-doc.yaml +++ b/resotocore/resotocore/static/api-doc.yaml @@ -205,8 +205,8 @@ paths: description: "If true, the hierarchy of complex kinds is flattened, holding all properties and all merged metadata." in: query schema: - type: boolean - default: false + type: boolean + default: false responses: "200": description: "The list of all kinds." @@ -2721,6 +2721,41 @@ paths: items: $ref: "#/components/schemas/InspectCheck" + /report/benchmarks: + get: + summary: "List available benchmarks." + description: "List available benchmarks." + tags: + - report + parameters: + - name: benchmark + in: query + description: "Comma separated list of benchmarks. All if not defined." + schema: + type: string + example: "aws_cis_1_5,aws_cis_1_6" + explode: false + - name: with_checks + in: query + description: "Include check definition into the benchmark" + schema: + type: boolean + default: false + - name: short + in: query + description: "Reduce the information to top level properties." + schema: + type: boolean + default: false + responses: + "200": + description: "List of benchmarks." + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Benchmark" /report/benchmark/{benchmark}/graph/{graph_id}: get: summary: "Perform a benchmark on a graph." @@ -2761,7 +2796,7 @@ paths: but the load action is much faster. schema: type: string - enum: [run, load] + enum: [ run, load ] default: run responses: "200": @@ -3146,32 +3181,32 @@ paths: post: summary: "This endpoint is used to create the first user." tags: - - authorization + - authorization description: "Create the first user from the create first user login form." requestBody: content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - company: - type: string - description: The company name. - fullname: - type: string - description: The full name of the user. - email: - type: string - description: The email of the user. - password: - type: string - description: The password of the user. - password_repeat: - type: string - description: The repeated password of the user. - redirect: - type: string - description: The redirect url after the user is created. + application/x-www-form-urlencoded: + schema: + type: object + properties: + company: + type: string + description: The company name. + fullname: + type: string + description: The full name of the user. + email: + type: string + description: The email of the user. + password: + type: string + description: The password of the user. + password_repeat: + type: string + description: The repeated password of the user. + redirect: + type: string + description: The redirect url after the user is created. responses: "303": description: "The redirect to the specified url in the form." @@ -3224,7 +3259,7 @@ paths: get: summary: "This endpoint is used to renew a JWT that is about to expire." tags: - - authorization + - authorization description: "Get the user information." responses: "200": @@ -3870,3 +3905,55 @@ components: items: type: string description: "The related checks of this check" + + CheckCollection: + type: object + properties: + title: + type: string + description: "The title of this check collection" + description: + type: string + description: "The description of this check collection" + documentation: + type: string + description: "The documentation of this check collection" + checks: + type: array + items: + type: string + description: "Ids of checks in this collection" + children: + type: array + items: + $ref: '#/components/schemas/CheckCollection' + description: "Ids of child collections" + + Benchmark: + type: object + allOf: + - $ref: '#/components/schemas/CheckCollection' + properties: + id: + type: string + description: "The id of this benchmark" + framework: + type: string + description: "The framework of this benchmark" + version: + type: string + description: "The version of this benchmark" + clouds: + type: array + items: + type: string + description: "The clouds relevant for this benchmark" + report_checks: + type: array + items: + oneOf: + - type: string + - $ref: '#/components/schemas/InspectCheck' + description: "Related checks of this benchmarks" + + diff --git a/resotocore/resotocore/web/api.py b/resotocore/resotocore/web/api.py index 66ff18e21c..93abfebb2b 100644 --- a/resotocore/resotocore/web/api.py +++ b/resotocore/resotocore/web/api.py @@ -91,6 +91,7 @@ from resotocore.model.json_schema import json_schema from resotocore.model.model import Kind, Model from resotocore.model.typed_model import to_json, from_js, to_js_str, to_js +from resotocore.report import Benchmark, ReportCheck from resotocore.service import Service from resotocore.task.model import Subscription from resotocore.types import Json, JsonElement @@ -252,6 +253,7 @@ def __add_routes(self, prefix: str) -> None: web.get(prefix + "/subscriber/{subscriber_id}/handle", require(self.handle_subscribed, a)), # report checks web.get(prefix + "/report/checks", require(self.inspection_checks, r)), + web.get(prefix + "/report/benchmarks", require(self.benchmarks, r)), web.get(prefix + "/report/checks/graph/{graph_id}", require(self.perform_benchmark_on_checks, r)), web.get(prefix + "/report/check/{check_id}/graph/{graph_id}", require(self.inspection_results, r)), web.get(prefix + "/report/benchmark/{benchmark}/graph/{graph_id}", require(self.perform_benchmark, r)), @@ -648,6 +650,31 @@ async def inspection_checks(self, request: Request, deps: TenantDependencies) -> inspections = await deps.inspector.list_checks(provider=provider, service=service, category=category, kind=kind) return await single_result(request, to_js(inspections)) + async def benchmarks(self, request: Request, deps: TenantDependencies) -> StreamResponse: + benchmark_filter = [b.strip() for b in request.query.get("benchmarks", "").split(",") if b.strip()] + short = request.query.get("short", "false").lower() == "true" + with_checks = request.query.get("with_checks", "false").lower() == "true" + lookup = {c.id: c for c in await deps.inspector.list_checks()} if with_checks else {} + + def to_js_check(c: ReportCheck) -> JsonElement: + return c.id if short else to_js(c, strip_nulls=True) + + def to_js_benchmark(b: Benchmark) -> Json: + bj: Json = to_js(b, strip_nulls=True) + if short: + bj.pop("checks", None) + bj.pop("children", None) + if with_checks: + bj["report_checks"] = [to_js_check(lookup[c]) for c in b.nested_checks()] + return bj + + benchmarks = [ + to_js_benchmark(b) + for b in await deps.inspector.list_benchmarks() + if (b.id in benchmark_filter or not benchmark_filter) + ] + return await single_result(request, benchmarks) + async def inspection_results(self, request: Request, deps: TenantDependencies) -> StreamResponse: graph = GraphName(request.match_info["graph_id"]) check_id = request.match_info["check_id"] diff --git a/resotocore/tests/resotocore/web/api_test.py b/resotocore/tests/resotocore/web/api_test.py index 11059843c4..b2bf7d5d96 100644 --- a/resotocore/tests/resotocore/web/api_test.py +++ b/resotocore/tests/resotocore/web/api_test.py @@ -472,6 +472,21 @@ async def test_config(core_client: ResotoClient, foo_kinds: List[rc.Kind]) -> No } +@pytest.mark.asyncio +async def test_report(core_client: ResotoClient, client_session: ClientSession) -> None: + url = core_client.resotocore_url + report = await client_session.get( + f"{url}/report/benchmarks", params={"with_checks": "true", "short": "true", "benchmarks": "aws_cis_1_5"} + ) + benchmarks = await report.json() + assert len(benchmarks) == 1 + benchmark = benchmarks[0] + assert benchmark["id"] == "aws_cis_1_5" + assert len(benchmark["report_checks"]) > 50 + assert benchmark.get("checks") is None + assert benchmark.get("children") is None + + @pytest.mark.asyncio async def test_authorization(core_client_with_psk: ResotoClient, client_session: ClientSession) -> None: url = core_client_with_psk.resotocore_url