From 5faaf7aaf5d3446f0fab82927cfc0ab0acce075d Mon Sep 17 00:00:00 2001 From: Philipp Eder Date: Tue, 22 Sep 2020 12:34:42 +0200 Subject: [PATCH 1/2] add endpoint to easyly create an markdown table about model description --- CHANGELOG.md | 4 + pheme/renderer.py | 53 ++++++++ pheme/transformation/scanreport/model.py | 162 ++++++++++++++++++++++- pheme/urls.py | 3 + pheme/views.py | 13 ++ poetry.lock | 4 +- pyproject.toml | 1 + 7 files changed, 237 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f66abe..d438084f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ curl -X POST 'http://localhost:8000/unmodified'\ -d @path_to/scanreport.xml ``` - add stack bar chart possibility [#33](https://github.com/greenbone/pheme/pull/33/) +- create a markdown table description of scanreport model +``` +curl -H 'accept: text/markdown+table' localhost:8000/scanreport/data/description +``` ### Changed ### Deprecated ### Removed diff --git a/pheme/renderer.py b/pheme/renderer.py index 940a3a45..1b8e1997 100644 --- a/pheme/renderer.py +++ b/pheme/renderer.py @@ -20,6 +20,59 @@ import xmltodict +class MarkDownTableRenderer(BaseRenderer): + media_type = 'text/markdown+table' + format = 'text' + charset = 'utf-8' + + def __as_md(self, previous_key, data): + def append(value): + return isinstance(value, str) or isinstance(value, int) + + if not (isinstance(data, dict) or isinstance(data, list)): + return [] + items = None + result = [] + + def to_new_key(key: str): + if not previous_key: + return key + if not key: + return previous_key + return "{}.{}".format(previous_key, key) + + if isinstance(data, list): + result.append( + "|{}|{}| a list of items|".format( + previous_key, + "{{% for item in {} %}} ... {{% endfor %}}".format( + previous_key + ), + ) + ) + previous_key = 'item' + items = [("", v) for v in data] + else: + items = data.items() + for key, value in items: + new_key = to_new_key(key) + if append(value): + result.append( + "|{}|{}|{}|".format(new_key, "{{ %s }}" % new_key, value) + ) + else: + result = result + self.__as_md(new_key, value) + return result + + def render(self, data, accepted_media_type=None, renderer_context=None): + if data is None: + return '' + first_line = "|key|template_example|description|\n" + table_indicator = "| :- | :--: | -: |\n" + rest = "\n".join(self.__as_md("", data)) + return first_line + table_indicator + rest + + class XMLRenderer(BaseRenderer): media_type = 'application/xml' format = 'xml' diff --git a/pheme/transformation/scanreport/model.py b/pheme/transformation/scanreport/model.py index debb1f55..1748cd01 100644 --- a/pheme/transformation/scanreport/model.py +++ b/pheme/transformation/scanreport/model.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # pylint: disable=W0614,W0511,W0401,C0103 -from typing import Dict, List, Union from dataclasses import dataclass +from typing import Dict, List, Union @dataclass @@ -135,3 +135,163 @@ class Report: vulnerability_overview: VulnerabilityOverview host_overviews: List[HostOverview] results: Results + + +def descripe(): + return Report( + id="str; identifier of a report", + summary=Summary( + scan=Scan( + name="str; name of the scan", + start="str; start date", + duration="str; end date (needs to be renamend)", + hosts_scanned="str; number of scanned hosts", + comment="str; comment of a report", + ), + report=SummaryReport( + applied_filter="str; applied filter of reports", + severities=[ + "str; High", + "str; Medium", + "str; Low", + ], + timezone="str; the timezone of the report", + ), + results=None, + ), + common_vulnerabilities=[ + CountGraph( + name="High", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + NVTCount( + oid="str; oid of nvt", + amount="int; amount of nvts with severity high", + name="str; name of nvt", + ) + ], + ), + CountGraph( + name="Medium", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + NVTCount( + oid="str; oid of nvt", + amount="int; amount of nvts within severityh", + name="str; name of nvt", + ) + ], + ), + CountGraph( + name="Low", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + NVTCount( + oid="str; oid of nvt", + amount="int; amount of nvts within severityh", + name="str; name of nvt", + ) + ], + ), + ], + vulnerability_overview=VulnerabilityOverview( + hosts=CountGraph( + name="str; hosts", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + HostCount( + ip="str; IP Address of the host", + amount="int; amount of found security breaches", + name="str; hostname", + ) + ], + ), + network_topology=None, + ports=CountGraph( + name="str; ports", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + PortCount( + port="str; port (e.g. tcp/20)", + amount="int; amount of found security breaches", + ) + ], + ), + cvss_distribution_ports=CountGraph( + name="str; cvss distribution ports", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + CVSSDistributionCount( + identifier="ports", + amount="int; amount of cvss across ports in report", + cvss="str; cvss", + ), + ], + ), + cvss_distribution_hosts=CountGraph( + name="str; cvss distribution hosts", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + CVSSDistributionCount( + identifier="hosts", + amount="int; amount of cvss across hosts in report", + cvss="str; cvss", + ), + ], + ), + cvss_distribution_vulnerabilities=CountGraph( + name="str; cvss distribution hosts", + chart="str; link to chart image (base64 encoded datalink)", + counts=[ + CVSSDistributionCount( + identifier="ports", + amount="int; amount of cvss across hosts in report", + cvss="str; cvss", + ), + ], + ), + ), + host_overviews=[ + HostOverview( + host="str; ip address of the host", + highest_severity="str; highest severity of the host", + counts=[ + SeverityCount( + severity="str; High", amount="int; amount of severity" + ), + SeverityCount( + severity="str; Medium", amount="int; amount of severity" + ), + SeverityCount( + severity="str; Low", amount="int; amount of severity" + ), + ], + ) + ], + results=Results( + max="str; max results", + start="str; start of results", + scans=[ + HostResults( + host="str; ip address of host", + results={ + 'nvt.oid': 'str; nvt.oid; optional', + 'nvt.type': 'str; nvt.type; optional', + 'nvt.name': 'str; nvt.name; optional', + 'nvt.family': 'str; nvt.family; optional', + 'nvt.cvss_base': 'str; nvt.cvss_base; optional', + 'nvt.tags': 'str; nvt.tags; optional', + 'nvt.refs.ref': 'str; nvt.refs.ref; optional', + 'nvt.solution.type': 'str; nvt.solution.type; optional', + 'nvt.solution.text': 'str; nvt.solution.text; optional', + 'port': 'str; port; optional', + 'threat': 'str; threat; optional', + 'severity': 'str; severity; optional', + 'qod.value': 'str; qod.value; optional', + 'qod.type': 'str; qod.type; optional', + 'description': 'str; description; optional', + }, + ) + ], + ), + ) diff --git a/pheme/urls.py b/pheme/urls.py index a4a692cb..f3aba7f2 100644 --- a/pheme/urls.py +++ b/pheme/urls.py @@ -42,6 +42,9 @@ path('unmodified', pheme.views.unmodified, name='unmodified'), path('transform', pheme.views.transform, name='transform'), path('transform/', pheme.views.transform), + path( + 'scanreport/data/description', pheme.views.scanreport_data_description + ), path('report/', pheme.views.report, name='report'), path('report//', pheme.views.report), re_path( diff --git a/pheme/views.py b/pheme/views.py index 7e6ea37c..046bd8db 100644 --- a/pheme/views.py +++ b/pheme/views.py @@ -23,6 +23,8 @@ from pheme.parser.xml import XMLParser from pheme.transformation import scanreport from pheme.storage import store, load +from pheme.renderer import MarkDownTableRenderer +from pheme.transformation.scanreport import model @api_view(['POST']) @@ -61,3 +63,14 @@ def unmodified(request): ) def report(request, name: str): return Response(load(name)) + + +@api_view(['GET']) +@renderer_classes( + [ + rest_framework.renderers.JSONRenderer, + MarkDownTableRenderer, + ] +) +def scanreport_data_description(request): + return Response(dataclasses.asdict(model.descripe())) diff --git a/poetry.lock b/poetry.lock index 6bbc135a..3c6d41c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -746,7 +746,7 @@ security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] -category = "dev" +category = "main" description = "a python refactoring library..." name = "rope" optional = false @@ -903,7 +903,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "a9d3ae5be2264b86cf8780e5a3918e2085f03b60222717bf7bdbe3ffdfd4821a" +content-hash = "9c77ea731b7d0a5afe8623aee75a9b1730e1b202c70fc507aba13c1492de62ea" lock-version = "1.0" python-versions = "^3.7" diff --git a/pyproject.toml b/pyproject.toml index 23916478..69aed74d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ pytest-django = "^3.9.0" weasyprint = "^51" matplotlib = "^3.3.1" pandas = "^1.1.2" +rope = "^0.17.0" [tool.poetry.dev-dependencies] pylint = "^2.6.0" From 8eff0cba8c41699fa5c0a958905dbc2a523f3323 Mon Sep 17 00:00:00 2001 From: Philipp Eder Date: Tue, 22 Sep 2020 12:46:02 +0200 Subject: [PATCH 2/2] create test for data-secription markdown --- CHANGELOG.md | 2 +- pheme/urls.py | 4 +++- tests/test_report_data_description.py | 28 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/test_report_data_description.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d438084f..2e8e2719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ curl -X POST 'http://localhost:8000/unmodified'\ -d @path_to/scanreport.xml ``` - add stack bar chart possibility [#33](https://github.com/greenbone/pheme/pull/33/) -- create a markdown table description of scanreport model +- create a markdown table description of scanreport model [#37](https://github.com/greenbone/pheme/pull/37/) ``` curl -H 'accept: text/markdown+table' localhost:8000/scanreport/data/description ``` diff --git a/pheme/urls.py b/pheme/urls.py index f3aba7f2..60862ed8 100644 --- a/pheme/urls.py +++ b/pheme/urls.py @@ -43,7 +43,9 @@ path('transform', pheme.views.transform, name='transform'), path('transform/', pheme.views.transform), path( - 'scanreport/data/description', pheme.views.scanreport_data_description + 'scanreport/data/description', + pheme.views.scanreport_data_description, + name='scanreport_data_description', ), path('report/', pheme.views.report, name='report'), path('report//', pheme.views.report), diff --git a/tests/test_report_data_description.py b/tests/test_report_data_description.py new file mode 100644 index 00000000..2d636d7e --- /dev/null +++ b/tests/test_report_data_description.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# tests/test_report_generation.py +# Copyright (C) 2020 Greenbone Networks GmbH +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from rest_framework.test import APIClient +from django.urls import reverse + + +def test_markdown_description_without_error(): + client = APIClient() + url = reverse('scanreport_data_description') + response = client.get(url, format='text/markdown+table') + assert response.status_code == 200