diff --git a/dojo/tools/github_vulnerability/parser.py b/dojo/tools/github_vulnerability/parser.py index 3c134342d20..ac44d815e86 100644 --- a/dojo/tools/github_vulnerability/parser.py +++ b/dojo/tools/github_vulnerability/parser.py @@ -17,137 +17,174 @@ def get_description_for_scan_types(self, scan_type): def get_findings(self, filename, test): data = json.load(filename) - if "data" not in data: - raise ValueError("Invalid report file, no 'data' node found") - - vulnerabilityAlerts = self._search_vulnerability_alerts(data["data"]) - if not vulnerabilityAlerts: - raise ValueError( - "Invalid report, no 'vulnerabilityAlerts' node found" - ) - - repository_url = None - if "repository" in data["data"]: - if "nameWithOwner" in data["data"]["repository"]: - repository_url = "https://github.com/{}".format( - data["data"]["repository"]["nameWithOwner"] - ) - if "url" in data["data"]["repository"]: - repository_url = data["data"]["repository"]["url"] - - dupes = dict() - for alert in vulnerabilityAlerts["nodes"]: - description = alert["securityVulnerability"]["advisory"][ - "description" - ] - - if "number" in alert and repository_url is not None: - dependabot_url = ( - repository_url - + "/security/dependabot/{}".format(alert["number"]) - ) - description = ( - "[{}]({})\n".format(dependabot_url, dependabot_url) - + description + if "data" in data: + vulnerabilityAlerts = self._search_vulnerability_alerts(data["data"]) + if not vulnerabilityAlerts: + raise ValueError( + "Invalid report, no 'vulnerabilityAlerts' node found" ) - - finding = Finding( - title=alert["securityVulnerability"]["advisory"]["summary"], - test=test, - description=description, - severity=self._convert_security( - alert["securityVulnerability"].get("severity", "MODERATE") - ), - static_finding=True, - dynamic_finding=False, - unique_id_from_tool=alert["id"], - ) - - if "vulnerableManifestPath" in alert: - finding.file_path = alert["vulnerableManifestPath"] - - if "vulnerableRequirements" in alert and alert["vulnerableRequirements"].startswith("= "): - finding.component_version = alert["vulnerableRequirements"][2:] - - if "createdAt" in alert: - finding.date = dateutil.parser.parse(alert["createdAt"]) - - if "state" in alert and ( - "FIXED" == alert["state"] or "DISMISSED" == alert["state"] - ): - finding.active = False - finding.is_mitigated = True - - # if the package is present - if "package" in alert["securityVulnerability"]: - finding.component_name = alert["securityVulnerability"][ - "package" - ].get("name") - - if "references" in alert["securityVulnerability"]["advisory"]: - finding.references = "" - for ref in alert["securityVulnerability"]["advisory"][ - "references" - ]: - finding.references += ref["url"] + "\r\n" - - if "identifiers" in alert["securityVulnerability"]["advisory"]: - unsaved_vulnerability_ids = list() - for identifier in alert["securityVulnerability"]["advisory"][ - "identifiers" - ]: - if identifier.get("value"): - unsaved_vulnerability_ids.append( - identifier.get("value") - ) - if unsaved_vulnerability_ids: - finding.unsaved_vulnerability_ids = ( - unsaved_vulnerability_ids + repository_url = None + if "repository" in data["data"]: + if "nameWithOwner" in data["data"]["repository"]: + repository_url = "https://github.com/{}".format( + data["data"]["repository"]["nameWithOwner"] ) - - if "cvss" in alert["securityVulnerability"]["advisory"]: - if ( - "score" - in alert["securityVulnerability"]["advisory"]["cvss"] + if "url" in data["data"]["repository"]: + repository_url = data["data"]["repository"]["url"] + dupes = dict() + for alert in vulnerabilityAlerts["nodes"]: + description = alert["securityVulnerability"]["advisory"][ + "description" + ] + if "number" in alert and repository_url is not None: + dependabot_url = ( + repository_url + + "/security/dependabot/{}".format(alert["number"]) + ) + description = ( + "[{}]({})\n".format(dependabot_url, dependabot_url) + + description + ) + finding = Finding( + title=alert["securityVulnerability"]["advisory"]["summary"], + test=test, + description=description, + severity=self._convert_security( + alert["securityVulnerability"].get("severity", "MODERATE") + ), + static_finding=True, + dynamic_finding=False, + unique_id_from_tool=alert["id"], + ) + if "vulnerableManifestPath" in alert: + finding.file_path = alert["vulnerableManifestPath"] + if "vulnerableRequirements" in alert and alert["vulnerableRequirements"].startswith("= "): + finding.component_version = alert["vulnerableRequirements"][2:] + if "createdAt" in alert: + finding.date = dateutil.parser.parse(alert["createdAt"]) + if "state" in alert and ( + "FIXED" == alert["state"] or "DISMISSED" == alert["state"] ): - score = alert["securityVulnerability"]["advisory"]["cvss"][ + finding.active = False + finding.is_mitigated = True + # if the package is present + if "package" in alert["securityVulnerability"]: + finding.component_name = alert["securityVulnerability"][ + "package" + ].get("name") + if "references" in alert["securityVulnerability"]["advisory"]: + finding.references = "" + for ref in alert["securityVulnerability"]["advisory"][ + "references" + ]: + finding.references += ref["url"] + "\r\n" + if "identifiers" in alert["securityVulnerability"]["advisory"]: + unsaved_vulnerability_ids = list() + for identifier in alert["securityVulnerability"]["advisory"][ + "identifiers" + ]: + if identifier.get("value"): + unsaved_vulnerability_ids.append( + identifier.get("value") + ) + if unsaved_vulnerability_ids: + finding.unsaved_vulnerability_ids = ( + unsaved_vulnerability_ids + ) + if "cvss" in alert["securityVulnerability"]["advisory"]: + if ( "score" - ] - if score is not None: - finding.cvssv3_score = score + in alert["securityVulnerability"]["advisory"]["cvss"] + ): + score = alert["securityVulnerability"]["advisory"]["cvss"][ + "score" + ] + if score is not None: + finding.cvssv3_score = score + if ( + "vectorString" + in alert["securityVulnerability"]["advisory"]["cvss"] + ): + cvss_vector_string = alert["securityVulnerability"][ + "advisory" + ]["cvss"]["vectorString"] + if cvss_vector_string is not None: + cvss_objects = cvss_parser.parse_cvss_from_text( + cvss_vector_string + ) + if len(cvss_objects) > 0: + finding.cvssv3 = cvss_objects[0].clean_vector() if ( - "vectorString" - in alert["securityVulnerability"]["advisory"]["cvss"] + "cwes" in alert["securityVulnerability"]["advisory"] + and "nodes" + in alert["securityVulnerability"]["advisory"]["cwes"] ): - cvss_vector_string = alert["securityVulnerability"][ - "advisory" - ]["cvss"]["vectorString"] - if cvss_vector_string is not None: - cvss_objects = cvss_parser.parse_cvss_from_text( - cvss_vector_string - ) - if len(cvss_objects) > 0: - finding.cvssv3 = cvss_objects[0].clean_vector() - - if ( - "cwes" in alert["securityVulnerability"]["advisory"] - and "nodes" - in alert["securityVulnerability"]["advisory"]["cwes"] - ): - cwe_nodes = alert["securityVulnerability"]["advisory"]["cwes"][ - "nodes" - ] - if cwe_nodes and len(cwe_nodes) > 0: - finding.cwe = int(cwe_nodes[0].get("cweId")[4:]) - - dupe_key = finding.unique_id_from_tool - if dupe_key in dupes: - find = dupes[dupe_key] - find.nb_occurences += 1 - else: - dupes[dupe_key] = finding - - return list(dupes.values()) + cwe_nodes = alert["securityVulnerability"]["advisory"]["cwes"][ + "nodes" + ] + if cwe_nodes and len(cwe_nodes) > 0: + finding.cwe = int(cwe_nodes[0].get("cweId")[4:]) + dupe_key = finding.unique_id_from_tool + if dupe_key in dupes: + find = dupes[dupe_key] + find.nb_occurences += 1 + else: + dupes[dupe_key] = finding + return list(dupes.values()) + elif isinstance(data, list): + findings = list() + for vuln in data: + url = vuln["url"] + html_url = vuln["html_url"] + if vuln["state"] == "open": + active = True + else: + active = False + ruleid = vuln["rule"]["id"] + ruleseverity = vuln["rule"]["severity"] + ruledescription = vuln["rule"]["description"] + rulename = vuln["rule"]["name"] + ruletags = vuln["rule"]["tags"] + severity = vuln["rule"]["security_severity_level"] + most_recent_instanceref = vuln["most_recent_instance"]["ref"] + most_recent_instanceanalysis_key = vuln["most_recent_instance"]["analysis_key"] + most_recent_instanceenvironment = vuln["most_recent_instance"]["environment"] + most_recent_instancecategory = vuln["most_recent_instance"]["category"] + most_recent_instancestate = vuln["most_recent_instance"]["state"] + most_recent_instancecommit_sha = vuln["most_recent_instance"]["commit_sha"] + most_recent_instancemessage = vuln["most_recent_instance"]["message"]["text"] + location = vuln["most_recent_instance"]["location"] + instancesurl = vuln["instances_url"] + description = ruledescription + "\n" + description += "**url:** " + url + "\n" + description += "**html_url:** " + html_url + "\n" + description += "**ruleid:** " + ruleid + "\n" + description += "**ruleseverity:** " + ruleseverity + "\n" + description += "**ruledescription:** " + ruledescription + "\n" + description += "**rulename:** " + rulename + "\n" + description += "**ruletags:** " + str(ruletags) + "\n" + description += "**most_recent_instanceref:** " + most_recent_instanceref + "\n" + description += "**most_recent_instanceanalysis_key:** " + most_recent_instanceanalysis_key + "\n" + description += "**most_recent_instanceenvironment:** " + most_recent_instanceenvironment + "\n" + description += "**most_recent_instancecategory:** " + most_recent_instancecategory + "\n" + description += "**most_recent_instancestate:** " + most_recent_instancestate + "\n" + description += "**most_recent_instancecommit_sha:** " + most_recent_instancecommit_sha + "\n" + description += "**most_recent_instancemessage:** " + most_recent_instancemessage + "\n" + description += "**location:** " + str(location) + "\n" + description += "**instancesurl:** " + instancesurl + "\n" + uniqueid = ruleid + url + most_recent_instanceanalysis_key + str(location) + finding = Finding( + title=ruleid, + test=test, + description=description, + severity=severity.capitalize(), + active=active, + static_finding=True, + dynamic_finding=False, + unique_id_from_tool=uniqueid, + ) + findings.append(finding) + return findings def _search_vulnerability_alerts(self, data): if isinstance(data, list): diff --git a/unittests/scans/github_vulnerability/issue_9582.json b/unittests/scans/github_vulnerability/issue_9582.json new file mode 100644 index 00000000000..7e297d8f1b2 --- /dev/null +++ b/unittests/scans/github_vulnerability/issue_9582.json @@ -0,0 +1,111 @@ +[ + { + "number":35, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35", + "html_url":"https://github.com/XX/YY/security/code-scanning/35", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/clear-text-storage-sensitive-data", + "severity":"error", + "description":"Clear-text storage of sensitive information", + "name":"py/clear-text-storage-sensitive-data", + "tags":[ + "external/cwe/cwe-312", + "external/cwe/cwe-315", + "external/cwe/cwe-359", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/XX/YY", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This expression stores sensitive data (secret) as clear text." + }, + "location":{ + "path":"Unsafe Deserialization/file.py", + "start_line":42, + "end_line":42, + "start_column":17, + "end_column":23 + }, + "classifications":[ + + ] + }, + "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35/instances" + }, + { + "number":34, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34", + "html_url":"https://github.com/XX/YY/security/code-scanning/34", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/path-injection", + "severity":"error", + "description":"Uncontrolled data used in path expression", + "name":"py/path-injection", + "tags":[ + "correctness", + "external/cwe/cwe-022", + "external/cwe/cwe-023", + "external/cwe/cwe-036", + "external/cwe/cwe-073", + "external/cwe/cwe-099", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/XX/YY", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This path depends on a user-provided value." + }, + "location":{ + "path":"Path Traversal/file2.py", + "start_line":78, + "end_line":78, + "start_column":25, + "end_column":63 + }, + "classifications":[ + + ] + }, + "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34/instances" + } +] \ No newline at end of file diff --git a/unittests/tools/test_github_vulnerability_parser.py b/unittests/tools/test_github_vulnerability_parser.py index 1453c02a39b..9b54d9fdc64 100644 --- a/unittests/tools/test_github_vulnerability_parser.py +++ b/unittests/tools/test_github_vulnerability_parser.py @@ -266,3 +266,15 @@ def test_parser_version(self): self.assertEqual(finding.severity, "Critical") self.assertEqual(finding.component_name, "org.springframework:spring-web") self.assertEqual(finding.component_version, "5.3.29") + + def test_parse_file_issue_9582(self): + testfile = open("unittests/scans/github_vulnerability/issue_9582.json") + parser = GithubVulnerabilityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(2, len(findings)) + for finding in findings: + finding.clean() + with self.subTest(i=0): + finding = findings[0] + self.assertEqual(finding.title, "py/clear-text-storage-sensitive-data") + self.assertEqual(finding.severity, "High")