From f00566130db82d1a2ded04de0bded4b9fd90ec9f Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:57:31 +0200 Subject: [PATCH] :sparkler: refactor sonarqube and add JSON parsing for api export (#9734) * :sparkler: refactor sonarqube and add JSON parsing for api export * :construction: start with api json * continue work * update * update * fix * :tada: also advance to support multiple files at once via zip due to pagination * advance unittests * advance documentation * update documentation * update documentation * add tags to distinguish between findings * :pencile: docs * add cve * add cwe * add cvssscore * :lipstick: * :tada: add components * add ghsa * :bug: fix for empty zip file * empty json file * fix documentation * :bug: fix for different message structure * parse hotspots * fix according to review * ruff --- .../en/integrations/parsers/file/sonarqube.md | 26 +- dojo/tools/sonarqube/parser.py | 317 ++---------------- .../tools/sonarqube/sonarqube_restapi_json.py | 226 +++++++++++++ dojo/tools/sonarqube/sonarqube_restapi_zip.py | 12 + dojo/tools/sonarqube/soprasteria_helper.py | 146 ++++++++ dojo/tools/sonarqube/soprasteria_html.py | 83 +++++ dojo/tools/sonarqube/soprasteria_json.py | 69 ++++ unittests/scans/sonarqube/empty_zip.zip | Bin 0 -> 174 bytes .../scans/sonarqube/findings_over_api.json | 177 ++++++++++ .../scans/sonarqube/findings_over_api.zip | Bin 0 -> 2466 bytes .../sonarqube/findings_over_api_empty.json | 14 + .../sonarqube/findings_over_api_hotspots.json | 127 +++++++ unittests/tools/test_sonarqube_parser.py | 84 +++++ 13 files changed, 984 insertions(+), 297 deletions(-) create mode 100644 dojo/tools/sonarqube/sonarqube_restapi_json.py create mode 100644 dojo/tools/sonarqube/sonarqube_restapi_zip.py create mode 100644 dojo/tools/sonarqube/soprasteria_helper.py create mode 100644 dojo/tools/sonarqube/soprasteria_html.py create mode 100644 dojo/tools/sonarqube/soprasteria_json.py create mode 100644 unittests/scans/sonarqube/empty_zip.zip create mode 100644 unittests/scans/sonarqube/findings_over_api.json create mode 100644 unittests/scans/sonarqube/findings_over_api.zip create mode 100644 unittests/scans/sonarqube/findings_over_api_empty.json create mode 100644 unittests/scans/sonarqube/findings_over_api_hotspots.json diff --git a/docs/content/en/integrations/parsers/file/sonarqube.md b/docs/content/en/integrations/parsers/file/sonarqube.md index 4f5e90ed12..4734796dd7 100644 --- a/docs/content/en/integrations/parsers/file/sonarqube.md +++ b/docs/content/en/integrations/parsers/file/sonarqube.md @@ -2,7 +2,26 @@ title: "SonarQube" toc_hide: true --- -## SonarQube Scan (Aggregates findings per cwe, title, description, file\_path.) +# SonarQube Scan +There are two ways to retrieve findings from SonarQube. You can either use the [soprasteria package](https://github.com/soprasteria/sonar-report) or the SonarQube REST API directly. +Both ways (**SonarQube REST API** and **Soprasteria**) are depicted below. + +### Sample Scan Data +Sample SonarQube scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/sonarqube). + +## SonarQube REST API +You can retrieve the JSON directly from SonarQube if you use one of the following REST API endpoint: +- `/api/issues/search?projects=` +- `/api/hotspots/search?projectKey=` + +### JSON +The REST API JSON output can be uploaded to DefectDojo with "SonarQube Scan". + +### ZIP +If you have too many findings in one project, you can implement a small script to handle pagination and put all JSON files in a .zip file. This zip file can also be parsed from DefectDojo with "SonarQube Scan". + +## Soprasteria +### Soprasteria SonarQube Scan (Aggregates findings per cwe, title, description, file\_path.) SonarQube output file can be imported in HTML format or JSON format. JSON format generated by options `--save-report-json` and have same behavior with HTML format. @@ -12,7 +31,7 @@ To generate the report, see Version: \>= 1.1.0 Recommend version for both format \>= 3.1.2 -## SonarQube Scan Detailed (Import all findings from SonarQube html report.) +### Soprasteria SonarQube Scan Detailed (Import all findings from SonarQube html report.) SonarQube output file can be imported in HTML format or JSON format. JSON format generated by options `--save-report-json` and have same behavior with HTML format. @@ -23,5 +42,4 @@ Version: \>= 1.1.0. Recommend version for both format \>= 3.1.2 -### Sample Scan Data -Sample SonarQube scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/sonarqube). + diff --git a/dojo/tools/sonarqube/parser.py b/dojo/tools/sonarqube/parser.py index e7a04c545d..b4491d1164 100644 --- a/dojo/tools/sonarqube/parser.py +++ b/dojo/tools/sonarqube/parser.py @@ -1,12 +1,11 @@ import logging -import re - -from django.utils.html import strip_tags +from dojo.tools.sonarqube.soprasteria_json import SonarQubeSoprasteriaJSON +from dojo.tools.sonarqube.soprasteria_html import SonarQubeSoprasteriaHTML +from dojo.tools.sonarqube.sonarqube_restapi_json import SonarQubeRESTAPIJSON +from dojo.tools.sonarqube.sonarqube_restapi_zip import SonarQubeRESTAPIZIP from lxml import etree +import zipfile import json - -from dojo.models import Finding - logger = logging.getLogger(__name__) @@ -24,301 +23,33 @@ def get_label_for_scan_types(self, scan_type): def get_description_for_scan_types(self, scan_type): if scan_type == "SonarQube Scan": - return "Aggregates findings per cwe, title, description, file_path. SonarQube output file can be imported in HTML format or JSON format. Generate with https://github.com/soprasteria/sonar-report version >= 1.1.0, recommend version >= 3.1.2" + return "Aggregates findings per cwe, title, description, file_path. SonarQube output file can be imported in HTML format or JSON format. You can get the JSON output directly if you use the SonarQube API or generate with https://github.com/soprasteria/sonar-report version >= 1.1.0, recommend version >= 3.1.2" else: return "Import all findings from sonarqube html report or JSON format. SonarQube output file can be imported in HTML format or JSON format. Generate with https://github.com/soprasteria/sonar-report version >= 1.1.0, recommend version >= 3.1.2" - def get_findings(self, filename, test): - if filename.name.strip().lower().endswith(".json"): - json_content = json.load(filename) - return self.get_json_items(json_content, test, self.mode) + def get_findings(self, file, test): + if file.name.endswith(".json"): + json_content = json.load(file) + if json_content.get("date") and json_content.get("projectName") and json_content.get("hotspotKeys"): + return SonarQubeSoprasteriaJSON().get_json_items(json_content, test, self.mode) + elif json_content.get("paging") and json_content.get("components"): + return SonarQubeRESTAPIJSON().get_json_items(json_content, test, self.mode) + else: + return [] + if file.name.endswith(".zip"): + if str(file.__class__) == "": + input_zip = zipfile.ZipFile(file.name, 'r') + else: + input_zip = zipfile.ZipFile(file, 'r') + zipdata = {name: input_zip.read(name) for name in input_zip.namelist()} + return SonarQubeRESTAPIZIP().get_items(zipdata, test, self.mode) else: parser = etree.HTMLParser() - tree = etree.parse(filename, parser) + tree = etree.parse(file, parser) if self.mode not in [None, "detailed"]: raise ValueError( "Internal error: Invalid mode " + self.mode + ". Expected: one of None, 'detailed'" ) - - return self.get_items(tree, test, self.mode) - - def get_json_items(self, json_content, test, mode): - dupes = dict() - rules = json_content["rules"] - issues = json_content["issues"] - for issue in issues: - key = issue["key"] - line = str(issue["line"]) - mitigation = issue["message"] - title = issue["description"] - file_path = issue["component"] - severity = self.convert_sonar_severity(issue["severity"]) - rule_id = issue["rule"] - - if title is None or mitigation is None: - raise ValueError( - "Parser ValueError: can't find a title or a mitigation for vulnerability of name " - + rule_id - ) - - try: - issue_detail = rules[rule_id] - parser = etree.HTMLParser() - html_desc_as_e_tree = etree.fromstring(issue_detail["htmlDesc"], parser) - issue_description = self.get_description(html_desc_as_e_tree) - logger.debug(issue_description) - issue_references = self.get_references( - rule_id, html_desc_as_e_tree - ) - issue_cwe = self.get_cwe(issue_references) - except KeyError: - issue_description = "No description provided" - issue_references = "" - issue_cwe = 0 - - if mode is None: - self.process_result_file_name_aggregated( - test, - dupes, - title, - issue_cwe, - issue_description, - file_path, - line, - severity, - mitigation, - issue_references, - ) - else: - self.process_result_detailed( - test, - dupes, - title, - issue_cwe, - issue_description, - file_path, - line, - severity, - mitigation, - issue_references, - key, - ) - return list(dupes.values()) - - def get_items(self, tree, test, mode): - # Check that there is at least one vulnerability (the vulnerabilities - # table is absent when no vuln are found) - detailTbody = tree.xpath( - "/html/body/div[contains(@class,'detail')]/table/tbody" - ) - dupes = dict() - if len(detailTbody) == 2: - # First is "Detail of the Detected Vulnerabilities" (not present if no vuln) - # Second is "Known Security Rules" - vulnerabilities_table = list(detailTbody[0].iter("tr")) - rules_table = list(detailTbody[1].xpath("tr")) - - # iterate over the rules once to get the information we need - rulesDic = dict() - for rule in rules_table: - rule_properties = list(rule.iter("td")) - rule_name = list(rule_properties[0].iter("a"))[0].text.strip() - rule_details = list(rule_properties[1].iter("details"))[0] - rulesDic[rule_name] = rule_details - - for vuln in vulnerabilities_table: - vuln_properties = list(vuln.iter("td")) - rule_key = list(vuln_properties[0].iter("a"))[0].text - vuln_rule_name = rule_key and rule_key.strip() - vuln_severity = self.convert_sonar_severity( - vuln_properties[1].text and vuln_properties[1].text.strip() - ) - vuln_file_path = vuln_properties[2].text and vuln_properties[2].text.strip() - vuln_line = vuln_properties[3].text and vuln_properties[3].text.strip() - vuln_title = vuln_properties[4].text and vuln_properties[4].text.strip() - vuln_mitigation = vuln_properties[5].text and vuln_properties[5].text.strip() - vuln_key = vuln_properties[6].text and vuln_properties[6].text.strip() - if vuln_title is None or vuln_mitigation is None: - raise ValueError( - "Parser ValueError: can't find a title or a mitigation for vulnerability of name " - + vuln_rule_name - ) - try: - vuln_details = rulesDic[vuln_rule_name] - vuln_description = self.get_description(vuln_details) - vuln_references = self.get_references( - vuln_rule_name, vuln_details - ) - vuln_cwe = self.get_cwe(vuln_references) - except KeyError: - vuln_description = "No description provided" - vuln_references = "" - vuln_cwe = 0 - if mode is None: - self.process_result_file_name_aggregated( - test, - dupes, - vuln_title, - vuln_cwe, - vuln_description, - vuln_file_path, - vuln_line, - vuln_severity, - vuln_mitigation, - vuln_references, - ) - else: - self.process_result_detailed( - test, - dupes, - vuln_title, - vuln_cwe, - vuln_description, - vuln_file_path, - vuln_line, - vuln_severity, - vuln_mitigation, - vuln_references, - vuln_key, - ) - return list(dupes.values()) - - # Process one vuln from the report for "SonarQube Scan detailed" - # Create the finding and add it into the dupes list - def process_result_detailed( - self, - test, - dupes, - vuln_title, - vuln_cwe, - vuln_description, - vuln_file_path, - vuln_line, - vuln_severity, - vuln_mitigation, - vuln_references, - vuln_key, - ): - # vuln_key is the unique id from tool which means that there is - # basically no aggregation except real duplicates - aggregateKeys = "{}{}{}{}{}".format( - vuln_cwe, vuln_title, vuln_description, vuln_file_path, vuln_key - ) - find = Finding( - title=vuln_title, - cwe=int(vuln_cwe), - description=vuln_description, - file_path=vuln_file_path, - line=vuln_line, - test=test, - severity=vuln_severity, - mitigation=vuln_mitigation, - references=vuln_references, - false_p=False, - duplicate=False, - out_of_scope=False, - mitigated=None, - impact="No impact provided", - static_finding=True, - dynamic_finding=False, - unique_id_from_tool=vuln_key, - ) - dupes[aggregateKeys] = find - - # Process one vuln from the report for "SonarQube Scan" - # Create the finding and add it into the dupes list - # For aggregated findings: - # - the description is enriched with each finding line number - # - the mitigation (message) is concatenated with each finding's mitigation value - def process_result_file_name_aggregated( - self, - test, - dupes, - vuln_title, - vuln_cwe, - vuln_description, - vuln_file_path, - vuln_line, - vuln_severity, - vuln_mitigation, - vuln_references, - ): - aggregateKeys = "{}{}{}{}".format( - vuln_cwe, vuln_title, vuln_description, vuln_file_path - ) - descriptionOneOccurence = "Line: {}".format(vuln_line) - if aggregateKeys not in dupes: - find = Finding( - title=vuln_title, - cwe=int(vuln_cwe), - description=vuln_description - + "\n\n-----\nOccurences:\n" - + descriptionOneOccurence, - file_path=vuln_file_path, - # No line number because we have aggregated different - # vulnerabilities that may have different line numbers - test=test, - severity=vuln_severity, - mitigation=vuln_mitigation, - references=vuln_references, - false_p=False, - duplicate=False, - out_of_scope=False, - mitigated=None, - impact="No impact provided", - static_finding=True, - dynamic_finding=False, - nb_occurences=1, - ) - dupes[aggregateKeys] = find - else: - # We have already created a finding for this aggregate: updates the - # description, nb_occurences and mitigation (message field in the - # report which may vary for each vuln) - find = dupes[aggregateKeys] - find.description = "{}\n{}".format( - find.description, descriptionOneOccurence - ) - find.mitigation = "{}\n______\n{}".format( - find.mitigation, vuln_mitigation - ) - find.nb_occurences = find.nb_occurences + 1 - - def convert_sonar_severity(self, sonar_severity): - sev = sonar_severity.lower() - if sev == "blocker": - return "Critical" - elif sev == "critical": - return "High" - elif sev == "major": - return "Medium" - elif sev == "minor": - return "Low" - else: - return "Info" - - def get_description(self, vuln_details): - rule_description = etree.tostring( - vuln_details, pretty_print=True - ).decode("utf-8", errors="replace") - rule_description = rule_description.split("

See", 1)[0] - rule_description = (str(rule_description)).replace("

", "**") - rule_description = (str(rule_description)).replace("

", "**") - rule_description = strip_tags(rule_description).strip() - return rule_description - - def get_references(self, rule_name, vuln_details): - rule_references = rule_name - for a in vuln_details.iter("a"): - rule_references += "\n" + str(a.text) - return rule_references - - def get_cwe(self, vuln_references): - # Match only the first CWE! - cweSearch = re.search("CWE-([0-9]*)", vuln_references, re.IGNORECASE) - if cweSearch: - return cweSearch.group(1) - else: - return 0 + return SonarQubeSoprasteriaHTML().get_items(tree, test, self.mode) diff --git a/dojo/tools/sonarqube/sonarqube_restapi_json.py b/dojo/tools/sonarqube/sonarqube_restapi_json.py new file mode 100644 index 0000000000..281e574ac6 --- /dev/null +++ b/dojo/tools/sonarqube/sonarqube_restapi_json.py @@ -0,0 +1,226 @@ +from dojo.models import Finding +import re + + +class SonarQubeRESTAPIJSON(object): + def get_json_items(self, json_content, test, mode): + items = [] + if json_content.get("issues"): + for issue in json_content.get("issues"): + if issue.get("type") == "BUG": + key = issue.get("key") + rule = issue.get("rule") + component = issue.get("component") + project = issue.get("project") + line = str(issue.get("line")) + textRange = str(issue.get("textRange")) + flows = str(issue.get("flows")) + status = issue.get("status") + message = issue.get("message") + tags = str(issue.get("tags")) + type = issue.get("type") + scope = issue.get("scope") + quickFixAvailable = str(issue.get("quickFixAvailable")) + codeVariants = str(issue.get("codeVariants")) + description = "" + description += "**key:** " + key + "\n" + description += "**rule:** " + rule + "\n" + description += "**component:** " + component + "\n" + description += "**project:** " + project + "\n" + description += "**line:** " + line + "\n" + description += "**textRange:** " + textRange + "\n" + description += "**flows:** " + flows + "\n" + description += "**status:** " + status + "\n" + description += "**message:** " + message + "\n" + description += "**tags:** " + tags + "\n" + description += "**type:** " + type + "\n" + description += "**scope:** " + scope + "\n" + description += self.returncomponent(json_content, component) + item = Finding( + title=rule + "_" + key, + description=description, + test=test, + severity=self.severitytranslator(issue.get("severity")), + static_finding=True, + dynamic_finding=False, + tags=["bug"], + ) + elif issue.get("type") == "VULNERABILITY": + key = issue.get("key") + rule = issue.get("rule") + component = issue.get("component") + project = issue.get("project") + flows = str(issue.get("flows")) + status = issue.get("status") + message = issue.get("message") + cve = None + if "Reference: CVE" in message: + cve_pattern = r'Reference: CVE-\d{4}-\d{4,7}' + cves = re.findall(cve_pattern, message) + if cves: + cve = cves[0].split("Reference: ")[1] + elif "References: CVE" in message: + cve_pattern = r'References: CVE-\d{4}-\d{4,7}' + cves = re.findall(cve_pattern, message) + if cves: + cve = cves[0].split("References: ")[1] + elif "Reference: GHSA" in message and cve is None: + cve_pattern = r'Reference: GHSA-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}' + cves = re.findall(cve_pattern, message) + if cves: + cve = cves[0].split("Reference: ")[1] + elif "References: GHSA" in message and cve is None: + cve_pattern = r'References: GHSA-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}-[23456789cfghjmpqrvwx]{4}' + cves = re.findall(cve_pattern, message) + if cves: + cve = cves[0].split("References: ")[1] + cwe = None + if "Category: CWE-" in message: + cwe_pattern = r'Category: CWE-\d{1,5}' + cwes = re.findall(cwe_pattern, message) + if cwes: + cwe = cwes[0].split("Category: CWE-")[1] + cvss = None + if "CVSS Score: " in message: + cvss_pattern = r'CVSS Score: \d{1}.\d{1}' + cvsss = re.findall(cvss_pattern, message) + if cvsss: + cvss = cvsss[0].split("CVSS Score: ")[1] + component_name = None + component_version = None + if "Filename: " in message and " | " in message: + component_pattern = r'Filename: .* \| ' + comp = re.findall(component_pattern, message) + if comp: + component_result = comp[0].split("Filename: ")[1].split(" | ")[0] + component_name = component_result.split(":")[0] + try: + component_version = component_result.split(":")[1] + except IndexError: + component_version = None + scope = issue.get("scope") + quickFixAvailable = str(issue.get("quickFixAvailable")) + codeVariants = str(issue.get("codeVariants")) + tags = str(issue.get("tags")) + description = "" + description += "**key:** " + key + "\n" + description += "**rule:** " + rule + "\n" + description += "**component:** " + component + "\n" + description += "**project:** " + project + "\n" + description += "**flows:** " + flows + "\n" + description += "**status:** " + status + "\n" + description += "**message:** " + message + "\n" + description += "**scope:** " + scope + "\n" + description += "**quickFixAvailable:** " + quickFixAvailable + "\n" + description += "**codeVariants:** " + codeVariants + "\n" + description += "**tags:** " + tags + "\n" + description += self.returncomponent(json_content, component) + item = Finding( + title=rule + "_" + key, + description=description, + test=test, + severity=self.severitytranslator(issue.get("severity")), + static_finding=True, + dynamic_finding=False, + component_name=component_name, + component_version=component_version, + cve=cve, + cwe=cwe, + cvssv3_score=cvss, + tags=["vulnerability"], + ) + elif issue.get("type") == "CODE_SMELL": + key = issue.get("key") + rule = issue.get("rule") + component = issue.get("component") + project = issue.get("project") + line = str(issue.get("line")) + textRange = str(issue.get("textRange")) + flows = str(issue.get("flows")) + status = issue.get("status") + message = issue.get("message") + tags = str(issue.get("tags")) + scope = issue.get("scope") + quickFixAvailable = str(issue.get("quickFixAvailable")) + codeVariants = str(issue.get("codeVariants")) + description = "" + description += "**rule:** " + rule + "\n" + description += "**component:** " + component + "\n" + description += "**project:** " + project + "\n" + description += "**line:** " + line + "\n" + description += "**textRange:** " + textRange + "\n" + description += "**flows:** " + flows + "\n" + description += "**status:** " + status + "\n" + description += "**message:** " + message + "\n" + description += "**tags:** " + tags + "\n" + description += "**scope:** " + scope + "\n" + description += "**quickFixAvailable:** " + quickFixAvailable + "\n" + description += "**codeVariants:** " + codeVariants + "\n" + description += self.returncomponent(json_content, component) + item = Finding( + title=rule + "_" + key, + description=description, + test=test, + severity=self.severitytranslator(issue.get("severity")), + static_finding=True, + dynamic_finding=False, + tags=["code_smell"], + ) + items.append(item) + if json_content.get("hotspots"): + for hotspot in json_content.get("hotspots"): + key = hotspot.get("key") + component = hotspot.get("component") + project = hotspot.get("project") + securityCategory = hotspot.get("securityCategory") + status = hotspot.get("status") + line = str(hotspot.get("line")) + message = hotspot.get("message") + textRange = str(hotspot.get("textRange")) + flows = str(hotspot.get("flows")) + ruleKey = hotspot.get("ruleKey") + messageFormattings = str(hotspot.get("messageFormattings")) + description = "" + description += "**key:** " + key + "\n" + description += "**component:** " + component + "\n" + description += "**project:** " + project + "\n" + description += "**securityCategory:** " + securityCategory + "\n" + description += "**status:** " + status + "\n" + description += "**line:** " + line + "\n" + description += "**message:** " + message + "\n" + description += "**textRange:** " + textRange + "\n" + description += "**flows:** " + flows + "\n" + description += "**ruleKey:** " + ruleKey + "\n" + description += "**messageFormattings:** " + messageFormattings + "\n" + description += self.returncomponent(json_content, component) + item = Finding( + title=ruleKey + "_" + key, + description=description, + test=test, + severity=self.severitytranslator(hotspot.get("vulnerabilityProbability")), + static_finding=True, + dynamic_finding=False, + tags=["hotspot"], + ) + items.append(item) + return items + + def severitytranslator(self, severity): + if severity == "BLOCKER": + return "High" + elif severity == "MAJOR": + return "Medium" + elif severity == "MINOR": + return "Low" + else: + return severity.lower().capitalize() + + def returncomponent(self, json_content, key): + components = json_content.get("components") + description = "" + for comp in components: + if comp.get("key") == key: + componentkeys = comp.keys() + for ck in componentkeys: + description += "**Componentkey " + ck + "**: " + str(comp.get(ck)) + "\n" + return description diff --git a/dojo/tools/sonarqube/sonarqube_restapi_zip.py b/dojo/tools/sonarqube/sonarqube_restapi_zip.py new file mode 100644 index 0000000000..0242939878 --- /dev/null +++ b/dojo/tools/sonarqube/sonarqube_restapi_zip.py @@ -0,0 +1,12 @@ +from dojo.tools.sonarqube.sonarqube_restapi_json import SonarQubeRESTAPIJSON +import json + + +class SonarQubeRESTAPIZIP(object): + def get_items(self, files, test, mode): + total_findings_per_file = list() + for dictkey in files.keys(): + if dictkey.endswith(".json"): + json_content = json.loads(files[dictkey].decode('ascii')) + total_findings_per_file += SonarQubeRESTAPIJSON().get_json_items(json_content, test, mode) + return total_findings_per_file diff --git a/dojo/tools/sonarqube/soprasteria_helper.py b/dojo/tools/sonarqube/soprasteria_helper.py new file mode 100644 index 0000000000..26f9662b8d --- /dev/null +++ b/dojo/tools/sonarqube/soprasteria_helper.py @@ -0,0 +1,146 @@ +import logging +import re +from django.utils.html import strip_tags +from lxml import etree +from dojo.models import Finding +logger = logging.getLogger(__name__) + + +class SonarQubeSoprasteriaHelper(object): + def convert_sonar_severity(self, sonar_severity): + sev = sonar_severity.lower() + if sev == "blocker": + return "Critical" + elif sev == "critical": + return "High" + elif sev == "major": + return "Medium" + elif sev == "minor": + return "Low" + else: + return "Info" + + def get_description(self, vuln_details): + rule_description = etree.tostring( + vuln_details, pretty_print=True + ).decode("utf-8", errors="replace") + rule_description = rule_description.split("

See", 1)[0] + rule_description = (str(rule_description)).replace("

", "**") + rule_description = (str(rule_description)).replace("

", "**") + rule_description = strip_tags(rule_description).strip() + return rule_description + + def get_references(self, rule_name, vuln_details): + rule_references = rule_name + for a in vuln_details.iter("a"): + rule_references += "\n" + str(a.text) + return rule_references + + def get_cwe(self, vuln_references): + # Match only the first CWE! + cweSearch = re.search("CWE-([0-9]*)", vuln_references, re.IGNORECASE) + if cweSearch: + return cweSearch.group(1) + else: + return 0 + + # Process one vuln from the report for "SonarQube Scan" + # Create the finding and add it into the dupes list + # For aggregated findings: + # - the description is enriched with each finding line number + # - the mitigation (message) is concatenated with each finding's mitigation value + def process_result_file_name_aggregated( + self, + test, + dupes, + vuln_title, + vuln_cwe, + vuln_description, + vuln_file_path, + vuln_line, + vuln_severity, + vuln_mitigation, + vuln_references, + ): + aggregateKeys = "{}{}{}{}".format( + vuln_cwe, vuln_title, vuln_description, vuln_file_path + ) + descriptionOneOccurence = "Line: {}".format(vuln_line) + if aggregateKeys not in dupes: + find = Finding( + title=vuln_title, + cwe=int(vuln_cwe), + description=vuln_description + + "\n\n-----\nOccurences:\n" + + descriptionOneOccurence, + file_path=vuln_file_path, + # No line number because we have aggregated different + # vulnerabilities that may have different line numbers + test=test, + severity=vuln_severity, + mitigation=vuln_mitigation, + references=vuln_references, + false_p=False, + duplicate=False, + out_of_scope=False, + mitigated=None, + impact="No impact provided", + static_finding=True, + dynamic_finding=False, + nb_occurences=1, + ) + dupes[aggregateKeys] = find + else: + # We have already created a finding for this aggregate: updates the + # description, nb_occurences and mitigation (message field in the + # report which may vary for each vuln) + find = dupes[aggregateKeys] + find.description = "{}\n{}".format( + find.description, descriptionOneOccurence + ) + find.mitigation = "{}\n______\n{}".format( + find.mitigation, vuln_mitigation + ) + find.nb_occurences = find.nb_occurences + 1 + + # Process one vuln from the report for "SonarQube Scan detailed" + # Create the finding and add it into the dupes list + def process_result_detailed( + self, + test, + dupes, + vuln_title, + vuln_cwe, + vuln_description, + vuln_file_path, + vuln_line, + vuln_severity, + vuln_mitigation, + vuln_references, + vuln_key, + ): + # vuln_key is the unique id from tool which means that there is + # basically no aggregation except real duplicates + aggregateKeys = "{}{}{}{}{}".format( + vuln_cwe, vuln_title, vuln_description, vuln_file_path, vuln_key + ) + find = Finding( + title=vuln_title, + cwe=int(vuln_cwe), + description=vuln_description, + file_path=vuln_file_path, + line=vuln_line, + test=test, + severity=vuln_severity, + mitigation=vuln_mitigation, + references=vuln_references, + false_p=False, + duplicate=False, + out_of_scope=False, + mitigated=None, + impact="No impact provided", + static_finding=True, + dynamic_finding=False, + unique_id_from_tool=vuln_key, + ) + dupes[aggregateKeys] = find diff --git a/dojo/tools/sonarqube/soprasteria_html.py b/dojo/tools/sonarqube/soprasteria_html.py new file mode 100644 index 0000000000..090fa59ec1 --- /dev/null +++ b/dojo/tools/sonarqube/soprasteria_html.py @@ -0,0 +1,83 @@ +import logging +from dojo.tools.sonarqube.soprasteria_helper import SonarQubeSoprasteriaHelper +logger = logging.getLogger(__name__) + + +class SonarQubeSoprasteriaHTML(object): + def get_items(self, tree, test, mode): + # Check that there is at least one vulnerability (the vulnerabilities + # table is absent when no vuln are found) + detailTbody = tree.xpath( + "/html/body/div[contains(@class,'detail')]/table/tbody" + ) + dupes = dict() + if len(detailTbody) == 2: + # First is "Detail of the Detected Vulnerabilities" (not present if no vuln) + # Second is "Known Security Rules" + vulnerabilities_table = list(detailTbody[0].iter("tr")) + rules_table = list(detailTbody[1].xpath("tr")) + + # iterate over the rules once to get the information we need + rulesDic = dict() + for rule in rules_table: + rule_properties = list(rule.iter("td")) + rule_name = list(rule_properties[0].iter("a"))[0].text.strip() + rule_details = list(rule_properties[1].iter("details"))[0] + rulesDic[rule_name] = rule_details + + for vuln in vulnerabilities_table: + vuln_properties = list(vuln.iter("td")) + rule_key = list(vuln_properties[0].iter("a"))[0].text + vuln_rule_name = rule_key and rule_key.strip() + vuln_severity = SonarQubeSoprasteriaHelper().convert_sonar_severity( + vuln_properties[1].text and vuln_properties[1].text.strip() + ) + vuln_file_path = vuln_properties[2].text and vuln_properties[2].text.strip() + vuln_line = vuln_properties[3].text and vuln_properties[3].text.strip() + vuln_title = vuln_properties[4].text and vuln_properties[4].text.strip() + vuln_mitigation = vuln_properties[5].text and vuln_properties[5].text.strip() + vuln_key = vuln_properties[6].text and vuln_properties[6].text.strip() + if vuln_title is None or vuln_mitigation is None: + raise ValueError( + "Parser ValueError: can't find a title or a mitigation for vulnerability of name " + + vuln_rule_name + ) + try: + vuln_details = rulesDic[vuln_rule_name] + vuln_description = SonarQubeSoprasteriaHelper().get_description(vuln_details) + vuln_references = SonarQubeSoprasteriaHelper().get_references( + vuln_rule_name, vuln_details + ) + vuln_cwe = SonarQubeSoprasteriaHelper().get_cwe(vuln_references) + except KeyError: + vuln_description = "No description provided" + vuln_references = "" + vuln_cwe = 0 + if mode is None: + SonarQubeSoprasteriaHelper().process_result_file_name_aggregated( + test, + dupes, + vuln_title, + vuln_cwe, + vuln_description, + vuln_file_path, + vuln_line, + vuln_severity, + vuln_mitigation, + vuln_references, + ) + else: + SonarQubeSoprasteriaHelper().process_result_detailed( + test, + dupes, + vuln_title, + vuln_cwe, + vuln_description, + vuln_file_path, + vuln_line, + vuln_severity, + vuln_mitigation, + vuln_references, + vuln_key, + ) + return list(dupes.values()) diff --git a/dojo/tools/sonarqube/soprasteria_json.py b/dojo/tools/sonarqube/soprasteria_json.py new file mode 100644 index 0000000000..b7e85011ad --- /dev/null +++ b/dojo/tools/sonarqube/soprasteria_json.py @@ -0,0 +1,69 @@ +import logging +from dojo.tools.sonarqube.soprasteria_helper import SonarQubeSoprasteriaHelper +from lxml import etree +logger = logging.getLogger(__name__) + + +class SonarQubeSoprasteriaJSON(object): + def get_json_items(self, json_content, test, mode): + dupes = dict() + rules = json_content["rules"] + issues = json_content["issues"] + for issue in issues: + key = issue["key"] + line = str(issue["line"]) + mitigation = issue["message"] + title = issue["description"] + file_path = issue["component"] + severity = SonarQubeSoprasteriaHelper().convert_sonar_severity(issue["severity"]) + rule_id = issue["rule"] + + if title is None or mitigation is None: + raise ValueError( + "Parser ValueError: can't find a title or a mitigation for vulnerability of name " + + rule_id + ) + + try: + issue_detail = rules[rule_id] + parser = etree.HTMLParser() + html_desc_as_e_tree = etree.fromstring(issue_detail["htmlDesc"], parser) + issue_description = SonarQubeSoprasteriaHelper().get_description(html_desc_as_e_tree) + logger.debug(issue_description) + issue_references = SonarQubeSoprasteriaHelper().get_references( + rule_id, html_desc_as_e_tree + ) + issue_cwe = SonarQubeSoprasteriaHelper().get_cwe(issue_references) + except KeyError: + issue_description = "No description provided" + issue_references = "" + issue_cwe = 0 + + if mode is None: + SonarQubeSoprasteriaHelper().process_result_file_name_aggregated( + test, + dupes, + title, + issue_cwe, + issue_description, + file_path, + line, + severity, + mitigation, + issue_references, + ) + else: + SonarQubeSoprasteriaHelper().process_result_detailed( + test, + dupes, + title, + issue_cwe, + issue_description, + file_path, + line, + severity, + mitigation, + issue_references, + key, + ) + return list(dupes.values()) diff --git a/unittests/scans/sonarqube/empty_zip.zip b/unittests/scans/sonarqube/empty_zip.zip new file mode 100644 index 0000000000000000000000000000000000000000..c82ad8319bb5ce8f89a008df7cf53236dfdc10ea GIT binary patch literal 174 zcmWIWW@h1H0D+5Z3M0S_D8a)Z!;qR=P*RzepOcbWq#qi>$-sOvOKb7~VR9n5d?*LQKUlk(CXkmJtYpfV3Nk!vFv*awSs$ literal 0 HcmV?d00001 diff --git a/unittests/scans/sonarqube/findings_over_api.json b/unittests/scans/sonarqube/findings_over_api.json new file mode 100644 index 0000000000..d39671bcda --- /dev/null +++ b/unittests/scans/sonarqube/findings_over_api.json @@ -0,0 +1,177 @@ +{ + "total": 42, + "p": 1, + "ps": 100, + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 3 + }, + "effortTotal": 87, + "issues": [ + { + "key": "fjioefjwoefijo", + "rule": "OWASP:UsingComponentWithKnownVulnerability", + "severity": "MAJOR", + "component": "testapplication", + "project": "testapplication", + "flows": [], + "status": "OPEN", + "message": "Filename: package:1.1.2 | Reference: CVE-2024-2529 | CVSS Score: 6.4 | Category: CWE-120 | Versions of the package vulndescription .", + "author": "", + "tags": [ + "cve", + "cwe", + "cwe-937", + "owasp-a9", + "vulnerability" + ], + "creationDate": "2023-10-16T15:05:35+0000", + "updateDate": "2023-11-03T08:00:46+0000", + "type": "VULNERABILITY", + "scope": "MAIN", + "quickFixAvailable": false, + "messageFormattings": [], + "codeVariants": [] + }, + { + "key": "asdfwfewfwefewf", + "rule": "Web:TableWithoutCaptionCheck", + "severity": "MINOR", + "component": "testapplication:src/app/pages/fjiowefjewio/fjwieof/fjiwoe/details.html", + "project": "testapplication", + "line": 59, + "hash": "1de6211e802eff1bb25bff705a0a4cf5", + "textRange": { + "startLine": 59, + "endLine": 59, + "startOffset": 6, + "endOffset": 113 + }, + "flows": [], + "status": "OPEN", + "message": "Add a description to this table.", + "effort": "5min", + "debt": "5min", + "author": "name.name@name.de", + "tags": [ + "accessibility", + "wcag2-a" + ], + "creationDate": "2023-07-25T13:16:32+0000", + "updateDate": "2023-08-18T08:33:52+0000", + "type": "BUG", + "scope": "MAIN", + "quickFixAvailable": false, + "messageFormattings": [], + "codeVariants": [] + }, + { + "key": "fjoiewfjoweifjoihugu-", + "rule": "typescript:S1533", + "severity": "MINOR", + "component": "testapplication:src/app/services/fjweiofwefjiofiwofjwof.fjewoi", + "project": "testapplication", + "line": 15, + "hash": "fw0fu90weu90u3904u1094u1409", + "textRange": { + "startLine": 15, + "endLine": 15, + "startOffset": 49, + "endOffset": 56 + }, + "flows": [], + "status": "OPEN", + "message": "Replace this \"Boolean\" wrapper object with primitive type \"boolean\".", + "effort": "1min", + "debt": "1min", + "author": "name.name@name.de", + "tags": [ + "pitfall" + ], + "creationDate": "2024-01-29T13:50:11+0000", + "updateDate": "2024-01-29T13:50:11+0000", + "type": "CODE_SMELL", + "scope": "MAIN", + "quickFixAvailable": true, + "messageFormattings": [], + "codeVariants": [] + }, + { + "key": "fjioefjwoefisdfjo", + "rule": "OWASP:UsingComponentWithKnownVulnerability", + "severity": "MAJOR", + "component": "testapplication", + "project": "testapplication", + "flows": [], + "status": "OPEN", + "message": "Filename: package:1.1.2 | Reference: GHSA-frr2-c345-p7c2 | CVSS Score: 6.4 | Category: CWE-120 | Versions of the package vulndescription .", + "author": "", + "tags": [ + "cve", + "cwe", + "cwe-937", + "owasp-a9", + "vulnerability" + ], + "creationDate": "2023-10-16T15:05:35+0000", + "updateDate": "2023-11-03T08:00:46+0000", + "type": "VULNERABILITY", + "scope": "MAIN", + "quickFixAvailable": false, + "messageFormattings": [], + "codeVariants": [] + }, + { + "key": "fjioefjwoefisdfjo", + "rule": "OWASP:UsingComponentWithKnownVulnerability", + "severity": "CRITICAL", + "component": "testapplication", + "project": "testapplication", + "flows": [], + "status": "OPEN", + "message": "Filename: nimbus-jose-jwt-9.24.4.jar | Highest CVSS Score: 7.5 | Amount of CVSS: 1 | References: CVE-2023-52428 (7.5)", + "author": "", + "tags": [ + "cve", + "cwe", + "cwe-937", + "owasp-a9", + "vulnerability" + ], + "creationDate": "2023-10-16T15:05:35+0000", + "updateDate": "2023-11-03T08:00:46+0000", + "type": "VULNERABILITY", + "scope": "MAIN", + "quickFixAvailable": false, + "messageFormattings": [], + "codeVariants": [] + } + ], + "components": [ + { + "key": "testapplication", + "enabled": true, + "qualifier": "TRK", + "name": "testapplication", + "longName": "testapplication" + }, + { + "key": "testapplication:src/app/pages/fjiowefjewio/fjwieof/fjiwoe/details.html", + "enabled": true, + "qualifier": "FIL", + "name": "details.html", + "longName": "src/app/pages/fjiowefjewio/fjwieof/fjiwoe/details.html", + "path": "src/app/pages/fjiowefjewio/fjwieof/fjiwoe/details.html" + }, + { + "key": "testapplication:src/app/services/fjweiofwefjiofiwofjwof.fjewoi", + "enabled": true, + "qualifier": "FIL", + "name": "fjweiofwefjiofiwofjwof.fjewoi", + "longName": "src/app/services/fjweiofwefjiofiwofjwof.fjewoi", + "path": "src/app/services/fjweiofwefjiofiwofjwof.fjewoi" + } + ], + "facets": [] + } \ No newline at end of file diff --git a/unittests/scans/sonarqube/findings_over_api.zip b/unittests/scans/sonarqube/findings_over_api.zip new file mode 100644 index 0000000000000000000000000000000000000000..92a82faf5acc359d3ce5c6731780abc1870f92fa GIT binary patch literal 2466 zcmajhc{CJi9|!QkSX0zx?A_3vv1F36-3G-ZW$bi^Q6uZMGb4sugc!2RlETQEb#lpw z>>|B}YK$dDS!Sl}L!yLtwA{D1-p+fT^E=P)`R99{zdq+{Zp;M~0RRAe09=eWQVHB< zy$c8cC<_4qAb>Q$)!oP0-Ny}!_6>BwpfCElLp`v*J~r0;050b~mWz4QvdbmhE&vDc z+2Kwd1buyC!UfrH~#p-iwm7xc$tf}Sd z@)sq$)ULoOjddn65r4}T3UpX!OlRb_dR*~Ld@pd4yVR)nNQH;CTE?4Lgo%Sosp}Mm zUyMK7pr?~>*(99i*FPAg=B89_>vaBibooSFaW?rtl-xXlbr-DLi}$p0qts2+$G354{=!N=WnOR#*=QYZkF-}m?y0_+ z8~bX@5_XJnA}JS z5(~>KALihw_Vh7=gtYiD2#nas=?th6f^_}i?st8~ilR19Dl1u#r4K!5is49=l9daU zjOYTE&cOTg*CIi&!`Y8L)0}@P26rVyn+GXGVJ97q<`<+)C1~l%@0YIt`T17%E#>`n z2r7PA=_Pzjn@S^}N8sZ+=C8Liqp{rJj~`GaV!MkL;%43}&GF& zpk6Ds`voreXm!Sbupx?0B)10KEt3dU8P^{kl0gbPq&XHS7AOe~4_A*ISAe(;uk#G7 zy3>Mk3=|+t4IRz2#;Qf{kP3yC`px-mvIgO7IKnIid~qByqE$oM>tkA54-6%QRQ+XN zCR&KyE8^gE;CX=L!EE_{Mh&?Q+ z^FWJ5MWp>`>-H%+HAxU7lp4c*&tVNk*MGu?{8;HoS7hw5G)_e5R!U3Nrx!YcYi|~4 zXULFGE3e43Qe;|N(zk(2u}_WQ0jgB>+vcP0;$=FMRt*c$N#Z2>f~STp(ZQFu zgv|e;<-Uavvel?95W=QKpG}L{|Dr|hKelwWEW32D=LcJiv6EWDR~J{5?d>7p)Pyrj z?)(OI!?Hah!j`DxC{(*ocuX}cBmM@N(PH*5HZFqG{>VLDGx&j-!J*|2$gZ%jwj{D` z;oY(2YBDwXlPv~EF6`Liw>Gz9OAFf;6}Bzi^ujN;=-7U;g}!aqKpceARK^Ov*ph15 z%?&O78}rsH%{7HT4|9n!7q&taH5^Z^fp-bdO5K5=owB++4qcaZk>AUI1^w=vDt_Rx zUvyj_-DZcEm7nl3{Q4z*^)oMn&4)hmLbS+<`^rn)7hY=qKQB$EDYw7pCG;O&G`{0S z;#*$K{)?9zUwA?Lm4Cy_03>%YguuV`B;4&x3}Y5L0A9G7{kyJgXl3(6{^E^x9FErH zwRX!b$H#4n`J!52x_vT##I}4U2>~QAC#$sGUu!xbMNAlwk1uzsl$aI&nR ztrD_Atf6Rz^(O?5h=MVbu_Oi+tLi(d&=8)Yr|K^e;9Q(i4^r+s;mO%H4-K$u>dT{e z1PT>(v#E)na-!%DRLe>`-C|R-XNQ`L&3W(xl*%#&3&jGp zH=DLa018B;c1zj>w{|6xFPwyFw}G|ts?27aXQeq|+V|fmaxQ+h2B+7!wYhDg&9=tb z>^ID80#?{iwWlkqX?I{v<)br88#q!(RpW|>*Ovb}bsb2G2!KXJ_zIrFL~3ox4l*>D zHe3%ye4}rLpz;{n6Sk4lSChWD^Q-!EoO(Z!U9~H1iV<$a&QhG|#ois_Q*1gw2*kj%81@HfKX^#A>A#x*gImnN|m*4>B z?Koe*l{zB$W3$Mx51>m`=Z+>0BlLCc3@+WJq& ztBwh>PIt+*af?u;-Zp3CAo$l(6sB18JvE!OnJGPiJ)_Lvl16VrBUr_EnX4IfGxwoW zG0AOpm9~C<1cpoZesiUR%#y3*?Yoz*nW(aw)!j`nH|F5n&-JrXBhD^6000Mj3d47{ wpH&?0&lTr8g1*iEQMGTgQEcme$o^^^JAB8uAHunLcD}^PJ`dP8g>UEeZ|j$oUjP6A literal 0 HcmV?d00001 diff --git a/unittests/scans/sonarqube/findings_over_api_empty.json b/unittests/scans/sonarqube/findings_over_api_empty.json new file mode 100644 index 0000000000..07afd179d8 --- /dev/null +++ b/unittests/scans/sonarqube/findings_over_api_empty.json @@ -0,0 +1,14 @@ +{ + "total": 0, + "p": 1, + "ps": 100, + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 0 + }, + "effortTotal": 0, + "issues": [], + "components": [], + "facets": [] + } \ No newline at end of file diff --git a/unittests/scans/sonarqube/findings_over_api_hotspots.json b/unittests/scans/sonarqube/findings_over_api_hotspots.json new file mode 100644 index 0000000000..bdf303e7f9 --- /dev/null +++ b/unittests/scans/sonarqube/findings_over_api_hotspots.json @@ -0,0 +1,127 @@ +{ + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 4 + }, + "hotspots": [ + { + "key": "fwafewef", + "component": "nana-ofewfe:src/app/component.asdf", + "project": "nana-ofewfe", + "securityCategory": "xss", + "vulnerabilityProbability": "HIGH", + "status": "TO_REVIEW", + "line": 47, + "message": "Make sure disabling asdfbuilt-in sanitization is safe here.", + "author": "", + "creationDate": "2024-02-13T09:15:26+0000", + "updateDate": "2024-02-13T09:15:26+0000", + "textRange": { + "startLine": 47, + "endLine": 47, + "startOffset": 26, + "endOffset": 56 + }, + "flows": [], + "ruleKey": "typescript:7777", + "messageFormattings": [] + }, + { + "key": "cyxcvyxcvyxv", + "component": "keyfoot.htmlfjoes", + "project": "nana-ofewfe", + "securityCategory": "others", + "vulnerabilityProbability": "LOW", + "status": "TO_REVIEW", + "line": 5, + "message": "Make sure nasdfre.", + "author": "", + "creationDate": "2023-07-27T08:53:15+0000", + "updateDate": "2023-08-15T06:40:11+0000", + "textRange": { + "startLine": 5, + "endLine": 5, + "startOffset": 6, + "endOffset": 75 + }, + "flows": [], + "ruleKey": "Web:1222", + "messageFormattings": [] + }, + { + "key": "werrwerwerwer", + "component": "keyfoot.htmlfjoes", + "project": "nana-ofewfe", + "securityCategory": "others", + "vulnerabilityProbability": "LOW", + "status": "TO_REVIEW", + "line": 8, + "message": "Make sure no123123.", + "author": "", + "creationDate": "2023-07-27T08:53:15+0000", + "updateDate": "2023-08-15T06:40:11+0000", + "textRange": { + "startLine": 8, + "endLine": 8, + "startOffset": 6, + "endOffset": 77 + }, + "flows": [], + "ruleKey": "Web:9876", + "messageFormattings": [] + }, + { + "key": "jztjztjtzj", + "component": "nana-ofewfe:src/path/to//1231235/ccommp.html", + "project": "nana-ofewfe", + "securityCategory": "others", + "vulnerabilityProbability": "LOW", + "status": "TO_REVIEW", + "line": 3, + "message": "Make surgrgergrege.", + "author": "", + "creationDate": "2023-07-27T08:53:15+0000", + "updateDate": "2023-08-15T06:40:11+0000", + "textRange": { + "startLine": 3, + "endLine": 3, + "startOffset": 4, + "endOffset": 105 + }, + "flows": [], + "ruleKey": "Web:12345", + "messageFormattings": [] + } + ], + "components": [ + { + "key": "nana-ofewfe:src/path/to//1231235/ccommp.html", + "qualifier": "FIL", + "name": "ccommp.html", + "longName": "src/path/to//1231235/ccommp.html", + "path": "src/path/to//1231235/ccommp.html" + }, + { + "key": "nana-ofewfe", + "qualifier": "TRK", + "name": "nana-ofewfe", + "longName": "nana-ofewfe" + }, + { + "key": "keyfoot.htmlfjoes", + "qualifier": "FIL", + "name": "anonymous-footer.component.html", + "longName": "src/app/2389479284379021-footer.component.html", + "path": "src/app/2389479284379021-footer.component.html" + }, + { + "key": "nana-ofewfe:src/app/component.asdf", + "qualifier": "FIL", + "name": "component.asdf", + "longName": "src/app/component.asdf", + "path": "src/app/component.asdf" + } + ] +} + \ No newline at end of file diff --git a/unittests/tools/test_sonarqube_parser.py b/unittests/tools/test_sonarqube_parser.py index 04e048a633..3172407f82 100644 --- a/unittests/tools/test_sonarqube_parser.py +++ b/unittests/tools/test_sonarqube_parser.py @@ -547,3 +547,87 @@ def test_detailed_parse_json_file_with_multiple_vulnerabilities_has_multiple_fin # common verifications # (there is no aggregation to be done here) self.assertEqual(6, len(findings)) + + def test_parse_json_file_from_api_with_multiple_findings_json(self): + my_file_handle, _product, _engagement, test = self.init( + get_unit_tests_path() + "/scans/sonarqube/findings_over_api.json" + ) + parser = SonarQubeParser() + findings = parser.get_findings(my_file_handle, test) + self.assertEqual(5, len(findings)) + item = findings[0] + self.assertEqual(str, type(item.description)) + self.assertEqual("OWASP:UsingComponentWithKnownVulnerability_fjioefjwoefijo", item.title) + self.assertEqual("Medium", item.severity) + self.assertEqual("CVE-2024-2529", item.cve) + self.assertEqual("120", item.cwe) + self.assertEqual("6.4", item.cvssv3_score) + self.assertEqual("package", item.component_name) + self.assertEqual("1.1.2", item.component_version) + item = findings[1] + self.assertEqual("Web:TableWithoutCaptionCheck_asdfwfewfwefewf", item.title) + self.assertEqual("Low", item.severity) + self.assertIsNone(item.cve) + self.assertEqual(0, item.cwe) + self.assertIsNone(item.cvssv3_score) + item = findings[2] + self.assertEqual("typescript:S1533_fjoiewfjoweifjoihugu-", item.title) + self.assertEqual("Low", item.severity) + item = findings[3] + self.assertEqual("GHSA-frr2-c345-p7c2", item.cve) + item = findings[4] + self.assertEqual("CVE-2023-52428", item.cve) + self.assertEqual("nimbus-jose-jwt-9.24.4.jar", item.component_name) + self.assertIsNone(item.component_version) + + def test_parse_json_file_from_api_with_multiple_findings_hotspots_json(self): + my_file_handle, _product, _engagement, test = self.init( + get_unit_tests_path() + "/scans/sonarqube/findings_over_api_hotspots.json" + ) + parser = SonarQubeParser() + findings = parser.get_findings(my_file_handle, test) + self.assertEqual(4, len(findings)) + item = findings[0] + self.assertEqual(str, type(item.description)) + self.assertEqual("typescript:7777_fwafewef", item.title) + self.assertEqual("High", item.severity) + item = findings[1] + self.assertEqual("Web:1222_cyxcvyxcvyxv", item.title) + self.assertEqual("Low", item.severity) + item = findings[2] + self.assertEqual("Web:9876_werrwerwerwer", item.title) + self.assertEqual("Low", item.severity) + + def test_parse_json_file_from_api_with_empty_json(self): + my_file_handle, _product, _engagement, test = self.init( + get_unit_tests_path() + "/scans/sonarqube/findings_over_api_empty.json" + ) + parser = SonarQubeParser() + findings = parser.get_findings(my_file_handle, test) + self.assertEqual(0, len(findings)) + + def test_parse_json_file_from_api_with_emppty_zip(self): + my_file_handle, _product, _engagement, test = self.init( + get_unit_tests_path() + "/scans/sonarqube/empty_zip.zip" + ) + parser = SonarQubeParser() + findings = parser.get_findings(my_file_handle, test) + self.assertEqual(0, len(findings)) + + def test_parse_json_file_from_api_with_multiple_findings_zip(self): + my_file_handle, _product, _engagement, test = self.init( + get_unit_tests_path() + "/scans/sonarqube/findings_over_api.zip" + ) + parser = SonarQubeParser() + findings = parser.get_findings(my_file_handle, test) + self.assertEqual(6, len(findings)) + item = findings[0] + self.assertEqual(str, type(item.description)) + self.assertEqual("OWASP:UsingComponentWithKnownVulnerability_fjioefjwoefijo", item.title) + self.assertEqual("Medium", item.severity) + item = findings[3] + self.assertEqual("OWASP:UsingComponentWithKnownVulnerability_fjioefjwo1123efijo", item.title) + self.assertEqual("Low", item.severity) + item = findings[5] + self.assertEqual("typescript:S112533_fjoiewfjo1235gweifjoihugu-", item.title) + self.assertEqual("Medium", item.severity)