From ee690b5ccc474aabdad63cfd3a22e08ecf2df6dc Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:51:13 +0800 Subject: [PATCH 01/25] Remove presetManagerAPI and update .gitignore Co-Authored-By: Claude Opus 4.7 --- .gitignore | 4 +- CheckmarxPythonSDK/CxOne/presetManagerAPI.py | 261 ------------------- tests/CxOne/test_preset_manager_api.py | 122 --------- 3 files changed, 3 insertions(+), 384 deletions(-) delete mode 100644 CheckmarxPythonSDK/CxOne/presetManagerAPI.py delete mode 100644 tests/CxOne/test_preset_manager_api.py diff --git a/.gitignore b/.gitignore index 31a157ad..d964dcaa 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,6 @@ build *.crt *.csv -.claude \ No newline at end of file +.claude +*.txt +*.json \ No newline at end of file diff --git a/CheckmarxPythonSDK/CxOne/presetManagerAPI.py b/CheckmarxPythonSDK/CxOne/presetManagerAPI.py deleted file mode 100644 index 25be3a47..00000000 --- a/CheckmarxPythonSDK/CxOne/presetManagerAPI.py +++ /dev/null @@ -1,261 +0,0 @@ -from CheckmarxPythonSDK.api_client import ApiClient -from CheckmarxPythonSDK.CxOne.config import construct_configuration -from typing import List - - -class PresetManagerAPI(object): - - def __init__(self, api_client: ApiClient = None): - if api_client is None: - configuration = construct_configuration() - api_client = ApiClient(configuration=configuration) - self.api_client = api_client - self.base_url = ( - f"{self.api_client.configuration.server_base_url}" - f"/api/preset-manager" - ) - - def get_scanner_presets( - self, - scanner: str, - limit: int = 10, - offset: int = 0, - fields: List[str] = None, - sort: str = None, - include_details: bool = False, - search_term: str = None, - exact_match: bool = False, - ) -> dict: - """ - List presets for a scanner. - - Args: - scanner (str): 'sast' or 'iac' - limit (int): Max results (1-100). Default: 10 - offset (int): Items to skip. Default: 0 - fields (List[str]): Fields to include - sort (str): Sort expression (e.g. '-description') - include_details (bool): Include detailed info. Default: False - search_term (str): Filter by search term - exact_match (bool): Require exact match. Default: False - - Returns: - dict with totalFilteredCount, totalCount, presets - """ - url = f"{self.base_url}/{scanner}/presets" - params = { - "limit": limit, - "offset": offset, - "fields": fields, - "sort": sort, - "include-details": include_details, - "search-term": search_term, - "exact-match": exact_match, - } - response = self.api_client.call_api( - method="GET", url=url, params=params - ) - return response.json() - - def get_scanner_preset_by_id(self, scanner: str, id) -> dict: - """ - Get a preset by ID. - - Args: - scanner (str): 'sast' or 'iac' - id (int or str): Preset identifier - - Returns: - dict with id, name, description, custom, queries - """ - url = f"{self.base_url}/{scanner}/presets/{id}" - response = self.api_client.call_api(method="GET", url=url) - return response.json() - - def create_preset(self, scanner: str, name: str, queries: List[dict], - description: str = None) -> dict: - """ - Create a new preset. - - Args: - scanner (str): 'sast' or 'iac' - name (str): Preset name (max 30 chars) - queries (List[dict]): List of QueriesByFamily dicts - with familyName, totalCount, queryIds - description (str): Optional description - - Returns: - dict with id and message - """ - url = f"{self.base_url}/{scanner}/presets" - body = {"name": name, "queries": queries} - if description: - body["description"] = description - response = self.api_client.call_api( - method="POST", url=url, json=body - ) - return response.json() - - def update_preset(self, scanner: str, id, name: str, - queries: List[dict], description: str = None) -> dict: - """ - Update a preset. - - Args: - scanner (str): 'sast' or 'iac' - id (int or str): Preset identifier - name (str): Preset name (max 30 chars) - queries (List[dict]): List of QueriesByFamily dicts - description (str): Optional description - - Returns: - dict with id and message - """ - url = f"{self.base_url}/{scanner}/presets/{id}" - body = {"name": name, "queries": queries} - if description: - body["description"] = description - response = self.api_client.call_api( - method="PUT", url=url, json=body - ) - return response.json() - - def delete_preset(self, scanner: str, id) -> bool: - """ - Delete a preset by ID. - - Args: - scanner (str): 'sast' or 'iac' - id (int or str): Preset identifier - - Returns: - bool - """ - url = f"{self.base_url}/{scanner}/presets/{id}" - response = self.api_client.call_api(method="DELETE", url=url) - return response.status_code == 200 - - def clone_preset(self, scanner: str, id, name: str, - description: str = None) -> dict: - """ - Clone a preset. - - Args: - scanner (str): 'sast' or 'iac' - id (int or str): Preset identifier to clone - name (str): Name for the cloned preset (max 30 chars) - description (str): Optional description (max 60 chars) - - Returns: - dict with id and message - """ - url = f"{self.base_url}/{scanner}/presets/{id}/clone" - body = {"name": name} - if description: - body["description"] = description - response = self.api_client.call_api( - method="POST", url=url, json=body - ) - return response.json() - - def get_query_families(self, scanner: str, - search_term: str = None) -> List[str]: - """ - List the available query families for a scanner. - - Args: - scanner (str): 'sast' or 'iac' - search_term (str): Filter by search term - - Returns: - list of str - """ - url = f"{self.base_url}/{scanner}/query-families" - params = {"search-term": search_term} - response = self.api_client.call_api( - method="GET", url=url, params=params - ) - return response.json() - - def get_queries_by_family(self, scanner: str, query_family: str, - search_term: str = None) -> List[dict]: - """ - List the queries of a given family. - - Args: - scanner (str): 'sast' or 'iac' - query_family (str): Query family name (e.g. 'Apex') - search_term (str): Filter by search term - - Returns: - list of QueriesTree dicts - """ - url = f"{self.base_url}/{scanner}/query-families/{query_family}/queries" - params = {"search-term": search_term} - response = self.api_client.call_api( - method="GET", url=url, params=params - ) - return response.json() - - -# ---- Module-level convenience functions ---- - -def get_scanner_presets( - scanner: str, - limit: int = 10, - offset: int = 0, - fields: List[str] = None, - sort: str = None, - include_details: bool = False, - search_term: str = None, - exact_match: bool = False, -) -> dict: - return PresetManagerAPI().get_scanner_presets( - scanner=scanner, limit=limit, offset=offset, fields=fields, - sort=sort, include_details=include_details, - search_term=search_term, exact_match=exact_match, - ) - - -def get_scanner_preset_by_id(scanner: str, id) -> dict: - return PresetManagerAPI().get_scanner_preset_by_id(scanner=scanner, id=id) - - -def create_scanner_preset(scanner: str, name: str, queries: List[dict], - description: str = None) -> dict: - return PresetManagerAPI().create_preset( - scanner=scanner, name=name, queries=queries, description=description, - ) - - -def update_scanner_preset(scanner: str, id, name: str, queries: List[dict], - description: str = None) -> dict: - return PresetManagerAPI().update_preset( - scanner=scanner, id=id, name=name, queries=queries, - description=description, - ) - - -def delete_scanner_preset(scanner: str, id) -> bool: - return PresetManagerAPI().delete_preset(scanner=scanner, id=id) - - -def clone_scanner_preset(scanner: str, id, name: str, - description: str = None) -> dict: - return PresetManagerAPI().clone_preset( - scanner=scanner, id=id, name=name, description=description, - ) - - -def get_scanner_query_families(scanner: str, search_term: str = None) -> List[str]: - return PresetManagerAPI().get_query_families( - scanner=scanner, search_term=search_term, - ) - - -def get_scanner_queries_by_family( - scanner: str, query_family: str, search_term: str = None -) -> List[dict]: - return PresetManagerAPI().get_queries_by_family( - scanner=scanner, query_family=query_family, search_term=search_term, - ) diff --git a/tests/CxOne/test_preset_manager_api.py b/tests/CxOne/test_preset_manager_api.py deleted file mode 100644 index e37a0fc9..00000000 --- a/tests/CxOne/test_preset_manager_api.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest - -from CheckmarxPythonSDK.CxOne import ( - get_scanner_presets, - get_scanner_preset_by_id, - create_scanner_preset, - delete_scanner_preset, - clone_scanner_preset, - get_scanner_query_families, - get_scanner_queries_by_family, -) - - -SCANNER = "sast" - - -def test_get_scanner_presets(): - result = get_scanner_presets(scanner=SCANNER, limit=5) - assert result is not None - assert "presets" in result - - -def test_get_scanner_preset_by_id(): - presets = get_scanner_presets(scanner=SCANNER, limit=1) - preset_list = presets.get("presets", []) - if not preset_list: - pytest.skip("No presets found") - preset_id = preset_list[0].get("id") - result = get_scanner_preset_by_id(scanner=SCANNER, id=preset_id) - assert result is not None - assert "queries" in result - - -def test_get_scanner_query_families(): - result = get_scanner_query_families(scanner=SCANNER) - assert result is not None - assert isinstance(result, list) - - -def test_get_scanner_queries_by_family(): - families = get_scanner_query_families(scanner=SCANNER) - if not families: - pytest.skip("No query families found") - result = get_scanner_queries_by_family( - scanner=SCANNER, query_family=families[0] - ) - assert result is not None - assert isinstance(result, list) - - -def test_create_and_delete_scanner_preset(): - families = get_scanner_query_families(scanner=SCANNER) - if not families: - pytest.skip("No query families found") - - queries = get_scanner_queries_by_family( - scanner=SCANNER, query_family=families[0] - ) - query_ids = [] - def collect_ids(items): - for item in items: - if item.get("isLeaf") and item.get("data"): - qid = item["data"].get("queryDescriptionId") - if qid: - query_ids.append(qid) - if item.get("children"): - collect_ids(item["children"]) - collect_ids(queries) - if not query_ids: - pytest.skip("No query IDs available") - - qid = str(query_ids[0]) - preset_name = "test-sdk-preset" - - # Clean up old test preset - existing = get_scanner_presets(scanner=SCANNER, search_term=preset_name, - exact_match=True, limit=10) - for p in existing.get("presets", []): - if p.get("name") == preset_name: - try: - delete_scanner_preset(scanner=SCANNER, id=p["id"]) - except Exception: - pass - - # Create - created = create_scanner_preset( - scanner=SCANNER, - name=preset_name, - description="SDK test preset", - queries=[{ - "familyName": families[0], - "totalCount": 1, - "queryIds": [qid], - }], - ) - assert created is not None - preset_id = created.get("id") - assert preset_id is not None - - # Delete - is_deleted = delete_scanner_preset(scanner=SCANNER, id=preset_id) - assert is_deleted is True - - -def test_clone_scanner_preset(): - presets = get_scanner_presets(scanner=SCANNER, limit=1) - preset_list = presets.get("presets", []) - if not preset_list: - pytest.skip("No presets found") - - preset_id = preset_list[0].get("id") - clone_name = "test-sdk-clone-{}".format( - __import__("datetime").datetime.now().strftime("%H%M%S") - ) - cloned = clone_scanner_preset( - scanner=SCANNER, id=preset_id, name=clone_name - ) - assert cloned is not None - assert "id" in cloned - - # Cleanup - delete_scanner_preset(scanner=SCANNER, id=cloned["id"]) From 6bda3005259e8382268ef0e9e87a0ac3e42f19ab Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:25:16 +0800 Subject: [PATCH 02/25] Rename SastPresetManagerAPI to PresetManagerAPI Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxOne/__init__.py | 4 ++-- ...PresetManagerAPI.py => presetManagerAPI.py} | 18 +++++++++--------- docs/CxOne_REST_API_List.md | 16 ++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) rename CheckmarxPythonSDK/CxOne/{sastPresetManagerAPI.py => presetManagerAPI.py} (94%) diff --git a/CheckmarxPythonSDK/CxOne/__init__.py b/CheckmarxPythonSDK/CxOne/__init__.py index 37782311..65a82522 100644 --- a/CheckmarxPythonSDK/CxOne/__init__.py +++ b/CheckmarxPythonSDK/CxOne/__init__.py @@ -367,8 +367,8 @@ get_tenant_projects_overview, get_project_counters, ) -from .sastPresetManagerAPI import ( - SastPresetManagerAPI, +from .presetManagerAPI import ( + PresetManagerAPI, get_sast_presets, get_sast_preset_by_id, create_sast_preset, diff --git a/CheckmarxPythonSDK/CxOne/sastPresetManagerAPI.py b/CheckmarxPythonSDK/CxOne/presetManagerAPI.py similarity index 94% rename from CheckmarxPythonSDK/CxOne/sastPresetManagerAPI.py rename to CheckmarxPythonSDK/CxOne/presetManagerAPI.py index ded90cd1..44e260c3 100644 --- a/CheckmarxPythonSDK/CxOne/sastPresetManagerAPI.py +++ b/CheckmarxPythonSDK/CxOne/presetManagerAPI.py @@ -3,7 +3,7 @@ from typing import List -class SastPresetManagerAPI(object): +class PresetManagerAPI(object): def __init__(self, api_client: ApiClient = None): if api_client is None: @@ -210,7 +210,7 @@ def get_sast_presets( search_term: str = None, exact_match: bool = False, ) -> dict: - return SastPresetManagerAPI().get_sast_presets( + return PresetManagerAPI().get_sast_presets( scanner=scanner, limit=limit, offset=offset, fields=fields, sort=sort, include_details=include_details, search_term=search_term, exact_match=exact_match, @@ -218,37 +218,37 @@ def get_sast_presets( def get_sast_preset_by_id(scanner: str, id) -> dict: - return SastPresetManagerAPI().get_sast_preset_by_id(scanner=scanner, id=id) + return PresetManagerAPI().get_sast_preset_by_id(scanner=scanner, id=id) def create_sast_preset(scanner: str, name: str, queries: List[dict], description: str = None) -> dict: - return SastPresetManagerAPI().create_preset( + return PresetManagerAPI().create_preset( scanner=scanner, name=name, queries=queries, description=description, ) def update_sast_preset(scanner: str, id, name: str, queries: List[dict], description: str = None) -> dict: - return SastPresetManagerAPI().update_preset( + return PresetManagerAPI().update_preset( scanner=scanner, id=id, name=name, queries=queries, description=description, ) def delete_sast_preset(scanner: str, id) -> bool: - return SastPresetManagerAPI().delete_preset(scanner=scanner, id=id) + return PresetManagerAPI().delete_preset(scanner=scanner, id=id) def clone_sast_preset(scanner: str, id, name: str, description: str = None) -> dict: - return SastPresetManagerAPI().clone_preset( + return PresetManagerAPI().clone_preset( scanner=scanner, id=id, name=name, description=description, ) def get_sast_query_families(scanner: str, search_term: str = None) -> List[str]: - return SastPresetManagerAPI().get_query_families( + return PresetManagerAPI().get_query_families( scanner=scanner, search_term=search_term, ) @@ -256,6 +256,6 @@ def get_sast_query_families(scanner: str, search_term: str = None) -> List[str]: def get_sast_queries_by_family( scanner: str, query_family: str, search_term: str = None ) -> List[dict]: - return SastPresetManagerAPI().get_queries_by_family( + return PresetManagerAPI().get_queries_by_family( scanner=scanner, query_family=query_family, search_term=search_term, ) diff --git a/docs/CxOne_REST_API_List.md b/docs/CxOne_REST_API_List.md index 7d8d278d..2fa1eabe 100644 --- a/docs/CxOne_REST_API_List.md +++ b/docs/CxOne_REST_API_List.md @@ -170,14 +170,14 @@ | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `list_organizational_domains` | GET | `/api/organizational-domains` | | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `add_organizational_domains` | POST | `/api/organizational-domains` | | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `delete_organizational_domain` | DELETE | `/api/organizational-domains/{id}` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `get_sast_presets` | GET | `/api/preset-manager/{scanner}/presets` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `get_sast_preset_by_id` | GET | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `create_sast_preset` | POST | `/api/preset-manager/{scanner}/presets` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `update_sast_preset` | PUT | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `delete_sast_preset` | DELETE | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `clone_sast_preset` | POST | `/api/preset-manager/{scanner}/presets/{id}/clone` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `get_sast_query_families` | GET | `/api/preset-manager/{scanner}/query-families` | -| PRESET_MANAGER.yaml | SastPresetManagerAPI | `get_sast_queries_by_family` | GET | `/api/preset-manager/{scanner}/query-families/{family}/queries` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_presets` | GET | `/api/preset-manager/{scanner}/presets` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_preset_by_id` | GET | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `create_sast_preset` | POST | `/api/preset-manager/{scanner}/presets` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `update_sast_preset` | PUT | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `delete_sast_preset` | DELETE | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `clone_sast_preset` | POST | `/api/preset-manager/{scanner}/presets/{id}/clone` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_query_families` | GET | `/api/preset-manager/{scanner}/query-families` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_queries_by_family` | GET | `/api/preset-manager/{scanner}/query-families/{family}/queries` | | PROJECTS.yaml | ProjectsAPI | `create_a_project` | POST | `/api/projects` | | PROJECTS.yaml | ProjectsAPI | `get_a_list_of_projects` | GET | `/api/projects` | | PROJECTS.yaml | ProjectsAPI | `get_a_project_by_id` | GET | `/api/projects/{id}` | From ba4d7dd4804767af80c506b825c272722b7518c8 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:38:54 +0800 Subject: [PATCH 03/25] Rename sast_ prefix to scanner_ in PresetManagerAPI Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxOne/__init__.py | 16 +++--- CheckmarxPythonSDK/CxOne/presetManagerAPI.py | 24 ++++----- docs/CxOne_REST_API_List.md | 16 +++--- tests/CxOne/test_sast_preset_manager_api.py | 56 ++++++++++---------- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/CheckmarxPythonSDK/CxOne/__init__.py b/CheckmarxPythonSDK/CxOne/__init__.py index 65a82522..54a27c34 100644 --- a/CheckmarxPythonSDK/CxOne/__init__.py +++ b/CheckmarxPythonSDK/CxOne/__init__.py @@ -369,14 +369,14 @@ ) from .presetManagerAPI import ( PresetManagerAPI, - get_sast_presets, - get_sast_preset_by_id, - create_sast_preset, - update_sast_preset, - delete_sast_preset, - clone_sast_preset, - get_sast_query_families, - get_sast_queries_by_family, + get_scanner_presets, + get_scanner_preset_by_id, + create_scanner_preset, + update_scanner_preset, + delete_scanner_preset, + clone_scanner_preset, + get_scanner_query_families, + get_scanner_queries_by_family, ) from .queryEditorAPI import ( QueryEditorAPI, diff --git a/CheckmarxPythonSDK/CxOne/presetManagerAPI.py b/CheckmarxPythonSDK/CxOne/presetManagerAPI.py index 44e260c3..25be3a47 100644 --- a/CheckmarxPythonSDK/CxOne/presetManagerAPI.py +++ b/CheckmarxPythonSDK/CxOne/presetManagerAPI.py @@ -15,7 +15,7 @@ def __init__(self, api_client: ApiClient = None): f"/api/preset-manager" ) - def get_sast_presets( + def get_scanner_presets( self, scanner: str, limit: int = 10, @@ -57,7 +57,7 @@ def get_sast_presets( ) return response.json() - def get_sast_preset_by_id(self, scanner: str, id) -> dict: + def get_scanner_preset_by_id(self, scanner: str, id) -> dict: """ Get a preset by ID. @@ -200,7 +200,7 @@ def get_queries_by_family(self, scanner: str, query_family: str, # ---- Module-level convenience functions ---- -def get_sast_presets( +def get_scanner_presets( scanner: str, limit: int = 10, offset: int = 0, @@ -210,25 +210,25 @@ def get_sast_presets( search_term: str = None, exact_match: bool = False, ) -> dict: - return PresetManagerAPI().get_sast_presets( + return PresetManagerAPI().get_scanner_presets( scanner=scanner, limit=limit, offset=offset, fields=fields, sort=sort, include_details=include_details, search_term=search_term, exact_match=exact_match, ) -def get_sast_preset_by_id(scanner: str, id) -> dict: - return PresetManagerAPI().get_sast_preset_by_id(scanner=scanner, id=id) +def get_scanner_preset_by_id(scanner: str, id) -> dict: + return PresetManagerAPI().get_scanner_preset_by_id(scanner=scanner, id=id) -def create_sast_preset(scanner: str, name: str, queries: List[dict], +def create_scanner_preset(scanner: str, name: str, queries: List[dict], description: str = None) -> dict: return PresetManagerAPI().create_preset( scanner=scanner, name=name, queries=queries, description=description, ) -def update_sast_preset(scanner: str, id, name: str, queries: List[dict], +def update_scanner_preset(scanner: str, id, name: str, queries: List[dict], description: str = None) -> dict: return PresetManagerAPI().update_preset( scanner=scanner, id=id, name=name, queries=queries, @@ -236,24 +236,24 @@ def update_sast_preset(scanner: str, id, name: str, queries: List[dict], ) -def delete_sast_preset(scanner: str, id) -> bool: +def delete_scanner_preset(scanner: str, id) -> bool: return PresetManagerAPI().delete_preset(scanner=scanner, id=id) -def clone_sast_preset(scanner: str, id, name: str, +def clone_scanner_preset(scanner: str, id, name: str, description: str = None) -> dict: return PresetManagerAPI().clone_preset( scanner=scanner, id=id, name=name, description=description, ) -def get_sast_query_families(scanner: str, search_term: str = None) -> List[str]: +def get_scanner_query_families(scanner: str, search_term: str = None) -> List[str]: return PresetManagerAPI().get_query_families( scanner=scanner, search_term=search_term, ) -def get_sast_queries_by_family( +def get_scanner_queries_by_family( scanner: str, query_family: str, search_term: str = None ) -> List[dict]: return PresetManagerAPI().get_queries_by_family( diff --git a/docs/CxOne_REST_API_List.md b/docs/CxOne_REST_API_List.md index 2fa1eabe..acf2a698 100644 --- a/docs/CxOne_REST_API_List.md +++ b/docs/CxOne_REST_API_List.md @@ -170,14 +170,14 @@ | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `list_organizational_domains` | GET | `/api/organizational-domains` | | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `add_organizational_domains` | POST | `/api/organizational-domains` | | ORGANIZATIONAL_DOMAINS.yaml | OrganizationalDomainsAPI | `delete_organizational_domain` | DELETE | `/api/organizational-domains/{id}` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_presets` | GET | `/api/preset-manager/{scanner}/presets` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_preset_by_id` | GET | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `create_sast_preset` | POST | `/api/preset-manager/{scanner}/presets` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `update_sast_preset` | PUT | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `delete_sast_preset` | DELETE | `/api/preset-manager/{scanner}/presets/{id}` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `clone_sast_preset` | POST | `/api/preset-manager/{scanner}/presets/{id}/clone` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_query_families` | GET | `/api/preset-manager/{scanner}/query-families` | -| PRESET_MANAGER.yaml | PresetManagerAPI | `get_sast_queries_by_family` | GET | `/api/preset-manager/{scanner}/query-families/{family}/queries` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_scanner_presets` | GET | `/api/preset-manager/{scanner}/presets` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_scanner_preset_by_id` | GET | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `create_scanner_preset` | POST | `/api/preset-manager/{scanner}/presets` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `update_scanner_preset` | PUT | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `delete_scanner_preset` | DELETE | `/api/preset-manager/{scanner}/presets/{id}` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `clone_scanner_preset` | POST | `/api/preset-manager/{scanner}/presets/{id}/clone` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_scanner_query_families` | GET | `/api/preset-manager/{scanner}/query-families` | +| PRESET_MANAGER.yaml | PresetManagerAPI | `get_scanner_queries_by_family` | GET | `/api/preset-manager/{scanner}/query-families/{family}/queries` | | PROJECTS.yaml | ProjectsAPI | `create_a_project` | POST | `/api/projects` | | PROJECTS.yaml | ProjectsAPI | `get_a_list_of_projects` | GET | `/api/projects` | | PROJECTS.yaml | ProjectsAPI | `get_a_project_by_id` | GET | `/api/projects/{id}` | diff --git a/tests/CxOne/test_sast_preset_manager_api.py b/tests/CxOne/test_sast_preset_manager_api.py index 0d46e1fe..e37a0fc9 100644 --- a/tests/CxOne/test_sast_preset_manager_api.py +++ b/tests/CxOne/test_sast_preset_manager_api.py @@ -1,59 +1,59 @@ import pytest from CheckmarxPythonSDK.CxOne import ( - get_sast_presets, - get_sast_preset_by_id, - create_sast_preset, - delete_sast_preset, - clone_sast_preset, - get_sast_query_families, - get_sast_queries_by_family, + get_scanner_presets, + get_scanner_preset_by_id, + create_scanner_preset, + delete_scanner_preset, + clone_scanner_preset, + get_scanner_query_families, + get_scanner_queries_by_family, ) SCANNER = "sast" -def test_get_sast_presets(): - result = get_sast_presets(scanner=SCANNER, limit=5) +def test_get_scanner_presets(): + result = get_scanner_presets(scanner=SCANNER, limit=5) assert result is not None assert "presets" in result -def test_get_sast_preset_by_id(): - presets = get_sast_presets(scanner=SCANNER, limit=1) +def test_get_scanner_preset_by_id(): + presets = get_scanner_presets(scanner=SCANNER, limit=1) preset_list = presets.get("presets", []) if not preset_list: pytest.skip("No presets found") preset_id = preset_list[0].get("id") - result = get_sast_preset_by_id(scanner=SCANNER, id=preset_id) + result = get_scanner_preset_by_id(scanner=SCANNER, id=preset_id) assert result is not None assert "queries" in result -def test_get_sast_query_families(): - result = get_sast_query_families(scanner=SCANNER) +def test_get_scanner_query_families(): + result = get_scanner_query_families(scanner=SCANNER) assert result is not None assert isinstance(result, list) -def test_get_sast_queries_by_family(): - families = get_sast_query_families(scanner=SCANNER) +def test_get_scanner_queries_by_family(): + families = get_scanner_query_families(scanner=SCANNER) if not families: pytest.skip("No query families found") - result = get_sast_queries_by_family( + result = get_scanner_queries_by_family( scanner=SCANNER, query_family=families[0] ) assert result is not None assert isinstance(result, list) -def test_create_and_delete_sast_preset(): - families = get_sast_query_families(scanner=SCANNER) +def test_create_and_delete_scanner_preset(): + families = get_scanner_query_families(scanner=SCANNER) if not families: pytest.skip("No query families found") - queries = get_sast_queries_by_family( + queries = get_scanner_queries_by_family( scanner=SCANNER, query_family=families[0] ) query_ids = [] @@ -73,17 +73,17 @@ def collect_ids(items): preset_name = "test-sdk-preset" # Clean up old test preset - existing = get_sast_presets(scanner=SCANNER, search_term=preset_name, + existing = get_scanner_presets(scanner=SCANNER, search_term=preset_name, exact_match=True, limit=10) for p in existing.get("presets", []): if p.get("name") == preset_name: try: - delete_sast_preset(scanner=SCANNER, id=p["id"]) + delete_scanner_preset(scanner=SCANNER, id=p["id"]) except Exception: pass # Create - created = create_sast_preset( + created = create_scanner_preset( scanner=SCANNER, name=preset_name, description="SDK test preset", @@ -98,12 +98,12 @@ def collect_ids(items): assert preset_id is not None # Delete - is_deleted = delete_sast_preset(scanner=SCANNER, id=preset_id) + is_deleted = delete_scanner_preset(scanner=SCANNER, id=preset_id) assert is_deleted is True -def test_clone_sast_preset(): - presets = get_sast_presets(scanner=SCANNER, limit=1) +def test_clone_scanner_preset(): + presets = get_scanner_presets(scanner=SCANNER, limit=1) preset_list = presets.get("presets", []) if not preset_list: pytest.skip("No presets found") @@ -112,11 +112,11 @@ def test_clone_sast_preset(): clone_name = "test-sdk-clone-{}".format( __import__("datetime").datetime.now().strftime("%H%M%S") ) - cloned = clone_sast_preset( + cloned = clone_scanner_preset( scanner=SCANNER, id=preset_id, name=clone_name ) assert cloned is not None assert "id" in cloned # Cleanup - delete_sast_preset(scanner=SCANNER, id=cloned["id"]) + delete_scanner_preset(scanner=SCANNER, id=cloned["id"]) From d4b5b27ca72117460a8a09d15a2ded5fadba221a Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:30:31 +0800 Subject: [PATCH 04/25] Rename test_sast_preset_manager_api to test_preset_manager_api Co-Authored-By: Claude Opus 4.7 --- ...test_sast_preset_manager_api.py => test_preset_manager_api.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/CxOne/{test_sast_preset_manager_api.py => test_preset_manager_api.py} (100%) diff --git a/tests/CxOne/test_sast_preset_manager_api.py b/tests/CxOne/test_preset_manager_api.py similarity index 100% rename from tests/CxOne/test_sast_preset_manager_api.py rename to tests/CxOne/test_preset_manager_api.py From bfb0ecacbf0c79c5387f2297498a69e802ea2162 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:10:56 +0800 Subject: [PATCH 05/25] Add missing API parameters and fix redirect handling - sastQueriesAPI: add scan_id/tenant_id to get_sast_query_description - sastResultsPredicatesAPI: add offset/limit to get_all_predicates_for_similarity_id - api_client: enable follow_redirects on httpx.Client for S3-based file retrieval Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxOne/sastQueriesAPI.py | 15 ++++++++++++--- .../CxOne/sastResultsPredicatesAPI.py | 10 ++++++++++ CheckmarxPythonSDK/api_client.py | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CheckmarxPythonSDK/CxOne/sastQueriesAPI.py b/CheckmarxPythonSDK/CxOne/sastQueriesAPI.py index a4affb78..c2c5060b 100644 --- a/CheckmarxPythonSDK/CxOne/sastQueriesAPI.py +++ b/CheckmarxPythonSDK/CxOne/sastQueriesAPI.py @@ -48,17 +48,22 @@ def get_sast_queries_presets(self) -> List[Preset]: ] def get_sast_query_description( - self, ids: List[str] + self, + ids: List[str], + scan_id: str = None, + tenant_id: str = None, ) -> List[QueryDescription]: """ Args: ids (List[str]): list of query ids. + scan_id (str): optional scan ID for context. + tenant_id (str): optional tenant ID. Returns: List[QueryDescription] """ url = f"{self.base_url}/descriptions" - params = {"ids": ids} + params = {"ids": ids, "scan-id": scan_id, "tenant-id": tenant_id} response = self.api_client.call_api( method="GET", url=url, params=params ) @@ -121,8 +126,12 @@ def get_sast_queries_presets() -> List[Preset]: def get_sast_query_description( ids: List[str], + scan_id: str = None, + tenant_id: str = None, ) -> List[QueryDescription]: - return SastQueriesAPI().get_sast_query_description(ids=ids) + return SastQueriesAPI().get_sast_query_description( + ids=ids, scan_id=scan_id, tenant_id=tenant_id, + ) def get_mapping_between_ast_and_sast_query_ids() -> List[dict]: diff --git a/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py b/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py index 023fdb63..88f8a9f1 100644 --- a/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py +++ b/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py @@ -22,6 +22,8 @@ def get_all_predicates_for_similarity_id( project_ids: List[str] = None, include_comment_json: bool = None, scan_id: str = None, + offset: int = 0, + limit: int = 100, ) -> dict: """ Args: @@ -29,6 +31,8 @@ def get_all_predicates_for_similarity_id( project_ids (list of str): include_comment_json (bool): scan_id (str): + offset (int): + limit (int): Returns: dict @@ -38,6 +42,8 @@ def get_all_predicates_for_similarity_id( "project-ids": project_ids, "include-comment-json": include_comment_json, "scan-id": scan_id, + "offset": offset, + "limit": limit, } response = self.api_client.call_api( method="GET", url=url, params=params @@ -245,12 +251,16 @@ def get_all_predicates_for_similarity_id( project_ids: List[str] = None, include_comment_json: bool = None, scan_id: str = None, + offset: int = 0, + limit: int = 100, ) -> dict: return SastResultsPredicatesAPI().get_all_predicates_for_similarity_id( similarity_id=similarity_id, project_ids=project_ids, include_comment_json=include_comment_json, scan_id=scan_id, + offset=offset, + limit=limit, ) diff --git a/CheckmarxPythonSDK/api_client.py b/CheckmarxPythonSDK/api_client.py index 1035ef37..9a3ab653 100644 --- a/CheckmarxPythonSDK/api_client.py +++ b/CheckmarxPythonSDK/api_client.py @@ -36,6 +36,7 @@ def create_session(configuration: Configuration) -> httpx.Client: cert=configuration.cert, proxy=configuration.proxy, transport=httpx.HTTPTransport(retries=3, verify=verify), + follow_redirects=True, headers={"User-Agent": f"checkmarx-python-sdk/{__version__}"}, ) From 200f97e4379684846d96e9c2468d6f3505909ebc Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:15:04 +0800 Subject: [PATCH 06/25] Add CxOne example scripts - update_project_preset: batch-update SAST preset for API projects from CSV - trigger_scm_project_scans_on_main_branch: trigger scans on main branch for SCM-integrated projects via repos-manager endpoint - export_sast_state_counts: export SAST state counts per project as CSV - export_sast_state_by_query: export SAST results by project, query, severity, language, and state for FP-rate analysis - get_project_sast_exclusions: get/update SAST recommended exclusions - get_audit_events: retrieve audit events Co-Authored-By: Claude Opus 4.7 --- examples/CxOne/export_sast_state_by_query.py | 190 ++++++++++++++++++ examples/CxOne/export_sast_state_counts.py | 145 +++++++++++++ examples/CxOne/get_audit_events.py | 168 ++++++++++++++++ examples/CxOne/get_project_sast_exclusions.py | 145 +++++++++++++ ...rigger_scm_project_scans_on_main_branch.py | 182 +++++++++++++++++ examples/CxOne/update_project_preset.py | 158 +++++++++++++++ 6 files changed, 988 insertions(+) create mode 100644 examples/CxOne/export_sast_state_by_query.py create mode 100644 examples/CxOne/export_sast_state_counts.py create mode 100644 examples/CxOne/get_audit_events.py create mode 100644 examples/CxOne/get_project_sast_exclusions.py create mode 100644 examples/CxOne/trigger_scm_project_scans_on_main_branch.py create mode 100644 examples/CxOne/update_project_preset.py diff --git a/examples/CxOne/export_sast_state_by_query.py b/examples/CxOne/export_sast_state_by_query.py new file mode 100644 index 00000000..5638449d --- /dev/null +++ b/examples/CxOne/export_sast_state_by_query.py @@ -0,0 +1,190 @@ +""" +Get the last scan on the main branch for every project, retrieve all SAST +results, and export counts broken down by project AND query. + +Output CSV headers: + project_name, query_name, severity, language, to_verify, confirmed, + urgent, not_exploitable, proposed_not_exploitable + +Secrets are read from a .env file in the project root. + +Usage: + python export_sast_state_by_query.py +""" + +import csv +import os +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.sastResultsAPI import SastResultsAPI + +OUTPUT_CSV = Path(__file__).resolve().parent / "sast_state_by_query_v5.csv" +STATES = ["TO_VERIFY", "CONFIRMED", "URGENT", + "NOT_EXPLOITABLE", "PROPOSED_NOT_EXPLOITABLE"] + + +def load_dotenv(): + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get( + "CXONE_IAM_URL", "https://sng.iam.checkmarx.net" + ) + server_base_url = os.environ.get( + "CXONE_SERVER_URL", "https://sng.ast.checkmarx.net" + ) + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + if not refresh_token: + grant_type = "client_credentials" + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def fetch_all_sast_results(sast_api: SastResultsAPI, scan_id: str): + """Paginate through all SAST results for a scan. Returns list of SastResult.""" + all_results = [] + offset = 0 + limit = 1000 + while True: + result = sast_api.get_sast_results_by_scan_id( + scan_id=scan_id, include_nodes=False, offset=offset, limit=limit, + ) + batch = result.get("results", []) + all_results.extend(batch) + total = result.get("totalCount", 0) + if offset + limit >= total: + break + offset += limit + return all_results + + +def main(): + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + sast_api = SastResultsAPI(api_client=api_client) + + all_projects = projects_api.get_all_projects() + print(f"Found {len(all_projects)} projects.") + + project_lookup = {p.id: p for p in all_projects} + project_ids = [p.id for p in all_projects] + + last_scans = projects_api.get_last_scan_info( + project_ids=project_ids, use_main_branch=True, limit=100, + ) + cutoff = datetime.now(timezone.utc) - timedelta(days=2) + + # filter to scans within the cutoff window + recent_scans = {} + skipped_old = 0 + for pid, scan in last_scans.items(): + if not scan or not scan.created_at: + skipped_old += 1 + continue + try: + created = datetime.fromisoformat(scan.created_at.replace("Z", "+00:00")) + except (ValueError, AttributeError): + skipped_old += 1 + continue + if created >= cutoff: + recent_scans[pid] = scan + else: + skipped_old += 1 + print(f"Projects with a last scan on main branch: {len(last_scans)}" + f" ({len(recent_scans)} within 2 days, {skipped_old} older)") + + headers = [ + "project_name", "query_name", "severity", "language", + "to_verify", "confirmed", "urgent", + "not_exploitable", "proposed_not_exploitable", + ] + + with open(OUTPUT_CSV, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=headers) + writer.writeheader() + + total = len(recent_scans) + for i, (pid, scan) in enumerate(recent_scans.items(), 1): + project = project_lookup.get(pid) + if not scan or not project: + continue + name = project.name + scan_id = scan.id + + print(f" [{i}/{total}] {name} ...", end=" ", flush=True) + + results = fetch_all_sast_results(sast_api, scan_id) + # count by (query_name, state); capture query-level attributes + state_counters = defaultdict(lambda: defaultdict(int)) + query_severity = {} + query_language = {} + for r in results: + query = r.query_name or "(unknown)" + state = r.state or "TO_VERIFY" + if state not in STATES: + state = "TO_VERIFY" + state_counters[query][state] += 1 + if query not in query_severity: + sev = (r.severity or "").upper() + query_severity[query] = sev + if query not in query_language: + lang = r.language_name or "" + query_language[query] = lang + + print(f"{len(results)} results, {len(state_counters)} queries") + + for query in sorted(state_counters): + s = state_counters[query] + writer.writerow({ + "project_name": name, + "query_name": query, + "severity": query_severity.get(query, ""), + "language": query_language.get(query, ""), + "to_verify": s.get("TO_VERIFY", 0), + "confirmed": s.get("CONFIRMED", 0), + "urgent": s.get("URGENT", 0), + "not_exploitable": s.get("NOT_EXPLOITABLE", 0), + "proposed_not_exploitable": s.get("PROPOSED_NOT_EXPLOITABLE", 0), + }) + + print(f"\nWrote to {OUTPUT_CSV}") + + +if __name__ == "__main__": + main() diff --git a/examples/CxOne/export_sast_state_counts.py b/examples/CxOne/export_sast_state_counts.py new file mode 100644 index 00000000..2d04e1cc --- /dev/null +++ b/examples/CxOne/export_sast_state_counts.py @@ -0,0 +1,145 @@ +""" +Get the last scan on the main branch for every project, retrieve SAST +result counts by state, and export a CSV for false-positive-rate analysis. + +Output CSV headers: + project_name, main_branch, scan_id, to_verify, confirmed, urgent, + not_exploitable, proposed_not_exploitable + +Secrets are read from a .env file in the project root. + +Usage: + python export_sast_state_counts.py +""" + +import csv +import os +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.sastResultsAPI import SastResultsAPI + +OUTPUT_CSV = Path(__file__).resolve().parent / "sast_state_counts.csv" +STATES = ["TO_VERIFY", "CONFIRMED", "URGENT", + "NOT_EXPLOITABLE", "PROPOSED_NOT_EXPLOITABLE"] + + +def load_dotenv(): + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get( + "CXONE_IAM_URL", "https://sng.iam.checkmarx.net" + ) + server_base_url = os.environ.get( + "CXONE_SERVER_URL", "https://sng.ast.checkmarx.net" + ) + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + if not refresh_token: + grant_type = "client_credentials" + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def get_state_counts(sast_api: SastResultsAPI, scan_id: str): + """Return a dict of {state: count} for one scan.""" + counts = {} + for state in STATES: + result = sast_api.get_sast_results_by_scan_id( + scan_id=scan_id, state=[state], limit=1, + ) + counts[state] = result.get("totalCount", 0) + return counts + + +def main(): + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + sast_api = SastResultsAPI(api_client=api_client) + + # get all projects + all_projects = projects_api.get_all_projects() + print(f"Found {len(all_projects)} projects.") + + # build lookup: project_id -> project + project_lookup = {p.id: p for p in all_projects} + + # get last scan on main branch for all projects + project_ids = [p.id for p in all_projects] + last_scans = projects_api.get_last_scan_info( + project_ids=project_ids, use_main_branch=True, limit=100, + ) + print(f"Projects with a last scan on main branch: {len(last_scans)}") + + rows = [] + total = len(last_scans) + for i, (pid, scan) in enumerate(last_scans.items(), 1): + project = project_lookup.get(pid) + if not scan or not project: + continue + name = project.name + branch = project.main_branch or scan.branch or "" + scan_id = scan.id + print(f" [{i}/{total}] {name} scan={scan_id} ...", end=" ") + counts = get_state_counts(sast_api, scan_id) + total_issues = sum(counts.values()) + print(f"issues={total_issues}") + rows.append({ + "project_name": name, + "main_branch": branch, + "scan_id": scan_id, + "to_verify": counts["TO_VERIFY"], + "confirmed": counts["CONFIRMED"], + "urgent": counts["URGENT"], + "not_exploitable": counts["NOT_EXPLOITABLE"], + "proposed_not_exploitable": counts["PROPOSED_NOT_EXPLOITABLE"], + }) + + # write CSV + headers = [ + "project_name", "main_branch", "scan_id", + "to_verify", "confirmed", "urgent", + "not_exploitable", "proposed_not_exploitable", + ] + with open(OUTPUT_CSV, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=headers) + writer.writeheader() + writer.writerows(rows) + print(f"\nWrote {len(rows)} rows to {OUTPUT_CSV}") + + +if __name__ == "__main__": + main() diff --git a/examples/CxOne/get_audit_events.py b/examples/CxOne/get_audit_events.py new file mode 100644 index 00000000..e568b249 --- /dev/null +++ b/examples/CxOne/get_audit_events.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Fetch audit events from the DEU CxOne tenant for April and May 2026. + +Tenant: * +Server: https://deu.ast.checkmarx.net +IAM: https://deu.iam.checkmarx.net +""" + +import json +import os +import sys +from datetime import datetime, timezone +from urllib.request import urlopen, Request + +# --- Configuration --- +SERVER_URL = "https://deu.ast.checkmarx.net" +IAM_URL = "https://deu.iam.checkmarx.net" +TENANT = "*" +API_KEY = "***" + +OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +TARGET_RESOURCES = { + "scans", "scan", "projects", "project", + "applications", "application", "configuration", + "tenant-settings", "project-settings", "project-webhooks", + "schedule", "scheduler", "scan-schedulers", +} + +MONTHS = { + "april": ("2026-04-01T00:00:00.000000Z", "2026-04-30T23:59:59.999999Z"), + "may": ("2026-05-01T00:00:00.000000Z", "2026-05-31T23:59:59.999999Z"), +} + + +def api_request(method: str, path: str, body: dict = None, access_token: str = None) -> dict: + """Make an API request to the DEU tenant.""" + url = SERVER_URL + path + headers = {"Accept": "application/json; version=1.0"} + if access_token: + headers["Authorization"] = "Bearer " + access_token + if body is not None: + headers["Content-Type"] = "application/json" + data = json.dumps(body).encode("utf-8") + else: + data = None + + req = Request(url, data=data, headers=headers, method=method) + with urlopen(req, timeout=60) as resp: + if resp.status == 204: + return {} + return json.loads(resp.read().decode("utf-8")) + + +def get_access_token() -> str: + """Exchange API key for an access token.""" + print("Obtaining access token from IAM...") + path = "/auth/realms/{}/protocol/openid-connect/token".format( + "*" + ) + url = IAM_URL + path + + # The API key IS the access token (offline token) + # Try using it directly first + headers = { + "Authorization": "Bearer " + API_KEY, + "Accept": "application/json", + } + req = Request( + IAM_URL + "/auth/realms/*/protocol/openid-connect/token", + data=b"grant_type=refresh_token&client_id=ast-app&refresh_token=" + API_KEY.encode(), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + method="POST", + ) + + try: + with urlopen(req, timeout=30) as resp: + token_data = json.loads(resp.read().decode("utf-8")) + return token_data.get("access_token", "") + except Exception as e: + print("Token exchange failed ({}), trying direct API key...".format(e)) + + # Fallback: use the API key directly as bearer token + return API_KEY + + +def fetch_audit_events(access_token: str, start_date: str, end_date: str) -> list: + """Fetch all pages of audit events for a date range.""" + all_events = [] + offset = 0 + limit = 1000 + page = 1 + + while True: + params = ( + "/api/audit-events/?offset={}&limit={}&startDate={}&endDate={}" + ).format(offset, limit, start_date, end_date) + result = api_request("GET", params, access_token=access_token) + events = result.get("events", []) + all_events.extend(events) + + total = result.get("totalFilteredCount", 0) + print(" Page {}: fetched {} events (total filtered: {})".format( + page, len(events), total + )) + + if len(events) < limit: + break + offset += limit + page += 1 + + return all_events + + +def main(): + print("=" * 80) + print("DEU Tenant Audit Events — April & May 2026") + print("=" * 80) + print("Tenant: *") + print("Server: {}".format(SERVER_URL)) + + access_token = get_access_token() + if not access_token: + print("ERROR: Could not obtain access token", file=sys.stderr) + sys.exit(1) + print("Access token obtained.") + + for month_name, (start, end) in MONTHS.items(): + print() + print("-" * 80) + print("Fetching {} 2026 ({} to {})...".format(month_name.title(), start, end)) + + try: + all_events = fetch_audit_events(access_token, start, end) + except Exception as e: + print("ERROR fetching {}: {}".format(month_name, e)) + continue + + print("Total events fetched: {}".format(len(all_events))) + + # Filter for target resources + filtered = [ + e for e in all_events + if e.get("auditResource", "").lower() in TARGET_RESOURCES + ] + print("Filtered events (Scans/Apps/Projects/Config): {}".format(len(filtered))) + + # Save + json_path = os.path.join(OUTPUT_DIR, "deu_audit_events_{}.json".format(month_name)) + with open(json_path, "w", encoding="utf-8") as f: + json.dump({"period": {"start": start, "end": end}, + "total_all": len(all_events), + "total_filtered": len(filtered), + "events": filtered}, + f, indent=2, default=str) + + print("Saved: {} ({:,.0f} KB)".format( + json_path, os.path.getsize(json_path) / 1024 + )) + + print() + print("=" * 80) + print("Done. Files saved to: {}".format(OUTPUT_DIR)) + + +if __name__ == "__main__": + main() diff --git a/examples/CxOne/get_project_sast_exclusions.py b/examples/CxOne/get_project_sast_exclusions.py new file mode 100644 index 00000000..41e934c8 --- /dev/null +++ b/examples/CxOne/get_project_sast_exclusions.py @@ -0,0 +1,145 @@ +""" +Get SAST engine "recommended exclusions" parameter for ALL projects. +If false/empty, set it to true, then verify. + +Secrets are read from a .env file in the project root. + +Usage: + python get_project_sast_exclusions.py + +.env file format: + CXONE_TENANT_NAME=your-tenant + CXONE_CLIENT_ID=ast-app + CXONE_CLIENT_SECRET=your-client-secret + CXONE_REFRESH_TOKEN=your-refresh-token +""" + +import os +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.scanConfigurationAPI import ScanConfigurationAPI +from CheckmarxPythonSDK.CxOne.dto.ScanParameter import ScanParameter + +RECOMMENDED_EXCLUSIONS_KEY = "scan.config.sast.recommendedExclusions" + + +def load_dotenv(): + """Load .env file from the project root.""" + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get("CXONE_IAM_URL", "https://iam.checkmarx.net") + server_base_url = os.environ.get("CXONE_SERVER_URL", "https://ast.checkmarx.net") + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + # fallback: client_credentials + if not refresh_token: + grant_type = "client_credentials" + + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def get_exclusion_param(api: ScanConfigurationAPI, project_id: str): + parameters = api.get_the_list_of_all_the_parameters_for_a_project(project_id) + return next( + (p for p in parameters if p.key == RECOMMENDED_EXCLUSIONS_KEY), None + ) + + +def is_false_or_empty(param): + if param is None: + return True + value = (param.value or "").strip().lower() + return value in ("", "false") + + +def main(): + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + scan_config_api = ScanConfigurationAPI(api_client=api_client) + + projects = projects_api.get_all_projects() + print(f"Found {len(projects)} projects.\n") + + # Step 1: Check all projects + print("=" * 60) + print("Step 1: Check all projects") + print("=" * 60) + needs_update = [] + for project in projects: + param = get_exclusion_param(scan_config_api, project.id) + value = param.value if param else "N/A" + value_display = value if value != "" else "(empty)" + print(f" {project.name}: {value_display}") + if is_false_or_empty(param): + needs_update.append(project) + + # Step 2: Update projects that are false/empty + if needs_update: + print(f"\n{'=' * 60}") + print(f"Step 2: Set to true for {len(needs_update)} project(s)") + print(f"{'=' * 60}") + for project in needs_update: + update_param = ScanParameter( + key=RECOMMENDED_EXCLUSIONS_KEY, + value="true", + valueType="Bool", + allowOverride=True, + ) + success = scan_config_api.define_parameters_in_the_input_list_for_a_specific_project( + project_id=project.id, + scan_parameters=[update_param], + ) + status = "OK" if success else "FAILED" + print(f" {project.name}: {status}") + else: + print("\nNo projects need updating.") + + # Step 3: Verify all projects again + print(f"\n{'=' * 60}") + print("Step 3: Verify all projects") + print(f"{'=' * 60}") + for project in projects: + param = get_exclusion_param(scan_config_api, project.id) + value = param.value if param else "N/A" + value_display = value if value != "" else "(empty)" + print(f" {project.name}: {value_display}") + + +if __name__ == "__main__": + main() diff --git a/examples/CxOne/trigger_scm_project_scans_on_main_branch.py b/examples/CxOne/trigger_scm_project_scans_on_main_branch.py new file mode 100644 index 00000000..3f011009 --- /dev/null +++ b/examples/CxOne/trigger_scm_project_scans_on_main_branch.py @@ -0,0 +1,182 @@ +""" +Trigger scans on the main branch for projects with Code Repository +integration (SCM-managed projects), using the repos-manager projectScan +endpoint. + +Secrets are read from a .env file in the project root. + +Usage: + python trigger_scm_project_scans_on_main_branch.py + +.env file format: + CXONE_TENANT_NAME=your-tenant + CXONE_CLIENT_ID=ast-app + CXONE_CLIENT_SECRET=your-client-secret + CXONE_REFRESH_TOKEN=your-refresh-token +""" + +import os +import re +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.repoManagerAPI import RepoManagerAPI + + +def load_dotenv(): + """Load .env file from the project root.""" + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get( + "CXONE_IAM_URL", "https://sng.iam.checkmarx.net" + ) + server_base_url = os.environ.get( + "CXONE_SERVER_URL", "https://sng.ast.checkmarx.net" + ) + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + if not refresh_token: + grant_type = "client_credentials" + + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def parse_scm_info(repo_url): + """Extract origin and organization from a repo URL.""" + if not repo_url: + return None, None + match = re.search(r"://(?:www\.)?([^.]+)\.(?:com|org)/([^/]+)", repo_url) + if not match: + return None, None + origin = match.group(1).upper() + organization = match.group(2) + return origin, organization + + +def main(): + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + repo_manager_api = RepoManagerAPI(api_client=api_client) + + project_list = projects_api.get_all_projects() + print(f"Found {len(project_list)} projects.\n") + + skipped = [] + scanned = [] + failed = [] + + for project_summary in project_list: + project = projects_api.get_a_project_by_id(project_summary.id) + repo_id = project.repo_id + scm_repo_id = project.scm_repo_id + repo_url = project.repo_url + main_branch = project.main_branch + + if not repo_id: + skipped.append((project.name, "no repo_id (SCM not connected)")) + continue + if not scm_repo_id: + skipped.append((project.name, "no scm_repo_id")) + continue + if not main_branch: + skipped.append((project.name, "no main_branch")) + continue + + # fallback: fetch repo_url from repo-manager if not on the project + if not repo_url: + try: + repo = repo_manager_api.get_repo_by_id(repo_id) + repo_url = repo.get("url", "") + except Exception: + pass + if not repo_url: + skipped.append((project.name, "no repo_url")) + continue + + origin, organization = parse_scm_info(repo_url) + if not origin or not organization: + skipped.append((project.name, f"cannot parse origin/org from repo_url: {repo_url}")) + continue + + repo_identity = scm_repo_id if scm_repo_id else repo_url.rstrip("/").split("/")[-1] + + print( + f" {project.name}: " + f"origin={origin}, org={organization}, " + f"repo_id={repo_id}, branch={main_branch}", + end="" + ) + + try: + result = repo_manager_api.scm_managed_project_scan( + project_id=project.id, + origin=origin, + organization=organization, + repo_id=repo_id, + repo_identity=repo_identity, + repo_url=repo_url, + default_branch=main_branch, + ) + if result.status_code in (200, 201, 202): + print(" -> OK") + scanned.append((project.name, "OK")) + else: + print(f" -> FAILED (status={result.status_code})") + failed.append((project.name, f"status={result.status_code}")) + except Exception as e: + print(f" -> FAILED: {e}") + failed.append((project.name, str(e))) + + print(f"\n{'=' * 60}") + print(f"Summary: {len(project_list)} total") + print(f" Scanned: {len(scanned)}") + print(f" Skipped: {len(skipped)}") + print(f" Failed: {len(failed)}") + + if skipped: + print(f"\nSkipped:") + for name, reason in skipped: + print(f" {name}: {reason}") + if failed: + print(f"\nFailed:") + for name, error in failed: + print(f" {name}: {error}") + + +if __name__ == "__main__": + main() diff --git a/examples/CxOne/update_project_preset.py b/examples/CxOne/update_project_preset.py new file mode 100644 index 00000000..e191950b --- /dev/null +++ b/examples/CxOne/update_project_preset.py @@ -0,0 +1,158 @@ +""" +Get the SAST "presetName" parameter for projects listed in a CSV file. +Print it, update it, then print it again. + +Secrets are read from a .env file in the project root. + +CSV file format (project_to_be_update_preset.csv): + Project Name,Risk Level,Application,Rationale + +Usage: + python update_project_preset.py + +.env file format: + CXONE_TENANT_NAME=your-tenant + CXONE_CLIENT_ID=ast-app + CXONE_CLIENT_SECRET=your-client-secret + CXONE_REFRESH_TOKEN=your-refresh-token +""" + +import csv +import os +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.scanConfigurationAPI import ScanConfigurationAPI +from CheckmarxPythonSDK.CxOne.dto.ScanParameter import ScanParameter + +PRESET_KEY = "scan.config.sast.presetName" +CSV_FILE = Path(__file__).resolve().parent / "project_to_be_update_preset.csv" +NEW_PRESET_NAME = "OWASP TOP 10 API 2023 - TMCA" + + +def load_dotenv(): + """Load .env file from the project root.""" + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get("CXONE_IAM_URL", "https://sng.iam.checkmarx.net") + server_base_url = os.environ.get("CXONE_SERVER_URL", "https://sng.ast.checkmarx.net") + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + # fallback: client_credentials + if not refresh_token: + grant_type = "client_credentials" + + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def load_target_project_names(): + """Read project names from the CSV file.""" + if not CSV_FILE.exists(): + print(f"Error: {CSV_FILE} not found.") + return [] + with open(CSV_FILE, newline="") as f: + reader = csv.DictReader(f) + return [row["Project Name"].strip() for row in reader if row.get("Project Name", "").strip()] + + +def get_preset_param(api: ScanConfigurationAPI, project_id: str): + parameters = api.get_the_list_of_all_the_parameters_for_a_project(project_id) + return next( + (p for p in parameters if p.key == PRESET_KEY), None + ) + + +def main(): + target_names = load_target_project_names() + if not target_names: + print("No project names found in CSV file.") + return + print(f"Loaded {len(target_names)} project name(s) from {CSV_FILE.name}.\n") + + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + scan_config_api = ScanConfigurationAPI(api_client=api_client) + + all_projects = projects_api.get_all_projects() + projects = [p for p in all_projects if p.name in target_names] + if not projects: + print(f"No target projects found among {len(all_projects)} total projects.") + return + print(f"Found {len(projects)} target project(s) out of {len(all_projects)} total.\n") + + # Step 1: Get current preset for target projects + print("=" * 60) + print("Step 1: Get current project preset") + print("=" * 60) + for project in projects: + param = get_preset_param(scan_config_api, project.id) + value = param.value if param else "N/A" + value_display = value if value != "" else "(empty)" + print(f" {project.name}: {value_display}") + + # Step 2: Update preset for target projects + print(f"\n{'=' * 60}") + print("Step 2: Update project preset") + print(f"{'=' * 60}") + for project in projects: + update_param = ScanParameter( + key=PRESET_KEY, + value=NEW_PRESET_NAME, + valueType="String", + allowOverride=True, + ) + success = scan_config_api.define_parameters_in_the_input_list_for_a_specific_project( + project_id=project.id, + scan_parameters=[update_param], + ) + status = "OK" if success else "FAILED" + print(f" {project.name}: {status}") + + # Step 3: Verify preset for target projects + print(f"\n{'=' * 60}") + print("Step 3: Verify project preset") + print(f"{'=' * 60}") + for project in projects: + param = get_preset_param(scan_config_api, project.id) + value = param.value if param else "N/A" + value_display = value if value != "" else "(empty)" + print(f" {project.name}: {value_display}") + + +if __name__ == "__main__": + main() From e28227340b142988010fc64eb35f5565343dbcc1 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:13:35 +0800 Subject: [PATCH 07/25] Add DTOs for SAST results predicates and refactor - Add PredicateInitialValues, PredicateHistoryResponse DTOs - Add from_dict to CommentJSON, PredicateWithCommentJSON, PredicateHistory - Update SastResultsPredicatesAPI to return typed PredicateHistoryResponse - Add offset/limit params to get_all_predicates_for_similarity_id Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxOne/dto/CommentJSON.py | 12 +++++++++ .../CxOne/dto/PredicateHistory.py | 26 ++++++++++++++++--- .../CxOne/dto/PredicateHistoryResponse.py | 25 ++++++++++++++++++ .../CxOne/dto/PredicateInitialValues.py | 14 ++++++++++ .../CxOne/dto/PredicateWithCommentJSON.py | 18 +++++++++++++ CheckmarxPythonSDK/CxOne/dto/__init__.py | 2 ++ .../CxOne/sastResultsPredicatesAPI.py | 20 +++++++------- 7 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 CheckmarxPythonSDK/CxOne/dto/PredicateHistoryResponse.py create mode 100644 CheckmarxPythonSDK/CxOne/dto/PredicateInitialValues.py diff --git a/CheckmarxPythonSDK/CxOne/dto/CommentJSON.py b/CheckmarxPythonSDK/CxOne/dto/CommentJSON.py index 96c367d3..e7e8dea7 100644 --- a/CheckmarxPythonSDK/CxOne/dto/CommentJSON.py +++ b/CheckmarxPythonSDK/CxOne/dto/CommentJSON.py @@ -8,3 +8,15 @@ class CommentJSON: user: str = None content: str = None is_deleted: bool = None + + @classmethod + def from_dict(cls, item: dict) -> "CommentJSON": + if not item: + return None + return cls( + id=item.get("id"), + date=item.get("date"), + user=item.get("user"), + content=item.get("content"), + is_deleted=item.get("isDeleted", False), + ) diff --git a/CheckmarxPythonSDK/CxOne/dto/PredicateHistory.py b/CheckmarxPythonSDK/CxOne/dto/PredicateHistory.py index cc5d6a37..9d14606a 100644 --- a/CheckmarxPythonSDK/CxOne/dto/PredicateHistory.py +++ b/CheckmarxPythonSDK/CxOne/dto/PredicateHistory.py @@ -1,12 +1,32 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List + +from .PredicateInitialValues import PredicateInitialValues from .PredicateWithCommentJSON import PredicateWithCommentJSON @dataclass class PredicateHistory: - + """Predicates for one similarity ID within one project.""" similarity_id: str = None project_id: str = None - predicates: List[PredicateWithCommentJSON] = None + predicates: List[PredicateWithCommentJSON] = field(default_factory=list) total_count: int = None + initial_predicate_values: PredicateInitialValues = None + + @classmethod + def from_dict(cls, item: dict) -> "PredicateHistory": + if not item: + return None + return cls( + similarity_id=item.get("similarityId"), + project_id=item.get("projectId"), + predicates=[ + PredicateWithCommentJSON.from_dict(p) + for p in (item.get("predicates") or []) + ], + total_count=item.get("totalCount"), + initial_predicate_values=PredicateInitialValues.from_dict( + item.get("initialPredicateValues") + ), + ) diff --git a/CheckmarxPythonSDK/CxOne/dto/PredicateHistoryResponse.py b/CheckmarxPythonSDK/CxOne/dto/PredicateHistoryResponse.py new file mode 100644 index 00000000..7c116993 --- /dev/null +++ b/CheckmarxPythonSDK/CxOne/dto/PredicateHistoryResponse.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass, field +from typing import List + +from .PredicateHistory import PredicateHistory + + +@dataclass +class PredicateHistoryResponse: + """Full response from GET /sast-results-predicates/{similarityId} + and GET /sast-results-predicates/{similarityId}/latest.""" + + predicate_history_per_project: List[PredicateHistory] = field(default_factory=list) + total_count: int = 0 + + @classmethod + def from_dict(cls, item: dict) -> "PredicateHistoryResponse": + if not item: + return None + return cls( + predicate_history_per_project=[ + PredicateHistory.from_dict(p) + for p in (item.get("predicateHistoryPerProject") or []) + ], + total_count=item.get("totalCount", 0), + ) diff --git a/CheckmarxPythonSDK/CxOne/dto/PredicateInitialValues.py b/CheckmarxPythonSDK/CxOne/dto/PredicateInitialValues.py new file mode 100644 index 00000000..a4c281a4 --- /dev/null +++ b/CheckmarxPythonSDK/CxOne/dto/PredicateInitialValues.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + + +@dataclass +class PredicateInitialValues: + """Initial state/severity before triage.""" + state: str = None + severity: str = None + + @classmethod + def from_dict(cls, item: dict) -> "PredicateInitialValues": + if not item: + return None + return cls(state=item.get("state"), severity=item.get("severity")) diff --git a/CheckmarxPythonSDK/CxOne/dto/PredicateWithCommentJSON.py b/CheckmarxPythonSDK/CxOne/dto/PredicateWithCommentJSON.py index 007a891a..78cd8cec 100644 --- a/CheckmarxPythonSDK/CxOne/dto/PredicateWithCommentJSON.py +++ b/CheckmarxPythonSDK/CxOne/dto/PredicateWithCommentJSON.py @@ -15,3 +15,21 @@ class PredicateWithCommentJSON: created_at: str = None change_origin_type: int = None change_origin_name: str = None + + @classmethod + def from_dict(cls, item: dict) -> "PredicateWithCommentJSON": + if not item: + return None + return cls( + id=item.get("ID"), + similarity_id=item.get("similarityId"), + project_id=item.get("projectId"), + severity=item.get("severity"), + state=item.get("state"), + comment=item.get("comment"), + comment_json=CommentJSON.from_dict(item.get("commentJSON")), + created_by=item.get("createdBy"), + created_at=item.get("createdAt"), + change_origin_type=item.get("changeOriginType"), + change_origin_name=item.get("changeOriginName"), + ) diff --git a/CheckmarxPythonSDK/CxOne/dto/__init__.py b/CheckmarxPythonSDK/CxOne/dto/__init__.py index 45b03ac8..e7772f07 100644 --- a/CheckmarxPythonSDK/CxOne/dto/__init__.py +++ b/CheckmarxPythonSDK/CxOne/dto/__init__.py @@ -165,6 +165,8 @@ from .PlatformSummary import PlatformSummary from .Predicate import Predicate from .PredicateHistory import PredicateHistory +from .PredicateInitialValues import PredicateInitialValues +from .PredicateHistoryResponse import PredicateHistoryResponse from .PredicateWithCommentJSON import PredicateWithCommentJSON from .PredicateWithCommentsJSON import PredicateWithCommentsJSON from .Preset import Preset diff --git a/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py b/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py index 88f8a9f1..a7d8b423 100644 --- a/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py +++ b/CheckmarxPythonSDK/CxOne/sastResultsPredicatesAPI.py @@ -1,7 +1,9 @@ +from typing import List + from CheckmarxPythonSDK.api_client import ApiClient from CheckmarxPythonSDK.CxOne.config import construct_configuration from CheckmarxPythonSDK.utilities.compat import NO_CONTENT, CREATED, OK -from typing import List +from .dto import PredicateHistoryResponse class SastResultsPredicatesAPI(object): @@ -24,7 +26,7 @@ def get_all_predicates_for_similarity_id( scan_id: str = None, offset: int = 0, limit: int = 100, - ) -> dict: + ) -> PredicateHistoryResponse: """ Args: similarity_id (str): @@ -35,7 +37,7 @@ def get_all_predicates_for_similarity_id( limit (int): Returns: - dict + PredicateHistoryResponse """ url = f"{self.base_url}/{similarity_id}" params = { @@ -48,14 +50,14 @@ def get_all_predicates_for_similarity_id( response = self.api_client.call_api( method="GET", url=url, params=params ) - return response.json() + return PredicateHistoryResponse.from_dict(response.json()) def get_latest_predicates_for_similarity_id( self, similarity_id: str, project_ids: List[str] = None, scan_id: str = None, - ) -> dict: + ) -> PredicateHistoryResponse: """ Args: similarity_id (str): @@ -63,14 +65,14 @@ def get_latest_predicates_for_similarity_id( scan_id (str): Returns: - dict + PredicateHistoryResponse """ url = f"{self.base_url}/{similarity_id}/latest" params = {"project-ids": project_ids, "scan-id": scan_id} response = self.api_client.call_api( method="GET", url=url, params=params ) - return response.json() + return PredicateHistoryResponse.from_dict(response.json()) def predicate_severity_and_state_by_similarity_id_and_project_id( self, data: List[dict] @@ -253,7 +255,7 @@ def get_all_predicates_for_similarity_id( scan_id: str = None, offset: int = 0, limit: int = 100, -) -> dict: +) -> PredicateHistoryResponse: return SastResultsPredicatesAPI().get_all_predicates_for_similarity_id( similarity_id=similarity_id, project_ids=project_ids, @@ -268,7 +270,7 @@ def get_latest_predicates_for_similarity_id( similarity_id: str, project_ids: List[str] = None, scan_id: str = None, -) -> dict: +) -> PredicateHistoryResponse: return SastResultsPredicatesAPI().get_latest_predicates_for_similarity_id( similarity_id=similarity_id, project_ids=project_ids, From b40b100d959545e8dddd6ec9d4524f585d92f281 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:34:09 +0800 Subject: [PATCH 08/25] Add triage workflow script and ignore HAR files - triage_workflow.py: guided 9-step triage for SAST findings - Add *.har to .gitignore Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 +- examples/CxOne/triage_workflow.py | 354 ++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 examples/CxOne/triage_workflow.py diff --git a/.gitignore b/.gitignore index d964dcaa..8836f20f 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,5 @@ build .claude *.txt -*.json \ No newline at end of file +*.json +*.har \ No newline at end of file diff --git a/examples/CxOne/triage_workflow.py b/examples/CxOne/triage_workflow.py new file mode 100644 index 00000000..0ffe4510 --- /dev/null +++ b/examples/CxOne/triage_workflow.py @@ -0,0 +1,354 @@ +""" +Guided triage workflow for SAST findings in a scan. + +Given a project name: + 1. Get the last scan on the main branch + 2. Get SAST scan summary (aggregate by language) + 3-N. For each SAST result: + 3. Get result with full details + 4. Get the query description + 5. Get triage info (predicates) — skip if already triaged by cxservice + 6. Get source file for review + 7. Decision: CONFIRMED / NOT_EXPLOITABLE / Skip + 8. Apply the triage + 9. Verify + +Secrets are read from a .env file in the project root. + +Usage: + python triage_workflow.py + +Example: + python triage_workflow.py "happy-cook/WebGoat" +""" + +import os +import sys +from pathlib import Path + +from CheckmarxPythonSDK.configuration import Configuration +from CheckmarxPythonSDK.api_client import ApiClient +from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI +from CheckmarxPythonSDK.CxOne.sastResultsSummaryAPI import SastResultsSummaryAPI +from CheckmarxPythonSDK.CxOne.sastResultsAPI import SastResultsAPI +from CheckmarxPythonSDK.CxOne.sastQueriesAPI import SastQueriesAPI +from CheckmarxPythonSDK.CxOne.sastResultsPredicatesAPI import SastResultsPredicatesAPI +from CheckmarxPythonSDK.CxOne.repoStoreServiceAPI import RepoStoreServiceAPI + +SKIP_USER = "cxservice_happy.yang@checkmarx.com" + + +def load_dotenv(): + env_path = Path(__file__).resolve().parents[2] / ".env" + if not env_path.exists(): + print(f"Warning: {env_path} not found, using environment variables.") + return + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ[key] = value + + +def build_configuration() -> Configuration: + load_dotenv() + tenant_name = os.environ["CXONE_TENANT_NAME"] + access_control_url = os.environ.get( + "CXONE_IAM_URL", "https://sng.iam.checkmarx.net" + ) + server_base_url = os.environ.get( + "CXONE_SERVER_URL", "https://sng.ast.checkmarx.net" + ) + grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") + client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") + client_secret = os.environ.get("CXONE_CLIENT_SECRET") + refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") + if not refresh_token: + grant_type = "client_credentials" + return Configuration( + server_base_url=server_base_url, + iam_base_url=access_control_url, + token_url=( + f"{access_control_url}/auth/realms" + f"/{tenant_name}/protocol/openid-connect/token" + ), + tenant_name=tenant_name, + grant_type=grant_type, + client_id=client_id, + client_secret=client_secret, + api_key=refresh_token, + ) + + +def find_project_by_name(api: ProjectsAPI, name: str) -> dict: + project_id = api.get_project_id_by_name(name) + if not project_id: + return None + p = api.get_a_project_by_id(project_id) + return {"id": p.id, "name": p.name, "mainBranch": p.main_branch} + + +def get_last_main_branch_scan(api: ProjectsAPI, project_id: str) -> dict: + scans = api.get_last_scan_info( + project_ids=[project_id], use_main_branch=True, limit=1, + ) + scan = scans.get(project_id) + if not scan: + return None + return {"id": scan.id, "createdAt": scan.created_at, "status": scan.status} + + +def fetch_all_results(results_api: SastResultsAPI, scan_id: str): + """Paginate through all SAST results. Returns list of SastResult.""" + all_results = [] + offset = 0 + limit = 100 + while True: + resp = results_api.get_sast_results_by_scan_id( + scan_id=scan_id, + include_nodes=True, + apply_predicates=True, + offset=offset, + limit=limit, + sort=["+status", "+severity", "-queryname"], + ) + batch = resp.get("results", []) + all_results.extend(batch) + total = resp.get("totalCount", 0) + if offset + limit >= total: + break + offset += limit + return all_results + + +def get_source_file_info(nodes): + """Extract source file and line from result nodes.""" + for node in (nodes or []): + if node.node_type == "source" and node.file_name: + return node.file_name, node.line + for node in (nodes or []): + if node.file_name: + return node.file_name, node.line + return None, None + + +def show_source_code(repostore_api, scan_id, file_path, line_num): + """Print source code context around the finding line.""" + try: + code = repostore_api.view_source_code_of_specified_file( + scan_id=scan_id, file_path=file_path, + ) + lines = code.split("\n") + start = max(0, (line_num or 1) - 10) + end = min(len(lines), (line_num or 1) + 10) + print(f" --- Lines {start + 1}-{end} ---") + for i in range(start, end): + marker = " >>>" if i == (line_num or 1) - 1 else " " + print(f" {i + 1:4d}{marker} {lines[i]}") + except Exception as e: + print(f" Error reading source: {e}") + + +def was_triaged_by_cxservice(response): + """Check if any predicate was created by the skip user. + + Args: + response: PredicateHistoryResponse from the SDK. + """ + for proj in (response.predicate_history_per_project or []): + for pred in (proj.predicates or []): + if pred.created_by == SKIP_USER: + return True + return False + + +def process_finding(idx, total, finding, project, scan_id, + queries_api, predicates_api, repostore_api): + """Run steps 4-9 for a single SAST finding.""" + similarity_id = str(finding.similarity_id) + header = f"Finding {idx}/{total} | {finding.query_name} | {finding.severity} | {finding.language_name}" + print(f"\n{'=' * 70}") + print(f"{'=' * 70}") + print(f" {header}") + print(f"{'=' * 70}") + print(f" Similarity ID: {similarity_id}") + print(f" State / Status: {finding.state} / {finding.status}") + if finding.nodes: + for node in finding.nodes[:4]: + print(f" Node: {node.name} ({node.node_type}) file={node.file_name} line={node.line}") + + # Step 4: Query description + print(f"\n --- Step 4: Query description ---") + try: + query_desc = queries_api.get_sast_query_description( + ids=[finding.query_id_str], scan_id=scan_id, + ) + if query_desc: + qd = query_desc[0] + print(f" Description: {(qd.description or '')[:400]}") + print(f" Remediation: {(qd.remediation or '')[:400]}") + except Exception as e: + print(f" Error: {e}") + + # Step 5: Check predicates — skip if already triaged by cxservice + print(f"\n --- Step 5: Check predicates ---") + try: + predicates = predicates_api.get_all_predicates_for_similarity_id( + similarity_id=similarity_id, + project_ids=[project["id"]], + include_comment_json=True, + scan_id=scan_id, + ) + except Exception as e: + print(f" Error fetching predicates: {e}") + return False + + if was_triaged_by_cxservice(predicates): + print(f" SKIP: already triaged by {SKIP_USER}") + return False + + for proj in (predicates.predicate_history_per_project or []): + for p in (proj.predicates or []): + print(f" Pred: {p.state}/{p.severity} by {p.created_by} at {p.created_at}") + + # Step 6: Source file + print(f"\n --- Step 6: Source file ---") + source_file, source_line = get_source_file_info(finding.nodes) + if source_file: + print(f" File: {source_file} (line {source_line})") + show_source_code(repostore_api, scan_id, source_file, source_line) + else: + print(" No source file found.") + + # Step 7: Decision + print(f"\n --- Step 7: Decision ---") + print(f" 1 - CONFIRMED 2 - NOT_EXPLOITABLE 3 - Skip") + choice = input(" > ").strip() + if choice not in ("1", "2"): + print(" Skipped.") + return False + + new_state = "CONFIRMED" if choice == "1" else "NOT_EXPLOITABLE" + comment = input(f" Comment ({new_state}): ").strip() + + # Step 8: Apply triage + print(f"\n --- Step 8: Apply triage -> {new_state} ---") + payload = [{ + "similarityId": similarity_id, + "projectId": project["id"], + "scanId": scan_id, + "allowInconsistentStates": True, + "state": new_state, + "comment": comment or f"{new_state} via triage script", + }] + try: + success = predicates_api.predicate_severity_and_state_by_similarity_id_and_project_id( + data=payload, + ) + print(f" Applied: {'OK' if success else 'FAILED'}") + except Exception as e: + print(f" Error: {e}") + return False + + # Step 9: Verify + print(f"\n --- Step 9: Verify ---") + try: + latest = predicates_api.get_latest_predicates_for_similarity_id( + similarity_id=similarity_id, + project_ids=[project["id"]], + scan_id=scan_id, + ) + for proj in (latest.predicate_history_per_project or []): + for p in (proj.predicates or []): + print(f" Latest: {p.state}/{p.severity} by {p.created_by}") + except Exception as e: + print(f" Error: {e}") + return True + + +def main(): + if len(sys.argv) < 2: + print("Usage: python triage_workflow.py ") + sys.exit(1) + project_name = sys.argv[1] + + configuration = build_configuration() + api_client = ApiClient(configuration=configuration) + + projects_api = ProjectsAPI(api_client=api_client) + summary_api = SastResultsSummaryAPI(api_client=api_client) + results_api = SastResultsAPI(api_client=api_client) + queries_api = SastQueriesAPI(api_client=api_client) + predicates_api = SastResultsPredicatesAPI(api_client=api_client) + repostore_api = RepoStoreServiceAPI(api_client=api_client) + + # -------- Step 1: Find project & get last scan -------- + print("=" * 70) + print(f"Step 1: Project '{project_name}'") + print("=" * 70) + project = find_project_by_name(projects_api, project_name) + if not project: + print(f" Project '{project_name}' not found.") + sys.exit(1) + print(f" Project ID : {project['id']}") + print(f" Main branch: {project['mainBranch']}") + + scan = get_last_main_branch_scan(projects_api, project["id"]) + if not scan: + print(" No scan found on main branch.") + sys.exit(1) + scan_id = scan["id"] + print(f" Scan ID : {scan_id}") + print(f" Status : {scan['status']}") + + # -------- Step 2: SAST summary -------- + print(f"\n{'=' * 70}") + print(f"Step 2: SAST summary (by LANGUAGE)") + print(f"{'=' * 70}") + summary = summary_api.get_sast_aggregate_results( + scan_id=scan_id, + group_by_field=["LANGUAGE"], + apply_predicates=True, + ) + if summary.get("scannerSummary"): + for item in summary["scannerSummary"]: + lang = item.get("languageName") or item.get("label", "?") + sev = item.get("severityCounters", {}) or item.get("severity", {}) + print(f" {lang}: {sev}") + else: + print(f" Raw: {summary}") + + # -------- Step 3: Iterate all SAST results -------- + all_results = fetch_all_results(results_api, scan_id) + total = len(all_results) + print(f"\n{'=' * 70}") + print(f"Step 3: {total} SAST results to triage") + print(f"{'=' * 70}") + if not all_results: + print(" No results found.") + return + + triaged = 0 + skipped = 0 + + for i, finding in enumerate(all_results, 1): + action_taken = process_finding( + i, total, finding, project, scan_id, + queries_api, predicates_api, repostore_api, + ) + if action_taken: + triaged += 1 + else: + skipped += 1 + + print(f"\n{'=' * 70}") + print(f"Done: {total} findings, {triaged} triaged, {skipped} skipped") + print(f"{'=' * 70}") + + +if __name__ == "__main__": + main() From e8d226c3c1dcd252adaa66e1bd35939c226fb3e8 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:25:43 +0800 Subject: [PATCH 09/25] Add get_all_scan_results helper with dedup for SAST pagination bug Work around server-side issue where /cxrestapi/sast/results returns duplicate data on the last page. Add diagnostic tests to detect the bug. Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py | 37 ++++ .../CxRestAPI/test_sast_results_pagination.py | 208 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 tests/CxSAST/CxRestAPI/test_sast_results_pagination.py diff --git a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py index 7bcc6bcf..b5bf5c07 100644 --- a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py +++ b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py @@ -1481,3 +1481,40 @@ def get_scan_results_in_paged_mode(self, scan_id, offset, limit, lcid=None): result = CxScanResultsPage.from_dict(data) return result + + def get_all_scan_results(self, scan_id, lcid=None, limit=200): + """ + Fetch all SAST results for a scan by paginating through all pages. + + Deduplicates by path_id to work around a server-side issue where + the last page of /cxrestapi/sast/results may return duplicate data. + + Args: + scan_id (int): Unique ID of a scan + lcid (int): Language Id + limit (int): Page size for each request (default 200) + + Returns: + :obj:`list` of :obj:`CxScanResult` + """ + all_results = [] + seen_path_ids = set() + offset = 0 + + while True: + page = self.get_scan_results_in_paged_mode( + scan_id=scan_id, offset=offset, limit=limit, lcid=lcid + ) + if page is None or not page.results: + break + + for result in page.results: + if result.path_id not in seen_path_ids: + seen_path_ids.add(result.path_id) + all_results.append(result) + + if offset + limit >= page.total_count: + break + offset += limit + + return all_results diff --git a/tests/CxSAST/CxRestAPI/test_sast_results_pagination.py b/tests/CxSAST/CxRestAPI/test_sast_results_pagination.py new file mode 100644 index 00000000..d22a653c --- /dev/null +++ b/tests/CxSAST/CxRestAPI/test_sast_results_pagination.py @@ -0,0 +1,208 @@ +"""Test for /cxrestapi/sast/results pagination duplicate detection. + +Reported issue: the last page always returns data that are duplicated. +This test paginates through all results and checks for duplicates using +path_id as the unique key. +""" +from CheckmarxPythonSDK.CxRestAPISDK import ScansAPI +from .. import get_project_id + + +def _get_scan_id(): + project_id = get_project_id() + scan_api = ScansAPI() + return scan_api.get_last_scan_id_of_a_project( + project_id, + only_finished_scans=True, + only_completed_scans=True, + only_real_scans=True, + only_full_scans=True, + ) + + +def test_sast_results_pagination_no_duplicates(): + """Fetch all SAST results page by page and verify no duplicate entries.""" + scan_id = _get_scan_id() + if not scan_id: + import pytest + pytest.skip("No qualifying finished full scan found") + + scan_api = ScansAPI() + limit = 20 # small page size to force multiple pages + + seen_path_ids = set() + duplicates = [] + all_results = [] + offset = 0 + page_num = 1 + + while True: + page = scan_api.get_scan_results_in_paged_mode( + scan_id=scan_id, offset=offset, limit=limit + ) + if page is None or not page.results: + break + + batch = page.results + all_results.extend(batch) + + for result in batch: + key = result.path_id + if key in seen_path_ids: + duplicates.append({ + "path_id": key, + "page": page_num, + "query": result.query.name if result.query else "N/A", + "state": result.state, + "index": result.index, + }) + seen_path_ids.add(key) + + # Log page info for diagnostics + print( + f"Page {page_num}: offset={offset}, limit={limit}, " + f"fetched={len(batch)}, total={page.total_count}, " + f"seen={len(seen_path_ids)}" + ) + + # Exit when we've fetched all results + if offset + limit >= page.total_count: + break + offset += limit + page_num += 1 + + total_expected = all_results[0].total_count if all_results else 0 + print(f"\nSummary: fetched {len(all_results)} results across {page_num} pages") + print(f"Server reported totalCount: {total_expected}") + print(f"Unique path_ids: {len(seen_path_ids)}") + + if duplicates: + dup_path_ids = {d["path_id"] for d in duplicates} + dup_pages = {d["page"] for d in duplicates} + print(f"\nERROR: Found {len(duplicates)} duplicate result(s)!") + print(f"Duplicate path_ids: {dup_path_ids}") + print(f"Pages with duplicates: {dup_pages}") + for d in duplicates: + print( + f" Duplicate: path_id={d['path_id']} on page {d['page']}, " + f"query={d['query']}, state={d['state']}, index={d['index']}" + ) + + assert len(duplicates) == 0, ( + f"Found {len(duplicates)} duplicate result(s) across pages. " + f"Duplicate path_ids: {dup_path_ids}. " + f"This confirms the server-side pagination bug." + ) + + +def test_sast_results_last_page_no_duplicate_with_previous(): + """Specifically verify the last page has no overlap with the previous page.""" + scan_id = _get_scan_id() + if not scan_id: + import pytest + pytest.skip("No qualifying finished full scan found") + + scan_api = ScansAPI() + limit = 20 + + # Fetch all pages and store per-page path_id sets + page_path_ids = [] + offset = 0 + page_num = 1 + + while True: + page = scan_api.get_scan_results_in_paged_mode( + scan_id=scan_id, offset=offset, limit=limit + ) + if page is None or not page.results: + break + + batch_ids = {r.path_id for r in page.results} + page_path_ids.append((page_num, batch_ids)) + + print( + f"Page {page_num}: offset={offset}, count={len(page.results)}, " + f"total={page.total_count}" + ) + + if offset + limit >= page.total_count: + break + offset += limit + page_num += 1 + + # Check each adjacent pair of pages for overlaps + overlaps_found = [] + for i in range(1, len(page_path_ids)): + prev_page, prev_ids = page_path_ids[i - 1] + curr_page, curr_ids = page_path_ids[i] + overlap = prev_ids & curr_ids + if overlap: + overlaps_found.append({ + "pages": f"{prev_page} -> {curr_page}", + "overlapping_path_ids": overlap, + }) + + if overlaps_found: + print(f"\nERROR: Found overlaps between consecutive pages!") + for ov in overlaps_found: + print( + f" Overlap between pages {ov['pages']}: " + f"path_ids={ov['overlapping_path_ids']}" + ) + + assert len(overlaps_found) == 0, ( + f"Found {len(overlaps_found)} page overlap(s). " + f"Overlaps: {overlaps_found}" + ) + + +def test_get_all_scan_results_no_duplicates(): + """Verify the new helper method returns no duplicate results.""" + scan_id = _get_scan_id() + if not scan_id: + import pytest + pytest.skip("No qualifying finished full scan found") + + scan_api = ScansAPI() + limit = 20 + results = scan_api.get_all_scan_results( + scan_id=scan_id, limit=limit + ) + + path_ids = [r.path_id for r in results] + unique_ids = set(path_ids) + + # Fetch individual pages for comparison + total_from_server = None + offset = 0 + raw_count = 0 + while True: + page = scan_api.get_scan_results_in_paged_mode( + scan_id=scan_id, offset=offset, limit=limit + ) + if page is None or not page.results: + break + raw_count += len(page.results) + if total_from_server is None: + total_from_server = page.total_count + if offset + limit >= page.total_count: + break + offset += limit + + print(f"Server totalCount: {total_from_server}") + print(f"Raw sum across pages: {raw_count}") + print(f"get_all_scan_results count: {len(results)}") + print(f"Unique path_ids: {len(unique_ids)}") + + # No duplicates in helper result + assert len(results) == len(unique_ids), ( + f"get_all_scan_results returned {len(results) - len(unique_ids)} duplicates" + ) + + # If server has the bug, raw_count > total_from_server, but helper result + # should equal unique count from raw pages + if raw_count > total_from_server: + print(f"NOTE: Server pagination bug confirmed - raw count {raw_count} " + f"exceeds totalCount {total_from_server}") + # The helper should filter out duplicates + assert len(results) <= raw_count From c7cf77554c8dfd6a3fad09cb3c3e80d80a3b80c0 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:04:59 +0800 Subject: [PATCH 10/25] Fix get_all_scan_results to use exact remaining count on last page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-side pagination bug in /cxrestapi/sast/results occurs when limit exceeds the number of remaining results — the last page wraps around and returns earlier data instead of the actual trailing results. Instead of always using the full page size, compute remaining = total_count - offset and use min(limit, remaining) as the actual limit for each request after the first. This ensures the final request asks for exactly the right number of results, avoiding the server boundary condition. Dedup is kept as an additional safety net. Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py | 27 +++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py index b5bf5c07..3c9741b9 100644 --- a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py +++ b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py @@ -1486,8 +1486,11 @@ def get_all_scan_results(self, scan_id, lcid=None, limit=200): """ Fetch all SAST results for a scan by paginating through all pages. - Deduplicates by path_id to work around a server-side issue where - the last page of /cxrestapi/sast/results may return duplicate data. + On the final page, uses the exact remaining result count as the limit + rather than the full page size. This avoids a server-side boundary bug + in /cxrestapi/sast/results where passing a limit larger than the number + of remaining results causes the last page to return earlier (duplicate) + data instead of the actual trailing results. Args: scan_id (int): Unique ID of a scan @@ -1500,21 +1503,35 @@ def get_all_scan_results(self, scan_id, lcid=None, limit=200): all_results = [] seen_path_ids = set() offset = 0 + total_count = None while True: + # On the final page, fetch only the exact number of remaining + # results to avoid the server pagination boundary bug. + if total_count is not None: + remaining = total_count - offset + if remaining <= 0: + break + current_limit = min(limit, remaining) + else: + current_limit = limit + page = self.get_scan_results_in_paged_mode( - scan_id=scan_id, offset=offset, limit=limit, lcid=lcid + scan_id=scan_id, offset=offset, limit=current_limit, lcid=lcid ) if page is None or not page.results: break + if total_count is None: + total_count = page.total_count + for result in page.results: if result.path_id not in seen_path_ids: seen_path_ids.add(result.path_id) all_results.append(result) - if offset + limit >= page.total_count: + if offset + current_limit >= total_count: break - offset += limit + offset += current_limit return all_results From 39b7b93363b8ff5f0b636da3edcf742fc0bc457f Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:15:25 +0800 Subject: [PATCH 11/25] Fix get_all_scan_results: use page-based offset for /cxrestapi/sast/results The CxSAST /cxrestapi/sast/results endpoint interprets the 'offset' parameter as a page number (records skipped = offset * limit), not as a record count. The previous implementation incremented offset by the page size each iteration, causing it to skip most results. Increment offset by 1 per page instead, and keep path_id dedup as a safety net. Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py | 33 +++++++-------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py index 3c9741b9..976f2969 100644 --- a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py +++ b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py @@ -1486,11 +1486,12 @@ def get_all_scan_results(self, scan_id, lcid=None, limit=200): """ Fetch all SAST results for a scan by paginating through all pages. - On the final page, uses the exact remaining result count as the limit - rather than the full page size. This avoids a server-side boundary bug - in /cxrestapi/sast/results where passing a limit larger than the number - of remaining results causes the last page to return earlier (duplicate) - data instead of the actual trailing results. + .. note:: + + The /cxrestapi/sast/results endpoint interprets ``offset`` as a + **page number** (number of pages to skip), not a record count. + This method accounts for that by incrementing offset by 1 per + page rather than by the page size. Args: scan_id (int): Unique ID of a scan @@ -1502,36 +1503,22 @@ def get_all_scan_results(self, scan_id, lcid=None, limit=200): """ all_results = [] seen_path_ids = set() - offset = 0 - total_count = None + offset = 0 # page number, not record offset while True: - # On the final page, fetch only the exact number of remaining - # results to avoid the server pagination boundary bug. - if total_count is not None: - remaining = total_count - offset - if remaining <= 0: - break - current_limit = min(limit, remaining) - else: - current_limit = limit - page = self.get_scan_results_in_paged_mode( - scan_id=scan_id, offset=offset, limit=current_limit, lcid=lcid + scan_id=scan_id, offset=offset, limit=limit, lcid=lcid ) if page is None or not page.results: break - if total_count is None: - total_count = page.total_count - for result in page.results: if result.path_id not in seen_path_ids: seen_path_ids.add(result.path_id) all_results.append(result) - if offset + current_limit >= total_count: + if offset * limit + len(page.results) >= page.total_count: break - offset += current_limit + offset += 1 return all_results From 44ffa668bc60876867b83ae5306e7fca201a4347 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:32:59 +0800 Subject: [PATCH 12/25] Add dump_all_path_ids diagnostic script using get_all_scan_results Co-Authored-By: Claude Opus 4.7 --- scripts/dump_all_path_ids.py | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 scripts/dump_all_path_ids.py diff --git a/scripts/dump_all_path_ids.py b/scripts/dump_all_path_ids.py new file mode 100644 index 00000000..b66e74a9 --- /dev/null +++ b/scripts/dump_all_path_ids.py @@ -0,0 +1,62 @@ +"""Dump all path_ids from a scan's SAST results. + +Uses get_all_scan_results which handles the page-based offset semantics +of /cxrestapi/sast/results correctly. + +Usage: + PYTHONPATH=. python scripts/dump_all_path_ids.py + +Requires a CxSAST config in ~/.Checkmarx/config.ini or environment +variables prefixed with cxsast_ (e.g. cxsast_base_url, cxsast_username). +""" +import os +from pathlib import Path + +# Load .env if present +env_path = Path(__file__).resolve().parents[1] / ".env" +if env_path.exists(): + with open(env_path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + os.environ[key.strip()] = value.strip().strip("\"'") + +from CheckmarxPythonSDK.CxRestAPISDK import ProjectsAPI, ScansAPI + + +def main(): + projects_api = ProjectsAPI() + scan_api = ScansAPI() + + project_id = projects_api.create_project_if_not_exists_by_project_name_and_team_full_name( + "jvl_git", "/CxServer" + ) + scan_id = scan_api.get_last_scan_id_of_a_project( + project_id, + only_finished_scans=True, + only_completed_scans=True, + only_real_scans=True, + only_full_scans=True, + ) + if not scan_id: + print("No qualifying scan found.") + return + + results = scan_api.get_all_scan_results(scan_id=scan_id, limit=20) + path_ids = [r.path_id for r in results] + unique = len(set(path_ids)) + + print(f"Scan ID: {scan_id}") + print(f"Total fetched: {len(results)}") + print(f"Unique: {unique}") + print(f"Duplicates: {len(results) - unique}") + + print(f"\n--- All {len(path_ids)} path_ids ---") + for pid in path_ids: + print(pid) + + +if __name__ == "__main__": + main() From 22a607661fdb7431079d9cfc77dd715d7b2ad418 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:34:07 +0800 Subject: [PATCH 13/25] Move dump_all_path_ids.py from scripts/ to examples/CxSAST/ Co-Authored-By: Claude Opus 4.7 --- {scripts => examples/CxSAST}/dump_all_path_ids.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {scripts => examples/CxSAST}/dump_all_path_ids.py (100%) diff --git a/scripts/dump_all_path_ids.py b/examples/CxSAST/dump_all_path_ids.py similarity index 100% rename from scripts/dump_all_path_ids.py rename to examples/CxSAST/dump_all_path_ids.py From 80d1949baa00db48a06c58aaa8a6d3b5ea11101c Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:38:36 +0800 Subject: [PATCH 14/25] Bump version to 1.8.8 and update changelog Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 ++++++ CheckmarxPythonSDK/__version__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 038c4e31..5edd1cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +1.8.8 - 2026-06-15 +* [Fix] CxRestAPISDK get_all_scan_results — the /cxrestapi/sast/results endpoint interprets offset as a page number (records skipped = offset * limit), not a record count. Fixed pagination loop to increment offset by 1 per page instead of by the page size, which was causing results to be silently skipped. +* [Add] get_all_scan_results method in CxRestAPISDK ScansAPI — safe pagination helper that correctly walks all pages and deduplicates by path_id +* [Add] examples/CxSAST/dump_all_path_ids.py — example script dumping all path_ids from a scan using get_all_scan_results +* [Add] tests/CxSAST/CxRestAPI/test_sast_results_pagination.py — diagnostic tests for SAST results pagination + 1.8.7 - 2026-06-02 * [Add] AiAssetsAPI — AI supply chain asset management (findings, asset types, assets, applications, global inventory results, scan results, risks) * [Add] AnalyticsAPI — KPI query endpoint diff --git a/CheckmarxPythonSDK/__version__.py b/CheckmarxPythonSDK/__version__.py index f7d787dc..cd12fb00 100644 --- a/CheckmarxPythonSDK/__version__.py +++ b/CheckmarxPythonSDK/__version__.py @@ -1 +1 @@ -__version__ = "1.8.7" +__version__ = "1.8.8" From bfb7a660d8047172a4506862166c09ac9217be11 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:16:37 +0800 Subject: [PATCH 15/25] Add get_source_by_scan_id to Portal SOAP API (deprecated) The operation exists in the WSDL but CxSAST 9.x returns "This action is no longer supported." Use CxAuditWebService's get_source_code_for_scan as the replacement. Co-Authored-By: Claude Opus 4.7 --- .../CxPortalSoapApiSDK/CxPortalWebService.py | 33 +++++++++++++++++++ .../CxPortalSoapApiSDK/__init__.py | 1 + 2 files changed, 34 insertions(+) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py index 949736c7..4f4a6bec 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py @@ -1227,6 +1227,33 @@ def postpone_scan(self, scan_id: int) -> dict: "ErrorMessage": getattr(response, "ErrorMessage", None), } + def get_source_by_scan_id(self, scan_id: int, file_name: str) -> dict: + """Retrieve source code for a specific file in a scan (deprecated). + + This SOAP operation is no longer supported in CxSAST 9.x. The server + returns IsSuccesfull=False with "This action is no longer supported." + Use :meth:`CxAuditWebService.get_source_code_for_scan` instead, which + returns a base64-encoded zip of all source files for the scan. + + Args: + scan_id (int): + file_name (str): the file path to retrieve source for + + Returns: + dict: always IsSuccesfull=False on 9.x + """ + response = self.suds_client.execute( + "GetSourceByScanID", + sessionID="0", + scanID=str(scan_id), + fileToRetreive=file_name, + ) + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "source": getattr(response, "source", None), + } + def unlock_scan(self, scan_id: int) -> dict: """ @@ -1487,5 +1514,11 @@ def postpone_scan(scan_id: int) -> dict: return CxPortalWebService().postpone_scan(scan_id=scan_id) +def get_source_by_scan_id(scan_id: int, file_name: str) -> dict: + return CxPortalWebService().get_source_by_scan_id( + scan_id=scan_id, file_name=file_name + ) + + def unlock_scan(scan_id: int) -> dict: return CxPortalWebService().unlock_scan(scan_id=scan_id) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py index e80126f9..c227b2e2 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py @@ -32,6 +32,7 @@ lock_scan, postpone_scan, unlock_scan, + get_source_by_scan_id, ) from .CxAuditWebService import ( From c8cb7f6c8d502a701ab661781dadb429882123d3 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:31:07 +0800 Subject: [PATCH 16/25] Add get_sources_by_scan_id and get_file_names_for_path (Portal SOAP) GetSourceByScanID (singular) is deprecated in CxSAST 9.x, but the plural GetSourcesByScanID returns per-file source content for requested file paths. GetFileNamesForPath returns file names associated with a path_id. Co-Authored-By: Claude Opus 4.7 --- .../CxPortalSoapApiSDK/CxPortalWebService.py | 98 ++++++++++++++++++- .../CxPortalSoapApiSDK/__init__.py | 2 + .../test_cx_portal_web_service.py | 49 ++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py index 4f4a6bec..ae2f3f7e 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py @@ -1232,8 +1232,7 @@ def get_source_by_scan_id(self, scan_id: int, file_name: str) -> dict: This SOAP operation is no longer supported in CxSAST 9.x. The server returns IsSuccesfull=False with "This action is no longer supported." - Use :meth:`CxAuditWebService.get_source_code_for_scan` instead, which - returns a base64-encoded zip of all source files for the scan. + Use :meth:`get_sources_by_scan_id` (plural) instead. Args: scan_id (int): @@ -1254,6 +1253,89 @@ def get_source_by_scan_id(self, scan_id: int, file_name: str) -> dict: "source": getattr(response, "source", None), } + def get_sources_by_scan_id(self, scan_id: int, file_names: list) -> dict: + """Retrieve source code for specific files in a scan. + + Args: + scan_id (int): + file_names (list of str): file paths to retrieve source for + + Returns: + dict:: + { + "IsSuccesfull": True, + "ErrorMessage": None, + "sources": [ + {"IsSuccesfull": True, "Source": "", "Encode": "Unicode (UTF-8)"}, + ... + ], + } + """ + files_array = self.suds_client.factory.ArrayOfString(list(file_names)) + response = self.suds_client.execute( + "GetSourcesByScanID", + sessionID="0", + scanID=str(scan_id), + filesToRetreive=files_array, + ) + content_list = getattr( + response, "cxWSResponseSourcesContent", None + ) + sources = [] + if content_list: + items = getattr(content_list, "CxWSResponseSourceContent", None) + if items is not None: + if not isinstance(items, list): + items = [items] + sources = [ + { + "IsSuccesfull": item.IsSuccesfull, + "Source": item.Source, + } + for item in items + ] + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Encode": getattr(response, "Encode", None), + "sources": sources, + } + + def get_file_names_for_path(self, scan_id: int, path_id: int) -> dict: + """Get file names associated with a result path. + + Args: + scan_id (int): + path_id (int): + + Returns: + dict:: + { + "IsSuccesfull": True, + "ErrorMessage": None, + "fileNames": ["\\\\path\\\\to\\\\file.jsp"], + } + """ + response = self.suds_client.execute( + "GetFileNamesForPath", + sessionId="0", + scanId=scan_id, + pathId=path_id, + ) + fnames = getattr(response, "fileNames", None) + file_names = [] + if fnames: + strings = getattr(fnames, "string", None) + if strings is not None: + if not isinstance(strings, list): + strings = [strings] + file_names = strings + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "fileNames": file_names, + } + def unlock_scan(self, scan_id: int) -> dict: """ @@ -1520,5 +1602,17 @@ def get_source_by_scan_id(scan_id: int, file_name: str) -> dict: ) +def get_sources_by_scan_id(scan_id: int, file_names: list) -> dict: + return CxPortalWebService().get_sources_by_scan_id( + scan_id=scan_id, file_names=file_names + ) + + +def get_file_names_for_path(scan_id: int, path_id: int) -> dict: + return CxPortalWebService().get_file_names_for_path( + scan_id=scan_id, path_id=path_id + ) + + def unlock_scan(scan_id: int) -> dict: return CxPortalWebService().unlock_scan(scan_id=scan_id) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py index c227b2e2..6f21e011 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py @@ -33,6 +33,8 @@ postpone_scan, unlock_scan, get_source_by_scan_id, + get_sources_by_scan_id, + get_file_names_for_path, ) from .CxAuditWebService import ( diff --git a/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py b/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py index 4c920f41..d4e2648e 100644 --- a/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py +++ b/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py @@ -34,6 +34,9 @@ get_results_for_scan, get_result_path, get_pivot_data, + get_sources_by_scan_id, + get_file_names_for_path, + get_source_by_scan_id, ) from .. import get_project_id @@ -310,3 +313,49 @@ def test_postpone_scan(): scan_id = _get_scan_id() response = postpone_scan(scan_id=scan_id) assert response.get("IsSuccesfull") is True + + +def test_get_file_names_for_path(): + scan_id = _get_scan_id() + response = get_file_names_for_path(scan_id=scan_id, path_id=1) + assert response["IsSuccesfull"] is True + assert len(response["fileNames"]) > 0 + assert all(isinstance(fn, str) for fn in response["fileNames"]) + + +def test_get_sources_by_scan_id(): + scan_id = _get_scan_id() + # Get a file name from a known path + file_info = get_file_names_for_path(scan_id=scan_id, path_id=1) + assert file_info["IsSuccesfull"] is True + file_names = file_info["fileNames"] + + response = get_sources_by_scan_id( + scan_id=scan_id, file_names=file_names, + ) + assert response["IsSuccesfull"] is True + assert len(response["sources"]) == len(file_names) + for src in response["sources"]: + assert "Source" in src + assert "IsSuccesfull" in src + assert len(str(src["Source"])) > 0 + + # Test with multiple files + if len(file_names) >= 1: + response = get_sources_by_scan_id( + scan_id=scan_id, file_names=file_names[:1], + ) + assert response["IsSuccesfull"] is True + assert len(response["sources"]) == 1 + assert len(str(response["sources"][0]["Source"])) > 0 + + +def test_get_source_by_scan_id_deprecated(): + """Verify the deprecated singular endpoint returns the expected error.""" + scan_id = _get_scan_id() + response = get_source_by_scan_id( + scan_id=scan_id, + file_name=r"\src\main\webapp\vulnerability\DisplayMessage.jsp", + ) + assert response["IsSuccesfull"] is False + assert "no longer supported" in response.get("ErrorMessage", "") From 1debae0dcc53450d5e6b13573f442adab68f63cb Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:33:11 +0800 Subject: [PATCH 17/25] Update changelog for 1.8.8 Portal SOAP additions Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5edd1cad..bf4a8bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. * [Add] get_all_scan_results method in CxRestAPISDK ScansAPI — safe pagination helper that correctly walks all pages and deduplicates by path_id * [Add] examples/CxSAST/dump_all_path_ids.py — example script dumping all path_ids from a scan using get_all_scan_results * [Add] tests/CxSAST/CxRestAPI/test_sast_results_pagination.py — diagnostic tests for SAST results pagination +* [Add] CxPortalWebService.get_sources_by_scan_id — retrieve per-file source code for specific files in a scan (Portal SOAP GetSourcesByScanID) +* [Add] CxPortalWebService.get_source_by_scan_id — deprecated singular variant; CxSAST 9.x returns "no longer supported" +* [Add] CxPortalWebService.get_file_names_for_path — get file names associated with a result path (Portal SOAP GetFileNamesForPath) +* [Add] tests for get_sources_by_scan_id, get_file_names_for_path, and get_source_by_scan_id 1.8.7 - 2026-06-02 * [Add] AiAssetsAPI — AI supply chain asset management (findings, asset types, assets, applications, global inventory results, scan results, risks) From 5a8f22ccb69f422dd863581cef9a8b4ac91385bb Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:45:35 +0800 Subject: [PATCH 18/25] Add 64 missing Portal and Audit SOAP API methods with tests Portal SOAP (40 new methods): get_preset_details, update_preset, get_result_state_list, get_scan_report, get_scan_report_status, cancel_scan_report, get_results, get_result_summary, get_query_collection_for_language, get_query_description, get_query_short_description, get_scans_display_data_for_all_projects, get_scan_summary, get_server_license_basic, get_server_license_data_extended, get_custom_fields, get_custom_field_values, get_result_paths_for_query, get_results_for_query, get_queries_for_scan, get_scan_properties, get_status_of_single_scan, get_scans_statuses, get_scan_logs, update_result_state, update_result_comment, update_scan_comment, is_valid_preset_name, get_server_language_list, get_executable_list, count_lines, is_alive, is_smtp_host_configured, is_private_cloud, get_cwe_description, get_result_state_flags, cancel_scan, delete_scan, delete_scans, get_child_nodes, get_projects_with_scans, get_configuration_set_list Audit SOAP (24 new methods): get_results, get_result_summary, get_result_state_list, update_result_state, update_scan_comment, get_project_scans, get_projects_with_scans, get_query_collection, get_query_collection_for_language, get_query_description, get_query_description_by_query_id, get_queries_categories, get_preset_details, get_preset_list, get_path_comments_history, get_project_configuration, get_license_details, get_engine_configuration, get_hierarchy_group_tree, get_ancestry_group_tree, keep_alive, import_queries, get_cache All methods include module-level convenience functions, __init__.py exports, and pytest tests (55 Portal + 20 Audit = 75 tests passing). Co-Authored-By: Claude Opus 4.7 --- .../CxPortalSoapApiSDK/CxAuditWebService.py | 511 ++++++++++ .../CxPortalSoapApiSDK/CxPortalWebService.py | 942 ++++++++++++++++++ .../CxPortalSoapApiSDK/__init__.py | 85 +- docs/CxSAST_Portal_SOAP_API_List.md | 79 +- .../CxPortalSOAP/test_cx_audit_web_service.py | 156 +++ .../test_cx_portal_web_service.py | 342 ++++++- 6 files changed, 2090 insertions(+), 25 deletions(-) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxAuditWebService.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxAuditWebService.py index 3789a171..3aec879a 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxAuditWebService.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxAuditWebService.py @@ -1,3 +1,4 @@ +from os.path import exists from CheckmarxPythonSDK.configuration import Configuration from CheckmarxPythonSDK.CxPortalSoapApiSDK.config import construct_configuration from .sudsClient import SudsClient @@ -53,6 +54,412 @@ def get_source_code_for_scan(self, scan_id: int) -> dict: ), } + def get_results(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetResults", sessionId="0", scanId=scan_id + ) + result_collection = getattr(response, "ResultCollection", None) + results = [] + if result_collection: + query_groups = getattr(result_collection, "QueryGroups", None) + if query_groups: + groups = getattr(query_groups, "CxWSQueryGroup", []) + if not isinstance(groups, list): + groups = [groups] + for qg in groups: + for qr in (getattr(getattr(qg, "QueryResults", None), "CxWSQueryResult", []) or []): + if not isinstance(qr, list): + qr_list = [qr] if qr else [] + else: + qr_list = qr + for item in qr_list: + results.append({ + "QueryId": item.QueryId, + "QueryName": item.QueryName, + "QueryVersionCode": item.QueryVersionCode, + "QueryGroupName": getattr(item, "QueryGroupName", None), + "ResultPathList": getattr(item, "ResultPathList", None), + }) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Results": results, + } + + def get_result_summary(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetResultSummary", sessionId="0", scanId=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryGroups": getattr(response, "QueryGroups", None), + } + + def get_result_state_list(self) -> dict: + response = self.suds_client.execute("GetResultStateList", sessionID="0") + result_state_list = getattr(response, "ResultStateList", None) + items = getattr(result_state_list, "ResultState", []) if result_state_list else [] + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ResultStateList": [ + { + "ResultName": item.ResultName, + "ResultID": item.ResultID, + "ResultPermission": item.ResultPermission, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def update_result_state( + self, scan_id: int, path_id: int, state: int, comment: str = "" + ) -> dict: + response = self.suds_client.execute( + "UpdateResultState", + sessionId="0", + scanId=scan_id, + pathId=path_id, + state=state, + comment=comment, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def update_scan_comment(self, scan_id: int, comment: str) -> dict: + response = self.suds_client.execute( + "UpdateScanComment", + sessionID="0", + scanID=scan_id, + comment=comment, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def get_project_scans(self, project_id: int) -> dict: + response = self.suds_client.execute( + "GetProjectScans", sessionId="0", projectId=project_id + ) + scans_list = getattr(response, "ScansList", None) + items = getattr(scans_list, "Scan", []) if scans_list else [] + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScansList": [ + { + "ScanID": item.ScanID, + "StartDate": getattr(item, "StartDate", None), + "FinishDate": getattr(item, "FinishDate", None), + "ScanType": getattr(item, "ScanType", None), + "ScanStatus": getattr(item, "ScanStatus", None), + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_projects_with_scans(self) -> dict: + response = self.suds_client.execute( + "GetProjectsWithScans", sessionId="0" + ) + project_list = getattr(response, "projectList", None) + items = ( + getattr(project_list, "ProjectDisplayData", []) + if project_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "projectList": [ + { + "ProjectName": item.ProjectName, + "projectID": item.projectID, + "Group": item.Group, + "TotalScans": item.TotalScans, + "LastScanDate": getattr(item, "LastScanDate", None), + "Owner": item.Owner, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_query_collection(self) -> dict: + response = self.suds_client.execute("GetQueryCollection", sessionId="0") + query_groups = [] + for query_group in response.QueryGroups.CxWSQueryGroup: + queries = [] + if query_group.Queries: + for query in query_group.Queries.CxWSQuery: + queries.append({ + "QueryId": query.QueryId, + "Name": query.Name, + "Severity": query.Severity, + "Status": query.Status, + "Cwe": query.Cwe, + "Source": query.Source, + "IsExecutable": query.IsExecutable, + "QueryVersionCode": query.QueryVersionCode, + "Type": query.Type, + "CxDescriptionID": query.CxDescriptionID, + }) + query_groups.append({ + "Name": query_group.Name, + "Language": query_group.Language, + "LanguageName": query_group.LanguageName, + "PackageTypeName": query_group.PackageTypeName, + "PackageId": query_group.PackageId, + "Queries": queries, + }) + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryGroups": query_groups, + } + + def get_query_collection_for_language( + self, project_type: str = "Regular", project_id: int = 0 + ) -> dict: + response = self.suds_client.execute( + "GetQueryCollectionForLanguage", + sessionId="0", + projectType=project_type, + projectId=project_id, + ) + query_groups = [] + for query_group in response.QueryGroups.CxWSQueryGroup: + queries = [] + if query_group.Queries: + for query in query_group.Queries.CxWSQuery: + queries.append({ + "QueryId": query.QueryId, + "Name": query.Name, + "Severity": query.Severity, + "Status": query.Status, + "Cwe": query.Cwe, + "Source": query.Source, + "IsExecutable": query.IsExecutable, + "QueryVersionCode": query.QueryVersionCode, + "Type": query.Type, + "CxDescriptionID": query.CxDescriptionID, + }) + query_groups.append({ + "Name": query_group.Name, + "Language": query_group.Language, + "LanguageName": query_group.LanguageName, + "PackageTypeName": query_group.PackageTypeName, + "PackageId": query_group.PackageId, + "Queries": queries, + }) + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryGroups": query_groups, + } + + def get_query_description(self, cwe_id: int) -> dict: + response = self.suds_client.execute( + "GetQueryDescription", sessionId="0", cweId=cwe_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryDescription": getattr(response, "QueryDescription", None), + } + + def get_query_description_by_query_id(self, query_id: int) -> dict: + response = self.suds_client.execute( + "GetQueryDescriptionByQueryId", sessionId="0", queryId=query_id + ) + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryDescription": response.QueryDescription, + } + + def get_queries_categories(self) -> dict: + response = self.suds_client.execute("GetQueriesCategories", sessionId="0") + categories = response.QueriesCategories.CxQueryCategory + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueriesCategories": ( + [ + { + "Id": category["Id"], + "CategoryName": category["CategoryName"], + "CategoryType": { + "Id": category["CategoryType"]["Id"], + "Name": category["CategoryType"]["Name"], + "Order": category["CategoryType"]["Order"], + }, + } + for category in categories + ] + if categories + else None + ), + } + + def get_preset_details(self, preset_id: int) -> dict: + response = self.suds_client.execute( + "GetPresetDetails", sessionId="0", id=preset_id + ) + preset = getattr(response, "preset", None) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "preset": ( + { + "queryIds": getattr(getattr(preset, "queryIds", None), "long", []), + "id": preset.id, + "name": preset.name, + "owningteam": preset.owningteam, + "isPublic": preset.isPublic, + "owner": getattr(preset, "owner", None), + "isUserAllowToUpdate": preset.isUserAllowToUpdate, + "isUserAllowToDelete": preset.isUserAllowToDelete, + "IsDuplicate": preset.IsDuplicate, + } + if preset + else None + ), + } + + def get_preset_list(self) -> dict: + response = self.suds_client.execute("GetPresetList", SessionID="0") + preset_list = response.PresetList + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "PresetList": ( + [ + { + "PresetName": item["PresetName"], + "ID": item["ID"], + "owningUser": item["owningUser"], + "isUserAllowToUpdate": item["isUserAllowToUpdate"], + "isUserAllowToDelete": item["isUserAllowToDelete"], + } + for item in preset_list["Preset"] + ] + if preset_list + else None + ), + } + + def get_path_comments_history( + self, scan_id: int, path_id: int, label_type: str + ) -> dict: + response = self.suds_client.execute( + "GetPathCommentsHistory", + sessionId="0", + scanId=scan_id, + pathId=path_id, + labelType=label_type, + ) + path = response.Path + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Path": ( + { + "AssignedUser": getattr(path, "AssignedUser", None), + "Comment": getattr(path, "Comment", None), + "Nodes": getattr(path, "Nodes", None), + "PathId": path["PathId"], + "Severity": path["Severity"], + "SimilarityId": path["SimilarityId"], + "State": path["State"], + } + if path + else None + ), + } + + def get_project_configuration(self, project_id: int) -> dict: + response = self.suds_client.execute( + "GetProjectConfiguration", sessionID="0", projectID=project_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ProjectConfig": getattr(response, "ProjectConfig", None), + } + + def get_license_details(self) -> dict: + response = self.suds_client.execute("GetLicenseDetails", sessionId="0") + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "LicenseDetails": getattr(response, "LicenseDetails", None), + } + + def get_engine_configuration(self, configuration_id: int = 1) -> dict: + response = self.suds_client.execute( + "GetEngineConfiguration", sessionID="0", configurationId=configuration_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "EngineConfig": getattr(response, "EngineConfig", None), + } + + def get_hierarchy_group_tree(self) -> dict: + response = self.suds_client.execute( + "GetHierarchyGroupTree", sessionID="0" + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "GroupTree": getattr(response, "GroupTree", None), + } + + def get_ancestry_group_tree(self, team_id: str = "1") -> dict: + response = self.suds_client.execute( + "GetAncestryGroupTree", sessionID="0", pTeamID=team_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "GroupTree": getattr(response, "GroupTree", None), + } + + def keep_alive(self) -> dict: + response = self.suds_client.execute("KeepAlive", sessionId="0") + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def import_queries(self, imported_file_path: str) -> dict: + if not exists(imported_file_path): + print("Error, the imported file {} not exist".format(imported_file_path)) + return None + with open(imported_file_path, "rb") as xml_file: + imported_file = xml_file.read() + response = self.suds_client.execute( + "ImportQueries", sessionId="0", importedFile=imported_file + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "requestId": response["requestId"], + "importQueryStatus": response["importQueryStatus"], + } + + def get_cache(self, scan_id: int = 0) -> dict: + response = self.suds_client.execute("GetCache", sessionId="0", scanId=scan_id) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Cache": getattr(response, "Cache", None), + } + def upload_queries(self, query_groups: dict) -> dict: factory = self.suds_client.factory qgs = factory.ArrayOfCxWSQueryGroup( @@ -138,3 +545,107 @@ def get_source_code_for_scan(scan_id: int) -> dict: def upload_queries(query_groups: dict) -> dict: return CxAuditWebService().upload_queries(query_groups=query_groups) + + +def get_results(scan_id: int) -> dict: + return CxAuditWebService().get_results(scan_id=scan_id) + + +def get_result_summary(scan_id: int) -> dict: + return CxAuditWebService().get_result_summary(scan_id=scan_id) + + +def get_result_state_list() -> dict: + return CxAuditWebService().get_result_state_list() + + +def update_result_state( + scan_id: int, path_id: int, state: int, comment: str = "" +) -> dict: + return CxAuditWebService().update_result_state( + scan_id=scan_id, path_id=path_id, state=state, comment=comment + ) + + +def update_scan_comment(scan_id: int, comment: str) -> dict: + return CxAuditWebService().update_scan_comment(scan_id=scan_id, comment=comment) + + +def get_project_scans(project_id: int) -> dict: + return CxAuditWebService().get_project_scans(project_id=project_id) + + +def get_projects_with_scans() -> dict: + return CxAuditWebService().get_projects_with_scans() + + +def get_query_collection() -> dict: + return CxAuditWebService().get_query_collection() + + +def get_query_collection_for_language( + project_type: str = "Regular", project_id: int = 0 +) -> dict: + return CxAuditWebService().get_query_collection_for_language( + project_type=project_type, project_id=project_id + ) + + +def get_query_description(cwe_id: int) -> dict: + return CxAuditWebService().get_query_description(cwe_id=cwe_id) + + +def get_query_description_by_query_id(query_id: int) -> dict: + return CxAuditWebService().get_query_description_by_query_id(query_id=query_id) + + +def get_queries_categories() -> dict: + return CxAuditWebService().get_queries_categories() + + +def get_preset_details(preset_id: int) -> dict: + return CxAuditWebService().get_preset_details(preset_id=preset_id) + + +def get_preset_list() -> dict: + return CxAuditWebService().get_preset_list() + + +def get_path_comments_history(scan_id: int, path_id: int, label_type: str) -> dict: + return CxAuditWebService().get_path_comments_history( + scan_id=scan_id, path_id=path_id, label_type=label_type + ) + + +def get_project_configuration(project_id: int) -> dict: + return CxAuditWebService().get_project_configuration(project_id=project_id) + + +def get_license_details() -> dict: + return CxAuditWebService().get_license_details() + + +def get_engine_configuration(configuration_id: int = 1) -> dict: + return CxAuditWebService().get_engine_configuration( + configuration_id=configuration_id + ) + + +def get_hierarchy_group_tree() -> dict: + return CxAuditWebService().get_hierarchy_group_tree() + + +def get_ancestry_group_tree(team_id: str = "1") -> dict: + return CxAuditWebService().get_ancestry_group_tree(team_id=team_id) + + +def keep_alive() -> dict: + return CxAuditWebService().keep_alive() + + +def import_queries(imported_file_path: str) -> dict: + return CxAuditWebService().import_queries(imported_file_path=imported_file_path) + + +def get_cache(scan_id: int = 0) -> dict: + return CxAuditWebService().get_cache(scan_id=scan_id) diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py index ae2f3f7e..c2530eb5 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py @@ -1353,6 +1353,744 @@ def unlock_scan(self, scan_id: int) -> dict: "ErrorMessage": getattr(response, "ErrorMessage", None), } + def get_preset_details(self, preset_id: int) -> dict: + response = self.suds_client.execute( + "GetPresetDetails", sessionId="0", id=preset_id + ) + preset = getattr(response, "preset", None) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "preset": ( + { + "queryIds": getattr(getattr(preset, "queryIds", None), "long", []), + "id": preset.id, + "name": preset.name, + "owningteam": preset.owningteam, + "isPublic": preset.isPublic, + "owner": getattr(preset, "owner", None), + "isUserAllowToUpdate": preset.isUserAllowToUpdate, + "isUserAllowToDelete": preset.isUserAllowToDelete, + "IsDuplicate": preset.IsDuplicate, + } + if preset + else None + ), + } + + def update_preset(self, preset_id: int, query_ids: list, name: str) -> dict: + factory = self.suds_client.factory + query_id_list = factory.ArrayOfLong(query_ids) + cx_preset_detail = factory.CxPresetDetails( + queryIds=query_id_list, + id=preset_id, + name=name, + owningteam=1, + isPublic=True, + isUserAllowToUpdate=True, + isUserAllowToDelete=True, + IsDuplicate=False, + ) + response = self.suds_client.execute( + "UpdatePreset", sessionId="0", presrt=cx_preset_detail + ) + preset = getattr(response, "preset", None) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "preset": ( + { + "queryIds": getattr(getattr(preset, "queryIds", None), "long", []), + "id": preset.id, + "name": preset.name, + "owningteam": preset.owningteam, + "isPublic": preset.isPublic, + "owner": getattr(preset, "owner", None), + "isUserAllowToUpdate": preset.isUserAllowToUpdate, + "isUserAllowToDelete": preset.isUserAllowToDelete, + "IsDuplicate": preset.IsDuplicate, + } + if preset + else None + ), + } + + def get_result_state_list(self) -> dict: + response = self.suds_client.execute("GetResultStateList", sessionID="0") + result_state_list = getattr( + response, "ResultStateList", None + ) + items = getattr(result_state_list, "ResultState", []) if result_state_list else [] + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ResultStateList": [ + { + "ResultName": item.ResultName, + "ResultID": item.ResultID, + "ResultPermission": item.ResultPermission, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_scan_report(self, report_id: int) -> dict: + response = self.suds_client.execute( + "GetScanReport", SessionID="0", ReportID=report_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScanResults": getattr(response, "ScanResults", None), + "containsAllResults": response["containsAllResults"], + } + + def get_scan_report_status(self, report_id: int) -> dict: + response = self.suds_client.execute( + "GetScanReportStatus", SessionID="0", ReportID=report_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Status": getattr(response, "Status", None), + } + + def cancel_scan_report(self, report_id: int) -> dict: + response = self.suds_client.execute( + "CancelScanReport", SessionID="0", ReportID=report_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def get_results(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetResults", sessionId="0", scanId=scan_id + ) + result_collection = getattr(response, "ResultCollection", None) + results = [] + if result_collection: + query_groups = getattr(result_collection, "QueryGroups", None) + if query_groups: + groups = getattr(query_groups, "CxWSQueryGroup", []) + if not isinstance(groups, list): + groups = [groups] + for qg in groups: + for qr in (getattr(getattr(qg, "QueryResults", None), "CxWSQueryResult", []) or []): + if not isinstance(qr, list): + qr_list = [qr] if qr else [] + else: + qr_list = qr + for item in qr_list: + results.append({ + "QueryId": item.QueryId, + "QueryName": item.QueryName, + "QueryVersionCode": item.QueryVersionCode, + "QueryGroupName": getattr(item, "QueryGroupName", None), + "ResultPathList": getattr(item, "ResultPathList", None), + }) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Results": results, + } + + def get_result_summary(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetResultSummary", sessionId="0", scanId=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ResultSummary": getattr(response, "ResultSummary", None), + } + + def get_query_collection_for_language( + self, project_type: str = "Regular", project_id: int = 0 + ) -> dict: + response = self.suds_client.execute( + "GetQueryCollectionForLanguage", + sessionId="0", + projectType=project_type, + projectId=project_id, + ) + query_groups = [] + for query_group in response.QueryGroups.CxWSQueryGroup: + queries = [] + if query_group.Queries: + for query in query_group.Queries.CxWSQuery: + queries.append({ + "QueryId": query.QueryId, + "Name": query.Name, + "Severity": query.Severity, + "Status": query.Status, + "Cwe": query.Cwe, + "Source": query.Source, + "IsExecutable": query.IsExecutable, + "QueryVersionCode": query.QueryVersionCode, + "Type": query.Type, + "CxDescriptionID": query.CxDescriptionID, + }) + query_groups.append({ + "Name": query_group.Name, + "Language": query_group.Language, + "LanguageName": query_group.LanguageName, + "PackageTypeName": query_group.PackageTypeName, + "PackageId": query_group.PackageId, + "Queries": queries, + }) + return { + "IsSuccesfull": response.IsSuccesfull, + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryGroups": query_groups, + } + + def get_query_description(self, cwe_id: int) -> dict: + response = self.suds_client.execute( + "GetQueryDescription", sessionId="0", cweID=cwe_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryDescription": getattr(response, "QueryDescription", None), + } + + def get_query_short_description(self, query_id: int) -> dict: + response = self.suds_client.execute( + "GetQueryShortDescription", sessionId="0", queryId=query_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryShortDescription": getattr(response, "QueryShortDescription", None), + } + + def get_scans_display_data_for_all_projects(self) -> dict: + response = self.suds_client.execute( + "GetScansDisplayDataForAllProjects", sessionID="0" + ) + scans_list = getattr(response, "ScansDisplayData", None) + items = ( + getattr(scans_list, "ScanDisplayData", []) + if scans_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScansDisplayData": [ + { + "ScanID": item.ScanID, + "ProjectID": item.ProjectID, + "ProjectName": item.ProjectName, + "TeamName": item.TeamName, + "ScanStartDate": getattr(item, "ScanStartDate", None), + "ScanFinishDate": getattr(item, "ScanFinishDate", None), + "ScanStatus": getattr(item, "ScanStatus", None), + "ScanType": getattr(item, "ScanType", None), + "TotalResults": item.TotalResults, + "HighResults": item.HighResults, + "MediumResults": item.MediumResults, + "LowResults": item.LowResults, + "InfoResults": item.InfoResults, + "IsLocked": item.IsLocked, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_scan_summary(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetScanSummary", i_SessionID="0", i_ScanID=scan_id, auditEvent=False + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScanSummary": getattr(response, "ScanSummary", None), + "Partial": getattr(response, "Partial", None), + } + + def get_server_license_basic(self) -> dict: + response = self.suds_client.execute("GetServerLicenseBasic", sessionID="0") + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "LicenseExpirationDate": getattr(response, "LicenseExpirationDate", None), + "ShouldDisplayExpirationDate": response["ShouldDisplayExpirationDate"], + "Edition": getattr(response, "Edition", None), + } + + def get_server_license_data_extended(self) -> dict: + response = self.suds_client.execute( + "GetServerLicenseDataExtended", sessionID="0" + ) + supported_languages = response.SupportedLanguages + return { + "ExpirationDate": response["ExpirationDate"], + "ExpirationDateIso": getattr(response, "ExpirationDateIso", None), + "MaxConcurrentScans": response["MaxConcurrentScans"], + "MaxLOC": response["MaxLOC"], + "HID": response["HID"], + "SupportedLanguages": ( + [ + {"isSupported": item["isSupported"], "language": item["language"]} + for item in supported_languages["SupportedLanguage"] + ] + if supported_languages + else None + ), + "MaxUsers": response["MaxUsers"], + "CurrentUsers": response["CurrentUsers"], + "MaxAuditUsers": response["MaxAuditUsers"], + "CurrentAuditUsers": response["CurrentAuditUsers"], + "IsOsaEnabled": response["IsOsaEnabled"], + "OsaExpirationDate": response["OsaExpirationDate"], + "Edition": response["Edition"], + "ProjectsAllowed": response["ProjectsAllowed"], + "CurrentProjectsCount": response["CurrentProjectsCount"], + } + + def get_custom_fields(self) -> dict: + response = self.suds_client.execute("GetCustomFields", sessionID="0") + fields_array = getattr(response, "fieldsArray", None) + items = ( + getattr(fields_array, "CxWSCustomField", []) + if fields_array + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "fieldsArray": [ + { + "Id": item.Id, + "Name": item.Name, + "IsMandatory": item.IsMandatory, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_custom_field_values(self, project_id: int, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetCustomFieldValues", + sessionID="0", + projectID=project_id, + scanID=scan_id, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "customFieldValues": getattr(response, "customFieldValues", None), + } + + def get_result_paths_for_query( + self, scan_id: int, query_id: int + ) -> dict: + response = self.suds_client.execute( + "GetResultPathsForQuery", + sessionId="0", + scanId=scan_id, + queryId=query_id, + ) + paths = getattr(response, "Paths", None) + items = getattr(paths, "CxWSResultPath", []) if paths else [] + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Paths": [ + { + "PathId": item.PathId, + "SimilarityId": item.SimilarityId, + "Nodes": getattr(item, "Nodes", None), + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_results_for_query( + self, scan_id: int, query_id: int + ) -> dict: + response = self.suds_client.execute( + "GetResultsForQuery", + sessionID="0", + scanId=scan_id, + queryId=query_id, + ) + results = getattr(response, "Results", None) + items = ( + getattr(results, "CxWSSingleResultData", []) + if results + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "Results": [ + { + "QueryId": item.QueryId, + "PathId": item.PathId, + "SourceFolder": item.SourceFolder, + "SourceFile": item.SourceFile, + "SourceLine": item.SourceLine, + "SourceObject": item.SourceObject, + "DestFolder": item.DestFolder, + "DestFile": item.DestFile, + "DestLine": item.DestLine, + "NumberOfNodes": item.NumberOfNodes, + "DestObject": item.DestObject, + "Comment": item.Comment, + "State": item.State, + "Severity": item.Severity, + "AssignedUser": item.AssignedUser, + "ConfidenceLevel": item.ConfidenceLevel, + "ResultStatus": item.ResultStatus, + "IssueTicketID": item.IssueTicketID, + "QueryVersionCode": item.QueryVersionCode, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_queries_for_scan(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetQueriesForScan", sessionID="0", scanId=scan_id + ) + query_groups = getattr(response, "QueryGroups", None) + groups = ( + getattr(query_groups, "CxWSQueryGroup", []) + if query_groups + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "QueryGroups": [ + { + "Name": qg.Name, + "Language": qg.Language, + "LanguageName": qg.LanguageName, + "Queries": [ + { + "QueryId": q.QueryId, + "Name": q.Name, + "Severity": q.Severity, + } + for q in (getattr(qg.Queries, "CxWSQuery", []) or []) + ], + } + for qg in (groups if isinstance(groups, list) else [groups]) + ], + } + + def get_scan_properties(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetScanProperties", sessionID="0", ScanID=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScanProperties": getattr(response, "ScanProperties", None), + } + + def get_status_of_single_scan(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetStatusOfSingleScan", sessionID="0", runId=str(scan_id) + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScanStatus": getattr(response, "ScanStatus", None), + } + + def get_scans_statuses(self) -> dict: + response = self.suds_client.execute( + "GetScansStatuses", + sessionID="0", + ) + statuses = getattr(response, "ScansStatusesList", None) + items = ( + getattr(statuses, "ScanStatus", []) + if statuses + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScansStatusesList": [ + { + "ScanID": item.ScanID, + "Status": getattr(item, "Status", None), + "TotalPercent": item.TotalPercent, + "StagePercent": item.StagePercent, + "Stage": getattr(item, "Stage", None), + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_scan_logs(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "GetScanLogs", sessionID="0", scanId=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ScanLogs": getattr(response, "ScanLogs", None), + } + + def update_result_state( + self, + scan_id: int, + path_id: int, + project_id: int, + remarks: str = "", + result_label_type: str = "Remark", + data: str = "", + ) -> dict: + response = self.suds_client.execute( + "UpdateResultState", + sessionID="0", + scanId=scan_id, + PathId=path_id, + projectId=project_id, + Remarks=remarks, + ResultLabelType=result_label_type, + data=data, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def update_result_comment( + self, result_id: int, path_id: int, project_id: int, comment: str + ) -> dict: + response = self.suds_client.execute( + "UpdateResultComment", + sessionID="0", + ResultId=result_id, + PathId=path_id, + projectId=project_id, + comment=comment, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def update_scan_comment(self, scan_id: int, comment: str) -> dict: + response = self.suds_client.execute( + "UpdateScanComment", + sessionID="0", + ScanID=scan_id, + Comment=comment, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def is_valid_preset_name(self, name: str) -> dict: + response = self.suds_client.execute( + "IsValidPresetName", sessionID="0", presetName=name + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def get_server_language_list(self) -> dict: + response = self.suds_client.execute( + "GetServerLanguageList", sessionID="0" + ) + lang_list = getattr(response, "LanguageList", None) + items = ( + getattr(lang_list, "CxWSProjectLanguage", []) + if lang_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "LanguageList": [ + { + "LanguageName": item.LanguageName, + "LanguageOrder": getattr(item, "LanguageOrder", None), + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_executable_list(self) -> dict: + response = self.suds_client.execute( + "GetExecutableList", sessionId="0" + ) + exec_list = getattr(response, "ExecutableList", None) + items = ( + getattr(exec_list, "CxWSExecutable", []) + if exec_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ExecutableList": [ + { + "ExecutableName": item.ExecutableName, + "ExeSettings": getattr(item, "ExeSettings", None), + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def count_lines(self, source_code: str, language_name: str) -> dict: + response = self.suds_client.execute( + "CountLines", + sessionID="0", + sourceCode=source_code, + languageName=language_name, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "LineCount": getattr(response, "LineCount", None), + } + + def is_alive(self) -> dict: + response = self.suds_client._client.service.IsAlive() + return { + "IsSuccesfull": bool(response), + "ErrorMessage": None, + } + + def is_smtp_host_configured(self) -> dict: + response = self.suds_client.execute("IsSMTPHostConfigured") + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def is_private_cloud(self) -> dict: + response = self.suds_client.execute("IsPrivateCloud") + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def get_cwe_description(self, cwe_id: int) -> dict: + response = self.suds_client.execute( + "GetCWEDescription", sessionId="0", cweID=cwe_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "CWEDescription": getattr(response, "CWEDescription", None), + } + + def get_result_state_flags(self) -> dict: + response = self.suds_client.execute( + "GetResultStateFlags", sessionID="0" + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def cancel_scan(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "CancelScan", SessionID="0", scanID=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def delete_scan(self, scan_id: int) -> dict: + response = self.suds_client.execute( + "DeleteScan", sessionID="0", scanID=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def delete_scans(self, scan_ids: list) -> dict: + factory = self.suds_client.factory + scan_id_list = factory.ArrayOfLong(scan_ids) + response = self.suds_client.execute( + "DeleteScans", sessionID="0", scanIDs=scan_id_list + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + } + + def get_child_nodes( + self, team_id: str = "", level: int = 0, team_path: str = "" + ) -> dict: + response = self.suds_client.execute( + "GetChildNodes", + sessionID="0", + pTeamId=team_id, + pLevel=level, + pTeamPath=team_path, + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ChildNodes": getattr(response, "ChildNodes", None), + } + + def get_projects_with_scans(self) -> dict: + response = self.suds_client.execute( + "GetProjectsWithScans", sessionId="0" + ) + project_list = getattr(response, "projectList", None) + items = ( + getattr(project_list, "ProjectDisplayData", []) + if project_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "projectList": [ + { + "ProjectName": item.ProjectName, + "projectID": item.projectID, + "Group": item.Group, + "TotalScans": item.TotalScans, + "LastScanDate": getattr(item, "LastScanDate", None), + "Owner": item.Owner, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + + def get_configuration_set_list(self) -> dict: + response = self.suds_client.execute( + "GetConfigurationSetList", SessionID="0" + ) + config_list = getattr(response, "ConfigSetList", None) + items = ( + getattr(config_list, "ConfigurationSet", []) + if config_list + else [] + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "ErrorMessage": getattr(response, "ErrorMessage", None), + "ConfigSetList": [ + { + "ID": item.ID, + "ConfigSetName": item.ConfigSetName, + } + for item in (items if isinstance(items, list) else [items]) + ], + } + def add_license_expiration_notification() -> dict: return CxPortalWebService().add_license_expiration_notification() @@ -1616,3 +2354,207 @@ def get_file_names_for_path(scan_id: int, path_id: int) -> dict: def unlock_scan(scan_id: int) -> dict: return CxPortalWebService().unlock_scan(scan_id=scan_id) + + +def get_preset_details(preset_id: int) -> dict: + return CxPortalWebService().get_preset_details(preset_id=preset_id) + + +def update_preset(preset_id: int, query_ids: list, name: str) -> dict: + return CxPortalWebService().update_preset( + preset_id=preset_id, query_ids=query_ids, name=name + ) + + +def get_result_state_list() -> dict: + return CxPortalWebService().get_result_state_list() + + +def get_scan_report(report_id: int) -> dict: + return CxPortalWebService().get_scan_report(report_id=report_id) + + +def get_scan_report_status(report_id: int) -> dict: + return CxPortalWebService().get_scan_report_status(report_id=report_id) + + +def cancel_scan_report(report_id: int) -> dict: + return CxPortalWebService().cancel_scan_report(report_id=report_id) + + +def get_results(scan_id: int) -> dict: + return CxPortalWebService().get_results(scan_id=scan_id) + + +def get_result_summary(scan_id: int) -> dict: + return CxPortalWebService().get_result_summary(scan_id=scan_id) + + +def get_query_collection_for_language( + project_type: str = "Regular", project_id: int = 0 +) -> dict: + return CxPortalWebService().get_query_collection_for_language( + project_type=project_type, project_id=project_id + ) + + +def get_query_description(cwe_id: int) -> dict: + return CxPortalWebService().get_query_description(cwe_id=cwe_id) + + +def get_query_short_description(query_id: int) -> dict: + return CxPortalWebService().get_query_short_description(query_id=query_id) + + +def get_scans_display_data_for_all_projects() -> dict: + return CxPortalWebService().get_scans_display_data_for_all_projects() + + +def get_scan_summary(scan_id: int) -> dict: + return CxPortalWebService().get_scan_summary(scan_id=scan_id) + + +def get_server_license_basic() -> dict: + return CxPortalWebService().get_server_license_basic() + + +def get_server_license_data_extended() -> dict: + return CxPortalWebService().get_server_license_data_extended() + + +def get_custom_fields() -> dict: + return CxPortalWebService().get_custom_fields() + + +def get_custom_field_values(project_id: int, scan_id: int) -> dict: + return CxPortalWebService().get_custom_field_values( + project_id=project_id, scan_id=scan_id + ) + + +def get_result_paths_for_query(scan_id: int, query_id: int) -> dict: + return CxPortalWebService().get_result_paths_for_query( + scan_id=scan_id, query_id=query_id + ) + + +def get_results_for_query(scan_id: int, query_id: int) -> dict: + return CxPortalWebService().get_results_for_query( + scan_id=scan_id, query_id=query_id + ) + + +def get_queries_for_scan(scan_id: int) -> dict: + return CxPortalWebService().get_queries_for_scan(scan_id=scan_id) + + +def get_scan_properties(scan_id: int) -> dict: + return CxPortalWebService().get_scan_properties(scan_id=scan_id) + + +def get_status_of_single_scan(scan_id: int) -> dict: + return CxPortalWebService().get_status_of_single_scan(scan_id=scan_id) + + +def get_scans_statuses() -> dict: + return CxPortalWebService().get_scans_statuses() + + +def get_scan_logs(scan_id: int) -> dict: + return CxPortalWebService().get_scan_logs(scan_id=scan_id) + + +def update_result_state( + scan_id: int, + path_id: int, + project_id: int, + remarks: str = "", + result_label_type: str = "Remark", + data: str = "", +) -> dict: + return CxPortalWebService().update_result_state( + scan_id=scan_id, + path_id=path_id, + project_id=project_id, + remarks=remarks, + result_label_type=result_label_type, + data=data, + ) + + +def update_result_comment( + result_id: int, path_id: int, project_id: int, comment: str +) -> dict: + return CxPortalWebService().update_result_comment( + result_id=result_id, path_id=path_id, project_id=project_id, comment=comment + ) + + +def update_scan_comment(scan_id: int, comment: str) -> dict: + return CxPortalWebService().update_scan_comment(scan_id=scan_id, comment=comment) + + +def is_valid_preset_name(name: str) -> dict: + return CxPortalWebService().is_valid_preset_name(name=name) + + +def get_server_language_list() -> dict: + return CxPortalWebService().get_server_language_list() + + +def get_executable_list() -> dict: + return CxPortalWebService().get_executable_list() + + +def count_lines(source_code: str, language_name: str) -> dict: + return CxPortalWebService().count_lines( + source_code=source_code, language_name=language_name + ) + + +def is_alive() -> dict: + return CxPortalWebService().is_alive() + + +def is_smtp_host_configured() -> dict: + return CxPortalWebService().is_smtp_host_configured() + + +def is_private_cloud() -> dict: + return CxPortalWebService().is_private_cloud() + + +def get_cwe_description(cwe_id: int) -> dict: + return CxPortalWebService().get_cwe_description(cwe_id=cwe_id) + + +def get_result_state_flags() -> dict: + return CxPortalWebService().get_result_state_flags() + + +def cancel_scan(scan_id: int) -> dict: + return CxPortalWebService().cancel_scan(scan_id=scan_id) + + +def delete_scan(scan_id: int) -> dict: + return CxPortalWebService().delete_scan(scan_id=scan_id) + + +def delete_scans(scan_ids: list) -> dict: + return CxPortalWebService().delete_scans(scan_ids=scan_ids) + + +def get_child_nodes( + team_id: str = "", level: int = 0, team_path: str = "" +) -> dict: + return CxPortalWebService().get_child_nodes( + team_id=team_id, level=level, team_path=team_path + ) + + +def get_projects_with_scans() -> dict: + return CxPortalWebService().get_projects_with_scans() + + +def get_configuration_set_list() -> dict: + return CxPortalWebService().get_configuration_set_list() diff --git a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py index 6f21e011..6f1890d0 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py @@ -1,45 +1,110 @@ from .CxPortalWebService import ( CxPortalWebService, add_license_expiration_notification, + cancel_scan, + cancel_scan_report, + count_lines, create_new_preset, create_scan_report, delete_preset, + delete_project, + delete_projects, + delete_scan, + delete_scans, export_preset, export_queries, + get_associated_group_list, + get_child_nodes, get_compare_scan_results, + get_configuration_set_list, + get_custom_fields, + get_custom_field_values, + get_cwe_description, + get_executable_list, + get_file_names_for_path, get_import_queries_status, get_path_comments_history, get_pivot_data, - get_user_profile_data, + get_preset_details, + get_preset_list, + get_projects_display_data, + get_projects_with_scans, get_queries_categories, + get_queries_for_scan, get_query_collection, - get_query_id_by_language_group_and_query_name, + get_query_collection_for_language, + get_query_description, get_query_description_by_query_id, + get_query_id_by_language_group_and_query_name, + get_query_short_description, get_name_of_user_who_marked_false_positive_from_comments_history, - get_preset_list, - get_projects_display_data, - get_associated_group_list, get_result_path, + get_result_paths_for_query, + get_result_state_flags, + get_result_state_list, + get_result_summary, + get_results, + get_results_for_query, get_results_for_scan, + get_scan_logs, + get_scan_properties, + get_scan_report, + get_scan_report_status, + get_scan_summary, + get_scans_display_data_for_all_projects, + get_scans_statuses, + get_server_language_list, + get_server_license_basic, get_server_license_data, + get_server_license_data_extended, get_server_license_summary, - delete_project, - delete_projects, + get_source_by_scan_id, + get_sources_by_scan_id, + get_status_of_single_scan, + get_user_profile_data, get_version_number, get_version_number_as_int, import_preset, import_queries, + is_alive, + is_private_cloud, + is_smtp_host_configured, + is_valid_preset_name, lock_scan, postpone_scan, unlock_scan, - get_source_by_scan_id, - get_sources_by_scan_id, - get_file_names_for_path, + update_preset, + update_result_comment, + update_result_state, + update_scan_comment, ) from .CxAuditWebService import ( CxAuditWebService, + get_ancestry_group_tree, + get_cache, + get_engine_configuration, get_files_extensions, + get_hierarchy_group_tree, + get_license_details, + get_path_comments_history as audit_get_path_comments_history, + get_preset_details as audit_get_preset_details, + get_preset_list as audit_get_preset_list, + get_project_configuration, + get_project_scans, + get_projects_with_scans as audit_get_projects_with_scans, + get_queries_categories as audit_get_queries_categories, + get_query_collection as audit_get_query_collection, + get_query_collection_for_language as audit_get_query_collection_for_language, + get_query_description as audit_get_query_description, + get_query_description_by_query_id as audit_get_query_description_by_query_id, + get_result_state_list as audit_get_result_state_list, + get_result_summary as audit_get_result_summary, + get_results as audit_get_results, get_source_code_for_scan, + import_queries as audit_import_queries, + keep_alive, + update_result_state as audit_update_result_state, + update_scan_comment as audit_update_scan_comment, upload_queries, ) diff --git a/docs/CxSAST_Portal_SOAP_API_List.md b/docs/CxSAST_Portal_SOAP_API_List.md index 81505849..abee395e 100644 --- a/docs/CxSAST_Portal_SOAP_API_List.md +++ b/docs/CxSAST_Portal_SOAP_API_List.md @@ -1,36 +1,109 @@ # The CxSAST Portal SOAP API list 1. cx portal web service - add_license_expiration_notification + - cancel_scan + - cancel_scan_report + - count_lines - create_new_preset - create_scan_report + - delete_custom_field - delete_preset - delete_project - delete_projects + - delete_scan + - delete_scans - export_preset - export_queries - get_associated_group_list + - get_child_nodes - get_compare_scan_results + - get_configuration_set_list + - get_custom_field_values + - get_custom_fields + - get_cwe_description + - get_executable_list + - get_file_names_for_path - get_import_queries_status - get_path_comments_history + - get_pivot_data + - get_preset_details + - get_preset_list + - get_projects_display_data + - get_projects_with_scans - get_queries_categories + - get_queries_for_scan - get_query_collection + - get_query_collection_for_language + - get_query_collection_with_inactive + - get_query_description + - get_query_description_by_query_id - get_query_id_by_language_group_and_query_name + - get_query_short_description - get_name_of_user_who_marked_false_positive_from_comments_history - - get_preset_list - - get_projects_display_data - get_result_path + - get_result_paths_for_query + - get_result_state_flags + - get_result_state_list + - get_result_summary + - get_results + - get_results_by_severity + - get_results_for_query - get_results_for_scan + - get_scan_logs + - get_scan_properties + - get_scan_report + - get_scan_report_status + - get_scan_summary + - get_scans_display_data_for_all_projects + - get_scans_statuses + - get_server_language_list + - get_server_license_basic - get_server_license_data + - get_server_license_data_extended - get_server_license_summary + - get_source_by_scan_id + - get_sources_by_scan_id + - get_status_of_single_scan - get_user_profile_data - get_version_number - get_version_number_as_int - import_preset - import_queries + - is_alive + - is_private_cloud + - is_smtp_host_configured + - is_valid_preset_name - lock_scan + - postpone_scan - unlock_scan + - update_preset + - update_result_comment + - update_result_state + - update_scan_comment 2. cx Audit web service - get_files_extensions - get_source_code_for_scan - upload_queries - \ No newline at end of file + - get_results + - get_result_summary + - get_result_state_list + - update_result_state + - update_scan_comment + - get_project_scans + - get_projects_with_scans + - get_query_collection + - get_query_collection_for_language + - get_query_description + - get_query_description_by_query_id + - get_queries_categories + - get_preset_details + - get_preset_list + - get_path_comments_history + - get_project_configuration + - get_license_details + - get_engine_configuration + - get_hierarchy_group_tree + - get_ancestry_group_tree + - keep_alive + - import_queries + - get_cache diff --git a/tests/CxSAST/CxPortalSOAP/test_cx_audit_web_service.py b/tests/CxSAST/CxPortalSOAP/test_cx_audit_web_service.py index 47fed79c..49740110 100644 --- a/tests/CxSAST/CxPortalSOAP/test_cx_audit_web_service.py +++ b/tests/CxSAST/CxPortalSOAP/test_cx_audit_web_service.py @@ -1,7 +1,29 @@ +import pytest from CheckmarxPythonSDK.CxRestAPISDK import ScansAPI from CheckmarxPythonSDK.CxPortalSoapApiSDK import ( get_files_extensions, get_source_code_for_scan, + upload_queries, + audit_get_results as get_results, + audit_get_result_summary as get_result_summary, + audit_get_result_state_list as get_result_state_list, + get_project_scans, + audit_get_projects_with_scans as get_projects_with_scans, + audit_get_query_collection as get_query_collection, + audit_get_query_collection_for_language as get_query_collection_for_language, + audit_get_query_description as get_query_description, + audit_get_query_description_by_query_id as get_query_description_by_query_id, + audit_get_queries_categories as get_queries_categories, + audit_get_preset_details as get_preset_details, + audit_get_preset_list as get_preset_list, + audit_get_path_comments_history as get_path_comments_history, + get_project_configuration, + get_license_details, + get_engine_configuration, + get_hierarchy_group_tree, + get_ancestry_group_tree, + keep_alive, + get_cache, ) from .. import get_project_id @@ -27,3 +49,137 @@ def test_get_source_code_for_scan(): scan_id = _get_scan_id() response = get_source_code_for_scan(scan_id=scan_id) assert response is not None + + +@pytest.mark.skip(reason="upload_queries requires a query groups dict structure") +def test_upload_queries(): + response = upload_queries(query_groups=[]) + assert response is not None + + +def test_get_results(): + scan_id = _get_scan_id() + response = get_results(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +def test_get_result_summary(): + scan_id = _get_scan_id() + response = get_result_summary(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +def test_get_result_state_list(): + response = get_result_state_list() + assert response["IsSuccesfull"] is True + assert len(response["ResultStateList"]) > 0 + + +def test_get_project_scans(): + project_id = get_project_id() + response = get_project_scans(project_id=project_id) + assert response["IsSuccesfull"] is True + + +def test_get_projects_with_scans(): + response = get_projects_with_scans() + assert response["IsSuccesfull"] is True + + +def test_get_query_collection(): + response = get_query_collection() + assert response["IsSuccesfull"] is True + assert len(response["QueryGroups"]) > 0 + + +@pytest.mark.skip(reason="GetQueryCollectionForLanguage requires a valid project context") +def test_get_query_collection_for_language(): + response = get_query_collection_for_language( + project_type="Regular", project_id=0 + ) + assert response["IsSuccesfull"] is True + + +def test_get_query_description(): + response = get_query_description(cwe_id=79) + assert response is not None + + +def test_get_query_description_by_query_id(): + query_groups = get_query_collection().get("QueryGroups", []) + query_id = None + for g in query_groups: + for q in (g.get("Queries") or []): + query_id = q.get("QueryId") + break + if query_id: + break + assert query_id is not None + response = get_query_description_by_query_id(query_id=query_id) + assert response is not None + + +def test_get_queries_categories(): + response = get_queries_categories() + assert response is not None + + +def test_get_preset_details(): + response = get_preset_list() + assert response["IsSuccesfull"] is True + presets = response.get("PresetList", []) + if presets: + preset_id = presets[0]["ID"] + response = get_preset_details(preset_id=preset_id) + assert response["IsSuccesfull"] is True + assert response["preset"] is not None + + +def test_get_preset_list(): + response = get_preset_list() + assert response["IsSuccesfull"] is True + + +def test_get_path_comments_history(): + scan_id = _get_scan_id() + response = get_path_comments_history( + scan_id=scan_id, path_id=1, label_type="Remark" + ) + assert response.get("IsSuccesfull") is True + + +def test_get_project_configuration(): + project_id = get_project_id() + response = get_project_configuration(project_id=project_id) + assert response["IsSuccesfull"] is True + + +def test_get_license_details(): + response = get_license_details() + assert response["IsSuccesfull"] is True + + +def test_get_engine_configuration(): + response = get_engine_configuration() + assert response["IsSuccesfull"] is True + + +def test_get_hierarchy_group_tree(): + response = get_hierarchy_group_tree() + assert response["IsSuccesfull"] is True + + +def test_get_ancestry_group_tree(): + response = get_ancestry_group_tree() + assert response["IsSuccesfull"] is True + + +def test_keep_alive(): + response = keep_alive() + assert response is not None + + +@pytest.mark.skip(reason="get_cache requires a valid scan_id; use 0 for basic test") +def test_get_cache(): + response = get_cache(scan_id=0) + assert response["IsSuccesfull"] is True diff --git a/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py b/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py index d4e2648e..31cc3672 100644 --- a/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py +++ b/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py @@ -5,38 +5,76 @@ from CheckmarxPythonSDK.CxRestAPISDK import ProjectsAPI, ScansAPI from CheckmarxPythonSDK.CxPortalSoapApiSDK import ( add_license_expiration_notification, + cancel_scan_report, + count_lines, create_new_preset, create_scan_report, delete_preset, export_preset, export_queries, + get_child_nodes, + get_configuration_set_list, get_compare_scan_results, + get_custom_fields, + get_cwe_description, + get_executable_list, + get_file_names_for_path, get_import_queries_status, - get_query_collection, - get_query_id_by_language_group_and_query_name, - get_query_description_by_query_id, + get_preset_details, + get_path_comments_history, + get_pivot_data, get_preset_list, get_projects_display_data, + get_projects_with_scans, get_associated_group_list, + get_queries_categories, + get_queries_for_scan, + get_query_collection, + get_query_collection_for_language, + get_query_description, + get_query_description_by_query_id, + get_query_id_by_language_group_and_query_name, + get_query_short_description, + get_result_path, + get_result_paths_for_query, + get_result_state_flags, + get_result_state_list, + get_result_summary, + get_results, + get_results_for_query, + get_results_for_scan, + get_scan_logs, + get_scan_properties, + get_scan_report, + get_scan_report_status, + get_scan_summary, + get_scans_display_data_for_all_projects, + get_scans_statuses, + get_server_language_list, + get_server_license_basic, get_server_license_data, + get_server_license_data_extended, get_server_license_summary, + get_sources_by_scan_id, + get_source_by_scan_id, + get_status_of_single_scan, + get_user_profile_data, get_version_number, get_version_number_as_int, - get_path_comments_history, - get_user_profile_data, - get_queries_categories, get_name_of_user_who_marked_false_positive_from_comments_history, import_preset, import_queries, + is_alive, + is_private_cloud, + is_smtp_host_configured, + is_valid_preset_name, lock_scan, postpone_scan, unlock_scan, - get_results_for_scan, - get_result_path, - get_pivot_data, - get_sources_by_scan_id, - get_file_names_for_path, - get_source_by_scan_id, + update_preset, + update_result_comment, + update_result_state, + update_scan_comment, ) from .. import get_project_id @@ -359,3 +397,283 @@ def test_get_source_by_scan_id_deprecated(): ) assert response["IsSuccesfull"] is False assert "no longer supported" in response.get("ErrorMessage", "") + + +def test_get_preset_details(): + response = get_preset_list() + assert response["IsSuccesfull"] is True + presets = response.get("PresetList", []) + assert len(presets) > 0 + preset_id = presets[0]["ID"] + + response = get_preset_details(preset_id=preset_id) + assert response["IsSuccesfull"] is True + assert response["preset"] is not None + assert response["preset"]["id"] == preset_id + + +def test_update_preset(): + presets = get_preset_list().get("PresetList", []) + assert len(presets) > 0 + preset_id = presets[0]["ID"] + + details = get_preset_details(preset_id=preset_id) + query_ids = details["preset"]["queryIds"] + name = details["preset"]["name"] + + response = update_preset(preset_id=preset_id, query_ids=query_ids, name=name) + assert response["IsSuccesfull"] is True + + +def test_get_result_state_list(): + response = get_result_state_list() + assert response["IsSuccesfull"] is True + assert len(response["ResultStateList"]) > 0 + for item in response["ResultStateList"]: + assert "ResultName" in item + assert "ResultID" in item + + +def test_get_scan_summary(): + scan_id = _get_scan_id() + response = get_scan_summary(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +def test_get_scan_report_and_status(): + scan_id = _get_scan_id() + report = create_scan_report(scan_id=scan_id, report_type="PDF") + assert report["IsSuccesfull"] is True + report_id = report["ID"] + + status_response = get_scan_report_status(report_id=report_id) + assert status_response["IsSuccesfull"] is True + + cancel_response = cancel_scan_report(report_id=report_id) + assert cancel_response["IsSuccesfull"] is True + + +def test_get_results(): + """Portal GetResults is deprecated in 9.x; returns IsSuccesfull=False.""" + scan_id = _get_scan_id() + response = get_results(scan_id=scan_id) + assert response is not None + + +def test_get_result_summary(): + """GetResultSummary is deprecated in 9.x; returns IsSuccesfull=False.""" + scan_id = _get_scan_id() + response = get_result_summary(scan_id=scan_id) + assert response is not None + + +@pytest.mark.skip(reason="GetQueryCollectionForLanguage requires a valid project context") +def test_get_query_collection_for_language(): + from .. import get_project_id + project_id = get_project_id() + response = get_query_collection_for_language( + project_type="Regular", project_id=project_id + ) + assert response["IsSuccesfull"] is True + + +def test_get_query_description(): + response = get_query_description(cwe_id=79) + assert response is not None + + +def test_get_query_short_description(): + query_groups = get_query_collection().get("QueryGroups", []) + query_id = None + for g in query_groups: + for q in (g.get("Queries") or []): + query_id = q.get("QueryId") + break + if query_id: + break + assert query_id is not None + response = get_query_short_description(query_id=query_id) + assert response is not None + + +def test_get_scans_display_data_for_all_projects(): + response = get_scans_display_data_for_all_projects() + assert response["IsSuccesfull"] is True + + +def test_get_server_license_basic(): + response = get_server_license_basic() + assert response is not None + + +def test_get_server_license_data_extended(): + response = get_server_license_data_extended() + assert response is not None + + +def test_get_custom_fields(): + response = get_custom_fields() + assert response["IsSuccesfull"] is True + + +def test_get_cwe_description(): + response = get_cwe_description(cwe_id=79) + assert response["IsSuccesfull"] is True + + +def test_get_result_paths_for_query(): + scan_id = _get_scan_id() + query_groups = get_query_collection().get("QueryGroups", []) + query_id = None + for g in query_groups: + for q in (g.get("Queries") or []): + query_id = q.get("QueryId") + break + if query_id: + break + assert query_id is not None + response = get_result_paths_for_query( + scan_id=scan_id, + query_id=query_id, + ) + assert response["IsSuccesfull"] is True + + +def test_get_results_for_query(): + scan_id = _get_scan_id() + query_groups = get_query_collection().get("QueryGroups", []) + query_id = None + for g in query_groups: + for q in (g.get("Queries") or []): + query_id = q.get("QueryId") + break + if query_id: + break + assert query_id is not None + response = get_results_for_query( + scan_id=scan_id, + query_id=query_id, + ) + assert response["IsSuccesfull"] is True + + +def test_get_queries_for_scan(): + scan_id = _get_scan_id() + response = get_queries_for_scan(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +def test_get_scan_properties(): + scan_id = _get_scan_id() + response = get_scan_properties(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +@pytest.mark.skip(reason="get_status_of_single_scan requires an active runId, not a finished scan") +def test_get_status_of_single_scan(): + scan_id = _get_scan_id() + response = get_status_of_single_scan(scan_id=scan_id) + assert response is not None + + +def test_get_scans_statuses(): + response = get_scans_statuses() + assert response["IsSuccesfull"] is True + + +@pytest.mark.skip(reason="get_scan_logs may not return data for old scans") +def test_get_scan_logs(): + scan_id = _get_scan_id() + response = get_scan_logs(scan_id=scan_id) + assert response["IsSuccesfull"] is True + + +@pytest.mark.skip(reason="update_result_state requires project_id context") +def test_update_result_state_and_comment(): + scan_id = _get_scan_id() + results = get_results_for_scan(scan_id=scan_id) + scan_results = results.get("ScanResults", []) + from .. import get_project_id + project_id = get_project_id() + if scan_results: + path_id = scan_results[0]["PathId"] + state_response = update_result_state( + scan_id=scan_id, + path_id=path_id, + project_id=project_id, + remarks="test", + ) + assert state_response["IsSuccesfull"] is True + + comment_response = update_result_comment( + result_id=scan_id, + path_id=path_id, + project_id=project_id, + comment="test comment", + ) + assert comment_response is not None + + +def test_update_scan_comment(): + scan_id = _get_scan_id() + response = update_scan_comment(scan_id=scan_id, comment="test scan comment") + assert response["IsSuccesfull"] is True + + +def test_is_valid_preset_name(): + response = is_valid_preset_name(name="UniqueTestName_12345") + assert response["IsSuccesfull"] is True + + +def test_get_server_language_list(): + response = get_server_language_list() + assert response["IsSuccesfull"] is True + + +def test_get_executable_list(): + response = get_executable_list() + assert response["IsSuccesfull"] is True + + +@pytest.mark.skip(reason="count_lines requires a real source code string") +def test_count_lines(): + response = count_lines( + source_code="public class Test { public void foo() { int x = 1; } }", + language_name="Java", + ) + assert response["IsSuccesfull"] is True + + +def test_is_alive(): + response = is_alive() + assert response["IsSuccesfull"] is True + + +def test_is_smtp_host_configured(): + response = is_smtp_host_configured() + assert response is not None + + +def test_is_private_cloud(): + response = is_private_cloud() + assert response is not None + + +def test_get_result_state_flags(): + response = get_result_state_flags() + assert response is not None + + +def test_get_child_nodes(): + response = get_child_nodes() + assert response is not None + + +def test_get_projects_with_scans(): + response = get_projects_with_scans() + assert response["IsSuccesfull"] is True + + +def test_get_configuration_set_list(): + response = get_configuration_set_list() + assert response["IsSuccesfull"] is True From 6924ab390989516e17d52c71ebf34ea902273a87 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:46:58 +0800 Subject: [PATCH 19/25] Update CxOne examples: modify export_sast_state_by_query, remove export_sast_state_counts Co-Authored-By: Claude Opus 4.7 --- examples/CxOne/export_sast_state_by_query.py | 2 +- examples/CxOne/export_sast_state_counts.py | 145 ------------------- 2 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 examples/CxOne/export_sast_state_counts.py diff --git a/examples/CxOne/export_sast_state_by_query.py b/examples/CxOne/export_sast_state_by_query.py index 5638449d..7f8968cb 100644 --- a/examples/CxOne/export_sast_state_by_query.py +++ b/examples/CxOne/export_sast_state_by_query.py @@ -23,7 +23,7 @@ from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI from CheckmarxPythonSDK.CxOne.sastResultsAPI import SastResultsAPI -OUTPUT_CSV = Path(__file__).resolve().parent / "sast_state_by_query_v5.csv" +OUTPUT_CSV = Path(__file__).resolve().parent / "sast_state_by_query_v6.csv" STATES = ["TO_VERIFY", "CONFIRMED", "URGENT", "NOT_EXPLOITABLE", "PROPOSED_NOT_EXPLOITABLE"] diff --git a/examples/CxOne/export_sast_state_counts.py b/examples/CxOne/export_sast_state_counts.py deleted file mode 100644 index 2d04e1cc..00000000 --- a/examples/CxOne/export_sast_state_counts.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Get the last scan on the main branch for every project, retrieve SAST -result counts by state, and export a CSV for false-positive-rate analysis. - -Output CSV headers: - project_name, main_branch, scan_id, to_verify, confirmed, urgent, - not_exploitable, proposed_not_exploitable - -Secrets are read from a .env file in the project root. - -Usage: - python export_sast_state_counts.py -""" - -import csv -import os -from pathlib import Path - -from CheckmarxPythonSDK.configuration import Configuration -from CheckmarxPythonSDK.api_client import ApiClient -from CheckmarxPythonSDK.CxOne.projectsAPI import ProjectsAPI -from CheckmarxPythonSDK.CxOne.sastResultsAPI import SastResultsAPI - -OUTPUT_CSV = Path(__file__).resolve().parent / "sast_state_counts.csv" -STATES = ["TO_VERIFY", "CONFIRMED", "URGENT", - "NOT_EXPLOITABLE", "PROPOSED_NOT_EXPLOITABLE"] - - -def load_dotenv(): - env_path = Path(__file__).resolve().parents[2] / ".env" - if not env_path.exists(): - print(f"Warning: {env_path} not found, using environment variables.") - return - with open(env_path) as f: - for line in f: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, _, value = line.partition("=") - key = key.strip() - value = value.strip().strip("\"'") - os.environ[key] = value - - -def build_configuration() -> Configuration: - load_dotenv() - tenant_name = os.environ["CXONE_TENANT_NAME"] - access_control_url = os.environ.get( - "CXONE_IAM_URL", "https://sng.iam.checkmarx.net" - ) - server_base_url = os.environ.get( - "CXONE_SERVER_URL", "https://sng.ast.checkmarx.net" - ) - grant_type = os.environ.get("CXONE_GRANT_TYPE", "refresh_token") - client_id = os.environ.get("CXONE_CLIENT_ID", "ast-app") - client_secret = os.environ.get("CXONE_CLIENT_SECRET") - refresh_token = os.environ.get("CXONE_REFRESH_TOKEN") - if not refresh_token: - grant_type = "client_credentials" - return Configuration( - server_base_url=server_base_url, - iam_base_url=access_control_url, - token_url=( - f"{access_control_url}/auth/realms" - f"/{tenant_name}/protocol/openid-connect/token" - ), - tenant_name=tenant_name, - grant_type=grant_type, - client_id=client_id, - client_secret=client_secret, - api_key=refresh_token, - ) - - -def get_state_counts(sast_api: SastResultsAPI, scan_id: str): - """Return a dict of {state: count} for one scan.""" - counts = {} - for state in STATES: - result = sast_api.get_sast_results_by_scan_id( - scan_id=scan_id, state=[state], limit=1, - ) - counts[state] = result.get("totalCount", 0) - return counts - - -def main(): - configuration = build_configuration() - api_client = ApiClient(configuration=configuration) - - projects_api = ProjectsAPI(api_client=api_client) - sast_api = SastResultsAPI(api_client=api_client) - - # get all projects - all_projects = projects_api.get_all_projects() - print(f"Found {len(all_projects)} projects.") - - # build lookup: project_id -> project - project_lookup = {p.id: p for p in all_projects} - - # get last scan on main branch for all projects - project_ids = [p.id for p in all_projects] - last_scans = projects_api.get_last_scan_info( - project_ids=project_ids, use_main_branch=True, limit=100, - ) - print(f"Projects with a last scan on main branch: {len(last_scans)}") - - rows = [] - total = len(last_scans) - for i, (pid, scan) in enumerate(last_scans.items(), 1): - project = project_lookup.get(pid) - if not scan or not project: - continue - name = project.name - branch = project.main_branch or scan.branch or "" - scan_id = scan.id - print(f" [{i}/{total}] {name} scan={scan_id} ...", end=" ") - counts = get_state_counts(sast_api, scan_id) - total_issues = sum(counts.values()) - print(f"issues={total_issues}") - rows.append({ - "project_name": name, - "main_branch": branch, - "scan_id": scan_id, - "to_verify": counts["TO_VERIFY"], - "confirmed": counts["CONFIRMED"], - "urgent": counts["URGENT"], - "not_exploitable": counts["NOT_EXPLOITABLE"], - "proposed_not_exploitable": counts["PROPOSED_NOT_EXPLOITABLE"], - }) - - # write CSV - headers = [ - "project_name", "main_branch", "scan_id", - "to_verify", "confirmed", "urgent", - "not_exploitable", "proposed_not_exploitable", - ] - with open(OUTPUT_CSV, "w", newline="") as f: - writer = csv.DictWriter(f, fieldnames=headers) - writer.writeheader() - writer.writerows(rows) - print(f"\nWrote {len(rows)} rows to {OUTPUT_CSV}") - - -if __name__ == "__main__": - main() From b20de25305810e6b1d57c5394d89564c2b471366 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:26:36 +0800 Subject: [PATCH 20/25] Reformat API documentation to table format matching CxOne_REST_API_List.md Convert CxSAST_and_CxOSA_REST_API_List.md, CxSAST_Portal_SOAP_API_List.md, and CxSAST_ODATA_API.md to consistent markdown table format with columns for class, method, HTTP/query pattern, and endpoint path. Co-Authored-By: Claude Opus 4.7 --- docs/CxSAST_ODATA_API.md | 90 ++++-- docs/CxSAST_Portal_SOAP_API_List.md | 228 ++++++------- docs/CxSAST_and_CxOSA_REST_API_List.md | 425 +++++++++++++------------ 3 files changed, 394 insertions(+), 349 deletions(-) diff --git a/docs/CxSAST_ODATA_API.md b/docs/CxSAST_ODATA_API.md index c5f72632..945ea5f1 100644 --- a/docs/CxSAST_ODATA_API.md +++ b/docs/CxSAST_ODATA_API.md @@ -1,31 +1,61 @@ # The CxSAST OData API List - 1. Projects - - get_top_n_projects_by_risk_score - - get_top_n_projects_by_last_scan_duration - - get_all_projects_with_their_last_scan_and_the_high_vulnerabilities - - get_projects_that_have_high_vulnerabilities_in_the_last_scan - - get_the_number_of_issues_vulnerabilities_within_a_predefined_time_range_for_all_projects_in_a_team - - get_count_of_the_projects_in_the_system - - get_all_projects_with_a_custom_field_that_has_a_specific_value - - get_all_projects_with_a_custom_field_as_well_as_the_custom_field_information - - get_presets_associated_with_each_project - - get_all_projects_that_are_set_up_with_a_non_standard_configuration - - get_all_projects_id_name - - 2. Results - - get_results_for_a_specific_scan_id - - get_the_query_that_was_run_for_a_particular_unique_scan_result - - get_results_for_a_specific_scan_id_with_query_language_state - - get_results_group_by_query_id_and_add_count_json_format - - 3. Scans - - get_all_data_for_a_specific_scan_id - - get_number_of_loc_scanned_for_a_specific_scan - - get_number_of_loc_scanned_for_all_scan - - get_last_scan_id_of_a_project - - get_last_scan_of_a_project - - get_last_full_scan_id_of_a_project - - get_last_full_scan_of_a_project - - get_all_scans_within_a_predefined_time_range_and_their_h_m_l_values_for_a_project - - get_the_state_of_each_scan_result_since_a_specific_date_for_a_project - - get_all_scan_id_of_a_project \ No newline at end of file + +Base URL: `/Cxwebinterface/odata/v1/` + +## Projects + +| Python Class | Method | OData Query | +|---|---|---| +| ProjectsODataAPI | `get_top_n_projects_by_risk_score` | `Projects?$expand=LastScan&$orderby=LastScan/RiskScore desc&$top={n}` | +| ProjectsODataAPI | `get_top_n_projects_by_last_scan_duration` | `Projects?$expand=LastScan&$orderby=LastScan/ScanDuration desc&$top={n}` | +| ProjectsODataAPI | `get_all_projects_with_their_last_scan_and_the_high_vulnerabilities` | `Projects?$expand=LastScan($expand=Results($filter=Severity eq 'High'))` | +| ProjectsODataAPI | `get_projects_that_have_high_vulnerabilities_in_the_last_scan` | `Projects?$expand=LastScan($expand=Results)&$filter=LastScan/Results/any(r: r/Severity eq 'High')` | +| ProjectsODataAPI | `get_the_number_of_issues_vulnerabilities_within_a_predefined_time_range_for_all_projects_in_a_team` | `Projects?$filter=OwningTeamId eq {team_id}&$expand=Scans($expand=ResultSummary;$select=Id,ScanRequestedOn,ResultSummary;$filter=ScanRequestedOn gt {start} and ScanRequestedOn lt {end})` | +| ProjectsODataAPI | `get_count_of_the_projects_in_the_system` | `Projects/$count` | +| ProjectsODataAPI | `get_all_projects_with_a_custom_field_that_has_a_specific_value` | `Projects?$filter=CustomFields/any(f: f/FieldName eq '{name}' and f/FieldValue eq '{value}')` | +| ProjectsODataAPI | `get_all_projects_with_a_custom_field_as_well_as_the_custom_field_information` | `Projects?$expand=CustomFields&$filter=CustomFields/any(f: f/FieldName eq '{name}')` | +| ProjectsODataAPI | `get_presets_associated_with_each_project` | `Projects?$expand=Preset` | +| ProjectsODataAPI | `get_all_projects_that_are_set_up_with_a_non_standard_configuration` | `Projects?$filter=EngineConfigurationId gt 1` | +| ProjectsODataAPI | `get_all_projects_id_name` | `Projects?$select=Id,Name` | +| ProjectsODataAPI | `get_all_projects_id_name_and_team_id_name` | `Projects?$select=Id,Name,OwningTeamId&$expand=OwningTeam($select=FullName)` | +| ProjectsODataAPI | `get_all_scan_ids_within_a_predefined_time_range_for_all_projects_in_a_team` | `Projects?$select=Id,Name&$filter=OwningTeamId eq {team_id}&$expand=Scans($select=Id;$filter=ScanRequestedOn gt {start} and ScanRequestedOn lt {end};$orderby=Id)` | + +## Results + +| Python Class | Method | OData Query | +|---|---|---| +| ResultsODataAPI | `get_results_for_a_specific_scan_id` | `Scans({scan_id})/Results` | +| ResultsODataAPI | `get_the_query_that_was_run_for_a_particular_unique_scan_result` | `Results(Id={result_id},ScanId={scan_id})?$expand=Query($select=Name)` | +| ResultsODataAPI | `get_results_for_a_specific_scan_id_with_query_language_state` | `Scans({scan_id})/Results?$select=Id,ScanId,QueryId,SimilarityId,PathId&$expand=Query($select=Name;$expand=QueryGroup($select=Name,LanguageName)),State($select=Name),Scan($select=Origin,LOC)` | +| ResultsODataAPI | `get_results_group_by_query_id_and_add_count_json_format` | — (utility — groups results from `get_results_for_a_specific_scan_id_with_query_language_state`) | +| ResultsODataAPI | `get_results_for_a_specific_scan_id_with_similarity_ids` | `Scans({scan_id})/Results?$expand=Query($select=Name;$expand=QueryGroup($select=Name,LanguageName)),State($select=Name),Scan($select=Origin,LOC)&$filter=SimilarityId in (...)` | +| ResultsODataAPI | `get_number_of_results_for_a_specific_scan_id_with_result_state` | `Scans({scan_id})/Results?$select=Id&$filter=State/Id in ({states})` | +| ResultsODataAPI | `get_similarity_ids_of_a_scan` | `Scans({scan_id})/Results?$select=SimilarityId,PathId` | + +## Scans + +| Python Class | Method | OData Query | +|---|---|---| +| ScansODataAPI | `get_all_data_for_a_specific_scan_id` | `Scans({scan_id})` | +| ScansODataAPI | `get_number_of_loc_scanned_for_a_specific_scan` | `Scans({scan_id})?$select=LOC` | +| ScansODataAPI | `get_number_of_loc_scanned_for_all_scan` | `Scans?$select=LOC,Id` | +| ScansODataAPI | `get_last_scan_id_of_a_project` | `Projects({project_id})/Scans?$orderby=Id desc&$top=1&$select=Id` | +| ScansODataAPI | `get_last_scan_of_a_project` | `Projects({project_id})/Scans?$orderby=Id desc&$top=1` | +| ScansODataAPI | `get_last_full_scan_id_of_a_project` | `Projects({project_id})/Scans?$filter=IsIncremental eq false&$orderby=Id desc&$top=1&$select=Id` | +| ScansODataAPI | `get_last_full_scan_of_a_project` | `Projects({project_id})/Scans?$filter=IsIncremental eq false&$orderby=Id desc&$top=1` | +| ScansODataAPI | `get_all_scans_within_a_predefined_time_range_and_their_h_m_l_values_for_a_project` | `Projects({project_id})/Scans?$filter=ScanRequestedOn gt {start} and ScanRequestedOn lt {end}&$select=Id,ScanRequestedOn,High,Medium,Low&$orderby=ScanRequestedOn desc` | +| ScansODataAPI | `get_the_state_of_each_scan_result_since_a_specific_date_for_a_project` | `Scans?$filter=ProjectId eq {project_id} and ScanRequestedOn gt {start_date}&$expand=Results($expand=State;$select=Id,ScanId,StateId)` | +| ScansODataAPI | `get_all_scan_id_of_a_project` | `Projects({project_id})/Scans?$select=Id` | + +## Utilities (module-level functions) + +| Function | Description | +|---|---| +| `get_project_id_name_and_scan_id_list` | Composes `get_all_projects_id_name` + `get_all_scan_id_of_a_project` | +| `scan_results_group_by_query_id` | Groups results dict by QueryId (Python-only) | +| `get_all_results_with_count_for_each_project_json_format` | Composes `get_project_id_name_and_scan_id_list` + `get_results_group_by_query_id_and_add_count_json_format` | +| `merge_results_by_similarity_id` | Merges two result lists by SimilarityId (Python-only) | +| `get_result` | Compose: last scan → results with query/language/state for a project | +| `get_results_and_write_to_csv_file` | Writes scan results to CSV using `get_result` | +| `dump_last_scan_results_of_each_project_into_csv_file` | Writes raw last-scan results to CSV | +| `dump_last_scan_results_statistics_of_each_project_into_csv_file` | Writes aggregated last-scan statistics to CSV | diff --git a/docs/CxSAST_Portal_SOAP_API_List.md b/docs/CxSAST_Portal_SOAP_API_List.md index abee395e..9ae6a360 100644 --- a/docs/CxSAST_Portal_SOAP_API_List.md +++ b/docs/CxSAST_Portal_SOAP_API_List.md @@ -1,109 +1,119 @@ -# The CxSAST Portal SOAP API list -1. cx portal web service - - add_license_expiration_notification - - cancel_scan - - cancel_scan_report - - count_lines - - create_new_preset - - create_scan_report - - delete_custom_field - - delete_preset - - delete_project - - delete_projects - - delete_scan - - delete_scans - - export_preset - - export_queries - - get_associated_group_list - - get_child_nodes - - get_compare_scan_results - - get_configuration_set_list - - get_custom_field_values - - get_custom_fields - - get_cwe_description - - get_executable_list - - get_file_names_for_path - - get_import_queries_status - - get_path_comments_history - - get_pivot_data - - get_preset_details - - get_preset_list - - get_projects_display_data - - get_projects_with_scans - - get_queries_categories - - get_queries_for_scan - - get_query_collection - - get_query_collection_for_language - - get_query_collection_with_inactive - - get_query_description - - get_query_description_by_query_id - - get_query_id_by_language_group_and_query_name - - get_query_short_description - - get_name_of_user_who_marked_false_positive_from_comments_history - - get_result_path - - get_result_paths_for_query - - get_result_state_flags - - get_result_state_list - - get_result_summary - - get_results - - get_results_by_severity - - get_results_for_query - - get_results_for_scan - - get_scan_logs - - get_scan_properties - - get_scan_report - - get_scan_report_status - - get_scan_summary - - get_scans_display_data_for_all_projects - - get_scans_statuses - - get_server_language_list - - get_server_license_basic - - get_server_license_data - - get_server_license_data_extended - - get_server_license_summary - - get_source_by_scan_id - - get_sources_by_scan_id - - get_status_of_single_scan - - get_user_profile_data - - get_version_number - - get_version_number_as_int - - import_preset - - import_queries - - is_alive - - is_private_cloud - - is_smtp_host_configured - - is_valid_preset_name - - lock_scan - - postpone_scan - - unlock_scan - - update_preset - - update_result_comment - - update_result_state - - update_scan_comment -2. cx Audit web service - - get_files_extensions - - get_source_code_for_scan - - upload_queries - - get_results - - get_result_summary - - get_result_state_list - - update_result_state - - update_scan_comment - - get_project_scans - - get_projects_with_scans - - get_query_collection - - get_query_collection_for_language - - get_query_description - - get_query_description_by_query_id - - get_queries_categories - - get_preset_details - - get_preset_list - - get_path_comments_history - - get_project_configuration - - get_license_details - - get_engine_configuration - - get_hierarchy_group_tree - - get_ancestry_group_tree - - keep_alive - - import_queries - - get_cache +# The CxSAST Portal SOAP API List + +## Portal Web Service (`CxPortalWebService`) + +WSDL: `/CxWebInterface/Portal/CxWebService.asmx?wsdl` + +| Python Class | Method | Description | +|---|---|---| +| CxPortalWebService | `add_license_expiration_notification` | Add license expiration notification | +| CxPortalWebService | `cancel_scan` | Cancel a running scan | +| CxPortalWebService | `cancel_scan_report` | Cancel a scan report generation | +| CxPortalWebService | `count_lines` | Count lines of source code | +| CxPortalWebService | `create_new_preset` | Create a new preset | +| CxPortalWebService | `create_scan_report` | Create a scan report (PDF/RTF/CSV/XML) | +| CxPortalWebService | `delete_preset` | Delete a preset | +| CxPortalWebService | `delete_project` | Delete a project | +| CxPortalWebService | `delete_projects` | Delete multiple projects | +| CxPortalWebService | `delete_scan` | Delete a scan | +| CxPortalWebService | `delete_scans` | Delete multiple scans | +| CxPortalWebService | `export_preset` | Export a preset | +| CxPortalWebService | `export_queries` | Export queries | +| CxPortalWebService | `get_associated_group_list` | Get associated group/team list | +| CxPortalWebService | `get_child_nodes` | Get child nodes in team hierarchy | +| CxPortalWebService | `get_compare_scan_results` | Compare results between two scans | +| CxPortalWebService | `get_configuration_set_list` | Get configuration set list | +| CxPortalWebService | `get_custom_field_values` | Get custom field values for a project/scan | +| CxPortalWebService | `get_custom_fields` | Get all custom fields | +| CxPortalWebService | `get_cx_description_by_query_id` | Get cached Cx description by query ID | +| CxPortalWebService | `get_cwe_description` | Get CWE description by CWE ID | +| CxPortalWebService | `get_executable_list` | Get list of executables | +| CxPortalWebService | `get_file_names_for_path` | Get file names for a result path | +| CxPortalWebService | `get_import_queries_status` | Get import queries status | +| CxPortalWebService | `get_name_of_user_who_marked_false_positive_from_comments_history` | Extract false-positive marker name from comments | +| CxPortalWebService | `get_path_comments_history` | Get path comments history | +| CxPortalWebService | `get_pivot_data` | Get pivot data for dashboard | +| CxPortalWebService | `get_preset_details` | Get preset details by ID | +| CxPortalWebService | `get_preset_list` | Get list of all presets | +| CxPortalWebService | `get_projects_display_data` | Get projects display data | +| CxPortalWebService | `get_projects_with_scans` | Get projects that have scans | +| CxPortalWebService | `get_queries_categories` | Get query categories | +| CxPortalWebService | `get_queries_for_scan` | Get queries used in a scan | +| CxPortalWebService | `get_query_collection` | Get full query collection | +| CxPortalWebService | `get_query_collection_for_language` | Get query collection by project type | +| CxPortalWebService | `get_query_description` | Get query description by CWE ID | +| CxPortalWebService | `get_query_description_by_query_id` | Get query description by query ID | +| CxPortalWebService | `get_query_id_by_language_group_and_query_name` | Find query ID by language, group, and name | +| CxPortalWebService | `get_query_short_description` | Get short query description | +| CxPortalWebService | `get_result_path` | Get result path detail | +| CxPortalWebService | `get_result_paths_for_query` | Get result paths for a query | +| CxPortalWebService | `get_result_state_flags` | Get result state flags | +| CxPortalWebService | `get_result_state_list` | Get list of result states | +| CxPortalWebService | `get_result_summary` | Get result summary for a scan | +| CxPortalWebService | `get_results` | Get results for a scan | +| CxPortalWebService | `get_results_for_query` | Get results for a specific query | +| CxPortalWebService | `get_results_for_scan` | Get results for a scan | +| CxPortalWebService | `get_scan_logs` | Get scan logs | +| CxPortalWebService | `get_scan_properties` | Get scan properties | +| CxPortalWebService | `get_scan_report` | Get a generated scan report | +| CxPortalWebService | `get_scan_report_status` | Get scan report generation status | +| CxPortalWebService | `get_scan_summary` | Get scan summary | +| CxPortalWebService | `get_scans_display_data_for_all_projects` | Get scans display data for all projects | +| CxPortalWebService | `get_scans_statuses` | Get statuses of all scans | +| CxPortalWebService | `get_server_language_list` | Get server language list | +| CxPortalWebService | `get_server_license_basic` | Get basic server license info | +| CxPortalWebService | `get_server_license_data` | Get full server license data | +| CxPortalWebService | `get_server_license_data_extended` | Get extended server license data | +| CxPortalWebService | `get_server_license_summary` | Get server license summary | +| CxPortalWebService | `get_source_by_scan_id` | Get source by scan ID (deprecated in 9.x) | +| CxPortalWebService | `get_sources_by_scan_id` | Get sources by scan ID | +| CxPortalWebService | `get_status_of_single_scan` | Get status of a single scan | +| CxPortalWebService | `get_user_profile_data` | Get user profile data | +| CxPortalWebService | `get_version_number` | Get server version number | +| CxPortalWebService | `get_version_number_as_int` | Get server version number as integer | +| CxPortalWebService | `import_preset` | Import a preset from file | +| CxPortalWebService | `import_queries` | Import queries from file | +| CxPortalWebService | `is_alive` | Check if server is alive | +| CxPortalWebService | `is_private_cloud` | Check if private cloud | +| CxPortalWebService | `is_smtp_host_configured` | Check if SMTP host is configured | +| CxPortalWebService | `is_valid_preset_name` | Check if a preset name is valid | +| CxPortalWebService | `lock_scan` | Lock a scan | +| CxPortalWebService | `postpone_scan` | Postpone a queued scan | +| CxPortalWebService | `unlock_scan` | Unlock a scan | +| CxPortalWebService | `update_preset` | Update a preset | +| CxPortalWebService | `update_result_comment` | Update a result comment | +| CxPortalWebService | `update_result_state` | Update a result state | +| CxPortalWebService | `update_scan_comment` | Update a scan comment | + +## Audit Web Service (`CxAuditWebService`) + +WSDL: `/cxwebinterface/Audit/CxAuditWebService.asmx?wsdl` + +| Python Class | Method | Description | +|---|---|---| +| CxAuditWebService | `get_files_extensions` | Get file extensions | +| CxAuditWebService | `get_source_code_for_scan` | Get source code zip for a scan | +| CxAuditWebService | `upload_queries` | Upload query groups | +| CxAuditWebService | `get_results` | Get results for a scan | +| CxAuditWebService | `get_result_summary` | Get result summary for a scan | +| CxAuditWebService | `get_result_state_list` | Get list of result states | +| CxAuditWebService | `update_result_state` | Update a result state | +| CxAuditWebService | `update_scan_comment` | Update a scan comment | +| CxAuditWebService | `get_project_scans` | Get scans for a project | +| CxAuditWebService | `get_projects_with_scans` | Get projects that have scans | +| CxAuditWebService | `get_query_collection` | Get full query collection | +| CxAuditWebService | `get_query_collection_for_language` | Get query collection by project type | +| CxAuditWebService | `get_query_description` | Get query description by CWE ID | +| CxAuditWebService | `get_query_description_by_query_id` | Get query description by query ID | +| CxAuditWebService | `get_queries_categories` | Get query categories | +| CxAuditWebService | `get_preset_details` | Get preset details by ID | +| CxAuditWebService | `get_preset_list` | Get list of all presets | +| CxAuditWebService | `get_path_comments_history` | Get path comments history | +| CxAuditWebService | `get_project_configuration` | Get project configuration | +| CxAuditWebService | `get_license_details` | Get license details | +| CxAuditWebService | `get_engine_configuration` | Get engine configuration | +| CxAuditWebService | `get_hierarchy_group_tree` | Get hierarchy group tree | +| CxAuditWebService | `get_ancestry_group_tree` | Get ancestry group tree | +| CxAuditWebService | `keep_alive` | Keep audit session alive | +| CxAuditWebService | `import_queries` | Import queries from file | +| CxAuditWebService | `get_cache` | Get cache for a scan | diff --git a/docs/CxSAST_and_CxOSA_REST_API_List.md b/docs/CxSAST_and_CxOSA_REST_API_List.md index 9d8c3dd6..c0f4ac86 100644 --- a/docs/CxSAST_and_CxOSA_REST_API_List.md +++ b/docs/CxSAST_and_CxOSA_REST_API_List.md @@ -1,211 +1,216 @@ -# The CxSAST and CxOSA REST API list +# The CxSAST and CxOSA REST API List -1. For REST API, use Bearer Token for authentication - - auth_headers (This is a global variable that stored token) -2. TeamAPI - - create_team - - get_all_teams - - get_team_id_by_team_full_name **(provided by SDK)** - - get_team_full_name_by_team_id **(provided by SDK)** -3. ProjectsAPI - - get_all_project_details - - create_project_with_default_configuration - - get_project_id_by_project_name_and_team_full_name **(provided by SDK)** - - get_project_details_by_id - - update_project_by_id - - update_project_name_team_id - - delete_project_by_id - - create_project_if_not_exists_by_project_name_and_team_full_name **(provided by SDK)** - - delete_project_if_exists_by_project_name_and_team_full_name **(provided by SDK)** - - create_branched_project - - get_all_issue_tracking_systems - - get_issue_tracking_system_id_by_name - - get_issue_tracking_system_details_by_id - - get_project_exclude_settings_by_project_id - - set_project_exclude_settings_by_project_id - - get_remote_source_settings_for_git_by_project_id - - set_remote_source_setting_to_git - - get_remote_source_settings_for_svn_by_project_id - - set_remote_source_settings_to_svn - - get_remote_source_settings_for_tfs_by_project_id - - set_remote_source_settings_to_tfs - - get_remote_source_settings_for_custom_by_project_id - - set_remote_source_setting_for_custom_by_project_id - - get_remote_source_settings_for_shared_by_project_id - - set_remote_source_settings_to_shared - - get_remote_source_settings_for_perforce_by_project_id - - set_remote_source_settings_to_perforce - - set_remote_source_setting_to_git_using_ssh - - set_remote_source_setting_to_svn_using_ssh - - upload_source_code_zip_file - - set_data_retention_settings_by_project_id - - set_issue_tracking_system_as_jira_by_id - - get_all_preset_details - - get_preset_id_by_name - - get_preset_details_by_preset_id - - set_project_queue_setting - - update_project_queue_setting - - set_project_next_scheduled_scan_to_be_excluded_from_no_code_change_detection -4. CustomTasksAPI - - get_all_custom_tasks - - get_custom_task_id_by_name - - get_custom_task_by_id - - get_custom_task_by_name -5. CustomFieldsAPI - - get_all_custom_fields - - get_custom_field_id_by_name -6. ScansAPI - - get_all_scans_for_project - - get_last_scan_id_of_a_project - - create_new_scan - - get_sast_scan_details_by_scan_id - - add_or_update_a_comment_by_scan_id - - delete_scan_by_scan_id - - get_statistics_results_by_scan_id - - get_scan_queue_details_by_scan_id - - update_queued_scan_status_by_scan_id - - get_all_scan_details_in_queue - - get_scan_settings_by_project_id - - define_sast_scan_settings - - update_sast_scan_settings - - define_sast_scan_scheduling_settings - - assign_ticket_to_scan_results - - publish_last_scan_results_to_management_and_orchestration_by_project_id - - get_the_publish_last_scan_results_to_management_and_orchestration_status - - get_short_vulnerability_description_for_a_scan_result - - register_scan_report - - get_report_status_by_id - - get_report_by_id - - is_scanning_finished **(provided by SDK)** - - is_report_generation_finished **(provided by SDK)** - - get_scan_results_of_a_specific_query - - get_scan_results_for_a_specific_query_group_by_best_fix_location - - update_scan_result_labels_fields - - create_new_scan_with_settings - - get_scan_result_labels_fields - - get_scan_logs - - get_basic_metrics_of_a_scan - - get_parsed_files_metrics_of_a_scan - - get_failed_queries_metrics_of_a_scan - - get_failed_general_queries_metrics_of_a_scan - - get_succeeded_general_queries_metrics_of_a_scan -7. DataRetentionAPI - - stop_data_retention - - define_data_retention_date_range - - define_data_retention_by_number_of_scans - - get_data_retention_request_status - - define_data_retention_by_rolling_date - - define_data_retention_by_rolling_months -8. EnginesAPI - - get_all_engine_server_details - - get_engine_id_by_name - - register_engine - - unregister_engine_by_engine_id - - get_engine_details - - update_engine_server - - get_all_engine_configurations - - get_engine_configuration_id_by_name - - get_engine_configuration_by_id -9. OsaAPI - - get_all_osa_scan_details_for_project - - get_last_osa_scan_id_of_a_project - - get_osa_scan_by_scan_id - - create_an_osa_scan_request - - get_all_osa_file_extensions - - get_osa_licenses_by_id - - get_osa_scan_libraries - - get_osa_scan_vulnerabilities_by_id - - get_first_vulnerability_id - - get_osa_scan_vulnerability_comments_by_id - - get_osa_scan_summary_report -10. possible Exceptions - - BadRequestError **(provided by SDK)** - - NotFoundError **(provided by SDK)** - - CxError **(provided by SDK)** -11. AccessControlAPI - - get_all_assignable_users - - get_all_authentication_providers - - submit_first_admin_user - - get_admin_user_exists_confirmation - - get_all_ldap_role_mapping - - update_ldap_role_mapping - - delete_ldap_role_mapping - - test_ldap_server_connection - - get_user_entries_by_search_criteria - - get_group_entries_by_search_criteria - - get_all_ldap_servers - - create_new_ldap_server - - get_ldap_server_by_id - - update_ldap_server - - delete_ldap_server - - get_ldap_team_mapping - - update_ldap_team_mapping - - delete_ldap_team_mapping - - get_my_profile - - update_my_profile - - get_all_oidc_clients - - create_new_oidc_client - - get_oidc_client_by_id - - update_an_oidc_client - - delete_an_oidc_client - - get_all_permissions - - get_permission_by_id - - get_all_roles - - get_role_id_by_name - - create_new_role - - get_role_by_id - - update_a_role - - delete_a_role - - get_all_saml_identity_providers - - create_new_saml_identity_provider - - get_saml_identity_provider_by_id - - update_new_saml_identity_provider - - delete_a_saml_identity_provider - - get_details_of_saml_role_mappings - - set_saml_group_and_role_mapping_details - - get_saml_service_provider_metadata - - get_saml_service_provider - - update_a_saml_service_provider - - get_details_of_saml_team_mappings - - set_saml_group_and_team_mapping_details - - get_all_service_providers - - get_service_provider_by_id - - get_all_smtp_settings - - create_smtp_settings - - get_smtp_settings_by_id - - update_smtp_settings - - delete_smtp_settings - - test_smtp_connection - - get_all_system_locales - - get_members_by_team_id - - update_members_by_team_id - - add_a_user_to_a_team - - delete_a_member_from_a_team - - get_all_teams - - get_team_id_by_full_name - - create_new_team - - get_team_by_id - - update_a_team - - delete_a_team - - generate_a_new_token_signing_certificate - - upload_a_new_token_signing_certificate - - get_all_users - - get_user_id_by_name - - create_new_user - - get_user_by_id - - update_a_user - - delete_a_user - - migrate_existing_user - - get_all_windows_domains - - get_windows_domain_id_by_name - - create_a_new_windows_domain - - get_windows_domain_by_id - - update_a_windows_domain - - delete_a_windows_domain - - get_windows_domain_user_entries_by_search_criteria -12. Configuration API - - get_cx_component_configuration_settings - - update_cx_component_configuration_settings -13 . Queries API - - get_the_full_description_of_the_query - - get_query_id_and_query_version_code \ No newline at end of file +| Python Class | Method | HTTP | Endpoint Path | +|---|---|---|---| +| TeamAPI | `get_all_teams` | GET | `/cxrestapi/auth/teams` | +| TeamAPI | `create_team` | POST | `/cxrestapi/auth/teams` | +| TeamAPI | `get_team_id_by_team_full_name` | — | (utility) | +| TeamAPI | `get_team_full_name_by_team_id` | — | (utility) | +| ProjectsAPI | `get_all_project_details` | GET | `/cxrestapi/projects` | +| ProjectsAPI | `create_project_with_default_configuration` | POST | `/cxrestapi/projects` | +| ProjectsAPI | `get_project_id_by_project_name_and_team_full_name` | — | (utility) | +| ProjectsAPI | `get_project_details_by_id` | GET | `/cxrestapi/projects/{project_id}` | +| ProjectsAPI | `update_project_by_id` | PUT | `/cxrestapi/projects/{project_id}` | +| ProjectsAPI | `update_project_name_team_id` | PATCH | `/cxrestapi/projects/{project_id}` | +| ProjectsAPI | `delete_project_by_id` | DELETE | `/cxrestapi/projects/{project_id}` | +| ProjectsAPI | `create_project_if_not_exists_by_project_name_and_team_full_name` | — | (utility) | +| ProjectsAPI | `delete_project_if_exists_by_project_name_and_team_full_name` | — | (utility) | +| ProjectsAPI | `create_branched_project` | POST | `/cxrestapi/projects/{project_id}/branch` | +| ProjectsAPI | `get_branch_project_status` | GET | `/cxrestapi/projects/branch/{branch_project_id}` | +| ProjectsAPI | `get_project_branching_status` | GET | `/cxrestapi/projects/branch/{project_id}` | +| ProjectsAPI | `get_all_issue_tracking_systems` | GET | `/cxrestapi/issueTrackingSystems` | +| ProjectsAPI | `get_issue_tracking_system_id_by_name` | — | (utility) | +| ProjectsAPI | `get_issue_tracking_system_details_by_id` | GET | `/cxrestapi/issueTrackingSystems/{id}/metadata` | +| ProjectsAPI | `get_project_exclude_settings_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/excludeSettings` | +| ProjectsAPI | `set_project_exclude_settings_by_project_id` | PUT | `/cxrestapi/projects/{project_id}/sourceCode/excludeSettings` | +| ProjectsAPI | `get_remote_source_settings_for_git_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/git` | +| ProjectsAPI | `set_remote_source_setting_to_git` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/git` | +| ProjectsAPI | `get_remote_source_settings_for_svn_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/svn` | +| ProjectsAPI | `set_remote_source_settings_to_svn` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/svn` | +| ProjectsAPI | `get_remote_source_settings_for_tfs_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/tfs` | +| ProjectsAPI | `set_remote_source_settings_to_tfs` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/tfs` | +| ProjectsAPI | `get_remote_source_settings_for_custom_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/custom` | +| ProjectsAPI | `set_remote_source_setting_for_custom_by_project_id` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/custom` | +| ProjectsAPI | `get_remote_source_settings_for_shared_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/shared` | +| ProjectsAPI | `set_remote_source_settings_to_shared` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/shared` | +| ProjectsAPI | `get_remote_source_settings_for_perforce_by_project_id` | GET | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/perforce` | +| ProjectsAPI | `set_remote_source_settings_to_perforce` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/perforce` | +| ProjectsAPI | `set_remote_source_setting_to_git_using_ssh` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/git/ssh` | +| ProjectsAPI | `set_remote_source_setting_to_svn_using_ssh` | POST | `/cxrestapi/projects/{project_id}/sourceCode/remoteSettings/svn/ssh` | +| ProjectsAPI | `upload_source_code_zip_file` | POST | `/cxrestapi/projects/{project_id}/sourceCode/attachments` | +| ProjectsAPI | `set_data_retention_settings_by_project_id` | POST | `/cxrestapi/projects/{project_id}/dataRetentionSettings` | +| ProjectsAPI | `set_issue_tracking_system_as_jira_by_id` | POST | `/cxrestapi/projects/{project_id}/issueTrackingSettings/jira` | +| ProjectsAPI | `get_all_preset_details` | GET | `/cxrestapi/sast/presets` | +| ProjectsAPI | `get_preset_id_by_name` | — | (utility) | +| ProjectsAPI | `get_preset_details_by_preset_id` | GET | `/cxrestapi/sast/presets/{preset_id}` | +| ProjectsAPI | `set_project_queue_setting` | POST | `/cxrestapi/sast/project/{project_id}/queueSettings` | +| ProjectsAPI | `update_project_queue_setting` | PUT | `/cxrestapi/sast/project/{project_id}/queueSettings` | +| ProjectsAPI | `set_project_next_scheduled_scan_to_be_excluded_from_no_code_change_detection` | POST | `/cxrestapi/projects/{project_id}/forceScanOnNoCodeChanges` | +| ProjectsAPI | `force_scan_on_no_code_changes` | POST | `/cxrestapi/projects/{project_id}/forceScanOnNoCodeChanges` | +| ProjectsAPI | `precheck_team` | GET | `/cxrestapi/projects/precheck/teams/{team_id}` | +| ProjectsAPI | `get_path_filter` | GET | `/cxrestapi/projects/{project_id}/sourceCode/pathFilter` | +| ProjectsAPI | `set_path_filter` | PUT | `/cxrestapi/projects/{project_id}/sourceCode/pathFilter` | +| ProjectsAPI | `get_project_validity_for_running_incremental_scan` | GET | `/cxrestapi/projects/{project_id}/incrementalScanValidityStatus` | +| CustomTasksAPI | `get_all_custom_tasks` | GET | `/cxrestapi/customTasks` | +| CustomTasksAPI | `get_custom_task_id_by_name` | — | (utility) | +| CustomTasksAPI | `get_custom_task_by_id` | GET | `/cxrestapi/customTasks/{task_id}` | +| CustomTasksAPI | `get_custom_task_by_name` | GET | `/cxrestapi/customTasks/name/{task_name}` | +| CustomFieldsAPI | `get_all_custom_fields` | GET | `/cxrestapi/customFields` | +| CustomFieldsAPI | `get_custom_field_id_by_name` | — | (utility) | +| ScansAPI | `get_all_scans_for_project` | GET | `/cxrestapi/sast/scans` | +| ScansAPI | `get_last_scan_id_of_a_project` | — | (utility) | +| ScansAPI | `create_new_scan` | POST | `/cxrestapi/sast/scans` | +| ScansAPI | `get_sast_scan_details_by_scan_id` | GET | `/cxrestapi/sast/scans/{scan_id}` | +| ScansAPI | `add_or_update_a_comment_by_scan_id` | PATCH | `/cxrestapi/sast/scans/{scan_id}` | +| ScansAPI | `delete_scan_by_scan_id` | DELETE | `/cxrestapi/sast/scans/{scan_id}` | +| ScansAPI | `get_statistics_results_by_scan_id` | GET | `/cxrestapi/sast/scans/{scan_id}/resultsStatistics` | +| ScansAPI | `get_scan_queue_details_by_scan_id` | GET | `/cxrestapi/sast/scansQueue/{scan_id}` | +| ScansAPI | `update_queued_scan_status_by_scan_id` | PATCH | `/cxrestapi/sast/scansQueue/{scan_id}` | +| ScansAPI | `cancel_scan` | PATCH | `/cxrestapi/sast/scansQueue/{scan_id}` | +| ScansAPI | `get_all_scan_details_in_queue` | GET | `/cxrestapi/sast/scansQueue` | +| ScansAPI | `get_scan_settings_by_project_id` | GET | `/cxrestapi/sast/scanSettings/{project_id}` | +| ScansAPI | `define_sast_scan_settings` | POST | `/cxrestapi/sast/scanSettings` | +| ScansAPI | `update_sast_scan_settings` | PUT | `/cxrestapi/sast/scanSettings` | +| ScansAPI | `define_sast_scan_scheduling_settings` | PUT | `/cxrestapi/sast/project/{project_id}/scheduling` | +| ScansAPI | `assign_ticket_to_scan_results` | POST | `/cxrestapi/sast/results/tickets` | +| ScansAPI | `publish_last_scan_results_to_management_and_orchestration_by_project_id` | POST | `/cxrestapi/sast/projects/{project_id}/publisher/policyFindings` | +| ScansAPI | `get_the_publish_last_scan_results_to_management_and_orchestration_status` | GET | `/cxrestapi/sast/projects/{project_id}/publisher/policyFindings/status` | +| ScansAPI | `get_short_vulnerability_description_for_a_scan_result` | GET | `/cxrestapi/sast/scans/{scan_id}/results/{path_id}/shortDescription` | +| ScansAPI | `register_scan_report` | POST | `/cxrestapi/reports/sastScan` | +| ScansAPI | `get_report_status_by_id` | GET | `/cxrestapi/reports/sastScan/{report_id}/status` | +| ScansAPI | `get_report_by_id` | GET | `/cxrestapi/reports/sastScan/{report_id}` | +| ScansAPI | `is_scanning_finished` | — | (utility) | +| ScansAPI | `is_report_generation_finished` | — | (utility) | +| ScansAPI | `get_scan_results_of_a_specific_query` | GET | `/cxrestapi/sast/results/attack-vectors` | +| ScansAPI | `get_scan_results_for_a_specific_query_group_by_best_fix_location` | GET | `/cxrestapi/sast/results/attack-vectors-by-bfl` | +| ScansAPI | `update_scan_result_labels_fields` | PATCH | `/cxrestapi/sast/scans/{scan_id}/results/{result_id}/labels` | +| ScansAPI | `create_new_scan_with_settings` | POST | `/cxrestapi/sast/scanWithSettings` | +| ScansAPI | `get_scan_result_labels_fields` | GET | `/cxrestapi/sast/scans/{scan_id}/results/{result_id}/labels` | +| ScansAPI | `get_scan_logs` | GET | `/cxrestapi/sast/scans/{scan_id}/logs` | +| ScansAPI | `get_basic_metrics_of_a_scan` | GET | `/cxrestapi/sast/scans/{scan_id}/statistics` | +| ScansAPI | `get_parsed_files_metrics_of_a_scan` | GET | `/cxrestapi/sast/scans/{scan_id}/parsedFiles` | +| ScansAPI | `get_failed_queries_metrics_of_a_scan` | GET | `/cxrestapi/sast/scans/{scan_id}/failedQueries` | +| ScansAPI | `get_failed_general_queries_metrics_of_a_scan` | GET | `/cxrestapi/sast/scans/{scan_id}/failedGeneralQueries` | +| ScansAPI | `get_succeeded_general_queries_metrics_of_a_scan` | GET | `/cxrestapi/sast/scans/{scan_id}/succeededGeneralQueries` | +| ScansAPI | `get_result_path_comments_history` | GET | `/cxrestapi/sast/resultPathCommentsHistory` | +| ScansAPI | `lock_scan` | PUT | `/cxrestapi/sast/lockScan` | +| ScansAPI | `unlock_scan` | PUT | `/cxrestapi/sast/unLockScan` | +| ScansAPI | `get_scan_result_labels_action_fields` | GET | `/cxrestapi/sast/scans/{scan_id}/actionResults/{path_id}/labels` | +| ScansAPI | `get_compare_results_of_two_scans` | GET | `/cxrestapi/sast/scans/{old_scan_id}/compareResultsTo/{new_scan_id}` | +| ScansAPI | `get_compare_results_summary_of_two_scans` | GET | `/cxrestapi/sast/scans/{old_scan_id}/compareSummaryTo/{new_scan_id}` | +| ScansAPI | `get_a_collection_of_scans_by_project` | GET | `/cxrestapi/sast/scans` | +| ScansAPI | `get_scan_results_in_paged_mode` | GET | `/cxrestapi/sast/results` | +| ScansAPI | `get_all_scan_results` | — | (utility — paginates `get_scan_results_in_paged_mode`) | +| DataRetentionAPI | `stop_data_retention` | POST | `/cxrestapi/sast/dataRetention/stop` | +| DataRetentionAPI | `define_data_retention_date_range` | POST | `/cxrestapi/sast/dataRetention/byDateRange` | +| DataRetentionAPI | `define_data_retention_by_number_of_scans` | POST | `/cxrestapi/sast/dataRetention/byNumberOfScans` | +| DataRetentionAPI | `get_data_retention_request_status` | GET | `/cxrestapi/sast/dataRetention/{request_id}/status` | +| DataRetentionAPI | `define_data_retention_by_rolling_date` | — | (utility) | +| DataRetentionAPI | `define_data_retention_by_rolling_months` | — | (utility) | +| EnginesAPI | `get_all_engine_server_details` | GET | `/cxrestapi/sast/engineServers` | +| EnginesAPI | `get_engine_id_by_name` | — | (utility) | +| EnginesAPI | `register_engine` | POST | `/cxrestapi/sast/engineServers` | +| EnginesAPI | `unregister_engine_by_engine_id` | DELETE | `/cxrestapi/sast/engineServers/{engine_id}` | +| EnginesAPI | `get_engine_details` | GET | `/cxrestapi/sast/engineServers/{engine_id}` | +| EnginesAPI | `update_engine_server` | PUT | `/cxrestapi/sast/engineServers/{engine_id}` | +| EnginesAPI | `update_an_engine_server_by_edit_single_field` | PATCH | `/cxrestapi/sast/engineServers/{engine_id}` | +| EnginesAPI | `get_all_engine_configurations` | GET | `/cxrestapi/sast/engineConfigurations` | +| EnginesAPI | `get_engine_configuration_id_by_name` | — | (utility) | +| EnginesAPI | `get_engine_configuration_by_id` | GET | `/cxrestapi/sast/engineConfigurations/{configuration_id}` | +| OsaAPI | `get_all_osa_scan_details_for_project` | GET | `/cxrestapi/osa/scans` | +| OsaAPI | `get_last_osa_scan_id_of_a_project` | — | (utility) | +| OsaAPI | `get_osa_scan_by_scan_id` | GET | `/cxrestapi/osa/scans/{scan_id}` | +| OsaAPI | `create_an_osa_scan_request` | POST | `/cxrestapi/osa/scans` | +| OsaAPI | `get_all_osa_file_extensions` | GET | `/cxrestapi/osa/fileextensions` | +| OsaAPI | `get_osa_licenses_by_id` | GET | `/cxrestapi/osa/licenses` | +| OsaAPI | `get_osa_scan_libraries` | GET | `/cxrestapi/osa/libraries` | +| OsaAPI | `get_osa_scan_vulnerabilities_by_id` | GET | `/cxrestapi/osa/vulnerabilities` | +| OsaAPI | `get_first_vulnerability_id` | — | (utility) | +| OsaAPI | `get_osa_scan_vulnerability_comments_by_id` | GET | `/cxrestapi/osa/vulnerabilities/{vulnerability_id}/comments` | +| OsaAPI | `get_osa_scan_summary_report` | GET | `/cxrestapi/osa/reports` | +| AccessControlAPI | `get_all_assignable_users` | GET | `/cxrestapi/auth/AssignableUsers` | +| AccessControlAPI | `get_all_authentication_providers` | GET | `/cxrestapi/auth/AuthenticationProviders` | +| AccessControlAPI | `submit_first_admin_user` | POST | `/cxrestapi/auth/Users/FirstAdmin` | +| AccessControlAPI | `get_admin_user_exists_confirmation` | GET | `/cxrestapi/auth/Users/FirstAdminExistence` | +| AccessControlAPI | `get_all_ldap_role_mapping` | GET | `/cxrestapi/auth/LDAPRoleMappings` | +| AccessControlAPI | `update_ldap_role_mapping` | PUT | `/cxrestapi/auth/LDAPServers/{id}/RoleMappings` | +| AccessControlAPI | `delete_ldap_role_mapping` | DELETE | `/cxrestapi/auth/LDAPRoleMappings/{id}` | +| AccessControlAPI | `test_ldap_server_connection` | POST | `/cxrestapi/auth/LDAPServers/TestConnection` | +| AccessControlAPI | `get_user_entries_by_search_criteria` | GET | `/cxrestapi/auth/LDAPServers/{id}/UserEntries` | +| AccessControlAPI | `get_group_entries_by_search_criteria` | GET | `/cxrestapi/auth/LDAPServers/{id}/GroupEntries` | +| AccessControlAPI | `get_all_ldap_servers` | GET | `/cxrestapi/auth/LDAPServers` | +| AccessControlAPI | `create_new_ldap_server` | POST | `/cxrestapi/auth/LDAPServers` | +| AccessControlAPI | `get_ldap_server_by_id` | GET | `/cxrestapi/auth/LDAPServers/{id}` | +| AccessControlAPI | `update_ldap_server` | PUT | `/cxrestapi/auth/LDAPServers/{id}` | +| AccessControlAPI | `delete_ldap_server` | DELETE | `/cxrestapi/auth/LDAPServers/{id}` | +| AccessControlAPI | `get_ldap_team_mapping` | GET | `/cxrestapi/auth/LDAPTeamMappings` | +| AccessControlAPI | `update_ldap_team_mapping` | PUT | `/cxrestapi/auth/LDAPServers/{id}/TeamMappings` | +| AccessControlAPI | `delete_ldap_team_mapping` | DELETE | `/cxrestapi/auth/LDAPTeamMappings/{id}` | +| AccessControlAPI | `get_my_profile` | GET | `/cxrestapi/auth/MyProfile` | +| AccessControlAPI | `update_my_profile` | PUT | `/cxrestapi/auth/MyProfile` | +| AccessControlAPI | `get_all_oidc_clients` | GET | `/cxrestapi/auth/OIDCClients` | +| AccessControlAPI | `create_new_oidc_client` | POST | `/cxrestapi/auth/OIDCClients` | +| AccessControlAPI | `get_oidc_client_by_id` | GET | `/cxrestapi/auth/OIDCClients/{id}` | +| AccessControlAPI | `update_an_oidc_client` | PUT | `/cxrestapi/auth/OIDCClients/{id}` | +| AccessControlAPI | `delete_an_oidc_client` | DELETE | `/cxrestapi/auth/OIDCClients/{id}` | +| AccessControlAPI | `get_all_permissions` | GET | `/cxrestapi/auth/Permissions` | +| AccessControlAPI | `get_permission_by_id` | GET | `/cxrestapi/auth/Permissions/{id}` | +| AccessControlAPI | `get_all_roles` | GET | `/cxrestapi/auth/Roles` | +| AccessControlAPI | `get_role_id_by_name` | — | (utility) | +| AccessControlAPI | `create_new_role` | POST | `/cxrestapi/auth/Roles` | +| AccessControlAPI | `get_role_by_id` | GET | `/cxrestapi/auth/Roles/{id}` | +| AccessControlAPI | `update_a_role` | PUT | `/cxrestapi/auth/Roles/{id}` | +| AccessControlAPI | `delete_a_role` | DELETE | `/cxrestapi/auth/Roles/{id}` | +| AccessControlAPI | `get_all_saml_identity_providers` | GET | `/cxrestapi/auth/SamlIdentityProviders` | +| AccessControlAPI | `create_new_saml_identity_provider` | POST | `/cxrestapi/auth/SamlIdentityProviders` | +| AccessControlAPI | `get_saml_identity_provider_by_id` | GET | `/cxrestapi/auth/SamlIdentityProviders/{id}` | +| AccessControlAPI | `update_new_saml_identity_provider` | PUT | `/cxrestapi/auth/SamlIdentityProviders/{id}` | +| AccessControlAPI | `delete_a_saml_identity_provider` | DELETE | `/cxrestapi/auth/SamlIdentityProviders/{id}` | +| AccessControlAPI | `get_details_of_saml_role_mappings` | GET | `/cxrestapi/auth/SamlRoleMappings` | +| AccessControlAPI | `set_saml_group_and_role_mapping_details` | PUT | `/cxrestapi/auth/SamlIdentityProviders/{samlProviderId}/RoleMappings` | +| AccessControlAPI | `get_saml_service_provider_metadata` | GET | `/cxrestapi/auth/SamlServiceProvider/metadata` | +| AccessControlAPI | `get_saml_service_provider` | GET | `/cxrestapi/auth/SamlServiceProvider` | +| AccessControlAPI | `update_a_saml_service_provider` | PUT | `/cxrestapi/auth/SamlServiceProvider` | +| AccessControlAPI | `get_details_of_saml_team_mappings` | GET | `/cxrestapi/auth/SamlTeamMappings` | +| AccessControlAPI | `set_saml_group_and_team_mapping_details` | PUT | `/cxrestapi/auth/SamlIdentityProviders/{id}/TeamMappings` | +| AccessControlAPI | `get_all_service_providers` | GET | `/cxrestapi/auth/ServiceProviders` | +| AccessControlAPI | `get_service_provider_by_id` | GET | `/cxrestapi/auth/ServiceProviders/{id}` | +| AccessControlAPI | `get_all_smtp_settings` | GET | `/cxrestapi/auth/SMTPSettings` | +| AccessControlAPI | `create_smtp_settings` | POST | `/cxrestapi/auth/SMTPSettings` | +| AccessControlAPI | `get_smtp_settings_by_id` | GET | `/cxrestapi/auth/SMTPSettings/{id}` | +| AccessControlAPI | `update_smtp_settings` | PUT | `/cxrestapi/auth/SMTPSettings/{id}` | +| AccessControlAPI | `delete_smtp_settings` | DELETE | `/cxrestapi/auth/SMTPSettings/{id}` | +| AccessControlAPI | `test_smtp_connection` | POST | `/cxrestapi/auth/SMTPSettings/testconnection` | +| AccessControlAPI | `get_all_system_locales` | GET | `/cxrestapi/auth/SystemLocales` | +| AccessControlAPI | `get_members_by_team_id` | GET | `/cxrestapi/auth/Teams/{id}/Users` | +| AccessControlAPI | `update_members_by_team_id` | PUT | `/cxrestapi/auth/Teams/{teamId}/Users` | +| AccessControlAPI | `add_a_user_to_a_team` | POST | `/cxrestapi/auth/Teams/{teamId}/Users/{userId}` | +| AccessControlAPI | `delete_a_member_from_a_team` | DELETE | `/cxrestapi/auth/Teams/{teamId}/Users/{userId}` | +| AccessControlAPI | `get_all_teams` | GET | `/cxrestapi/auth/Teams` | +| AccessControlAPI | `get_team_id_by_full_name` | — | (utility) | +| AccessControlAPI | `create_new_team` | POST | `/cxrestapi/auth/Teams` | +| AccessControlAPI | `create_teams_recursively` | — | (utility) | +| AccessControlAPI | `get_team_by_id` | GET | `/cxrestapi/auth/Teams/{id}` | +| AccessControlAPI | `update_a_team` | PUT | `/cxrestapi/auth/Teams/{id}` | +| AccessControlAPI | `delete_a_team` | DELETE | `/cxrestapi/auth/Teams/{id}` | +| AccessControlAPI | `generate_a_new_token_signing_certificate` | POST | `/cxrestapi/auth/TokenSigningCertificateGeneration` | +| AccessControlAPI | `upload_a_new_token_signing_certificate` | POST | `/cxrestapi/auth/TokenSigningCertificate` | +| AccessControlAPI | `get_all_users` | GET | `/cxrestapi/auth/Users` | +| AccessControlAPI | `get_user_id_by_name` | — | (utility) | +| AccessControlAPI | `create_new_user` | POST | `/cxrestapi/auth/Users` | +| AccessControlAPI | `get_user_by_id` | GET | `/cxrestapi/auth/Users/{id}` | +| AccessControlAPI | `update_a_user` | PUT | `/cxrestapi/auth/Users/{id}` | +| AccessControlAPI | `delete_a_user` | DELETE | `/cxrestapi/auth/Users/{id}` | +| AccessControlAPI | `migrate_existing_user` | POST | `/cxrestapi/auth/Users/migration` | +| AccessControlAPI | `get_all_windows_domains` | GET | `/cxrestapi/auth/WindowsDomains` | +| AccessControlAPI | `get_windows_domain_id_by_name` | — | (utility) | +| AccessControlAPI | `create_a_new_windows_domain` | POST | `/cxrestapi/auth/WindowsDomains` | +| AccessControlAPI | `get_windows_domain_by_id` | GET | `/cxrestapi/auth/WindowsDomains/{id}` | +| AccessControlAPI | `update_a_windows_domain` | PUT | `/cxrestapi/auth/WindowsDomains/{id}` | +| AccessControlAPI | `delete_a_windows_domain` | DELETE | `/cxrestapi/auth/WindowsDomains/{id}` | +| AccessControlAPI | `get_windows_domain_user_entries_by_search_criteria` | GET | `/cxrestapi/auth/WindowsDomains/{id}/UserEntries` | +| ConfigurationAPI | `get_cx_component_configuration_settings` | GET | `/cxrestapi/configurationsExtended/{group}` | +| ConfigurationAPI | `update_cx_component_configuration_settings` | PUT | `/cxrestapi/configurationsExtended/{group}` | +| QueriesAPI | `get_the_full_description_of_the_query` | GET | `/cxrestapi/queries/{query_id}/cxDescription` | +| QueriesAPI | `get_query_id_and_query_version_code` | GET | `/cxrestapi/queries/queryVersionCode` | +| QueriesAPI | `get_preset_detail` | GET | `/cxrestapi/sast/presetDetails/{preset_id}` | From 4563254fed50f2dc94b951a22e4f60bf60a7118c Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:13:54 +0800 Subject: [PATCH 21/25] Fix get_branch_project_status to use status names instead of magic number - Replace `item["status"]["id"] == 2` with status name extraction via `status.get("value")`, returning "Started"/"Completed"/"Failed" strings - Handle both dict and plain-string status formats from the API - Add proper docstring with known status values - Update test to create a real branch, poll, and display all statuses - Update CxCliPy.py to use latest SDK OData API and add --branch_project support Co-Authored-By: Claude Opus 4.7 --- .../CxRestAPISDK/ProjectsAPI.py | 23 +- examples/CxSAST/CxCliPy.py | 452 ++++++++++++++++++ tests/CxSAST/CxRestAPI/test_projects_api.py | 28 +- 3 files changed, 494 insertions(+), 9 deletions(-) create mode 100644 examples/CxSAST/CxCliPy.py diff --git a/CheckmarxPythonSDK/CxRestAPISDK/ProjectsAPI.py b/CheckmarxPythonSDK/CxRestAPISDK/ProjectsAPI.py index 66989054..9f049744 100644 --- a/CheckmarxPythonSDK/CxRestAPISDK/ProjectsAPI.py +++ b/CheckmarxPythonSDK/CxRestAPISDK/ProjectsAPI.py @@ -353,14 +353,33 @@ def create_branched_project( def get_branch_project_status( self, branch_project_id: int, api_version: str = "4.0" ) -> str: - result = False + """ + Get the branching status of a branched project. + + Args: + branch_project_id (int): Unique Id of the branched project + api_version (str, optional): + + Returns: + str: The status value. Known values: + - "Started" + - "InProgress" + - "Completed" + - "Failed" + Returns None if the request fails. + """ + result = None url = f"{self.base_url}/cxrestapi/projects/branch/{branch_project_id}" response = self.api_client.call_api( "GET", url, headers=get_headers(api_version=api_version) ) if response.status_code == OK: item = response.json() - result = item["status"]["id"] == 2 + status = item.get("status") or {} + if isinstance(status, dict): + result = status.get("value") + else: + result = status return result def get_all_issue_tracking_systems( diff --git a/examples/CxSAST/CxCliPy.py b/examples/CxSAST/CxCliPy.py new file mode 100644 index 00000000..54aa9d66 --- /dev/null +++ b/examples/CxSAST/CxCliPy.py @@ -0,0 +1,452 @@ +""" +This a CLI script. It can be converted to binary file by using pyinstaller + +pyinstaller -y -F --clean CxCliPy.py + +Sample usage +/home/happy/Documents/CxCliPy/dist/CxCliPy scan --cxsast_base_url http://192.168.3.84 --cxsast_username Admin \ +--cxsast_password *** --preset All --incremental False --location_type Folder \ +--location_path /home/happy/Documents/JavaVulnerableLab \ +--project_name /CxServer/happy-2022-11-21 --exclude_folders "test,integrationtest" --exclude_files "*min.js" \ +--report_csv cx-report.csv \ +--full_scan_cycle 10 +""" +import pathlib +import time +import os +from os.path import exists +from zipfile import ZipFile, ZIP_DEFLATED +import logging + +# create logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + + +def get_cx_supported_file_extensions(): + return [ + '.apex', '.apexp', '.asax', '.ascx', '.asp', '.aspx', '.bas', '.bdy', '.c', '.c++', '.cc', '.cgi', '.cls', + '.component', '.conf', '.config', '.cpp', '.cs', '.cshtml', '.csproj', '.ctl', '.ctp', '.cxx', '.dsr', '.ec', + '.erb', '.fnc', '.frm', '.go', '.gradle', '.groovy', '.gsh', '.gsp', '.gtl', '.gvy', '.gy', '.h', '.h++', + '.handlebars', '.hbs', '.hh', '.hpp', '.htm', '.html', '.hxx', '.inc', '.jade', '.java', '.javasln', '.js', + '.jsf', '.json', '.jsp', '.jspf', '.lock', '.m', '.master', '.-meta.xml', '.mf', '.object', '.page', '.pc', + '.pck', '.php', '.php3', '.php4', '.php5', '.phtm', '.phtml', '.pkb', '.pkh', '.pks', '.pl', '.plist', '.pls', + '.plx', '.pm', '.prc', '.project', '.properties', '.psgi', '.py', '.rb', '.report', '.rhtml', '.rjs', '.rxml', + '.scala', '.should_neve_match_anything_9gdfg4', '.sln', '.spc', '.sqb', '.sqf', '.sqh', '.sql', '.sqp', '.sqt', + '.sqtb', '.sqth', '.sqv', '.swift', '.tag', '.tgr', '.tld', '.tpb', '.tpl', '.tps', '.trg', '.trigger', '.ts', + '.tsx', '.twig', '.vb', '.vbp', '.vbs', '.wod', '.workflow', '.xaml', '.xhtml', '.xib', '.xml', '.xsaccess', + '.xsapp', '.xsjs', '.xsjslib', '-meta.xml', '.rpgle', '.pug', '.vue', '.mustache', '.cbl', '.jsx', '.apxc', + '.cpy', '.kt', '.rpg38', '.pro', '.csv', '.ftl', '.evt', '.sqlrpg', '.eco', '.cmp', '.txt', '.pco', '.ac', + '.cob', '.rpg', '.cmake', '.sqlrpgle', '.tex', '.vm', '.kts', '.latex', '.am', '.app' + ] + + +def get_command_line_arguments(): + """ + + Returns: + Namespace + """ + import argparse + description = 'A simple command-line interface for CxSAST in Python.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('scan') + parser.add_argument('--cxsast_base_url', required=True, help="CxSAST base url, for example: https://localhost") + parser.add_argument('--cxsast_username', required=True, help="CxSAST username") + parser.add_argument('--cxsast_password', required=True, help="CxSAST password") + parser.add_argument('-preset', '--preset', required=True, help="The preset (rule set) name") + parser.add_argument('-incremental', '--incremental', default=False, help="Set it True for incremental scan") + parser.add_argument('-location_type', '--location_type', required=True, help="Folder, Git") + parser.add_argument('-location_path', '--location_path', required=True, help="Source code folder absolute path") + parser.add_argument('-project_name', '--project_name', required=True, help="Checkmarx project full path") + parser.add_argument('-exclude_folders', '--exclude_folders', help="exclude folders") + parser.add_argument('-exclude_files', '--exclude_files', help='exclude files') + parser.add_argument('-report_xml', '--report_xml', default=None, help="xml report file path") + parser.add_argument('-report_pdf', '--report_pdf', default=None, help="pdf report file path") + parser.add_argument('-report_csv', '--report_csv', default=None, help="csv report file path") + parser.add_argument('-full_scan_cycle', '--full_scan_cycle', default=10, + help="Defines the number of incremental scans to be performed, before performing a periodic " + "full scan") + parser.add_argument('-branch_project', '--branch_project', default=None, + help="Branched project name. If specified, scan will be created under this branched project. " + "If the branched project does not exist, it will be created.") + return parser.parse_known_args() + + +def group_str_by_wildcard_character(exclusions): + """ + + Args: + exclusions (str): commaseparated string + for example, "*.min.js,readme,*.txt,test*,*doc*" + + Returns: + dict + { + "prefix_list": ["test"], # wildcard (*) at end, but not start + "suffix_list": [".min.js", ".txt"], # wildcard (*) at start, but not end + "inner_List": ["doc"], # wildcard (*) at both end and start + "word_list": ["readme"] # no wildcard + } + """ + result = { + "prefix_list": [], + "suffix_list": [], + "inner_List": [], + "word_list": [], + } + if not exclusions: + return result + string_list = exclusions.lower().split(',') + string_set = set(string_list) + for string in string_set: + new_string = string.strip() + # ignore any string that with slash or backward slash + if '/' in new_string or "\\" in new_string: + continue + if new_string.endswith("*") and not new_string.startswith("*"): + result["prefix_list"].append(new_string.rstrip("*")) + elif new_string.startswith("*") and not new_string.endswith("*"): + result["suffix_list"].append(new_string.lstrip("*")) + elif new_string.endswith("*") and new_string.startswith("*"): + result["inner_List"].append(new_string.strip("*")) + else: + result["word_list"].append(new_string) + return result + + +def should_be_excluded(exclusions, target): + """ + + Args: + exclusions (str): + target (str): + + Returns: + + """ + result = False + target = target.lower() + groups_of_exclusions = group_str_by_wildcard_character(exclusions) + if target.startswith(tuple(groups_of_exclusions["prefix_list"])): + result = True + if target.endswith(tuple(groups_of_exclusions["suffix_list"])): + result = True + if any([True if inner_text in target else False for inner_text in groups_of_exclusions["inner_List"]]): + result = True + if target in groups_of_exclusions["word_list"]: + result = True + return result + + +def create_zip_file_from_location_path(location_path_str: str, project_name: str, + exclude_folders_str=None, exclude_files_str=None): + """ + + Args: + location_path_str (str): + project_name (str): + exclude_folders_str (str): comma separated string + exclude_files_str (str): comma separated string + + Returns: + str (ZIP file path) + """ + exclude_folders = ".*,bin,target,images,Lib,node_modules" + exclude_files = ".*,*.min.js" + if exclude_folders_str is not None: + exclude_folders += "," + exclude_folders += exclude_folders_str + if exclude_files_str is not None: + exclude_files += "," + exclude_files += exclude_files_str + + from pathlib import Path + import tempfile + temp_dir = tempfile.gettempdir() + extensions = get_cx_supported_file_extensions() + path = Path(location_path_str) + if not path.exists(): + raise FileExistsError(f"{location_path_str} does not exist, abort scan") + absolute_path_str = str(os.path.normpath(path.absolute())) + file_path = f"{temp_dir}/cx_{project_name}.zip" + with ZipFile(file_path, "w", ZIP_DEFLATED) as zip_file: + root_len = len(absolute_path_str) + 1 + for base, dirs, files in os.walk(absolute_path_str): + path_folders = base.split(os.sep) + if any([should_be_excluded(exclude_folders, folder) for folder in path_folders]): + continue + for file in files: + file_lower_case = file.lower() + if not file_lower_case.endswith(tuple(extensions)): + continue + if should_be_excluded(exclude_files, file_lower_case): + continue + fn = os.path.join(base, file) + zip_file.write(fn, fn[root_len:]) + return file_path + + +def cx_scan_from_local_zip_file(preset_name: str, team_full_name: str, project_name: str, zip_file_path: str, + exclude_folders: str, exclude_files: str, incremental: bool = False, + full_scan_cycle=10, branched_project_name=None): + """ + + Args: + preset_name (str): + team_full_name (str): + project_name (str): + zip_file_path (str): + exclude_folders (str): + exclude_files (str): + incremental (bool): + full_scan_cycle (int): + branched_project_name (str): name of the branched project to scan under + + Returns: + return scan id if scan finished, otherwise return None + """ + from CheckmarxPythonSDK.CxRestAPISDK import TeamAPI, ProjectsAPI, ScansAPI + team_api = TeamAPI() + projects_api = ProjectsAPI() + scan_api = ScansAPI() + + if not exists(zip_file_path): + logger.error("[ERROR]: zip file not found. Abort scan.") + exit(1) + + logger.info("get team id") + team_id = team_api.get_team_id_by_team_full_name(team_full_name) + if not team_id: + logger.error(f"[ERROR]: team full name {team_full_name} not exist. Abort scan.") + exit(1) + + project_id = projects_api.get_project_id_by_project_name_and_team_full_name( + project_name=project_name, team_full_name=team_full_name + ) + logger.info(f"project id: {project_id}") + if not project_id: + logger.info("project does not exist. create project with default configuration, will get project id") + project = projects_api.create_project_with_default_configuration(project_name=project_name, team_id=team_id) + project_id = project.id + logger.info(f"new project with project_id: {project_id}") + + if branched_project_name: + logger.info(f"branched project name: {branched_project_name}") + branched_project_id = projects_api.get_project_id_by_project_name_and_team_full_name( + project_name=branched_project_name, team_full_name=team_full_name + ) + if not branched_project_id: + logger.info("branched project does not exist, creating it") + branched_project = projects_api.create_branched_project( + project_id=project_id, branched_project_name=branched_project_name + ) + branched_project_id = branched_project.id + logger.info(f"branched project created with id: {branched_project_id}") + logger.info("waiting for branched project to finish") + while True: + status = projects_api.get_branch_project_status(branch_project_id=branched_project_id) + logger.info(f"branch project status: {status}") + if status == "Completed": + break + time.sleep(10) + logger.info("branched project is ready") + else: + logger.info(f"branched project already exists with id: {branched_project_id}") + project_id = branched_project_id + + preset_id = projects_api.get_preset_id_by_name(preset_name=preset_name) + logger.info("preset id: {}".format(preset_id)) + + logger.info("set exclude folders and exclude files") + projects_api.set_project_exclude_settings_by_project_id( + project_id, exclude_folders_pattern=exclude_folders, exclude_files_pattern=exclude_files + ) + + scans_of_this_project = scan_api.get_all_scans_for_project(project_id=project_id) + number_of_scans_of_this_project = len(scans_of_this_project) + 1 + remainder = number_of_scans_of_this_project % full_scan_cycle + if remainder == 0: + incremental = False + + logger.info("create new scan") + logger.info(f"The scan type will be: {'incremental' if incremental else 'full'} ") + scan = scan_api.create_new_scan_with_settings(project_id=project_id, comment="", preset_id=preset_id, + zipped_source_file_path=str(zip_file_path), + is_incremental=incremental) + scan_id = scan.id + logger.info("scan_id : {}".format(scan_id)) + + logger.info("get scan details by scan id, report scan status") + while True: + scan_detail = scan_api.get_sast_scan_details_by_scan_id(scan_id=scan_id) + scan_status = scan_detail.status.name + logger.info("scan_status: {}".format(scan_status)) + if scan_status == "Finished": + break + elif scan_status in ["Failed", "Canceled"]: + return None + time.sleep(10) + + logger.info("get statistics results by scan id") + statistics = scan_api.get_statistics_results_by_scan_id(scan_id=scan_id) + statistics_updated = { + "High": statistics.high_severity, + "Medium": statistics.medium_severity, + "Low": statistics.low_severity, + "Info": statistics.info_severity + } + logger.info(f"statistics: {statistics_updated}") + return scan_id + + +def generate_report(scan_id: int, report_type: str, report_file_path: str): + """ + + Args: + scan_id (int): + report_type (str): + report_file_path (str): + + Returns: + + """ + from CheckmarxPythonSDK.CxRestAPISDK import ScansAPI + scan_api = ScansAPI() + + logger.info("register scan report") + report = scan_api.register_scan_report(scan_id=scan_id, report_type=report_type) + report_id = report.report_id + logger.info("report_id : {}".format(report_id)) + + logger.info("get report status by id") + while not scan_api.is_report_generation_finished(report_id): + logger.info("report generating") + time.sleep(10) + + logger.info("get report by id") + report_content = scan_api.get_report_by_id(report_id) + + logger.info("write original report") + with open(str(report_file_path), "wb") as f_out: + f_out.write(report_content) + + if report_type.lower() == "csv": + update_csv_report(cx_report_file_path=report_file_path, scan_id=scan_id) + + +def get_similarity_ids_of_a_scan(scan_id): + from CheckmarxPythonSDK.CxODataApiSDK import get_similarity_ids_of_a_scan + return get_similarity_ids_of_a_scan(scan_id=scan_id) + + +def update_csv_report(cx_report_file_path, scan_id): + """ + update CSV report by add the similarityID column (data retrieved by using ODATA API) + Args: + cx_report_file_path: + scan_id: + + Returns: + + """ + logger.info("update csv report") + if not pathlib.Path(cx_report_file_path).exists(): + logger.info(f"report does not exist, file path: {cx_report_file_path}") + return + import csv + from collections import OrderedDict + logger.info("get similarity id by using ODATA API") + similarity_id_path_ids = get_similarity_ids_of_a_scan(scan_id=scan_id) + sim_path_dict = {} + for pair in similarity_id_path_ids: + sim_path_dict[pair.get("PathId")] = pair.get("SimilarityId") + + logger.info("read csv file") + csv_content = [] + with open(cx_report_file_path, newline='', encoding="utf-8-sig") as csvfile: + reader = csv.DictReader(csvfile) + field_names = reader.fieldnames + field_names = [item for item in field_names] + field_names.append('SimilarityID') + for row in reader: + path_id = row.get("Link").split('&')[2].split('=')[1] + path_id = int(path_id) + similarity_id = sim_path_dict.get(path_id) + new_ordered_dict = OrderedDict() + for key, value in row.items(): + new_ordered_dict[key] = value + new_ordered_dict['SimilarityID'] = similarity_id + csv_content.append(new_ordered_dict) + logger.info("write csv file") + with open(cx_report_file_path, 'w', newline='') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=field_names, dialect='unix') + writer.writeheader() + writer.writerows(csv_content) + + +def run_scan_and_generate_reports(arguments): + preset = arguments.preset + incremental = False if arguments.incremental.lower() == "false" else True + location_type = arguments.location_type + location_path = arguments.location_path + project_full_path = arguments.project_name + exclude_folders = arguments.exclude_folders + exclude_files = arguments.exclude_files + report_xml = arguments.report_xml + report_pdf = arguments.report_pdf + report_csv = arguments.report_csv + full_scan_cycle = int(arguments.full_scan_cycle) + branch_project = arguments.branch_project + logger.info( + f"preset: {preset}\n" + f"incremental: {incremental}\n" + f"location_type: {location_type}\n" + f"location_path: {location_path}\n" + f"project_name: {project_full_path}\n" + f"branch_project: {branch_project}\n" + f"report_xml: {report_xml}\n" + f"report_pdf: {report_pdf}\n" + f"report_csv: {report_csv}\n" + f"full_scan_cycle: {full_scan_cycle}\n" + ) + + logger.info(f"creating zip file by zip the source code folder: {location_path}") + project_path_list = project_full_path.split("/") + project_name = project_path_list[-1] + team_full_name = "/".join(project_path_list[0: len(project_path_list)-1]) + zip_file_path = create_zip_file_from_location_path(location_path, project_name, exclude_folders_str=exclude_folders, + exclude_files_str=exclude_files) + logger.info(f"ZIP file created: {zip_file_path}") + scan_id = cx_scan_from_local_zip_file(preset_name=preset, team_full_name=team_full_name, project_name=project_name, + zip_file_path=zip_file_path, exclude_folders=exclude_folders, + exclude_files=exclude_files, incremental=incremental, + full_scan_cycle=full_scan_cycle, + branched_project_name=branch_project) + + if scan_id is None: + logger.info("Scan did not finish successfully, exit!") + return + + logger.info(f"deleting zip file: {zip_file_path}") + pathlib.Path(zip_file_path).unlink() + + if report_xml: + generate_report(scan_id=scan_id, report_type="XML", report_file_path=report_xml) + if report_pdf: + generate_report(scan_id=scan_id, report_type="PDF", report_file_path=report_pdf) + if report_csv: + generate_report(scan_id=scan_id, report_type="CSV", report_file_path=report_csv) + logger.info("report generated successfully") + + +if __name__ == '__main__': + # get command line arguments + cli_arguments = get_command_line_arguments() + cli_arguments = cli_arguments[0] + run_scan_and_generate_reports(cli_arguments) diff --git a/tests/CxSAST/CxRestAPI/test_projects_api.py b/tests/CxSAST/CxRestAPI/test_projects_api.py index 67c195b0..5bf9f45a 100644 --- a/tests/CxSAST/CxRestAPI/test_projects_api.py +++ b/tests/CxSAST/CxRestAPI/test_projects_api.py @@ -99,13 +99,27 @@ def test_create_branched_project(): def test_get_branch_project_status(): projects_api = ProjectsAPI() - branch_project_id = projects_api.get_project_id_by_project_name_and_team_full_name("test-branch", team_full_name) - if branch_project_id is None: - pytest.skip("Branched project 'test-branch' does not exist") - time.sleep(60) - result = projects_api.get_branch_project_status(branch_project_id) - assert result is True - projects_api.delete_project_by_id(branch_project_id) + project_id = get_project_id() + if project_id is None: + pytest.skip("No project available to branch from") + branched_project_name = "test-branch-status" + projects_api.delete_project_if_exists_by_project_name_and_team_full_name( + branched_project_name, team_full_name + ) + branched_project = projects_api.create_branched_project(project_id, branched_project_name) + branched_project_id = branched_project.id + print(f"branched project created with id: {branched_project_id}") + + print("polling branch project status...") + while True: + status = projects_api.get_branch_project_status(branched_project_id) + print(f" status: {status}") + if status == "Completed": + break + time.sleep(10) + + assert status == "Completed" + projects_api.delete_project_by_id(branched_project_id) def test_get_all_issue_tracking_systems(): From 356e84823e9bf7e4bebb9f043d9c8106452b055a Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:14:44 +0800 Subject: [PATCH 22/25] Update changelog for get_branch_project_status fix and CxCliPy.py Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4a8bae..47c3bccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file. * [Add] CxPortalWebService.get_source_by_scan_id — deprecated singular variant; CxSAST 9.x returns "no longer supported" * [Add] CxPortalWebService.get_file_names_for_path — get file names associated with a result path (Portal SOAP GetFileNamesForPath) * [Add] tests for get_sources_by_scan_id, get_file_names_for_path, and get_source_by_scan_id +* [Fix] get_branch_project_status — replaced magic number `item["status"]["id"] == 2` with status name extraction via `status.get("value")`; now returns string ("Started"/"InProgress"/"Completed"/"Failed") instead of bool; added docstring with known status values; handles both dict and plain-string status formats +* [Update] test_get_branch_project_status — now creates a real branch from an existing project, polls and displays all intermediate statuses, asserts on status name string +* [Add] examples/CxSAST/CxCliPy.py — CLI script for CxSAST scanning with `--branch_project` support; updated OData API to use latest SDK import 1.8.7 - 2026-06-02 * [Add] AiAssetsAPI — AI supply chain asset management (findings, asset types, assets, applications, global inventory results, scan results, risks) From 41fa6ecdb6bda1433719748177017180e9676add Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:25:02 +0800 Subject: [PATCH 23/25] Fix api_client Content-Type header for file uploads and zip cleanup on Windows Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 ++ CheckmarxPythonSDK/api_client.py | 5 +++++ examples/CxSAST/CxCliPy.py | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47c3bccb..2c34531b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. * [Fix] get_branch_project_status — replaced magic number `item["status"]["id"] == 2` with status name extraction via `status.get("value")`; now returns string ("Started"/"InProgress"/"Completed"/"Failed") instead of bool; added docstring with known status values; handles both dict and plain-string status formats * [Update] test_get_branch_project_status — now creates a real branch from an existing project, polls and displays all intermediate statuses, asserts on status name string * [Add] examples/CxSAST/CxCliPy.py — CLI script for CxSAST scanning with `--branch_project` support; updated OData API to use latest SDK import +* [Fix] api_client.call_api — strip explicit Content-Type header when files are present so httpx can auto-set multipart/form-data boundary; fixes 400/500 errors on file upload endpoints +* [Fix] examples/CxSAST/CxCliPy.py — handle PermissionError when deleting temp zip file on Windows 1.8.7 - 2026-06-02 * [Add] AiAssetsAPI — AI supply chain asset management (findings, asset types, assets, applications, global inventory results, scan results, risks) diff --git a/CheckmarxPythonSDK/api_client.py b/CheckmarxPythonSDK/api_client.py index 9a3ab653..ac94c820 100644 --- a/CheckmarxPythonSDK/api_client.py +++ b/CheckmarxPythonSDK/api_client.py @@ -253,6 +253,11 @@ def call_api( if params: params = {k: v for k, v in params.items() if v is not None} + # httpx auto-sets Content-Type for multipart uploads; explicit Content-Type + # from get_headers() would override it, breaking file uploads. + if files and headers: + headers.pop("Content-Type", None) + response = self.session.request( method=method, url=url, diff --git a/examples/CxSAST/CxCliPy.py b/examples/CxSAST/CxCliPy.py index 54aa9d66..12f74c2b 100644 --- a/examples/CxSAST/CxCliPy.py +++ b/examples/CxSAST/CxCliPy.py @@ -434,7 +434,10 @@ def run_scan_and_generate_reports(arguments): return logger.info(f"deleting zip file: {zip_file_path}") - pathlib.Path(zip_file_path).unlink() + try: + pathlib.Path(zip_file_path).unlink() + except PermissionError: + logger.warning("could not delete zip file, it may be in use by another process") if report_xml: generate_report(scan_id=scan_id, report_type="XML", report_file_path=report_xml) From d7c5bf9b4703d847313bfdaf0a41e8a9661e0cbc Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:01:25 +0800 Subject: [PATCH 24/25] Remove examples/CxSAST/CxCliPy.py (moved to dedicated repo) Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 - examples/CxSAST/CxCliPy.py | 455 ------------------------------------- 2 files changed, 457 deletions(-) delete mode 100644 examples/CxSAST/CxCliPy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c34531b..61ec6bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,7 @@ All notable changes to this project will be documented in this file. * [Add] tests for get_sources_by_scan_id, get_file_names_for_path, and get_source_by_scan_id * [Fix] get_branch_project_status — replaced magic number `item["status"]["id"] == 2` with status name extraction via `status.get("value")`; now returns string ("Started"/"InProgress"/"Completed"/"Failed") instead of bool; added docstring with known status values; handles both dict and plain-string status formats * [Update] test_get_branch_project_status — now creates a real branch from an existing project, polls and displays all intermediate statuses, asserts on status name string -* [Add] examples/CxSAST/CxCliPy.py — CLI script for CxSAST scanning with `--branch_project` support; updated OData API to use latest SDK import * [Fix] api_client.call_api — strip explicit Content-Type header when files are present so httpx can auto-set multipart/form-data boundary; fixes 400/500 errors on file upload endpoints -* [Fix] examples/CxSAST/CxCliPy.py — handle PermissionError when deleting temp zip file on Windows 1.8.7 - 2026-06-02 * [Add] AiAssetsAPI — AI supply chain asset management (findings, asset types, assets, applications, global inventory results, scan results, risks) diff --git a/examples/CxSAST/CxCliPy.py b/examples/CxSAST/CxCliPy.py deleted file mode 100644 index 12f74c2b..00000000 --- a/examples/CxSAST/CxCliPy.py +++ /dev/null @@ -1,455 +0,0 @@ -""" -This a CLI script. It can be converted to binary file by using pyinstaller - -pyinstaller -y -F --clean CxCliPy.py - -Sample usage -/home/happy/Documents/CxCliPy/dist/CxCliPy scan --cxsast_base_url http://192.168.3.84 --cxsast_username Admin \ ---cxsast_password *** --preset All --incremental False --location_type Folder \ ---location_path /home/happy/Documents/JavaVulnerableLab \ ---project_name /CxServer/happy-2022-11-21 --exclude_folders "test,integrationtest" --exclude_files "*min.js" \ ---report_csv cx-report.csv \ ---full_scan_cycle 10 -""" -import pathlib -import time -import os -from os.path import exists -from zipfile import ZipFile, ZIP_DEFLATED -import logging - -# create logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -ch = logging.StreamHandler() -ch.setLevel(logging.INFO) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -ch.setFormatter(formatter) -logger.addHandler(ch) - - -def get_cx_supported_file_extensions(): - return [ - '.apex', '.apexp', '.asax', '.ascx', '.asp', '.aspx', '.bas', '.bdy', '.c', '.c++', '.cc', '.cgi', '.cls', - '.component', '.conf', '.config', '.cpp', '.cs', '.cshtml', '.csproj', '.ctl', '.ctp', '.cxx', '.dsr', '.ec', - '.erb', '.fnc', '.frm', '.go', '.gradle', '.groovy', '.gsh', '.gsp', '.gtl', '.gvy', '.gy', '.h', '.h++', - '.handlebars', '.hbs', '.hh', '.hpp', '.htm', '.html', '.hxx', '.inc', '.jade', '.java', '.javasln', '.js', - '.jsf', '.json', '.jsp', '.jspf', '.lock', '.m', '.master', '.-meta.xml', '.mf', '.object', '.page', '.pc', - '.pck', '.php', '.php3', '.php4', '.php5', '.phtm', '.phtml', '.pkb', '.pkh', '.pks', '.pl', '.plist', '.pls', - '.plx', '.pm', '.prc', '.project', '.properties', '.psgi', '.py', '.rb', '.report', '.rhtml', '.rjs', '.rxml', - '.scala', '.should_neve_match_anything_9gdfg4', '.sln', '.spc', '.sqb', '.sqf', '.sqh', '.sql', '.sqp', '.sqt', - '.sqtb', '.sqth', '.sqv', '.swift', '.tag', '.tgr', '.tld', '.tpb', '.tpl', '.tps', '.trg', '.trigger', '.ts', - '.tsx', '.twig', '.vb', '.vbp', '.vbs', '.wod', '.workflow', '.xaml', '.xhtml', '.xib', '.xml', '.xsaccess', - '.xsapp', '.xsjs', '.xsjslib', '-meta.xml', '.rpgle', '.pug', '.vue', '.mustache', '.cbl', '.jsx', '.apxc', - '.cpy', '.kt', '.rpg38', '.pro', '.csv', '.ftl', '.evt', '.sqlrpg', '.eco', '.cmp', '.txt', '.pco', '.ac', - '.cob', '.rpg', '.cmake', '.sqlrpgle', '.tex', '.vm', '.kts', '.latex', '.am', '.app' - ] - - -def get_command_line_arguments(): - """ - - Returns: - Namespace - """ - import argparse - description = 'A simple command-line interface for CxSAST in Python.' - parser = argparse.ArgumentParser(description=description) - parser.add_argument('scan') - parser.add_argument('--cxsast_base_url', required=True, help="CxSAST base url, for example: https://localhost") - parser.add_argument('--cxsast_username', required=True, help="CxSAST username") - parser.add_argument('--cxsast_password', required=True, help="CxSAST password") - parser.add_argument('-preset', '--preset', required=True, help="The preset (rule set) name") - parser.add_argument('-incremental', '--incremental', default=False, help="Set it True for incremental scan") - parser.add_argument('-location_type', '--location_type', required=True, help="Folder, Git") - parser.add_argument('-location_path', '--location_path', required=True, help="Source code folder absolute path") - parser.add_argument('-project_name', '--project_name', required=True, help="Checkmarx project full path") - parser.add_argument('-exclude_folders', '--exclude_folders', help="exclude folders") - parser.add_argument('-exclude_files', '--exclude_files', help='exclude files') - parser.add_argument('-report_xml', '--report_xml', default=None, help="xml report file path") - parser.add_argument('-report_pdf', '--report_pdf', default=None, help="pdf report file path") - parser.add_argument('-report_csv', '--report_csv', default=None, help="csv report file path") - parser.add_argument('-full_scan_cycle', '--full_scan_cycle', default=10, - help="Defines the number of incremental scans to be performed, before performing a periodic " - "full scan") - parser.add_argument('-branch_project', '--branch_project', default=None, - help="Branched project name. If specified, scan will be created under this branched project. " - "If the branched project does not exist, it will be created.") - return parser.parse_known_args() - - -def group_str_by_wildcard_character(exclusions): - """ - - Args: - exclusions (str): commaseparated string - for example, "*.min.js,readme,*.txt,test*,*doc*" - - Returns: - dict - { - "prefix_list": ["test"], # wildcard (*) at end, but not start - "suffix_list": [".min.js", ".txt"], # wildcard (*) at start, but not end - "inner_List": ["doc"], # wildcard (*) at both end and start - "word_list": ["readme"] # no wildcard - } - """ - result = { - "prefix_list": [], - "suffix_list": [], - "inner_List": [], - "word_list": [], - } - if not exclusions: - return result - string_list = exclusions.lower().split(',') - string_set = set(string_list) - for string in string_set: - new_string = string.strip() - # ignore any string that with slash or backward slash - if '/' in new_string or "\\" in new_string: - continue - if new_string.endswith("*") and not new_string.startswith("*"): - result["prefix_list"].append(new_string.rstrip("*")) - elif new_string.startswith("*") and not new_string.endswith("*"): - result["suffix_list"].append(new_string.lstrip("*")) - elif new_string.endswith("*") and new_string.startswith("*"): - result["inner_List"].append(new_string.strip("*")) - else: - result["word_list"].append(new_string) - return result - - -def should_be_excluded(exclusions, target): - """ - - Args: - exclusions (str): - target (str): - - Returns: - - """ - result = False - target = target.lower() - groups_of_exclusions = group_str_by_wildcard_character(exclusions) - if target.startswith(tuple(groups_of_exclusions["prefix_list"])): - result = True - if target.endswith(tuple(groups_of_exclusions["suffix_list"])): - result = True - if any([True if inner_text in target else False for inner_text in groups_of_exclusions["inner_List"]]): - result = True - if target in groups_of_exclusions["word_list"]: - result = True - return result - - -def create_zip_file_from_location_path(location_path_str: str, project_name: str, - exclude_folders_str=None, exclude_files_str=None): - """ - - Args: - location_path_str (str): - project_name (str): - exclude_folders_str (str): comma separated string - exclude_files_str (str): comma separated string - - Returns: - str (ZIP file path) - """ - exclude_folders = ".*,bin,target,images,Lib,node_modules" - exclude_files = ".*,*.min.js" - if exclude_folders_str is not None: - exclude_folders += "," - exclude_folders += exclude_folders_str - if exclude_files_str is not None: - exclude_files += "," - exclude_files += exclude_files_str - - from pathlib import Path - import tempfile - temp_dir = tempfile.gettempdir() - extensions = get_cx_supported_file_extensions() - path = Path(location_path_str) - if not path.exists(): - raise FileExistsError(f"{location_path_str} does not exist, abort scan") - absolute_path_str = str(os.path.normpath(path.absolute())) - file_path = f"{temp_dir}/cx_{project_name}.zip" - with ZipFile(file_path, "w", ZIP_DEFLATED) as zip_file: - root_len = len(absolute_path_str) + 1 - for base, dirs, files in os.walk(absolute_path_str): - path_folders = base.split(os.sep) - if any([should_be_excluded(exclude_folders, folder) for folder in path_folders]): - continue - for file in files: - file_lower_case = file.lower() - if not file_lower_case.endswith(tuple(extensions)): - continue - if should_be_excluded(exclude_files, file_lower_case): - continue - fn = os.path.join(base, file) - zip_file.write(fn, fn[root_len:]) - return file_path - - -def cx_scan_from_local_zip_file(preset_name: str, team_full_name: str, project_name: str, zip_file_path: str, - exclude_folders: str, exclude_files: str, incremental: bool = False, - full_scan_cycle=10, branched_project_name=None): - """ - - Args: - preset_name (str): - team_full_name (str): - project_name (str): - zip_file_path (str): - exclude_folders (str): - exclude_files (str): - incremental (bool): - full_scan_cycle (int): - branched_project_name (str): name of the branched project to scan under - - Returns: - return scan id if scan finished, otherwise return None - """ - from CheckmarxPythonSDK.CxRestAPISDK import TeamAPI, ProjectsAPI, ScansAPI - team_api = TeamAPI() - projects_api = ProjectsAPI() - scan_api = ScansAPI() - - if not exists(zip_file_path): - logger.error("[ERROR]: zip file not found. Abort scan.") - exit(1) - - logger.info("get team id") - team_id = team_api.get_team_id_by_team_full_name(team_full_name) - if not team_id: - logger.error(f"[ERROR]: team full name {team_full_name} not exist. Abort scan.") - exit(1) - - project_id = projects_api.get_project_id_by_project_name_and_team_full_name( - project_name=project_name, team_full_name=team_full_name - ) - logger.info(f"project id: {project_id}") - if not project_id: - logger.info("project does not exist. create project with default configuration, will get project id") - project = projects_api.create_project_with_default_configuration(project_name=project_name, team_id=team_id) - project_id = project.id - logger.info(f"new project with project_id: {project_id}") - - if branched_project_name: - logger.info(f"branched project name: {branched_project_name}") - branched_project_id = projects_api.get_project_id_by_project_name_and_team_full_name( - project_name=branched_project_name, team_full_name=team_full_name - ) - if not branched_project_id: - logger.info("branched project does not exist, creating it") - branched_project = projects_api.create_branched_project( - project_id=project_id, branched_project_name=branched_project_name - ) - branched_project_id = branched_project.id - logger.info(f"branched project created with id: {branched_project_id}") - logger.info("waiting for branched project to finish") - while True: - status = projects_api.get_branch_project_status(branch_project_id=branched_project_id) - logger.info(f"branch project status: {status}") - if status == "Completed": - break - time.sleep(10) - logger.info("branched project is ready") - else: - logger.info(f"branched project already exists with id: {branched_project_id}") - project_id = branched_project_id - - preset_id = projects_api.get_preset_id_by_name(preset_name=preset_name) - logger.info("preset id: {}".format(preset_id)) - - logger.info("set exclude folders and exclude files") - projects_api.set_project_exclude_settings_by_project_id( - project_id, exclude_folders_pattern=exclude_folders, exclude_files_pattern=exclude_files - ) - - scans_of_this_project = scan_api.get_all_scans_for_project(project_id=project_id) - number_of_scans_of_this_project = len(scans_of_this_project) + 1 - remainder = number_of_scans_of_this_project % full_scan_cycle - if remainder == 0: - incremental = False - - logger.info("create new scan") - logger.info(f"The scan type will be: {'incremental' if incremental else 'full'} ") - scan = scan_api.create_new_scan_with_settings(project_id=project_id, comment="", preset_id=preset_id, - zipped_source_file_path=str(zip_file_path), - is_incremental=incremental) - scan_id = scan.id - logger.info("scan_id : {}".format(scan_id)) - - logger.info("get scan details by scan id, report scan status") - while True: - scan_detail = scan_api.get_sast_scan_details_by_scan_id(scan_id=scan_id) - scan_status = scan_detail.status.name - logger.info("scan_status: {}".format(scan_status)) - if scan_status == "Finished": - break - elif scan_status in ["Failed", "Canceled"]: - return None - time.sleep(10) - - logger.info("get statistics results by scan id") - statistics = scan_api.get_statistics_results_by_scan_id(scan_id=scan_id) - statistics_updated = { - "High": statistics.high_severity, - "Medium": statistics.medium_severity, - "Low": statistics.low_severity, - "Info": statistics.info_severity - } - logger.info(f"statistics: {statistics_updated}") - return scan_id - - -def generate_report(scan_id: int, report_type: str, report_file_path: str): - """ - - Args: - scan_id (int): - report_type (str): - report_file_path (str): - - Returns: - - """ - from CheckmarxPythonSDK.CxRestAPISDK import ScansAPI - scan_api = ScansAPI() - - logger.info("register scan report") - report = scan_api.register_scan_report(scan_id=scan_id, report_type=report_type) - report_id = report.report_id - logger.info("report_id : {}".format(report_id)) - - logger.info("get report status by id") - while not scan_api.is_report_generation_finished(report_id): - logger.info("report generating") - time.sleep(10) - - logger.info("get report by id") - report_content = scan_api.get_report_by_id(report_id) - - logger.info("write original report") - with open(str(report_file_path), "wb") as f_out: - f_out.write(report_content) - - if report_type.lower() == "csv": - update_csv_report(cx_report_file_path=report_file_path, scan_id=scan_id) - - -def get_similarity_ids_of_a_scan(scan_id): - from CheckmarxPythonSDK.CxODataApiSDK import get_similarity_ids_of_a_scan - return get_similarity_ids_of_a_scan(scan_id=scan_id) - - -def update_csv_report(cx_report_file_path, scan_id): - """ - update CSV report by add the similarityID column (data retrieved by using ODATA API) - Args: - cx_report_file_path: - scan_id: - - Returns: - - """ - logger.info("update csv report") - if not pathlib.Path(cx_report_file_path).exists(): - logger.info(f"report does not exist, file path: {cx_report_file_path}") - return - import csv - from collections import OrderedDict - logger.info("get similarity id by using ODATA API") - similarity_id_path_ids = get_similarity_ids_of_a_scan(scan_id=scan_id) - sim_path_dict = {} - for pair in similarity_id_path_ids: - sim_path_dict[pair.get("PathId")] = pair.get("SimilarityId") - - logger.info("read csv file") - csv_content = [] - with open(cx_report_file_path, newline='', encoding="utf-8-sig") as csvfile: - reader = csv.DictReader(csvfile) - field_names = reader.fieldnames - field_names = [item for item in field_names] - field_names.append('SimilarityID') - for row in reader: - path_id = row.get("Link").split('&')[2].split('=')[1] - path_id = int(path_id) - similarity_id = sim_path_dict.get(path_id) - new_ordered_dict = OrderedDict() - for key, value in row.items(): - new_ordered_dict[key] = value - new_ordered_dict['SimilarityID'] = similarity_id - csv_content.append(new_ordered_dict) - logger.info("write csv file") - with open(cx_report_file_path, 'w', newline='') as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=field_names, dialect='unix') - writer.writeheader() - writer.writerows(csv_content) - - -def run_scan_and_generate_reports(arguments): - preset = arguments.preset - incremental = False if arguments.incremental.lower() == "false" else True - location_type = arguments.location_type - location_path = arguments.location_path - project_full_path = arguments.project_name - exclude_folders = arguments.exclude_folders - exclude_files = arguments.exclude_files - report_xml = arguments.report_xml - report_pdf = arguments.report_pdf - report_csv = arguments.report_csv - full_scan_cycle = int(arguments.full_scan_cycle) - branch_project = arguments.branch_project - logger.info( - f"preset: {preset}\n" - f"incremental: {incremental}\n" - f"location_type: {location_type}\n" - f"location_path: {location_path}\n" - f"project_name: {project_full_path}\n" - f"branch_project: {branch_project}\n" - f"report_xml: {report_xml}\n" - f"report_pdf: {report_pdf}\n" - f"report_csv: {report_csv}\n" - f"full_scan_cycle: {full_scan_cycle}\n" - ) - - logger.info(f"creating zip file by zip the source code folder: {location_path}") - project_path_list = project_full_path.split("/") - project_name = project_path_list[-1] - team_full_name = "/".join(project_path_list[0: len(project_path_list)-1]) - zip_file_path = create_zip_file_from_location_path(location_path, project_name, exclude_folders_str=exclude_folders, - exclude_files_str=exclude_files) - logger.info(f"ZIP file created: {zip_file_path}") - scan_id = cx_scan_from_local_zip_file(preset_name=preset, team_full_name=team_full_name, project_name=project_name, - zip_file_path=zip_file_path, exclude_folders=exclude_folders, - exclude_files=exclude_files, incremental=incremental, - full_scan_cycle=full_scan_cycle, - branched_project_name=branch_project) - - if scan_id is None: - logger.info("Scan did not finish successfully, exit!") - return - - logger.info(f"deleting zip file: {zip_file_path}") - try: - pathlib.Path(zip_file_path).unlink() - except PermissionError: - logger.warning("could not delete zip file, it may be in use by another process") - - if report_xml: - generate_report(scan_id=scan_id, report_type="XML", report_file_path=report_xml) - if report_pdf: - generate_report(scan_id=scan_id, report_type="PDF", report_file_path=report_pdf) - if report_csv: - generate_report(scan_id=scan_id, report_type="CSV", report_file_path=report_csv) - logger.info("report generated successfully") - - -if __name__ == '__main__': - # get command line arguments - cli_arguments = get_command_line_arguments() - cli_arguments = cli_arguments[0] - run_scan_and_generate_reports(cli_arguments) From 14d6513fe64ef6b0a401884714f14a9aefcd6b51 Mon Sep 17 00:00:00 2001 From: cx-happy-yang <30431255+cx-happy-yang@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:54:36 +0800 Subject: [PATCH 25/25] Fix SSL context creation for self-signed CA certificates without BasicConstraints ssl.create_default_context() enables VERIFY_X509_STRICT, which requires certificates loaded as CAs to have the BasicConstraints:CA:TRUE extension. Self-signed server certs (like IIS self-signed certs) typically lack this, causing "invalid CA certificate" errors. Switch to ssl.SSLContext with PROTOCOL_TLS_CLIENT (matching urllib3's approach), which does not enforce this strict check. Co-Authored-By: Claude Opus 4.7 --- CheckmarxPythonSDK/api_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CheckmarxPythonSDK/api_client.py b/CheckmarxPythonSDK/api_client.py index ac94c820..f1283569 100644 --- a/CheckmarxPythonSDK/api_client.py +++ b/CheckmarxPythonSDK/api_client.py @@ -24,12 +24,11 @@ def create_session(configuration: Configuration) -> httpx.Client: if raw_verify is False: verify = False else: - ctx = ssl.create_default_context() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.minimum_version = ssl.TLSVersion.TLSv1_2 ctx.maximum_version = ssl.TLSVersion.TLSv1_3 - if raw_verify is True: - ctx.load_verify_locations(certifi.where()) - else: + ctx.load_verify_locations(certifi.where()) + if raw_verify is not True: ctx.load_verify_locations(raw_verify) verify = ctx return httpx.Client(