From 32aa59ee85b026ba1fa8e48f4b59468eeb74ad53 Mon Sep 17 00:00:00 2001 From: Manuel Sommer Date: Mon, 5 Feb 2024 15:03:38 +0100 Subject: [PATCH] :bug: fix yarn_audit, #6495 --- dojo/tools/yarn_audit/parser.py | 213 ++++++++++++++------- unittests/scans/yarn_audit/issue_6495.json | 142 ++++++++++++++ unittests/tools/test_yarn_audit_parser.py | 8 + 3 files changed, 289 insertions(+), 74 deletions(-) create mode 100644 unittests/scans/yarn_audit/issue_6495.json diff --git a/dojo/tools/yarn_audit/parser.py b/dojo/tools/yarn_audit/parser.py index 325049dd514..5f0ae8b39a1 100644 --- a/dojo/tools/yarn_audit/parser.py +++ b/dojo/tools/yarn_audit/parser.py @@ -17,15 +17,24 @@ def get_description_for_scan_types(self, scan_type): def get_findings(self, json_output, test): if json_output is None: return list() - tree = (json.loads(line) for line in json_output) - return self.get_items(tree, test) + tree = None + lines = json_output.read() + if isinstance(lines, bytes): + lines = lines.decode("utf-8") # passes in unittests, but would fail in production + if '"type"' in lines: + lines = lines.split('\n') + tree = (json.loads(line) for line in lines if "{" in line) + return self.get_items_yarn(tree, test) + else: + tree = json.loads(lines) + return self.get_items_auditci(tree, test) - def get_items(self, tree, test): + def get_items_yarn(self, tree, test): items = {} for element in tree: if element.get("type") == "auditAdvisory": node = element.get("data").get("advisory") - item = get_item(node, test) + item = self.get_item_yarn(node, test) unique_key = str(node.get("id")) + str(node.get("module_name")) items[unique_key] = item elif element.get("type") == "error": @@ -33,78 +42,134 @@ def get_items(self, tree, test): raise ValueError( "yarn audit report contains errors: %s", error ) - return list(items.values()) + def get_items_auditci(self, tree, test): # https://github.com/DefectDojo/django-DefectDojo/issues/6495 + items = [] + for element in tree.get("advisories"): + findings = "**findings:** " + str(tree.get("advisories").get(element).get("findings")) + metadata = "**metadata:** " + str(tree.get("advisories").get(element).get("metadata")) + vulnerable_versions = "**vulnerable_versions:** " + str(tree.get("advisories").get(element).get("vulnerable_versions")) + github_advisory_id = "**github_advisory_id:** " + str(tree.get("advisories").get(element).get("github_advisory_id")) + access = "**access:** " + str(tree.get("advisories").get(element).get("access")) + patched_versions = "**patched_versions:** " + str(tree.get("advisories").get(element).get("patched_versions")) + cvss = "**cvss:** " + str(tree.get("advisories").get(element).get("cvss")) + found_by = "**found_by:** " + str(tree.get("advisories").get(element).get("found_by")) + deleted = "**deleted:** " + str(tree.get("advisories").get(element).get("deleted")) + id = "**id:** " + str(tree.get("advisories").get(element).get("id")) + references = "**references:** " + str(tree.get("advisories").get(element).get("references")) + created = "**created:** " + str(tree.get("advisories").get(element).get("created")) + reported_by = "**reported_by:** " + str(tree.get("advisories").get(element).get("reported_by")) + title = "**title:** " + str(tree.get("advisories").get(element).get("title")) + npm_advisory_id = "**npm_advisory_id:** " + str(tree.get("advisories").get(element).get("npm_advisory_id")) + overview = "**overview:** " + str(tree.get("advisories").get(element).get("overview")) + url = "**url:** " + str(tree.get("advisories").get(element).get("url")) + description = "" + description += findings + "\n" + description += metadata + "\n" + description += vulnerable_versions + "\n" + description += github_advisory_id + "\n" + description += access + "\n" + description += patched_versions + "\n" + description += cvss + "\n" + description += found_by + "\n" + description += deleted + "\n" + description += id + "\n" + description += created + "\n" + description += reported_by + "\n" + description += title + "\n" + description += npm_advisory_id + "\n" + description += overview + "\n" + dojo_finding = Finding( + title=tree.get("advisories").get(element).get("cves")[0] + "_" + tree.get("advisories").get(element).get("module_name"), + test=test, + severity=self.severitytranslator(severity=tree.get("advisories").get(element).get("severity")), + description=description, + cve=tree.get("advisories").get(element).get("cves")[0], + mitigation=tree.get("advisories").get(element).get("recommendation"), + references=url + "\n" + references, + component_name=tree.get("advisories").get(element).get("module_name"), + component_version=tree.get("advisories").get(element).get("findings")[0]["version"], + false_p=False, + duplicate=False, + out_of_scope=False, + mitigated=None, + impact="No impact provided", + static_finding=True, + dynamic_finding=False, + ) + if tree.get("advisories").get(element).get("cwe") != []: + dojo_finding.cwe = tree.get("advisories").get(element).get("cwe")[0].strip("CWE-") + items.append(dojo_finding) + return items -def get_item(item_node, test): - if item_node["severity"] == "low": - severity = "Low" - elif item_node["severity"] == "moderate": - severity = "Medium" - elif item_node["severity"] == "high": - severity = "High" - elif item_node["severity"] == "critical": - severity = "Critical" - else: - severity = "Info" + def severitytranslator(self, severity): + if severity == "low": + severity = "Low" + elif severity == "moderate": + severity = "Medium" + elif severity == "high": + severity = "High" + elif severity == "critical": + severity = "Critical" + else: + severity = "Info" + return severity - paths = "" - for finding in item_node["findings"]: - paths += ( - "\n - " - + str(finding["version"]) - + ":" - + str(",".join(finding["paths"][:25])) + def get_item_yarn(self, item_node, test): + severity = self.severitytranslator(severity=item_node["severity"]) + paths = "" + for finding in item_node["findings"]: + paths += ( + "\n - " + + str(finding["version"]) + + ":" + + str(",".join(finding["paths"][:25])) + ) + if len(finding["paths"]) > 25: + paths += "\n - ..... (list of paths truncated after 25 paths)" + cwe = get_npm_cwe(item_node) + dojo_finding = Finding( + title=item_node["title"] + + " - " + + "(" + + item_node["module_name"] + + ", " + + item_node["vulnerable_versions"] + + ")", + test=test, + severity=severity, + file_path=item_node["findings"][0]["paths"][0], + description=item_node["url"] + + "\n" + + item_node["overview"] + + "\n Vulnerable Module: " + + item_node["module_name"] + + "\n Vulnerable Versions: " + + str(item_node["vulnerable_versions"]) + + "\n Patched Version: " + + str(item_node["patched_versions"]) + + "\n Vulnerable Paths: " + + str(paths) + + "\n CWE: " + + str(item_node["cwe"]) + + "\n Access: " + + str(item_node["access"]), + cwe=cwe, + mitigation=item_node["recommendation"], + references=item_node["url"], + component_name=item_node["module_name"], + component_version=item_node["findings"][0]["version"], + false_p=False, + duplicate=False, + out_of_scope=False, + mitigated=None, + impact="No impact provided", + static_finding=True, + dynamic_finding=False, ) - if len(finding["paths"]) > 25: - paths += "\n - ..... (list of paths truncated after 25 paths)" - - cwe = get_npm_cwe(item_node) - - dojo_finding = Finding( - title=item_node["title"] - + " - " - + "(" - + item_node["module_name"] - + ", " - + item_node["vulnerable_versions"] - + ")", - test=test, - severity=severity, - file_path=item_node["findings"][0]["paths"][0], - description=item_node["url"] - + "\n" - + item_node["overview"] - + "\n Vulnerable Module: " - + item_node["module_name"] - + "\n Vulnerable Versions: " - + str(item_node["vulnerable_versions"]) - + "\n Patched Version: " - + str(item_node["patched_versions"]) - + "\n Vulnerable Paths: " - + str(paths) - + "\n CWE: " - + str(item_node["cwe"]) - + "\n Access: " - + str(item_node["access"]), - cwe=cwe, - mitigation=item_node["recommendation"], - references=item_node["url"], - component_name=item_node["module_name"], - component_version=item_node["findings"][0]["version"], - false_p=False, - duplicate=False, - out_of_scope=False, - mitigated=None, - impact="No impact provided", - static_finding=True, - dynamic_finding=False, - ) - - if len(item_node["cves"]) > 0: - dojo_finding.unsaved_vulnerability_ids = list() - for vulnerability_id in item_node["cves"]: - dojo_finding.unsaved_vulnerability_ids.append(vulnerability_id) - - return dojo_finding + if len(item_node["cves"]) > 0: + dojo_finding.unsaved_vulnerability_ids = list() + for vulnerability_id in item_node["cves"]: + dojo_finding.unsaved_vulnerability_ids.append(vulnerability_id) + return dojo_finding diff --git a/unittests/scans/yarn_audit/issue_6495.json b/unittests/scans/yarn_audit/issue_6495.json new file mode 100644 index 00000000000..f7a594c0eee --- /dev/null +++ b/unittests/scans/yarn_audit/issue_6495.json @@ -0,0 +1,142 @@ +{ + "actions": [], + "advisories": { + "1068298": { + "findings": [ + { + "version": "1.3.5", + "paths": [ + "@angular/cli>ini", + "danger>parse-git-config>ini", + "@datorama/akita>schematics-utilities>@schematics/update>ini", + "@datorama/akita-ng-entity-service>@datorama/akita>schematics-utilities>@schematics/update>ini", + "nodemon>update-notifier>latest-version>package-json>registry-auth-token>rc>ini", + "@mikro-orm/cli>@mikro-orm/migrations>knex>liftoff>findup-sync>resolve-dir>global-modules>global-prefix>ini", + "@mikro-orm/cli>@mikro-orm/knex>@mikro-orm/migrations>knex>liftoff>findup-sync>resolve-dir>global-modules>global-prefix>ini", + "@mikro-orm/cli>@mikro-orm/entity-generator>@mikro-orm/knex>@mikro-orm/migrations>knex>liftoff>findup-sync>resolve-dir>global-modules>global-prefix>ini" + ] + } + ], + "metadata": null, + "vulnerable_versions": "<1.3.6", + "module_name": "ini", + "severity": "high", + "github_advisory_id": "GHSA-qqgx-2p2h-9c37", + "cves": [ + "CVE-2020-7788" + ], + "access": "public", + "patched_versions": ">=1.3.6", + "cvss": { + "score": 7.3, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L" + }, + "updated": "2021-07-28T21:12:38.000Z", + "recommendation": "Upgrade to version 1.3.6 or later", + "cwe": [ + "CWE-1321" + ], + "found_by": null, + "deleted": null, + "id": 1068298, + "references": "- https://github.com/npm/ini/commit/56d2805e07ccd94e2ba0984ac9240ff02d44b6f1\n- https://www.npmjs.com/advisories/1589\n- https://snyk.io/vuln/SNYK-JS-INI-1048974\n- https://nvd.nist.gov/vuln/detail/CVE-2020-7788\n- https://lists.debian.org/debian-lts-announce/2020/12/msg00032.html\n- https://github.com/advisories/GHSA-qqgx-2p2h-9c37", + "created": "2020-12-10T16:53:45.000Z", + "reported_by": null, + "title": "Prototype Pollution", + "npm_advisory_id": null, + "overview": "### Overview\nThe `ini` npm package before version 1.3.6 has a Prototype Pollution vulnerability.\n\nIf an attacker submits a malicious INI file to an application that parses it with `ini.parse`, they will pollute the prototype on the application. This can be exploited further depending on the context.\n\n### Patches\n\nThis has been patched in 1.3.6\n\n### Steps to reproduce\n\npayload.ini\n```\n[__proto__]\npolluted = \"polluted\"\n```\n\npoc.js:\n```\nvar fs = require('fs')\nvar ini = require('ini')\n\nvar parsed = ini.parse(fs.readFileSync('./payload.ini', 'utf-8'))\nconsole.log(parsed)\nconsole.log(parsed.__proto__)\nconsole.log(polluted)\n```\n\n```\n> node poc.js\n{}\n{ polluted: 'polluted' }\n{ polluted: 'polluted' }\npolluted\n```", + "url": "https://github.com/advisories/GHSA-qqgx-2p2h-9c37" + }, + "1075625": { + "findings": [ + { + "version": "0.4.3", + "paths": [ + "@playwright/test>jpeg-js", + "@playwright/test>playwright-core>jpeg-js" + ] + } + ], + "metadata": null, + "vulnerable_versions": "<0.4.4", + "module_name": "jpeg-js", + "severity": "high", + "github_advisory_id": "GHSA-xvf7-4v9q-58w6", + "cves": [ + "CVE-2022-25851" + ], + "access": "public", + "patched_versions": ">=0.4.4", + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "updated": "2022-06-20T21:58:36.000Z", + "recommendation": "Upgrade to version 0.4.4 or later", + "cwe": [ + "CWE-835" + ], + "found_by": null, + "deleted": null, + "id": 1075625, + "references": "- https://nvd.nist.gov/vuln/detail/CVE-2022-25851\n- https://github.com/jpeg-js/jpeg-js/issues/105\n- https://github.com/jpeg-js/jpeg-js/pull/106/\n- https://github.com/jpeg-js/jpeg-js/commit/9ccd35fb5f55a6c4f1902ac5b0f270f675750c27\n- https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-2860295\n- https://snyk.io/vuln/SNYK-JS-JPEGJS-2859218\n- https://github.com/advisories/GHSA-xvf7-4v9q-58w6", + "created": "2022-06-11T00:00:17.000Z", + "reported_by": null, + "title": "Infinite loop in jpeg-js", + "npm_advisory_id": null, + "overview": "The package jpeg-js before 0.4.4 is vulnerable to Denial of Service (DoS) where a particular piece of input will cause the program to enter an infinite loop and never return.", + "url": "https://github.com/advisories/GHSA-xvf7-4v9q-58w6" + }, + "1075701": { + "findings": [ + { + "version": "9.6.0", + "paths": [ + "nodemon>update-notifier>latest-version>package-json>got" + ] + } + ], + "metadata": null, + "vulnerable_versions": "<11.8.5", + "module_name": "got", + "severity": "moderate", + "github_advisory_id": "GHSA-pfrx-2q88-qq97", + "cves": [ + "CVE-2022-33987" + ], + "access": "public", + "patched_versions": ">=11.8.5", + "cvss": { + "score": 0, + "vectorString": null + }, + "updated": "2022-06-27T17:09:23.000Z", + "recommendation": "Upgrade to version 11.8.5 or later", + "cwe": [], + "found_by": null, + "deleted": null, + "id": 1075701, + "references": "- https://nvd.nist.gov/vuln/detail/CVE-2022-33987\n- https://github.com/sindresorhus/got/pull/2047\n- https://github.com/sindresorhus/got/compare/v12.0.3...v12.1.0\n- https://github.com/sindresorhus/got/commit/861ccd9ac2237df762a9e2beed7edd88c60782dc\n- https://github.com/sindresorhus/got/releases/tag/v11.8.5\n- https://github.com/sindresorhus/got/releases/tag/v12.1.0\n- https://github.com/advisories/GHSA-pfrx-2q88-qq97", + "created": "2022-06-19T00:00:21.000Z", + "reported_by": null, + "title": "Got allows a redirect to a UNIX socket", + "npm_advisory_id": null, + "overview": "The got package before 11.8.5 and 12.1.0 for Node.js allows a redirect to a UNIX socket.", + "url": "https://github.com/advisories/GHSA-pfrx-2q88-qq97" + } + }, + "muted": [], + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 1, + "high": 10, + "critical": 0 + }, + "dependencies": 2236, + "devDependencies": 121, + "optionalDependencies": 0, + "totalDependencies": 2357 + } +} \ No newline at end of file diff --git a/unittests/tools/test_yarn_audit_parser.py b/unittests/tools/test_yarn_audit_parser.py index 0fb5c64d496..e5b13234740 100644 --- a/unittests/tools/test_yarn_audit_parser.py +++ b/unittests/tools/test_yarn_audit_parser.py @@ -75,3 +75,11 @@ def test_yarn_audit_parser_empty_with_error(self): "yarn audit report contains errors:" in str(context.exception) ) self.assertTrue("ECONNREFUSED" in str(context.exception)) + + def test_yarn_audit_parser_issue_6495(self): + testfile = open("unittests/scans/yarn_audit/issue_6495.json") + parser = YarnAuditParser() + findings = parser.get_findings(testfile, self.get_test()) + testfile.close() + self.assertEqual(3, len(findings)) + self.assertEqual(findings[0].cwe, "1321")