From 6f85578e409327e13c493ee928220334d3fcd1ec Mon Sep 17 00:00:00 2001 From: happy yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:23:44 +0800 Subject: [PATCH 1/2] Fix 404 on AI Triage results by URL-encoding group_id retrieve_ai_triage_results() built the request path as "{base_url}/triage/{project_id}/{group_id}" without encoding group_id. SCA group ids can contain reserved URL characters, e.g. "CVE-2015-4852#-#Maven-commons-collections:commons-collections-3.2.1#-#", and '#' starts a URL fragment, so the server received a truncated path and returned 404. group_id is now percent-encoded with urllib.parse.quote before being substituted into the path, so callers pass the raw value instead of having to pre-encode it themselves. Updated the existing integration tests (which previously pre-encoded group_id, which would now double- encode it) and added unit tests covering the encoding behavior. --- CheckmarxPythonSDK/CxOne/aiTriageAPI.py | 25 ++++++---- .../test_ai_triage_and_remediation_api.py | 8 ++-- tests/CxOne/test_ai_triage_api_unit.py | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 tests/CxOne/test_ai_triage_api_unit.py 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..eb4dfbf --- /dev/null +++ b/tests/CxOne/test_ai_triage_api_unit.py @@ -0,0 +1,46 @@ +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" From 025a0de314974c7a69d0eae0a54dce04054a4673 Mon Sep 17 00:00:00 2001 From: happy yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:49:25 +0800 Subject: [PATCH 2/2] Add unit test for negative-integer group_id Covers the SAST case reported with a negative similarity_id (e.g. -501015144): '-' is an unreserved URL character, so quote() must leave it unchanged rather than encoding it. Verified end-to-end against a live tenant: - SCA case from the original report (CVE-...#-#...#-#...): 200 OK, was 404 - SAST positive similarity_id (existing integration test): PASSED - SAST negative similarity_id (-501015144): 200 OK, unchanged --- tests/CxOne/test_ai_triage_api_unit.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/CxOne/test_ai_triage_api_unit.py b/tests/CxOne/test_ai_triage_api_unit.py index eb4dfbf..952383a 100644 --- a/tests/CxOne/test_ai_triage_api_unit.py +++ b/tests/CxOne/test_ai_triage_api_unit.py @@ -44,3 +44,16 @@ def test_group_id_without_reserved_characters_is_unchanged(self): 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"