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..b7165ad7384 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/openvas.md +++ b/docs/content/en/connecting_your_tools/parsers/file/openvas.md @@ -15,3 +15,9 @@ By default, DefectDojo identifies duplicate Findings using these [hashcode field - line - file path - description + +### Parser V2 Changes +Version 2 comes with multiple improvments: +- Increased parsing Consistensy between the xml and csv parser +- Combined findings where the only differences are in fields that can’t be rehashed due to inconsistent values between scans e.g fields with timestamps or packet ids. +- Parser now combines multiple identical findings with different endpoints into one findings with multiple endpoints (instead of multiple findings with one endpoint each) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 76a82c39c28..67a7cc38f74 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1356,6 +1356,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"], } # Override the hardcoded settings here via the env var @@ -1427,6 +1428,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) @@ -1613,6 +1615,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..ebf1b488213 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 OpenVASCSVParserV2 +from dojo.tools.openvas.parser_v2.xml_parser import OpenVASXMLParserV2 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, filename, test): + if str(filename.name).endswith(".csv"): + return OpenVASCSVParserV2().get_findings(filename, test) + if str(filename.name).endswith(".xml"): + return OpenVASXMLParserV2().get_findings(filename, 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..cd69640ee6c --- /dev/null +++ b/dojo/tools/openvas/parser_v2/common.py @@ -0,0 +1,81 @@ +import hashlib +from dataclasses import dataclass + +from dojo.models import Finding + + +@dataclass +class OpenVASFindingAuxData: + + """Dataclass to contain all information added later to fields""" + + summary: str = "" + qod: str = "" + openvas_result: str = "" + + +def is_valid_severity(severity): + valid_severity = ("Info", "Low", "Medium", "High", "Critical") + return severity in valid_severity + + +def cleanup_openvas_text(text: str): + return text.replace("\n ", " ") + + +def update_finding(finding: Finding, aux_info: OpenVASFindingAuxData): + """Update finding description""" + if aux_info.openvas_result: + finding.steps_to_reproduce = 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}" + + +def deduplicate(dupes: dict[str, Finding], finding: Finding): + """Combine multiple openvas findings into one defectdojo finding with multiple endpoints""" + finding_hash = dedup_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] + 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 dedup_finding_hash(finding: Finding): + """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.protocol), + str(endpoint.userinfo), + str(endpoint.port), + str(endpoint.path), + str(endpoint.fragment), + 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..db21819f398 --- /dev/null +++ b/dojo/tools/openvas/parser_v2/csv_parser.py @@ -0,0 +1,113 @@ +import csv +import io + +from dateutil.parser import parse + +from dojo.models import Endpoint, Finding +from dojo.tools.openvas.parser_v2.common import ( + OpenVASFindingAuxData, + cleanup_openvas_text, + deduplicate, + is_valid_severity, + update_finding, +) + + +def evaluate_bool_value(column_value): + value = column_value.lower() + if value == "true": + return True + if value == "false": + return False + return None + + +class OpenVASCSVParserV2: + def get_findings(self, filename, test): + dupes = {} + content = filename.read() + if isinstance(content, bytes): + content = content.decode("utf-8") + + csv_reader = csv.reader(io.StringIO(content), 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) + + for row in csv_reader: + finding = Finding(test=test, dynamic_finding=True, static_finding=False, severity="Info") + finding.unsaved_vulnerability_ids = [] + finding.unsaved_endpoints = [Endpoint()] + aux_info = OpenVASFindingAuxData() + + for value, name in zip(row, column_names, strict=False): + self.process_column_element(value, name, finding, aux_info) + + update_finding(finding, aux_info) + deduplicate(dupes, finding) + + return list(dupes.values()) + + def process_column_element( + self, + column_value: str, + column_name: str, + finding: Finding, + aux_info: OpenVASFindingAuxData, + ): + # skip columns with empty values + if not column_value: + return + + # process column names + if column_name == "nvt name": + finding.title = column_value + elif column_name == "cweid": + if column_value.isdigit(): + finding.cwe = int(column_value) + elif column_name == "cves": + for cve in column_value.split(","): + finding.unsaved_vulnerability_ids.append(cve) + elif column_name == "nvt oid": + finding.vuln_id_from_tool = column_value + elif column_name == "hostname": + # strip due to https://github.com/greenbone/gvmd/issues/2378 + finding.unsaved_endpoints[0].host = column_value.strip() + elif column_name == "ip": + # fallback to ip if hostname is not aviable + if not finding.unsaved_endpoints[0].host: + # strip due to https://github.com/greenbone/gvmd/issues/2378 + finding.unsaved_endpoints[0].host = column_value.strip() + elif column_name == "port": + if column_value.isdigit(): + finding.unsaved_endpoints[0].port = int(column_value) + elif column_name == "port protocol": + finding.unsaved_endpoints[0].protocol = column_value + elif column_name == "severity": + if is_valid_severity(column_value): + finding.severity = column_value + elif column_name == "cvss": + finding.cvssv3_score = float(column_value) + elif column_name == "summary": + aux_info.summary = column_value + elif column_name == "solution": + finding.mitigation = cleanup_openvas_text(column_value) + elif column_name == "vulnerability insight": + finding.impact = cleanup_openvas_text(column_value) + elif column_name == "specific result": + aux_info.openvas_result = column_value + elif column_name == "qod": + aux_info.qod = column_value + # columns not part of default openvas csv export + elif column_name == "active": + finding.active = evaluate_bool_value(column_value) + elif column_name == "verified": + finding.verified = evaluate_bool_value(column_value) + elif column_name == "falsepositive": + finding.false_p = evaluate_bool_value(column_value) + elif column_name == "duplicate": + finding.duplicate = evaluate_bool_value(column_value) + elif column_name == "timestamp": + finding.date = parse(column_value).date() 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..7c410452c9e --- /dev/null +++ b/dojo/tools/openvas/parser_v2/xml_parser.py @@ -0,0 +1,117 @@ +import contextlib +from xml.dom import NamespaceErr + +from defusedxml import ElementTree + +from dojo.models import Endpoint, Finding +from dojo.tools.openvas.parser_v2.common import ( + OpenVASFindingAuxData, + cleanup_openvas_text, + deduplicate, + is_valid_severity, + update_finding, +) + + +class OpenVASXMLParserV2: + def get_findings(self, filename, test): + dupes = {} + tree = ElementTree.parse(filename) + 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") + + for result in results: + finding = Finding( + test=test, + dynamic_finding=True, + static_finding=False, + severity="Info", + ) + aux_info = OpenVASFindingAuxData() + + finding.unsaved_vulnerability_ids = [] + finding.unsaved_endpoints = [Endpoint()] + + for field in result: + self.process_field_element(field, finding, aux_info) + + update_finding(finding, aux_info) + deduplicate(dupes, finding) + + return list(dupes.values()) + + def parse_nvt_tags(self, text): + 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 + + def process_field_element(self, field, finding: Finding, aux_info: OpenVASFindingAuxData): + if field.tag == "nvt": + # parse general field + finding.vuln_id_from_tool = field.get("oid") + nvt_name = field.find("name").text + if nvt_name: + finding.title = nvt_name + + # parse tags field + tag_field = field.find("tags") + tags = self.parse_nvt_tags(tag_field.text) + summary = tags.get("summary", None) + if summary: + aux_info.summary = summary + + impact = tags.get("impact", None) + if impact: + finding.impact = cleanup_openvas_text(impact) + + # parse cves + refs_node = field.find("refs") + if refs_node is not None: + refs = refs_node.findall(".//ref[@type='cve']") + finding.unsaved_vulnerability_ids = [ref.get("id") for ref in refs] + elif field.tag == "qod": + aux_info.qod = field.find("value").text + + if not field.text: + return + + if field.tag == "name": + finding.title = field.text + elif field.tag == "host": + 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 + finding.unsaved_endpoints[0].host = hostname_field.text.strip() + else: + # strip due to https://github.com/greenbone/gvmd/issues/2378 + finding.unsaved_endpoints[0].host = field.text.strip() + elif field.tag == "port": + port_str, protocol = field.text.split("/") + finding.unsaved_endpoints[0].protocol = protocol + with contextlib.suppress(ValueError): + finding.unsaved_endpoints[0].port = int(port_str) + elif field.tag == "severity": + finding.cvssv3_score = float(field.text) + elif field.tag == "threat": + if is_valid_severity(field.text): + finding.severity = field.text + elif field.tag == "description": + aux_info.openvas_result = field.text.strip() + elif field.tag == "solution": + finding.mitigation = cleanup_openvas_text(field.text) 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..b39505c15a9 100644 --- a/unittests/tools/test_openvas_parser.py +++ b/unittests/tools/test_openvas_parser.py @@ -1,8 +1,115 @@ 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 report Critical findings as High + self.assertEqual(9.8, finding.cvssv3_score) + + # 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_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) + self.assertEqual(f_xml.cvssv3_score, f_csv.cvssv3_score) + # 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)) + + 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 +212,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):