diff --git a/docs/content/en/connecting_your_tools/parsers/file/openvas.md b/docs/content/en/connecting_your_tools/parsers/file/openvas.md index b0153900161..78596cd1188 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/openvas.md +++ b/docs/content/en/connecting_your_tools/parsers/file/openvas.md @@ -2,16 +2,39 @@ title: "OpenVAS Parser" toc_hide: true --- -You can either upload the exported results of an OpenVAS Scan in a .csv or .xml format. +You can upload the results of an OpenVAS/Greenbone report in either .csv or .xml format. ### Sample Scan Data Sample OpenVAS scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/openvas). -### Default Deduplication Hashcode Fields -By default, DefectDojo identifies duplicate Findings using these [hashcode fields](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/): +### Parser versions +The OpenVAS parser has two versions: Version 2 and the legacy version. Only version 2 should be used going forward. This documentation assumes Version 2 going forward. + +Version 2 comes with a number of improvements: +- Use of a hash code algorithm for deduplication +- Increased consistency in parsing between the XML and CSV parsers. +- Combined findings where the only differences are in fields that cannot be rehashed due to inconsistent values between scans (e.g. fields containing timestamps or packet IDs). This prevents duplicates if the vulnerability is found multiple times on the same endpoint. +- Increased parser value coverage +- Heuristic for fix_available detection +- Updated mapping to DefectDojo fields compared to version 1. + +### Deduplication Algorithm +Default Deduplication Hashcode Fields: +By default, DefectDojo Parser V2 identifies duplicate findings using the following [hashcode fields](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/): - title -- cwe -- line -- file path -- description +- severity +- vuln_id_from_tool +- endpoints + +The legacy version (version 1) uses the legacy deduplication algorithm. + +### CSV and XML differences and similarityies +The parser attempts to parse XML and CSV files in a similar way. However, this is not always possible. The following lists the differences between the parsers: + +- EPSS scores and percentiles are only available in CSV format. +- CVSS vectors are only available in the XML format. +- The CVSS score will always be reported as CVSS v3 in the CSV parser +- The references in the CSV parser will never contain URLs. + +If no supported CVSS version is detected, the score (if present) is registered as a CVSS v3 score, even if this is incorrect. diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 7b134e52ad3..b93b577dc2c 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1363,6 +1363,7 @@ def saml2_attrib_map_format(din): "Qualys Hacker Guardian Scan": ["title", "severity", "description"], "Cyberwatch scan (Galeax)": ["title", "description", "severity"], "Cycognito Scan": ["title", "severity"], + "OpenVAS Parser v2": ["title", "severity", "vuln_id_from_tool", "endpoints"], } # Override the hardcoded settings here via the env var @@ -1434,6 +1435,7 @@ def saml2_attrib_map_format(din): "HCL AppScan on Cloud SAST XML": True, "AWS Inspector2 Scan": True, "Cyberwatch scan (Galeax)": True, + "OpenVAS Parser v2": True, } # List of fields that are known to be usable in hash_code computation) @@ -1620,6 +1622,7 @@ def saml2_attrib_map_format(din): "Red Hat Satellite": DEDUPE_ALGO_HASH_CODE, "Qualys Hacker Guardian Scan": DEDUPE_ALGO_HASH_CODE, "Cyberwatch scan (Galeax)": DEDUPE_ALGO_HASH_CODE, + "OpenVAS Parser v2": DEDUPE_ALGO_HASH_CODE, } # Override the hardcoded settings here via the env var diff --git a/dojo/tools/factory.py b/dojo/tools/factory.py index f5c100266a1..a536607f640 100644 --- a/dojo/tools/factory.py +++ b/dojo/tools/factory.py @@ -119,7 +119,12 @@ def requires_tool_type(scan_type): module = import_module(f"dojo.tools.{module_name}.parser") for attribute_name in dir(module): attribute = getattr(module, attribute_name) - if isclass(attribute) and attribute_name.lower() == module_name.replace("_", "") + "parser": + # Allow parser class names with optional v[number] suffix (e.g., OpenVASParser, OpenVASParserV2) + expected_base = module_name.replace("_", "") + "parser" + if isclass(attribute) and ( + attribute_name.lower() == expected_base or + re.match(rf"^{re.escape(expected_base)}v\d+$", attribute_name.lower()) + ): register(attribute) except: logger.exception("failed to load %s", module_name) diff --git a/dojo/tools/openvas/parser.py b/dojo/tools/openvas/parser.py index 9f366c17694..ac65858eec9 100644 --- a/dojo/tools/openvas/parser.py +++ b/dojo/tools/openvas/parser.py @@ -1,5 +1,7 @@ -from dojo.tools.openvas.csv_parser import OpenVASCSVParser -from dojo.tools.openvas.xml_parser import OpenVASXMLParser +from dojo.tools.openvas.parser_v1.csv_parser import OpenVASCSVParser +from dojo.tools.openvas.parser_v1.xml_parser import OpenVASXMLParser +from dojo.tools.openvas.parser_v2.csv_parser import get_findings_from_csv +from dojo.tools.openvas.parser_v2.xml_parser import get_findings_from_xml class OpenVASParser: @@ -18,3 +20,21 @@ def get_findings(self, filename, test): if str(filename.name).endswith(".xml"): return OpenVASXMLParser().get_findings(filename, test) return None + + +class OpenVASParserV2: + def get_scan_types(self): + return ["OpenVAS Parser v2"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import CSV or XML output of Greenbone OpenVAS report." + + def get_findings(self, file, test): + if str(file.name).endswith(".csv"): + return get_findings_from_csv(file, test) + if str(file.name).endswith(".xml"): + return get_findings_from_xml(file, test) + return None diff --git a/dojo/tools/openvas/parser_v1/__init__.py b/dojo/tools/openvas/parser_v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/openvas/csv_parser.py b/dojo/tools/openvas/parser_v1/csv_parser.py similarity index 100% rename from dojo/tools/openvas/csv_parser.py rename to dojo/tools/openvas/parser_v1/csv_parser.py diff --git a/dojo/tools/openvas/xml_parser.py b/dojo/tools/openvas/parser_v1/xml_parser.py similarity index 100% rename from dojo/tools/openvas/xml_parser.py rename to dojo/tools/openvas/parser_v1/xml_parser.py diff --git a/dojo/tools/openvas/parser_v2/__init__.py b/dojo/tools/openvas/parser_v2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/openvas/parser_v2/common.py b/dojo/tools/openvas/parser_v2/common.py new file mode 100644 index 00000000000..ad36e8fa871 --- /dev/null +++ b/dojo/tools/openvas/parser_v2/common.py @@ -0,0 +1,112 @@ +import hashlib +from dataclasses import dataclass + +from dojo.models import Endpoint, Finding + + +@dataclass +class OpenVASFindingAuxData: + + """Dataclass to contain all information added later to a finding""" + + references: list[str] + summary: str = "" + qod: str = "" + openvas_result: str = "" + fallback_cvss_score: float | None = None + + +def setup_finding(test) -> tuple[Finding, OpenVASFindingAuxData]: + """Base setup and init for findings and auxiliary data""" + finding = Finding(test=test, dynamic_finding=True, static_finding=False, severity="Info", nb_occurences=1, cwe=None) + finding.unsaved_vulnerability_ids = [] + finding.unsaved_endpoints = [Endpoint()] + + aux_info = OpenVASFindingAuxData([]) + + return finding, aux_info + + +def is_valid_severity(severity: str) -> bool: + valid_severity = ("Info", "Low", "Medium", "High", "Critical") + return severity in valid_severity + + +def cleanup_openvas_text(text: str) -> str: + """Removes unnessesary defectojo newlines""" + return text.replace("\n ", " ") + + +def escape_restructured_text(text: str) -> str: + """Changes text so that restructured text symbols are not interpreted""" + # OpenVAS likes to include markdown like tables in some fields + # Defectdojo uses reStructuredText which causes them to be rendered wrong + text = text.replace("```", "") + text = text.replace("```", "") + return f"```\n{text}\n```" + + +def postprocess_finding(finding: Finding, aux_info: OpenVASFindingAuxData): + """Update finding with AuxData content""" + if aux_info.openvas_result: + finding.steps_to_reproduce = escape_restructured_text(cleanup_openvas_text(aux_info.openvas_result)) + if aux_info.summary: + finding.description += f"\n**Summary**: {cleanup_openvas_text(aux_info.summary)}" + if aux_info.qod: + finding.description += f"\n**QoD**: {aux_info.qod}" + if len(aux_info.references) > 0: + finding.references = "\n".join(["- " + ref for ref in aux_info.references]) + # fallback in case no cvss version is detected + if aux_info.fallback_cvss_score and not finding.cvssv3_score and not finding.cvssv4_score: + finding.cvssv3_score = aux_info.fallback_cvss_score + + # heuristic for fixed-available detection + if finding.mitigation: + search_terms = ["Update to version", "The vendor has released updates"] + if any(text in finding.mitigation for text in search_terms): + finding.fix_available = True + + +def deduplicate(dupes: dict[str, Finding], finding: Finding): + """Combine multiple openvas findings into one defectdojo finding with potentially multiple endpoints""" + finding_hash = gen_finding_hash(finding) + + if finding_hash not in dupes: + dupes[finding_hash] = finding + else: + # OpenVas does not combine multiple findings into one + # e.g if 2 vulnerable java runtimes are present on the host this is reported as 2 finding. + # The only way do differantiate theese findings when they are based on the same vulnerabilty + # is the data in mapped to steps to reproduce. + # However we cannot hash this field as it can contain data that changes between scans + # e.g timestamps or packet ids + # we therfore combine them into one defectdojo finding because duplicates during reimport cause + # https://github.com/DefectDojo/django-DefectDojo/issues/3958 + org = dupes[finding_hash] + org.nb_occurences += 1 + if org.steps_to_reproduce != finding.steps_to_reproduce: + if "Endpoint" in org.steps_to_reproduce: + org.steps_to_reproduce += "\n---------------------------------------\n" + org.steps_to_reproduce += f"**Endpoint**: {finding.unsaved_endpoints[0].host}\n" + org.steps_to_reproduce += finding.steps_to_reproduce + else: + tmp = org.steps_to_reproduce + org.steps_to_reproduce = f"**Endpoint**: {org.unsaved_endpoints[0].host}\n" + org.steps_to_reproduce += tmp + + # combine identical findings on different hosts into one with multiple hosts + endpoint = finding.unsaved_endpoints[0] + if endpoint not in org.unsaved_endpoints: + org.unsaved_endpoints += finding.unsaved_endpoints + + +def gen_finding_hash(finding: Finding) -> str: + """Generate a hash for a finding that is used for deduplication of findings inside the current report""" + endpoint = finding.unsaved_endpoints[0] + hash_data = [ + str(endpoint), + finding.title, + finding.vuln_id_from_tool, + finding.severity, + ] + return hashlib.sha256("|".join(hash_data).encode("utf-8")).hexdigest() diff --git a/dojo/tools/openvas/parser_v2/csv_parser.py b/dojo/tools/openvas/parser_v2/csv_parser.py new file mode 100644 index 00000000000..23a8cd9d5ca --- /dev/null +++ b/dojo/tools/openvas/parser_v2/csv_parser.py @@ -0,0 +1,179 @@ +import csv +import io +import logging + +from dateutil.parser import parse as parse_date + +from dojo.models import Finding +from dojo.tools.openvas.parser_v2.common import ( + OpenVASFindingAuxData, + cleanup_openvas_text, + deduplicate, + is_valid_severity, + postprocess_finding, + setup_finding, +) + +logger = logging.getLogger(__name__) + + +def get_findings_from_csv(file, test) -> list[Finding]: + """Returns list of findings as defectdojo factory contract expects""" + dupes = {} + if not isinstance(file, io.TextIOWrapper): + file = io.TextIOWrapper(file, encoding="utf-8") + csv_reader = csv.reader(file, delimiter=",", quotechar='"') + column_names = [column_name.lower() for column_name in next(csv_reader) if column_name] + + if "nvt name" not in column_names: + msg = "Invalid OpenVAS csv file" + raise ValueError(msg) + + parser = CSVParserV2() + for row in csv_reader: + finding, aux_info = setup_finding(test) + + for column_value, column_name in zip(row, column_names, strict=False): + parser.process_column(column_name, column_value, finding, aux_info) + + postprocess_finding(finding, aux_info) + deduplicate(dupes, finding) + + return list(dupes.values()) + + +class CSVParserV2: + def __init__(self): + self.column_handlers = { + "nvt name": self._handle_nvt_name, + "cweid": self._handle_cweid, + "cves": self._handle_cves, + "nvt oid": self._handle_nvt_oid, + "hostname": self._handle_hostname, + "ip": self._handle_ip, + "port": self._handle_port, + "port protocol": self._handle_port_protocol, + "severity": self._handle_severity, + "cvss": self._handle_cvss, + "summary": self._handle_summary, + "solution": self._handle_solution, + "vulnerability insight": self._handle_vulnerability_insight, + "specific result": self._handle_specific_result, + "qod": self._handle_qod, + "max severity epss score": self._handle_epss_score, + "max severity epss percentile": self._handle_epss_percentile, + "timestamp": self._handle_timestamp, + "active": self._handle_active, + "verified": self._handle_verified, + "falsepositive": self._handle_falsepositive, + "duplicate": self._handle_duplicate, + "other references": self._handle_references, + } + + def process_column( + self, + column_name: str, + column_value: str, + finding: Finding, + aux_info: OpenVASFindingAuxData, + ): + # skip columns with empty values + if not column_value: + return + + # tmp save common values in object for cleaner method signature + self.finding = finding + self.aux_info = aux_info + + handler = self.column_handlers.get(column_name) + try: + if handler: + handler(column_value) + except ValueError as e: + logger.debug("openvas parser v2: error parsing column %s: %s", column_name, e) + + def _handle_nvt_name(self, column_value: str): + self.finding.title = column_value + + def _handle_cweid(self, column_value: str): + if column_value.isdigit(): + self.finding.cwe = int(column_value) + + def _handle_cves(self, column_value: str): + for cve in column_value.split(","): + self.finding.unsaved_vulnerability_ids.append(cve) + + def _handle_nvt_oid(self, column_value: str): + self.finding.vuln_id_from_tool = column_value + + def _handle_hostname(self, column_value: str): + # strip due to https://github.com/greenbone/gvmd/issues/2378 + self.finding.unsaved_endpoints[0].host = column_value.strip() + + def _handle_ip(self, column_value: str): + # fallback to ip if hostname is not aviable + if not self.finding.unsaved_endpoints[0].host: + # strip due to https://github.com/greenbone/gvmd/issues/2378 + self.finding.unsaved_endpoints[0].host = column_value.strip() + + def _handle_port(self, column_value: str): + if column_value.isdigit(): + self.finding.unsaved_endpoints[0].port = int(column_value) + + def _handle_port_protocol(self, column_value: str): + self.finding.unsaved_endpoints[0].protocol = column_value + + def _handle_severity(self, column_value: str): + if is_valid_severity(column_value): + self.finding.severity = column_value + + def _handle_cvss(self, column_value: str): + self.aux_info.fallback_cvss_score = float(column_value) + + def _handle_summary(self, column_value: str): + self.aux_info.summary = column_value + + def _handle_solution(self, column_value: str): + self.finding.mitigation = cleanup_openvas_text(column_value) + + def _handle_vulnerability_insight(self, column_value: str): + self.finding.impact = cleanup_openvas_text(column_value) + + def _handle_specific_result(self, column_value: str): + self.aux_info.openvas_result = column_value + + def _handle_qod(self, column_value: str): + self.aux_info.qod = column_value + + def _handle_epss_score(self, column_value: str): + self.finding.epss_score = float(column_value) + + def _handle_epss_percentile(self, column_value: str): + self.finding.epss_percentile = float(column_value) + + def _handle_timestamp(self, column_value: str): + self.finding.date = parse_date(column_value).date() + + def _handle_references(self, column_value: str): + self.aux_info.references = column_value.split(",") + + def _handle_active(self, column_value: str): + self.finding.active = self._str_to_bool(column_value) + + def _handle_verified(self, column_value: str): + self.finding.verified = self._str_to_bool(column_value) + + def _handle_falsepositive(self, column_value: str): + self.finding.false_p = self._str_to_bool(column_value) + + def _handle_duplicate(self, column_value: str): + self.finding.duplicate = self._str_to_bool(column_value) + + def _str_to_bool(self, column_value: str) -> bool | None: + """Converts string to bool or None""" + value = column_value.lower() + if value == "true": + return True + if value == "false": + return False + return None diff --git a/dojo/tools/openvas/parser_v2/xml_parser.py b/dojo/tools/openvas/parser_v2/xml_parser.py new file mode 100644 index 00000000000..60facc087bf --- /dev/null +++ b/dojo/tools/openvas/parser_v2/xml_parser.py @@ -0,0 +1,169 @@ +import contextlib +import logging +from xml.dom import NamespaceErr + +from defusedxml import ElementTree + +from dojo.models import Finding +from dojo.tools.openvas.parser_v2.common import ( + OpenVASFindingAuxData, + cleanup_openvas_text, + deduplicate, + is_valid_severity, + postprocess_finding, + setup_finding, +) +from dojo.utils import parse_cvss_data + +logger = logging.getLogger(__name__) + + +def get_findings_from_xml(file, test) -> list[Finding]: + """Returns list of findings as defectdojo factory contract expects""" + dupes = {} + tree = ElementTree.parse(file) + root = tree.getroot() + + if "report" not in root.tag: + msg = "This doesn't seem to be a valid Greenbone/ OpenVAS XML file." + raise NamespaceErr(msg) + + report = root.find("report") + results = report.find("results") + + parser = XMLParserV2() + for result in results: + finding, aux_info = setup_finding(test) + for element in result: + parser.process_element(element, finding, aux_info) + + postprocess_finding(finding, aux_info) + deduplicate(dupes, finding) + + return list(dupes.values()) + + +class XMLParserV2: + def __init__(self): + self.tag_handlers = { + "nvt": self._handle_nvt, + "qod": self._handle_qod, + "name": self._handle_name, + "host": self._handle_host, + "port": self._handle_port, + "severity": self._handle_severity, + "threat": self._handle_threat, + "description": self._handle_description, + } + + def process_element(self, field, finding: Finding, aux_info: OpenVASFindingAuxData): + # tmp save common values in object for cleaner method signature + self.finding = finding + self.aux_info = aux_info + + handler = self.tag_handlers.get(field.tag) + try: + if handler: + handler(field) + except ValueError as e: + logger.debug("openvas parser v2: error parsing field %s: %s", field.tag, e) + + def _handle_nvt(self, field): + self.finding.vuln_id_from_tool = field.get("oid") + nvt_name = field.find("name").text + if nvt_name: + self.finding.title = nvt_name + + # parse solution (also included in tags field if backup is needed) + solution = field.find("solution") + if solution is not None: + self.finding.mitigation = cleanup_openvas_text(solution.text) + + # parse cves and references + refs_node = field.find("refs") + if refs_node is not None: + # this field can contain cves, other security vendors ids or urls + refs = refs_node.findall(".//ref") + self.finding.unsaved_vulnerability_ids = [ref.get("id") for ref in refs if ref.get("type") == "cve"] + # only include urls in references + self.aux_info.references = [ref.get("id") for ref in refs if ref.get("type") != "cve"] + + # parse tags fields + tag_field = field.find("tags") + tags = self._parse_nvt_tags(tag_field.text) + summary = tags.get("summary", None) + if summary: + self.aux_info.summary = summary + + impact = tags.get("impact", None) + if impact: + self.finding.impact = cleanup_openvas_text(impact) + + cvss_base_vector = tags.get("cvss_base_vector", None) + if cvss_base_vector: + cvss_data = parse_cvss_data(cvss_base_vector) + self.finding.cvssv3 = cvss_data["cvssv3"] + self.finding.cvssv4 = cvss_data["cvssv4"] + + # only report the score as cvssv3 if cvss major version is 2 + # as cvss v2 vectors are not supported + if cvss_data["major_version"] == 2: + self.finding.cvssv3_score = cvss_data["cvssv2_score"] + + def _handle_qod(self, field): + self.aux_info.qod = field.find("value").text + + def _handle_name(self, field): + if field.text: + self.finding.title = field.text + + def _handle_host(self, field): + if field.text: + hostname_field = field.find("hostname") + # default to hostname else ip + if hostname_field is not None and hostname_field.text: + # strip due to https://github.com/greenbone/gvmd/issues/2378 + self.finding.unsaved_endpoints[0].host = hostname_field.text.strip() + else: + # strip due to https://github.com/greenbone/gvmd/issues/2378 + self.finding.unsaved_endpoints[0].host = field.text.strip() + + def _handle_port(self, field): + if field.text: + port_str, protocol = field.text.split("/") + self.finding.unsaved_endpoints[0].protocol = protocol + with contextlib.suppress(ValueError): + self.finding.unsaved_endpoints[0].port = int(port_str) + + def _handle_severity(self, field): + if field.text: + self.aux_info.fallback_cvss_score = float(field.text) + + def _handle_threat(self, field): + if field.text and is_valid_severity(field.text): + self.finding.severity = field.text + + def _handle_description(self, field): + if field.text: + self.aux_info.openvas_result = field.text.strip() + + def _parse_nvt_tags(self, text: str) -> dict[str, str]: + """ + Parse tags in nvt field into dict + Example: + + Input: "summary=This is a test|impact=High|solution=Update software" + Output: {"summary": "This is a test", "impact": "High", "solution": "Update software"} + """ + parts = text.strip().split("|") + tags = {} + + for part in parts: + idx = part.find("=") + if idx == -1 or (len(part) < idx + 2): + continue + + key = part[0:idx] + val = part[idx + 1 :] + tags[key] = val + return tags diff --git a/unittests/scans/openvas/many_vuln.xml b/unittests/scans/openvas/many_vuln.xml index d3f975d3ef7..baec0128837 100644 --- a/unittests/scans/openvas/many_vuln.xml +++ b/unittests/scans/openvas/many_vuln.xml @@ -149,13 +149,13 @@ - {v1}467e39e554a + 467e39e554a gps 2023-09-29T11:36:37.717168Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 512/tcp nvt @@ -189,13 +189,13 @@ 5 - {v1}530765cf437 + 530765cf437 gps 2023-09-29T11:36:37.717208Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 1524/tcp nvt @@ -224,13 +224,13 @@ 5 - {v1}5f5c7518c92 + 5f5c7518c92 gps 2023-09-29T11:36:37.717216Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 8787/tcp nvt @@ -304,13 +304,13 @@ 5 - {v1}8c49cb44d75 + 8c49cb44d75 gps 2023-09-29T11:36:37.717246Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 general/tcp nvt @@ -349,13 +349,13 @@ 5 - {v1}22a938294ad + 22a938294ad gps 2023-09-29T11:36:37.717262Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -402,13 +402,13 @@ 5 - {v1}9e2edd735b3 + 9e2edd735b3 gps 2023-09-29T11:36:37.717281Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 3632/tcp nvt @@ -453,13 +453,13 @@ 5 - {v1}0b02451a968 + 0b02451a968 gps 2023-09-29T11:36:37.717494Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5900/tcp nvt @@ -498,13 +498,13 @@ 5 - {v1}e93a2434477 + e93a2434477 gps 2023-09-29T11:36:37.717503Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -531,13 +531,13 @@ 5 - {v1}3723bfe0094 + 3723bfe0094 gps 2023-09-29T11:36:37.717511Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 6667/tcp nvt @@ -580,13 +580,13 @@ 5 - {v1}3723bfe0094 + 3723bfe0094 gps 2023-09-29T11:36:37.717520Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 6697/tcp nvt @@ -629,13 +629,13 @@ 5 - {v1}a358693375b + a358693375b gps 2023-09-29T11:36:37.717529Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 21/tcp nvt @@ -690,13 +690,13 @@ 5 - {v1}4ecebea5997 + 4ecebea5997 gps 2023-09-29T11:36:37.717538Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -740,13 +740,13 @@ 5 - {v1}dcc8491b116 + dcc8491b116 gps 2023-09-29T11:36:37.717558Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 21/tcp nvt @@ -781,13 +781,13 @@ 5 - {v1}a358693375b + a358693375b gps 2023-09-29T11:36:37.717575Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 2121/tcp nvt @@ -839,13 +839,13 @@ 5 - {v1}edca4d29119 + edca4d29119 gps 2023-09-29T11:36:37.717584Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -883,13 +883,13 @@ 5 - {v1}28996b2da9a + 28996b2da9a gps 2023-09-29T11:36:37.717594Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -956,13 +956,13 @@ 5 - {v1}f209b933bd1 + f209b933bd1 gps 2023-09-29T11:36:37.717604Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 22/tcp nvt @@ -1029,13 +1029,13 @@ 5 - {v1}dcc8491b116 + dcc8491b116 gps 2023-09-29T11:36:37.717613Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 6200/tcp nvt @@ -1070,13 +1070,13 @@ 5 - {v1}d803f61f444 + d803f61f444 gps 2023-09-29T11:36:37.717621Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -1115,13 +1115,13 @@ 5 - {v1}e70046de17f + e70046de17f gps 2023-09-29T11:36:37.717637Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1164,13 +1164,13 @@ 5 - {v1}944cfcaaf66 + 944cfcaaf66 gps 2023-09-29T11:36:37.717645Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 21/tcp nvt @@ -1222,13 +1222,13 @@ 5 - {v1}cc1c4db6d4f + cc1c4db6d4f gps 2023-09-29T11:36:37.717654Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1276,13 +1276,13 @@ 5 - {v1}44d224b77c4 + 44d224b77c4 gps 2023-09-29T11:36:37.717662Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1314,13 +1314,13 @@ 5 - {v1}e70046de17f + e70046de17f gps 2023-09-29T11:36:37.717670Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1361,13 +1361,13 @@ 5 - {v1}71c655fd352 + 71c655fd352 gps 2023-09-29T11:36:37.717677Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 445/tcp nvt @@ -1400,13 +1400,13 @@ 5 - {v1}e79b358813f + e79b358813f gps 2023-09-29T11:36:37.717686Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -1463,13 +1463,13 @@ 5 - {v1}75693259c28 + 75693259c28 gps 2023-09-29T11:36:37.717697Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1538,13 +1538,13 @@ 5 - {v1}316b754124f + 316b754124f gps 2023-09-29T11:36:37.717709Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 22/tcp nvt @@ -1601,13 +1601,13 @@ 5 - {v1}79868c7d9b2 + 79868c7d9b2 gps 2023-09-29T11:36:37.717720Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 22/tcp nvt @@ -1638,13 +1638,13 @@ 5 - {v1}e3e389ce2ba + e3e389ce2ba gps 2023-09-29T11:36:37.717728Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -1712,13 +1712,13 @@ 5 - {v1}66ec0c4c6a4 + 66ec0c4c6a4 gps 2023-09-29T11:36:37.717749Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1756,13 +1756,13 @@ 5 - {v1}fec842e796e + fec842e796e gps 2023-09-29T11:36:37.717762Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -1808,13 +1808,13 @@ 5 - {v1}bccd1cd5b97 + bccd1cd5b97 gps 2023-09-29T11:36:37.717769Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1854,13 +1854,13 @@ 5 - {v1}68aaba31879 + 68aaba31879 gps 2023-09-29T11:36:37.717783Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -1909,13 +1909,13 @@ 5 - {v1}4406907af6b + 4406907af6b gps 2023-09-29T11:36:37.717794Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5900/tcp nvt @@ -1951,13 +1951,13 @@ 5 - {v1}1fa3ebb87ec + 1fa3ebb87ec gps 2023-09-29T11:36:37.717806Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 21/tcp nvt @@ -1991,13 +1991,13 @@ 5 - {v1}1fa3ebb87ec + 1fa3ebb87ec gps 2023-09-29T11:36:37.717816Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 2121/tcp nvt @@ -2031,13 +2031,13 @@ 5 - {v1}e79b358813f + e79b358813f gps 2023-09-29T11:36:37.717825Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -2099,13 +2099,13 @@ 5 - {v1}9c322581ba5 + 9c322581ba5 gps 2023-09-29T11:36:37.717836Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -2144,13 +2144,13 @@ 5 - {v1}2b0831858b0 + 2b0831858b0 gps 2023-09-29T11:36:37.717847Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -2192,13 +2192,13 @@ 5 - {v1}55390940921 + 55390940921 gps 2023-09-29T11:36:37.717855Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 22/tcp nvt @@ -2266,13 +2266,13 @@ 5 - {v1}1fe916ed11d + 1fe916ed11d gps 2023-09-29T11:36:37.717864Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 80/tcp nvt @@ -2320,13 +2320,13 @@ 5 - {v1}101c559718c + 101c559718c gps 2023-09-29T11:36:37.717875Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -2370,13 +2370,13 @@ 5 - {v1}fec842e796e + fec842e796e gps 2023-09-29T11:36:37.717887Z - 192.168.1.1001{v1}b6b9f466d63 + 192.168.1.1001b6b9f466d63 5432/tcp nvt @@ -2500,4 +2500,4 @@ 2023-09-26T13:04:00Z - \ No newline at end of file + diff --git a/unittests/scans/openvas/no_vuln.csv b/unittests/scans/openvas/no_vuln.csv new file mode 100644 index 00000000000..0830a74615a --- /dev/null +++ b/unittests/scans/openvas/no_vuln.csv @@ -0,0 +1 @@ +IP,Hostname,Port,Port Protocol,CVSS,Severity,Solution Type,NVT Name,Summary,Specific Result,NVT OID,CVEs,Task ID,Task Name,Timestamp,Result ID,Impact,Solution,Affected Software/OS,Vulnerability Insight,Vulnerability Detection Method,Product Detection Result,BIDs,CERTs,Other References diff --git a/unittests/scans/openvas/report_combine_v2.csv b/unittests/scans/openvas/report_combine_v2.csv new file mode 100644 index 00000000000..3ae134cd638 --- /dev/null +++ b/unittests/scans/openvas/report_combine_v2.csv @@ -0,0 +1,57 @@ +IP,Hostname,Port,Port Protocol,CVSS,Severity,QoD,Solution Type,NVT Name,Summary,Specific Result,NVT OID,CVEs,Task ID,Task Name,Timestamp,Result ID,Impact,Solution,Affected Software/OS,Vulnerability Insight,Vulnerability Detection Method,Product Detection Result,BIDs,CERTs,Other References +45.33.32.156,,,,2.6,Low,80,"Mitigation","TCP Timestamps Information Disclosure","The remote host implements TCP timestamps and therefore allows + to compute the uptime.","It was detected that the host implements RFC1323/RFC7323. + +The following timestamps were retrieved with a delay of 1 seconds in-between: +Packet 1: 1912048204 +Packet 2: 1912049513 +",1.3.6.1.4.1.25623.1.0.80091,"",ef9cd713-0144-4fe5-a19d-6849983ae3d1,"ScanmeNmap",2024-03-18T12:46:31Z,167b0841-3f29-450b-bb44-a7d88999b3bc,"A side effect of this feature is that the uptime of the remote + host can sometimes be computed.","To disable TCP timestamps on linux add the line + 'net.ipv4.tcp_timestamps = 0' to /etc/sysctl.conf. Execute 'sysctl -p' to apply the settings at + runtime. + + To disable TCP timestamps on Windows execute 'netsh int tcp set global timestamps=disabled' + + Starting with Windows Server 2008 and Vista, the timestamp can not be completely disabled. + + The default behavior of the TCP/IP stack on this Systems is to not use the Timestamp options when + initiating TCP connections, but use them if the TCP peer that is initiating communication includes + them in their synchronize (SYN) segment. + + See the references for more information.","TCP implementations that implement RFC1323/RFC7323.","The remote host implements TCP timestamps, as defined by + RFC1323/RFC7323.","Special IP packets are forged and sent with a little delay in + between to the target IP. The responses are searched for a timestamps. If found, the timestamps + are reported. +Details: +TCP Timestamps Information Disclosure +(OID: 1.3.6.1.4.1.25623.1.0.80091) +Version used: 2023-12-15T16:10:08Z +","","","","" +45.33.32.156,,,,2.6,Low,80,"Mitigation","TCP Timestamps Information Disclosure","The remote host implements TCP timestamps and therefore allows + to compute the uptime.","It was detected that the host implements RFC1323/RFC7323. + +The following timestamps were retrieved with a delay of 1 seconds in-between: +Packet 1: 1912048205 +Packet 2: 1912049516 +",1.3.6.1.4.1.25623.1.0.80091,"",ef9cd713-0144-4fe5-a19d-6849983ae3d1,"ScanmeNmap",2024-03-18T12:46:31Z,167b0841-3f29-450b-bb44-a7d88999b3bc,"A side effect of this feature is that the uptime of the remote + host can sometimes be computed.","To disable TCP timestamps on linux add the line + 'net.ipv4.tcp_timestamps = 0' to /etc/sysctl.conf. Execute 'sysctl -p' to apply the settings at + runtime. + + To disable TCP timestamps on Windows execute 'netsh int tcp set global timestamps=disabled' + + Starting with Windows Server 2008 and Vista, the timestamp can not be completely disabled. + + The default behavior of the TCP/IP stack on this Systems is to not use the Timestamp options when + initiating TCP connections, but use them if the TCP peer that is initiating communication includes + them in their synchronize (SYN) segment. + + See the references for more information.","TCP implementations that implement RFC1323/RFC7323.","The remote host implements TCP timestamps, as defined by + RFC1323/RFC7323.","Special IP packets are forged and sent with a little delay in + between to the target IP. The responses are searched for a timestamps. If found, the timestamps + are reported. +Details: +TCP Timestamps Information Disclosure +(OID: 1.3.6.1.4.1.25623.1.0.80091) +Version used: 2023-12-15T16:10:08Z +","","","","" diff --git a/unittests/scans/openvas/report_detail_v2.csv b/unittests/scans/openvas/report_detail_v2.csv new file mode 100644 index 00000000000..31f5968f2fb --- /dev/null +++ b/unittests/scans/openvas/report_detail_v2.csv @@ -0,0 +1,20 @@ +IP,Hostname,Port,Port Protocol,CVSS,Severity,QoD,Solution Type,NVT Name,Summary,Specific Result,NVT OID,CVEs,Task ID,Task Name,Timestamp,Result ID,Impact,Solution,Affected Software/OS,Vulnerability Insight,Vulnerability Detection Method,Product Detection Result,BIDs,CERTs,Other References,Max Severity EPSS score,Max Severity EPSS percentile +10.99.99.99,server99,42,tcp,9.8,High,80,"VendorFix","Microsoft Windows Multiple Vulnerabilities (KB5062557)","This host is missing an important security + update according to Microsoft KB5062557","Vulnerable range: 10.0.17763.0 - 10.0.17763.7557 +File checked: C:\Windows\system32\Ntoskrnl.exe +File version: 10.0.17763.7434 + +",1.3.6.1.4.1.25623.1.0.836484,"CVE-2025-49659,CVE-2025-48823,CVE-2025-49684,CVE-2025-49668,CVE-2025-49744,CVE-2025-49683,CVE-2025-49663,CVE-2025-49725,CVE-2025-49675,CVE-2025-49732,CVE-2025-49722,CVE-2025-49669,CVE-2025-48822,CVE-2025-49740,CVE-2025-49729,CVE-2025-49679,CVE-2025-49667,CVE-2025-49666,CVE-2025-48819,CVE-2025-49742,CVE-2025-49733,CVE-2025-49727,CVE-2025-49680,CVE-2025-49678,CVE-2025-48816,CVE-2025-49673,CVE-2025-49665,CVE-2025-49660,CVE-2025-48821,CVE-2025-48818,CVE-2025-48811,CVE-2025-48806,CVE-2025-48001,CVE-2025-47982,CVE-2025-49753,CVE-2025-49686,CVE-2025-47999,CVE-2025-49730,CVE-2025-49724,CVE-2025-49685,CVE-2025-49681,CVE-2025-49664,CVE-2025-48820,CVE-2025-48817,CVE-2025-48815,CVE-2025-48814,CVE-2025-48808,CVE-2025-48805,CVE-2025-48804,CVE-2025-48803,CVE-2025-48800,CVE-2025-48799,CVE-2025-48003,CVE-2025-48000,CVE-2025-47998,CVE-2025-47996,CVE-2025-47981,CVE-2025-47980,CVE-2025-47975,CVE-2025-47973,CVE-2025-49760,CVE-2025-49726,CVE-2025-49723,CVE-2025-49721,CVE-2025-49716,CVE-2025-36350,CVE-2025-36357,CVE-2025-47991,CVE-2025-49691,CVE-2025-49690,CVE-2025-49689,CVE-2025-49688,CVE-2025-49687,CVE-2025-49676,CVE-2025-49674,CVE-2025-49672,CVE-2025-49671,CVE-2025-49670,CVE-2025-49661,CVE-2025-49658,CVE-2025-49657,CVE-2025-48824,CVE-2025-47987,CVE-2025-47986,CVE-2025-47985,CVE-2025-47984,CVE-2025-47976,CVE-2025-47972,CVE-2025-47971,CVE-2025-47159,CVE-2025-48807,CVE-2025-53789,CVE-2025-49757",4949d3d6-705b-41d5-b494-383860f8c970,"Report",2025-08-22T16:27:22+02:00,ec5f93ff-3447-4171-8485-3b3b3af2edc0,"Successful exploitation allows an attacker + to elevate privileges, execute arbitrary commands, disclose information, + bypass security restrictions, conduct spoofing and denial of service attacks.","The vendor has released updates. Please see + the references for more information.","'- Microsoft Windows 10 Version 1809 for 32-bit Systems + + - Microsoft Windows 10 Version 1809 for x64-based Systems + + - Microsoft Windows Server 2019","","Checks if a vulnerable version is present + on the target host. +Details: +Microsoft Windows Multiple Vulnerabilities (KB5062557) +(OID: 1.3.6.1.4.1.25623.1.0.836484) +Version used: 2025-08-15T07:40:49+02:00 +","","","DFN-CERT-2025-2181,DFN-CERT-2025-1825,WID-SEC-2025-1850,WID-SEC-2025-1790,WID-SEC-2025-1495","",0.00143,0.35177 diff --git a/unittests/scans/openvas/report_detail_v2.xml b/unittests/scans/openvas/report_detail_v2.xml new file mode 100644 index 00000000000..bfa03f3a786 --- /dev/null +++ b/unittests/scans/openvas/report_detail_v2.xml @@ -0,0 +1,217 @@ + + + + admin + + 2025-08-22T15:00:08+02:00 + + 2025-08-22T15:00:08+02:00 + 2025-08-22T17:09:58+02:00 + 0 + 0 + + Report + + + XML + + + + 22.6 + + + severitydescending + + Done + + Report + disable_BruteForce_default + + 0 + Test + + + 100 + + 2025-08-22T15:00:08+02:00 + 2025-08-22T15:01:38+02:00 + Europe/Berlin + CEST + + 1 + 42/tcp10.99.99.999.8High + + + + Microsoft Windows Multiple Vulnerabilities (KB5062557) + + admin + + 2025-08-22T16:55:31+02:00 + + 2025-08-22T16:55:31+02:00 + 10.99.99.99server99 + 42/tcp + + nvt + Microsoft Windows Multiple Vulnerabilities (KB5062557) + Windows : Microsoft Bulletins + 9.8 + + + NVD + 2025-07-08T19:15:38+02:00 + 9.8 + CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + + cvss_base_vector=CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H|summary=This host is missing an important security + update according to Microsoft KB5062557|insight=|affected=- Microsoft Windows 10 Version 1809 for 32-bit Systems + + - Microsoft Windows 10 Version 1809 for x64-based Systems + + - Microsoft Windows Server 2019|impact=Successful exploitation allows an attacker + to elevate privileges, execute arbitrary commands, disclose information, + bypass security restrictions, conduct spoofing and denial of service attacks.|solution=The vendor has released updates. Please see + the references for more information.|vuldetect=Checks if a vulnerable version is present + on the target host.|solution_type=VendorFix + The vendor has released updates. Please see + the references for more information. + + + 0.00143 + 0.35177 + + 9.8 + + + + 0.09023 + 0.92284 + + 7.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2025-08-15T07:40:49+02:00 + High + 9.8 + + 80 + + + Vulnerable range: 10.0.17763.0 - 10.0.17763.7557 +File checked: C:\Windows\system32\Ntoskrnl.exe +File version: 10.0.17763.7434 + + + High + 9.8 + undefined + + + 2025-08-22T17:09:57+02:00 + + diff --git a/unittests/tools/test_openvas_parser.py b/unittests/tools/test_openvas_parser.py index 7ec8cf7ebf2..b17ffa6108e 100644 --- a/unittests/tools/test_openvas_parser.py +++ b/unittests/tools/test_openvas_parser.py @@ -1,8 +1,128 @@ from dojo.models import Engagement, Product, Test -from dojo.tools.openvas.parser import OpenVASParser +from dojo.tools.openvas.parser import OpenVASParser, OpenVASParserV2 from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path +# V2 Parser tests +def openvas_open(file): + """Helper to get file handle to openvas test files""" + return (get_unit_tests_scans_path("openvas") / file).open(encoding="utf-8") + + +def setup_openvas_v2_test(f): + """Setup helper for general openvas_v2 test setup""" + test = Test() + test.engagement = Engagement() + test.engagement.product = Product() + parser = OpenVASParserV2() + findings = parser.get_findings(f, test) + for finding in findings: + for endpoint in finding.unsaved_endpoints: + endpoint.clean() + return findings + + +class TestOpenVASParserV2(DojoTestCase): + # test empty cases + def test_openvas_csv_no_vuln(self): + """Ensure that an empty report does not throw and error and reports 0 findings""" + with openvas_open("no_vuln.csv") as f: + findings = setup_openvas_v2_test(f) + self.assertEqual(0, len(findings)) + + def test_openvas_xml_no_vuln(self): + """Ensure that an empty report does not throw and error and reports 0 findings""" + with openvas_open("no_vuln.xml") as f: + findings = setup_openvas_v2_test(f) + self.assertEqual(0, len(findings)) + + def test_openvas_parser_csv_detail(self): + """Ensure finding contains report data as expected""" + with openvas_open("report_detail_v2.csv") as f: + findings = setup_openvas_v2_test(f) + + # ensure single finding + self.assertEqual(len(findings), 1) + finding = findings[0] + + # general finding info tests + self.assertEqual("Microsoft Windows Multiple Vulnerabilities (KB5062557)", finding.title) + self.assertEqual("High", finding.severity) # OpenVAS reports Critical findings as High + self.assertEqual(9.8, finding.cvssv3_score) + self.assertEqual(0.00143, finding.epss_score) + self.assertEqual(0.35177, finding.epss_percentile) + + # vulnerability id tests + self.assertEqual(finding.vuln_id_from_tool, "1.3.6.1.4.1.25623.1.0.836484") + self.assertEqual(finding.unsaved_vulnerability_ids[1], "CVE-2025-48823") + self.assertEqual(93, len(finding.unsaved_vulnerability_ids)) + + # endpoint tests + self.assertEqual(1, len(finding.unsaved_endpoints)) + self.assertEqual("server99", finding.unsaved_endpoints[0].host) + # this is example data normaly tested finding does not include this + self.assertEqual(42, finding.unsaved_endpoints[0].port) + self.assertEqual("tcp", finding.unsaved_endpoints[0].protocol) + + def test_openvas_parser_xml_detail(self): + """Ensure finding contains report data as expected""" + with openvas_open("report_detail_v2.xml") as f: + findings = setup_openvas_v2_test(f) + + # ensure single finding + self.assertEqual(len(findings), 1) + finding = findings[0] + self.assertEqual(9.8, finding.cvssv3_score) + + def test_openvas_parser_csv_xml_parity(self): + """Ensure xml and csv parser parse data that is the same between report in the same way""" + with openvas_open("report_detail_v2.csv") as f: + findings_csv = setup_openvas_v2_test(f) + with openvas_open("report_detail_v2.xml") as f: + findings_xml = setup_openvas_v2_test(f) + + f_xml = findings_xml[0] + f_csv = findings_csv[0] + + # ensure same general finding parsing behaviour + self.assertEqual(f_xml.title, f_csv.title) + self.assertEqual(f_xml.severity, f_csv.severity) + # remove this if future parser versions want different description behaviour + self.assertEqual(f_xml.description, f_csv.description) + + # ensure same vulnerability id parsing behaviour + self.assertEqual(f_xml.vuln_id_from_tool, f_csv.vuln_id_from_tool) + # xml has multiple types of vulnerability ids, change this if a new one is parsed + self.assertEqual(len(f_xml.unsaved_vulnerability_ids), len(f_csv.unsaved_vulnerability_ids)) + self.assertEqual(f_xml.unsaved_vulnerability_ids, f_csv.unsaved_vulnerability_ids) + + # ensure same endpoint parsing behaviour + self.assertEqual(f_xml.unsaved_endpoints[0].host, f_csv.unsaved_endpoints[0].host) + self.assertEqual(f_xml.unsaved_endpoints[0].protocol, f_csv.unsaved_endpoints[0].protocol) + self.assertEqual(f_xml.unsaved_endpoints[0].port, f_csv.unsaved_endpoints[0].port) + + def test_openvas_csv_report_combined_findings(self): + """Ensure findings combinding behaviour""" + with openvas_open("report_combine_v2.csv") as f: + findings = setup_openvas_v2_test(f) + self.assertEqual(1, len(findings)) + finding = findings[0] + self.assertEqual(2, finding.nb_occurences) + + def test_openvas_csv_many_findings(self): + """Ensure findings combinding behaviour""" + with openvas_open("many_vuln.csv") as f: + findings = setup_openvas_v2_test(f) + self.assertEqual(4, len(findings)) + + def test_openvas_xml_many_findings(self): + """Ensure findings combinding behaviour""" + with openvas_open("many_vuln.xml") as f: + findings = setup_openvas_v2_test(f) + self.assertEqual(44, len(findings)) + + +# V1 Parser tests class TestOpenVASParser(DojoTestCase): def test_openvas_csv_one_vuln(self): with (get_unit_tests_scans_path("openvas") / "one_vuln.csv").open(encoding="utf-8") as f: @@ -105,7 +225,10 @@ def test_openvas_xml_one_vuln(self): self.assertEqual(1, len(findings)) with self.subTest(i=0): finding = findings[0] - self.assertEqual("Mozilla Firefox Security Update (mfsa_2023-32_2023-36) - Windows_10.0.101.2_general/tcp", finding.title) + self.assertEqual( + "Mozilla Firefox Security Update (mfsa_2023-32_2023-36) - Windows_10.0.101.2_general/tcp", + finding.title, + ) self.assertEqual("High", finding.severity) def test_openvas_xml_many_vuln(self):