From d26928395676eab6e5ceaa20d99422dc47d25350 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:25:04 -0600 Subject: [PATCH 1/4] Reimport: Special statuses should be respected from reports --- dojo/importers/default_reimporter.py | 9 + dojo/tools/checkmarx_one/parser.py | 32 ++- unittests/dojo_test_case.py | 12 +- .../one-open-one-false-positive.json | 238 ++++++++++++++++++ .../checkmarx_one/two-false-positive.json | 238 ++++++++++++++++++ unittests/test_import_reimport.py | 24 +- unittests/tools/test_checkmarx_one_parser.py | 12 + 7 files changed, 552 insertions(+), 13 deletions(-) create mode 100644 unittests/scans/checkmarx_one/one-open-one-false-positive.json create mode 100644 unittests/scans/checkmarx_one/two-false-positive.json diff --git a/dojo/importers/default_reimporter.py b/dojo/importers/default_reimporter.py index af88d373fe9..b2a9f7b61d8 100644 --- a/dojo/importers/default_reimporter.py +++ b/dojo/importers/default_reimporter.py @@ -264,6 +264,15 @@ def close_old_findings( # Determine if pushing to jira or if the finding groups are enabled mitigated_findings = [] for finding in findings: + # Get any status changes that could have occurred earlier in the process + # for special statuses only. + # An example of such is a finding being reported as false positive, and + # reimport makes this change in the database. However, the findings here + # are calculated based from the original values before the reimport, so + # any updates made during reimport are discarded without first getting the + # state of the finding as it stands at this moment + finding.refresh_from_db(fields=["false_p", "risk_accepted", "out_of_scope"]) + # Ensure the finding is not already closed if not finding.mitigated or not finding.is_mitigated: logger.debug("mitigating finding: %i:%s", finding.id, finding) self.mitigate_finding( diff --git a/dojo/tools/checkmarx_one/parser.py b/dojo/tools/checkmarx_one/parser.py index b06525782ba..678477c703d 100644 --- a/dojo/tools/checkmarx_one/parser.py +++ b/dojo/tools/checkmarx_one/parser.py @@ -71,7 +71,6 @@ def parse_iac_vulnerabilities( "description": ( f"{query.get('description')}\n\n" f"**Category**: {query.get('category')}\n"), - "verified": query.get("state") != "TO_VERIFY", "test": test, } # Iterate over the individual issues @@ -81,6 +80,8 @@ def parse_iac_vulnerabilities( date = self._parse_date(instance.get("firstDetectionDate")) else: date = self._parse_date(instance.get("lastDetectionDate")) + instance_details = self.determine_state(instance) + instance_details.update(base_finding_details) # Create the finding object finding = Finding( severity=instance.get("severity").title(), @@ -174,14 +175,15 @@ def get_node_snippet(nodes: list) -> str: date = self._parse_date(instance.get("firstFoundDate")) else: date = self._parse_date(instance.get("foundDate")) + instance_details = self.determine_state(instance) + instance_details.update(base_finding_details) # Create the finding object finding = Finding( severity=instance.get("severity").title(), date=date, file_path=instance.get("destinationFileName"), line=instance.get("destinationLine"), - verified=instance.get("state") != "TO_VERIFY", - **base_finding_details, + **instance_details, ) # Add some details to the description if node_snippet := get_node_snippet(instance.get("nodes", [])): @@ -219,11 +221,9 @@ def parse_vulnerabilities( + "**uri**: " + locations_uri + "\n" + "**startLine**: " + str(locations_startLine) + "\n" + "**endLine**: " + str(locations_endLine) + "\n", - false_p=False, - duplicate=False, - out_of_scope=False, static_finding=True, dynamic_finding=False, + **self.determine_state(result), ) findings.append(finding) return findings @@ -273,6 +273,7 @@ def get_results_sast( test=test, static_finding=True, unique_id_from_tool=unique_id_from_tool, + **self.determine_state(vulnerability), ) def get_results_kics( @@ -290,11 +291,11 @@ def get_results_kics( title=description, description=description, severity=vulnerability.get("severity").title(), - verified=vulnerability.get("state") != "TO_VERIFY", file_path=file_path, test=test, static_finding=True, unique_id_from_tool=unique_id_from_tool, + **self.determine_state(vulnerability), ) def get_results_sca( @@ -311,10 +312,10 @@ def get_results_sca( title=description, description=description, severity=vulnerability.get("severity").title(), - verified=vulnerability.get("state") != "TO_VERIFY", test=test, static_finding=True, unique_id_from_tool=unique_id_from_tool, + **self.determine_state(vulnerability), ) if (cveId := vulnerability.get("cveId")) is not None: finding.unsaved_vulnerability_ids = [cveId] @@ -332,3 +333,18 @@ def get_findings(self, file, test): findings = self.parse_results(test, results) return findings + + def determine_state(self, data: dict) -> dict: + """ + Determine the state of the findings as set by Checkmarx One docs + https://docs.checkmarx.com/en/34965-68516-managing--triaging--vulnerabilities0.html#UUID-bc2397a3-1614-48bc-ff2f-1bc342071c5a_UUID-ad4991d6-161f-f76e-7d04-970f158eff9b + """ + state = data.get("state") + return { + "active": state in ["TO_VERIFY", "PROPOSED_NOT_EXPLOITABLE", "CONFIRMED", "URGENT"], + "verified": state in ["NOT_EXPLOITABLE", "CONFIRMED", "URGENT"], + "false_p": state in ["NOT_EXPLOITABLE"], + # These are not managed by checkmarx one, but is nice to explicitly set them + "duplicate": False, + "out_of_scope": False, + } \ No newline at end of file diff --git a/unittests/dojo_test_case.py b/unittests/dojo_test_case.py index caa150ea857..a0f5b9ec230 100644 --- a/unittests/dojo_test_case.py +++ b/unittests/dojo_test_case.py @@ -513,14 +513,18 @@ def import_scan_with_params(self, filename, scan_type="ZAP Scan", engagement=1, with (get_unit_tests_path() / filename).open(encoding="utf-8") as testfile: payload = { "minimum_severity": minimum_severity, - "active": active, - "verified": verified, "scan_type": scan_type, "file": testfile, "version": "1.0.1", "close_old_findings": close_old_findings, } + if active is not None: + payload["active"] = active + + if verified is not None: + payload["verified"] = verified + if engagement: payload["engagement"] = engagement @@ -669,7 +673,7 @@ def patch_finding_api(self, finding_id, finding_details, push_to_jira=None): def assert_finding_count_json(self, count, findings_content_json): self.assertEqual(findings_content_json["count"], count) - def get_test_findings_api(self, test_id, active=None, verified=None, is_mitigated=None, component_name=None, component_version=None, severity=None): + def get_test_findings_api(self, test_id, active=None, verified=None, is_mitigated=None, false_p=None, component_name=None, component_version=None, severity=None): payload = {"test": test_id} if active is not None: payload["active"] = active @@ -677,6 +681,8 @@ def get_test_findings_api(self, test_id, active=None, verified=None, is_mitigate payload["verified"] = verified if is_mitigated is not None: payload["is_mitigated"] = is_mitigated + if false_p is not None: + payload["false_p"] = false_p if component_name is not None: payload["component_name"] = component_name if severity is not None: diff --git a/unittests/scans/checkmarx_one/one-open-one-false-positive.json b/unittests/scans/checkmarx_one/one-open-one-false-positive.json new file mode 100644 index 00000000000..654d66da503 --- /dev/null +++ b/unittests/scans/checkmarx_one/one-open-one-false-positive.json @@ -0,0 +1,238 @@ +{ + "results": [ + { + "type": "sast", + "label": "sast", + "id": "0qB0NWis13+GAUmerweOUDyZzmk=", + "similarityId": "-1781066682", + "status": "RECURRENT", + "state": "NOT_EXPLOITABLE", + "severity": "CRITICAL", + "created": "2025-04-11T10:20:15Z", + "firstFoundAt": "2024-07-22T14:05:10Z", + "foundAt": "2025-04-11T10:20:15Z", + "firstScanId": "7083ee4e-2eff-4e2f-9d98-1aae8023169f", + "description": "The application's method executes an SQL query with executeUpdate, at line 63 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\n\nAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input query; this input is then read by the method at line 53 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. This input then flows through the code, into a query and to the database server - without sanitization.\r\n\r\nThis may enable an SQL Injection attack.\n\n", + "descriptionHTML": "\u003cp\u003eThe application‘s method executes an SQL query with executeUpdate, at line 63 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\u003c/p\u003e\n\n\u003cp\u003eAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input query; this input is then read by the method at line 53 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. This input then flows through the code, into a query and to the database server - without sanitization.\u003c/p\u003e\n\n\u003cp\u003eThis may enable an SQL Injection attack.\u003c/p\u003e\n", + "data": { + "queryId": 14517067005933136034, + "queryName": "SQL_Injection", + "group": "Java_Critical_Risk", + "resultHash": "0qB0NWis13+GAUmerweOUDyZzmk=", + "languageName": "Java", + "nodes": [ + { + "id": "0BQetI+3nx066RIjhL4RXixfgGs=", + "line": 53, + "name": "query", + "column": 54, + "length": 5, + "nodeID": 79013, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.completed.query", + "methodLine": 53 + }, + { + "id": "ZZtfb86lLjmfYOKSTKP04NigAnI=", + "line": 54, + "name": "query", + "column": 28, + "length": 5, + "nodeID": 79011, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.completed.query", + "methodLine": 53 + }, + { + "id": "34zDkUrPrO+2mIQJIxZOImmdHOE=", + "line": 57, + "name": "query", + "column": 49, + "length": 5, + "nodeID": 79379, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.query", + "methodLine": 57 + }, + { + "id": "Wv2DomzUnGITGzTxS9xYSIr4pL4=", + "line": 63, + "name": "query", + "column": 33, + "length": 5, + "nodeID": 79113, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.query", + "methodLine": 57 + }, + { + "id": "Wk8aYr9/jNUfhXn42U+ia7A0YVU=", + "line": 63, + "name": "executeUpdate", + "column": 32, + "length": 1, + "nodeID": 79109, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.statement.executeUpdate", + "methodLine": 57 + } + ] + }, + "comments": {}, + "vulnerabilityDetails": { + "cweId": 89, + "cvss": {}, + "compliances": [ + "ASD STIG 6.1", + "Base Preset", + "OWASP Mobile Top 10 2016", + "SANS top 25", + "PCI DSS v4.0", + "OWASP Mobile Top 10 2024", + "OWASP Top 10 2013", + "ASA Mobile Premium", + "ASA Premium", + "OWASP Top 10 API", + "PCI DSS v3.2.1", + "FISMA 2014", + "OWASP Top 10 2017", + "MOIS(KISA) Secure Coding 2021", + "OWASP ASVS", + "OWASP Top 10 2021", + "Top Tier", + "CWE top 25", + "NIST SP 800-53" + ] + } + }, + { + "type": "sast", + "label": "sast", + "id": "4/tlPdgnp5kS5PNqjnoXQagLbQE=", + "similarityId": "2067045827", + "status": "RECURRENT", + "state": "TO_VERIFY", + "severity": "CRITICAL", + "created": "2025-04-11T10:20:15Z", + "firstFoundAt": "2024-07-22T14:05:10Z", + "foundAt": "2025-04-11T10:20:15Z", + "firstScanId": "7083ee4e-2eff-4e2f-9d98-1aae8023169f", + "description": "The application's method executes an SQL query with executeQuery, at line 74 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\n\nAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input userId; this input is then read by the method at line 56 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. This input then flows through the code, into a query and to the database server - without sanitization.\r\n\r\nThis may enable an SQL Injection attack.\n\n", + "descriptionHTML": "\u003cp\u003eThe application‘s method executes an SQL query with executeQuery, at line 74 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\u003c/p\u003e\n\n\u003cp\u003eAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input userId; this input is then read by the method at line 56 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. This input then flows through the code, into a query and to the database server - without sanitization.\u003c/p\u003e\n\n\u003cp\u003eThis may enable an SQL Injection attack.\u003c/p\u003e\n", + "data": { + "queryId": 14517067005933136034, + "queryName": "SQL_Injection", + "group": "Java_Critical_Risk", + "resultHash": "4/tlPdgnp5kS5PNqjnoXQagLbQE=", + "languageName": "Java", + "nodes": [ + { + "id": "h9+K/VCYy1hzaOuJGXAg7Q/2EGE=", + "line": 56, + "name": "userId", + "column": 75, + "length": 6, + "nodeID": 76692, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.completed.userId", + "methodLine": 56 + }, + { + "id": "QV2emb87v+UWl0+V/EIowjpHYuY=", + "line": 57, + "name": "userId", + "column": 28, + "length": 6, + "nodeID": 76690, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.completed.userId", + "methodLine": 56 + }, + { + "id": "8E8sNxUhnzn4/CcQ2zMi0zOfr/4=", + "line": 62, + "name": "accountName", + "column": 46, + "length": 11, + "nodeID": 77202, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.accountName", + "methodLine": 62 + }, + { + "id": "3a/7HgY2k0OJlt2CDKR+aVmb0vM=", + "line": 66, + "name": "accountName", + "column": 63, + "length": 11, + "nodeID": 76766, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.accountName", + "methodLine": 62 + }, + { + "id": "z5XE8sZNwVqn0L+7hHnlfBT5gZg=", + "line": 66, + "name": "query", + "column": 7, + "length": 5, + "nodeID": 76768, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.query", + "methodLine": 62 + }, + { + "id": "wFKklmrWDUKyZ973GslHu6IJ3l8=", + "line": 74, + "name": "query", + "column": 52, + "length": 5, + "nodeID": 76826, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.query", + "methodLine": 62 + }, + { + "id": "qRZ9lc8euTZVkXlHbHwO6kpHsoA=", + "line": 74, + "name": "executeQuery", + "column": 51, + "length": 1, + "nodeID": 76822, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.statement.executeQuery", + "methodLine": 62 + } + ] + }, + "comments": {}, + "vulnerabilityDetails": { + "cweId": 89, + "cvss": {}, + "compliances": [ + "ASD STIG 6.1", + "Base Preset", + "OWASP Mobile Top 10 2016", + "SANS top 25", + "PCI DSS v4.0", + "OWASP Mobile Top 10 2024", + "OWASP Top 10 2013", + "ASA Mobile Premium", + "ASA Premium", + "OWASP Top 10 API", + "PCI DSS v3.2.1", + "FISMA 2014", + "OWASP Top 10 2017", + "MOIS(KISA) Secure Coding 2021", + "OWASP ASVS", + "OWASP Top 10 2021", + "Top Tier", + "CWE top 25", + "NIST SP 800-53" + ] + } + } + ], + "totalCount": 75, + "scanID": "d121274d-3cb6-4bae-b02a-3eede9eac3d9" +} diff --git a/unittests/scans/checkmarx_one/two-false-positive.json b/unittests/scans/checkmarx_one/two-false-positive.json new file mode 100644 index 00000000000..ea46e916e58 --- /dev/null +++ b/unittests/scans/checkmarx_one/two-false-positive.json @@ -0,0 +1,238 @@ +{ + "results": [ + { + "type": "sast", + "label": "sast", + "id": "0qB0NWis13+GAUmerweOUDyZzmk=", + "similarityId": "-1781066682", + "status": "RECURRENT", + "state": "NOT_EXPLOITABLE", + "severity": "CRITICAL", + "created": "2025-04-11T10:20:15Z", + "firstFoundAt": "2024-07-22T14:05:10Z", + "foundAt": "2025-04-11T10:20:15Z", + "firstScanId": "7083ee4e-2eff-4e2f-9d98-1aae8023169f", + "description": "The application's method executes an SQL query with executeUpdate, at line 63 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\n\nAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input query; this input is then read by the method at line 53 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. This input then flows through the code, into a query and to the database server - without sanitization.\r\n\r\nThis may enable an SQL Injection attack.\n\n", + "descriptionHTML": "\u003cp\u003eThe application‘s method executes an SQL query with executeUpdate, at line 63 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\u003c/p\u003e\n\n\u003cp\u003eAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input query; this input is then read by the method at line 53 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java. This input then flows through the code, into a query and to the database server - without sanitization.\u003c/p\u003e\n\n\u003cp\u003eThis may enable an SQL Injection attack.\u003c/p\u003e\n", + "data": { + "queryId": 14517067005933136034, + "queryName": "SQL_Injection", + "group": "Java_Critical_Risk", + "resultHash": "0qB0NWis13+GAUmerweOUDyZzmk=", + "languageName": "Java", + "nodes": [ + { + "id": "0BQetI+3nx066RIjhL4RXixfgGs=", + "line": 53, + "name": "query", + "column": 54, + "length": 5, + "nodeID": 79013, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.completed.query", + "methodLine": 53 + }, + { + "id": "ZZtfb86lLjmfYOKSTKP04NigAnI=", + "line": 54, + "name": "query", + "column": 28, + "length": 5, + "nodeID": 79011, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.completed.query", + "methodLine": 53 + }, + { + "id": "34zDkUrPrO+2mIQJIxZOImmdHOE=", + "line": 57, + "name": "query", + "column": 49, + "length": 5, + "nodeID": 79379, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.query", + "methodLine": 57 + }, + { + "id": "Wv2DomzUnGITGzTxS9xYSIr4pL4=", + "line": 63, + "name": "query", + "column": 33, + "length": 5, + "nodeID": 79113, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.query", + "methodLine": 57 + }, + { + "id": "Wk8aYr9/jNUfhXn42U+ia7A0YVU=", + "line": 63, + "name": "executeUpdate", + "column": 32, + "length": 1, + "nodeID": 79109, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson3.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.introduction.SqlInjectionLesson3.injectableQuery.statement.executeUpdate", + "methodLine": 57 + } + ] + }, + "comments": {}, + "vulnerabilityDetails": { + "cweId": 89, + "cvss": {}, + "compliances": [ + "ASD STIG 6.1", + "Base Preset", + "OWASP Mobile Top 10 2016", + "SANS top 25", + "PCI DSS v4.0", + "OWASP Mobile Top 10 2024", + "OWASP Top 10 2013", + "ASA Mobile Premium", + "ASA Premium", + "OWASP Top 10 API", + "PCI DSS v3.2.1", + "FISMA 2014", + "OWASP Top 10 2017", + "MOIS(KISA) Secure Coding 2021", + "OWASP ASVS", + "OWASP Top 10 2021", + "Top Tier", + "CWE top 25", + "NIST SP 800-53" + ] + } + }, + { + "type": "sast", + "label": "sast", + "id": "4/tlPdgnp5kS5PNqjnoXQagLbQE=", + "similarityId": "2067045827", + "status": "RECURRENT", + "state": "NOT_EXPLOITABLE", + "severity": "CRITICAL", + "created": "2025-04-11T10:20:15Z", + "firstFoundAt": "2024-07-22T14:05:10Z", + "foundAt": "2025-04-11T10:20:15Z", + "firstScanId": "7083ee4e-2eff-4e2f-9d98-1aae8023169f", + "description": "The application's method executes an SQL query with executeQuery, at line 74 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\n\nAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input userId; this input is then read by the method at line 56 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. This input then flows through the code, into a query and to the database server - without sanitization.\r\n\r\nThis may enable an SQL Injection attack.\n\n", + "descriptionHTML": "\u003cp\u003eThe application‘s method executes an SQL query with executeQuery, at line 74 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. The application constructs this SQL query by embedding an untrusted string into the query without proper sanitization. The concatenated string is submitted to the database, where it is parsed and executed accordingly.\u003c/p\u003e\n\n\u003cp\u003eAn attacker would be able to inject arbitrary syntax and data into the SQL query, by crafting a malicious payload and providing it via the input userId; this input is then read by the method at line 56 of /src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java. This input then flows through the code, into a query and to the database server - without sanitization.\u003c/p\u003e\n\n\u003cp\u003eThis may enable an SQL Injection attack.\u003c/p\u003e\n", + "data": { + "queryId": 14517067005933136034, + "queryName": "SQL_Injection", + "group": "Java_Critical_Risk", + "resultHash": "4/tlPdgnp5kS5PNqjnoXQagLbQE=", + "languageName": "Java", + "nodes": [ + { + "id": "h9+K/VCYy1hzaOuJGXAg7Q/2EGE=", + "line": 56, + "name": "userId", + "column": 75, + "length": 6, + "nodeID": 76692, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.completed.userId", + "methodLine": 56 + }, + { + "id": "QV2emb87v+UWl0+V/EIowjpHYuY=", + "line": 57, + "name": "userId", + "column": 28, + "length": 6, + "nodeID": 76690, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.completed.userId", + "methodLine": 56 + }, + { + "id": "8E8sNxUhnzn4/CcQ2zMi0zOfr/4=", + "line": 62, + "name": "accountName", + "column": 46, + "length": 11, + "nodeID": 77202, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.accountName", + "methodLine": 62 + }, + { + "id": "3a/7HgY2k0OJlt2CDKR+aVmb0vM=", + "line": 66, + "name": "accountName", + "column": 63, + "length": 11, + "nodeID": 76766, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.accountName", + "methodLine": 62 + }, + { + "id": "z5XE8sZNwVqn0L+7hHnlfBT5gZg=", + "line": 66, + "name": "query", + "column": 7, + "length": 5, + "nodeID": 76768, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.query", + "methodLine": 62 + }, + { + "id": "wFKklmrWDUKyZ973GslHu6IJ3l8=", + "line": 74, + "name": "query", + "column": 52, + "length": 5, + "nodeID": 76826, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.query", + "methodLine": 62 + }, + { + "id": "qRZ9lc8euTZVkXlHbHwO6kpHsoA=", + "line": 74, + "name": "executeQuery", + "column": 51, + "length": 1, + "nodeID": 76822, + "fileName": "/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionLesson6a.java", + "fullName": "org.owasp.webgoat.lessons.sqlinjection.advanced.SqlInjectionLesson6a.injectableQuery.statement.executeQuery", + "methodLine": 62 + } + ] + }, + "comments": {}, + "vulnerabilityDetails": { + "cweId": 89, + "cvss": {}, + "compliances": [ + "ASD STIG 6.1", + "Base Preset", + "OWASP Mobile Top 10 2016", + "SANS top 25", + "PCI DSS v4.0", + "OWASP Mobile Top 10 2024", + "OWASP Top 10 2013", + "ASA Mobile Premium", + "ASA Premium", + "OWASP Top 10 API", + "PCI DSS v3.2.1", + "FISMA 2014", + "OWASP Top 10 2017", + "MOIS(KISA) Secure Coding 2021", + "OWASP ASVS", + "OWASP Top 10 2021", + "Top Tier", + "CWE top 25", + "NIST SP 800-53" + ] + } + } + ], + "totalCount": 75, + "scanID": "d121274d-3cb6-4bae-b02a-3eede9eac3d9" +} diff --git a/unittests/test_import_reimport.py b/unittests/test_import_reimport.py index a3c27375bc3..2c4316d313b 100644 --- a/unittests/test_import_reimport.py +++ b/unittests/test_import_reimport.py @@ -102,6 +102,10 @@ def __init__(self, *args, **kwargs): self.anchore_grype_file_name = get_unit_tests_scans_path("anchore_grype") / "check_all_fields.json" self.anchore_grype_scan_type = "Anchore Grype" + self.checkmarx_one_open_and_false_positive = get_unit_tests_scans_path("checkmarx_one") / "one-open-one-false-positive.json" + self.checkmarx_one_two_false_positive = get_unit_tests_scans_path("checkmarx_one") / "two-false-positive.json" + self.scan_type_checkmarx_one = "Checkmarx One Scan" + # import zap scan, testing: # - import # - active/verifed = True @@ -1481,6 +1485,22 @@ def test_dynamic_parsing_field_set_to_false(self): test = Test.objects.get(id=test_id) self.assertFalse(test.test_type.dynamically_generated) + def test_false_positive_status_applied_after_reimport(self): + # Test that checkmarx one with a file that has one open finding, and one false positive finding + import0 = self.import_scan_with_params(self.checkmarx_one_open_and_false_positive, scan_type=self.scan_type_checkmarx_one, active=None, verified=None) + test_id = import0["test"] + active_finding_before = self.get_test_findings_api(test_id, active=True) + false_p_finding_before = self.get_test_findings_api(test_id, false_p=True) + # Make sure we get the expeceted results + self.assertEqual(1, active_finding_before.get("count", 0)) + self.assertEqual(1, false_p_finding_before.get("count", 0)) + # reimport the next report that sets the active finding to false positive + self.reimport_scan_with_params(test_id, self.checkmarx_one_two_false_positive, scan_type=self.scan_type_checkmarx_one) + active_finding_after = self.get_test_findings_api(test_id, active=True) + false_p_finding_after = self.get_test_findings_api(test_id, false_p=True) + # Make sure we get the expeceted results + self.assertEqual(0, active_finding_after.get("count", 0)) + self.assertEqual(2, false_p_finding_after.get("count", 0)) class ImportReimportTestAPI(DojoAPITestCase, ImportReimportMixin): fixtures = ["dojo_testdata.json"] @@ -1814,13 +1834,13 @@ def import_scan_with_params_ui(self, filename, scan_type="ZAP Scan", engagement= activePayload = "not_specified" if force_active: activePayload = "force_to_true" - elif not active: + elif active is False: activePayload = "force_to_false" verifiedPayload = "not_specified" if force_verified: verifiedPayload = "force_to_true" - elif not verified: + elif verified is False: verifiedPayload = "force_to_false" with Path(filename).open(encoding="utf-8") as testfile: diff --git a/unittests/tools/test_checkmarx_one_parser.py b/unittests/tools/test_checkmarx_one_parser.py index 0039c09db11..881448f4497 100644 --- a/unittests/tools/test_checkmarx_one_parser.py +++ b/unittests/tools/test_checkmarx_one_parser.py @@ -153,3 +153,15 @@ def test_sca_finding(finding): sast_finding = findings[124] self.maxDiff = None test_sast_finding(sast_finding) + + def test_checkmarx_one_false_positive_status(self): + with (get_unit_tests_scans_path("checkmarx_one") / "one-open-one-false-positive.json").open(encoding="utf-8") as testfile: + parser = CheckmarxOneParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(2, len(findings)) + # check the first first finding is false positive + self.assertEqual(True, findings[0].false_p) + self.assertEqual(False, findings[0].active) + # check the first second finding is not false positive + self.assertEqual(False, findings[1].false_p) + self.assertEqual(True, findings[1].active) From 84b90bea7d413dc2ea3bd842f966a2d967d04f58 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:26:16 -0600 Subject: [PATCH 2/4] Fixing ruff --- dojo/tools/checkmarx_one/parser.py | 10 +++++----- unittests/dojo_test_case.py | 2 +- unittests/test_import_reimport.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dojo/tools/checkmarx_one/parser.py b/dojo/tools/checkmarx_one/parser.py index 678477c703d..4a025f8310b 100644 --- a/dojo/tools/checkmarx_one/parser.py +++ b/dojo/tools/checkmarx_one/parser.py @@ -339,12 +339,12 @@ def determine_state(self, data: dict) -> dict: Determine the state of the findings as set by Checkmarx One docs https://docs.checkmarx.com/en/34965-68516-managing--triaging--vulnerabilities0.html#UUID-bc2397a3-1614-48bc-ff2f-1bc342071c5a_UUID-ad4991d6-161f-f76e-7d04-970f158eff9b """ - state = data.get("state") + state = data.get("state") return { - "active": state in ["TO_VERIFY", "PROPOSED_NOT_EXPLOITABLE", "CONFIRMED", "URGENT"], - "verified": state in ["NOT_EXPLOITABLE", "CONFIRMED", "URGENT"], - "false_p": state in ["NOT_EXPLOITABLE"], + "active": state in {"TO_VERIFY", "PROPOSED_NOT_EXPLOITABLE", "CONFIRMED", "URGENT"}, + "verified": state in {"NOT_EXPLOITABLE", "CONFIRMED", "URGENT"}, + "false_p": state == "NOT_EXPLOITABLE", # These are not managed by checkmarx one, but is nice to explicitly set them "duplicate": False, "out_of_scope": False, - } \ No newline at end of file + } diff --git a/unittests/dojo_test_case.py b/unittests/dojo_test_case.py index a0f5b9ec230..c1f3d462afe 100644 --- a/unittests/dojo_test_case.py +++ b/unittests/dojo_test_case.py @@ -521,7 +521,7 @@ def import_scan_with_params(self, filename, scan_type="ZAP Scan", engagement=1, if active is not None: payload["active"] = active - + if verified is not None: payload["verified"] = verified diff --git a/unittests/test_import_reimport.py b/unittests/test_import_reimport.py index 2c4316d313b..ee933550353 100644 --- a/unittests/test_import_reimport.py +++ b/unittests/test_import_reimport.py @@ -1502,6 +1502,7 @@ def test_false_positive_status_applied_after_reimport(self): self.assertEqual(0, active_finding_after.get("count", 0)) self.assertEqual(2, false_p_finding_after.get("count", 0)) + class ImportReimportTestAPI(DojoAPITestCase, ImportReimportMixin): fixtures = ["dojo_testdata.json"] From 86dbba6e0123f3a06cc52709b4893248836bf234 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:02:15 -0600 Subject: [PATCH 3/4] Update unittests/tools/test_checkmarx_one_parser.py Co-authored-by: Charles Neill <1749665+cneill@users.noreply.github.com> --- unittests/tools/test_checkmarx_one_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unittests/tools/test_checkmarx_one_parser.py b/unittests/tools/test_checkmarx_one_parser.py index 881448f4497..e8896fed116 100644 --- a/unittests/tools/test_checkmarx_one_parser.py +++ b/unittests/tools/test_checkmarx_one_parser.py @@ -159,9 +159,9 @@ def test_checkmarx_one_false_positive_status(self): parser = CheckmarxOneParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(2, len(findings)) - # check the first first finding is false positive + # check the first finding is false positive self.assertEqual(True, findings[0].false_p) self.assertEqual(False, findings[0].active) - # check the first second finding is not false positive + # check the second finding is not false positive self.assertEqual(False, findings[1].false_p) self.assertEqual(True, findings[1].active) From 8e0fb7d1500b39c7bda9cce134aaed5b9373bd7b Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:03:48 -0600 Subject: [PATCH 4/4] Use the correct dict for statuses --- dojo/tools/checkmarx_one/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/tools/checkmarx_one/parser.py b/dojo/tools/checkmarx_one/parser.py index 4a025f8310b..0e25631e0c7 100644 --- a/dojo/tools/checkmarx_one/parser.py +++ b/dojo/tools/checkmarx_one/parser.py @@ -91,7 +91,7 @@ def parse_iac_vulnerabilities( f"**Actual Value**: {instance.get('actualValue')}\n" f"**Expected Value**: {instance.get('expectedValue')}\n" ), - **base_finding_details, + **instance_details, ) # Add some details to the description finding.description += (