import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from xmlrpc.client import Boolean from CommonServerUserPython import * from typing import Any import urllib.parse import re PIPELINE_FIELDS_TO_EXTRACT = { "id", "project_id", "status", "ref", "sha", "created_at", "updated_at", "started_at", "finished_at", "duration", "web_url", "user", } PIPELINE_SCHEDULE_FIELDS_TO_EXTRACT = { "id", "description", "ref", "next_run_at", "active", "created_at", "updated_at", "last_pipeline", } JOB_FIELDS_TO_EXTRACT = { "created_at", "started_at", "finished_at", "duration", "id", "name", "pipeline", "ref", "stage", "web_url", "status", } """--------------------- CLIENT CLASS --------------------""" class Client(BaseClient): def __init__(self, project_id, base_url, verify, proxy, headers, trigger_token=None): super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers) self.project_id = project_id self.trigger_token = trigger_token def group_projects_list_request(self, params: dict | None, group_id: str | None) -> dict: headers = self._headers suffix = f"/groups/{group_id}/projects" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def get_project_list_request(self, params: dict | None) -> list: headers = self._headers suffix = "/projects" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def issue_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def commit_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/commits" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202], resp_type="json") return response def get_raw_file_request(self, file_path: str, ref: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/files/{file_path}/raw" params = {"ref": ref} response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202], resp_type="text") return response def branch_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/branches" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202], resp_type="json") return response def group_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = "/groups" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202], resp_type="json") return response def issue_note_list_request(self, params: dict | None, issue_iid: str | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues/{issue_iid}/notes" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def merge_request_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202], resp_type="json") return response def merge_request_note_list_request(self, params: dict | None, merge_request_iid: str | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests/{merge_request_iid}/notes" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def group_member_list_request(self, group_id: str | None) -> dict: headers = self._headers suffix = f"/groups/{group_id}/members" response = self._http_request("GET", suffix, headers=headers, ok_codes=[200, 202]) return response def codes_search_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/search" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def project_user_list_request(self, params: dict | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/users" response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def create_issue_request(self, labels: str, title: str, description: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues" params = assign_params(labels=labels, title=title, description=description) response = self._http_request("POST", suffix, headers=headers, params=params, ok_codes=[201]) return response def create_branch_request(self, branch: str, ref: str) -> dict: params = assign_params(branch=branch, ref=ref) headers = self._headers suffix = f"/projects/{self.project_id}/repository/branches" response = self._http_request("POST", suffix, params=params, headers=headers) return response def branch_delete_request(self, branch: str) -> dict: headers = self._headers response = self._http_request( "DELETE", f"projects/{self.project_id}/repository/branches/{branch}", headers=headers, resp_type="text", ok_codes=[200, 202, 204], ) return response def delete_merged_branches_request(self) -> dict: headers = self._headers response = self._http_request( "DELETE", f"/projects/{self.project_id}/repository/merged_branches", headers=headers, ok_codes=[200, 202, 204] ) return response def version_get_request(self) -> dict: headers = self._headers suffix = "/version" response = self._http_request("GET", suffix, headers=headers, ok_codes=[200, 202]) return response def issue_update_request(self, issue_id: str | Any, params: dict) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues/{issue_id}" response = self._http_request("PUT", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def file_get_request(self, file_path: str, ref: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/files/{file_path}" params = {"ref": ref} response = self._http_request("GET", suffix, headers=headers, params=params, ok_codes=[200, 202]) return response def file_create_request( self, file_path: str | None, branch: str | None, commit_msg: str, author_email: str, author_name: str | None, content: str | None, execute_filemode: str | None, ) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/files/{file_path}" params = assign_params(author_email=author_email, author_name=author_name, execute_filemode=execute_filemode) body = assign_params(branch=branch, commit_message=commit_msg, content=content) response = self._http_request("POST", suffix, headers=headers, data=body, params=params, ok_codes=[201]) return response def commit_single_request(self, commit_id: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/commits/{commit_id}" response = self._http_request("GET", suffix, headers=headers, ok_codes=[200, 202], resp_type="json") return response def branch_single_request(self, branch_name: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/branches/{branch_name}" response = self._http_request("GET", suffix, headers=headers, ok_codes=[200, 202], resp_type="json") return response def file_update_request( self, file_path: str, branch: str | None, start_branch: str | None, encoding: str | None, author_email: str | None, author_name: str | None, commit_message: str | None, last_commit_id: str | None, execute_filemode: str | None, content: str | None, ) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/files/{file_path}" params = assign_params( start_branch=start_branch, encoding=encoding, author_email=author_email, author_name=author_name, last_commit_id=last_commit_id, execute_filemode=execute_filemode, ) body = assign_params(branch=branch, commit_message=commit_message, content=content) response = self._http_request("PUT", suffix, headers=headers, data=body, params=params, ok_codes=[200, 202]) return response def file_delete_request(self, file_path: str, branch: str, commit_message: str) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/repository/files/{file_path}" params = assign_params(branch=branch, commit_message=commit_message) response = self._http_request( "DELETE", suffix, headers=headers, params=params, ok_codes=[200, 202, 204], resp_type="text" ) return response def issue_note_create_request(self, issue_iid_: str, body_: str | Any, confidential_: str | Any) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues/{issue_iid_}/notes" params = assign_params(confidential=confidential_) data = assign_params(body=body_) response = self._http_request("POST", suffix, headers=headers, params=params, json_data=data, ok_codes=[201]) return response def issue_note_delete_request(self, issue_iid: int | None, note_id: int | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues/{issue_iid}/notes/{note_id}" response = self._http_request("DELETE", suffix, headers=headers, ok_codes=[200, 202, 204], resp_type="text") return response def issue_note_update_request(self, issue_iid: int | None, note_id: int | None, body: str | None) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/issues/{issue_iid}/notes/{note_id}" data = assign_params(body=body) response = self._http_request("PUT", suffix, headers=headers, json_data=data, ok_codes=[200, 202]) return response def merge_request_create_request( self, source_branch: str | None, target_branch: str | None, title: str | None, assignee_ids: str | None, reviewer_ids: str | None, description: str | None, target_project_id: str | None, labels: str | None, milestone_id: str | None, remove_source_branch: str | None, allow_collaboration: str | None, allow_maintainer_to_push: str | None, approvals_before_merge: str | None, squash: str | None, ) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests" params = assign_params( assignee_ids=assignee_ids, reviewer_ids=reviewer_ids, description=description, target_project_id=target_project_id, labels=labels, milestone_id=milestone_id, remove_source_branch=remove_source_branch, allow_collaboration=allow_collaboration, allow_maintainer_to_push=allow_maintainer_to_push, approvals_before_merge=approvals_before_merge, squash=squash, ) data = assign_params(source_branch=source_branch, target_branch=target_branch, title=title) response = self._http_request("POST", suffix, headers=headers, json_data=data, params=params, ok_codes=[201]) return response def merge_request_update_request( self, merge_request_id: str | None, target_branch: str | None, title: str | None, assignee_ids: str | None, reviewer_ids: str | None, description: str | None, target_project_id: str | None, add_labels: str | None, remove_labels: str | None, milestone_id: str | None, state_event: str | None, remove_source_branch: str | None, allow_collaboration: str | None, allow_maintainer_to_push: str | None, approvals_before_merge: str | None, discussion_locked: str | None, squash: str | None, ) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests/{merge_request_id}" params = assign_params( assignee_ids=assignee_ids, reviewer_ids=reviewer_ids, description=description, target_project_id=target_project_id, add_labels=add_labels, remove_labels=remove_labels, milestone_id=milestone_id, state_event=state_event, remove_source_branch=remove_source_branch, allow_collaboration=allow_collaboration, allow_maintainer_to_push=allow_maintainer_to_push, approvals_before_merge=approvals_before_merge, squash=squash, discussion_locked=discussion_locked, ) data = assign_params(target_branch=target_branch, title=title) response = self._http_request("PUT", suffix, headers=headers, json_data=data, params=params, ok_codes=[200, 202]) return response def merge_request_note_create_request(self, merge_request_iid: str | Any, body: str | Any) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests/{merge_request_iid}/notes" data = assign_params(body=body) response = self._http_request("POST", suffix, headers=headers, json_data=data, ok_codes=[201]) return response def merge_request_note_update_request(self, merge_request_iid: str | Any, note_id: str | Any, body: str | Any) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests/{merge_request_iid}/notes/{note_id}" data = assign_params(body=body) response = self._http_request("PUT", suffix, headers=headers, json_data=data, ok_codes=[200, 202]) return response def merge_request_note_delete_request(self, merge_request_iid: str | Any, note_id: str | Any) -> dict: headers = self._headers suffix = f"/projects/{self.project_id}/merge_requests/{merge_request_iid}/notes/{note_id}" response = self._http_request("DELETE", suffix, headers=headers, ok_codes=[200, 202, 204], resp_type="text") return response def get_pipeline_request(self, project_id: str, pipeline_id: Optional[str], ref: Optional[str], status: Optional[str]): headers = self._headers base_suffix = f"projects/{project_id}/pipelines" final_suffix = f"{base_suffix}/{pipeline_id}" if pipeline_id else base_suffix return self._http_request( "get", final_suffix, headers=headers, params=assign_params(ref=ref, status=status), ) def get_pipeline_schedules_request(self, project_id: str, pipeline_schedule_id: Optional[str]): headers = self._headers base_suffix = f"projects/{project_id}/pipeline_schedules" final_suffix = f"{base_suffix}/{pipeline_schedule_id}" if pipeline_schedule_id else base_suffix return self._http_request("get", final_suffix, headers=headers) def get_pipeline_job_request(self, project_id: str, pipeline_id: str): headers = self._headers suffix = f"projects/{project_id}/pipelines/{pipeline_id}/jobs" return self._http_request("get", suffix, headers=headers) def get_job_artifact_request(self, project_id: str, job_id: str, artifact_path_suffix: str): headers = self._headers suffix = f"projects/{project_id}/jobs/{job_id}/artifacts/{artifact_path_suffix}" return self._http_request("get", suffix, headers=headers, resp_type="text") def gitlab_trigger_pipeline(self, project_id: str, data: dict) -> dict: """Triggers a pipeline on GitLab. Args: project_id: Project ID on which to run the pipeline. data: The request body in JSON format. Returns: dict: The response in JSON format. """ suffix = f"projects/{project_id}/trigger/pipeline" return self._http_request("POST", suffix, data=data) def gitlab_cancel_pipeline(self, project_id: str, pipeline_id: str) -> dict: """Cancel a pipeline on GitLab. Args: project_id: Project ID on which to cancel the pipeline. pipeline_id: Pipeline ID to cancel. Returns: dict: The response in JSON format. """ suffix = f"/projects/{project_id}/pipelines/{pipeline_id}/cancel" return self._http_request("POST", suffix) """ HELPER FUNCTIONS """ def encode_file_path_if_needed(file_path: str) -> str: """Encode the file path if not already encoded. Args: file_path (str): The file path, can be URL encoded or not. Returns: str: Return the file path as is if already URL encoded, else, returns the encoding it. """ file_path_prefix = "./" if file_path.startswith("./") else "" # If starts with ./, then we don't want to encode the suffix, only the rest file_path_to_encode = file_path[2:] if file_path_prefix else file_path encoded_file_path = "" # To decode file_path_to_encode decoded_file_path = urllib.parse.unquote(file_path_to_encode) if decoded_file_path == file_path_to_encode: # If they are equal, that means file_path_to_encode is not encoded, # since we tried to decode it, and we got the same value # We can go ahead and encode it encoded_file_path = urllib.parse.quote(file_path_to_encode, safe="") else: # file_path_to_encode is already encoded, no need to encode it encoded_file_path = file_path_to_encode return f"{file_path_prefix}{encoded_file_path}" def check_args_for_update(args: dict, optional_params: list) -> dict: """ This function checks that at least one argument from optional params is in args. input: optional params, args from user. output: if there isn't at least one argument then throw an exception. otherwise- dict of params for update and True boolean argument. """ params, args_valid = {}, False for optional_param in optional_params: if args.get(optional_param): params[optional_param] = args.get(optional_param) args_valid = True if not args_valid: raise DemistoException("At least one of arguments is required for the request to be successful\n") return params def validate_pagination_values(limit: int, page_number: int) -> tuple[int, int, int]: if limit < 0 or page_number < 0: raise DemistoException("limit and page arguments must be positive") per_page = limit if limit < 100 else 100 return limit, per_page, page_number def response_according_pagination(client_function: Any, limit: int, page_number: int, params: dict, suffix_id: str | None): """ This function gets results according to the pagination values. input: 1. parameters for the client function 2. suffix_id- if the suffix contain id(issue id for example) suffix_id would contain it, otherwise None. 3. name of the client function. output: list(representing the pages) of list of raw dictionary results. """ limit, per_page, page_number = validate_pagination_values(limit, page_number) params.update({"per_page": per_page, "page": page_number}) items_count_total = 0 response: list[dict[str, Any]] = [] while items_count_total < limit: response_temp = client_function(params, suffix_id) if suffix_id else client_function(params) if not response_temp: break response.extend(response_temp) items_count_total += len(response_temp) params["per_page"] = 50 if (limit - items_count_total >= 50) else limit - items_count_total params["page"] = params["page"] + 1 return response def partial_response_fields(object_name: str): """ This function returns the fields for context data after filtering them. If a wanted field is inside a dict it the name of the dict would be his data, otherwise the data is empty, input: name of object returns: wanted fields. """ if object_name == "Branch": return { "name": None, "commit": ["id", "title", "short_id", "committed_date", "author_name"], "merged": None, "protected": None, } if object_name == "Issue": return { "id": None, "iid": None, "title": None, "description": None, "author": ["name", "id"], "assignee": ["name", "id"], "created_at": None, "updated_at": None, "closed_at": None, "state": None, "severity": None, } if object_name == "Merge Request": return { "id": None, "iid": None, "title": None, "description": None, "state": None, "author": ["name", "id"], "created_at": None, "closed_at": None, "source_branch": None, "target_branch": None, } if object_name == "Commit": return {"id": None, "short_id": None, "title": None, "message": None, "author": ["name"], "created_at": None} if object_name == "Issue Note": return {"id": None, "created_at": None, "updated_at": None, "body": None, "noteable_iid": None, "author": ["name", "id"]} if object_name == "Merge Request Note": return {"id": None, "created_at": None, "updated_at": None, "body": None, "noteable_iid": None, "author": ["name", "id"]} if object_name == "Project": return { "id": None, "description": None, "name": None, "created_at": None, "default_branch": None, "namespace": ["name", "id"], } return {} def partial_response(response: list, object_type: str): """ This function filters the raw response from the API according to the dict of fields given. input: raw response which is a list of dictionaries, fields for the context data display. output: partial dictionary results. """ partial_response: list[dict[str, Any]] = [] fields = partial_response_fields(object_type) for raw_dict in response: partial_dict: dict[str, Any] = {} for field_key, field_dict_vals in fields.items(): if not (field_dict_vals): partial_dict[field_key] = raw_dict.get(field_key, "") elif raw_dict.get(field_key): temp_dict_vals: dict[str, Any] = {} for val in field_dict_vals: temp_dict_vals[val] = raw_dict.get(field_key, {}).get(val, "") partial_dict[field_key] = temp_dict_vals partial_response.append(partial_dict) return partial_response def verify_project_id(client: Client, project_id: int) -> Boolean: """ This function verify that the user can access the project. input: project_id output: True is the project_id is valid, otherwise an error will occur. """ # This is a way to search the project_id api. params = assign_params(id_before=(project_id + 1), per_page=1) response = client.get_project_list_request(params) if response[0].get("id") != project_id: raise DemistoException(f"Project with project_id {project_id} does not exist") return True def return_date_arg_as_iso(arg: str | None) -> str | None: """ This function converts timestamp format (