diff --git a/.gitignore b/.gitignore index d964dcaa..47708e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,5 @@ build .claude *.txt -*.json \ No newline at end of file +*.json +*.har diff --git a/CHANGELOG.md b/CHANGELOG.md index 038c4e31..61ec6bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # 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 +* [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 +* [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 +* [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 + 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/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/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..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): @@ -22,34 +24,40 @@ def get_all_predicates_for_similarity_id( project_ids: List[str] = None, include_comment_json: bool = None, scan_id: str = None, - ) -> dict: + offset: int = 0, + limit: int = 100, + ) -> PredicateHistoryResponse: """ Args: similarity_id (str): project_ids (list of str): include_comment_json (bool): scan_id (str): + offset (int): + limit (int): Returns: - dict + PredicateHistoryResponse """ url = f"{self.base_url}/{similarity_id}" params = { "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 ) - 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): @@ -57,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] @@ -245,12 +253,16 @@ def get_all_predicates_for_similarity_id( project_ids: List[str] = None, include_comment_json: bool = None, scan_id: str = None, -) -> dict: + offset: int = 0, + limit: int = 100, +) -> PredicateHistoryResponse: 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, ) @@ -258,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, 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 949736c7..c2530eb5 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/CxPortalWebService.py @@ -1227,21 +1227,868 @@ 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:`get_sources_by_scan_id` (plural) instead. + + 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 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: """ - Args: - scan_id (int): + Args: + scan_id (int): + + Returns: + + """ + response = self.suds_client.execute( + "UnlockScan", i_SessionID="0", i_ScanID=scan_id + ) + return { + "IsSuccesfull": response["IsSuccesfull"], + "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), + } - Returns: + 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( - "UnlockScan", i_SessionID="0", i_ScanID=scan_id + "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]) + ], } @@ -1487,5 +2334,227 @@ 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 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) + + +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 e80126f9..6f1890d0 100644 --- a/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py +++ b/CheckmarxPythonSDK/CxPortalSoapApiSDK/__init__.py @@ -1,42 +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, + 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/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/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py index 7bcc6bcf..976f2969 100644 --- a/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py +++ b/CheckmarxPythonSDK/CxRestAPISDK/ScansAPI.py @@ -1481,3 +1481,44 @@ 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. + + .. 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 + 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 # page number, not record offset + + 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 + len(page.results) >= page.total_count: + break + offset += 1 + + return all_results 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" diff --git a/CheckmarxPythonSDK/api_client.py b/CheckmarxPythonSDK/api_client.py index 1035ef37..f1283569 100644 --- a/CheckmarxPythonSDK/api_client.py +++ b/CheckmarxPythonSDK/api_client.py @@ -24,18 +24,18 @@ 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( cert=configuration.cert, proxy=configuration.proxy, transport=httpx.HTTPTransport(retries=3, verify=verify), + follow_redirects=True, headers={"User-Agent": f"checkmarx-python-sdk/{__version__}"}, ) @@ -252,6 +252,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/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 81505849..9ae6a360 100644 --- a/docs/CxSAST_Portal_SOAP_API_List.md +++ b/docs/CxSAST_Portal_SOAP_API_List.md @@ -1,36 +1,119 @@ -# The CxSAST Portal SOAP API list -1. cx portal web service - - add_license_expiration_notification - - create_new_preset - - create_scan_report - - delete_preset - - delete_project - - delete_projects - - export_preset - - export_queries - - get_associated_group_list - - get_compare_scan_results - - get_import_queries_status - - get_path_comments_history - - get_queries_categories - - get_query_collection - - get_query_id_by_language_group_and_query_name - - get_name_of_user_who_marked_false_positive_from_comments_history - - get_preset_list - - get_projects_display_data - - get_result_path - - get_results_for_scan - - get_server_license_data - - get_server_license_summary - - get_user_profile_data - - get_version_number - - get_version_number_as_int - - import_preset - - import_queries - - lock_scan - - unlock_scan -2. cx Audit web service - - get_files_extensions - - get_source_code_for_scan - - upload_queries - \ No newline at end of file +# 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}` | 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..7f8968cb --- /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_v6.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/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/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() 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() diff --git a/examples/CxSAST/dump_all_path_ids.py b/examples/CxSAST/dump_all_path_ids.py new file mode 100644 index 00000000..b66e74a9 --- /dev/null +++ b/examples/CxSAST/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() diff --git a/tests/CxOne/test_sast_preset_manager_api.py b/tests/CxOne/test_sast_preset_manager_api.py deleted file mode 100644 index e37a0fc9..00000000 --- a/tests/CxOne/test_sast_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"]) 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 4c920f41..31cc3672 100644 --- a/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py +++ b/tests/CxSAST/CxPortalSOAP/test_cx_portal_web_service.py @@ -5,35 +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, + update_preset, + update_result_comment, + update_result_state, + update_scan_comment, ) from .. import get_project_id @@ -310,3 +351,329 @@ 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", "") + + +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 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(): 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