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.
from __future__ import annotations from typing import Any import demistomock as demisto import pytest import RecordedFutureAlerts Client = RecordedFutureAlerts.Client Actions = RecordedFutureAlerts.Actions CommandResults = RecordedFutureAlerts.CommandResults # Test Client def _capture_method_call( monkeypatch: pytest.MonkeyPatch, method_name: str, *, return_value: Any | None = None, ) -> dict[str, Any]: """Patch Client.*method_name* and capture keyword arguments.""" captured: dict[str, Any] = {} if return_value is None: return_value = {"ok": True} def _fake_method(self, **kwargs): captured.update(kwargs) return return_value monkeypatch.setattr(Client, method_name, _fake_method, raising=True) return captured def test_client_response_to_command_results_uses_raw_outputs(): result_action = { "outputs_prefix": "RecordedFutureAlerts.Alert", "outputs_key_field": "id", "raw_response": {"outputs": {"id": "42"}}, "readable_output": "done", } results = Client._response_to_command_results({"result_actions": [result_action]}) assert results[0].outputs == {"id": "42"} assert results[0].readable_output == "done" def test_client_request_results_returns_no_results_on_404(monkeypatch: pytest.MonkeyPatch): def _fake_request_raw(self, **kwargs): raise RecordedFutureAlerts.DemistoException("404 not found") monkeypatch.setattr(Client, "_request_raw", _fake_request_raw, raising=True) client = Client(base_url="x", verify=False, headers={}) results = client._request_results(method="GET", url_suffix="/v3/alert/search") assert len(results) == 1 assert results[0].readable_output == "No results found." assert results[0].outputs == {} def test_client_whoami_delegates_to_request_raw(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(demisto, "args", dict, raising=True) captured = _capture_method_call(monkeypatch, "_request_raw") client = Client(base_url="x", verify=False, headers={}) client.whoami() assert captured["method"] == "GET" assert captured["url_suffix"] == "/info/whoami" def test_client_alert_update_delegates_to_request_results( monkeypatch: pytest.MonkeyPatch, ): expected_json = { "alert_id": "1", "status": "InProgress", "comment": "Investigating", "assignee": "analyst@example.com", } monkeypatch.setattr(demisto, "args", lambda: expected_json, raising=True) captured = _capture_method_call( monkeypatch, "_request_results", return_value=[CommandResults(readable_output="ok")], ) client = Client(base_url="x", verify=False, headers={}) client.alert_update() assert captured["method"] == "POST" assert captured["url_suffix"] == "/v3/alert/update" assert captured["json_data"] == expected_json def test_client_alert_search_delegates_to_request_results(monkeypatch: pytest.MonkeyPatch): expected_args = { "limit": "10", "created_from": "2024-01-01T00:00:00Z", "include_classic_alerts": "true", "statuses": "New", } monkeypatch.setattr(demisto, "args", lambda: expected_args, raising=True) captured = _capture_method_call( monkeypatch, "_request_results", return_value=[CommandResults(readable_output="ok")], ) client = Client(base_url="x", verify=False, headers={}) client.alert_search() assert captured["method"] == "GET" assert captured["url_suffix"] == "/v3/alert/search" assert captured["params"] == expected_args def test_client_alert_rule_search_delegates_to_request_results( monkeypatch: pytest.MonkeyPatch, ): expected_args = {"rule_name": "Malware", "limit": "5"} monkeypatch.setattr(demisto, "args", lambda: expected_args, raising=True) captured = _capture_method_call( monkeypatch, "_request_results", return_value=[CommandResults(readable_output="ok")], ) client = Client(base_url="x", verify=False, headers={}) client.alert_rule_search() assert captured["method"] == "GET" assert captured["url_suffix"] == "/v3/alert/rules" assert captured["params"] == expected_args def test_client_alert_lookup_delegates_to_request_results(monkeypatch: pytest.MonkeyPatch): captured = _capture_method_call( monkeypatch, "_request_results", return_value=[CommandResults(readable_output="ok")], ) client = Client(base_url="x", verify=False, headers={}) client.alert_lookup("42") assert captured["method"] == "GET" assert captured["url_suffix"] == "/v3/alert/lookup" assert captured["params"] == {"alert_id": "42"} def test_client_get_alert_image_calls_http_request( monkeypatch: pytest.MonkeyPatch, ): captured: dict[str, Any] = {} image_data = b"bytes" def _fake_http(self, *, url_suffix: str, params=None, resp_type=None, **kwargs): captured.update(url_suffix=url_suffix, params=params, resp_type=resp_type) return image_data monkeypatch.setattr(Client, "_http_request", _fake_http, raising=True) client = Client(base_url="x", verify=False, headers={}) result = client.get_alert_image( alert_type="classic-alert", alert_id="1234", image_id="img1", alert_subtype="classic-alert", ) assert result == image_data assert captured["url_suffix"] == "/v3/alert/image" assert captured["resp_type"] == "content" assert captured["params"] == { "alert_type": "classic-alert", "alert_subtype": "classic-alert", "alert_id": "1234", "image_id": "img1", } def test_client_fetch_incidents_delegates_to_request_raw( monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setattr( demisto, "getLastRun", lambda: { "next_query_classic": {"query_from": None}, "next_query_playbook": {"query_from": None}, }, raising=True, ) integration_conf = {"first_fetch": 60, "max_fetch": 10} monkeypatch.setattr(demisto, "params", lambda: integration_conf, raising=True) captured = _capture_method_call(monkeypatch, "_request_raw") client = Client(base_url="x", verify=False, headers={}) client.fetch_incidents() assert captured["method"] == "POST" assert captured["url_suffix"] == "/v3/alert/fetch" json_data = captured["json_data"] assert json_data["integration_config"] == integration_conf assert json_data["classic_query_params"] == {"query_from": None} assert json_data["playbook_query_params"] == {"query_from": None} # Test Action def test_actions_alert_search_pass_through(monkeypatch: pytest.MonkeyPatch): expected: list[CommandResults] = [CommandResults(readable_output="hi")] monkeypatch.setattr(Client, "alert_search", lambda *_: expected, raising=True) actions = Actions(Client(base_url="x", verify=False, headers={})) assert actions.alert_search_command() is expected def test_actions_alert_rule_search_pass_through( monkeypatch: pytest.MonkeyPatch, ): expected: list[CommandResults] = [CommandResults(readable_output="hi")] monkeypatch.setattr(Client, "alert_rule_search", lambda *_: expected, raising=True) actions = Actions(Client(base_url="x", verify=False, headers={})) assert actions.alert_rule_search_command() is expected def test_actions_alert_update_pass_through(monkeypatch: pytest.MonkeyPatch): expected: list[CommandResults] = [CommandResults(readable_output="hi")] monkeypatch.setattr(Client, "alert_update", lambda *_: expected, raising=True) actions = Actions(Client(base_url="x", verify=False, headers={})) assert actions.alert_update_command() is expected def test_actions_alert_lookup_pass_through(monkeypatch: pytest.MonkeyPatch): expected: list[CommandResults] = [CommandResults(readable_output="hi")] monkeypatch.setattr(demisto, "args", lambda: {"alert_id": "42"}, raising=True) captured: dict[str, Any] = {} def _fake_alert_lookup(self, alert_id: str): captured["alert_id"] = alert_id return expected monkeypatch.setattr(Client, "alert_lookup", _fake_alert_lookup, raising=True) actions = Actions(Client(base_url="x", verify=False, headers={})) assert actions.alert_lookup_command() is expected assert captured["alert_id"] == "42" def test_actions_fetch_incidents_builds_incident_objects( monkeypatch: pytest.MonkeyPatch, ): mock_alerts = [ {"title": "Alert A", "created": "2024-01-01T00:00:00Z", "id": "a"}, {"title": "Alert B", "created": "2024-01-02T00:00:00Z", "id": "b"}, ] mock_next = {"foo": "bar"} def _fake_fetch_incidents(self): return { "alerts": mock_alerts, "next_query_classic": mock_next, "next_query_playbook": mock_next, } monkeypatch.setattr(Client, "fetch_incidents", _fake_fetch_incidents, raising=True) captured_incidents = {} monkeypatch.setattr(demisto, "incidents", lambda i: captured_incidents.update(i=i)) monkeypatch.setattr(demisto, "setLastRun", lambda v: captured_incidents.update(last=v)) actions = Actions(Client(base_url="x", verify=False, headers={})) actions.fetch_incidents() assert len(captured_incidents["i"]) == 2 assert captured_incidents["last"]["next_query_classic"] == mock_next @pytest.mark.parametrize( "image_id,expected_file_name", [ ("img:abcd", "abcd.png"), ("img:1234", "1234.png"), ], ) def test_get_file_name_from_image_id(image_id: str, expected_file_name: str): assert Actions._get_file_name_from_image_id(image_id) == expected_file_name def test_actions_get_alert_images_no_images(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( demisto, "incident", lambda: {"CustomFields": {"alertid": "42"}}, raising=True, ) lookup_outputs = { "alert_id": "42", "type": "classic-alert", "subtype": "classic-alert", "images": [], } monkeypatch.setattr( Client, "alert_lookup", lambda *_: [CommandResults(outputs=lookup_outputs)], raising=True, ) monkeypatch.setattr(demisto, "context", dict) res = Actions(Client(base_url="x", verify=False, headers={})).get_alert_images_command() assert res[0].readable_output.startswith("No screenshots found in alert details.") @pytest.mark.parametrize( "incident, expected_error", [ ( None, "This command can only run from an incident War Room context.", ), ( {"id": "425492fc-bd3a-4322-8908-4729c099d982", "isPlayground": True, "CustomFields": None}, "This command can only run from an incident War Room context.", ), ( {"id": "123", "CustomFields": None}, "Failed to get alert id from the incident.", ), ], ) def test_actions_get_alert_images_precondition_errors( monkeypatch: pytest.MonkeyPatch, incident: Any, expected_error: str, ): monkeypatch.setattr(demisto, "incident", lambda: incident, raising=True) called = {"lookup": False, "error": ""} def _fake_alert_lookup(*_, **__): called["lookup"] = True return [] def _fake_return_error(msg: str, *_, **__): called["error"] = msg raise RuntimeError("return_error_called") monkeypatch.setattr(Client, "alert_lookup", _fake_alert_lookup, raising=True) monkeypatch.setattr(RecordedFutureAlerts, "return_error", _fake_return_error, raising=True) with pytest.raises(RuntimeError, match="return_error_called"): Actions(Client(base_url="x", verify=False, headers={})).get_alert_images_command() assert called["error"] == expected_error assert called["lookup"] is False def test_actions_get_alert_images_fetches_missing( monkeypatch: pytest.MonkeyPatch, ): img_ids = ["img:1", "img:2"] monkeypatch.setattr( demisto, "incident", lambda: {"CustomFields": {"alertid": "42"}}, raising=True, ) lookup_outputs = { "alert_id": "42", "type": "classic-alert", "subtype": "classic-alert", "images": img_ids, } monkeypatch.setattr( Client, "alert_lookup", lambda *_: [CommandResults(outputs=lookup_outputs)], raising=True, ) # No files in context so both images are missing monkeypatch.setattr(demisto, "context", dict) calls = [] def _fake_get_image_and_create_attachment(self, *_, **__): calls.append(1) return {"name": "file.png", "path": "123"} monkeypatch.setattr( Actions, "_get_image_and_create_attachment", _fake_get_image_and_create_attachment, raising=True, ) res = Actions(Client(base_url="x", verify=False, headers={})).get_alert_images_command() # Ensure both images attempted. assert len(calls) == len(img_ids) assert "Fetched" in res[0].readable_output def test_actions_get_alert_images_fetches_only_missing( monkeypatch: pytest.MonkeyPatch, ): img_ids = ["img:1", "img:2"] monkeypatch.setattr( demisto, "incident", lambda: {"CustomFields": {"alertid": "42"}}, raising=True, ) lookup_outputs = { "alert_id": "42", "type": "classic-alert", "subtype": "classic-alert", "images": img_ids, } monkeypatch.setattr( Client, "alert_lookup", lambda *_: [CommandResults(outputs=lookup_outputs)], raising=True, ) # Context already contains 1.png - derived from "img:1" context_with_one_image = {"File": [{"Name": "1.png"}]} monkeypatch.setattr(demisto, "context", lambda: context_with_one_image) calls = [] def _fake_get_image_and_create_attachment(self, *_, **__): calls.append(1) return {"name": "2.png", "path": "123"} monkeypatch.setattr( Actions, "_get_image_and_create_attachment", _fake_get_image_and_create_attachment, raising=True, ) res = Actions(Client(base_url="x", verify=False, headers={})).get_alert_images_command() assert len(calls) == 1 assert "Fetched 1 new image" in res[0].readable_output # Test Main def _exercise_main(monkeypatch: pytest.MonkeyPatch, command: str, actions_attr: str): """Utility to run *main* with *command* and record side-effects.""" monkeypatch.setattr(demisto, "command", lambda: command, raising=True) monkeypatch.setattr( demisto, "params", lambda: {"url": "x", "credentials": {"password": "token"}}, raising=True, ) expected = [CommandResults(readable_output="done")] call_counter = {"n": 0} def _fake_action(self): call_counter["n"] += 1 return expected monkeypatch.setattr(Actions, actions_attr, _fake_action, raising=True) captured: dict[str, Any] = {} monkeypatch.setattr( RecordedFutureAlerts, "return_results", lambda v: captured.update(res=v), raising=True, ) RecordedFutureAlerts.main() assert call_counter["n"] == 1 assert captured["res"] is expected def test_main_dispatch_rf_alert_rules(monkeypatch: pytest.MonkeyPatch): _exercise_main( monkeypatch, command="rf-alert-rules", actions_attr="alert_rule_search_command", ) def test_main_dispatch_rf_alerts(monkeypatch: pytest.MonkeyPatch): _exercise_main(monkeypatch, command="rf-alerts", actions_attr="alert_search_command") def test_main_dispatch_rf_alert_update(monkeypatch: pytest.MonkeyPatch): _exercise_main( monkeypatch, command="rf-alert-update", actions_attr="alert_update_command", ) def test_main_dispatch_rf_alert_lookup(monkeypatch: pytest.MonkeyPatch): _exercise_main( monkeypatch, command="rf-alert-lookup", actions_attr="alert_lookup_command", ) def test_main_dispatch_rf_alert_images(monkeypatch: pytest.MonkeyPatch): _exercise_main( monkeypatch, command="rf-alert-images", actions_attr="get_alert_images_command", )