diff --git a/CheckmarxPythonSDK/CxOne/aiTriageAPI.py b/CheckmarxPythonSDK/CxOne/aiTriageAPI.py index a7ef88a..e524a3a 100644 --- a/CheckmarxPythonSDK/CxOne/aiTriageAPI.py +++ b/CheckmarxPythonSDK/CxOne/aiTriageAPI.py @@ -1,4 +1,5 @@ import json +from urllib.parse import quote from CheckmarxPythonSDK.api_client import ApiClient from CheckmarxPythonSDK.CxOne.config import construct_configuration @@ -109,19 +110,22 @@ def retrieve_ai_triage_results( The recommended way to obtain group_id is to call GET /api/risks for the specified project and use the groupId field of the corresponding - risk object. If group_id contains reserved URL characters (e.g. #), - URL-encode the value before passing it (replace # with %23). + risk object. group_id may contain reserved URL characters (e.g. #, + as in an SCA group id like "CVE-2015-4852#-#Maven-commons-collections: + commons-collections-3.2.1#-#2db0b158-3068-4d7e-86e6-0d922f22a69c"); + this method URL-encodes it before using it as a path parameter, so + callers should pass the raw value. Args: project_id (str): Unique identifier of the project. - group_id (str): Identifier of the vulnerability group. URL-encode - if the value contains reserved characters. + group_id (str): Identifier of the vulnerability group, as + returned by the API (no need to pre-encode it). Returns: AiTriageResult: Triage verdict, summary, confidence score, reachability, exploitability, and supporting analysis details. """ - url = f"{self.base_url}/triage/{project_id}/{group_id}" + url = f"{self.base_url}/triage/{project_id}/{quote(str(group_id), safe='')}" response = self.api_client.call_api( method="GET", url=url, @@ -180,13 +184,16 @@ def retrieve_ai_triage_results(project_id: str, group_id: str) -> AiTriageResult The recommended way to obtain group_id is to call GET /api/risks for the specified project and use the groupId field of the corresponding risk - object. If group_id contains reserved URL characters (e.g. #), URL-encode - the value before passing it (replace # with %23). + object. group_id may contain reserved URL characters (e.g. #, as in an + SCA group id like "CVE-2015-4852#-#Maven-commons-collections:commons- + collections-3.2.1#-#2db0b158-3068-4d7e-86e6-0d922f22a69c"); this function + URL-encodes it before using it as a path parameter, so callers should + pass the raw value. Args: project_id (str): Unique identifier of the project. - group_id (str): Identifier of the vulnerability group. URL-encode if - the value contains reserved characters. + group_id (str): Identifier of the vulnerability group, as returned + by the API (no need to pre-encode it). Returns: AiTriageResult: Triage verdict, summary, confidence score, diff --git a/tests/CxOne/test_ai_triage_and_remediation_api.py b/tests/CxOne/test_ai_triage_and_remediation_api.py index 9986c49..a894ef6 100644 --- a/tests/CxOne/test_ai_triage_and_remediation_api.py +++ b/tests/CxOne/test_ai_triage_and_remediation_api.py @@ -85,9 +85,10 @@ def triage_status(project_id, sql_injection_result): Used by remediation tests to skip when the result is already determined to be non-exploitable, avoiding unnecessary credit consumption. """ - group_id = quote(str(sql_injection_result.similarity_id), safe="") try: - result = retrieve_ai_triage_results(project_id=project_id, group_id=group_id) + result = retrieve_ai_triage_results( + project_id=project_id, group_id=sql_injection_result.similarity_id + ) return result.triageStatus except Exception: return None @@ -138,11 +139,10 @@ def test_get_ai_triage_status(project_id, scan_id, sql_injection_result): def test_retrieve_ai_triage_results(project_id, scan_id, sql_injection_result): - group_id = quote(str(sql_injection_result.similarity_id), safe="") try: triage_result = retrieve_ai_triage_results( project_id=project_id, - group_id=group_id, + group_id=sql_injection_result.similarity_id, ) except Exception as e: msg = str(e) diff --git a/tests/CxOne/test_ai_triage_api_unit.py b/tests/CxOne/test_ai_triage_api_unit.py new file mode 100644 index 0000000..952383a --- /dev/null +++ b/tests/CxOne/test_ai_triage_api_unit.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock + +from CheckmarxPythonSDK.CxOne.aiTriageAPI import AiTriageAPI + + +def _api_client(json_body=None): + client = MagicMock() + client.configuration.server_base_url = "https://example.com" + response = MagicMock() + response.json.return_value = json_body or {} + client.call_api.return_value = response + return client + + +class TestRetrieveAiTriageResults: + + def test_group_id_with_hash_characters_is_url_encoded(self): + """group_id values like SCA group ids ("CVE-...#-#pkg#-#uuid") must be + percent-encoded before being substituted into the path, otherwise the + '#' is treated as a URL fragment separator and the request 404s. + """ + client = _api_client() + api = AiTriageAPI(api_client=client) + group_id = ( + "CVE-2015-4852#-#Maven-commons-collections:commons-collections-" + "3.2.1#-#2db0b158-3068-4d7e-86e6-0d922f22a69c" + ) + + api.retrieve_ai_triage_results(project_id="proj-1", group_id=group_id) + + called_url = client.call_api.call_args.kwargs["url"] + assert "#" not in called_url + assert called_url == ( + "https://example.com/api/ai-triage/triage/proj-1/" + "CVE-2015-4852%23-%23Maven-commons-collections%3Acommons-" + "collections-3.2.1%23-%232db0b158-3068-4d7e-86e6-0d922f22a69c" + ) + + def test_group_id_without_reserved_characters_is_unchanged(self): + client = _api_client() + api = AiTriageAPI(api_client=client) + + api.retrieve_ai_triage_results(project_id="proj-1", group_id="12345") + + called_url = client.call_api.call_args.kwargs["url"] + assert called_url == "https://example.com/api/ai-triage/triage/proj-1/12345" + + def test_negative_integer_group_id_is_unchanged(self): + """SAST similarity_id values can be negative integers (e.g. -501015144). + '-' is an unreserved URL character, so quote() must leave it as-is + rather than encoding it. + """ + client = _api_client() + api = AiTriageAPI(api_client=client) + + api.retrieve_ai_triage_results(project_id="proj-1", group_id=-501015144) + + called_url = client.call_api.call_args.kwargs["url"] + assert called_url == "https://example.com/api/ai-triage/triage/proj-1/-501015144"