RecordedFutureAlerts
Fetch and triage alerts from Recorded Future.
Data Enrichment & Threat Intelligence · Recorded Future
Details
| ID | RecordedFutureAlerts |
|---|---|
| Provider | Mastercard |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.1.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Recorded Future Alerts
Fetch and triage Recorded Future Classic Alerts and Recorded Future Playbook Alerts directly from Cortex
XSOAR.
The integration allows you to:
- Search and fetch alerts from the Recorded Future platform.
- Look up full alert details for a single Classic Alert or Playbook Alert by ID.
- Update alert status, assignee and comment/note from inside XSOAR.
- Automatically fetch screenshots that accompany the alert.
Migration from Recorded Future Intelligence pack
Guide: migrating from Recorded Future Intelligence pack
Configure Recorded Future Alerts in Cortex
| Parameter | Description | Required |
|---|---|---|
| Fetch incidents | Turn on incident fetching. When enabled, the integration will poll Recorded Future at the defined interval and create / update incidents for new or updated alerts. | False |
| Incident type | Incident type will be set by this field if a Classifier does not exist. If a Classifier is selected, it will take precedence. Leave empty to let the built-in classifier decide based on the alert subtype. | False |
| Your server URL | Base URL for the Recorded Future XSOAR gateway API. The default value https://api.recordedfuture.com/gw/xsoar/. |
True |
| API Key | Recorded Future user API token used to authenticate the requests. | True |
| Source Reliability | How trustworthy Recorded Future should be considered when the integration sets DBot scores. | False |
| Incidents fetch interval | How often to poll for new alerts. | False |
| Maximum number of incidents per fetch | Hard cap on the number of alerts to pull in a single fetch cycle (maximum 50, due to API limits). | False |
| First fetch time | How far back to look on the very first fetch run. Maximum look-back is 90 days. | False |
| Enable Classic Alerts | Toggle fetching of Classic Alerts. Disable if you only need Playbook Alerts. | False |
| Classic Alerts: Rule names to fetch | Semicolon-separated Classic Alert rule names to include (e.g., Malware;Typosquat). Leave blank to fetch alerts from all rules. |
False |
| Classic Alerts: Statuses to fetch | Classic Alert statuses to be fetched. Choose one or more of New, InProgress, Resolved, Dismissed. | True |
| Enable Playbook Alerts | Toggle fetching of Playbook Alerts. Disable if you only need Classic Alerts. | False |
| Playbook Alerts: Priority to fetch | Minimum priority threshold. Alerts with lower priority than selected value will not be fetched. Possible values: Informational, Moderate, High. | False |
| Playbook Alerts: Categories to fetch | Comma-separated list of Playbook Alert categories to include (e.g., domain_abuse,cyber_vulnerability). Leave blank to fetch all categories available to your licence. |
False |
| Playbook Alerts: Statuses to fetch | Playbook Alert statuses to be fetched (choose one or more of New, InProgress, Resolved, Dismissed). | True |
| Trust any certificate (not secure) | Skip TLS certificate validation. For example, enable this when using a proxy that re-signs SSL traffic or for testing with self-signed certs. | False |
| Use system proxy settings | Route all HTTP/S requests through the system-wide proxy settings configured in Cortex XSOAR. | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
rf-alerts
List Classic or Playbook alerts.
Base Command
rf-alerts
Input
| Argument Name | Description | Required |
|---|---|---|
| include_classic_alerts | Whether classic alerts should be included in the response. Possible values are: true, false. Default is true. | Optional |
| include_playbook_alerts | Whether playbook alerts should be included in the response. Possible values are: true, false. Default is true. | Optional |
| classic_alert_rule_ids | Comma-separated Classic Alert Rule IDs. Only applied to Classic Alert search. | Optional |
| playbook_alert_categories | Comma-separated Playbook Alert categories. Only applied to Playbook Alert search. Possible values are: domain_abuse, cyber_vulnerability, code_repo_leakage, third_party_risk, geopolitics_facility. | Optional |
| playbook_alert_priorities | Comma-separated Playbook Alert priorities. Only applied to Playbook Alert search. Possible values are: Informational, Moderate, High. | Optional |
| statuses | Comma-separated list of statuses to include. Possible values are: New, InProgress, Resolved, Dismissed. | Optional |
| limit | Maximum number of alerts to return. Maximum allowed value is 50. Default is 10. | Optional |
| order_by | Field to sort by. Possible values are: created_at, updated_at. Default value is updated_at. | Optional |
| order_direction | Direction to sort by. Possible values are: asc, desc. Default value is desc. | Optional |
| created_from | Return only alerts created on or after this datetime (ex. “2025-05-17T16:06:00Z”). | Optional |
| created_to | Return only alerts created on or before this datetime (ex. “2025-05-17T16:06:00Z”). | Optional |
| updated_from | Return only alerts updated on or after this datetime (ex. “2025-05-17T16:06:00Z”). | Optional |
| updated_to | Return only alerts updated on or before this datetime (ex. “2025-05-17T16:06:00Z”). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| RecordedFutureAlerts.Alert.id | string | Unique id of the alert in Recorded Future. |
| RecordedFutureAlerts.Alert.title | string | Title of the alert. |
| RecordedFutureAlerts.Alert.type | string | Alert type (classic-alert / playbook-alert). |
| RecordedFutureAlerts.Alert.subtype | string | Alert subtype (domain_abuse / cyber_vulnerability / code_repo_leakage / third_party_risk / geopolitics_facility / classic-alert). |
| RecordedFutureAlerts.Alert.status | string | Status of the alert. |
| RecordedFutureAlerts.Alert.created | string | When the alert was created as an ISO8601 string. |
| RecordedFutureAlerts.Alert.updated | string | When the alert was updated as an ISO8601 string. |
| RecordedFutureAlerts.Alert.classic_alert_rule_name | string | If alert is a classic alert, this is the name of the rule that triggered the alert. |
| RecordedFutureAlerts.Alert.classic_alert_rule_id | string | If alert is a classic alert, this is the name of the rule that triggered the alert. |
| RecordedFutureAlerts.Alert.playbook_alert_category | string | If alert is a playbook alert, this is the category of the alert. |
| RecordedFutureAlerts.Alert.playbook_alert_priority | string | If alert is a playbook alert, this is the priority of the alert. |
Command Example
!rf-alerts include_classic_alerts=false playbook_alert_categories=domain_abuse playbook_alert_priorities=High statuses=New limit=5 order_by=updated_at order_direction=desc created_from="2025-05-17T12:06:00Z"
Context Example
{
"RecordedFutureAlerts": {
"Alert": [
{
"id": "task:fc34c790-293b-42bd-8f23-c1f571323f8f",
"title": "Potential Typosquat of example.com",
"type": "playbook-alert",
"subtype": "domain_abuse",
"status": "New",
"created": "2025-05-17T16:06:00Z",
"updated": "2025-05-17T17:14:12Z",
"playbook_alert_category": "domain_abuse",
"playbook_alert_priority": "High",
"classic_alert_rule_name": null,
"classic_alert_rule_id": null
},
{
"id": "7SKZ26",
"title": "ClassiAlert",
"type": "classic-alert",
"subtype": "classic-alert",
"status": "New",
"created": "2025-05-17T15:58:30Z",
"updated": "2025-05-17T16:40:00Z",
"classic_alert_rule_name": "Alert rule name 1",
"classic_alert_rule_id": "fDasdfwea",
"playbook_alert_category": null,
"playbook_alert_priority": null
}
]
}
}
rf-alert-lookup
Look up a single Recorded Future alert by ID.
Base Command
rf-alert-lookup
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | ID of the alert. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| RecordedFutureAlerts.Alert.id | string | Unique ID of the alert in Recorded Future. |
| RecordedFutureAlerts.Alert.title | string | Title of the alert. |
| RecordedFutureAlerts.Alert.type | string | Alert type (classic-alert / playbook-alert). |
| RecordedFutureAlerts.Alert.subtype | string | Alert subtype / category. |
| RecordedFutureAlerts.Alert.status | string | Current status of the alert. |
| RecordedFutureAlerts.Alert.created | string | Timestamp when the alert was created in ISO 8601 format, for example, 2025-05-17T16:06:00Z. |
| RecordedFutureAlerts.Alert.updated | string | Timestamp when the alert was last updated in ISO 8601 format, for example, 2025-05-17T16:06:00Z. |
| RecordedFutureAlerts.Alert.markdown | string | Markdown-formatted summary of the alert details. |
| RecordedFutureAlerts.Alert.images | Unknown | Image IDs attached to the alert. |
| RecordedFutureAlerts.Alert.classic_alert_rule_name | string | If alert is a Classic Alert, name of the rule that triggered the alert. |
| RecordedFutureAlerts.Alert.classic_alert_rule_id | string | If alert is a Classic Alert, ID of the rule that triggered the alert. |
| RecordedFutureAlerts.Alert.portal_url | string | If alert is a Classic Alert, portal URL for the alert. |
| RecordedFutureAlerts.Alert.ai_insights | string | If alert is a Classic Alert, AI-generated insight summary. |
| RecordedFutureAlerts.Alert.review.status | string | If alert is a Classic Alert, review status. |
| RecordedFutureAlerts.Alert.review.assignee | string | If alert is a Classic Alert, assignee. |
| RecordedFutureAlerts.Alert.review.note | string | If alert is a Classic Alert, review note. |
| RecordedFutureAlerts.Alert.entities | Unknown | If alert is a Classic Alert, entities referenced in the alert. |
| RecordedFutureAlerts.Alert.fragments | Unknown | If alert is a Classic Alert, text fragments from the alert hits. |
| RecordedFutureAlerts.Alert.documents | Unknown | If alert is a Classic Alert, documents associated with alert hits. |
| RecordedFutureAlerts.Alert.analyst_notes | Unknown | If alert is a Classic Alert, analyst-note URLs linked in the alert. |
| RecordedFutureAlerts.Alert.triggered_by | Unknown | If alert is a Classic Alert, entities or paths that triggered the alert. |
| RecordedFutureAlerts.Alert.playbook_alert_category | string | If alert is a Playbook Alert, category of the alert. |
| RecordedFutureAlerts.Alert.playbook_alert_priority | string | If alert is a Playbook Alert, priority of the alert. |
| RecordedFutureAlerts.Alert.entity_name | string | If alert is a Playbook Alert, primary entity name. |
| RecordedFutureAlerts.Alert.targets | Unknown | If alert is a Playbook Alert, target names associated with the alert. |
| RecordedFutureAlerts.Alert.log_entries | Unknown | If alert is a Playbook Alert, alert activity log entries. |
| RecordedFutureAlerts.Alert.log_entries.created | string | If alert is a Playbook Alert, timestamp when the log entry was created in ISO 8601 format, for example, 2025-05-17T16:06:00Z. |
| RecordedFutureAlerts.Alert.log_entries.author_name | string | If alert is a Playbook Alert, name of the log entry author. |
| RecordedFutureAlerts.Alert.log_entries.author_id | string | If alert is a Playbook Alert, ID of the log entry author. |
| RecordedFutureAlerts.Alert.log_entries.changes | Unknown | If alert is a Playbook Alert, raw change records from the alert activity log. |
| RecordedFutureAlerts.Alert.details | Unknown | If alert is a Playbook Alert, category-specific detailed panel data. |
Command Example
!rf-alert-lookup alert_id=task:75ac1730-3811-44be-8ea7-de7916166c10
Context Example
{
"RecordedFutureAlerts": {
"Alert": {
"id": "task:75ac1730-3811-44be-8ea7-de7916166c10",
"title": "RF Playbook Alert - Vulnerability CVE-2021-44228",
"type": "playbook-alert",
"subtype": "cyber_vulnerability",
"status": "New",
"created": "2025-05-14T02:31:00Z",
"updated": "2025-05-14T03:40:10Z",
"markdown": "## Vulnerability\n\n### Summary\n\n**ID:** task:75ac1730-3811-44be-8ea7-de7916166c10",
"playbook_alert_category": "cyber_vulnerability",
"playbook_alert_priority": "Moderate",
"entity_name": "CVE-2021-44228",
"log_entries": [
{
"created": "2025-05-14T02:36:04Z",
"author_name": "RF PI test Phantom modules",
"author_id": "uhash:60ptFTXURB",
"changes": [
{
"type": "status_change",
"old": "New",
"new": "Resolved"
},
{
"type": "comment_change",
"comment": "This has been handled."
}
]
}
],
"details": {
"type": "cyber_vulnerability",
"lifecycle_stage": "Exploitation"
}
}
}
}
rf-alert-update
Update an alert in the Recorded Future platform.
Base Command
rf-alert-update
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | ID of alert to update. | Required |
| status | New status to set for the alert. Possible values are: New, InProgress, Dismissed, Resolved. | Optional |
| comment | Add comment / Replace note. | Optional |
| reopen | Only for Playbook Alerts. Set the reopen strategy for the alert. Reopen on significant updates or keep the alert Resolved. Can only be used with status=Resolved. Possible values are: never, significant_updates. Default: reopen on significant updates. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| RecordedFutureAlerts.Alert.id | string | Unique id of the alert in Recorded Future. |
| RecordedFutureAlerts.Alert.type | string | Alert type (classic-alert / playbook-alert). |
| RecordedFutureAlerts.Alert.status | string | Status of alert in Recorded Future. |
| RecordedFutureAlerts.Alert.comment | string | Note (Classic) or comment (Playbook) that was just applied. |
Command Example
!rf-alert-update alert_id=task:fc34c790-293b-42bd-8f23-c1f571323f8f status=Resolved comment="Alert resolved - false-positive." reopen=never
Context Example
{
"RecordedFutureAlerts": {
"Alert": {
"id": "task:fc34c790-293b-42bd-8f23-c1f571323f8f",
"type": "playbook-alert",
"status": "Resolved",
"comment": "Alert resolved - false-positive."
}
}
}
rf-alert-rules
Search for alert rule IDs.
Base Command
rf-alert-rules
Input
| Argument Name | Description | Required |
|---|---|---|
| rule_name | Rule name to search. Can be a partial name. | Optional |
| limit | Maximum number of rules to return. Default is 10. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| RecordedFutureAlerts.AlertRule.id | string | Alert rule ID. |
| RecordedFutureAlerts.AlertRule.name | string | Alert rule name. |
Command Example
!rf-alert-rules rule_name="malware" limit=3
Context Example
{
"RecordedFutureAlerts": {
"AlertRule": [
{
"id": "mZbDYT",
"name": "Malware Communication - External IP"
},
{
"id": "mZbDZT",
"name": "Malware Communication - Suspicious Domain"
},
{
"id": "mxbDZT",
"name": "Malware Communication - Command & Control"
}
]
}
}
rf-alert-images
Fetch alert images and attach to incident in context Files.
Base Command
rf-alert-images
Command Example
!rf-alert-images
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Files | Unknown | New images are attached into incident Files. |
Configuration parameters
isFetch— Fetch incidentsincidentType— Incident typeurl— Your server URL (required)credentials— (required)integrationReliability— Source ReliabilityincidentFetchInterval— Incidents fetch intervalmax_fetch— Maximum number of incidents per fetchfirst_fetch— First fetch timeclassic_alerts_enabled— Enable Classic Alertsclassic_alert_rule_names— Classic Alerts: Rule names to fetchclassic_alert_statuses— Classic Alerts: Statuses to fetch (required)playbook_alerts_enabled— Enable Playbook Alertsplaybook_alert_priority— Playbook Alerts: Priority to fetchplaybook_alert_categories— Playbook Alerts: Categories to fetchplaybook_alert_statuses— Playbook Alerts: Statuses to fetch (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (5)
-
rf-alert-imagesFetch alert images.
-
rf-alert-lookupLook up a single Recorded Future alert by ID. Supports both Classic Alerts and Playbook Alerts. Returns full alert data including rule details, entities, AI insights, and Playbook Alert panel data.
-
rf-alert-rulesSearch for alert rule IDs.
-
rf-alert-updateUpdate an alert in the Recorded Future platform.
-
rf-alertsList Classic or Playbook alerts.
import concurrent.futures import platform from typing import Any import urllib3 import demistomock as demisto from CommonServerPython import * # noqa # pylint: disable=unused-wildcard-import from CommonServerUserPython import * # noqa # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR MAX_IMAGES_TO_FETCH = 25 STATUS_TO_RETRY = [500, 501, 502, 503, 504] __version__ = "0.1.0" TIMEOUT_60 = 60 TIMEOUT_90 = 90 TIMEOUT_120 = 120 """ CLIENT CLASS """ class Client(BaseClient): """Client class to interact with the service API""" def _request_raw( self, *, method: str, url_suffix: str, params: dict | None = None, json_data: dict | None = None, timeout: int = 90, retries: int = 3, ) -> dict: response: Any = self._http_request( method=method, url_suffix=url_suffix, params=params, json_data=json_data, timeout=timeout, retries=retries, status_list_to_retry=STATUS_TO_RETRY, ) if not isinstance(response, dict): raise DemistoException(f"Bad Response, response was not a dict: {str(response)}") if response.get("return_error"): return_error(**response["return_error"]) raise DemistoException("return_error returned unexpectedly") return response @staticmethod def _no_results_found() -> list[CommandResults]: return [ CommandResults( outputs_prefix="", outputs={}, raw_response={}, readable_output="No results found.", outputs_key_field="", ) ] @classmethod def _response_to_command_results(cls, response: dict) -> list[CommandResults]: result_actions = response.get("result_actions") if not isinstance(result_actions, list): raise DemistoException(f"Bad Response, result_actions was present but not a list: {str(response)}") command_results: list[CommandResults] = [] for result_action in result_actions: if not isinstance(result_action, dict): continue raw_response = result_action.get("raw_response") outputs = raw_response.get("outputs") if isinstance(raw_response, dict) else None command_results.append( CommandResults( outputs=outputs, **result_action, ) ) return command_results def _request_results( self, *, method: str, url_suffix: str, params: dict | None = None, json_data: dict | None = None, timeout: int = 90, retries: int = 3, ) -> list[CommandResults]: try: response = self._request_raw( method=method, url_suffix=url_suffix, params=params, json_data=json_data, timeout=timeout, retries=retries, ) except DemistoException as err: if "404" in str(err): return self._no_results_found() raise return self._response_to_command_results(response) def whoami(self) -> dict: return self._request_raw( method="GET", url_suffix="/info/whoami", timeout=60, ) def alert_update(self) -> list[CommandResults]: """Update alert""" return self._request_results( method="POST", url_suffix="/v3/alert/update", json_data=demisto.args(), timeout=90, ) def alert_search(self) -> list[CommandResults]: """Search alerts""" return self._request_results( method="GET", url_suffix="/v3/alert/search", params=demisto.args(), ) def alert_rule_search(self) -> list[CommandResults]: """Search alert rules.""" return self._request_results( method="GET", url_suffix="/v3/alert/rules", params=demisto.args(), ) def alert_lookup(self, alert_id: str) -> list[CommandResults]: return self._request_results( method="GET", url_suffix="/v3/alert/lookup", params={"alert_id": alert_id}, timeout=90, ) def get_alert_image( self, alert_type: str, alert_id: str, image_id: str, alert_subtype: str | None, ) -> bytes: """ Get an image from the v3 alert image endpoint. Returns the raw binary content of the image. """ response_content: Any = self._http_request( method="get", url_suffix="/v3/alert/image", params={ "alert_type": alert_type, "alert_subtype": alert_subtype, "alert_id": alert_id, "image_id": image_id, }, timeout=90, resp_type="content", ) return response_content def fetch_incidents(self) -> dict: """Fetch incidents.""" classic_query_params = demisto.getLastRun().get("next_query_classic", {}) playbook_query_params = demisto.getLastRun().get("next_query_playbook", {}) return self._request_raw( method="POST", url_suffix="/v3/alert/fetch", json_data={ "integration_config": demisto.params(), "classic_query_params": classic_query_params, "playbook_query_params": playbook_query_params, }, timeout=120, ) # === === === === === === === === === === === === === === === # === === === === === === ACTIONS === === === === === === === # === === === === === === === === === === === === === === === class Actions: def __init__(self, rf_client: Client): self.client = rf_client def test_module(self) -> None: # This is the call made when pressing the integration Test button. # Returning 'ok' indicates that the integration works like it suppose to and # connection to the service is successful. # Returning 'ok' will make the test result be green. # Any other response will make the test result be red. demisto_params = demisto.params() # Validate first_fetch first_fetch_str = str(demisto_params.get("first_fetch", "")) if first_fetch_str.isnumeric(): first_fetch = int(first_fetch_str) else: raise ValueError("'first_fetch' parameter must be a number") ninety_days_in_minutes = 90 * 24 * 60 if first_fetch > ninety_days_in_minutes: raise ValueError("'first_fetch' parameter cannot be bigger than 90 days") # Validate max_fetch max_fetch_str = str(demisto_params.get("max_fetch", "")) if max_fetch_str.isnumeric(): max_fetch = int(max_fetch_str) else: raise ValueError("'max_fetch' parameter must be a number") if max_fetch > 50: raise ValueError("'max_fetch' parameter cannot be bigger than 50") try: self.client.whoami() return_results("ok") except Exception as err: message = str(err) try: error = json.loads(str(err).split("\n")[1]) if "fail" in error.get("result", {}).get("status", ""): message = error.get("result", {})["message"] except Exception: message = f"Unknown error. Please verify that the API URL and Token are correctly configured. RAW Error: {err}" raise DemistoException(f"Failed due to - {message}") def fetch_incidents(self) -> None: try: response = self.client.fetch_incidents() except DemistoException as err: if "404" in str(err): return_error("404 in fetch incidents") return raise alerts = response.get("alerts", []) next_query_classic = response.get("next_query_classic", {}) next_query_playbook = response.get("next_query_playbook", {}) next_query = { "next_query_classic": next_query_classic, "next_query_playbook": next_query_playbook, } incidents = [ { "name": alert.get("title"), "occurred": alert.get("created"), "dbotMirrorId": alert.get("id"), "rawJSON": json.dumps(alert), } for alert in alerts ] demisto.incidents(incidents) demisto.setLastRun(next_query) def alert_search_command(self) -> list[CommandResults]: return self.client.alert_search() def alert_rule_search_command( self, ) -> list[CommandResults]: return self.client.alert_rule_search() def alert_update_command(self) -> list[CommandResults]: return self.client.alert_update() def alert_lookup_command(self) -> list[CommandResults]: alert_id = demisto.args().get("alert_id", "") return self.client.alert_lookup(alert_id) @staticmethod def _get_file_name_from_image_id(image_id: str) -> str: return f"{image_id.replace('img:', '')}.png" def _get_image_and_create_attachment( self, alert_type: str, alert_id: str, image_id: str, alert_subtype: str | None, ) -> dict | None: try: return_results(f"Trying to fetch {image_id=} ({alert_type=} {alert_subtype=} {alert_id=})") image_content = self.client.get_alert_image( alert_type=alert_type, alert_id=alert_id, image_id=image_id, alert_subtype=alert_subtype, ) return_results( f"Fetched {image_id=} ({alert_type=} {alert_subtype=} {alert_id=}): {str(image_content[:50])} " f"(truncated)" ) file_name = self._get_file_name_from_image_id(image_id) file_result_obj = fileResult(file_name, image_content) return_results(file_result_obj) # Important attachment = { "description": "Alert image", "name": file_result_obj.get("File"), "path": file_result_obj.get("FileID"), "showMediaFile": True, } return attachment except Exception as e: demisto.error(f"Failed to fetch image {image_id}: {str(e)}") return None def get_alert_images_command(self) -> list[CommandResults]: incident = demisto.incident() if not isinstance(incident, dict) or incident.get("isPlayground") is True: return_error("This command can only run from an incident War Room context.") return [] # return_error will exit(0), but to make linter happy. custom_fields = incident.get("CustomFields") if not isinstance(custom_fields, dict): custom_fields = {} alert_id = custom_fields.get("alertid") if not alert_id: return_error("Failed to get alert id from the incident.") return [] # return_error will exit(0), but to make linter happy. lookup_result = self.client.alert_lookup(alert_id) if isinstance(lookup_result, list) and lookup_result and isinstance(lookup_result[0], CommandResults): lookup_data: dict = lookup_result[0].outputs # type: ignore else: return_error("Failed to lookup alert.") return [] # return_error will exit(0), but to make linter happy. alert_type: str = lookup_data.get("type") or "" alert_subtype: str = lookup_data.get("subtype") or "" image_ids = lookup_data.get("images", []) or [] if not image_ids: return [CommandResults(readable_output="No screenshots found in alert details.")] context = demisto.context() or {} files = demisto.get(context, "File") if not files: files = [] if not isinstance(files, list): files = [files] existing_file_names = {f.get("Name") for f in files if isinstance(f, dict)} # Determine missing image IDs. missing_image_ids: set = set() for img_id in image_ids: # Limit to only 25 images. if len(missing_image_ids) >= MAX_IMAGES_TO_FETCH: break file_name = self._get_file_name_from_image_id(img_id) if file_name not in existing_file_names: missing_image_ids.add(img_id) if not missing_image_ids: return [CommandResults(readable_output="No new images to fetch.")] # Fetch missing images concurrently using thread pool. new_attachments = [] with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: futures = {} for img_id in missing_image_ids: future = executor.submit( self._get_image_and_create_attachment, alert_type=alert_type, alert_id=alert_id, image_id=img_id, alert_subtype=alert_subtype, ) futures[future] = img_id for future in concurrent.futures.as_completed(futures): attachment = future.result() if attachment: new_attachments.append(attachment) if not new_attachments: return [ CommandResults( readable_output="No new images were fetched.", ) ] message = f"Fetched {len(new_attachments)} new image(s)." return [ CommandResults( readable_output=message, ) ] def get_client(): demisto_params = demisto.params() base_url = demisto_params.get("url", "").rstrip("/") verify_ssl = not demisto_params.get("insecure", False) proxy = demisto_params.get("proxy", False) api_token = demisto_params.get("credentials", {}).get("password") if not api_token: return_error("Please provide a valid API token") headers = { "X-RFToken": api_token, "X-RF-User-Agent": ( f"RecordedFuture.py/{__version__} ({platform.platform()}) " f"XSOAR/{__version__} " f"RFClient/{__version__} (Cortex_XSOAR_{demisto.demistoVersion()['version']})" ), } return Client(base_url=base_url, verify=verify_ssl, headers=headers, proxy=proxy) def main(): try: client = get_client() command = demisto.command() actions = Actions(client) if command == "test-module": actions.test_module() elif command == "fetch-incidents": actions.fetch_incidents() elif command == "rf-alert-rules": return_results(actions.alert_rule_search_command()) elif command == "rf-alerts": return_results(actions.alert_search_command()) elif command == "rf-alert-update": return_results(actions.alert_update_command()) elif command == "rf-alert-lookup": return_results(actions.alert_lookup_command()) elif command == "rf-alert-images": return_results(actions.get_alert_images_command()) except Exception as e: return_error( message=f"Failed to execute {demisto.command()} command: {str(e)}", error=e, ) if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()