diff --git a/content/response_integrations/third_party/community/pager_duty/actions/Ping.py b/content/response_integrations/third_party/community/pager_duty/actions/Ping.py index 6cc4af3ca..c0e99abe4 100644 --- a/content/response_integrations/third_party/community/pager_duty/actions/Ping.py +++ b/content/response_integrations/third_party/community/pager_duty/actions/Ping.py @@ -12,9 +12,7 @@ from typing import NoReturn -SUCCESS_MESSAGE: str = ( - "Successfully connected to the PagerDuty API." -) +SUCCESS_MESSAGE: str = "Successfully connected to the PagerDuty API." ERROR_MESSAGE: str = "Failed to connect to the PagerDuty API." @@ -31,10 +29,17 @@ def _extract_action_parameters(self) -> None: param_name="api_key", is_mandatory=True, ) + self.verify_ssl = extract_configuration_param( + self.soar_action, + provider_name=INTEGRATION_NAME, + param_name="Verify SSL", + default_value=True, + input_type=bool, + ) def _init_api_clients(self): """Prepare API client""" - return PagerDutyManager(self.api_key) + return PagerDutyManager(self.api_key, verify_ssl=self.verify_ssl) def _perform_action(self, _=None) -> None: self.api_client.test_connectivity() diff --git a/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.py b/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.py index a243e9576..639930aeb 100644 --- a/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.py +++ b/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.py @@ -1,7 +1,8 @@ from __future__ import annotations +import datetime import sys -import uuid +from typing import Any from soar_sdk.SiemplifyConnectors import SiemplifyConnectorExecution from soar_sdk.SiemplifyConnectorsDataModel import AlertInfo @@ -10,7 +11,9 @@ dict_to_flat, output_handler, ) +from TIPCommon.types import SingleJson +from ..core.constants import SEVERITY_HIGH, SEVERITY_LOW from ..core.PagerDutyManager import PagerDutyManager CONNECTOR_NAME = "PagerDuty" @@ -19,9 +22,9 @@ @output_handler -def main(is_test_run): - processed_alerts = [] # The main output of each connector run - siemplify = SiemplifyConnectorExecution() # Siemplify main SDK wrapper +def main(is_test_run: bool) -> None: + processed_alerts: list[AlertInfo] = [] + siemplify = SiemplifyConnectorExecution() siemplify.script_name = CONNECTOR_NAME if is_test_run: @@ -31,14 +34,56 @@ def main(is_test_run): siemplify.LOGGER.info("----------------- Main - Param Init -----------------") - api_key = siemplify.extract_connector_param(param_name="apiKey") - acknowledge_enabled = siemplify.extract_connector_param(param_name="acknowledge") + api_key: str = siemplify.extract_connector_param(param_name="apiKey") + acknowledge_enabled: str = siemplify.extract_connector_param( + param_name="acknowledge" + ) + max_hours_backwards: int = siemplify.extract_connector_param( + param_name="Max Hours Backwards", + input_type=int, + default_value=24, + ) + services: str = siemplify.extract_connector_param(param_name="Services") + proxy_address: str = siemplify.extract_connector_param( + param_name="Proxy Server Address" + ) + proxy_username: str = siemplify.extract_connector_param(param_name="Proxy Username") + proxy_password: str = siemplify.extract_connector_param(param_name="Proxy Password") siemplify.LOGGER.info("------------------- Main - Started -------------------") - manager = PagerDutyManager(api_key) + manager = PagerDutyManager( + api_key=api_key, + ) + + if proxy_address: + if "://" not in proxy_address: + proxy_address = "http://" + proxy_address + from urllib.parse import urlparse + server_url = urlparse(proxy_address) + scheme: str = server_url.scheme + hostname: str | None = server_url.hostname + port: int | None = server_url.port + credentials: str = "" + if proxy_username and proxy_password: + credentials = f"{proxy_username}:{proxy_password}@" + proxy_str: str = f"{scheme}://{credentials}{hostname}" + if port: + proxy_str += f":{port}" + manager.requests_session.proxies = {"http": proxy_str, "https": proxy_str} try: - incidents_list = manager.list_incidents() + time_diff: datetime.timedelta = datetime.timedelta(hours=max_hours_backwards) + since: str = ( + datetime.datetime.now(datetime.timezone.utc) - time_diff + ).strftime("%Y-%m-%dT%H:%M:%SZ") + params: dict[str, Any] = {"since": since} + if services: + params["service_ids[]"] = [ + s.strip() for s in services.split(",") if s.strip() + ] + incidents_list: list[SingleJson] = manager.list_filtered_incidents( + params=params + ) if incidents_list is None: siemplify.LOGGER.info( "No events were retrieved for the specified timeframe from PagerDuty", @@ -46,18 +91,20 @@ def main(is_test_run): return siemplify.LOGGER.info(f"Retrieved {len(incidents_list)} events from PagerDuty") for incident in incidents_list: - alert_id = incident["incident_key"] - # Map the severity in PagerDuty to the severity levels in Siemplify - severity = get_siemplify_mapped_severity(incident["urgency"]) + alert_id: str = incident.get("incident_key", "") + + severity: str | None = get_siemplify_mapped_severity( + incident.get("urgency", "low") + ) - siemplify_alert = build_alert_info(siemplify, incident, severity) + siemplify_alert: AlertInfo = build_alert_info(siemplify, incident, severity) if siemplify_alert: processed_alerts.append(siemplify_alert) siemplify.LOGGER.info(f"Added incident {alert_id} to package results") - # `acknowledge_enabled` is a str, hence the str comparison below + if acknowledge_enabled: - incident_got = manager.acknowledge_incident(alert_id) + incident_got = manager.acknowledge_incident(incident["id"]) siemplify.LOGGER.info( f"Incident {incident_got} acknowledged in PagerDuty", ) @@ -71,18 +118,22 @@ def main(is_test_run): siemplify.return_package(processed_alerts) -def get_siemplify_mapped_severity(severity): - severity_map = {"high": "100", "low": "-1"} - return severity_map.get(severity) +def get_siemplify_mapped_severity(severity: str) -> str | None: + severity_map: dict[str, str] = {"high": SEVERITY_HIGH, "low": SEVERITY_LOW} + return severity_map.get(severity.lower()) if severity else None -def build_alert_info(siemplify, incident, severity): +def build_alert_info( + siemplify: SiemplifyConnectorExecution, incident: SingleJson, severity: str | None +) -> AlertInfo: """Returns an alert, which is an aggregation of basic events.""" - alert_info = AlertInfo() + alert_info: AlertInfo = AlertInfo() alert_info.display_id = incident["id"] - alert_info.ticket_id = str(uuid.uuid4()) - alert_info.name = "PagerDuty Incident: " + incident["title"] - alert_info.rule_generator = incident["first_trigger_log_entry"]["summary"] + alert_info.ticket_id = incident["id"] + alert_info.name = incident['id'] + alert_info.rule_generator = ( + incident.get("first_trigger_log_entry", {}).get("summary", "No Summary") + ) alert_info.start_time = convert_string_to_unix_time(incident["created_at"]) alert_info.end_time = alert_info.start_time alert_info.severity = severity diff --git a/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.yaml b/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.yaml index 28ac1bf4a..1c9b3e0c8 100644 --- a/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.yaml +++ b/content/response_integrations/third_party/community/pager_duty/connectors/PagerDutyConnector.yaml @@ -16,6 +16,20 @@ parameters: is_mandatory: true is_advanced: false mode: script + - name: Max Hours Backwards + default_value: '24' + type: integer + description: Max hours backwards to fetch incidents on first run. + is_mandatory: false + is_advanced: false + mode: script + - name: Services + default_value: '' + type: string + description: Comma-separated list of service IDs to filter incidents by. + is_mandatory: false + is_advanced: true + mode: script - name: DeviceProductField default_value: device_product type: string @@ -38,6 +52,27 @@ parameters: is_mandatory: true is_advanced: false mode: regular + - name: Proxy Server Address + default_value: '' + type: string + description: Address of the proxy server, if applicable. + is_mandatory: false + is_advanced: true + mode: script + - name: Proxy Username + default_value: 'null' + type: string + description: Username for proxy authentication. + is_mandatory: false + is_advanced: true + mode: script + - name: Proxy Password + default_value: 'null' + type: string + description: Password for proxy authentication. + is_mandatory: false + is_advanced: true + mode: script description: The connector pulls events from the Pagerduty API https://developer.pagerduty.com/api-reference/9d0b4b12e36f9-list-incidents. integration: PagerDuty rules: [ ] diff --git a/content/response_integrations/third_party/community/pager_duty/core/PagerDutyManager.py b/content/response_integrations/third_party/community/pager_duty/core/PagerDutyManager.py index 856a52a4c..0b33df457 100644 --- a/content/response_integrations/third_party/community/pager_duty/core/PagerDutyManager.py +++ b/content/response_integrations/third_party/community/pager_duty/core/PagerDutyManager.py @@ -1,48 +1,69 @@ from __future__ import annotations +from typing import Any from urllib.parse import quote_plus import requests +from TIPCommon.types import SingleJson class PagerDutyManager: - BASE_URL = "https://api.pagerduty.com" - INCIDENTS_URI = "/incidents" + BASE_URL: str = "https://api.pagerduty.com" + INCIDENTS_URI: str = "/incidents" - def __init__(self, api_key: str, verify_ssl: bool = False): - """Initializes PagerDutyManager with params as set in connector config""" - self.api_key = api_key - self.verify_ssl = verify_ssl + def __init__( + self, + api_key: str, + verify_ssl: bool = True, + from_email: str | None = None, + ) -> None: + """Initializes PagerDutyManager with params as set in connector config. - self.requests_session = requests.Session() - self.requests_session.verify = self.verify_ssl + Args: + api_key: PagerDuty API key. + verify_ssl: Whether to verify SSL certificates. + from_email: The email address of the user performing the action. + """ + self.api_key: str = api_key + self.verify_ssl: bool = verify_ssl + self.from_email: str | None = from_email + + self.requests_session: requests.Session = requests.Session() + self.requests_session.verify: bool = self.verify_ssl - def test_connectivity(self): + def test_connectivity(self) -> None: """Tests connectivity and authentication to the PagerDuty API.""" - url = self.BASE_URL + "/abilities" - headers = { + url: str = self.BASE_URL + "/abilities" + headers: dict[str, str] = { "Accept": "application/vnd.pagerduty+json;version=2", "Authorization": f"Token token={self.api_key}", } - response = self.requests_session.get( + response: requests.Response = self.requests_session.get( url, headers=headers, timeout=10, ) response.raise_for_status() - def acknowledge_incident(self, incident_id): - """Acknowledges an incident in PagerDuty + def acknowledge_incident(self, incident_id: str) -> requests.Response: + """Acknowledges an incident in PagerDuty. + API Reference: https://developer.pagerduty.com/api-reference/8a0e1aa2ec666-update-an-incident + + Args: + incident_id (str): The ID of the incident to acknowledge. + + Returns: + requests.Response: The API response. """ - url = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" - data = {"statuses[]": "acknowledged"} - headers = { + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" + data: dict[str, str] = {"statuses[]": "acknowledged"} + headers: dict[str, str] = { "Content-Type": "application/json", "Accept": "application/vnd.pagerduty+json;version=2", "Authorization": f"Token token={self.api_key}", } - response = requests.request( + response: requests.Response = requests.request( "GET", url, headers=headers, @@ -51,48 +72,177 @@ def acknowledge_incident(self, incident_id): ) return response - def list_oncalls(self): - url = f"{self.BASE_URL}/oncalls" + def resolve_incident(self, incident_id: str) -> SingleJson: + """Resolves an incident in PagerDuty. + + Args: + incident_id: The ID of the incident to resolve. + + Returns: + SingleJson: The API response. + """ + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" + payload: SingleJson = { + "incident": { + "type": "incident", + "status": "resolved" + } + } + headers: dict[str, str] = self._get_auth_headers() + headers["Content-Type"] = "application/json" + response = self.requests_session.put( + url, + json=payload, + headers=headers, + timeout=10, + ) + response.raise_for_status() + return response.json() + + def add_incident_note(self, incident_id: str, content: str) -> SingleJson: + """Adds a note to an incident in PagerDuty. + + Args: + incident_id: The ID of the incident to add the note to. + content: The content of the note. + + Returns: + SingleJson: The API response. + """ + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}/notes" + payload: SingleJson = { + "note": { + "content": content + } + } + headers: dict[str, str] = self._get_auth_headers() + headers["Content-Type"] = "application/json" + response = self.requests_session.post( + url, + json=payload, + headers=headers, + timeout=10, + ) + response.raise_for_status() + return response.json() + + def get_incident_notes(self, incident_id: str) -> list[SingleJson]: + """Gets notes for an incident from PagerDuty. + + Args: + incident_id: The ID of the incident. + + Returns: + list[SingleJson]: List of notes. + """ + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}/notes" + response = self.requests_session.get( + url, + headers=self._get_auth_headers(), + timeout=10, + ) + response.raise_for_status() + return response.json().get("notes", []) + + def get_incident(self, incident_id: str) -> SingleJson: + """Gets an incident from PagerDuty by ID. + + Args: + incident_id (str): The ID of the incident to retrieve. + + Returns: + SingleJson: The incident details. + """ + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" + response = self.requests_session.get( + url, + headers=self._get_auth_headers(), + timeout=10, + ) + response.raise_for_status() + return response.json().get("incident") + + def list_oncalls(self) -> list[SingleJson]: + """Lists on-calls. + + Returns: + list[SingleJson]: List of on-calls. + """ + url: str = f"{self.BASE_URL}/oncalls" response = self.requests_session.get( url=url, headers=self._get_auth_headers(), timeout=10 ) response.raise_for_status() return response.json().get("oncalls", []) - def get_all_incidents(self): + def get_all_incidents(self) -> list[SingleJson]: + """Gets all incidents. + + Returns: + list[SingleJson]: List of incidents. + """ self.requests_session.headers.update( {"Authorization": f"Token token={self.api_key}"}, ) - url = self.BASE_URL + "/incidents" - response = self.requests_session.get(url=url, timeout=10) + url: str = self.BASE_URL + "/incidents" + response: requests.Response = self.requests_session.get(url=url, timeout=10) response.raise_for_status() - incident_data = response.json() + incident_data: SingleJson = response.json() return incident_data.get("incidents") - def list_incidents(self): - url = f"{self.BASE_URL}/incidents" - response = self.requests_session.get( + def list_incidents(self) -> list[SingleJson] | str: + """Lists incidents. + + Returns: + list[SingleJson] | str: List of incidents or "No Incidents Found". + """ + url: str = f"{self.BASE_URL}/incidents" + response: requests.Response = self.requests_session.get( url=url, headers=self._get_auth_headers(), timeout=10 ) response.raise_for_status() - incidents = response.json().get("incidents") + incidents: list[SingleJson] = response.json().get("incidents") if incidents: return incidents return "No Incidents Found" - def list_users(self): - url = f"{self.BASE_URL}/users" - response = self.requests_session.get( + def list_users(self) -> list[SingleJson]: + """Lists users. + + Returns: + list[SingleJson]: List of users. + """ + url: str = f"{self.BASE_URL}/users" + response: requests.Response = self.requests_session.get( url=url, headers=self._get_auth_headers(), timeout=10 ) response.raise_for_status() return response.json().get("users", []) - def create_incident(self, email_from, title, service, urgency, body): + def create_incident( + self, + email_from: str, + title: str, + service: str, + urgency: str, + body: str, + ) -> SingleJson: + """Creates an incident. + + Args: + email_from (str): Email address of the user creating the incident. + title (str): Title of the incident. + service (str): Service ID. + urgency (str): Urgency level. + body (str): Incident details. + + Returns: + SingleJson: The created incident or message. + """ self.requests_session.headers.update( {"Authorization": f"Token token={self.api_key}", "From": f"{email_from}"}, ) - payload = { + payload: SingleJson = { "incident": { "type": "incident", "title": f"{title}", @@ -101,24 +251,37 @@ def create_incident(self, email_from, title, service, urgency, body): "body": {"type": "incident_body", "details": f"{body}"}, }, } - url = self.BASE_URL + "/incidents" + url: str = self.BASE_URL + "/incidents" response = self.requests_session.post(url=url, json=payload, timeout=10) + if response.status_code == 400: + raise Exception(f"400 Bad Request: {response.text}") response.raise_for_status() if response.json().get("incident_number") != 0: return response.json() return {"message": "No Incident Found"} - def get_incident_ID(self, incidentID, email_from): + def get_incident_ID(self, incidentID: str, email_from: str) -> SingleJson: + """Gets incident by ID. + + Args: + incidentID (str): Incident ID or key. + email_from (str): Email address. + + Returns: + SingleJson: Incident data. + """ self.requests_session.headers.update( {"Authorization": f"Token token={self.api_key}", "From": f"{email_from}"}, ) - parameters = {"user_ids[]": incidentID} - url = self.BASE_URL + self.INCIDENTS_URI - response = self.requests_session.get(url=url, json=parameters, timeout=10) + parameters: dict[str, str] = {"user_ids[]": incidentID} + url: str = self.BASE_URL + self.INCIDENTS_URI + response: requests.Response = self.requests_session.get( + url=url, json=parameters, timeout=10 + ) response.raise_for_status() - incident_data = {} - info_got = response.json().get("incidents") + incident_data: SingleJson = {} + info_got: list[SingleJson] = response.json().get("incidents") for incident in info_got: if incident.get("incident_key") == incidentID: @@ -126,27 +289,43 @@ def get_incident_ID(self, incidentID, email_from): return incident_data - def get_user_by_email(self, email): - url = f"{self.BASE_URL}/users" - params = {"query": email} - response = self.requests_session.get( + def get_user_by_email(self, email: str) -> SingleJson | str: + """Gets user by email. + + Args: + email (str): User email. + + Returns: + SingleJson | str: User dict or "No User Found". + """ + url: str = f"{self.BASE_URL}/users" + params: dict[str, str] = {"query": email} + response: requests.Response = self.requests_session.get( url=url, headers=self._get_auth_headers(), params=params, timeout=10 ) response.raise_for_status() - users = response.json().get("users", []) - # The API returns a list, find the exact email match + users: list[SingleJson] = response.json().get("users", []) + for user in users: if user.get("email") == email: return user return "No User Found" - def get_user_by_ID(self, userID): - headers = { + def get_user_by_ID(self, userID: str) -> SingleJson | str: + """Gets user by ID. + + Args: + userID (str): User ID. + + Returns: + SingleJson | str: User dict or "No User Found". + """ + headers: dict[str, str] = { "Content-Type": "application/json", "Accept": "application/vnd.pagerduty+json;version=2", "Authorization": f"Token token={self.api_key}", } - url = self.BASE_URL + "/users/" + userID + url: str = self.BASE_URL + "/users/" + userID response = self.requests_session.request( "GET", url, headers=headers, timeout=10 ) @@ -155,15 +334,20 @@ def get_user_by_ID(self, userID): return response.json()["user"] return "No User Found" - def list_filtered_incidents(self, params: dict): - base_url = self.BASE_URL + self.INCIDENTS_URI - headers = { - "Content-Type": "application/json", - "Accept": "application/vnd.pagerduty+json;version=2", - "Authorization": f"Token token={self.api_key}", - } + def list_filtered_incidents(self, params: dict[str, Any]) -> list[SingleJson]: + """Lists filtered incidents. + + Args: + params (dict[str, Any]): Filter parameters. + + Returns: + list[SingleJson]: List of incidents. + """ + base_url: str = self.BASE_URL + self.INCIDENTS_URI + headers: dict[str, str] = self._get_auth_headers() + headers["Content-Type"] = "application/json" - query_parts = [] + query_parts: list[str] = [] for key, value in params.items(): encoded_key = quote_plus(key) if isinstance(value, list): @@ -176,11 +360,11 @@ def list_filtered_incidents(self, params: dict): query_string = "&".join(query_parts) - full_url = base_url + full_url: str = base_url if query_string: full_url += f"?{query_string}" - response = self.requests_session.get( + response: requests.Response = self.requests_session.get( full_url, headers=headers, timeout=10, @@ -189,27 +373,52 @@ def list_filtered_incidents(self, params: dict): response.raise_for_status() return response.json().get("incidents") - def snooze_incident(self, email_from, incident_id): + def snooze_incident(self, email_from: str, incident_id: str) -> SingleJson: + """Snoozes an incident. + + Args: + email_from (str): Email address. + incident_id (str): Incident ID. + + Returns: + SingleJson: API response. + """ self.requests_session.headers.update( {"Authorization": f"Token token={self.api_key}", "From": f"{email_from}"}, ) - payload = {"duration": 3600} - url = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" + "/snooze" - response = self.requests_session.post(url=url, json=payload, timeout=10) + payload: dict[str, int] = {"duration": 3600} + url: str = self.BASE_URL + self.INCIDENTS_URI + f"/{incident_id}" + "/snooze" + response: requests.Response = self.requests_session.post( + url=url, json=payload, timeout=10 + ) response.raise_for_status() return response.json() - def run_response_play(self, email, response_plays_id, user_id): - payload = {"incident": {"id": f"{user_id}", "type": "incident_reference"}} + def run_response_play( + self, + email: str, + response_plays_id: str, + user_id: str + ) -> SingleJson: + """Runs a response play. - headers = { - "Content-Type": "application/json", - "Accept": "application/vnd.pagerduty+json;version=2", - "From": f"{email}", - "Authorization": f"Token token={self.api_key}", + Args: + email (str): Email address. + response_plays_id (str): Response play ID. + user_id (str): User ID (used as incident ID in payload). + + Returns: + SingleJson: API response message. + """ + payload: SingleJson = { + "incident": {"id": f"{user_id}", "type": "incident_reference"} } - full_url = self.BASE_URL + "/response_plays/" + response_plays_id + "/run" + headers: dict[str, str] = self._get_auth_headers() + headers["Content-Type"] = "application/json" + headers["From"] = f"{email}" + + full_url: str = self.BASE_URL + "/response_plays/" + response_plays_id + "/run" response = self.requests_session.request( "POST", full_url, @@ -220,9 +429,16 @@ def run_response_play(self, email, response_plays_id, user_id): response.raise_for_status() return {"message": response} - def _get_auth_headers(self): - """Returns a dictionary with standard authentication headers.""" - return { + def _get_auth_headers(self) -> dict[str, str]: + """Returns a dictionary with standard authentication headers. + + Returns: + dict[str, str]: Auth headers. + """ + headers: dict[str, str] = { "Accept": "application/vnd.pagerduty+json;version=2", "Authorization": f"Token token={self.api_key}", } + if self.from_email: + headers["From"] = self.from_email + return headers diff --git a/content/response_integrations/third_party/community/pager_duty/core/constants.py b/content/response_integrations/third_party/community/pager_duty/core/constants.py index 6b0930281..238e90cc2 100644 --- a/content/response_integrations/third_party/community/pager_duty/core/constants.py +++ b/content/response_integrations/third_party/community/pager_duty/core/constants.py @@ -11,3 +11,19 @@ SCRIPT_NAME_RUN_RESPONSE = "_Run_Response_Play" SCRIPT_NAME_PING = "_Ping" SCRIPT_NAME_SNOOZE = "_SnoozeIncident" + +REASON_MALICIOUS: str = "Malicious" +REASON_NOT_MALICIOUS: str = "Not Malicious" +CLASSIFICATION_TRUE_POSITIVE: str = "True Positive" +CLASSIFICATION_FALSE_POSITIVE: str = "False Positive" +CLASSIFICATION_OTHER: str = "Other" +REASON_RESOLVED_IN_PAGERDUTY: str = "Resolved in PagerDuty" +SEVERITY_HIGH: str = "80" +SEVERITY_LOW: str = "40" + +PAGERDUTY_COMMENT_PREFIX: str = "PagerDuty:" +SIEM_COMMENT_PREFIX: str = "SecOps:" +CONTEXT_KEY: str = "TICKET_ID" +ENTITY_TYPE_TICKET: int = 1 +ENTITY_TYPE_ALERT: int = 2 +ALERT_ID_CONTEXT_KEY: str = "Alert_ID" diff --git a/content/response_integrations/third_party/community/pager_duty/definition.yaml b/content/response_integrations/third_party/community/pager_duty/definition.yaml index 45a5c4630..771b7f275 100644 --- a/content/response_integrations/third_party/community/pager_duty/definition.yaml +++ b/content/response_integrations/third_party/community/pager_duty/definition.yaml @@ -13,6 +13,12 @@ parameters: description: '' is_mandatory: true integration_identifier: PagerDuty +- name: Verify SSL + default_value: 'true' + type: boolean + description: 'Whether to verify SSL certificates.' + is_mandatory: false + integration_identifier: PagerDuty categories: [] svg_logo_path: resources/logo.svg image_path: resources/image.png diff --git a/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.py b/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.py new file mode 100644 index 000000000..fff9aa855 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.py @@ -0,0 +1,494 @@ +from __future__ import annotations + +from datetime import datetime +from types import SimpleNamespace +from typing import Any + +from TIPCommon.base.job.base_sync_job import BaseSyncJob +from TIPCommon.base.job.job_case import ( + JobCase, + JobStatusResult, + SyncMetadata, +) +from TIPCommon.data_models import AlertCard +from TIPCommon.transformation import convert_comma_separated_to_list + +from ..core.constants import ( + CONTEXT_KEY, + ENTITY_TYPE_ALERT, + ENTITY_TYPE_TICKET, + PAGERDUTY_COMMENT_PREFIX, + SIEM_COMMENT_PREFIX, +) +from ..core.PagerDutyManager import PagerDutyManager + +MS_IN_SECOND: int = 1000 + + +class SyncIncidents(BaseSyncJob[PagerDutyManager]): + def __init__(self) -> None: + super().__init__( + job_name="PagerDuty Sync Incidents", + context_identifier="Pagerduty Ticket", + tags_identifiers=["Pagerduty Ticket"], + ) + + def _init_api_clients(self) -> PagerDutyManager: + """Initializes the API client. + + Returns: + The initialized API client. + """ + verify_ssl: bool = getattr(self.params, "Verify SSL", True) + from_email: str | None = getattr(self.params, "From Email", None) + return PagerDutyManager( + api_key=self.params.api_key, + verify_ssl=verify_ssl, + from_email=from_email, + ) + + def modified_synced_case_ids_by_product( + self, + alert_ids: list[str], + sorted_modified_ids: list[tuple[str, int]], + ) -> list[tuple[str, int]]: + """Fetches modified incidents from PagerDuty and maps them to SecOps case IDs. + + Args: + alert_ids: list of alert IDs to check for modifications. + sorted_modified_ids: list of tuples containing case IDs and their + last modified timestamps. + + Returns: + A list of tuples containing SecOps case IDs and their last modified times. + """ + if not alert_ids: + return [] + + product_ids_to_case_map: dict[str, str] = {} + for case_id, product_ids in self.processed_items.items(): + for product_id in product_ids: + product_ids_to_case_map[product_id] = case_id + + from_timestamp = getattr(self, "last_run_time", 0) + if not from_timestamp: + return [] + + self.logger.info( + "Checking synced PagerDuty incidents for updates and new notes." + ) + modified_cases = [] + + for incident_id, case_id in product_ids_to_case_map.items(): + is_modified, latest_timestamp = self._is_incident_modified( + incident_id, from_timestamp + ) + if is_modified: + self.logger.info( + f"Found update or new note for incident {incident_id}, " + f"triggering sync for case {case_id}" + ) + modified_cases.append((case_id, latest_timestamp)) + + return modified_cases + + def _get_incident_latest_timestamp(self, incident_id: str) -> int: + """Gets the latest timestamp of the incident itself. + + Returns: + The timestamp in milliseconds, or 0 if failed/not found. + """ + try: + incident = self.api_client.get_incident(incident_id) + updated_at_str = incident.get("updated_at") + if updated_at_str: + dt = datetime.fromisoformat(updated_at_str.replace("Z", "+00:00")) + return int(dt.timestamp() * MS_IN_SECOND) + except Exception as e: + self.logger.debug( + f"Failed to get incident timestamp for {incident_id}: {e}" + ) + return 0 + + def _get_notes_latest_timestamp(self, incident_id: str) -> int: + """Gets the latest timestamp among all incident notes. + + Returns: + The timestamp in milliseconds, or 0 if failed/not found. + """ + latest_timestamp = 0 + try: + notes = self.api_client.get_incident_notes(incident_id) + for note in notes: + created_at_str = note.get("created_at") + if created_at_str: + try: + dt = datetime.fromisoformat( + created_at_str.replace("Z", "+00:00") + ) + created_at_ms = int(dt.timestamp() * MS_IN_SECOND) + latest_timestamp = max(latest_timestamp, created_at_ms) + except Exception as e: + self.logger.debug( + f"Failed to parse created_at for note in " + f"incident {incident_id}: {e}" + ) + continue + except Exception as e: + self.logger.debug(f"Failed to get notes timestamp for {incident_id}: {e}") + return latest_timestamp + + def _is_incident_modified( + self, incident_id: str, from_timestamp: int + ) -> tuple[bool, int]: + """Checks if an incident or its notes have been modified since + from_timestamp. + """ + incident_ts = self._get_incident_latest_timestamp(incident_id) + notes_ts = self._get_notes_latest_timestamp(incident_id) + + latest_timestamp = max(incident_ts, notes_ts) + is_modified = latest_timestamp > from_timestamp + + if latest_timestamp == 0: + latest_timestamp = from_timestamp + + return is_modified, latest_timestamp + + def _extract_product_ids_from_case(self, job_case: JobCase) -> list[str]: + """Extracts product incident IDs from the SecOps case alerts and context. + Args: + job_case: The SecOps case containing the alerts. + Returns: + A list of unique product incident IDs extracted from the case. + """ + incident_ids: list[str] = [] + + context_value: str | None = self.soar_job.get_context_property( + ENTITY_TYPE_TICKET, + str(job_case.case_detail.id_), + CONTEXT_KEY, + ) + if context_value: + ids = convert_comma_separated_to_list(context_value) + incident_ids.extend(ids) + first_alert = job_case.get_first_alert(open_only=False) + if first_alert: + for incident_id in ids: + job_case.product_ids_from_secops_alerts[incident_id] = first_alert + + for alert in job_case.case_detail.alerts: + incident_id: str | None = self._extract_product_id_from_ticket(alert) + if incident_id: + incident_ids.append(incident_id) + job_case.product_ids_from_secops_alerts[incident_id] = alert + + return sorted(set(incident_ids)) + + def _extract_product_id_from_ticket(self, ticket: AlertCard) -> str | None: + """Extracts PagerDuty Incident ID from a single ticket. + Args: + ticket: The ticket to extract ID from. + Returns: + The incident ID if found, None otherwise. + """ + if ticket.ticket_id: + return ticket.ticket_id + + return self.soar_job.get_context_property( + ENTITY_TYPE_ALERT, ticket.alert_group_identifier, CONTEXT_KEY + ) + + def map_product_data_to_case(self, job_case: JobCase) -> None: + """Maps PagerDuty incident data into metadata. + + Args: + job_case: The SecOps case to map data for. + """ + product_ids = self._extract_product_ids_from_case(job_case) + product_details = self._fetch_product_details(product_ids) + self._attach_comments_to_product_details(product_details) + self._attach_product_details_to_case(job_case, product_details) + self._populate_alert_metadata(job_case, product_details) + self.remove_synced_data_from_db(job_case, product_details) + + def _fetch_product_details(self, product_ids: list[str]) -> list[dict[str, Any]]: + """Fetches PagerDuty incident details for given IDs.""" + results = [] + for product_id in product_ids: + try: + incident = self.api_client.get_incident(product_id) + if incident: + results.append(incident) + except Exception as e: + self.logger.error(f"Failed to fetch incident {product_id}: {e}") + return results + + def _attach_comments_to_product_details( + self, product_details: list[dict[str, Any]] + ) -> None: + """Attaches PagerDuty notes as comments to product details.""" + for detail in product_details: + incident_id = detail.get("id") + try: + notes = self.api_client.get_incident_notes(incident_id) + detail["comments"] = [ + SimpleNamespace(message=note.get("content", "")) for note in notes + ] + except Exception as e: + self.logger.error( + f"Failed to fetch notes for incident {incident_id}: {e}" + ) + + def _attach_product_details_to_case( + self, job_case: JobCase, product_details: list[dict[str, Any]] + ) -> None: + """Adds product incident details to the SecOps case.""" + for product_detail in product_details: + job_case.add_product_incident( + SimpleNamespace(**product_detail), product_key="id" + ) + + def _populate_alert_metadata( + self, job_case: JobCase, product_details: list[dict[str, Any]] + ) -> None: + """Populates sync metadata for each SecOps alert.""" + product_map = {pd.get("id"): pd for pd in product_details} + case_id = str(job_case.case_detail.id_) + mapped_ids = self.processed_items.get(case_id, []) + for alert in job_case.case_detail.alerts: + product_id = self._extract_product_id_from_ticket(alert) + matching_product = None + if product_id: + matching_product = product_map.get(product_id) + else: + for pid in mapped_ids: + if pid in product_map: + matching_product = product_map[pid] + break + + if matching_product: + comments = matching_product.get("comments", []) + closure_reason = None + if comments: + closure_reason = getattr(comments[-1], "message", "") + + job_case.alert_metadata[alert.identifier] = SyncMetadata( + status=matching_product.get("status"), + incident_number=matching_product.get("id"), + closure_reason=closure_reason, + ) + + def sync_status(self, job_case: JobCase) -> None: + """Syncs closure status between SecOps case alerts and PagerDuty incidents.""" + res = job_case.get_status_to_sync(product_closed_status="resolved") + self.sync_product_status_to_case(res, job_case) + self._sync_case_status_to_product(res, job_case) + + def sync_product_status_to_case( + self, res: JobStatusResult, job_case: JobCase + ) -> None: + """Syncs closures from PagerDuty to SecOps case alerts.""" + for alert, meta in res.alerts_to_close_in_soar: + reason = "Maintenance" + root_cause = "Other" + open_alerts = [ + a + for a in job_case.case_detail.alerts + if a.status.lower() not in ["close", "closed"] + ] + comment = meta.closure_reason or "Ticket was closed" + if len(open_alerts) <= 1: + try: + self.soar_job.close_case( + root_cause=root_cause, + comment=comment, + reason=reason, + case_id=job_case.case_detail.id_, + alert_identifier=alert.identifier, + ) + self.logger.info( + f"Successfully closed case {job_case.case_detail.id_} " + f"because alert {alert.identifier} was the last open alert " + f"and PagerDuty incident {meta.incident_number} was resolved." + ) + except Exception as e: + self.logger.error( + f"Failed to close case {job_case.case_detail.id_}: {e}" + ) + else: + try: + self.soar_job.close_alert( + root_cause=root_cause, + comment=comment, + reason=reason, + case_id=job_case.case_detail.id_, + alert_id=alert.identifier, + ) + self.logger.info( + f"Successfully closed alert {alert.identifier} in case " + f"{job_case.case_detail.id_} because PagerDuty incident " + f"{meta.incident_number} was resolved." + ) + except Exception as e: + self.logger.error(f"Failed to close alert {alert.identifier}: {e}") + + self._remove_synced_entries( + synced_list=[(job_case.case_detail.id_, f"{meta.incident_number}")], + ) + + def _sync_case_status_to_product( + self, res: JobStatusResult, job_case: JobCase + ) -> None: + """Syncs case closure status from SecOps to PagerDuty.""" + if not res.incidents_to_close_in_product: + return + + for req in res.incidents_to_close_in_product: + meta = req.get("meta") + if not meta or not meta.incident_number: + continue + + try: + closing_comment = self.get_secops_closure_comment(job_case, req) + + if closing_comment: + self.api_client.add_incident_note( + meta.incident_number, closing_comment + ) + self.api_client.resolve_incident(meta.incident_number) + self.logger.info( + f"Successfully resolved PagerDuty incident {meta.incident_number}." + ) + self._remove_synced_entries( + synced_list=[ + (job_case.case_detail.id_, f"{meta.incident_number}") + ], + ) + except Exception as e: + self.logger.error( + f"Failed to resolve PagerDuty incident " + f"{meta.incident_number}: {e}" + ) + + def sync_comments(self, job_case: JobCase) -> None: + """Syncs comments between SecOps and PagerDuty.""" + if job_case.case_detail.status == "Closed": + return + + comments_to_sync = self.get_comments_to_sync( + job_case, + product_comment_prefix=PAGERDUTY_COMMENT_PREFIX, + case_comment_prefix=SIEM_COMMENT_PREFIX, + product_comment_key="message", + product_incident_key="id", + ) + + self.sync_product_comments_to_case( + case_id=job_case.case_detail.id_, + comments=comments_to_sync.product_comments_sync_to_case, + ) + if comments_to_sync.product_comments_sync_to_case: + count = len(comments_to_sync.product_comments_sync_to_case) + self.logger.info( + f"Successfully synced {count} " + f"comments from PagerDuty to SecOps case {job_case.case_detail.id_}." + ) + + self.sync_case_comments_to_product( + job_case=job_case, + comments=comments_to_sync.case_comments_sync_to_product, + ) + + def sync_case_comments_to_product( + self, job_case: JobCase, comments: list[str] + ) -> None: + """Syncs comments from SecOps case to PagerDuty incidents.""" + incident_ids = self._extract_product_ids_from_case(job_case) + for incident_id in incident_ids: + for comment in comments: + try: + self.api_client.add_incident_note( + incident_id, f"{SIEM_COMMENT_PREFIX} {comment}" + ) + self.logger.info( + f"Successfully added comment to PagerDuty incident " + f"{incident_id}." + ) + except Exception as e: + self.logger.error( + f"Failed to add note to PagerDuty incident {incident_id}: {e}" + ) + + def is_alert_and_product_closed( + self, + job_case: JobCase, + product: dict[str, Any], + ) -> bool: + """Checks if both the SecOps alert and the corresponding PagerDuty + incident are closed. + """ + product_id = product.get("id") + + alert = next( + ( + alert + for alert in job_case.case_detail.alerts + if self._extract_product_id_from_ticket(alert) == product_id + ), + None, + ) + + if not alert: + return False + + alert_status = getattr(alert, "status", "") + alert_closed = False + if isinstance(alert_status, str): + alert_closed = alert_status.lower() in ["close", "closed"] + else: + alert_closed = str(alert_status).lower() in ["close", "closed"] + + product_closed = product.get("status") == "resolved" + + return alert_closed and product_closed + + def remove_synced_data_from_db( + self, job_case: JobCase, product_details: list[dict[str, Any]] + ) -> None: + """Removes entries from synchronization tracking when both alert and + product are closed. + """ + for alert in job_case.case_detail.alerts: + alert_id = self._extract_product_id_from_ticket(alert) + if not alert_id: + continue + + matching_product = None + for product in product_details: + if product.get("id") == alert_id: + matching_product = product + break + + if not matching_product: + continue + + if self.is_alert_and_product_closed(job_case, matching_product): + self._remove_synced_entries([(job_case.case_detail.id_, alert_id)]) + + def sync_assignee(self, job_case: JobCase) -> None: + pass + + def sync_severity(self, job_case: JobCase) -> None: + pass + + def sync_tags(self, job_case: JobCase) -> None: + pass + + +def main() -> None: + SyncIncidents().start() + + +if __name__ == "__main__": + main() diff --git a/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.yaml b/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.yaml new file mode 100644 index 000000000..9f9d5226e --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/jobs/SyncIncidents.yaml @@ -0,0 +1,31 @@ +name: Sync Incidents +parameters: + - name: api_key + type: password + description: The API key for the PagerDuty instance. + is_mandatory: true + - name: Max Hours Backwards + type: integer + default_value: 24 + description: The number of hours prior to the current time to synchronize incidents during the first job iteration. + is_mandatory: true + - name: Verify SSL + type: boolean + default_value: true + description: If selected, the integration validates the SSL certificate when connecting to the PagerDuty server. + is_mandatory: true + - name: From Email + type: string + description: The email address associated with the PagerDuty account. This parameter is required to synchronize notes and status changes. + is_mandatory: true + - name: Environment Name + type: string + default_value: Default Environment + description: The name of the environment from which to synchronize cases. + is_mandatory: true + + +description: | + This job synchronizes Google SecOps alerts and PagerDuty Tickets. It ensures that comments and statuses sync bi-directionally between both systems. Note: This job requires the 'Pagerduty Ticket' tag on the SecOps case. If the ticket wasn’t created using the PagerDuty connector, you must add a 'TICKET_ID' context value (or 'TICKET_ID' at the Case level for playbook-generated cases). To prevent synchronization loops, all mirrored comments are prefixed with 'SecOps:' or 'PagerDuty:' +integration: PagerDuty +creator: Admin diff --git a/content/response_integrations/third_party/community/pager_duty/pyproject.toml b/content/response_integrations/third_party/community/pager_duty/pyproject.toml index 9fd45d849..d47e6bc14 100644 --- a/content/response_integrations/third_party/community/pager_duty/pyproject.toml +++ b/content/response_integrations/third_party/community/pager_duty/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "PagerDuty" -version = "21.0" +version = "22.0" description = "PagerDuty helps developers, ITOps, DevOps and teams across the business provide a perfect digital experience to their customers, every time." requires-python = ">=3.11,<3.12" dependencies = [ "pdpyras==5.4.0", - "requests==2.32.3", + "requests==2.32.5", "tipcommon", "environmentcommon", ] @@ -20,8 +20,8 @@ dev = [ [tool.uv.sources] soar-sdk = { git = "https://github.com/chronicle/soar-sdk.git" } -integration-testing = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.7-py3-none-any.whl" } -tipcommon = { path = "../../../../../packages/tipcommon/whls/TIPCommon-2.2.7-py2.py3-none-any.whl" } +integration-testing = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl" } +tipcommon = { path = "../../../../../packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl" } environmentcommon = { path = "../../../../../packages/envcommon/whls/EnvironmentCommon-1.0.2-py2.py3-none-any.whl" } [[tool.uv.index]] diff --git a/content/response_integrations/third_party/community/pager_duty/release_notes.yaml b/content/response_integrations/third_party/community/pager_duty/release_notes.yaml index 36360660c..84d32cc3e 100644 --- a/content/response_integrations/third_party/community/pager_duty/release_notes.yaml +++ b/content/response_integrations/third_party/community/pager_duty/release_notes.yaml @@ -82,3 +82,9 @@ item_type: Integration publish_time: '2026-03-18' ticket_number: '' +- description: SyncIncidents - New Job added. + integration_version: 22.0 + item_name: SyncIncidents + item_type: Job + publish_time: '2026-04-14' + ticket_number: '475592230' diff --git a/content/response_integrations/third_party/community/pager_duty/resources/FilteredIncidentList_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/FilteredIncidentList_JsonResult_example.json new file mode 100644 index 000000000..9a74b2b18 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/FilteredIncidentList_JsonResult_example.json @@ -0,0 +1,10 @@ +[ + { + "id": "Q1C5WQ0U8TSCXE", + "incident_number": 1, + "title": "TEST TEST server under attack", + "status": "triggered", + "created_at": "2023-04-03T09:33:53Z", + "urgency": "high" + } +] diff --git a/content/response_integrations/third_party/community/pager_duty/resources/GetUserByEmail_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/GetUserByEmail_JsonResult_example.json new file mode 100644 index 000000000..f067de95e --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/GetUserByEmail_JsonResult_example.json @@ -0,0 +1,7 @@ +{ + "id": "PCRYIVB", + "name": "Gal Pk", + "email": "gal@example.com", + "role": "admin", + "type": "user" +} diff --git a/content/response_integrations/third_party/community/pager_duty/resources/GetUserById_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/GetUserById_JsonResult_example.json new file mode 100644 index 000000000..f067de95e --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/GetUserById_JsonResult_example.json @@ -0,0 +1,7 @@ +{ + "id": "PCRYIVB", + "name": "Gal Pk", + "email": "gal@example.com", + "role": "admin", + "type": "user" +} diff --git a/content/response_integrations/third_party/community/pager_duty/resources/ListAllOncall_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/ListAllOncall_JsonResult_example.json new file mode 100644 index 000000000..60e9cdf21 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/ListAllOncall_JsonResult_example.json @@ -0,0 +1,16 @@ +[ + { + "user": { + "id": "PCRYIVB", + "summary": "Gal Pk" + }, + "schedule": { + "id": "P12345", + "summary": "Daily Schedule" + }, + "escalation_policy": { + "id": "PMTZZA8", + "summary": "Community OnCall-ep" + } + } +] diff --git a/content/response_integrations/third_party/community/pager_duty/resources/ListIncidents_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/ListIncidents_JsonResult_example.json new file mode 100644 index 000000000..9a74b2b18 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/ListIncidents_JsonResult_example.json @@ -0,0 +1,10 @@ +[ + { + "id": "Q1C5WQ0U8TSCXE", + "incident_number": 1, + "title": "TEST TEST server under attack", + "status": "triggered", + "created_at": "2023-04-03T09:33:53Z", + "urgency": "high" + } +] diff --git a/content/response_integrations/third_party/community/pager_duty/resources/ListUsers_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/ListUsers_JsonResult_example.json new file mode 100644 index 000000000..d0d857421 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/ListUsers_JsonResult_example.json @@ -0,0 +1,9 @@ +[ + { + "id": "PCRYIVB", + "name": "Gal Pk", + "email": "gal@example.com", + "role": "admin", + "type": "user" + } +] diff --git a/content/response_integrations/third_party/community/pager_duty/resources/RunResponsePlay_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/RunResponsePlay_JsonResult_example.json new file mode 100644 index 000000000..29df4c88e --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/RunResponsePlay_JsonResult_example.json @@ -0,0 +1,4 @@ +{ + "status": "success", + "message": "Response play triggered" +} diff --git a/content/response_integrations/third_party/community/pager_duty/resources/SnoozeIncident_JsonResult_example.json b/content/response_integrations/third_party/community/pager_duty/resources/SnoozeIncident_JsonResult_example.json new file mode 100644 index 000000000..9d8aa9676 --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/resources/SnoozeIncident_JsonResult_example.json @@ -0,0 +1,4 @@ +{ + "status": "success", + "message": "Incident snoozed" +} diff --git a/content/response_integrations/third_party/community/pager_duty/tests/conftest.py b/content/response_integrations/third_party/community/pager_duty/tests/conftest.py index 3c97ff3f5..f30d802e3 100644 --- a/content/response_integrations/third_party/community/pager_duty/tests/conftest.py +++ b/content/response_integrations/third_party/community/pager_duty/tests/conftest.py @@ -1,7 +1,14 @@ from __future__ import annotations +import io +import sys +from unittest.mock import MagicMock + import pytest from integration_testing.common import use_live_api +from TIPCommon.base.job.job_case import JobCase, SyncMetadata + +from pager_duty.jobs.SyncIncidents import SyncIncidents from .core.product import PagerDuty from .core.session import PagerDutySession @@ -11,6 +18,7 @@ @pytest.fixture def pagerduty() -> PagerDuty: + """Fixture providing a PagerDuty mock container.""" return PagerDuty() @@ -19,10 +27,161 @@ def script_session( monkeypatch: pytest.MonkeyPatch, pagerduty: PagerDuty, ) -> PagerDutySession: - """Mock PagerDuty scripts' session and get back an object to view request history""" + """Fixture to mock PagerDuty scripts' session and view request history.""" session: PagerDutySession = PagerDutySession(pagerduty) if not use_live_api(): monkeypatch.setattr("requests.Session", lambda: session) return session + + +@pytest.fixture +def mock_job_env(): + """Fixture to mock sys.stdin for job execution.""" + + class MockStdin: + def __init__(self) -> None: + self.buffer: io.BytesIO = io.BytesIO(b'{"parameters": {}}') + + original_stdin = sys.stdin + sys.stdin = MockStdin() + yield + sys.stdin = original_stdin + + +@pytest.fixture +def job(mock_job_env): + """Fixture providing a SyncIncidents job instance with mocked properties.""" + original_params = SyncIncidents.params + original_api_client = SyncIncidents.api_client + original_logger = SyncIncidents.logger + original_soar_job = SyncIncidents.soar_job + + SyncIncidents.params = property(lambda self: MagicMock(api_key="test_key")) + mock_logger = MagicMock() + + SyncIncidents.logger = property(lambda self: mock_logger) + mock_soar_job = MagicMock() + + SyncIncidents.soar_job = property(lambda self: mock_soar_job) + job_instance = SyncIncidents() + + client = job_instance._init_api_clients() + SyncIncidents.api_client = property(lambda self: client) + job_instance._remove_synced_entries = MagicMock() + + yield job_instance + SyncIncidents.params = original_params + SyncIncidents.api_client = original_api_client + SyncIncidents.logger = original_logger + SyncIncidents.soar_job = original_soar_job + + +@pytest.fixture +def job_failing_api(mock_job_env): + """Fixture providing a SyncIncidents job instance with a failing API client.""" + original_params = SyncIncidents.params + original_api_client = SyncIncidents.api_client + original_logger = SyncIncidents.logger + original_soar_job = SyncIncidents.soar_job + + SyncIncidents.params = property(lambda self: MagicMock(api_key="test_key")) + mock_logger = MagicMock() + SyncIncidents.logger = property(lambda self: mock_logger) + mock_soar_job = MagicMock() + SyncIncidents.soar_job = property(lambda self: mock_soar_job) + + job_instance = SyncIncidents() + client = job_instance._init_api_clients() + client.resolve_incident = MagicMock(side_effect=Exception("API Failure")) + SyncIncidents.api_client = property(lambda self: client) + job_instance._remove_synced_entries = MagicMock() + + yield job_instance + + SyncIncidents.params = original_params + SyncIncidents.api_client = original_api_client + SyncIncidents.logger = original_logger + SyncIncidents.soar_job = original_soar_job + + +@pytest.fixture +def job_case_map(): + """Fixture providing a JobCase mock for mapping tests.""" + job_case = MagicMock(spec=JobCase) + alert = MagicMock() + alert.identifier = "alert_1" + alert.ticket_id = "P123" + job_case.case_detail.alerts = [alert] + job_case.alert_metadata = {} + return job_case + + +@pytest.fixture +def job_case_sync(): + """Fixture providing a JobCase mock for syncing status (SOAR to PagerDuty).""" + job_case = MagicMock(spec=JobCase) + res = MagicMock() + + alert = MagicMock() + alert.status = "close" + alert.closure_details = {"reason": "Malicious"} + + meta = SyncMetadata(status="triggered", incident_number="P123", closure_reason=None) + + res.incidents_to_close_in_product = [{"meta": meta, "alert": alert}] + res.alerts_to_close_in_soar = [] + + job_case.get_status_to_sync.return_value = res + job_case.case_detail.id_ = 1 + + return job_case + + +@pytest.fixture +def job_case_sync_close_case(): + """Fixture providing a JobCase mock for syncing status (PagerDuty to SOAR, close case).""" + job_case = MagicMock(spec=JobCase) + res = MagicMock() + + alert = MagicMock() + alert.identifier = "alert_1" + alert.status = "open" + + meta = SyncMetadata(status="resolved", incident_number="P123", closure_reason=None) + + res.incidents_to_close_in_product = [] + res.alerts_to_close_in_soar = [(alert, meta)] + + job_case.get_status_to_sync.return_value = res + job_case.case_detail.id_ = 1 + job_case.case_detail.alerts = [alert] + + return job_case + + +@pytest.fixture +def job_case_sync_close_alert(): + """Fixture providing a JobCase mock for syncing status (PagerDuty to SOAR, close alert only).""" + job_case = MagicMock(spec=JobCase) + res = MagicMock() + + alert1 = MagicMock() + alert1.identifier = "alert_1" + alert1.status = "open" + + alert2 = MagicMock() + alert2.identifier = "alert_2" + alert2.status = "open" + + meta = SyncMetadata(status="resolved", incident_number="P123", closure_reason=None) + + res.incidents_to_close_in_product = [] + res.alerts_to_close_in_soar = [(alert1, meta)] + + job_case.get_status_to_sync.return_value = res + job_case.case_detail.id_ = 1 + job_case.case_detail.alerts = [alert1, alert2] + + return job_case diff --git a/content/response_integrations/third_party/community/pager_duty/tests/core/product.py b/content/response_integrations/third_party/community/pager_duty/tests/core/product.py index e15d1ce5b..fbd4021a9 100644 --- a/content/response_integrations/third_party/community/pager_duty/tests/core/product.py +++ b/content/response_integrations/third_party/community/pager_duty/tests/core/product.py @@ -7,15 +7,13 @@ @dataclasses.dataclass(slots=True) class PagerDuty: + """Mock data container for PagerDuty integration tests.""" incidents: SingleJson = dataclasses.field(default_factory=dict) users: SingleJson = dataclasses.field(default_factory=dict) snoozed_incidents: dict[str, SingleJson] = dataclasses.field(default_factory=dict) def get_incidents(self, params: SingleJson) -> SingleJson: - """ - Returns incidents, filtering them based on provided parameters to - simulate the real API behavior. - """ + """Get incidents, optionally filtered by incident_key.""" if not self.incidents.get("incidents"): return {"incidents": []} @@ -30,6 +28,7 @@ def get_incidents(self, params: SingleJson) -> SingleJson: return self.incidents def snooze_incident(self, incident_id: str) -> SingleJson: + """Simulate snoozing an incident.""" self.snoozed_incidents[incident_id] = { "status": "snoozed", "snooze_until": "2025-10-06T14:05:03Z" } @@ -38,11 +37,37 @@ def snooze_incident(self, incident_id: str) -> SingleJson: "incident": {"id": incident_id, "status": "snoozed", "snooze_until": until} } + def get_incident(self, incident_id: str) -> SingleJson: + """Get a specific incident by ID.""" + if not self.incidents.get("incidents"): + return {} + for inc in self.incidents["incidents"]: + if inc.get("id") == incident_id: + return inc + return {} + + def resolve_incident(self, incident_id: str) -> SingleJson: + """Resolve a specific incident by ID.""" + if not self.incidents.get("incidents"): + return {} + for inc in self.incidents["incidents"]: + if inc.get("id") == incident_id: + inc["status"] = "resolved" + return {"incident": inc} + return {} + + def add_incident_note(self, incident_id: str, content: str) -> SingleJson: + """Simulate adding a note to an incident.""" + return {"note": {"content": content}} + def set_incidents(self, mock_incidents: SingleJson) -> None: + """Set the mock incidents data.""" self.incidents = mock_incidents def get_users(self) -> SingleJson: + """Get all mock users.""" return self.users def set_users(self, mock_users: SingleJson) -> None: + """Set the mock users data.""" self.users = mock_users diff --git a/content/response_integrations/third_party/community/pager_duty/tests/core/session.py b/content/response_integrations/third_party/community/pager_duty/tests/core/session.py index 183e7238a..37834be1c 100644 --- a/content/response_integrations/third_party/community/pager_duty/tests/core/session.py +++ b/content/response_integrations/third_party/community/pager_duty/tests/core/session.py @@ -13,17 +13,24 @@ class PagerDutySession(MockSession[MockRequest, MockResponse, PagerDuty]): + """Mock session for PagerDuty integration tests, handling routing.""" + def get_routed_functions(self) -> Iterable[RouteFunction[Response]]: + """Get the list of routed functions.""" return [ self.list_incidents, self.test_connectivity, self.get_incident_by_key, self.list_users, self.snooze_incident, + self.get_incident_by_id, + self.resolve_incident_by_id, + self.add_incident_note, ] @router.get(r"/incidents") def list_incidents(self, request: MockRequest) -> MockResponse: + """Mock for listing incidents.""" auth_header = request.headers.get("Authorization", "") if "invalid_key" in auth_header: return MockResponse( @@ -96,3 +103,30 @@ def snooze_incident(self, request: MockRequest) -> MockResponse: ) return MockResponse(content=self._product.snooze_incident(incident_id)) + + @router.get(r"/incidents/(?P[^/]+)") + def get_incident_by_id(self, request: MockRequest) -> MockResponse: + """Mock for getting a single incident by ID.""" + incident_id = request.url.path.split("/")[-1] + incident = self._product.get_incident(incident_id) + if incident: + return MockResponse(content={"incident": incident}) + return MockResponse(status_code=404, content={"error": {"message": "Incident not found"}}) + + @router.put(r"/incidents/(?P[^/]+)") + def resolve_incident_by_id(self, request: MockRequest) -> MockResponse: + """Mock for resolving an incident.""" + incident_id = request.url.path.split("/")[-1] + res = self._product.resolve_incident(incident_id) + if res: + return MockResponse(content=res) + return MockResponse(status_code=404, content={"error": {"message": "Incident not found"}}) + + @router.post(r"/incidents/(?P[^/]+)/notes") + def add_incident_note(self, request: MockRequest) -> MockResponse: + """Mock for adding a note to an incident.""" + incident_id = request.url.path.split("/")[-2] + payload = get_request_payload(request) + content = payload.get("note", {}).get("content", "") + res = self._product.add_incident_note(incident_id, content) + return MockResponse(content=res) diff --git a/content/response_integrations/third_party/community/pager_duty/tests/test_jobs/test_sync_incidents.py b/content/response_integrations/third_party/community/pager_duty/tests/test_jobs/test_sync_incidents.py new file mode 100644 index 000000000..6f173f96c --- /dev/null +++ b/content/response_integrations/third_party/community/pager_duty/tests/test_jobs/test_sync_incidents.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from pager_duty.tests.core.product import PagerDuty +from pager_duty.tests.core.session import PagerDutySession + + +def test_map_product_data_to_case_success( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job, + job_case_map, +) -> None: + """Tests mapping product data to case successfully.""" + pagerduty.set_incidents({ + "incidents": [{"id": "P123", "status": "resolved", "incident_key": "key1"}] + }) + + job.map_product_data_to_case(job_case_map) + + assert len(script_session.request_history) == 1 + assert script_session.request_history[0].request.url.path.endswith("/incidents/P123") + assert "alert_1" in job_case_map.alert_metadata + assert job_case_map.alert_metadata["alert_1"].status == "resolved" + + +def test_map_product_data_to_case_not_found_success( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job, + job_case_map, +) -> None: + """Tests mapping product data to case when incident is not found.""" + job.map_product_data_to_case(job_case_map) + + assert len(script_session.request_history) == 1 + assert script_session.request_history[0].request.url.path.endswith("/incidents/P123") + assert "alert_1" not in job_case_map.alert_metadata + + +def test_sync_status_soar_to_pagerduty_success( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job, + job_case_sync, +) -> None: + """Tests syncing status from SOAR to PagerDuty successfully.""" + pagerduty.set_incidents({ + "incidents": [{"id": "P123", "status": "triggered", "incident_key": "key1"}] + }) + + job.sync_status(job_case_sync) + + assert len(script_session.request_history) == 2 + assert script_session.request_history[0].request.url.path.endswith("/incidents/P123/notes") + assert script_session.request_history[1].request.url.path.endswith("/incidents/P123") + + incident = pagerduty.get_incident("P123") + assert incident["status"] == "resolved" + + +def test_sync_status_pagerduty_to_soar_close_case_success( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job, + job_case_sync_close_case, +) -> None: + """Tests syncing status from PagerDuty to SOAR resulting in closing the case.""" + job.sync_status(job_case_sync_close_case) + + assert job.soar_job.close_case.called + + +def test_sync_status_pagerduty_to_soar_close_alert_success( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job, + job_case_sync_close_alert, +) -> None: + """Tests syncing status from PagerDuty to SOAR resulting in closing only the alert.""" + job.sync_status(job_case_sync_close_alert) + + assert job.soar_job.close_alert.called + assert not job.soar_job.close_case.called + + +def test_sync_status_soar_to_pagerduty_failure( + script_session: PagerDutySession, + pagerduty: PagerDuty, + job_failing_api, + job_case_sync, +) -> None: + """Tests syncing status from SOAR to PagerDuty with API failure handling.""" + pagerduty.set_incidents({ + "incidents": [{"id": "P123", "status": "triggered", "incident_key": "key1"}] + }) + + job_failing_api.sync_status(job_case_sync) + + assert len(script_session.request_history) == 1 + assert script_session.request_history[0].request.url.path.endswith("/incidents/P123/notes") + assert job_failing_api.logger.error.called diff --git a/content/response_integrations/third_party/community/pager_duty/uv.lock b/content/response_integrations/third_party/community/pager_duty/uv.lock index 91d894a60..45bbf6096 100644 --- a/content/response_integrations/third_party/community/pager_duty/uv.lock +++ b/content/response_integrations/third_party/community/pager_duty/uv.lock @@ -93,15 +93,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] -[[package]] -name = "cachetools" -version = "5.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, -] - [[package]] name = "certifi" version = "2025.6.15" @@ -113,25 +104,26 @@ wheels = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, ] [[package]] @@ -176,43 +168,47 @@ wheels = [ [[package]] name = "cryptography" -version = "45.0.4" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" }, - { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, - { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, - { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, - { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, - { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" }, - { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" }, - { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, - { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, - { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, - { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, - { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, - { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, - { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ba/cf442ae99ef363855ed84b39e0fb3c106ac66b7a7703f3c9c9cfe05412cb/cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c", size = 3590512, upload-time = "2025-06-10T00:03:36.982Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" }, - { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" }, - { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" }, - { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" }, - { url = "https://files.pythonhosted.org/packages/99/49/0ab9774f64555a1b50102757811508f5ace451cf5dc0a2d074a4b9deca6a/cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d", size = 3337594, upload-time = "2025-06-10T00:03:45.523Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -267,7 +263,7 @@ wheels = [ [[package]] name = "google-api-python-client" -version = "2.174.0" +version = "2.188.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -276,36 +272,35 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/fd/860fef0cf3edbad828e2ab4d2ddee5dfe8e595b6da748ac6c77e95bc7bef/google_api_python_client-2.174.0.tar.gz", hash = "sha256:9eb7616a820b38a9c12c5486f9b9055385c7feb18b20cbafc5c5a688b14f3515", size = 13127872, upload-time = "2025-06-25T19:27:12.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/d7/14613c7efbab5b428b400961f5dbac46ad9e019c44e1f3fd14d67c33111c/google_api_python_client-2.188.0.tar.gz", hash = "sha256:5c469db6614f071009e3e5bb8b6aeeccae3beb3647fa9c6cd97f0d551edde0b6", size = 14302906, upload-time = "2026-01-13T22:15:13.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/2d/4250b81e8f5309b58650660f403584db6f64067acac74475893a8f33348d/google_api_python_client-2.174.0-py3-none-any.whl", hash = "sha256:f695205ceec97bfaa1590a14282559c4109326c473b07352233a3584cdbf4b89", size = 13650466, upload-time = "2025-06-25T19:27:10.426Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/a99a7d79d7a37a67cb8008f1d7dcedc46d29c6df5063aeb446112afd4aa4/google_api_python_client-2.188.0-py3-none-any.whl", hash = "sha256:3cad1b68f9d48b82b93d77927e8370a6f43f33d97848242601f14a93a1c70ef5", size = 14870005, upload-time = "2026-01-13T22:15:11.345Z" }, ] [[package]] name = "google-auth" -version = "2.40.3" +version = "2.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, ] [[package]] name = "google-auth-httplib2" -version = "0.2.0" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, ] [[package]] @@ -389,8 +384,8 @@ wheels = [ [[package]] name = "integration-testing" -version = "2.2.7" -source = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.7-py3-none-any.whl" } +version = "2.3.6" +source = { path = "../../../../../packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl" } dependencies = [ { name = "aiohttp" }, { name = "environmentcommon" }, @@ -401,7 +396,7 @@ dependencies = [ { name = "yarl" }, ] wheels = [ - { filename = "integration_testing-2.2.7-py3-none-any.whl", hash = "sha256:50d8f71e80162f4b312c1048d775fc4247cae15933b2d6954b5b941ed5555b53" }, + { filename = "integration_testing-2.3.6-py3-none-any.whl", hash = "sha256:341a902ca71a227fa242cf843ff5a17a029437438647172b56e7acbc8756ca03" }, ] [package.metadata] @@ -453,7 +448,7 @@ wheels = [ [[package]] name = "pagerduty" -version = "21.0" +version = "22.0" source = { virtual = "." } dependencies = [ { name = "environmentcommon" }, @@ -474,13 +469,13 @@ dev = [ requires-dist = [ { name = "environmentcommon", path = "../../../../../packages/envcommon/whls/EnvironmentCommon-1.0.2-py2.py3-none-any.whl" }, { name = "pdpyras", specifier = "==5.4.0" }, - { name = "requests", specifier = "==2.32.3" }, - { name = "tipcommon", path = "../../../../../packages/tipcommon/whls/TIPCommon-2.2.7-py2.py3-none-any.whl" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "tipcommon", path = "../../../../../packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl" }, ] [package.metadata.requires-dev] dev = [ - { name = "integration-testing", path = "../../../../../packages/integration_testing_whls/integration_testing-2.2.7-py3-none-any.whl" }, + { name = "integration-testing", path = "../../../../../packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-json-report", specifier = ">=1.5.0" }, { name = "soar-sdk", git = "https://github.com/chronicle/soar-sdk.git" }, @@ -619,15 +614,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, ] [[package]] @@ -720,7 +715,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -728,9 +723,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -794,28 +789,32 @@ dependencies = [ [[package]] name = "tipcommon" -version = "2.2.7" -source = { path = "../../../../../packages/tipcommon/whls/TIPCommon-2.2.7-py2.py3-none-any.whl" } +version = "2.3.6" +source = { path = "../../../../../packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl" } dependencies = [ { name = "google-api-python-client" }, { name = "google-auth" }, { name = "google-auth-httplib2" }, { name = "httpx" }, { name = "pycryptodome" }, + { name = "pyopenssl" }, { name = "requests" }, + { name = "requests-toolbelt" }, ] wheels = [ - { filename = "tipcommon-2.2.7-py2.py3-none-any.whl", hash = "sha256:e814f0ffa0b177aac36b49aee479bbafdce756bed5eedf9955f4d3d0d5e7fa85" }, + { filename = "tipcommon-2.3.6-py3-none-any.whl", hash = "sha256:069de7378d9be267462e093c16b53a1cfac6333f4be2a3aa86eae9f504809ad9" }, ] [package.metadata] requires-dist = [ - { name = "google-api-python-client" }, - { name = "google-auth" }, - { name = "google-auth-httplib2" }, - { name = "httpx" }, - { name = "pycryptodome" }, - { name = "requests" }, + { name = "google-api-python-client", specifier = "==2.188.0" }, + { name = "google-auth", specifier = "==2.47.0" }, + { name = "google-auth-httplib2", specifier = "==0.3.0" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "pycryptodome", specifier = "==3.23.0" }, + { name = "pyopenssl", specifier = "==25.3.0" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "requests-toolbelt", specifier = "==1.0.0" }, ] [[package]] diff --git a/packages/integration_testing/pyproject.toml b/packages/integration_testing/pyproject.toml index 19be97204..31c106a66 100644 --- a/packages/integration_testing/pyproject.toml +++ b/packages/integration_testing/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "integration-testing" -version = "2.3.5" +version = "2.3.6" description = "Blackbox testing infrastructure to test and run marketplace integration scripts locally using mocks." readme = "README.md" authors = [ diff --git a/packages/integration_testing/src/integration_testing/common.py b/packages/integration_testing/src/integration_testing/common.py index bf36855e3..a87e33ee4 100644 --- a/packages/integration_testing/src/integration_testing/common.py +++ b/packages/integration_testing/src/integration_testing/common.py @@ -23,8 +23,8 @@ from typing import TYPE_CHECKING import yaml -from OverflowManager import OverflowManager, OverflowManagerSettings from SiemplifyDataModel import DomainEntityInfo +from soar_sdk.OverflowManager import OverflowManager, OverflowManagerSettings from TIPCommon.base.action import CaseComment, EntityTypesEnum from TIPCommon.base.job import JobParameter from TIPCommon.consts import NUM_OF_MILLI_IN_SEC diff --git a/packages/integration_testing/uv.lock b/packages/integration_testing/uv.lock index 4489834f7..3c47e852d 100644 --- a/packages/integration_testing/uv.lock +++ b/packages/integration_testing/uv.lock @@ -395,7 +395,7 @@ wheels = [ [[package]] name = "integration-testing" -version = "2.3.5" +version = "2.3.6" source = { editable = "." } dependencies = [ { name = "aiohttp" }, diff --git a/packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl b/packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl new file mode 100644 index 000000000..7112977cb Binary files /dev/null and b/packages/integration_testing_whls/integration_testing-2.3.6-py3-none-any.whl differ diff --git a/packages/tipcommon/TIPCommon/pyproject.toml b/packages/tipcommon/TIPCommon/pyproject.toml index 043e8baeb..a08fc1adb 100644 --- a/packages/tipcommon/TIPCommon/pyproject.toml +++ b/packages/tipcommon/TIPCommon/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "TIPCommon" -version = "2.3.5" +version = "2.3.6" description = "General Purpose CLI tool for Google SecOps Marketplace" readme = "README.md" authors = [ diff --git a/packages/tipcommon/TIPCommon/src/TIPCommon/base/job/base_sync_job.py b/packages/tipcommon/TIPCommon/src/TIPCommon/base/job/base_sync_job.py index aec033c0e..eb8731102 100644 --- a/packages/tipcommon/TIPCommon/src/TIPCommon/base/job/base_sync_job.py +++ b/packages/tipcommon/TIPCommon/src/TIPCommon/base/job/base_sync_job.py @@ -103,23 +103,21 @@ def sync_status(self, job_case: JobCase) -> None: """ raise NotImplementedError - @abstractmethod def sync_comments(self, job_case: JobCase) -> None: """Sync comments between source and target items. Args: job_case (JobCase): The JobCase object containing the details of the case to sync. """ - raise NotImplementedError + pass - @abstractmethod def sync_tags(self, job_case: JobCase) -> None: """Sync tags between source and target items. Args: job_case (JobCase): The JobCase object containing the details of the case to sync. """ - raise NotImplementedError + pass @abstractmethod def is_alert_and_product_closed(self, job_case: JobCase, product: Any) -> bool: @@ -134,7 +132,6 @@ def is_alert_and_product_closed(self, job_case: JobCase, product: Any) -> bool: """ raise NotImplementedError - @abstractmethod def remove_synced_data_from_db(self, job_case: JobCase, product_details: Any) -> None: """Removes synced data from db. @@ -142,25 +139,23 @@ def remove_synced_data_from_db(self, job_case: JobCase, product_details: Any) -> job_case (JobCase): The JobCase object containing the details of the case to sync. product_details (Any): The details of the product to sync. """ - raise NotImplementedError + pass - @abstractmethod def sync_severity(self, job_case: JobCase) -> None: """Sync severity between source and target items. Args: job_case (JobCase): The JobCase object containing the details of the case to sync. """ - raise NotImplementedError + pass - @abstractmethod def sync_assignee(self, job_case: JobCase) -> None: """Sync assignee between source and target items. Args: job_case (JobCase): The JobCase object containing the details of the case to sync. """ - raise NotImplementedError + pass @abstractmethod def map_product_data_to_case(self, job_case: JobCase) -> None: diff --git a/packages/tipcommon/TIPCommon/uv.lock b/packages/tipcommon/TIPCommon/uv.lock index e3cfce573..fb35338a1 100644 --- a/packages/tipcommon/TIPCommon/uv.lock +++ b/packages/tipcommon/TIPCommon/uv.lock @@ -413,7 +413,7 @@ wheels = [ [[package]] name = "tipcommon" -version = "2.3.5" +version = "2.3.6" source = { editable = "." } dependencies = [ { name = "google-api-python-client" }, diff --git a/packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl b/packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl new file mode 100644 index 000000000..cc4cd274e Binary files /dev/null and b/packages/tipcommon/whls/TIPCommon-2.3.6-py3-none-any.whl differ