Recorded Future Identity

Fetch & triage | Search & Lookup | Access Recorded Future Identity data and Playbook Alerts.

Data Enrichment & Threat Intelligence · Recorded Future Identity

Details

IDRecorded Future Identity
ProviderMastercard
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Recorded Future Identity Integration

Overview

This integration allows you to leverage Recorded Future’s identity-related threat intelligence within Cortex XSOAR. It
provides commands to search and lookup identity data, fetch playbook alerts, and manage incident statuses, enhancing
your security operations with actionable intelligence.

Setup

A valid API Token for Recorded Future Identity Intelligence needed to fetch information.
Get help with Recorded Future for Cortex XSOAR.

Configuration

  1. Navigate to Integrations:
    • Go to Settings > Integrations > Servers & Services.
  2. Search for Recorded Future Identity:
    • In the search bar, type Recorded Future Identity.
  3. Add a New Instance:
    • Click Add instance to create and configure a new integration instance.
  4. Enter Configuration Parameters:
    • Fill in the required parameters such as Server URL and API Token.
    • Adjust optional settings like proxy usage and incident fetching as needed.
  5. Test the Configuration:
    • Click Test to ensure the settings are correct and that the connection to Recorded Future is successful.

Configuration Parameters

Parameter Description Required
Server URL The URL to the Recorded Future ConnectAPI. True
API Token Valid API Token from Recorded Future. True
Trust any certificate Trust any certificate (not secure). False
Use system proxy settings Use system proxy settings. False
Fetch incidents Enable fetching incidents. False
First Incident Fetch: Time Range Limit incidents to include in the first fetch by time range. Input format: “NN hours” or “NN days”. False
Maximum number of incidents per fetch Limit the number of incidents returned per single fetch. Default value: 50. False
Incident type The type of incidents to fetch. False
Password properties Password properties that are used as a filter. False
Limit Identities Limit of identities to get, minimum is 0 and maximum is 10,000. False
Domains List of domains to use in search and lookup commands (e.g., mycompany.com, nextcompany.com). True
Playbook Alerts: Fetched Statuses Choose what statuses are included in the fetch (New, In Progress, Dismissed, Resolved). False
Playbook Alerts: Fetched Priorities Threshold Choose the priority threshold to fetch alerts of the selected priority and higher (Informational < Moderate < High). False
  1. Click Test to validate the URLs, token, and connection.

Pre-Process Rule

The integration pulls in Playbook alerts from Recorded Future based on its updates, creating the need for a
preprocessing rule that updates existing incidents instead of creating duplicates. Follow the guidelines below to
configure the preprocessing rule.

  1. Navigate to Settings > Objects Setup > Pre-Process Rules.
  2. Click New Rule.
  3. Name the rule appropriately.
  4. In the Conditions for Incoming Incident section, enter:
    • Name - Includes - Recorded Future Playbook Alert
  5. In the Action section, select Drop and update.
  6. In the Update section, enter
    • Link to - Oldest incident - Created within the last - Your desired timeframe
    • DbotMirrorId - Is identical (Incoming Incident) - to incoming incident

The configuration of the preprocessing rule is optional, but highly recommended.

Pre-process Rule

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. After executing a
command, a DBot message appears in the War Room with the command details.

recordedfuture-identity-search

Search for identities in the Recorded Future dataset.

Base Command

recordedfuture-identity-search

Input

Argument Name Description Required
first-downloaded Time period for “first_downloaded_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
latest-downloaded Time period for “last_downloaded_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
exfiltration-date Time period for “exfiltration_date_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
domain-type Type of the domain (Email, Authorization, All). Optional
domains Domains separated by comma (if not specified, domains from app instance will be used). Optional

Context Output

Path Type Description
RecordedFuture.Credentials.SearchIdentities List List of Identities that were found in search command

Command Example

!recordedfuture-identity-search latest-downloaded="All time" domain-type=Authorization

Context Example

{
    "RecordedFuture": {
        "Credentials": {
            "SearchIdentities": [
                {
                    "login": "30fake",
                    "domain": "fakeyahoo.com"
                },
                {
                    "login": "3072882fake",
                    "domain": "fakeyahoo.com"
                },
                "fake3@fake.com",
                "test@fakeyahoo.com"
            ]
        }
    }
}

Human Readable Output

This is search results for fakeyahoo.com, fake.com
  • 30fake in domain fakeyahoo.com
  • 3072882fake in domain fakeyahoo.com
  • fake3@fake.com
  • test@fakeyahoo.com

recordedfuture-identity-lookup

Get detailed information regarding identities.

Base Command

recordedfuture-identity-lookup

Input

Argument Name Description Required
identities String of identities separated by comma. Required
first-downloaded Time period for “first_downloaded_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
latest-downloaded Time period for “last_downloaded_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
exfiltration-date Time period for “exfiltration_date_gte” filter (e.g., “7 days ago”, “Three months ago”, “Last Year”, “All time”). Default value: “All time”. Optional
domains Domains separated by comma. Optional

Context Output

Path Type Description
RecordedFuture.Credentials.Identities.identity.subjects string Identity value
RecordedFuture.Credentials.Identities.count number Leaked credentials count number
RecordedFuture.Credentials.Identities.credentials.subject string Identity value
RecordedFuture.Credentials.Identities.credentials.dumps.name string Dump name
RecordedFuture.Credentials.Identities.credentials.dumps.description string Dump description
RecordedFuture.Credentials.Identities.credentials.dumps.downloaded string Datetime string that show the day when dump was downloaded
RecordedFuture.Credentials.Identities.credentials.type string Dump type
RecordedFuture.Credentials.Identities.credentials.breaches.name string Breach name
RecordedFuture.Credentials.Identities.credentials.breaches.domain string Breach domain
RecordedFuture.Credentials.Identities.credentials.breaches.type string Breach type
RecordedFuture.Credentials.Identities.credentials.breaches.breached string Datetime string that show the day when breach happened
RecordedFuture.Credentials.Identities.credentials.breaches.description string Breach description
RecordedFuture.Credentials.Identities.credentials.breaches.site_description string Breach site description
RecordedFuture.Credentials.Identities.credentials.first_downloaded string Datetime string representing firs time downloaded
RecordedFuture.Credentials.Identities.credentials.latest_downloaded string Datetime string representing last time downloaded
RecordedFuture.Credentials.Identities.credentials.exposed_secret.type string Exposed secret type
RecordedFuture.Credentials.Identities.credentials.exposed_secret.hashes.algorithm string Exposed secret hash algorithm
RecordedFuture.Credentials.Identities.credentials.exposed_secret.hashes.hash string Exposed secret hash value
RecordedFuture.Credentials.Identities.credentials.exposed_secret.effectively_clear boolean Exposed secret clear or not
RecordedFuture.Credentials.Identities.credentials.exposed_secret.details.properties string Exposed secret properties
RecordedFuture.Credentials.Identities.credentials.exposed_secret.details.clear_text_hint string Exposed secret text hint
RecordedFuture.Credentials.Identities.credentials.exposed_secret.details.rank string Rank for the exposed password

Command Example

!recordedfuture-identity-lookup identities="fake@fakeyahoo.com,real@notfake.com" first-downloaded="3 Months ago"

Context Example

{
    "RecordedFuture": {
        "Credentials": {
            "Identities": [
                {
                    "identity":{
                        "subjects":[
                            "fake@yahoo.com"
                        ]
                    },
                    "count":4,
                    "credentials":[
                        {
                            "subject":"fake@yahoo.com",
                            "dumps":[
                                {
                                    "name":"FAKE Dump November 2020",
                                    "description":"This SQL Dump linked to the 2020",
                                    "downloaded":"2020-11-05T00:00:00.000Z",
                                    "type":"SQL Dump",
                                    "breaches":[
                                        {
                                            "name":"FAKE",
                                            "domain":"fake.com",
                                            "type":"breach",
                                            "breached":"2020-10-01T00:00:00.000Z",
                                            "description":"In October 2020, fake suffered a breach that exposed a portion of their backup data from November 2017. The exposed data included",
                                            "site_description":"Fale provides backup and related services for mobile devices."
                                        }
                                    ]
                                }
                            ],
                            "first_downloaded":"2020-11-05T00:00:00.000Z",
                            "latest_downloaded":"2020-11-05T00:00:00.000Z",
                            "exposed_secret":{
                                "type":"pbkdf2_sha256",
                                "hashes":[
                                    {
                                        "algorithm":"PBKDF2_SHA256",
                                        "hash":"12000$IWIs9x2tM7U3afasdasd3d23nRfo0sFRjpkMlim2GA2+p/2Y7RQLpODP4S0="
                                    }
                                ],
                                "effectively_clear":false
                            }
                        },
                        {
                            "subject":"fake@yahoo.com",
                            "dumps":[
                                {
                                    "name":"Dark Web Dump October 2019",
                                    "description":"This combo list of email addresses and clear passwords is not associated with any specific breach",
                                    "downloaded":"2019-10-31T00:00:00.000Z",
                                    "type":"Combo List"
                                },
                                {
                                    "name":"Dark Web Dump September 2019",
                                    "description":"This combo list of email addresses and clear passwords is not associated with any specific breach..",
                                    "downloaded":"2019-09-02T00:00:00.000Z",
                                    "type":"Combo List"
                                }
                            ],
                            "first_downloaded":"2019-09-02T00:00:00.000Z",
                            "latest_downloaded":"2019-10-31T00:00:00.000Z",
                            "exposed_secret":{
                                "type":"clear",
                                "hashes":[
                                    {
                                        "algorithm":"SHA1",
                                        "hash":"bbebf4e24e6631570cd8f60e1b0f77c"
                                    },
                                    {
                                        "algorithm":"SHA256",
                                        "hash":"7bf5626ca4944595aa89bf5fdsdfsdf9c9a1e9b014356d9b10ef31c9e9aadc7835e4"
                                    },
                                    {
                                        "algorithm":"NTLM",
                                        "hash":"1b408e981311312e21eefc0f5e37a20808"
                                    },
                                    {
                                        "algorithm":"MD5",
                                        "hash":"a6b9607b296pfk941dd390d9e690ade8e"
                                    }
                                ],
                                "details":{
                                    "properties":[
                                        "Letter",
                                        "Number",
                                        "UpperCase",
                                        "LowerCase",
                                        "AtLeast8Characters"
                                    ],
                                    "clear_text_hint":"oo"
                                },
                                "effectively_clear":true
                            }
                        }
                    ]
                },
                {
                    "identity":{
                        "subjects":[
                            "007"
                        ]
                    },
                    "count":1,
                    "credentials":[
                        {
                            "subject":"007",
                            "dumps":[
                                {
                                    "name":"Dark Web Password Dump 2020-11-07"
                                },
                                {
                                    "name":"Dark Web Password Dump 2020-09-28"
                                }
                            ],
                            "first_downloaded":"2020-09-28T15:52:05.000Z",
                            "latest_downloaded":"2020-11-07T08:38:56.000Z",
                            "exposed_secret":{
                                "type":"clear",
                                "hashes":[
                                    {
                                        "algorithm":"SHA1",
                                        "hash":"6204f1659eb8dasdas871be7f4b3ee560207bda2c5"
                                    },
                                    {
                                        "algorithm":"SHA256",
                                        "hash":"2fff03500b9335t4g5427fa105aa0bb0d5b3e770d314352aef010b6ae0dff7ab9a"
                                    },
                                    {
                                        "algorithm":"NTLM",
                                        "hash":"a2abf21c7669d58137kffsa83381d55cc"
                                    },
                                    {
                                        "algorithm":"MD5",
                                        "hash":"c399d3ec33214df310b11dcb30b1b5e03"
                                    }
                                ],
                                "details":{
                                    "properties":[
                                        "Letter",
                                        "LowerCase"
                                    ],
                                    "clear_text_hint":"lo"
                                },
                                "effectively_clear":true
                            },
                            "compromise":{
                                "exfiltration_date":"2020-11-07T08:38:56.000Z"
                            },
                            "authorization_service":{
                                "url":"https://login.fakeyahoo.com/",
                                "domain":"fakeyahoo.com"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Human Readable Output

Credentials Lookup


Results for fake1@fake.com

We found 1 passwords that were leaked for this identity


Password 1

Rank: TopMillionCommonPasswords

Properties: Letter, Number, LowerCase, AtLeast8Characters

Type: clear

Effectively Clear: True

Clear Text Hint: wa

Algorithm: SHA1 Hash:21b1ee2d6764b61b038605378f361599a8b503ed

Algorithm: SHA256 Hash:99dbda619dfd82cf9dae074b5c3168e75961b642f3245fe7f400ad03940a0bd8

Algorithm: NTLM Hash:da89071afe87527dc0e89a09d35cb9c0

Algorithm: MD5 Hash:a0b1c21221b29780fc5e3373e626ab9b

Authorization service url: https://signup.norsegods.online/signup
Authorization service url: https://signup.norsegods.online/signup
Authorization service url: https://signup.norsegods.online/signup
Authorization service url: https://signup.norsegods.online/signup
Authorization service url: https://signup.norsegods.online/signup

Domain: norsegods.online

First Downloaded: Nov 2022

Last Downloaded: Nov 2022

Exfiltration date: N/A

Malware Family: RedLine Stealer

Information about dumps where we found for Password 1


Stealer Malware Logs 2022-11-03, Nov 2022

Description: This credential data was derived from stealer malware logs.
Dump type: N/A

Compromised Host
Operating System: Windows 10 Enterprise x64

OS User Name: gabi2

File Path Location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\AppLaunch.exe

Time Zone: UTC+01:00

Name of the Machine: N/A

User Account Control Setting: AllowAll

Antivirus: Windows Defender

IP Address: 138.255.250.246

Country: DO

Postal Code: 11403

recordedfuture-password-lookup

Lookup for password in Recorded Future Dataset.

Base Command

recordedfuture-password-lookup

Input

Argument Name Description Required
password-hash Password hash value. Required
hash-algorithm Password hash algorithm (e.g., SHA1, SHA256, NTLM, MD5). Default value: “SHA256”. Required

Context Output

Path Type Description
RecordedFuture.Credentials.Password.Hash String Recorded Future password hash value.
RecordedFuture.Credentials.Password.Algorithm String Recorded Future password hash algorithm.
RecordedFuture.Credentials.Password.ExposureStatus String Recorded Future password exposure status. One of Common, UnCommon, NeverExposed

Command Example

!recordedfuture-password-lookup password-hash="0e44ce7308af2b3de5232e4616403ce7d49ba2aec83f79c196409556422a4927" hash-algorithm="SHA256"

Context Example

{
    "RecordedFuture": {
        "Credentials": {
            "Password": [
                {
                    "password": {
                        "algorithm": "SHA256"
                        "hash": "da6a0f1c706df7e864f9d6f9431de9950450880e"
                    }
                    "exposure_status": NeverExposed
                },
                {
                    "password": {
                        "algorithm": "SHA256"
                        "hash": "0e44ce7308af2b3de5232e4616403ce7d49ba2aec83f79c196409556422a"
                    }
                    "exposure_status": Common
                },
            ]
        }
    }
}

Human Readable Output

This is search results for password you provided

Password hash: 0e44ce7308af2b3de5232e4616403ce7d49ba2aec83f79c196409556422a4927

Password hash algorithm: SHA256

Password status: Common

recordedfuture-identity-playbook-alerts-search

Search Playbook alerts based on filters.

Base Command

recordedfuture-identity-playbook-alerts-search

Input

Argument Name Description Required
limit The maximum number of alerts to fetch. Default: 50. Optional
time_since_update The amount of time since the last update. E.g., “2 hours” or “7 days” ago. Default: “24 hours”. Optional
playbook_alert_status The statuses to retrieve. Defaults to only new status if not specified. Possible values are: new, in-progress, dismissed, resolved. Optional
priority Actions priority assigned in Recorded Future. Possible values are: high, moderate, informational. Optional
order_search_by The order by which to search for playbook alerts. Possible values are: updated, created. Optional

Context Output

Path Type Description
IdentityRecordedFuture.PlaybookAlerts.playbook_alert_id String Unique ID of the playbook alert
IdentityRecordedFuture.PlaybookAlerts.category String Playbook alert category
IdentityRecordedFuture.PlaybookAlerts.priority String Recommended Priority of the alert
IdentityRecordedFuture.PlaybookAlerts.status String Current alert status in Recorded Future
IdentityRecordedFuture.PlaybookAlerts.title String Title of the alert
IdentityRecordedFuture.PlaybookAlerts.updated date Date of last update
IdentityRecordedFuture.PlaybookAlerts.created date Date of creation
IdentityRecordedFuture.PlaybookAlerts.organization_id String Organization uhash
IdentityRecordedFuture.PlaybookAlerts.organization_name String Plaintext Organization name
IdentityRecordedFuture.PlaybookAlerts.assignee_id String uhash of the assigned user
IdentityRecordedFuture.PlaybookAlerts.assignee_name unknown name of the assigned user
IdentityRecordedFuture.PlaybookAlerts.owner_id String uhash of the enterprise that owns the alert
IdentityRecordedFuture.PlaybookAlerts.owner_name String Name of the enterprise that owns the alert
Command Example
!recordedfuture-identity-playbook-alerts-search

!recordedfuture-identity-playbook-alerts-search

!recordedfuture-identity-playbook-alerts-search limit=10

!recordedfuture-identity-playbook-alerts-search playbook_alert_status=in-progress

!recordedfuture-identity-playbook-alerts-search priority=high

!recordedfuture-identity-playbook-alerts-search order_search_by=updated

recordedfuture-identity-playbook-alerts-details

Get Playbook alert details by ID.

Base Command

recordedfuture-identity-playbook-alerts-details

Input

Argument Name Description Required
alert_ids IDs of the playbook alert that should be fetched. Required
detail_sections What evidence sections to include in the fetch, fetches all available if not specified. Optional

Context Output

Path Type Description
IdentityRecordedFuture.PlaybookAlerts.playbook_alert_id String Unique ID of the playbook alert.
IdentityRecordedFuture.PlaybookAlerts.category String Playbook alert category.
IdentityRecordedFuture.PlaybookAlerts.priority String Recommended Priority of the alert.
IdentityRecordedFuture.PlaybookAlerts.status String Current alert status in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.title String Title of the alert.
IdentityRecordedFuture.PlaybookAlerts.updated date Date of last update.
IdentityRecordedFuture.PlaybookAlerts.created date Date of creation.
IdentityRecordedFuture.PlaybookAlerts.organization_id String Organization uhash.
IdentityRecordedFuture.PlaybookAlerts.organization_name String Plaintext Organization name.
IdentityRecordedFuture.PlaybookAlerts.assignee_id String uhash of the assigned user.
IdentityRecordedFuture.PlaybookAlerts.assignee_name String name of the assigned user.
IdentityRecordedFuture.PlaybookAlerts.owner_id String uhash of the enterprise that owns the alert.
IdentityRecordedFuture.PlaybookAlerts.owner_name String Name of the enterprise that owns the alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.playbook_alert_id String Unique ID of the playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.category String Playbook alert category.
IdentityRecordedFuture.PlaybookAlerts.panel_status.priority String Recommended Priority of the alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.status String Current alert status in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_status.title String Title of the alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.updated date Date of last update.
IdentityRecordedFuture.PlaybookAlerts.panel_status.created date Date of creation.
IdentityRecordedFuture.PlaybookAlerts.panel_status.organization_id String Organization uhash.
IdentityRecordedFuture.PlaybookAlerts.panel_status.organization_name String Plaintext Organization name.
IdentityRecordedFuture.PlaybookAlerts.panel_status.assignee_id String uhash of the assigned user.
IdentityRecordedFuture.PlaybookAlerts.panel_status.assignee_name unknown name of the assigned user.
IdentityRecordedFuture.PlaybookAlerts.panel_status.owner_id String uhash of the enterprise that owns the alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.owner_name String Name of the enterprise that owns the alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.case_rule_id String ID of the playbook alert category.
IdentityRecordedFuture.PlaybookAlerts.panel_status.case_rule_label String Name of the playbook alert category.
IdentityRecordedFuture.PlaybookAlerts.panel_status.context_list.context Array Context of entity connected to the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.created String Date marking the creation of the Playbook alert in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_status.entity_criticality String Criticality of the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.entity_id String ID of the entity in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_status.entity_name String Name of the entity.
IdentityRecordedFuture.PlaybookAlerts.panel_status.risk_score String Risk score of the entity in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_status.targets Array List of targets of the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_status.lifecycle_stage String Indicates what lifecycle the vulerability is in.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.explanation String Entails the explanation to the triggering of the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.resolved_record_list.context_list.context String Context of entity connected to the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.resolved_record_list.criticality String Level of criticality.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.resolved_record_list.entity String ID of the entitiy in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.resolved_record_list.record_type String Type of record A, CNAME or MX.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.resolved_record_list.risk_score String Risk score of the entity in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.screenshots.description String Description of the image.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.screenshots.image_id String ID of the screenshot in recorded future.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.screenshots.tag String Image Analisys tag.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.screenshots.created String When the image was created.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.screenshots.base64 String The image binary encoded as a base64 string.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.summary.targets.name String Target affected by the vulnerability.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.summary.lifecycle_stage String The current lifecycle stage of the Playbook Alert.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.summary.riskrules.rule String Name of the rule that triggered.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.summary.riskrules.description String Short description of the trigger (E.g 13 sightings on 1 source..).
IdentityRecordedFuture.PlaybookAlerts.panel_summary.affected_products.name String Name of of affected product.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.insikt_notes.id String The ID of the Insikt note.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.insikt_notes.title String The title of the Insikt note.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.insikt_notes.topic String The topic of the Insikt note.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.insikt_notes.published String The time at which the Insikt note was published.
IdentityRecordedFuture.PlaybookAlerts.panel_summary.insikt_notes.fragment String A fragment of the Insikt note text.
IdentityRecordedFuture.PlaybookAlerts.panel_log.id String Log ID in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.panel_log.actor_id String ID of the actor.
IdentityRecordedFuture.PlaybookAlerts.panel_log.created String When was the log created.
IdentityRecordedFuture.PlaybookAlerts.panel_log.modified String When was the log last modified.
IdentityRecordedFuture.PlaybookAlerts.panel_log.action_priority String The priority of the Playbook alert.
IdentityRecordedFuture.PlaybookAlerts.panel_log.message String Log message.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.assigne_change.old String Previous assignee.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.assigne_change.new String New assignee.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.assigne_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.status_change.old String Previous status.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.status_change.new String New status.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.status_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.title_change.old String Previous title.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.title_change.new String New title.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.title_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.priority_change.old String Previous priority.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.priority_change.new String New priority.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.priority_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.reopen_strategy_change.old String Previous reopen strategy.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.reopen_strategy_change.new String New reopen strategy.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.reopen_strategy_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.entities_change.removed String Removed entity.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.entities_change.added String Added entity.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.entities_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.related_entities_change.removed String Removed related entity.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.related_entities_change.added String Added related entity.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.related_entities_changetype String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.description_change.old String Previous description.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.description_change.new String New description.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.description_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.external_id_change.old String Previous external ID.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.external_id_change.new String New external ID.
IdentityRecordedFuture.PlaybookAlerts.panel_log.changes.external_id_change.type String Type of change.
IdentityRecordedFuture.PlaybookAlerts.panel_action.action String The name of the action.
IdentityRecordedFuture.PlaybookAlerts.panel_action.updated String When was the action last updated.
IdentityRecordedFuture.PlaybookAlerts.panel_action.assignee_name String Full name of the assignee.
IdentityRecordedFuture.PlaybookAlerts.panel_action.assignee_id String ID of the assignee.
IdentityRecordedFuture.PlaybookAlerts.panel_action.status String The status of the action.
IdentityRecordedFuture.PlaybookAlerts.panel_action.description String A short description of the action.
IdentityRecordedFuture.PlaybookAlerts.panel_action.link String A link associated with the action.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ip_list.record String The DNS record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ip_list.risk_score String Risk score associated with the record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ip_list.criticality String The level of criticality.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ip_list.record_type String Type of record A, CNAME or MX.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ip_list.context_list.context String Labels of malicious behavior types that can be associated with an entity.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.mx_list.record String The DNS record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.mx_list.risk_score String Risk score associated with the record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.mx_list.criticality String The level of criticality.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.mx_list.record_type String Type of record A, CNAME or MX.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.mx_list.context_list.context String Labels of malicious behavior types that can be associated with an entity.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ns_list.record String The DNS record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ns_list.risk_score String Risk score associated with the record.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ns_list.criticality String The level of criticality.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ns_list.record_type String Type of record A, CNAME or MX.
IdentityRecordedFuture.PlaybookAlerts.panel_dns.ns_list.context_list.context String Labels of malicious behavior types that can be associated with an entity.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.added String When the whois information was added.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.attribute String Attribute, either whois or whoisContancts.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.entity String ID of whois entity.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.provider String Name of provider.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.createdDate String When was it created.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.nameServers Array List of name server IDs.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.privateRegistration Bool Boolean indicating private registration.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.registrarName String Name of the registrar.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.status String Status of registrar.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.city String Contact located in this city.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.country String Contact located in this city.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.name String Name of contact.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.organization String Name of contact organization.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.postalCode String Postal code of contact organization.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.state String Contact located in state.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.street1 String Street name of contact.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.telephone String Phone number of contact.
IdentityRecordedFuture.PlaybookAlerts.panel_whois.body.value.type String Type of contact.

Command Example

!recordedfuture-identity-playbook-alerts-details alert_ids="12312312-1231-1231-1231-123123123123" detail_sections="status,log"

recordedfuture-identity-playbook-alerts-update

Update the status of one or multiple Playbook alerts.

Base Command

recordedfuture-identity-playbook-alerts-update

Input

Argument Name Description Required
alert_ids IDs of the playbook alerts that will be updated. Required
new_status New status to set for all alerts in alert_ids (e.g., new, in-progress, dismissed, resolved). Required
add_actions_taken Add ‘actions taken’ to all alerts in alert_ids. Can only be used with ‘new_status=resolved’ or ‘new_status=dismissed’. Possible values: enforced_password_reset, placed_in_risky_group, reviewed_incident_report, account_disabled_or_terminated, account_remediated, other. Optional
remove_actions_taken Remove ‘actions taken’ from all alerts in alert_ids. You can specify multiple values by providing a quoted string with values separated by commas. Possible values: enforced_password_reset, placed_in_risky_group, reviewed_incident_report, account_disabled_or_terminated, account_remediated, other. Optional
comment Add a comment to all alerts in alert_ids. Optional
reopen Set the reopen strategy for the alert. Reopen on significant updates or keep the alert Resolved. Default: reopen on significant updates. Can only be used with new_status=resolved. Optional

Context Output

Path Type Description
IdentityRecordedFuture.PlaybookAlerts.playbook_alert_id string Unique ID of the playbook alert in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.current_status string Current status of playbook alert in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.title string Title of the playbook alert in Recorded Future.
IdentityRecordedFuture.PlaybookAlerts.status_message string Message describing the outcome of the update.

Command Example

!recordedfuture-identity-playbook-alerts-update alert_ids="12312312-1231-1231-1231-123123123123" new_status="in-progress"

!recordedfuture-identity-playbook-alerts-update alert_ids="12312312-1231-1231-1231-123123123123" new_status="resolved" comment="Resolved in XSOAR" add_actions_taken=""

Configuration parameters

  • server_url — Server URL (e.g., https://api.recordedfuture.com/gw/xsoar-identity) (required)
  • token — API Token
  • credential
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • first_fetch — First Incident Fetch: Time Range
  • max_fetch — Maximum number of incidents per fetch
  • pa_statuses — Playbook Alerts: Fetched Statuses
  • pa_priorities — Playbook Alerts: Fetched Priorities Threshold
  • password_properties — Password properties
  • limit_identities — Limit of identities to get (min is 0 and max is 10 000)
  • domains — Domains list separated by comma (e.g. norsegods.online, norsegods.online ). This will be used for identity search command as a default value. (required)

Commands (6)

  • recordedfuture-identity-lookup

    Lookup for identities in Recorded Future Identity Dataset.

  • recordedfuture-identity-playbook-alerts-details

    Get Playbook alert details by id.

  • recordedfuture-identity-playbook-alerts-search

    Search playbook alerts based on filters.

  • recordedfuture-identity-playbook-alerts-update

    Update the status of one or multiple Playbook alerts.

  • recordedfuture-identity-search

    Search for identities in Recorded Future Identity Dataset.

  • recordedfuture-password-lookup

    Lookup for password in Recorded Future Dataset.

from pathlib import Path
from unittest.mock import Mock, patch

import pytest
import vcr as vcrpy
from IdentityRecordedFuture import Client

CASSETTES = Path(__file__).parent / "test_data"
DATETIME_STR_VALUE = "2021-12-08T12:10:21.837Z"


def filter_out_whoami(response):
    import json

    body = response["body"]["string"]
    try:
        body.decode("utf-8")
        json_blob = json.loads(body)
        json_blob.pop("api_key", None)
        response["body"]["string"] = json.dumps(json_blob).encode("utf-8")
    except UnicodeDecodeError:
        pass  # It's not a json string
    return response


vcr = vcrpy.VCR(
    serializer="yaml",
    cassette_library_dir=str(CASSETTES),
    record_mode="once",
    filter_headers=[("X-RFToken", "XXXXXX")],
    before_record_response=filter_out_whoami,
)


def util_load_json(path):
    import json

    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def create_client() -> Client:
    import os

    base_url = "https://api.recordedfuture.com/gw/xsoar-identity/"
    verify_ssl = True
    token = os.environ.get("RF_TOKEN")
    headers = {
        "X-RFToken": token,
        "X-RF-User-Agent": "Cortex_XSOAR/2.0 Cortex_XSOAR_unittest_0.1",
    }

    return Client(base_url=base_url, verify=verify_ssl, headers=headers, proxy=None)


@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_client_whoami(mock_http_request: Mock) -> None:
    """Test whoami()."""
    client = create_client()
    response = {1: 1}
    mock_http_request.return_value = response
    result = client.whoami()
    assert response == result
    mock_http_request.assert_called_once_with(
        method="get",
        url_suffix="info/whoami",
        timeout=60,
    )


@patch("IdentityRecordedFuture.Client._call")
def test_credentials_search(mock_call: Mock) -> None:
    """Test credentials_search()."""
    client = create_client()
    client.credentials_search()
    mock_call.assert_called_once_with(url_suffix="/v2/identity/credentials/search")


@patch("IdentityRecordedFuture.Actions._process_result_actions")
def test_credentials_search_process_result(process_result_mock: Mock) -> None:
    """Test search identities code."""
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    some_random_data = "data"
    client.credentials_search.return_value = some_random_data
    process_result_mock.return_value = some_random_data
    action_result = actions.identity_search_command()
    assert some_random_data == action_result
    client.credentials_search.assert_called_once()
    process_result_mock.assert_called_once_with(response=some_random_data)


@patch("IdentityRecordedFuture.Client._call")
def test_credentials_lookup(mock_call: Mock) -> None:
    """Test credentials_lookup()."""
    client = create_client()
    client.credentials_lookup()
    mock_call.assert_called_once_with(url_suffix="/v2/identity/credentials/lookup")


@patch("IdentityRecordedFuture.Actions._process_result_actions")
def test_credentials_lookup_process_result(process_result_mock: Mock) -> None:
    """Test lookup identities code."""
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    some_random_data = "data"
    client.credentials_lookup.return_value = some_random_data
    process_result_mock.return_value = some_random_data
    action_result = actions.identity_lookup_command()
    assert some_random_data == action_result
    client.credentials_lookup.assert_called_once()
    process_result_mock.assert_called_once_with(response=some_random_data)


@patch("IdentityRecordedFuture.Client._call")
def test_password_lookup(mock_call: Mock) -> None:
    """Test password_lookup()."""
    client = create_client()
    client.password_lookup()
    mock_call.assert_called_once_with(url_suffix="/v2/identity/password/lookup")


@patch("IdentityRecordedFuture.Actions._process_result_actions")
def test_password_lookup_process_result(process_result_mock: Mock) -> None:
    """Test password lookup code."""

    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    some_random_data = "data"
    client.password_lookup.return_value = some_random_data
    process_result_mock.return_value = some_random_data
    action_result = actions.password_lookup_command()
    assert some_random_data == action_result
    client.password_lookup.assert_called_once()
    process_result_mock.assert_called_once_with(response=some_random_data)


@patch("IdentityRecordedFuture.demisto")
@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_call(mock_http_request: Mock, mocked_demisto: Mock):
    """Test _call()."""

    client = create_client()
    STATUS_TO_RETRY = [500, 501, 502, 503, 504]

    mock_demisto_last_run = {"last_run": "mock"}
    mock_demisto_command = "command"
    mock_demisto_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}
    mock_demisto_params = {"someparam": "some param value"}

    mocked_demisto.command.return_value = mock_demisto_command
    mocked_demisto.getLastRun.return_value = mock_demisto_last_run
    mocked_demisto.args.return_value = mock_demisto_command_args
    mocked_demisto.params.return_value = mock_demisto_params
    http_response = {"some": "http response"}
    mock_http_request.return_value = http_response

    mock_url_suffix = "mock_url_suffix"

    result = client._call(url_suffix=mock_url_suffix)

    json_data = {
        "demisto_args": mock_demisto_command_args,
        "demisto_params": mock_demisto_params,
        "demisto_command": mock_demisto_command,
        "demisto_last_run": mock_demisto_last_run,
    }

    mock_http_request.assert_called_once_with(
        method="post",
        url_suffix=mock_url_suffix,
        json_data=json_data,
        timeout=90,
        retries=3,
        status_list_to_retry=STATUS_TO_RETRY,
    )

    assert result == http_response


@patch("IdentityRecordedFuture.return_error")
@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_call_return_error(mock_http_request: Mock, return_error_mock: Mock):
    """Test _call() when error message was returned."""
    from CommonServerPython import DemistoException

    client = create_client()
    mock_url_suffix = "mock_url_suffix"
    error_message_res = {"message": "error"}
    mock_error_response = Mock()
    mock_error_response.json.return_value = error_message_res
    mock_http_request.side_effect = DemistoException(message="", res=mock_error_response)
    client._call(url_suffix=mock_url_suffix)
    return_error_mock.assert_called_once_with(**error_message_res)


@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_call_return_error_not_json(mock_http_request: Mock):
    """Test _call() when error message was returned and it is not json serializable."""
    import json

    from CommonServerPython import DemistoException

    client = create_client()
    mock_url_suffix = "mock_url_suffix"
    mock_error_response = Mock()
    mock_error_response.json.side_effect = json.JSONDecodeError("some not json error", "some", 3)
    mock_http_request.side_effect = DemistoException(message="", res=mock_error_response)
    with pytest.raises(DemistoException):
        client._call(url_suffix=mock_url_suffix)


@patch("IdentityRecordedFuture.CommandResults")
@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_call_return_http_404_error(mock_http_request: Mock, command_results_mock: Mock):
    """Test _call() when error message was returned."""
    from CommonServerPython import DemistoException

    client = create_client()
    mock_url_suffix = "mock_url_suffix"
    mock_http_request.side_effect = DemistoException("There is HTTP 404 error")
    client._call(url_suffix=mock_url_suffix)
    command_results_mock.assert_called_once_with(
        outputs_prefix="",
        outputs={},
        raw_response={},
        readable_output="No results found.",
        outputs_key_field="",
    )


@patch("IdentityRecordedFuture.BaseClient._http_request")
def test_call_return_http_error(mock_http_request: Mock):
    """Test _call() when error message was returned."""
    from CommonServerPython import DemistoException

    client = create_client()
    mock_url_suffix = "mock_url_suffix"
    mock_http_request.side_effect = DemistoException("Some error from the Server")
    with pytest.raises(DemistoException):
        client._call(url_suffix=mock_url_suffix)


def test_call_with_kwargs(mocker):
    """
    Test the `_call` method to ensure it correctly processes
    additional keyword arguments and sends a request with the expected parameters.
    """

    import os

    import demistomock as demisto

    client = create_client()
    STATUS_TO_RETRY = [500, 501, 502, 503, 504]

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_http_request = mocker.patch.object(client, "_http_request")

    mock_url_suffix = "mock_url_suffix"

    client._call(url_suffix=mock_url_suffix, timeout=120, any_other_kwarg=True)

    json_data = {
        "demisto_command": mock_command_name,
        "demisto_args": mock_command_args,
        "demisto_params": {},
        "demisto_last_run": {"lastRun": "2018-10-24T14:13:20+00:00"},
    }

    mock_http_request.assert_called_once_with(
        method="post",
        url_suffix=mock_url_suffix,
        json_data=json_data,
        timeout=120,
        retries=3,
        status_list_to_retry=STATUS_TO_RETRY,
        any_other_kwarg=True,
    )


def test_call_returns_response(mocker):
    """
    Test that the `_call` method correctly returns the response from the HTTP request.
    """

    import os

    import demistomock as demisto

    client = create_client()

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_response = {"response": {"data": "mock data"}}

    mocker.patch.object(client, "_http_request", return_value=mock_response)

    mock_url_suffix = "mock_url_suffix"

    response = client._call(url_suffix=mock_url_suffix)
    assert response == mock_response


def test_call_response_processing_return_error(mocker):
    """
    Test that the `_call` method correctly processes a return_error response.
    """

    import os

    import demistomock as demisto

    client = create_client()

    STATUS_TO_RETRY = [500, 501, 502, 503, 504]

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_return_error = mocker.patch("IdentityRecordedFuture.return_error")

    mock_http_request = mocker.patch.object(
        client,
        "_http_request",
        return_value={"return_error": {"message": "mock error"}},
    )

    mock_url_suffix = "mock_url_suffix"

    client._call(url_suffix=mock_url_suffix)

    json_data = {
        "demisto_command": mock_command_name,
        "demisto_args": mock_command_args,
        "demisto_params": {},
        "demisto_last_run": {"lastRun": "2018-10-24T14:13:20+00:00"},
    }

    mock_http_request.assert_called_once_with(
        method="post",
        url_suffix=mock_url_suffix,
        json_data=json_data,
        timeout=90,
        retries=3,
        status_list_to_retry=STATUS_TO_RETRY,
    )

    mock_return_error.assert_called_once_with(message="mock error")


def test_call_response_processing_404(mocker):
    """
    Test that the `_call` method correctly handles a 404 error response and returns the appropriate CommandResults.
    """

    import os

    import demistomock as demisto
    from CommonServerPython import CommandResults, DemistoException

    client = create_client()

    STATUS_TO_RETRY = [500, 501, 502, 503, 504]

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mocker.patch("IdentityRecordedFuture.return_error")

    def mock_http_request_method(*args, **kwargs):
        # Imitate how CommonServerPython handles bad responses (when status code not in ok_codes,
        # or if ok_codes=None - it uses requests.Response.ok to check whether response is good).
        raise DemistoException("404")

    mocker.patch.object(client, "_http_request", mock_http_request_method)

    spy_http_request = mocker.spy(client, "_http_request")

    mock_url_suffix = "mock_url_suffix"

    result = client._call(url_suffix=mock_url_suffix)

    json_data = {
        "demisto_command": mock_command_name,
        "demisto_args": mock_command_args,
        "demisto_params": {},
        "demisto_last_run": {"lastRun": "2018-10-24T14:13:20+00:00"},
    }

    spy_http_request.assert_called_once_with(
        method="post",
        url_suffix=mock_url_suffix,
        json_data=json_data,
        timeout=90,
        retries=3,
        status_list_to_retry=STATUS_TO_RETRY,
    )

    assert isinstance(result, CommandResults)

    assert result.outputs_prefix == ""
    assert result.outputs_key_field == ""
    assert result.outputs == {}
    assert result.raw_response == {}
    assert result.readable_output == "No results found."


def test_call_DemistoException_res_json_error(mocker):
    """Test _call when err.res.json() raises an exception (e.g., JSONDecodeError)."""
    import json

    import demistomock as demisto
    from CommonServerPython import DemistoException

    client = create_client()

    mocker.patch.object(demisto, "command", return_value="command_name")
    mocker.patch.object(demisto, "args", return_value={})
    mocker.patch.object(demisto, "params", return_value={})
    mocker.patch.object(demisto, "getLastRun", return_value={})

    class MockResponse:
        def json(self):
            raise json.JSONDecodeError("Expecting value", "doc", 0)

    def mock_http_request(*args, **kwargs):
        err = DemistoException("Error with response")
        err.res = MockResponse()
        raise err

    mocker.patch.object(client, "_http_request", side_effect=mock_http_request)

    with pytest.raises(DemistoException):
        client._call(url_suffix="mock_url_suffix")


def test_call_DemistoException_res_None(mocker):
    """Test _call when DemistoException has no response (err.res is None)."""

    import demistomock as demisto
    from CommonServerPython import DemistoException

    client = create_client()

    mocker.patch.object(demisto, "command", return_value="command_name")
    mocker.patch.object(demisto, "args", return_value={})
    mocker.patch.object(demisto, "params", return_value={})
    mocker.patch.object(demisto, "getLastRun", return_value={})

    def mock_http_request(*args, **kwargs):
        err = DemistoException("Some error without response")
        err.res = None
        raise err

    mocker.patch.object(client, "_http_request", side_effect=mock_http_request)

    with pytest.raises(DemistoException) as excinfo:
        client._call(url_suffix="mock_url_suffix")

    assert str(excinfo.value) == "Some error without response"


def test_fetch_incidents(mocker):
    """
    Test the `fetch_incidents` method to ensure it sends the correct request and processes the response as expected.
    """
    import os

    import demistomock as demisto

    client = create_client()

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}
    mock_params = {"param1": "param1 value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)
    mocker.patch.object(demisto, "params", return_value=mock_params)

    mock_last_run_dict = {"lastRun": "2022-08-31T12:12:20+00:00"}
    mocker.patch.object(demisto, "getLastRun", return_value=mock_last_run_dict)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.fetch_incidents()

    mock_call.assert_called_once_with(
        timeout=120,
        url_suffix="/playbook_alert/fetch",
    )

    assert response == mock_call_response


def test_playbook_alert_search(mocker):
    """
    Test the `search_playbook_alerts` method to ensure it sends the correct request and processes the response as expected.
    """
    import os

    import demistomock as demisto

    client = create_client()

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.search_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/search")

    assert response == mock_call_response


def test_playbook_alert_details_multi_input(mocker):
    """
    Test the `details_playbook_alerts` method to ensure it correctly processes multiple input arguments.
    """
    import os

    import demistomock as demisto

    client = create_client()

    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    mock_command_name = "command_name"
    mock_alert_ids = "input1,mock_value"
    mock_detail_sections = "input1,mock_value"
    mock_command_args = {
        "alert_ids": mock_alert_ids,
        "detail_sections": mock_detail_sections,
    }

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.details_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/lookup")

    assert response == mock_call_response


def test_playbook_alert_update_multi_input(mocker):
    """
    Test the `update_playbook_alerts` method to ensure it correctly processes multiple input arguments.
    """
    import os

    import demistomock as demisto

    client = create_client()

    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    mock_command_name = "command_name"
    mock_alert_ids = "input1,input2"

    mock_command_args = {"alert_ids": mock_alert_ids}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.update_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/update")

    assert response == mock_call_response


def test_playbook_alert_search_multi_input(mocker):
    """
    Test the `search_playbook_alerts` method to ensure it correctly processes multiple input arguments.
    """
    import os

    import demistomock as demisto

    client = create_client()

    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    mock_command_name = "command_name"
    mock_priority = "high"
    mock_detail_sections = "sdadwa,adinhw0ijd"
    mock_command_args = {
        "priority": mock_priority,
        "playbook_alert_status": mock_detail_sections,
    }

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.search_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/search")

    assert response == mock_call_response


def test_playbook_alert_details(mocker):
    """
    Test the `details_playbook_alerts` method to ensure it sends the correct request and processes the response as expected.
    """
    import os

    import demistomock as demisto

    client = create_client()

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.details_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/lookup")

    assert response == mock_call_response


def test_playbook_alert_update(mocker):
    """
    Test the `update_playbook_alerts` method to ensure it sends the correct request and processes the response as expected.
    """
    import os

    import demistomock as demisto

    client = create_client()

    # This is needed for CommonServerPython module to not add demisto.params() into callingContext.
    os.environ["COMMON_SERVER_NO_AUTO_PARAMS_REMOVE_NULLS"] = "True"

    # Mock demisto command and args.
    mock_command_name = "command_name"
    mock_command_args = {"arg1": "arg1_value", "arg2": "arg2_value"}

    mocker.patch.object(demisto, "command", return_value=mock_command_name)
    mocker.patch.object(demisto, "args", return_value=mock_command_args)

    mock_call_response = {"response": {"data": "mock response"}}
    mock_call = mocker.patch.object(client, "_call", return_value=mock_call_response)

    response = client.update_playbook_alerts()

    mock_call.assert_called_once_with(url_suffix="/playbook_alert/update")

    assert response == mock_call_response


def test_actions_init(mocker):
    """
    Test the initialization of the `Actions` class to ensure the client is correctly assigned.
    """
    from IdentityRecordedFuture import Actions

    mock_client = mocker.Mock()
    actions = Actions(mock_client)
    assert actions.client == mock_client


def test_process_result_actions_returns_list() -> None:
    """Test result processing function with the case when we received 404 error."""

    from CommonServerPython import CommandResults
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    response = CommandResults()
    result = actions._process_result_actions(response)
    assert isinstance(result, list)


def test_process_result_actions_returns_none() -> None:
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    response = {}
    result = actions._process_result_actions(response)
    assert result is None


def test_process_result_actions_404_error() -> None:
    """Test result processing function with the case when we received 404 error."""

    from CommonServerPython import CommandResults
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    response = CommandResults()
    result = actions._process_result_actions(response)
    assert [response] == result


def test_process_result_actions_wrong_type() -> None:
    """Test result processing function with the case when we received string data."""
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)
    response = "Some bad response from API"
    result = actions._process_result_actions(response)
    assert result is None


def test_process_result_actions_no_key_value() -> None:
    """Test result processing function with the case when we received date without action_result key."""
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)
    response = {}
    result = actions._process_result_actions(response)
    assert result is None


def test_process_result_actions() -> None:
    """Test result processing function with the case when we received good data."""
    from CommonServerPython import CommandResults
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)
    response = {"action_result": {"readable_output": "data"}}
    results = actions._process_result_actions(response)
    for x in results:
        assert isinstance(x, CommandResults)


def test_process_result_actions_404(mocker):
    """
    Test the `_process_result_actions` method to ensure it handles a CommandResults response correctly.
    """
    from CommonServerPython import CommandResults
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    # Test if response is CommandResults
    # (case when we got 404 on response, and it was processed in client._call() method).
    response = CommandResults(readable_output="Mock")
    result_actions = actions._process_result_actions(response=response)
    assert result_actions == [response]


def test_process_result_actions_response_is_not_dict(mocker):
    """
    Test the `_process_result_actions` method to ensure it returns None when the response is not a dictionary.
    """
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    # Test if response is not CommandResults and not Dict.
    response = "Mock string - not CommandResults and not dict"
    result_actions = actions._process_result_actions(response=response)  # type: ignore
    assert result_actions is None


def test_process_result_actions_no_or_empty_result_actions_in_response(mocker):
    """
    Test the `_process_result_actions` method to ensure it returns None when there are no result actions in the response.
    """
    from IdentityRecordedFuture import Actions

    client = Mock()
    actions = Actions(client)

    # Test no results_actions in response.
    response = {"data": "mock"}
    result_actions = actions._process_result_actions(response=response)
    assert result_actions is None

    # Test case when bool(results_actions) in response is False.
    response = {"data": "mock", "result_actions": None}
    result_actions = actions._process_result_actions(response=response)
    assert result_actions is None

    response = {"data": "mock", "result_actions": []}
    result_actions = actions._process_result_actions(response=response)
    assert result_actions is None

    response = {"data": "mock", "result_actions": {}}
    result_actions = actions._process_result_actions(response=response)
    assert result_actions is None


def test_process_result_actions_command_results_only(mocker):
    """
    Test the `_process_result_actions` method to ensure it processes a response containing CommandResults correctly.
    """
    from IdentityRecordedFuture import Actions, CommandResults

    client = Mock()
    actions = Actions(client)

    response = {
        "data": "mock",
        "result_actions": [
            {
                "CommandResults": {
                    "outputs_prefix": "mock_outputs_prefix",
                    "outputs": "mock_outputs",
                    "raw_response": "mock_raw_response",
                    "readable_output": "mock_readable_output",
                    "outputs_key_field": "mock_outputs_key_field",
                },
            }
        ],
    }
    result_actions = actions._process_result_actions(response=response)

    assert len(result_actions) == 1

    r_a = result_actions[0]

    assert isinstance(r_a, CommandResults)

    assert r_a.outputs_prefix == "mock_outputs_prefix"
    assert r_a.outputs == "mock_outputs"
    assert r_a.raw_response == "mock_raw_response"
    assert r_a.readable_output == "mock_readable_output"
    assert r_a.outputs_key_field == "mock_outputs_key_field"


def test_process_result_actions_with_invalid_actions(mocker):
    """Test processing result actions with invalid keys."""
    from CommonServerPython import CommandResults
    from IdentityRecordedFuture import Actions

    actions = Actions(rf_client=None)

    response = {
        "result_actions": [
            {"InvalidKey": {}},
            {
                "CommandResults": {
                    "outputs_prefix": "mock_prefix",
                    "outputs": "mock_outputs",
                }
            },
            {
                "CommandResults": {
                    "outputs_prefix": "another_prefix",
                    "outputs": "another_outputs",
                }
            },
        ]
    }

    result = actions._process_result_actions(response)

    assert len(result) == 2
    assert isinstance(result[0], CommandResults)
    assert result[0].outputs_prefix == "mock_prefix"
    assert result[0].outputs == "mock_outputs"
    assert isinstance(result[1], CommandResults)
    assert result[1].outputs_prefix == "another_prefix"
    assert result[1].outputs == "another_outputs"


def test_fetch_incidents_with_attachment(mocker):
    """
    Test the `fetch_incidents` method to ensure it correctly processes incidents with attachments.
    """
    import json

    import CommonServerPython as csp
    import demistomock as demisto
    from IdentityRecordedFuture import Actions

    client = create_client()
    screenshot_dict = {
        "panel_evidence_summary": {
            "screenshots": [
                {
                    "image_id": "an_id",
                    "base64": "YWJhc2U2NHN0cmluZw==",
                    "description": "vivid description of image",
                }
            ]
        }
    }
    mock_incidents_value = {
        "name": "incident_name",
        "rawJSON": json.dumps(screenshot_dict),
    }

    mock_demisto_last_run_value = "mock_demisto_last_run"

    mock_client_fetch_incidents_response = {
        "incidents": [mock_incidents_value],
        "demisto_last_run": mock_demisto_last_run_value,
    }

    mock_client_fetch_incidents = mocker.patch.object(
        client, "fetch_incidents", return_value=mock_client_fetch_incidents_response
    )

    mock_demisto_incidents = mocker.patch.object(demisto, "incidents")
    mock_demisto_set_last_run = mocker.patch.object(demisto, "setLastRun")
    mock_file_result = mocker.patch.object(
        csp,
        "fileResult",
        return_value={"File": "mockfilepath", "FileID": "mock_file_id"},
    )

    mock_incidents_value.update(
        {
            "attachment": {
                "description": "vivid description of image",
                "showMediaFile": True,
            }.update(mock_file_result)
        }
    )
    actions = Actions(client)

    actions.fetch_incidents()

    mock_client_fetch_incidents.assert_called_once_with()

    mock_demisto_incidents.assert_called_once_with([mock_incidents_value])

    mock_demisto_set_last_run.assert_called_once_with(mock_demisto_last_run_value)


def test_transform_incidents_attachments_without_screenshots(mocker):
    """Test transforming incidents without screenshots."""
    import json

    from IdentityRecordedFuture import Actions

    incidents = [{"rawJSON": json.dumps({"panel_evidence_summary": {}})}]

    mock_fileResult = mocker.patch("IdentityRecordedFuture.fileResult")

    Actions._transform_incidents_attachments(incidents)

    assert "attachment" not in incidents[0]
    mock_fileResult.assert_not_called()


def test_fetch_incidents_with_incidents_present(mocker):
    """
    Test the `fetch_incidents` method to ensure it correctly processes incidents when incidents are present in the response.
    """
    import demistomock as demisto
    from IdentityRecordedFuture import Actions

    client = create_client()

    mock_incidents_value = [
        {"mock_incident_key1": "mock_incident_value1"},
        {"mock_incident_key2": "mock_incident_value2"},
    ]

    mock_demisto_last_run_value = "mock_demisto_last_run"

    mock_alerts_update_data_value = "mock_alerts_update_data_value"

    mock_client_fetch_incidents_response = {
        "incidents": mock_incidents_value,
        "demisto_last_run": mock_demisto_last_run_value,
        "data": "mock",
        "alerts_update_data": mock_alerts_update_data_value,
    }
    mock_client_fetch_incidents = mocker.patch.object(
        client, "fetch_incidents", return_value=mock_client_fetch_incidents_response
    )

    mock_demisto_incidents = mocker.patch.object(demisto, "incidents")
    mock_demisto_set_last_run = mocker.patch.object(demisto, "setLastRun")

    actions = Actions(client)

    actions.fetch_incidents()

    mock_client_fetch_incidents.assert_called_once_with()

    mock_demisto_incidents.assert_called_once_with(mock_incidents_value)
    mock_demisto_set_last_run.assert_called_once_with(mock_demisto_last_run_value)


def test_playbook_alert_details_command_with_result_actions(mocker):
    """
    Test the `playbook_alert_details_command` method to ensure it correctly processes result actions.
    """
    from IdentityRecordedFuture import Actions

    client = create_client()

    mock_response = "mock_response"

    mock_client_playbook_alert_details = mocker.patch.object(client, "details_playbook_alerts", return_value=mock_response)

    actions = Actions(client)

    mock_process_result_actions_return_value = "mock_process_result_actions_return_value"
    mock_process_result_actions = mocker.patch.object(
        actions,
        "_process_result_actions",
        return_value=mock_process_result_actions_return_value,
    )

    result = actions.playbook_alert_details_command()

    mock_client_playbook_alert_details.assert_called_once_with()

    mock_process_result_actions.assert_called_once_with(response=mock_response)

    # As there are some result actions - return those result actions.
    assert result == mock_process_result_actions_return_value


def test_playbook_alert_details_command_without_result_actions(mocker):
    """
    Test the `playbook_alert_details_command` method to ensure it handles the case when there are no result actions.
    """
    from IdentityRecordedFuture import Actions

    client = create_client()

    mock_response = "mock_response"

    mock_client_playbook_alert_details = mocker.patch.object(client, "details_playbook_alerts", return_value=mock_response)

    actions = Actions(client)

    mock_process_result_actions_return_value = None
    mock_process_result_actions = mocker.patch.object(
        actions,
        "_process_result_actions",
        return_value=mock_process_result_actions_return_value,
    )

    actions.playbook_alert_details_command()

    mock_client_playbook_alert_details.assert_called_once_with()

    mock_process_result_actions.assert_called_once_with(response=mock_response)


def test_playbook_alert_search_command_without_result_actions(mocker):
    """
    Test the `playbook_alert_search_command` method to ensure it handles the case when there are no result actions.
    """
    from IdentityRecordedFuture import Actions

    client = create_client()

    mock_response = "mock_response"

    mock_client_playbook_alert_search = mocker.patch.object(client, "search_playbook_alerts", return_value=mock_response)

    actions = Actions(client)

    mock_process_result_actions_return_value = None
    mock_process_result_actions = mocker.patch.object(
        actions,
        "_process_result_actions",
        return_value=mock_process_result_actions_return_value,
    )

    actions.playbook_alert_search_command()

    mock_client_playbook_alert_search.assert_called_once_with()

    mock_process_result_actions.assert_called_once_with(response=mock_response)


def test_playbook_alert_update_command(mocker):
    """
    Test the `playbook_alert_update_command` method to ensure it correctly processes result actions.
    """
    from IdentityRecordedFuture import Actions

    client = create_client()

    mock_response = "mock_response"

    mock_client_alert_set_status = mocker.patch.object(client, "update_playbook_alerts", return_value=mock_response)

    actions = Actions(client)

    mock_process_result_actions_return_value = "mock_process_result_actions_return_value"
    mock_process_result_actions = mocker.patch.object(
        actions,
        "_process_result_actions",
        return_value=mock_process_result_actions_return_value,
    )

    result = actions.playbook_alert_update_command()

    mock_client_alert_set_status.assert_called_once_with()

    mock_process_result_actions.assert_called_once_with(response=mock_response)

    assert result == mock_process_result_actions_return_value


def test_test_module(mocker):
    """
    Test the `test-module` command to ensure it verifies the integration setup correctly.
    """
    import platform

    import demistomock as demisto
    import IdentityRecordedFuture

    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "demistoVersion", return_value={"version": "mock_version"})
    mocker.patch.object(demisto, "params", return_value={"credential": {"password": "example"}})
    mocker.patch.object(platform, "platform", return_value="mock_platform")
    mocker.patch.object(IdentityRecordedFuture.Client, "whoami")
    mocked_return_res = mocker.patch.object(IdentityRecordedFuture, "return_results")
    IdentityRecordedFuture.main()
    mocked_return_res.assert_called_with("ok")


def test_test_module_with_boom(mocker):
    """
    Test the `test-module` command to ensure it handles exceptions and returns the appropriate error message.
    """
    import platform

    import demistomock as demisto
    import IdentityRecordedFuture

    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "demistoVersion", return_value={"version": "mock_version"})
    mocker.patch.object(demisto, "params", return_value={"credential": {"password": "example"}})
    mocker.patch.object(platform, "platform", return_value="mock_platform")
    mock_whoami = mocker.patch.object(IdentityRecordedFuture.Client, "whoami")
    mock_whoami.side_effect = Exception("Side effect triggered")
    mocked_return_err = mocker.patch.object(IdentityRecordedFuture, "return_error")
    IdentityRecordedFuture.main()

    mocked_return_err.assert_called_with(
        message=(
            f"Failed to execute {demisto.command()} command. Error: Failed due to - "
            "Unknown error. Please verify that the API URL and Token are correctly configured. "
            "RAW Error: Side effect triggered"
        ),
        error=mocker.ANY,
    )


@patch("IdentityRecordedFuture.demisto")
@patch("IdentityRecordedFuture.Client")
@patch("IdentityRecordedFuture.Actions")
def test_main_general(
    actions_mock: Mock,
    client_mock: Mock,
    mocked_demisto: Mock,
):
    """Test main function if it runs correctly and calls general functions"""
    import IdentityRecordedFuture

    # Mocking a known command to ensure that the 'else' block is not triggered
    mocked_demisto.command.return_value = "test-module"
    mocked_demisto.params.return_value = {
        "server_url": "https://example.com",
        "credential": {"password": "example"},
    }

    IdentityRecordedFuture.main()

    client_mock.assert_called_once()
    mocked_demisto.params.assert_called_once_with()
    mocked_demisto.command.assert_called_once_with()
    actions_mock.assert_called_once_with(client_mock.return_value)


def test_main_with_unknown_command(mocker):
    """Test main function with an unknown command."""
    import demistomock as demisto
    import IdentityRecordedFuture

    mocker.patch.object(demisto, "command", return_value="unknown-command")
    mock_return_error = mocker.patch("IdentityRecordedFuture.return_error")
    mock_get_client = mocker.patch("IdentityRecordedFuture.get_client")

    IdentityRecordedFuture.main()

    mock_get_client.assert_called_once()
    mock_return_error.assert_called_once_with(message="Unknown command: unknown-command")


def test_main_exception_handling(mocker):
    """Test main function's exception handling."""
    import demistomock as demisto
    import IdentityRecordedFuture

    mocker.patch.object(demisto, "command", return_value="test-module")
    mock_get_client = mocker.patch("IdentityRecordedFuture.get_client")
    mock_get_client.return_value.whoami.side_effect = Exception("Test exception")
    mock_return_error = mocker.patch("IdentityRecordedFuture.return_error")

    IdentityRecordedFuture.main()

    mock_get_client.assert_called_once()
    mock_return_error.assert_called_once_with(
        message=(
            "Failed to execute test-module command. Error: Failed due to - Unknown error. "
            "Please verify that the API URL and Token are correctly configured. RAW Error: Test exception"
        ),
        error=mocker.ANY,
    )


def test_get_client_no_api_token(mocker):
    """Test get_client when no API token is provided."""
    import demistomock as demisto
    import IdentityRecordedFuture

    mock_params = {
        "server_url": "https://api.recordedfuture.com/gw/xsoar-identity/",
        "unsecure": False,
        "credential": {"password": None},
        "token": None,
    }
    mocker.patch.object(demisto, "params", return_value=mock_params)
    mock_return_error = mocker.patch("IdentityRecordedFuture.return_error")

    proxies = {}
    IdentityRecordedFuture.get_client(proxies=proxies)

    mock_return_error.assert_called_once_with(message="Please provide a valid API token")


@patch("IdentityRecordedFuture.Client")
def test_get_client_with_proxy(client_mock, mocker):
    """Test get_client when proxy is used."""
    import demistomock as demisto
    import IdentityRecordedFuture

    server_url = "https://api.recordedfuture.com/gw/xsoar-identity/"

    unsecure = False
    verify_ssl = not unsecure

    mock_params = {
        "server_url": server_url,
        "unsecure": unsecure,
        "credential": {"password": "example"},
        "proxy": True,
    }
    mocker.patch.object(demisto, "params", return_value=mock_params)

    proxies = {"http": "example.com", "https": "example.com"}
    IdentityRecordedFuture.get_client(proxies=proxies)

    client_mock.assert_called_once_with(
        base_url=server_url.rstrip("/"),
        verify=verify_ssl,
        headers=mocker.ANY,
        proxy=bool(proxies),
    )


@patch("IdentityRecordedFuture.Client")
def test_get_client_without_proxy(client_mock, mocker):
    """Test get_client when proxy is not used."""
    import demistomock as demisto
    import IdentityRecordedFuture

    server_url = "https://api.recordedfuture.com/gw/xsoar-identity/"

    unsecure = False
    verify_ssl = not unsecure

    mock_params = {
        "server_url": server_url,
        "unsecure": unsecure,
        "credential": {"password": "example"},
        "proxy": False,
    }
    mocker.patch.object(demisto, "params", return_value=mock_params)

    proxies = {}
    IdentityRecordedFuture.get_client(proxies=proxies)

    client_mock.assert_called_once_with(
        base_url=server_url.rstrip("/"),
        verify=verify_ssl,
        headers=mocker.ANY,
        proxy=bool(proxies),
    )


def test_main_calls_handle_proxy(mocker):
    """
    Test main function to ensure it calls handle_proxy() and provides proxies to the get_client.
    """
    import demistomock as demisto
    import IdentityRecordedFuture

    mock_proxies = {"http": "example.com", "https": "example.com"}
    mock_handle_proxy = mocker.patch("IdentityRecordedFuture.handle_proxy", return_value=mock_proxies)

    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "params", return_value={"credential": {"password": "example"}})
    mock_get_client = mocker.patch("IdentityRecordedFuture.get_client")
    mocker.patch("IdentityRecordedFuture.Client.whoami")
    mock_return_results = mocker.patch("IdentityRecordedFuture.return_results")

    IdentityRecordedFuture.main()

    mock_handle_proxy.assert_called_once()

    mock_get_client.assert_called_once_with(proxies=mock_proxies)

    mock_return_results.assert_called_once_with("ok")