From d105ddc9e8e72453a5f661a7e3d50fe448eabb7b Mon Sep 17 00:00:00 2001 From: Bar Date: Wed, 23 Jul 2025 11:57:06 +0300 Subject: [PATCH 01/10] delete logs --- .../CybleEventsV2/CybleEventsV2.py | 35 ++++++++++--------- Packs/Gem/Integrations/Gem/Gem.py | 1 - .../QutteraWebsiteMalwareScanner.py | 1 - 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py index 35d3372e22bd..397b10a9dad6 100644 --- a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py +++ b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py @@ -714,7 +714,8 @@ def main(): # pragma: no cover verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) hide_cvv_expiry = params.get("hide_data", False) - demisto.debug(f"Command being called is {params}") + command = demisto.command() + demisto.debug(f"Command being called is {command}") mirror = params.get("mirror", False) incident_collections = params.get("incident_collections", []) incident_severity = params.get("incident_severity", []) @@ -723,15 +724,15 @@ def main(): # pragma: no cover client = Client(base_url=params.get("base_url"), verify=verify_certificate, proxy=proxy) args = demisto.args() - if demisto.command() == "test-module": + if command == "test-module": # request was successful return_results(test_response(client, "GET", base_url, token)) - elif demisto.command() == "fetch-incidents": + elif command == "fetch-incidents": # This is the call made when cyble-fetch-events command. last_run = demisto.getLastRun() - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) data, next_run = cyble_events( client, "POST", token, url, args, last_run, hide_cvv_expiry, incident_collections, incident_severity, False ) @@ -739,44 +740,44 @@ def main(): # pragma: no cover demisto.setLastRun(next_run) demisto.incidents(data) - elif demisto.command() == "update-remote-system": + elif command == "update-remote-system": # Updates changes in incidents to remote system if mirror: - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) return_results(update_remote_system(client, "PUT", token, args, url)) return - elif demisto.command() == "get-mapping-fields": + elif command == "get-mapping-fields": # Fetches mapping fields for outgoing mapper - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) return_results(get_mapping_fields(client, token, url)) - elif demisto.command() == "cyble-vision-subscribed-services": + elif command == "cyble-vision-subscribed-services": # This is the call made when subscribed-services command. return_results(fetch_subscribed_services_alert(client, "GET", base_url, token)) - elif demisto.command() == "cyble-vision-fetch-alert-groups": + elif command == "cyble-vision-fetch-alert-groups": # Fetch alert group. validate_input(args, False) - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) return_results(cyble_alert_group(client, "POST", token, url, args)) - elif demisto.command() == "cyble-vision-fetch-iocs": + elif command == "cyble-vision-fetch-iocs": # This is the call made when cyble-vision-v2-fetch-iocs command. validate_input(args, True) - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) command_results = cyble_fetch_iocs(client, "GET", token, args, url) return_results(command_results) - elif demisto.command() == "cyble-vision-fetch-alerts": + elif command == "cyble-vision-fetch-alerts": # This is the call made when cyble-vision-v2-fetch-alerts command. - url = base_url + str(ROUTES[COMMAND[demisto.command()]]) + url = base_url + str(ROUTES[COMMAND[command]]) lst_alerts, next_run = cyble_events( client, "POST", token, url, args, {}, hide_cvv_expiry, incident_collections, incident_severity, True ) @@ -789,10 +790,10 @@ def main(): # pragma: no cover ) ) else: - raise NotImplementedError(f"{demisto.command()} command is not implemented.") + raise NotImplementedError(f"{command} command is not implemented.") except Exception as e: - return_error(f"Failed to execute {demisto.command()} command. Error: {e!s}") + return_error(f"Failed to execute {command} command. Error: {e!s}") if __name__ in ("__main__", "__builtin__", "builtins"): diff --git a/Packs/Gem/Integrations/Gem/Gem.py b/Packs/Gem/Integrations/Gem/Gem.py index 3b968bc752bb..4c72f356d51a 100644 --- a/Packs/Gem/Integrations/Gem/Gem.py +++ b/Packs/Gem/Integrations/Gem/Gem.py @@ -1118,7 +1118,6 @@ def main() -> None: command = demisto.command() demisto.debug(f"args {args}") - demisto.debug(f"params {params}") demisto.debug(f"Command being called is {command}") try: diff --git a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.py b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.py index 66766a71c66d..f2a7521aa47e 100644 --- a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.py +++ b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.py @@ -205,7 +205,6 @@ def main(): # pragma: no cover command = demisto.command() demisto.info(f"Command called {command}") demisto.info(f"Args are {args}") - demisto.info(f"params are {params}") client = Client(params) try: if command == "quttera-scan-start": From 25f071f465dfcedf028a065a5605bce882ca64fa Mon Sep 17 00:00:00 2001 From: Bar Date: Wed, 23 Jul 2025 12:12:35 +0300 Subject: [PATCH 02/10] resolve-conflicts --- .../CybleEventsV2/CybleEventsV2.py | 1406 ++++++++++++----- 1 file changed, 1008 insertions(+), 398 deletions(-) diff --git a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py index 397b10a9dad6..1902cc14a67a 100644 --- a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py +++ b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py @@ -1,12 +1,18 @@ from CommonServerPython import * +from typing import Any """ IMPORTS """ -import json -from datetime import datetime - -import pytz import requests +from datetime import datetime, timedelta +import pytz import urllib3 +import dateparser +import json +from collections.abc import Sequence + +from dateutil.parser import parse as parse_date + +import concurrent.futures UTC = pytz.UTC @@ -15,9 +21,15 @@ """ CONSTANTS """ -MAX_ALERTS = 1600 -LIMIT_EVENT_ITEMS = 1600 +MAX_ALERTS = 50 +LIMIT_EVENT_ITEMS = 200 MAX_RETRIES = 3 +MAX_THREADS = 5 +MIN_MINUTES_TO_FETCH = 10 +DEFAULT_REQUEST_TIMEOUT = 600 +DEFAULT_TAKE_LIMIT = 5 +DEFAULT_STATUSES = ["VIEWED", "UNREVIEWED", "CONFIRMED_INCIDENT", "UNDER_REVIEW", "INFORMATIONAL"] +SAMPLE_ALERTS = 10 INCIDENT_SEVERITY = {"unknown": 0, "informational": 0.5, "low": 1, "medium": 2, "high": 3, "critical": 4} INCIDENT_STATUS = { "Unreviewed": "UNREVIEWED", @@ -30,13 +42,13 @@ "Remediation in Progress": "REMEDIATION_IN_PROGRESS", "Remediation not Required": "REMEDIATION_NOT_REQUIRED", } -SEVERITIES = {"Low": "LOW", "Medium": "MEDIUM", "High": "HIGH"} +SEVERITIES = {"Low": "LOW", "Medium": "MEDIUM", "High": "HIGH", "Critical": "HIGH", "Informational": "LOW", "Unknown": "LOW"} ROUTES = { - "services": r"/apollo/api/v1/y/services", + "services": r"/y/tpi/cortex/alerts/services", "alerts-groups": r"/apollo/api/v1/y/alerts/groups", - "alerts": r"/apollo/api/v1/y/alerts", + "alerts": r"/y/tpi/cortex/alerts", "iocs": r"/engine/api/v2/y/iocs", - "test": r"/apollo/api/v1/y/services", + "test": r"/y/tpi/cortex/alerts/services", } COMMAND = { @@ -48,9 +60,183 @@ "fetch-incidents": "alerts", "update-remote-system": "alerts", "get-mapping-fields": "alerts", + "get-modified-remote-data": "alerts", + "get-remote-data": "alerts", } +def get_headers(alerts_api_key: str) -> dict: + return {"Content-Type": "application/json", "Authorization": f"Bearer {alerts_api_key}"} + + +def encode_headers(headers: dict) -> dict: + return {k: v.encode("utf-8") for k, v in headers.items()} + + +def get_event_format(event): + """ + Converts an event from Cyble to a format suitable for Demisto. + :param event: The event to format + :return: A dictionary with the event's information + """ + return { + "name": event.get("name"), + "severity": event.get("severity"), + "rawJSON": json.dumps(event), + "event_id": event.get("event_id"), + "keyword": event.get("keyword"), + "created": event.get("created_at"), + } + + +def get_alert_payload(service, input_params: dict[str, Any], is_update=False): + """ + Generate the payload for a call to the Cyble alerts API. + + :param service: The service to fetch alerts for + :param input_params: A dictionary of parameters for the API call + :param is_update: If True, use `updated_at` instead of `created_at` + :return: A dictionary containing the payload for the API call + """ + try: + # Determine the timestamp field based on `is_update` + timestamp_field = "updated_at" if is_update else "created_at" + + return { + "filters": { + "service": service if isinstance(service, list) else [service], + timestamp_field: { # Use dynamic field based on `is_update` + "gte": ensure_aware(datetime.fromisoformat(input_params["gte"])).strftime("%Y-%m-%dT%H:%M:%S+00:00"), + "lte": ensure_aware(datetime.fromisoformat(input_params["lte"])).strftime("%Y-%m-%dT%H:%M:%S+00:00"), + }, + "status": [ + "VIEWED", + "UNREVIEWED", + "CONFIRMED_INCIDENT", + "UNDER_REVIEW", + "INFORMATIONAL", + "REMEDIATION_IN_PROGRESS", + "REMEDIATION_NOT_REQUIRED", + "FALSE_POSITIVE", + ], + "severity": input_params["severity"], + }, + "orderBy": [{timestamp_field: input_params["order_by"]}], + "skip": input_params["skip"], + "take": input_params["take"], + "countOnly": False, + "taggedAlert": False, + "withDataMessage": True, + } + except Exception as e: + demisto.error(f"Error in formatting: {e}") + + +def get_alert_payload_by_id( + client, alert_id: str, token: str, url: str, incident_collections: dict, incident_severity: dict, hide_cvv_expiry: bool +) -> dict: + demisto.debug(f"[get_alert_payload_by_id] Called with alert_id: {alert_id}") + + try: + alert = get_alert_by_id(client, alert_id, token, url) + if not alert: + error_msg = f"[get_alert_payload_by_id] Alert with ID {alert_id} could not be fetched." + demisto.error(error_msg) + raise ValueError(error_msg) + + if "service" not in alert: + error_msg = f"[get_alert_payload_by_id] Alert ID {alert_id} is missing required 'service' field." + demisto.error(error_msg) + raise ValueError(error_msg) + + demisto.debug("[get_alert_payload_by_id] Alert fetched successfully") + + incidents = format_incidents([alert], hide_cvv_expiry) + if not incidents: + error_msg = f"[get_alert_payload_by_id] Formatting failed for alert ID {alert_id}" + demisto.error(error_msg) + raise ValueError(error_msg) + + incident = incidents[0] + incident["rawJSON"] = json.dumps(alert) + + demisto.debug("[get_alert_payload_by_id] Converted alert to incident using format_incidents") + return incident + + except Exception as e: + # Keep the log for debugging + demisto.error(f"[get_alert_payload_by_id] Exception occurred: {str(e)}") + raise # Propagate the exception to the caller + + +def time_diff_in_mins(gte: datetime, lte: datetime): + """ + Calculates the difference in minutes between two datetime objects. + + :param gte: The start date time + :param lte: The end date time + :return: The difference in minutes + """ + diff = (lte - gte).total_seconds() / 60 + return diff + + +def format_incidents(alerts, hide_cvv_expiry): + """ + Format the incidents to feed into XSOAR + :param alerts events fetched from the server + :return: incidents to feed into XSOAR + """ + events = [] + for alert in alerts: + try: + if hide_cvv_expiry and alert["service"] == "compromised_cards": + alert["data"]["bank"]["card"]["cvv"] = "xxx" + alert["data"]["bank"]["card"]["expiry"] = "xx/xx/xxxx" + alert_details = { + "name": "Cyble Vision Alert on {}".format(alert.get("service")), + "event_type": "{}".format(alert.get("service")), + "severity": INCIDENT_SEVERITY.get((alert.get("user_severity") or alert.get("severity") or "").lower()), + "event_id": "{}".format(alert.get("id")), + "data_message": json.dumps(alert.get("data")), + "keyword": "{}".format(alert.get("keyword_name")), + "created_at": "{}".format(alert.get("created_at")), + "status": REVERSE_INCIDENT_STATUS.get(alert.get("status")), + "mirrorInstance": demisto.integrationInstance(), + } + if alert.get("service") == "compromised_cards": + card_details = alert["data"]["bank"]["card"] + alert_details.update( + { + "card_brand": card_details.get("brand"), + "card_no": card_details.get("card_no"), + "card_cvv": card_details.get("cvv"), + "card_expiry": card_details.get("expiry"), + "card_level": card_details.get("level"), + "card_type": card_details.get("type"), + } + ) + elif alert.get("service") == "stealer_logs": + content = alert["data"].get("content") + if content: + alert_details.update( + { + "application": content.get("Application"), + "password": content.get("Password"), + "url": content.get("URL"), + "username": content.get("Username"), + } + ) + alert_details.update({"filename": alert["data"]["filename"]}) + events.append(alert_details) + except Exception as e: + error_msg = f"Unable to format alert (ID: {alert.get('id', 'unknown')}), error: {e}" + demisto.error(error_msg) + raise + + return events + + class Client(BaseClient): """ Client will implement the service API, and should not contain any Demisto logic. @@ -65,7 +251,6 @@ def get_response(self, url, headers, payload, method): :param method: Contains the request method :param payload: Contains the request body """ - for _ in range(MAX_RETRIES): try: if method == "POST" or method == "PUT": @@ -81,57 +266,304 @@ def get_response(self, url, headers, payload, method): pass return None + def make_request(self, url, api_key, method="GET", payload_json=None, params=None): + """ + Makes an HTTP request to the specified host and path with the specified API key, + method, and payload_json. Returns the response object. + + :param host: The host to make the request to + :param path: The path to make the request to + :param api_key: The API key to use for the request + :param method: The HTTP method to use for the request (default: GET) + :param payload_json: The JSON payload to send with the request (default: None) + :param params: The query parameters to send with the request (default: None) + :return: The response object + """ + headers = get_headers(api_key) + encoded_headers = encode_headers(headers) + return requests.request( + method, url, data=payload_json, headers=encoded_headers, params=params, timeout=DEFAULT_REQUEST_TIMEOUT + ) + + def get_data(self, service, input_params, is_update=False): + """ + Sends an HTTP POST request to the given host with the provided payload and API key, + and logs errors if the request fails. + + Logs the final payload, URL, API key, and checks the response. + + :param service: The service to fetch data from + :param input_params: A dictionary containing parameters for the API call + :param is_update: Whether this is an update fetch (based on updated_at + instead of created_at) + :return: The JSON response from the request as a dictionary, + or an empty dictionary if the request fails + """ + payload = get_alert_payload(service, input_params, is_update) + payload_json = json.dumps(payload) + url = input_params.get("url") + alerts_api_key = input_params.get("api_key") + + demisto.debug(f"[get_data] Sending request to {url} for service: {service}, is_update: {is_update}") + + if not url or not alerts_api_key: + raise ValueError("Missing required URL or API key in input_params.") + + try: + response = self.make_request(url, alerts_api_key, "POST", payload_json) + demisto.debug(f"[get_data] Response status code: {response.status_code}") + + except Exception as request_error: + raise Exception(f"HTTP request failed for service '{service}': {str(request_error)}") + + if response.status_code != 200: + raise Exception( + f"Failed to fetch data from {service}. Status code: {response.status_code}, Response text: {response.text}" + ) + + try: + json_response = response.json() + demisto.debug(f"[get_data] JSON response received with keys: {list(json_response.keys())}") + return json_response + except ValueError as json_error: + raise Exception(f"Invalid JSON response from {service}: {str(json_error)}") + + def get_all_services(self, api_key, url): + """ + Requests the list of all services from the Cyble API with the given API key and logs errors if the request fails. + + :param api_key: The API key to be used for the request + :param ew: An event writer object for logging + :return: A list of service dictionaries, or an empty list if the request fails + """ + try: + url = url + "/services" + response = self.make_request(url, api_key) + if response.status_code != 200: + raise Exception(f"Wrong status code: {response.status_code}") + response = response.json() + + if "data" in response and isinstance(response["data"], Sequence): + demisto.debug(f"Received services: {json.dumps(response['data'], indent=2)}") + return response["data"] + + else: + raise Exception("Wrong Format for services response") + except Exception as e: + raise Exception(f"Failed to get services: {str(e)}") + + def insert_data_in_cortex(self, service, input_params, is_update): + """ + Fetches and inserts data into Cortex XSOAR from the given service based on the given parameters. + + :param service: The service to fetch data from + :param input_params: A dictionary containing parameters for the API call, + including the API key, base URL, skip, take, and time range + :return: The latest created time of the data inserted + """ + latest_created_time = datetime.utcnow() + input_params.update({"skip": 0, "take": int(input_params["limit"])}) + all_incidents = [] + + try: + while True: + try: + response = self.get_data(service, input_params, is_update) + demisto.debug( + "[insert_data_in_cortex] Received response for " + f"skip: {input_params['skip']}, " + f"items: {len(response.get('data', [])) if 'data' in response else 'N/A'}" + ) + + except Exception as e: + demisto.error(f"[insert_data_in_cortex] get_data failed for service: {service} with error: {str(e)}") + raise + + input_params["skip"] += input_params["take"] + + if "data" in response and isinstance(response["data"], Sequence): + if not response["data"]: + demisto.debug("[insert_data_in_cortex] No more data, exiting loop") + break + + try: + latest_created_time = parse_date(response["data"][-1].get("created_at")) + timedelta(microseconds=1) + demisto.debug(f"[insert_data_in_cortex] Updated latest_created_time: {latest_created_time}") + + except Exception as e: + demisto.error(f"[insert_data_in_cortex] Failed to parse created_at: {str(e)}") + raise + + try: + events, incidentsArr = format_incidents(response["data"], input_params["hce"]), [] + demisto.debug(f"[insert_data_in_cortex] Formatting incidents, total events: {len(events)}") + for event in events: + try: + incident = get_event_format(event) + incidentsArr.append(incident) + except Exception as e: + demisto.error(f"[insert_data_in_cortex] get_event_format failed: {str(e)}") + continue + except Exception as e: + demisto.error(f"[insert_data_in_cortex] format_incidents failed: {str(e)}") + raise + + all_incidents.extend(incidentsArr) + demisto.debug(f"[insert_data_in_cortex] Pushing {len(incidentsArr)} incidents to Cortex") + demisto.incidents(incidentsArr) + + else: + raise Exception( + "[insert_data_in_cortex] Unable to fetch data for " + f"gte: {input_params['gte']}, " + f"lte: {input_params['lte']}, " + f"skip: {input_params['skip']}, " + f"take: {input_params['take']}" + ) + + except Exception as e: + demisto.error(f"[insert_data_in_cortex] Failed for service '{service}': {str(e)}") + raise + + demisto.debug(f"[insert_data_in_cortex] Completed. Total incidents pushed: {len(all_incidents)}") + return all_incidents, latest_created_time + + def get_data_with_retry(self, service, input_params, is_update=False): + """ + Recursively splits time ranges and fetches data, inserting it into Cortex. + Returns a tuple of (alerts, latest_created_time). + """ + gte = parse_date(input_params["gte"]) + lte = parse_date(input_params["lte"]) + demisto.debug(f"[get_data_with_retry] Time range: gte={gte}, lte={lte}") + + que = [[gte, lte]] + latest_created_time = None + all_alerts = [] + + while que: + current_gte, current_lte = que.pop(0) + demisto.debug(f"[get_data_with_retry] Processing time range: {current_gte} to {current_lte}") + + current_params = input_params.copy() + current_params["gte"] = current_gte.isoformat() + current_params["lte"] = current_lte.isoformat() + + response = self.get_data(service, current_params, is_update=is_update) + + if "data" in response: + curr_alerts, curr_time = self.insert_data_in_cortex(service, current_params, is_update) + demisto.debug(f"[get_data_with_retry] Retrieved {len(curr_alerts)} alerts, curr_time: {curr_time}") + + all_alerts.extend(curr_alerts) + + if latest_created_time is None: + latest_created_time = curr_time + else: + latest_created_time = max(latest_created_time, curr_time) + + elif time_diff_in_mins(current_gte, current_lte) >= MIN_MINUTES_TO_FETCH: + mid_datetime = current_gte + (current_lte - current_gte) / 2 + que.extend([[current_gte, mid_datetime], [mid_datetime + timedelta(microseconds=1), current_lte]]) + demisto.debug( + "[get_data_with_retry] Splitting time range further: " + f"{current_gte} to {mid_datetime}, " + f"{mid_datetime + timedelta(microseconds=1)} to {current_lte}" + ) + else: + demisto.debug(f"[get_data_with_retry] Unable to fetch data for time range: {current_gte} to {current_lte}") + + if latest_created_time is None: + latest_created_time = datetime.utcnow() + demisto.debug("No data processed, using current time as latest_created_time") + + demisto.debug( + f"[get_data_with_retry] Finished. Total alerts: {len(all_alerts)}, latest_created_time: {latest_created_time}" + ) + return all_alerts, latest_created_time + timedelta(microseconds=1) + + def get_ids_with_retry(self, service, input_params, is_update=False): + """ + Recursively splits time ranges and fetches data, inserting it into Cortex. + Returns a tuple of (alerts, latest_created_time). + """ + + gte = parse_date(input_params["gte"]) + lte = parse_date(input_params["lte"]) + + que = [[gte, lte]] + ids = [] + + while que: + current_gte, current_lte = que.pop(0) + + # Serialize datetime objects to strings BEFORE placing in input_params + input_params["gte"] = current_gte.isoformat() + input_params["lte"] = current_lte.isoformat() + + response = self.get_data(service, input_params, is_update=is_update) + if "data" in response: + for alert in response["data"]: + alert_id = alert.get("id") + if isinstance(alert_id, str) and alert_id.strip(): + ids.append(alert_id) + elif time_diff_in_mins(current_gte, current_lte) >= MIN_MINUTES_TO_FETCH: + mid_datetime = current_gte + (current_lte - current_gte) / 2 + que.extend([[current_gte, mid_datetime], [mid_datetime + timedelta(microseconds=1), current_lte]]) + else: + demisto.debug(f"Unable to fetch data for gte: {current_gte} to lte: {current_lte}") + demisto.debug(f"ids:{ids}") + + return ids + + def update_alert(self, payload, url, api_key): + """ + Updates the alert with the given payload and API key. + + :param payload: A dictionary of key-value pairs containing the alert data to be updated. + :param url: The URL of the Cyble API endpoint to be used for the request. + :param api_key: The API key to be used for the request. + :return: None + :raises Exception: If the request fails. + """ + try: + payload_json = json.dumps(payload) + response = self.make_request(url, api_key, "PUT", payload_json) + if response.status_code != 200: + return_error(f"[update_alert] Unexpected status code: {response.status_code}, response: {response.text}") + except Exception as e: + return_error(f"[update_alert] Exception while updating alert: {str(e)}") + -def validate_input(args, is_iocs=False): +def validate_iocs_input(args): """ - Check if the input params for the command are valid. Return an error if any - :param args: dictionary of input params - :param is_iocs: check if the params are for iocs command + Validates the input arguments for the fetch-iocs command. + + :param args: A dictionary of input arguments. + :return: None + :raises ValueError: If the input arguments are invalid. """ try: - # we assume all the params to be non-empty, as cortex ensures it if int(args.get("from")) < 0: raise ValueError(f"The parameter from has a negative value, from: {arg_to_number(args.get('from'))}'") - limit = int(args.get("limit", 1)) - - if is_iocs: - date_format = "%Y-%m-%d" - if args.get("start_date") and args.get("end_date"): - _start_date = datetime.strptime(args.get("start_date"), date_format) - _end_date = datetime.strptime(args.get("end_date"), date_format) - else: - _start_date = datetime(1, 1, 1, 0, 0) - _end_date = datetime(1, 1, 1, 0, 0) - - if limit <= 0 or limit > 100: - raise ValueError(f"The limit argument should contain a positive number, up to 100, limit: {limit}") - - if _start_date > datetime.utcnow(): - raise ValueError(f"Start date must be a date before or equal to {datetime.today().strftime(date_format)}") - if _end_date > datetime.utcnow(): - raise ValueError(f"End date must be a date before or equal to {datetime.today().strftime(date_format)}") - if _start_date > _end_date: - raise ValueError(f"Start date {args.get('start_date')} cannot be after end date {args.get('end_date')}") + limit, date_format = int(args.get("limit", 1)), "%Y-%m-%d" + if args.get("start_date") and args.get("end_date"): + _start_date, _end_date = ( + datetime.strptime(args.get("start_date"), date_format), + datetime.strptime(args.get("end_date"), date_format), + ) else: - date_format = "%Y-%m-%dT%H:%M:%S%z" - _start_date = datetime.strptime(args.get("start_date"), date_format) - _end_date = datetime.strptime(args.get("end_date"), date_format) - if limit <= 0 or limit > LIMIT_EVENT_ITEMS: - raise ValueError(f"The limit argument should contain a positive number, up to 1000, limit: {limit}") - if _start_date > datetime.now(tz=UTC): - raise ValueError(f"Start date must be a date before or equal to {datetime.now(tz=UTC).strftime(date_format)}") - if _end_date > datetime.now(tz=UTC): - raise ValueError(f"End date must be a date before or equal to {args.get('end_date')}") - if _start_date > _end_date: - raise ValueError(f"Start date {args.get('start_date')} cannot be after end date {args.get('end_date')}") - return + _start_date, _end_date = datetime(1, 1, 1, 0, 0), datetime(1, 1, 1, 0, 0) + if limit <= 0 or limit > 100: + raise ValueError(f"The limit argument number should, up to 100, given limit: {limit}") + if _start_date > _end_date: + raise ValueError(f"Start date {args.get('start_date')} cannot be after end date {args.get('end_date')}") except Exception as e: - demisto.error(f"Exception with validating inputs [{e}]") - raise e + demisto.error(f"Failed to process validate_iocs_input with {str(e)}") def alert_input_structure(input_params): - input_params_alerts: dict[str, Any] = { + input_params_alerts = { "orderBy": [{"created_at": input_params["order_by"]}], "select": { "alert_group_id": True, @@ -186,79 +618,11 @@ def set_request(client, method, token, input_params, url): return response -def format_incidents(alerts, hide_cvv_expiry): - """ - Format the incidents to feed into XSOAR - :param alerts events fetched from the server - :return: incidents to feed into XSOAR - """ - events: List[dict[str, Any]] = [] - for alert in alerts: - try: - if hide_cvv_expiry and alert["service"] == "compromised_cards": - alert["data_message"]["data"]["bank"]["card"]["cvv"] = "xxx" - alert["data_message"]["data"]["bank"]["card"]["expiry"] = "xx/xx/xxxx" - - keyword = "" - if ( - alert.get("metadata") - and alert["metadata"].get("entity") - and alert["metadata"]["entity"].get("keyword") - and alert["metadata"]["entity"]["keyword"]["tag_name"] - ): - keyword = alert["metadata"]["entity"]["keyword"]["tag_name"] - - alert_details = { - "name": "Cyble Vision Alert on {}".format(alert.get("service")), - "event_type": "{}".format(alert.get("service")), - "severity": INCIDENT_SEVERITY.get(alert.get("severity").lower()), - "alert_group_id": "{}".format(alert.get("alert_group_id")), - "event_id": "{}".format(alert.get("id")), - "data_message": json.dumps(alert.get("data_message")), - "keyword": f"{keyword}", - "created_at": "{}".format(alert.get("created_at")), - "status": "{}".format(alert.get("status")), - "mirrorInstance": demisto.integrationInstance(), - } - - if alert.get("service") == "compromised_cards": - card_details = alert["data_message"]["data"]["bank"]["card"] - alert_details.update( - { - "card_brand": card_details.get("brand"), - "card_no": card_details.get("card_no"), - "card_cvv": card_details.get("cvv"), - "card_expiry": card_details.get("expiry"), - "card_level": card_details.get("level"), - "card_type": card_details.get("type"), - } - ) - elif alert.get("service") == "stealer_logs": - content = alert["data_message"]["data"].get("content") - if content: - alert_details.update( - { - "application": content.get("Application"), - "password": content.get("Password"), - "url": content.get("URL"), - "username": content.get("Username"), - } - ) - alert_details.update({"filename": alert["data_message"]["data"]["filename"]}) - - events.append(alert_details) - except Exception as e: - demisto.debug(f"Unable to format incidents, error: {e}") - continue - return events - - -def fetch_service_details(client, base_url, token): - service_name_lists = fetch_subscribed_services(client, "GET", base_url, token) - lst = [] - for service_name_list in service_name_lists: - lst.append(service_name_list["name"]) - return lst +def ensure_aware(dt: datetime) -> datetime: + """Ensure datetime is timezone-aware in UTC.""" + if dt.tzinfo is None: + return dt.replace(tzinfo=pytz.UTC) + return dt.astimezone(pytz.UTC) def fetch_subscribed_services(client, method, base_url, token): @@ -272,329 +636,566 @@ def fetch_subscribed_services(client, method, base_url, token): Returns: subscribed service list """ - get_subscribed_service_url = base_url + str(ROUTES[COMMAND["cyble-vision-subscribed-services"]]) - subscribed_services = set_request(client, method, token, {}, get_subscribed_service_url) + subscribed_services = client.get_all_services(token, base_url) service_name_list = [] - if subscribed_services: for subscribed_service in subscribed_services: service_name_list.append({"name": subscribed_service["name"]}) return service_name_list -def test_response(client, method, base_url, token): +def get_fetch_service_list(client, incident_collections, service_url, token): """ - Test the integration state - Args: - client: client instance - method: Requests method to be used - base_url: base url for the server - token: API access token + Determines the list of services to fetch based on provided incident collections. - Returns: test response + Args: + client: An instance of the client to communicate with the server. + incident_collections: A list of incident collection names to filter the services. + service_url: The base URL for the server. + token: The API access token. + + If specific incident collections are provided (excluding "All collections"), + it appends corresponding service names to `fetch_services`. Otherwise, it fetches + all services using the client. """ - fetch = fetch_subscribed_services(client, method, base_url, token) - if fetch: - return "ok" + fetch_services = [] + if len(incident_collections) > 0 and "All collections" not in incident_collections: + if "Darkweb Marketplaces" in incident_collections: + fetch_services.append("darkweb_marketplaces") + if "Data Breaches" in incident_collections: + fetch_services.append("darkweb_data_breaches") + if "Compromised Endpoints" in incident_collections: + fetch_services.append("stealer_logs") + if "Compromised Cards" in incident_collections: + fetch_services.append("compromised_cards") else: - demisto.error("Failed to connect") - raise Exception("failed to connect") + subscribed_services = client.get_all_services(token, service_url) + if subscribed_services: + fetch_services = [service["name"] for service in subscribed_services] + return fetch_services -def cyble_events(client, method, token, url, args, last_run, hide_cvv_expiry, incident_collections, incident_severity, skip=True): + +def fetch_subscribed_services_alert(client, method, base_url, token): """ - Fetch alert details from server for creating incidents in XSOAR + Fetch cyble subscribed services Args: client: instance of client to communicate with server method: Requests method to be used - token: API access token - url: end point URL - args: input args - last_run: get last run details - hide_cvv_expiry: hide expiry / cvv number from cards - incident_collections: list of collections to be fetched - incident_severity: list of severities to be fetched - skip: skip the validation for fetch incidnet + base_url: base url for the server + token: server access token - Returns: events from the server + Returns: subscribed service list """ + try: + subscribed_services = client.get_all_services(token, base_url) + service_name_list = [] - input_params = {} - input_params["order_by"] = args.get("order_by", "asc") - input_params["from_da"] = arg_to_number(args.get("from", 0)) - input_params["limit"] = MAX_ALERTS - max_fetch = arg_to_number(demisto.params().get("max_fetch", 1)) + for subscribed_service in subscribed_services: + service_name_list.append({"name": subscribed_service["name"]}) - if skip: - validate_input(args, False) - input_params["start_date"] = args.get("start_date", "") - input_params["end_date"] = args.get("end_date", "") - if not args.get("end_date", ""): - input_params["end_date"] = datetime.utcnow().astimezone().isoformat() - else: - initial_interval = demisto.params().get("first_fetch_timestamp", 1) - if "event_pull_start_date" not in last_run: - event_pull_start_date = datetime.utcnow() - timedelta(days=int(initial_interval)) - input_params["start_date"] = event_pull_start_date.astimezone().isoformat() + markdown = tableToMarkdown("Alerts Group Details:", service_name_list) + return CommandResults( + readable_output=markdown, + outputs_prefix="CybleEvents.ServiceList", + raw_response=service_name_list, + outputs=service_name_list, + ) + except Exception as e: + return_error(f"Failed to fetch subscribed services: {str(e)}") + + +def test_response(client, method, base_url, token): + """ + Test the integration state + """ + try: + # The test mocks this specific endpoint + url_suffix = "/y/tpi/cortex/alerts" + headers = {"Authorization": f"Bearer {token}"} + + response = client._http_request(method=method, url_suffix=url_suffix, headers=headers) + + if response: + return "ok" else: - input_params["start_date"] = last_run["event_pull_start_date"] - input_params["end_date"] = datetime.utcnow().astimezone().isoformat() + raise Exception("failed to connect") + except Exception as e: + demisto.error(f"Failed to connect: {e}") + raise Exception("failed to connect") - latest_created_time = input_params["start_date"] - final_input_structure = alert_input_structure(input_params) - if len(incident_collections) > 0 and "All collections" not in incident_collections: - fetch_services = [] - if "Darkweb Marketplaces" in incident_collections: - fetch_services.append("darkweb_marketplaces") - if "Data Breaches" in incident_collections: - fetch_services.append("darkweb_data_breaches") - if "Compromised Endpoints" in incident_collections: - fetch_services.append("stealer_logs") - if "Compromised Cards" in incident_collections: - fetch_services.append("compromised_cards") - final_input_structure["where"]["service"] = {"in": fetch_services} +def migrate_data(client: Client, input_params: dict[str, Any], is_update=False): + """ + Migrates data from cyble to demisto cortex. - if len(incident_severity) > 0 and "All severities" not in incident_severity: - fetch_severities = [] - for severity in incident_severity: - fetch_severities.append(SEVERITIES.get(severity)) - final_input_structure["where"]["severity"] = {"in": fetch_severities} + Args: + client: instance of client to communicate with server + input_params: dict containing the parameters for the migration, including services and their associated parameters + is_update: Boolean flag indicating whether this is an update (used for get-modified-remote-data) - all_alerts = set_request(client, method, token, final_input_structure, url) - timestamp_count = {} # type: ignore + Returns: the max of the last fetched timestamp + """ + # Add type check and default value to prevent indexing errors - if not all_alerts: - return [], {"event_pull_start_date": latest_created_time} + demisto.debug(f"[migrate_data] Function called with is_update={is_update}") + demisto.debug(f"[migrate_data] input_params: {json.dumps(input_params)}") - for alert in all_alerts: - timestamp = alert["created_at"] - if timestamp in timestamp_count: - timestamp_count[timestamp] += 1 - else: - timestamp_count[timestamp] = 1 + services = input_params.get("services", []) + if not services: + demisto.debug("[migrate_data] No services found in input_params. Returning empty alert list.") + demisto.debug("No services found in input_params") + return [], datetime.utcnow() - alert_count = 0 - prev_timestamp = all_alerts[0].get("created_at") - last_timestamp = all_alerts[-1].get("created_at") + demisto.debug(f"[migrate_data] Services to process: {services}") - alerts = [] - for alert in all_alerts: - current_timestamp = alert.get("created_at") - if current_timestamp == prev_timestamp: - alerts.append(alert) - else: - alert_count += timestamp_count[prev_timestamp] - prev_timestamp = current_timestamp + chunkedServices = [services[i : i + MAX_THREADS] for i in range(0, len(services), MAX_THREADS)] + last_fetched = ensure_aware(datetime.utcnow()) - if alert_count + timestamp_count[current_timestamp] <= max_fetch and current_timestamp != last_timestamp: - alerts.append(alert) - else: - break - - del all_alerts - del timestamp_count - - incidents = [] - - if alerts: - timestamp_str = alerts[-1].get("created_at") - original_datetime = datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S.%fZ") - updated_datetime = original_datetime + timedelta(microseconds=1000) - latest_created_time = updated_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - events = format_incidents(alerts, hide_cvv_expiry) - - for event in events: - inci = { - "name": event.get("name"), - "severity": event.get("severity"), - "rawJSON": json.dumps(event), - "alert_group_id": event.get("alert_group_id"), - "event_id": event.get("event_id"), - "keyword": event.get("keyword"), - "created": event.get("created_at"), - } - incidents.append(inci) - next_run = {"event_pull_start_date": latest_created_time} + all_alerts = [] - return incidents, next_run - else: - return [], {"event_pull_start_date": latest_created_time} + try: + for chunk in chunkedServices: + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [executor.submit(client.get_data_with_retry, service, input_params, is_update) for service in chunk] + for future in concurrent.futures.as_completed(futures): + try: + alerts, fetched_time = future.result() + demisto.debug(f"[migrate_data] Fetched {len(alerts)} alerts. fetched_time: {fetched_time}") + all_alerts.extend(alerts) + if isinstance(fetched_time, datetime): + last_fetched = max(last_fetched, ensure_aware(fetched_time)) + except Exception as inner_e: + demisto.error(f"[migrate_data] Error in future: {str(inner_e)}") + return_error(f"[migrate_data] Failed to process service thread: {str(inner_e)}") + except Exception as e: + return_error(f"[migrate_data] Migration failed: {str(e)}") -def update_remote_system(client, method, token, args, url): - """ - Updates any changes in any mappable incident to remote server - Args: - client: instance of client to communicate with server - method: Requests method to be used - token: API access token - url: end point URL - args: input args + return all_alerts, last_fetched - Returns: None - """ - parsed_args = UpdateRemoteSystemArgs(args) - if parsed_args.delta: - severities = {"1": "LOW", "2": "MEDIUM", "3": "HIGH", "4": "CRITICAL"} - data = parsed_args.data - incident_id = data.get("id") - status = data.get("status") - assignee_id = data.get("assignee_id") - updated_severity = str(data.get("severity")) - - updated_event = {"id": incident_id} - if status in INCIDENT_STATUS: - updated_event["status"] = INCIDENT_STATUS.get(status) - if assignee_id: - updated_event["assignee_id"] = assignee_id - if updated_severity: - if updated_severity == "0.5" or updated_severity == "0": - updated_event["user_severity"] = None +def fetch_few_alerts(client, input_params, services, url, token, is_update=False): + result = [] + input_params["take"] = SAMPLE_ALERTS # override limit for sample + demisto.debug(f"[fetch_few_alerts] Updated 'take' to SAMPLE_ALERTS ({SAMPLE_ALERTS})") + + for service in services: + try: + # Append transport details only for internal use by get_data + input_params_with_context = input_params.copy() + input_params_with_context["url"] = url + input_params_with_context["api_key"] = token + + response = client.get_data(service, input_params_with_context, is_update=is_update) + + if "data" in response and isinstance(response["data"], Sequence): + demisto.debug(f"[fetch_few_alerts] Received {len(response['data'])} alerts") + + hce = input_params.get("hce", False) + events = format_incidents(response["data"], hce) + + for event in events: + formatted_event = get_event_format(event) + result.append(formatted_event) else: - updated_event["user_severity"] = severities.get(updated_severity) + demisto.debug("[fetch_few_alerts] No valid data in response") + except Exception as e: + return_error(f"[fetch_few_alerts] Failed to fetch data from service {service}: {e}") - body = {"alerts": [updated_event]} - set_request(client, method, token, body, url) + if result: + break + demisto.debug(f"[fetch_few_alerts] Total alerts returned: {len(result)}") + return result -def get_mapping_fields(client, token, url): + +def build_get_alert_payload(alert_id): """ - Fetches all the fields associated with incidents for creating outgoing mapper - Args: - client: instance of client to communicate with server - token: API access token - url: end point URL + Builds the payload for fetching an alert by ID. + """ + return { + "filters": {"id": [alert_id]}, + "excludes": {"status": ["FALSE_POSITIVE"]}, + "orderBy": [{"created_at": "desc"}], + "skip": 0, + "take": 1, + "taggedAlert": False, + "withDataMessage": True, + "countOnly": False, + } - Returns: None + +def build_auth_headers(token): + """ + Builds the authorization headers for the API request. """ + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} - input_params: dict[str, Any] = {} - input_params["order_by"] = "asc" - input_params["from_da"] = 0 - input_params["limit"] = 500 +def get_alert_by_id(client, alert_id, token, url): + """ + Fetches a specific alert by its ID. + """ + demisto.debug(f"[get_alert_by_id] Fetching alert with ID: {alert_id}") - initial_interval = 1 - event_pull_start_date = datetime.utcnow() - timedelta(days=int(initial_interval)) + payload = build_get_alert_payload(alert_id) + headers = build_auth_headers(token) - input_params["start_date"] = event_pull_start_date.astimezone().isoformat() - input_params["end_date"] = datetime.utcnow().astimezone().isoformat() - final_input_structure = alert_input_structure(input_params) + demisto.debug("[get_alert_by_id] Final payload being sent:") + demisto.debug(json.dumps(payload, indent=2)) - alerts = set_request(client, "POST", token, final_input_structure, url) + try: + response = client._http_request( + method="POST", url_suffix="/y/tpi/cortex/alerts", headers=headers, json_data=payload, timeout=30 + ) - fields = {} - for alert in alerts: - for key in alert: - fields[key] = alert[key] + demisto.debug("[get_alert_by_id] Raw response:") + demisto.debug(json.dumps(response, indent=2)) - incident_type_scheme = SchemeTypeMapping(type_name="cyble_outgoing_mapper") + data = response.get("data", []) + if data: + demisto.debug(f"[get_alert_by_id] Alert found: ID {alert_id}") + return data[0] - for field, description in fields.items(): - incident_type_scheme.add_field(field, description) + demisto.debug(f"[get_alert_by_id] No alert found for ID: {alert_id}") + return None - return GetMappingFieldsResponse([incident_type_scheme]) + except Exception as e: + raise DemistoException(f"[get_alert_by_id] Error during HTTP request: {str(e)}") -def fetch_subscribed_services_alert(client, method, base_url, token): +def get_fetch_severities(incident_severity): """ - Fetch cyble subscribed services + Determines the list of severities to fetch based on provided incident severities. + Args: - client: instance of client to communicate with server - method: Requests method to be used - base_url: base url for the server - token: server access token + incident_severity: A list of incident severity levels to filter the results. + + Returns: + A list of severities to fetch. If specific severities are provided (excluding "All severities"), + it returns the corresponding severities from the SEVERITIES mapping. Otherwise, it defaults to + ["LOW", "MEDIUM", "HIGH"]. + """ + fetch_severities = [] + if len(incident_severity) > 0 and "All severities" not in incident_severity: + for severity in incident_severity: + fetch_severities.append(SEVERITIES.get(severity)) + else: + fetch_severities = ["LOW", "MEDIUM", "HIGH"] + return fetch_severities - Returns: subscribed service list +def cyble_events(client, method, token, url, args, last_run, hide_cvv_expiry, incident_collections, incident_severity, skip=True): """ - get_subscribed_service_url = base_url + str(ROUTES[COMMAND["cyble-vision-subscribed-services"]]) - subscribed_services = set_request(client, method, token, {}, get_subscribed_service_url) - service_name_list = [] + Entry point for fetching alerts from Cyble Vision. + Calls the appropriate fetch function based on manual or scheduled execution. + """ + demisto.debug("[cyble_events] Function called") - for subscribed_service in subscribed_services: - service_name_list.append({"name": subscribed_service["name"]}) + if skip: + return manual_fetch(client, args, token, url, incident_collections, incident_severity) - markdown = tableToMarkdown( - "Alerts Group Details:", - service_name_list, + input_params = {"order_by": args.get("order_by", "asc"), "skip": 0, "limit": MAX_ALERTS} + demisto.debug("[cyble_events] skip=False, proceeding with scheduled fetch") + demisto.debug(f"[cyble_events] Initial input_params: {input_params}") + + initial_interval = demisto.params().get("first_fetch_timestamp", 1) + if "event_pull_start_date" not in last_run: + event_pull_start_date = datetime.utcnow() - timedelta(days=int(initial_interval)) + input_params["gte"] = event_pull_start_date.astimezone().isoformat() + demisto.debug(f"[cyble_events] event_pull_start_date not in last_run, setting to: {event_pull_start_date.isoformat()}") + + else: + input_params["gte"] = last_run["event_pull_start_date"] + demisto.debug(f"[cyble_events] event_pull_start_date found in last_run: {input_params['gte']}") + + input_params["lte"] = datetime.utcnow().astimezone().isoformat() + + fetch_services = get_fetch_service_list(client, incident_collections, url, token) + + demisto.debug(f"[cyble_events] Retrieved fetch_services: {fetch_services}") + + fetch_severities = get_fetch_severities(incident_severity) + demisto.debug(f"[cyble_events] Retrieved fetch_severities: {fetch_severities}") + + input_params.update( + { + "severity": fetch_severities, + "take": input_params["limit"], + "services": fetch_services or [], + "url": url, + "hce": hide_cvv_expiry, + "api_key": token, + "lte": input_params["lte"], + "gte": input_params["gte"], + } ) - return CommandResults( - readable_output=markdown, - outputs_prefix="CybleEvents.ServiceList", - raw_response=service_name_list, - outputs=service_name_list, + demisto.debug(f"[cyble_events] Final input_params after update: {json.dumps(input_params)}") + + all_alerts, latest_created_time = migrate_data(client, input_params, False) + demisto.debug( + f"[cyble_events] migrate_data returned {len(all_alerts)} alerts, latest_created_time: {latest_created_time.isoformat()}" ) + last_run = {"event_pull_start_date": latest_created_time.astimezone().isoformat()} + demisto.debug(f"[cyble_events] Updated last_run: {last_run}") -def cyble_alert_group(client, method, token, url, args): - """ - Call the client module to fetch alert group using the input parameters - Args: - client: instance of client to communicate with server - method: Requests method to be used - token: API access token - url: URL - input_params: input parameter for api + return all_alerts, last_run - Returns: alert group from server - """ +def get_modified_remote_data_command(client, url, token, args, hide_cvv_expiry, incident_collections, incident_severity): + demisto.debug("[get-modified-remote-data] Starting command...") + + try: + remote_args = GetModifiedRemoteDataArgs(args) + last_update = dateparser.parse(remote_args.last_update, settings={"TIMEZONE": "UTC"}) + + if last_update is None: + demisto.error("[get-modified-remote-data] last_update is None after parsing") + return GetModifiedRemoteDataResponse([]) - input_params_alerts_group: dict[str, Any] = { - "orderBy": [{"created_at": args.get("order_by", "asc")}], - "skip": arg_to_number(args.get("from", 0)), - "take": arg_to_number(args.get("limit", 10)), - "include": {"tags": True}, + if last_update.tzinfo is None: + last_update = last_update.replace(tzinfo=pytz.UTC) + else: + last_update = last_update.astimezone(pytz.UTC) + + except Exception as e: + return_error(f"[get-modified-remote-data] Error parsing last_update: {e}") + + services = get_fetch_service_list(client, incident_collections, url, token) + severities = get_fetch_severities(incident_severity) + + if last_update is None: + raise ValueError("Missing required parameter: 'last_update' must not be None") + + input_params = { + "order_by": args.get("order_by", "asc"), + "skip": 0, + "limit": MAX_ALERTS, + "take": MAX_ALERTS, + "url": url, + "api_key": token, + "hce": hide_cvv_expiry, + "services": services or [], + "severity": severities or [], + "gte": last_update.isoformat(), + "lte": datetime.utcnow().replace(tzinfo=pytz.UTC).isoformat(), } + ids = client.get_ids_with_retry(service=services, input_params=input_params, is_update=True) - if args.get("start_date", "") and args.get("end_date", ""): - input_params_alerts_group["where"] = {} - input_params_alerts_group["where"]["created_at"] = {} + if isinstance(ids, list): + return GetModifiedRemoteDataResponse(ids) + else: + return_error("[get-modified-remote-data] Invalid response format: Expected list of IDs") + return GetModifiedRemoteDataResponse([]) + + +SEVERITY_MAP = {"LOW": 1, "MEDIUM": 2, "HIGH": 3} + +REVERSE_INCIDENT_STATUS = { + "UNREVIEWED": "Unreviewed", + "VIEWED": "Viewed", + "FALSE_POSITIVE": "False Positive", + "FALSE POSITIVE": "False Positive", + "CONFIRMED_INCIDENT": "Confirmed Incident", + "CONFIRMED INCIDENT": "Confirmed Incident", + "UNDER_REVIEW": "Under Review", + "UNDER REVIEW": "Under Review", + "INFORMATIONAL": "Informational", + "RESOLVED": "Resolved", + "REMEDIATION_IN_PROGRESS": "Remediation in Progress", + "REMEDIATION IN PROGRESS": "Remediation in Progress", + "REMEDIATION_NOT_REQUIRED": "Remediation not Required", + "REMEDIATION NOT REQUIRED": "Remediation not Required", +} - if args.get("start_date", ""): - input_params_alerts_group["where"]["created_at"]["gte"] = ( - datetime.strptime(args.get("start_date", ""), "%Y-%m-%dT%H:%M:%S%z").astimezone().isoformat() - ) - if args.get("end_date", ""): - input_params_alerts_group["where"]["created_at"]["lte"] = ( - datetime.strptime(args.get("end_date", ""), "%Y-%m-%dT%H:%M:%S%z").astimezone().isoformat() +def get_remote_data_command(client, url, token, args, incident_collections, incident_severity, hide_cvv_expiry): + demisto.debug("[get-remote-data] Starting command") + + try: + remote_args = GetRemoteDataArgs(args) + alert_id = remote_args.remote_incident_id + demisto.debug(f"[get-remote-data] Parsed alert_id: {alert_id}") + except Exception as e: + return_error(f"[get-remote-data] Invalid arguments: {e}") + return None + + try: + updated_incident = get_alert_payload_by_id( + client=client, + alert_id=alert_id, + token=token, + url=url, + incident_collections=incident_collections, + incident_severity=incident_severity, + hide_cvv_expiry=hide_cvv_expiry, ) + except Exception as e: + demisto.error(f"[get-remote-data] Failed to fetch alert payload: {e}") + return_error(f"[get-remote-data] Failed to fetch alert payload: {e}") + return None - alert_groups = set_request(client, method, token, input_params_alerts_group, url) - lst_alert_group = [] + if not updated_incident: + demisto.debug("[get-remote-data] No incident payload returned") + return GetRemoteDataResponse(mirrored_object={}, entries=[]) - if alert_groups: - for alert_group in alert_groups: - lst_alert_group.append( - { - "service": "{}".format(alert_group["service"]), - "keyword": "{}".format(alert_group["metadata"]["entity"]["keyword"]["tag_name"]), - "alert_group_id": "{}".format(alert_group["id"]), - "severity": "{}".format(alert_group["severity"]), - "status": "{}".format(alert_group["status"]), - "total_alerts": "{}".format(alert_group["total_alerts"]), - "created_at": "{}".format(alert_group["created_at"]), - } - ) + demisto.debug("[get-remote-data] Payload successfully retrieved") - markdown = tableToMarkdown( - "Alerts Group Details:", - lst_alert_group, - ) + severity = updated_incident.get("severity") + if severity is not None: + demisto.debug(f"[get-remote-data] Received severity: {severity}") + else: + demisto.debug("[get-remote-data] Missing severity field in incident payload") - return CommandResults( - readable_output=markdown, - outputs_prefix="CybleEvents.AlertsGroup", - raw_response=lst_alert_group, - outputs=lst_alert_group, - ) + # Map status from Cyble to human-readable format + status = updated_incident.get("status") + demisto.debug(f"[get-remote-data] status before : {status}") + + if status: + status = status.upper() + demisto.debug(f"[get-remote-data] status upper: {status}") + else: + demisto.debug("[get-remote-data] status is None or empty, skipping upper conversion") + + if status in REVERSE_INCIDENT_STATUS: + updated_incident["cybleeventsv2status"] = REVERSE_INCIDENT_STATUS[status] + demisto.debug(f"[get-remote-data] Received status: {REVERSE_INCIDENT_STATUS[status]}") else: - return CommandResults(readable_output="There aren't alerts.") + demisto.debug(f"[get-remote-data] Unknown status received: {status}") + demisto.debug(f"[get-remote-data] updated_incident: {updated_incident}") + + return GetRemoteDataResponse(mirrored_object=updated_incident, entries=[]) + + +def manual_fetch(client, args, token, url, incident_collections, incident_severity): + demisto.debug("[manual_fetch] Manual run detected") + + gte = args.get("start_date") + lte = args.get("end_date") or datetime.utcnow().astimezone().isoformat() + + try: + gte = datetime.fromisoformat(gte).isoformat() + lte = datetime.fromisoformat(lte).isoformat() + except ValueError as e: + raise DemistoException(f"[manual_fetch] Invalid date format: {e}") + + services = get_fetch_service_list(client, incident_collections, url, token) or [] + + # Build the payload to be passed to the API, excluding transport-related values + api_input_params = { + "gte": gte, + "lte": lte, + "severity": get_fetch_severities(incident_severity), + "order_by": args.get("order_by", "asc"), + "skip": 0, + "take": int(args.get("limit", DEFAULT_TAKE_LIMIT)), + } + + alerts = fetch_few_alerts(client, api_input_params, services, url, token, is_update=False) or [] + + return alerts + + +def update_remote_system(client, method, token, args, url): + """ + Pushes status or severity changes to Cyble Vision for bi-directional mirroring. + + Args: + client: Client instance + method: HTTP method (unused here) + token: API key for Cyble Vision + args: Incoming args from Cortex XSOAR + url: Cyble Vision API endpoint + + Returns: + str: ID of updated incident + """ + try: + parsed_args = UpdateRemoteSystemArgs(args) + incident_id = parsed_args.remote_incident_id or parsed_args.data.get("id") + + if not incident_id: + return_error("[update_remote_system] Missing incident ID, cannot update") + + demisto.debug(f"[update_remote_system] Parsed args: [{parsed_args.__dict__}]") + + if not parsed_args.delta: + demisto.debug(f"[update_remote_system] No delta provided for incident [{incident_id}], skipping update.") + return incident_id + + service = parsed_args.data.get("service") + if not service: + demisto.debug(f"[update_remote_system] No service found for incident [{incident_id}], cannot update.") + return incident_id + + demisto.debug(f"[update_remote_system] Delta received: {parsed_args.delta}") + + update_payload = {"id": incident_id, "service": service} + + # Handle status + status = parsed_args.delta.get("status") + + if status: + mapped_status = INCIDENT_STATUS.get(status) + if mapped_status: + update_payload["status"] = mapped_status + demisto.debug(f"[update_remote_system] mapped status : {mapped_status}") + else: + demisto.debug(f"[update_remote_system] Unmapped status received in delta: {status}") + + # Handle severity conversion + severity = parsed_args.delta.get("severity") + if severity is not None: + try: + severity = float(severity) + if severity in (0, 0.5, 1): + update_payload["user_severity"] = "LOW" + elif severity == 2: + update_payload["user_severity"] = "MEDIUM" + elif severity in (3, 4): + update_payload["user_severity"] = "HIGH" + else: + demisto.debug(f"[update_remote_system] Severity value [{severity}] does not map to known levels.") + except ValueError: + demisto.debug(f"[update_remote_system] Invalid numeric severity: {severity}") + + # If no valid fields beyond ID and service, skip + if len(update_payload) <= 2: + demisto.debug(f"[update_remote_system] No valid status or severity to update for incident [{incident_id}].") + return incident_id + + final_payload = {"alerts": [update_payload]} + demisto.debug(f"[update_remote_system] Sending update payload: {final_payload}") + + client.update_alert(final_payload, url, token) + + return incident_id + + except Exception as e: + return_error(f"[update_remote_system] Failed to update alert: {str(e)}") + + +def get_mapping_fields(client, token, url): + """ + Defines fields available for outgoing mirroring to Cyble Vision. + + Args: + client: Client instance + token: API token + url: API endpoint + + Returns: + GetMappingFieldsResponse: Field structure for outgoing mapper + """ + incident_type_scheme = SchemeTypeMapping(type_name="cyble_outgoing_mapper") + + incident_type_scheme.add_field("status", "The status of the alert in Cyble Vision") + incident_type_scheme.add_field("severity", "The severity of the alert in Cyble Vision") + + return GetMappingFieldsResponse([incident_type_scheme]) def cyble_fetch_iocs(client, method, token, args, url): @@ -702,12 +1303,24 @@ def cyble_fetch_iocs(client, method, token, args, url): return command_results -def main(): # pragma: no cover +def main(): """ - PARSE AND VALIDATE INTEGRATION PARAMS + Main function to execute Cyble Events commands in Cortex XSOAR. + + This function initializes the client using parameters provided in the + integration settings, and executes commands based on the input from + the Cortex XSOAR platform. Commands supported include testing the + integration, fetching incidents, updating remote systems, fetching + mapping fields, and various Cyble Vision specific commands such as + fetching subscribed services, alert groups, IOCs, and alerts. + + Raises: + NotImplementedError: If a command is not implemented. + Exception: If there is an error executing a command. + + Returns: None """ - # get the service API url params = demisto.params() base_url = params.get("base_url") token = demisto.params().get("credentials", {}).get("password", "") @@ -725,11 +1338,10 @@ def main(): # pragma: no cover args = demisto.args() if command == "test-module": - # request was successful - return_results(test_response(client, "GET", base_url, token)) + url = base_url + str(ROUTES[COMMAND[command]]) + return_results(test_response(client, "GET", url, token)) elif command == "fetch-incidents": - # This is the call made when cyble-fetch-events command. last_run = demisto.getLastRun() url = base_url + str(ROUTES[COMMAND[command]]) @@ -741,59 +1353,57 @@ def main(): # pragma: no cover demisto.incidents(data) elif command == "update-remote-system": - # Updates changes in incidents to remote system if mirror: url = base_url + str(ROUTES[COMMAND[command]]) return_results(update_remote_system(client, "PUT", token, args, url)) - return elif command == "get-mapping-fields": - # Fetches mapping fields for outgoing mapper url = base_url + str(ROUTES[COMMAND[command]]) - return_results(get_mapping_fields(client, token, url)) elif command == "cyble-vision-subscribed-services": - # This is the call made when subscribed-services command. return_results(fetch_subscribed_services_alert(client, "GET", base_url, token)) - elif command == "cyble-vision-fetch-alert-groups": - # Fetch alert group. - - validate_input(args, False) - url = base_url + str(ROUTES[COMMAND[command]]) - return_results(cyble_alert_group(client, "POST", token, url, args)) - elif command == "cyble-vision-fetch-iocs": - # This is the call made when cyble-vision-v2-fetch-iocs command. - - validate_input(args, True) + validate_iocs_input(args) url = base_url + str(ROUTES[COMMAND[command]]) command_results = cyble_fetch_iocs(client, "GET", token, args, url) - return_results(command_results) elif command == "cyble-vision-fetch-alerts": - # This is the call made when cyble-vision-v2-fetch-alerts command. - url = base_url + str(ROUTES[COMMAND[command]]) - lst_alerts, next_run = cyble_events( + lst_alerts = cyble_events( client, "POST", token, url, args, {}, hide_cvv_expiry, incident_collections, incident_severity, True ) - - markdown = tableToMarkdown("Alerts Details:", lst_alerts) - return_results( CommandResults( - readable_output=markdown, outputs_prefix="CybleEvents.Alerts", raw_response=lst_alerts, outputs=lst_alerts + readable_output="Fetched alerts successfully.", + outputs_prefix="CybleEvents.Alerts", + raw_response=lst_alerts, + outputs=lst_alerts, ) ) + + elif command == "get-modified-remote-data": + url = base_url + str(ROUTES[COMMAND[command]]) + return_results( + get_modified_remote_data_command( + client, url, token, args, hide_cvv_expiry, incident_collections, incident_severity + ) + ) + + elif command == "get-remote-data": + url = base_url + str(ROUTES[COMMAND[command]]) + return_results( + get_remote_data_command(client, url, token, args, incident_collections, incident_severity, hide_cvv_expiry) + ) + else: raise NotImplementedError(f"{command} command is not implemented.") except Exception as e: - return_error(f"Failed to execute {command} command. Error: {e!s}") + return_error(f"Failed to execute {command} command. Error: {str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): From 7233a91a0cad92b631507a999de91cf378c246dd Mon Sep 17 00:00:00 2001 From: Bar Date: Wed, 23 Jul 2025 12:19:27 +0300 Subject: [PATCH 03/10] delete duplication from merge --- .../CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py index 246737218105..1902cc14a67a 100644 --- a/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py +++ b/Packs/CybleEventsV2/Integrations/CybleEventsV2/CybleEventsV2.py @@ -1382,10 +1382,6 @@ def main(): outputs_prefix="CybleEvents.Alerts", raw_response=lst_alerts, outputs=lst_alerts, - readable_output="Fetched alerts successfully.", - outputs_prefix="CybleEvents.Alerts", - raw_response=lst_alerts, - outputs=lst_alerts, ) ) From 41e2265571b7eab90c54a3aeb2883be377deb576 Mon Sep 17 00:00:00 2001 From: Bar Date: Wed, 23 Jul 2025 16:33:40 +0300 Subject: [PATCH 04/10] add RN --- Packs/CybleEventsV2/ReleaseNotes/1_1_2.md | 6 ++++++ Packs/CybleEventsV2/pack_metadata.json | 2 +- Packs/Gem/ReleaseNotes/1_0_6.md | 6 ++++++ Packs/Gem/pack_metadata.json | 2 +- Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md | 6 ++++++ Packs/QutteraWebsiteMalwareScanner/pack_metadata.json | 2 +- 6 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Packs/CybleEventsV2/ReleaseNotes/1_1_2.md create mode 100644 Packs/Gem/ReleaseNotes/1_0_6.md create mode 100644 Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md diff --git a/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md b/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md new file mode 100644 index 000000000000..ee39d8b0e1eb --- /dev/null +++ b/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### CybleEvents v2 + +- logging improvements. diff --git a/Packs/CybleEventsV2/pack_metadata.json b/Packs/CybleEventsV2/pack_metadata.json index 784ae3cc5bcc..c817b3c802c9 100644 --- a/Packs/CybleEventsV2/pack_metadata.json +++ b/Packs/CybleEventsV2/pack_metadata.json @@ -2,7 +2,7 @@ "name": "CybleEventsV2", "description": "Cyble Events for Vision Users. Must have Vision API access to use the threat intelligence.", "support": "partner", - "currentVersion": "1.1.1", + "currentVersion": "1.1.2", "author": "Cyble Info Sec", "url": "https://cyble.com/", "email": "", diff --git a/Packs/Gem/ReleaseNotes/1_0_6.md b/Packs/Gem/ReleaseNotes/1_0_6.md new file mode 100644 index 000000000000..bbcc1711f2ec --- /dev/null +++ b/Packs/Gem/ReleaseNotes/1_0_6.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Gem + +- logging improvements. diff --git a/Packs/Gem/pack_metadata.json b/Packs/Gem/pack_metadata.json index 1d5f3668420a..b2d8a9bba5e1 100644 --- a/Packs/Gem/pack_metadata.json +++ b/Packs/Gem/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Gem", "description": "Integrate with Gem to use alerts as a trigger for Cortex XSOAR’s custom playbooks, and automate response to specific TTPs and scenarios.", "support": "partner", - "currentVersion": "1.0.5", + "currentVersion": "1.0.6", "author": "Gem Security", "url": "https://gem.security/", "email": "support@gem.security", diff --git a/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md new file mode 100644 index 000000000000..1930970a6204 --- /dev/null +++ b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### QutteraWebsiteMalwareScanner + +- logging improvements. diff --git a/Packs/QutteraWebsiteMalwareScanner/pack_metadata.json b/Packs/QutteraWebsiteMalwareScanner/pack_metadata.json index 5289dafe8003..e8e77caf3122 100644 --- a/Packs/QutteraWebsiteMalwareScanner/pack_metadata.json +++ b/Packs/QutteraWebsiteMalwareScanner/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Quttera Website Malware Scanner", "description": "Detect suspicious/malicious/blocklisted content on domains/URLs. Run real-time normal/heuristic scan and database queries.", "support": "partner", - "currentVersion": "1.0.20", + "currentVersion": "1.0.21", "author": "Quttera LTD", "url": "https://scannerapi.quttera.com/api/v3", "email": "support@quttera.com", From 3bc22890f2d430b6b10a6f096887c1fbd6de6064 Mon Sep 17 00:00:00 2001 From: Bar Gali <75535203+BarGali@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:44:06 +0300 Subject: [PATCH 05/10] Update Packs/CybleEventsV2/ReleaseNotes/1_1_2.md Co-authored-by: Shachar Kidor <82749224+ShacharKidor@users.noreply.github.com> --- Packs/CybleEventsV2/ReleaseNotes/1_1_2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md b/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md index ee39d8b0e1eb..a52a3dc15750 100644 --- a/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md +++ b/Packs/CybleEventsV2/ReleaseNotes/1_1_2.md @@ -3,4 +3,4 @@ ##### CybleEvents v2 -- logging improvements. +- Logging improvements. From 33c29f6cfa24d3e79038924b7c6a341d13db25f8 Mon Sep 17 00:00:00 2001 From: Bar Gali <75535203+BarGali@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:44:14 +0300 Subject: [PATCH 06/10] Update Packs/Gem/ReleaseNotes/1_0_6.md Co-authored-by: Shachar Kidor <82749224+ShacharKidor@users.noreply.github.com> --- Packs/Gem/ReleaseNotes/1_0_6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/Gem/ReleaseNotes/1_0_6.md b/Packs/Gem/ReleaseNotes/1_0_6.md index bbcc1711f2ec..f5c35cf04368 100644 --- a/Packs/Gem/ReleaseNotes/1_0_6.md +++ b/Packs/Gem/ReleaseNotes/1_0_6.md @@ -3,4 +3,4 @@ ##### Gem -- logging improvements. +- Logging improvements. From f87894442497d62e982a102ad98eb3a430e46ae9 Mon Sep 17 00:00:00 2001 From: Bar Gali <75535203+BarGali@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:44:21 +0300 Subject: [PATCH 07/10] Update Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md Co-authored-by: Shachar Kidor <82749224+ShacharKidor@users.noreply.github.com> --- Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md index 1930970a6204..5c660f4eaf86 100644 --- a/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md +++ b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md @@ -3,4 +3,4 @@ ##### QutteraWebsiteMalwareScanner -- logging improvements. +- Logging improvements. From 130353d4d40d8369cc7fc69f92d61a7e4be935ff Mon Sep 17 00:00:00 2001 From: Content Bot Date: Thu, 24 Jul 2025 11:44:44 +0000 Subject: [PATCH 08/10] Bump pack from version Gem to 1.0.7. --- Packs/Gem/ReleaseNotes/1_0_7.md | 6 ++++++ Packs/Gem/pack_metadata.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Packs/Gem/ReleaseNotes/1_0_7.md diff --git a/Packs/Gem/ReleaseNotes/1_0_7.md b/Packs/Gem/ReleaseNotes/1_0_7.md new file mode 100644 index 000000000000..f5c35cf04368 --- /dev/null +++ b/Packs/Gem/ReleaseNotes/1_0_7.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Gem + +- Logging improvements. diff --git a/Packs/Gem/pack_metadata.json b/Packs/Gem/pack_metadata.json index b2d8a9bba5e1..05d21ea2d65a 100644 --- a/Packs/Gem/pack_metadata.json +++ b/Packs/Gem/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Gem", "description": "Integrate with Gem to use alerts as a trigger for Cortex XSOAR’s custom playbooks, and automate response to specific TTPs and scenarios.", "support": "partner", - "currentVersion": "1.0.6", + "currentVersion": "1.0.7", "author": "Gem Security", "url": "https://gem.security/", "email": "support@gem.security", From 54b5d75807fdfdeabee829daf6ef96a19efb6618 Mon Sep 17 00:00:00 2001 From: Bar Date: Sun, 27 Jul 2025 10:59:17 +0300 Subject: [PATCH 09/10] fixes for pre-commit --- Packs/Gem/Integrations/Gem/Gem.yml | 1 + .../QutteraWebsiteMalwareScanner.yml | 5 ++++- Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Packs/Gem/Integrations/Gem/Gem.yml b/Packs/Gem/Integrations/Gem/Gem.yml index 7cf121f8c6c6..344debd596f9 100644 --- a/Packs/Gem/Integrations/Gem/Gem.yml +++ b/Packs/Gem/Integrations/Gem/Gem.yml @@ -36,6 +36,7 @@ configuration: name: first_fetch required: false type: 0 + section: Collect - display: Use system proxy settings name: proxy type: 8 diff --git a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml index 229d5ac7e434..6237b6a0db9f 100644 --- a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml +++ b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml @@ -1,3 +1,6 @@ +sectionOrder: +- Connect +- Collect commonfields: id: QutteraWebsiteMalwareScanner version: -1 @@ -23,7 +26,7 @@ configuration: script: type: python subtype: python3 - dockerimage: demisto/python3:3.11.10.115186 + dockerimage: demisto/python3:3.12.11.4208709 script: '' commands: - name: quttera-scan-start diff --git a/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md index 5c660f4eaf86..b5bf1dc94557 100644 --- a/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md +++ b/Packs/QutteraWebsiteMalwareScanner/ReleaseNotes/1_0_21.md @@ -2,5 +2,6 @@ #### Integrations ##### QutteraWebsiteMalwareScanner +- Updated the Docker image to: *demisto/python3:3.12.11.4208709*. - Logging improvements. From fc1a868686e351cffdc882237cd9ea540d9e343d Mon Sep 17 00:00:00 2001 From: Bar Date: Sun, 27 Jul 2025 15:40:39 +0300 Subject: [PATCH 10/10] fixes for pre-commit --- .../QutteraWebsiteMalwareScanner.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml index 6237b6a0db9f..2af0f744dbc1 100644 --- a/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml +++ b/Packs/QutteraWebsiteMalwareScanner/Integrations/QutteraWebsiteMalwareScanner/QutteraWebsiteMalwareScanner.yml @@ -18,11 +18,13 @@ configuration: name: apikey required: true type: 4 + section: Connect - display: Quttera Scanner URL name: base_url required: true defaultvalue: "https://scannerapi.quttera.com" type: 0 + section: Connect script: type: python subtype: python3