CTM360_CyberBlindspot

Take action on incidents derived from CTM360 CBS threat intelligence that is directly linked to your organization.

Analytics & SIEM · CTM360

Details

IDCTM360_CyberBlindspot
ProviderCTM360
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Take action on incidents derived from CTM360 CBS threat intelligence that is directly linked to your organization.
This integration was integrated and tested with version 1.0.0 of CTM360_CyberBlindspot.

Configure CTM360 CyberBlindspot in Cortex

Parameter Description Required
Incident Mirroring Direction Choose the direction to mirror the incident: Incoming (from CyberBlindspot to Cortex XSOAR), Outgoing (from Cortex XSOAR to CyberBlindspot), or Incoming and Outgoing (from/to Cortex XSOAR and CyberBlindspot). False
Module To Use The module to use: Incidents, Compromised Cards, Breached Credentials, Malware Logs, Domain/Subdomain Infringements, Social Media Fraud, Gambling Sites, or Money Mules. False
Retrieve Screenshots   False
First fetch (<number> <time unit>, e.g., 12 hours. Default is 7 days) The time the incidents should be fetched starting from. False
API Key The CTM360 CyberBlindspot API Key to use for fetching data. True
Maximum Number of Incidents per Fetch Default is 25. Maximum is 200. True
Fetch incidents   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Incident type   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.

ctm360-cbs-incident-list


Get the list of incidents from CBS.

Base Command

ctm360-cbs-incident-list

Input

Argument Name Description Required
dateFrom Select “From” date to fetch incidents starting from it. Optional
dateTo Select “To” date to fetch incidents up to it. Optional
maxHits Set number of results to fetch. Optional
order Set the order of the results. Optional

Context Output

Path Type Description
CyberBlindspot.IncidentList unknown List of all CBS incidents.

Command example

!ctm360-cbs-incident-list dateFrom="23-10-2023 07:00" dateTo="23-10-2023 23:00" order=asc maxHits=2

Context Example

{
    "CyberBlindspot": {
        "IncidentList": [
            {
                "CustomFields": {
                    "cbs_class": "Link",
                    "cbs_coa": "Member Side Action",
                    "cbs_status": "Member Feedback",
                    "cbs_subject": "2 customer credentials compromised (5d65815)",
                    "cbs_timestamp": 1698049692779,
                    "cbs_type": "Leaked Credential",
                    "cbs_updated_date": "2023-10-23T08:00:00+00:00"
                },
                "externalstatus": "Member Feedback",
                "name": "New leaked_credential with severity High found",
                "occurred": "2023-10-23T08:00:00+00:00",
                "rawJson": "{\"name\": \"New leaked_credential with severity High found\", \"occurred\": \"2023-10-23T08:00:00+00:00\", \"type\": \"Leaked Credential\", \"externalstatus\": \"Member Feedback\", \"severity\": 3, \"CustomFields\": {\"cbs_status\": \"Member Feedback\", \"cbs_subject\": \"2 customer credentials compromised (5d65815)\", \"cbs_class\": \"Link\", \"cbs_type\": \"Leaked Credential\", \"cbs_coa\": \"Member Side Action\", \"cbs_timestamp\": 1698049692779, \"cbs_updated_date\": \"2023-10-23T08:00:00+00:00\"}, \"xsoar_mirroring\": {\"mirror_direction\": \"Both\", \"mirror_id\": \"COMY123642991153\", \"mirror_instance\": \"CTM360_CyberBlindspot_instance_1\"}}",
                "severity": 3,
                "type": "Leaked Credential",
                "xsoar_mirroring": {
                    "mirror_direction": "Both",
                    "mirror_id": "COMY123642991153",
                    "mirror_instance": "CTM360_CyberBlindspot_instance_1"
                }
            },
            {
                "CustomFields": {
                    "cbs_class": "Link",
                    "cbs_coa": "Member Side Action",
                    "cbs_status": "Member Feedback",
                    "cbs_subject": "2 customer credentials compromised (a86fda8)",
                    "cbs_timestamp": 1698051145410,
                    "cbs_type": "Leaked Credential",
                    "cbs_updated_date": "2023-10-23T08:00:00+00:00"
                },
                "externalstatus": "Member Feedback",
                "name": "New leaked_credential with severity High found",
                "occurred": "2023-10-23T08:00:00+00:00",
                "rawJson": "{\"name\": \"New leaked_credential with severity High found\", \"occurred\": \"2023-10-23T08:00:00+00:00\", \"type\": \"Leaked Credential\", \"externalstatus\": \"Member Feedback\", \"severity\": 3, \"CustomFields\": {\"cbs_status\": \"Member Feedback\", \"cbs_subject\": \"2 customer credentials compromised (a86fda8)\", \"cbs_class\": \"Link\", \"cbs_type\": \"Leaked Credential\", \"cbs_coa\": \"Member Side Action\", \"cbs_timestamp\": 1698051145410, \"cbs_updated_date\": \"2023-10-23T08:00:00+00:00\"}, \"xsoar_mirroring\": {\"mirror_direction\": \"Both\", \"mirror_id\": \"COMY123073588255\", \"mirror_instance\": \"CTM360_CyberBlindspot_instance_1\"}}",
                "severity": 3,
                "type": "Leaked Credential",
                "xsoar_mirroring": {
                    "mirror_direction": "Both",
                    "mirror_id": "COMY123073588255",
                    "mirror_instance": "CTM360_CyberBlindspot_instance_1"
                }
            }
        ]
    }
}

Human Readable Output

Results

CustomFields externalstatus name occurred rawJson severity type xsoar_mirroring
cbs_status: Member Feedback
cbs_subject: 2 customer credentials compromised (5d65815)
cbs_class: Link
cbs_type: Leaked Credential
cbs_coa: Member Side Action
cbs_timestamp: 1698049692779
cbs_updated_date: 2023-10-23T08:00:00+00:00
Member Feedback New leaked_credential with severity High found 2023-10-23T08:00:00+00:00 {“name”: “New leaked_credential with severity High found”, “occurred”: “2023-10-23T08:00:00+00:00”, “type”: “Leaked Credential”, “externalstatus”: “Member Feedback”, “severity”: 3, “CustomFields”: {“cbs_status”: “Member Feedback”, “cbs_subject”: “2 customer credentials compromised (5d65815)”, “cbs_class”: “Link”, “cbs_type”: “Leaked Credential”, “cbs_coa”: “Member Side Action”, “cbs_timestamp”: 1698049692779, “cbs_updated_date”: “2023-10-23T08:00:00+00:00”}, “xsoar_mirroring”: {“mirror_direction”: “Both”, “mirror_id”: “COMY123642991153”, “mirror_instance”: “CTM360_CyberBlindspot_instance_1”}} 3 Leaked Credential mirror_direction: Both
mirror_id: COMY123642991153
mirror_instance: CTM360_CyberBlindspot_instance_1
cbs_status: Member Feedback
cbs_subject: 2 customer credentials compromised (a86fda8)
cbs_class: Link
cbs_type: Leaked Credential
cbs_coa: Member Side Action
cbs_timestamp: 1698051145410
cbs_updated_date: 2023-10-23T08:00:00+00:00
Member Feedback New leaked_credential with severity High found 2023-10-23T08:00:00+00:00 {“name”: “New leaked_credential with severity High found”, “occurred”: “2023-10-23T08:00:00+00:00”, “type”: “Leaked Credential”, “externalstatus”: “Member Feedback”, “severity”: 3, “CustomFields”: {“cbs_status”: “Member Feedback”, “cbs_subject”: “2 customer credentials compromised (a86fda8)”, “cbs_class”: “Link”, “cbs_type”: “Leaked Credential”, “cbs_coa”: “Member Side Action”, “cbs_timestamp”: 1698051145410, “cbs_updated_date”: “2023-10-23T08:00:00+00:00”}, “xsoar_mirroring”: {“mirror_direction”: “Both”, “mirror_id”: “COMY123073588255”, “mirror_instance”: “CTM360_CyberBlindspot_instance_1”}} 3 Leaked Credential mirror_direction: Both
mirror_id: COMY123073588255
mirror_instance: CTM360_CyberBlindspot_instance_1

ctm360-cbs-incident-close


Close a CBS incident.

Base Command

ctm360-cbs-incident-close

Input

Argument Name Description Required
ticketId “Ticket ID” of the incident to close. Required

Context Output

There is no context output for this command.

Command example

!ctm360-cbs-incident-close ticketId="COMX41148897294"

Human Readable Output

Incident closed successfully

ctm360-cbs-incident-request-takedown


Request a takedown of the asset where the incident was found.

Base Command

ctm360-cbs-incident-request-takedown

Input

Argument Name Description Required
ticketId “Ticket ID” of the incident to request takedown. Required

Context Output

There is no context output for this command.

Command example

!ctm360-cbs-incident-request-takedown ticketId="COMX415993788418"

Human Readable Output

Takedown request executed successfully

get-mapping-fields


Returns the list of fields for an incident type.

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

ctm360-cbs-incident-details


Fetch details of a single incident from the CyberBlindspot platform.

Base Command

ctm360-cbs-incident-details

Input

Argument Name Description Required
ticketId “Ticket ID” of the incident to close. Required

Context Output

Path Type Description
CyberBlindspot.RemoteIncident.id unknown The unique ID for the incident record.
CyberBlindspot.RemoteIncident.brand unknown The organization the incident is associated with.
CyberBlindspot.RemoteIncident.coa unknown The course of action to take.
CyberBlindspot.RemoteIncident.class unknown The classification of the incident on remote server.
CyberBlindspot.RemoteIncident.status unknown The current status of the incident on remote server.
CyberBlindspot.RemoteIncident.severity unknown The severity of the incident.
CyberBlindspot.RemoteIncident.subject unknown The asset or title of the incident.
CyberBlindspot.RemoteIncident.type unknown The incident type on the remote server.
CyberBlindspot.RemoteIncident.remarks unknown The remarks about the incident.
CyberBlindspot.RemoteIncident.created_date unknown The creation date of the incident (legacy).
CyberBlindspot.RemoteIncident.updated_date unknown The date the incident last got updated (legacy).
CyberBlindspot.RemoteIncident.first_seen unknown The creation date of the incident.
CyberBlindspot.RemoteIncident.last_seen unknown The date the incident last got updated.
CyberBlindspot.RemoteIncident.timestamp unknown The timestamp of when the record was created.
CyberBlindspot.RemoteIncident.card_number unknown The compromised card’s number.
CyberBlindspot.RemoteIncident.cvv unknown The compromised card’s Card Verification Value (CVV).
CyberBlindspot.RemoteIncident.expiry_month unknown The compromised card’s expiration month.
CyberBlindspot.RemoteIncident.expiry_year unknown The compromised card’s expiration year.
CyberBlindspot.RemoteIncident.breach_source unknown The source of the breached data.
CyberBlindspot.RemoteIncident.domain unknown The domain related to the breached data or compromised device.
CyberBlindspot.RemoteIncident.email unknown The email found in the breached data.
CyberBlindspot.RemoteIncident.username unknown The username found in the breached data.
CyberBlindspot.RemoteIncident.password unknown The password found in the breached data or compromised account.
CyberBlindspot.RemoteIncident.executive_name unknown The executive member’s name related to the breached data.
CyberBlindspot.RemoteIncident.confirmation_time unknown The time of infringement confirmation.
CyberBlindspot.RemoteIncident.risks unknown The potential difficulties carried by the infringement.
CyberBlindspot.RemoteIncident.incident_status unknown The status of the infringement incident.
CyberBlindspot.RemoteIncident.screenshots unknown The screenshot evidence if available.
CyberBlindspot.RemoteIncident.date_compromised unknown The date of the compromise.
CyberBlindspot.RemoteIncident.computer_name unknown The name of the computer that was compromised.
CyberBlindspot.RemoteIncident.operating_system unknown The operating system of the compromised device.
CyberBlindspot.RemoteIncident.malware_path unknown The path in which the malware was installed on the compromised device.
CyberBlindspot.RemoteIncident.url_path unknown The path of the URL.
CyberBlindspot.RemoteIncident.masked_password unknown The masked password of the compromised account.
CyberBlindspot.RemoteIncident.software unknown The software that was compromised.
CyberBlindspot.RemoteIncident.user unknown The user of the compromised account.
CyberBlindspot.RemoteIncident.user_domain unknown The domain of the compromised user.
CyberBlindspot.RemoteIncident.website unknown The website that was compromised.
CyberBlindspot.RemoteIncident.sources unknown The sources reporting the compromise.
CyberBlindspot.RemoteIncident.source_uri unknown The source URI of the compromise report.
CyberBlindspot.RemoteIncident.hostname unknown The hostname of the compromised device.
CyberBlindspot.RemoteIncident.stealer_family unknown The family of the malware.
CyberBlindspot.RemoteIncident.external_link unknown The external link to the remote platform.
CyberBlindspot.RemoteIncident.compromise_details unknown The details of the compromise.
CyberBlindspot.RemoteIncident.platform unknown The social network platform for social media fraud findings.
CyberBlindspot.RemoteIncident.risk_score unknown The numeric risk score from CBS.
CyberBlindspot.RemoteIncident.money_mule_id unknown The CBS money mule finding ID.
CyberBlindspot.RemoteIncident.account_identifier unknown The account identifier tied to the money mule.
CyberBlindspot.RemoteIncident.suspect_names unknown The names associated with the money mule.
CyberBlindspot.RemoteIncident.suspect_emails unknown The email addresses associated with the money mule.
CyberBlindspot.RemoteIncident.suspect_phones unknown The phone numbers associated with the money mule.
CyberBlindspot.RemoteIncident.transfer_amount unknown The transfer amount when present.
CyberBlindspot.RemoteIncident.transfer_currency unknown The currency code for the transfer.
CyberBlindspot.RemoteIncident.bank_account_holder_name unknown The name on the bank account.
CyberBlindspot.RemoteIncident.bank_name unknown The bank name tied to the money mule.
CyberBlindspot.RemoteIncident.bank_account_country unknown The country of the bank account.
CyberBlindspot.RemoteIncident.bic unknown The Bank Identifier Code.
CyberBlindspot.RemoteIncident.finding_id unknown The CBS gambling-site finding ID.
CyberBlindspot.RemoteIncident.url unknown The primary gambling site URL.
CyberBlindspot.RemoteIncident.submitted_url unknown The URL submitted to CBS for scanning.
CyberBlindspot.RemoteIncident.landing_url unknown The landing page URL observed for the site.
CyberBlindspot.RemoteIncident.title unknown The page title observed during scan.
CyberBlindspot.RemoteIncident.resolving_ip unknown The resolved IP for the gambling site.
CyberBlindspot.RemoteIncident.tags unknown The tags applied to the gambling site finding.
CyberBlindspot.RemoteIncident.status_code unknown The HTTP status code from scan.
CyberBlindspot.RemoteIncident.url_status unknown The URL reachability status.
CyberBlindspot.RemoteIncident.scan_status unknown The scan completion status.
CyberBlindspot.RemoteIncident.enrichment unknown The DNS enrichment payload.
CyberBlindspot.RemoteIncident.external_links unknown The external links discovered on the gambling site.
CyberBlindspot.RemoteIncident.internal_links unknown The internal links discovered on the gambling site.

ctm360-cbs-incident-retrieve-screenshots


Retrieves screenshot evidence if available.

Base Command

ctm360-cbs-incident-retrieve-screenshots

Input

Argument Name Description Required
files The files to retrieve. Required

Context Output

Path Type Description
InfoFile.Name string FileName.
InfoFile.EntryID string The EntryID of the report.
InfoFile.Size number File Size.
InfoFile.Type string File type e.g. “PE”.
InfoFile.Info string Basic information of the file.

get-remote-data


Gets remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.

Base Command

get-remote-data

Input

Argument Name Description Required
id The incident ID. Required
lastUpdate Retrieves entries that were created after lastUpdate. Required

Context Output

There is no context output for this command.

get-modified-remote-data


Gets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available in Cortex XSOAR from version 6.1.

Base Command

get-modified-remote-data

Input

Argument Name Description Required
lastUpdate A date string in local time representing the last time the incident was updated. The incident is only returned if it was modified after the last update time. Required

Context Output

There is no context output for this command.

update-remote-system


Updates the remote system with local changes.

Base Command

update-remote-system

Input

Argument Name Description Required
remoteId Remote ID of incident to update in the remote system. Required

Context Output

There is no context output for this command.

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and CTM360 CyberBlindspot corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:

  1. Enable Fetching incidents in your instance configuration.
  2. In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:

    Option Description
    None Turns off incident mirroring.
    Incoming Any changes in CTM360 CyberBlindspot events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents.
    Outgoing Any changes in Cortex XSOAR incidents will be reflected in CTM360 CyberBlindspot events (outgoing mirrored fields).
    Incoming And Outgoing Changes in Cortex XSOAR incidents and CTM360 CyberBlindspot events will be reflected in both directions.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and CTM360 CyberBlindspot.

Configuration parameters

  • mirror_direction — Incident Mirroring Direction
  • module_to_use — Module To Use
  • retrieve_screenshots — Retrieve Screenshots
  • first_fetch — First fetch (<number> <time unit>, e.g., 12 hours. Default is `7 days`)
  • api_key — (required)
  • max_fetch — Maximum Number of Incidents per Fetch (required)
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (9)

  • ctm360-cbs-incident-close

    Close a CBS incident.

  • ctm360-cbs-incident-details

    Fetch details of a single incident from the CyberBlindspot platform.

  • ctm360-cbs-incident-list

    Get the list of incidents from CBS.

  • ctm360-cbs-incident-request-takedown

    Request a takedown of the asset where the incident was found.

  • ctm360-cbs-incident-retrieve-screenshots

    Retrieves screenshot evidence if available.

  • get-mapping-fields

    Returns the list of fields for an incident type.

  • get-modified-remote-data

    Gets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available in Cortex XSOAR from version 6.1.

  • get-remote-data

    Gets remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.

  • update-remote-system

    Updates the remote system with local changes.

import logging
from datetime import datetime
from unittest.mock import patch

import pytest
from CommonServerPython import (
    CommandResults,
    DemistoException,
    EntryType,
    IncidentSeverity,
    IncidentStatus,
)
from CyberBlindspot import (
    ABSOLUTE_MAX_FETCH,
    CBS_CARD_FIELDS,
    CBS_CRED_FIELDS,
    CBS_DOMAIN_INFRINGE_FIELDS,
    CBS_GS_FIELDS,
    CBS_INCIDENT_FIELDS,
    CBS_INCOMING_DATE_FORMAT,
    CBS_MALWARE_LOG_FIELDS,
    CBS_MM_FIELDS,
    CBS_OUTGOING_DATE_FORMAT,
    CBS_SMF_FIELDS,
    LOGGING_PREFIX,
)
from dateparser import parse

"""CONSTANTS"""
BASE_URL = "https://example.com:443"


STATUS_ENTRIES = {
    "wip": "Incident is undergoing a process on CyberBlindspot. Actions will be unavailable until processing is done.",
    "closed": "Incident was closed on CyberBlindspot.",
    "monitoring": "Incident is being monitored on CyberBlindspot. Actions will be unavailable until monitoring is done.",
    "resolved": "Incident was resolved by response team on CyberBlindspot. Not closed yet.",
    "disregarded": "Incident was disregarded on CyberBlindspot. Not closed yet.",
    "unconfirmed": "Incident was unconfirmed on CyberBlindspot. Not closed yet.",
    "auto_resolved": "Incident was resolved through monitoring on CyberBlindspot. Not closed yet.",
}

MODULES = [
    (
        "incidents",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_INCIDENT_FIELDS,
    ),
    (
        "compromised_cards",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_CARD_FIELDS,
    ),
    (
        "breached_credentials",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_CRED_FIELDS,
    ),
    (
        "domain_infringement",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_DOMAIN_INFRINGE_FIELDS,
    ),
    (
        "subdomain_infringement",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_DOMAIN_INFRINGE_FIELDS,
    ),
    (
        "malware_logs",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_MALWARE_LOG_FIELDS,
    ),
    (
        "social_media_fraud",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_SMF_FIELDS,
    ),
    (
        "money_mules",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_MM_FIELDS,
    ),
    (
        "gambling_sites",
        "CyberBlindspot.IncidentList",
        "CyberBlindspot.RemoteIncident",
        CBS_GS_FIELDS,
    ),
]


def load_mock_response(file_name: str) -> dict | list:
    """
    Given:
        - Name of json file inside `test-data` directory. Load mock file that simulates an API response.
    When:
        - load_mock_response is called.
    Then:
        - Read the file requested into a dictionary and return it.
    """
    import json
    import os

    with open(os.path.join("test_data", file_name), encoding="utf-8") as mock_file:
        return json.loads(mock_file.read())


""" MOCK CLIENT"""


@pytest.fixture()
def mock_client():
    """
    Given: Nothing
    When:
        - mock_client is called.
    Then:
        - Return a new mock CyberBlindspot Client.
    """
    from CyberBlindspot import Client

    return Client(
        base_url="https://example.com",
        verify=False,
        headers={"api-key": "some_mock_api_key"},
    )


""" MOCK HASHES"""


@pytest.fixture()
def mock_last_fetch_ids():
    """
    Given: Nothing
    When:
        - mock_last_fetch_ids is called.
    Then:
        - Return a list of ids.
    """

    def mock_ids(module_type: str):
        match module_type:
            case "compromised_cards":
                return [
                    "LC-A1E202F49",
                    "LC-A1R252F49",
                    "LC-A1CV42F49",
                ]
            case "breached_credentials":
                return [
                    "BC-7FPF679C6",
                    "BC-2EL85279F",
                    "BC-1EF22EW64",
                ]
            case "domain_infringement":
                return [
                    "RD-D7A4ABBC5",
                    "RD-E4F5B5E7E",
                    "RD-9F1AF54E4",
                ]
            case "subdomain_infringement":
                return [
                    "RD-FA144D66C",
                    "RD-5049BCA31",
                    "RD-FBD354127",
                ]
            case "malware_logs":
                return [
                    "ML-904CE0968",
                    "ML-DEAF1ABD5",
                    "ML-919AE2387",
                ]
            case _:
                return [
                    "COMX123456123456",
                    "COMX123456654321",
                    "COMX987654",
                ]

    return mock_ids


""" HELPER FUNCTION TESTS"""


@pytest.mark.parametrize(
    "cyberblindspot_severity, expected_xsoar_severity",
    [
        ("fyi", 0),
        ("info", 0.5),
        ("informational", 0.5),
        ("low", 1),
        ("medium", 2),
        ("high", 3),
        ("critical", 4),
    ],
)
def test_convert_to_demisto_severity(cyberblindspot_severity, expected_xsoar_severity):
    """
    Given:
        - A string represents a CyberBlindspot severity.
    When:
        - convert_to_demisto_severity is called.
    Then:
        - Verify that the severity was correctly translated to a Cortex XSOAR severity.
    """
    from CyberBlindspot import convert_to_demisto_severity

    assert convert_to_demisto_severity(cyberblindspot_severity) == expected_xsoar_severity


@pytest.mark.parametrize(
    "mock_log_type,mock_log_function,mock_message,mock_asserts",
    [
        (
            [logging.INFO, "INFO"],
            "log",
            "This is a log message at debug level",
            f"{LOGGING_PREFIX} This is a log message at debug level",
        ),
        (
            [logging.INFO, "INFO"],
            "log",
            "This is a log message at info level",
            f"{LOGGING_PREFIX} This is a log message at info level",
        ),
        (
            [logging.INFO, "INFO"],
            "log",
            "This is a log message at error level",
            f"{LOGGING_PREFIX} This is a log message at error level",
        ),
    ],
)
def test_log(mock_log_type, mock_log_function, mock_message, mock_asserts, caplog):
    """
    Given:
        - Demisto log level.
        - Message to log.
    When:
        - helper function log is called.
    Then:
        - Ensure logged message is as expected.
    """
    from CyberBlindspot import log

    logging.getLogger().propagate = True
    with caplog.at_level(mock_log_type[0]):
        log(mock_log_type[1], mock_message)
    assert mock_asserts in caplog.text


@pytest.mark.parametrize(
    "mock_input,mock_args,mock_asserts",
    [
        (
            "",
            {"timestamp": False, "input_format_string": "", "output_format": "", "kwargs": {}},
            parse("", [""]),
        ),
        (
            "",
            {"timestamp": False, "input_format_string": "NOT_VALID", "output_format": "", "kwargs": {}},
            "",
        ),
        (
            "05-12-2022 11:03:34 PM",
            {"timestamp": False, "input_format_string": CBS_INCOMING_DATE_FORMAT, "output_format": "", "kwargs": {}},
            datetime(2022, 12, 5, 23, 3, 34),
        ),
        (
            "05-12-2022 11:03:34 PM",
            {
                "timestamp": True,
                "input_format_string": CBS_INCOMING_DATE_FORMAT,
                "output_format": "",
                "kwargs": {"settings": {"TIMEZONE": "UTC+3", "TO_TIMEZONE": "UTC"}},
            },
            1670270614000,
        ),
        (
            "05-12-2022 11:03:34 PM",
            {
                "timestamp": False,
                "input_format_string": CBS_INCOMING_DATE_FORMAT,
                "output_format": "",
                "kwargs": {"settings": {"TIMEZONE": "UTC", "TO_TIMEZONE": "UTC"}},
            },
            datetime(2022, 12, 5, 23, 3, 34),
        ),
        (
            "05-12-2022 11:03:34 PM",
            {
                "timestamp": False,
                "input_format_string": CBS_INCOMING_DATE_FORMAT,
                "output_format": CBS_OUTGOING_DATE_FORMAT,
                "kwargs": {"settings": {"TIMEZONE": "UTC", "TO_TIMEZONE": "UTC"}},
            },
            "05-12-2022 23:03",
        ),
    ],
)
def test_convert_time_string(mock_input, mock_args, mock_asserts, capfd, caplog):
    """
    Given:
        - Input time string.
        - Input format string.
        - (Optional) Output format string.
        - (Optional) Timestamp switch
    When:
        - convert_time_string is called.
    Then:
        - Return time as either: another time format, <datetime> object, or timestamp in milliseconds
    """
    from CyberBlindspot import convert_time_string

    with capfd.disabled():
        result = convert_time_string(
            mock_input,
            mock_args["input_format_string"],
            mock_args["output_format"],
            mock_args["timestamp"],
            **mock_args["kwargs"],
        )
        caplog.set_level(logging.INFO)
        assert result == mock_asserts
        if caplog.text:
            assert (
                f"{LOGGING_PREFIX} An error was encountered at `convert_time_string()` \
                err=ValueError('The passed date string and/or format string is not valid')"
                in caplog.text
            )


@pytest.mark.parametrize(
    "value,expected",
    [
        (1642607418000, 1642607418000),
        ("1642607418000", 1642607418000),
        ("2022-03-01T13:02:00", 1646139720000),
        ("", ""),
        (None, None),
    ],
)
def test_normalize_timestamp(value, expected):
    """ISO and numeric-string CBS timestamps must normalize to epoch milliseconds."""
    from CyberBlindspot import normalize_timestamp

    assert normalize_timestamp(value) == expected


@pytest.mark.parametrize(
    "mock_input_file,mock_assert_file,mock_module",
    [
        ("fetch_incidents_response_valid.json", "incident_list_cmd_result_valid.json", MODULES[0][0]),
        ("fetch_cards_response_valid.json", "card_list_cmd_result_valid.json", MODULES[1][0]),
        ("fetch_creds_response_valid.json", "cred_list_cmd_result_valid.json", MODULES[2][0]),
        ("fetch_domains_response_valid.json", "domain_list_cmd_result_valid.json", MODULES[3][0]),
        ("fetch_subdomains_response_valid.json", "subdomain_list_cmd_result_valid.json", MODULES[4][0]),
        ("fetch_malware_logs_response_valid.json", "malware_logs_list_cmd_result_valid.json", MODULES[5][0]),
        ("fetch_social_media_fraud_response_valid.json", "social_media_fraud_list_cmd_result_valid.json", MODULES[6][0]),
        ("fetch_money_mules_response_valid.json", "money_mules_list_cmd_result_valid.json", MODULES[7][0]),
        ("fetch_gambling_sites_response_valid.json", "gambling_sites_list_cmd_result_valid.json", MODULES[8][0]),
    ],
)
def test_map_and_create_incident(mock_input_file, mock_assert_file, mock_module):
    """
    Given:
        - A dictionary of an unmapped incident.
    When:
        - map_and_create_incident is called.
    Then:
        - Create a new incident dictionary that is in XSOAR-appropriate structure and return it.
    """
    from CyberBlindspot import map_and_create_incident

    mock_fetched_incident = load_mock_response(mock_input_file)[0]
    mock_assert = load_mock_response(mock_assert_file)[0]
    mock_assert.pop("rawJson", None)

    with patch("CyberBlindspot.INSTANCE.module", new=mock_module):
        result = map_and_create_incident(mock_fetched_incident)
        result.pop("rawJson", None)
        logging.debug(result)
        logging.debug(mock_assert)
        assert result == mock_assert


@pytest.mark.parametrize(
    "input_file_name,mock_input,mock_asserts,mock_module",
    [
        ("", ([], []), ([], []), MODULES[0][0]),
        ("fetch_incidents_response_valid.json", 2, ([], []), MODULES[0][0]),
        ("fetch_incidents_response_valid.json", -2, ([], []), MODULES[0][0]),
        ("", ([], []), ([], []), MODULES[1][0]),
        ("fetch_cards_response_valid.json", 2, ([], []), MODULES[1][0]),
        ("fetch_cards_response_valid.json", -2, ([], []), MODULES[1][0]),
        ("", ([], []), ([], []), MODULES[2][0]),
        ("fetch_creds_response_valid.json", 2, ([], []), MODULES[2][0]),
        ("fetch_creds_response_valid.json", -2, ([], []), MODULES[2][0]),
        ("", ([], []), ([], []), MODULES[3][0]),
        ("fetch_domains_response_valid.json", 2, ([], []), MODULES[3][0]),
        ("fetch_domains_response_valid.json", -2, ([], []), MODULES[3][0]),
        ("", ([], []), ([], []), MODULES[4][0]),
        ("fetch_subdomains_response_valid.json", 2, ([], []), MODULES[4][0]),
        ("fetch_subdomains_response_valid.json", -2, ([], []), MODULES[4][0]),
        ("", ([], []), ([], []), MODULES[5][0]),
        ("fetch_malware_logs_response_valid.json", 2, ([], []), MODULES[5][0]),
        ("fetch_malware_logs_response_valid.json", -2, ([], []), MODULES[5][0]),
        ("", ([], []), ([], []), MODULES[6][0]),
        ("fetch_social_media_fraud_response_valid.json", 2, ([], []), MODULES[6][0]),
        ("fetch_social_media_fraud_response_valid.json", -2, ([], []), MODULES[6][0]),
        ("", ([], []), ([], []), MODULES[7][0]),
        ("fetch_money_mules_response_valid.json", 2, ([], []), MODULES[7][0]),
        ("fetch_money_mules_response_valid.json", -2, ([], []), MODULES[7][0]),
        ("", ([], []), ([], []), MODULES[8][0]),
        ("fetch_gambling_sites_response_valid.json", 2, ([], []), MODULES[8][0]),
        ("fetch_gambling_sites_response_valid.json", -2, ([], []), MODULES[8][0]),
    ],
)
def test_deduplicate_and_create_incidents(
    input_file_name, mock_input, mock_asserts, mock_module, mock_last_fetch_ids, capfd, caplog
):
    """
    Given:
        - List of fetched incidents.
        - List of last run's calculated hashes.
    When:
        - deduplicate_and_create_incidents is called.
    Then:
        - Calculate hashes for the passed list of incidents.
        - Create a new list of XSOAR-ready incidents only for incidents not found in the last run.
    """
    from CyberBlindspot import deduplicate_and_create_incidents

    with capfd.disabled():
        caplog.set_level(logging.DEBUG)
        if input_file_name:
            mock_input = [
                load_mock_response(input_file_name),
                mock_last_fetch_ids(mock_module)[mock_input:] if mock_input != -2 else [],
            ]
        new_hashes, unique_incidents = deduplicate_and_create_incidents(mock_input[1], mock_input[0])
        assert new_hashes == mock_asserts[0]
        assert unique_incidents == mock_asserts[1]


@pytest.mark.parametrize(
    "mock_input,mock_assert",
    [
        ("PascalCaseTest", "pascal_case_test"),
        ("camelCaseTest", "camel_case_test"),
    ],
)
def test_to_snake_case(mock_input, mock_assert):
    """
    Given:
        - String in a case other than snake case
    When:
        - to_snake_case is called
    Then:
        - Convert the input to snake case and return it
    """
    from CyberBlindspot import to_snake_case

    assert to_snake_case(mock_input) == mock_assert


@pytest.mark.parametrize(
    "mock_response",
    [
        {"hits": [{"testkey": "testval"}], "statusCode": 200},
        {"statusCode": 200},
        {"errors": "Invalid API-KEY", "statusCode": 400},
        {"statusCode": 500},
    ],
)
def test_test_configuration(mock_response, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client.
    When:
        - test_configuration is called.
    Then:
        - The returned value must be a list.
    """

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

    if mock_response.get("statusCode") > 200:
        # Expecting an exception
        with pytest.raises(DemistoException) as e:
            incidents = mock_client.test_configuration({})
            if type(e) is DemistoException:
                assert e == mock_response.get("errors", "request was not successful")
    else:
        incidents = mock_client.test_configuration({})
        assert type(incidents) is list


""" COMMAND TESTS """


@pytest.mark.parametrize(
    "mock_params,mock_side_effect",
    [
        (
            {
                "mirror_direction": "",
            },
            DemistoException('Invalid "Mirroring Direction" Value'),
        ),
        (
            {
                "mirror_direction": "None",
                "first_fetch": "wrong",
            },
            DemistoException('Invalid "First Fetch" Value'),
        ),
        (
            {
                "mirror_direction": "None",
                "max_fetch": "-1",
            },
            DemistoException(f'Invalid "Max Fetch" Value. Should be between 1 to {ABSOLUTE_MAX_FETCH}'),
        ),
        (
            {
                "mirror_direction": "None",
                "date_from": "wrong",
            },
            DemistoException('Invalid "Date From" Value (Does not match format "%d-%m-%Y %H:%M")'),
        ),
        (
            {
                "mirror_direction": "None",
                "date_to": "wrong",
            },
            DemistoException('Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")'),
        ),
        ({"mirror_direction": "None", "api_key": {"password": ""}}, DemistoException('Invalid "API Key" Value')),
    ],
)
def test_test_module(mock_params, mock_side_effect, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client.
        - Client arguments.
    When:
        - test-module is called.
    Then:
        - The key is checked against known valid keys.
        - If invalid, an exception is raised with a clear message.
    """
    from CyberBlindspot import test_module

    mocker.patch.object(
        mock_client,
        "test_configuration",
        side_effect=mock_side_effect,
    )
    with pytest.raises(DemistoException) as e:
        test_module(mock_client, mock_params)
    assert str(e.value) == mock_side_effect.message


def test_test_module_ok_when_module_to_use_missing(mock_client, mocker):
    """Upgraded instances without module_to_use should still test and fetch as Incidents."""
    from CyberBlindspot import test_module

    mock_test = mocker.patch.object(mock_client, "test_configuration", return_value=[])
    result = test_module(
        mock_client,
        {"mirror_direction": "None", "api_key": {"password": "test"}},
    )
    assert result == "ok"
    assert mock_test.call_args[0][0]["module_type"] == "incidents"


@pytest.mark.parametrize(
    "module_to_use,expected",
    [
        (None, "incidents"),
        ("", "incidents"),
        ("Incidents", "incidents"),
        ("Malware Logs", "malware_logs"),
        ("Social Media Fraud", "social_media_fraud"),
        ("Gambling Sites", "gambling_sites"),
        ("Money Mules", "money_mules"),
        ("legacy-unknown", "incidents"),
    ],
)
def test_resolve_cbs_module_defaults_to_incidents(module_to_use, expected):
    """Missing or blank module_to_use must keep pre-module-selector behavior (Incidents)."""
    from CyberBlindspot import resolve_cbs_module

    assert resolve_cbs_module(module_to_use) == expected


@pytest.mark.parametrize(
    "is_xsiam_platform, expected_severity",
    [
        (True, IncidentSeverity.MEDIUM),
        (False, IncidentSeverity.MEDIUM),
    ],
)
def test_map_and_create_incident_severity_by_platform(is_xsiam_platform, expected_severity, mocker):
    """Incident fetch maps API severity to numeric Demisto severity on all platforms."""
    from CyberBlindspot import map_and_create_incident

    mocker.patch("CyberBlindspot.is_xsiam", return_value=is_xsiam_platform)
    record = {
        "remarks": "Test incident",
        "first_seen": "05-07-2024 06:28:28",
        "last_seen": "26-10-2024 12:15:24",
        "status": "monitoring",
        "severity": "medium",
        "id": "CBS-1",
        "timestamp": 1720161042000,
        "external_link": "https://example.com",
    }
    result = map_and_create_incident(record)

    assert result["severity"] == expected_severity


@pytest.mark.parametrize(
    "mock_module,module_fields",
    [(module[0], module[3]) for module in MODULES],
    ids=[module[0] for module in MODULES],
)
def test_get_mapping_fields_command(mock_module, module_fields):
    """
    Given: Nothing.
    When:
        - User schema in the application contains the fields 'field1' and 'field2'.
        - Calling function get_mapping_fields_command.
    Then:
        - Ensure a GetMappingFieldsResponse object that contains the application fields is returned.
    """
    from CyberBlindspot import Instance, get_mapping_fields_command

    mock_instance = Instance(module=mock_module)
    expected_mappings = {"CyberBlindspot Incident": {field["name"]: field["description"] for field in module_fields}}

    with patch("CyberBlindspot.INSTANCE", new=mock_instance):
        mappings = get_mapping_fields_command()
        assert mappings.extract_mapping() == expected_mappings


@pytest.mark.parametrize(
    "response_files_names,mock_params,mock_module",
    [
        (
            ["fetch_incidents_response_valid.json", "fetch_incidents_response_invalid.json"],
            {
                "max_hits": "3",
            },
            "incidents",
        ),
    ],
)
def test_fetch_incidents_command(response_files_names, mock_params, mock_module, mock_last_fetch_ids, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client
        - Client arguments
        # Case 1:
            - User has provided valid pagination params.
            - First run with no new incidents yet, so an empty list will be returned.
        # Case 2:
            - User has provided valid pagination params.
            - Not first run with 1 duplicate incidents, so 3 hashes are calculated and 2 unique incidents are returned.
        # Case 3:
            - User has provided valid pagination params.
            - Not first run with no duplicate incidents, so 3 hashes are calculated and 3 unique incidents are returned.
        # Case 4:
            - User has provided invalid pagination params.
    When:
        - fetch-incidents command is called.
    Then:
        - Ensure response has correct number of records
        # Case 1:
            - An empty list will be returned.
        # Case 2:
            - 3 hashes are calculated and 2 unique incidents are returned.
        # Case 3:
            - 3 hashes are calculated and 3 unique incidents are returned.
        # Case 4:
            - DemistoException is raised.
    """
    from CyberBlindspot import fetch_incidents

    # First run with no incidents returned
    mocker.patch.object(mock_client, "fetch_incidents", return_value=[])
    next_run, incidents = fetch_incidents(mock_client, [], mock_params, {})
    assert next_run == {}
    assert incidents == []

    # Not first run with 1 duplicate in the returned incidents
    mocker.patch.object(mock_client, "fetch_incidents", return_value=load_mock_response(response_files_names[0]))
    next_run, incidents = fetch_incidents(mock_client, mock_last_fetch_ids(mock_module)[:1], mock_params, {"not_empty": ""})
    assert len(incidents) == 2
    assert len(next_run.get("last_fetch_ids", [])) == 3
    if incidents and incidents[0].get("xsoar_mirroring", {}).get("mirror_direction"):
        assert incidents[0].get("xsoar_mirroring", {}).get("mirror_id") == "COMX123456654321"
    assert incidents[0].get("name") == "New leaked_credential with severity High found"
    assert incidents[0].get("CustomFields", {}).get("cbs_type") == "Leaked Credential"

    # Not first run with no duplicates in the returned incidents

    mocker.patch.object(mock_client, "fetch_incidents", return_value=load_mock_response(response_files_names[0]))
    next_run, incidents = fetch_incidents(mock_client, [], mock_params, {"not_empty": ""})
    assert len(incidents) == 3
    assert next_run.get("last_fetch_ids") == mock_last_fetch_ids(mock_module)
    if incidents and incidents[0].get("xsoar_mirroring", {}).get("mirror_direction"):
        assert incidents[0].get("xsoar_mirroring", {}).get("mirror_id") != ""
    assert incidents[1].get("name") == "New leaked_credential with severity High found"
    assert incidents[1].get("CustomFields", {}).get("cbs_type") == "Leaked Credential"

    # Run with bad params

    fetch_exception = DemistoException("Error received: Please contact Threat Manager Team")
    mocker.patch.object(mock_client, "fetch_incidents", side_effect=fetch_exception)
    bad_mock_params = {**mock_params, "date_from": "abcdefg123"}
    with pytest.raises(DemistoException) as e:
        fetch_incidents(mock_client, [], bad_mock_params, {"not_empty": ""})
    assert str(e.value) == "Error received: Please contact Threat Manager Team"


@pytest.mark.parametrize(
    "response_file_name,mock_args,mock_asserts_file,mock_module,mock_module_prefix",
    [
        (
            "fetch_incidents_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "incident_list_cmd_result_valid.json",
            MODULES[0][0],
            MODULES[0][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[0][0],
            MODULES[0][1],
        ),
        (
            "fetch_cards_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "card_list_cmd_result_valid.json",
            MODULES[1][0],
            MODULES[1][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[1][0],
            MODULES[1][1],
        ),
        (
            "fetch_creds_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "cred_list_cmd_result_valid.json",
            MODULES[2][0],
            MODULES[2][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[2][0],
            MODULES[2][1],
        ),
        (
            "fetch_domains_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "domain_list_cmd_result_valid.json",
            MODULES[3][0],
            MODULES[3][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[3][0],
            MODULES[3][1],
        ),
        (
            "fetch_subdomains_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "subdomain_list_cmd_result_valid.json",
            MODULES[4][0],
            MODULES[4][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[4][0],
            MODULES[4][1],
        ),
        (
            "fetch_malware_logs_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "malware_logs_list_cmd_result_valid.json",
            MODULES[5][0],
            MODULES[5][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[5][0],
            MODULES[5][1],
        ),
        (
            "fetch_social_media_fraud_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "social_media_fraud_list_cmd_result_valid.json",
            MODULES[6][0],
            MODULES[6][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[6][0],
            MODULES[6][1],
        ),
        (
            "fetch_money_mules_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "money_mules_list_cmd_result_valid.json",
            MODULES[7][0],
            MODULES[7][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[7][0],
            MODULES[7][1],
        ),
        (
            "fetch_gambling_sites_response_valid.json",
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            "gambling_sites_list_cmd_result_valid.json",
            MODULES[8][0],
            MODULES[8][1],
        ),
        (
            False,
            {"maxHits": "3", "order": "asc", "dateFrom": "23-10-2023 07:00", "dateTo": "23-10-2023 23:00"},
            False,
            MODULES[8][0],
            MODULES[8][1],
        ),
    ],
)
def test_ctm360_cbs_incident_list_command(
    response_file_name, mock_args, mock_asserts_file, mock_module, mock_module_prefix, mock_client, mocker, capfd, caplog
):
    """
    Given:
        - CyberBlindspot Client.
        - Client arguments.
    When:
        - fetch_incidents is called.
    Then:
        - Fetch the list of incidents from the remote server.
    """
    from CyberBlindspot import Instance, ctm360_cbs_list_command

    patched_response = load_mock_response(response_file_name) if response_file_name else []
    mocker.patch.object(mock_client, "fetch_incidents", return_value=patched_response)
    mock_instance = Instance(module=mock_module)
    with capfd.disabled():
        caplog.set_level(logging.DEBUG)
        with patch("CyberBlindspot.INSTANCE", new=mock_instance):
            cmd_results = ctm360_cbs_list_command(mock_client, mock_args)
            expected_results = load_mock_response(mock_asserts_file) if mock_asserts_file else []
            cmd_results = cmd_results.to_context().get("Contents")
            if cmd_results and expected_results:
                cmd_results = [{k: v for k, v in item.items() if k != "rawJson"} for item in cmd_results]
                expected_results = [{k: v for k, v in item.items() if k != "rawJson"} for item in expected_results]
            logging.debug(cmd_results)
            logging.debug(expected_results)
            assert cmd_results == expected_results


@pytest.mark.parametrize(
    "response_file_name,mock_args,mock_asserts,mock_module",
    [
        (
            "incident_details_response_valid.json",
            {"ticketId": "COMX584598490058"},
            {
                "id": "COMX584598490058",
                "subject": "1 customer credentials compromised (6vd6e97)",
                "severity": "High",
                "type": "Leaked Credential",
                "class": "Link",
                "status": "Member Feedback",
                "coa": "Member Side Action",
                "remarks": "New leaked_credential with severity High found",
                "external_link": "https://platform.ctm360.com/cbs/threat_manager/incidents/COMX584598490058",
                "first_seen": "27-12-2023 05:42:18 AM",
                "last_seen": "27-12-2023 05:42:18 AM",
                "screenshots": [{"filename": "screenshot1.png", "filepath": "29207a3d7f2ce17cd6309d1fc0f5ad7e"}],
                "brand": "RiskAssess Demo",
                "timestamp": "1703655740964",
            },
            MODULES[0],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[0],
        ),
        (
            "cred_details_response_valid.json",
            {"ticketId": "BC-0D7FDA777"},
            {
                "first_seen": "24-11-2024 09:23:11 PM",
                "last_seen": "24-11-2024 09:23:11 PM",
                "breach_source": ["Pure Incubation"],
                "domain": "example.local",
                "type": "",
                "remarks": "Pure Incubation Breach - yash.abc@example.local",
                "external_link": "https://platform.ctm360.com/cbs/leaks/breached_credentials?filters=[]&searchQuery=BC-0D7FDA777&selectedSearchField=leak_id",
                "email": "yash.abc@example.local",
                "username": "yser",
                "password": "ypass123!",
                "brand": "RiskAssess Demo",
                "id": "BC-0D7FDA777",
                "status": "new",
            },
            MODULES[1],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[1],
        ),
        (
            "card_details_response_valid.json",
            {"ticketId": "LC-A1E4B2F49"},
            {
                "first_seen": "25-12-2024 09:16:59 AM",
                "last_seen": "25-12-2024 09:16:59 AM",
                "card_number": "0123456789012345",
                "cvv": "123",
                "expiry_month": 4,
                "expiry_year": 2026,
                "brand": "RiskAssess Demo",
                "id": "LC-A1E4B2F49",
                "external_link": "https://platform.ctm360.com/cbs/leaks/compromised_cards?filters=[]&searchQuery=LC-A1E4B2F49&selectedSearchField=leak_id",
                "status": "new",
                "timestamp": "1735107419000",
                "severity": "Low",
                "type": "Compromised Cards",
                "remarks": "Compromised Card 0123456789012345",
            },
            MODULES[2],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[2],
        ),
        (
            "domain_details_response_valid.json",
            {"ticketId": "RD-0285ED4LA"},
            {
                "first_seen": "29-10-2024 09:58:25 PM",
                "last_seen": "31-12-2024 03:51:58 AM",
                "id": "RD-0285ED4LA",
                "subject": "trybackme.xyz",
                "severity": "High",
                "type": "Domain Infringement",
                "risks": ["Has A Record", "Has MX Record", "Newly registered when detected", "Has NS Record", "Has DNS Record"],
                "external_link": "https://platform.ctm360.com/cbs/domains/registered/uYa30ZYB_inRxraHlAO4",
                "status": "monitoring",
                "incident_status": "member_feedback",
                "brand": "RiskAssess Demo",
                "timestamp": "1730239105000",
            },
            MODULES[3],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[3],
        ),
        (
            "subdomain_details_response_valid.json",
            {"ticketId": "RD-C4G04E22B"},
            {
                "first_seen": "26-10-2024 01:06:43 AM",
                "last_seen": "26-10-2024 01:09:07 AM",
                "id": "RD-C4G04E22B",
                "subject": "sse.host-master.trybackme.wo",
                "severity": "High",
                "type": "Subdomain Infringement",
                "risks": ["SSL issued on domain", "Has DNS Record", "Has NS Record"],
                "external_link": "https://platform.ctm360.com/cbs/domains/registered/uYa30ZYB_inRxraHlAO4",
                "status": "monitoring",
                "brand": "RiskAssess Demo",
                "timestamp": "1729904803311",
            },
            MODULES[4],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[4],
        ),
        (
            "malware_logs_details_response_valid.json",
            {"ticketId": "ML-919AE2387"},
            {
                "brand": "RiskAssess Demo",
                "compromise_details": [
                    {
                        "antiviruses": ["Windows Defender"],
                        "clientAt": ["trybackme.io"],
                        "computer_name": "DESKTOP-KPLOECB (DELL)",
                        "date_compromised": "2025-05-23T03:48:49.000Z",
                        "date_uploaded": "2025-05-23T08:51:12.080Z",
                        "employeeAt": ["sit.edu.lb"],
                        "employee_session_cookies": [
                            {
                                "domain": "trybackme.io",
                                "expiry": "2025-07-25T23:04:06.000Z",
                                "name": "SNID",
                                "url": "trybackme.io",
                                "value": "AILqd3Wz3s_6iqTeieieLAII1KgBAAQOBFY3RDT6ncttjkiZ4SJgKF00Gq3kHD07-D95EwhPEoflTkPv0PFXiUM"
                                "adqwSnwNE4e",
                            }
                        ],
                        "installed_software": None,
                        "ip": "154.134.143.229",
                        "malware_path": " C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\maxerste.exe",
                        "operating_system": "Windows 10 Professionnel (10.0.19045) x64",
                        "stealer": "[MA]154.134.143.229",
                        "stealer_family": "Lumma",
                        "type": "client",
                    }
                ],
                "computer_name": "DESKTOP-KPLOECB (DELL)",
                "date_compromised": "23-05-2025 03:48:49 AM",
                "domain": "trybackme.io",
                "external_link": "https://platform.ctm360.com/cbs/leaks/malware_logs/user_credentials_web_app?filters=[]&searchQuery=ML-919AE2387&selectedSearchField=leak_id",
                "first_seen": "23-05-2025 09:35:21 PM",
                "hostname": "trybackme.io",
                "id": "ML-919AE2387",
                "last_seen": "23-05-2025 09:35:21 PM",
                "malware_path": " C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\maxerste.exe",
                "masked_password": "***pezz753",
                "operating_system": "Windows 10 Professionnel (10.0.19045) x64",
                "password": "Suppezz753",
                "remarks": "ML-919AE2387",
                "source_uri": ["leak/malware_logs?path=eaf75fb8f9431fe403b9"],
                "sources": ["Chr log files"],
                "status": "new",
                "stealer_family": ["Lumma"],
                "timestamp": "1748036121189",
                "type": "User Credentials Web App",
                "user": "otronerfinn",
                "user_domain": "",
                "website": "trybackme.io",
            },
            MODULES[5],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[5],
        ),
        (
            "social_media_fraud_details_response_valid.json",
            {"ticketId": "BI-A81ET1215"},
            "social_media_fraud_details_response_valid.json",
            MODULES[6],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[6],
        ),
        (
            "money_mules_details_response_valid.json",
            {"ticketId": "MM-C7FDB1092"},
            "money_mules_details_response_valid.json",
            MODULES[7],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[7],
        ),
        (
            "gambling_sites_details_response_valid.json",
            {"ticketId": "GS-162RTEE07"},
            "gambling_sites_details_response_valid.json",
            MODULES[8],
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            {},
            MODULES[8],
        ),
    ],
)
def test_ctm360_cbs_incident_details_command(response_file_name, mock_args, mock_asserts, mock_module, mock_client, mocker):
    """
    Given:
        - Ticket ID of incident.
    When:
        - ctm360-cbs-incident-details command is called.
    Then:
        - Ensure result is as expected.
    """
    from CyberBlindspot import Instance, ctm360_cbs_details_command, normalize_timestamp

    mock_instance = Instance(module=mock_module)

    with patch("CyberBlindspot.INSTANCE", new=mock_instance):
        patched_response = load_mock_response(response_file_name) if response_file_name else {}
        mocker.patch.object(mock_client, "fetch_incident", return_value=patched_response)
        if isinstance(mock_asserts, str):
            mock_asserts = load_mock_response(mock_asserts)
            if mock_asserts.get("timestamp", ""):
                mock_asserts = {**mock_asserts, "timestamp": str(normalize_timestamp(mock_asserts["timestamp"]))}
        cmd_results = ctm360_cbs_details_command(mock_client, mock_args)
        assert cmd_results.to_context().get("Contents") == mock_asserts


@pytest.mark.parametrize(
    "response_file_name,mock_args,mock_asserts",
    [
        (
            "close_incident_response_valid.json",
            {"ticketId": "COMX165756654321"},
            "Incident closed successfully",
        ),
        (
            "close_incident_response_invalid.json",
            {"ticketId": "COMX165756654321"},
            "Incident should be under Member Feedback or Monitoring to Close",
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            None,
        ),
    ],
)
def test_ctm360_cbs_incident_close_command(response_file_name, mock_args, mock_asserts, mock_client, mocker):
    """
    Given:
        - Ticket ID of incident.
    When:
        - ctm360-cbs-incident-close command is called.
    Then:
        - Ensure result is as expected.
    """
    from CyberBlindspot import ctm360_cbs_incident_close_command

    mock_response = load_mock_response(response_file_name) if response_file_name else {}
    mocker.patch.object(mock_client, "close_incident", return_value=mock_response)
    cmd_results = ctm360_cbs_incident_close_command(mock_client, mock_args)
    assert cmd_results.to_context().get("HumanReadable") == mock_asserts


@pytest.mark.parametrize(
    "response_file_name,mock_args,mock_asserts",
    [
        (
            "request_takedown_response_valid.json",
            {"ticketId": "COMX165756654321"},
            "Takedown request executed successfully",
        ),
        (
            "request_takedown_response_invalid.json",
            {"ticketId": "COMX165756654321"},
            "Incident should be under Member Feedback for Takedown",
        ),
        (
            False,
            {"ticketId": "COMX165756654321"},
            None,
        ),
    ],
)
def test_ctm360_cbs_incident_request_takedown_command(response_file_name, mock_args, mock_asserts, mock_client, mocker):
    """
    Given:
        - Ticket ID of incident.
    When:
        - ctm360-cbs-incident-request-takedown command is called.
    Then:
        - Ensure result is as expected.
    """
    from CyberBlindspot import ctm360_cbs_incident_request_takedown_command

    mock_response = load_mock_response(response_file_name) if response_file_name else {}
    mocker.patch.object(mock_client, "request_takedown", return_value=mock_response)
    cmd_results = ctm360_cbs_incident_request_takedown_command(mock_client, mock_args)
    assert cmd_results.to_context().get("HumanReadable") == mock_asserts


@pytest.mark.parametrize(
    "mock_module,mock_response,mock_entry,mock_status",
    [
        (MODULES[0][0], "incident_details_response_valid.json", "", ""),
        (MODULES[0][0], "incident_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[0][0], "incident_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[0][0], "incident_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[0][0], "incident_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[0][0], "incident_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
        (MODULES[1][0], "card_details_response_valid.json", "", ""),
        (MODULES[1][0], "card_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[1][0], "card_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[1][0], "card_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[1][0], "card_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[1][0], "card_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
        (MODULES[2][0], "cred_details_response_valid.json", "", ""),
        (MODULES[2][0], "cred_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[2][0], "cred_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[2][0], "cred_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[2][0], "cred_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[2][0], "cred_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
        (MODULES[3][0], "domain_details_response_valid.json", "", ""),
        (MODULES[3][0], "domain_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[3][0], "domain_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[3][0], "domain_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[3][0], "domain_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[3][0], "domain_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
        (MODULES[4][0], "subdomain_details_response_valid.json", "", ""),
        (MODULES[4][0], "subdomain_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[4][0], "subdomain_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[4][0], "subdomain_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[4][0], "subdomain_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[4][0], "subdomain_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
        (MODULES[5][0], "malware_logs_details_response_valid.json", "", ""),
        (MODULES[5][0], "malware_logs_details_response_valid.json", STATUS_ENTRIES["wip"], "wip"),
        (MODULES[5][0], "malware_logs_details_response_valid.json", STATUS_ENTRIES["closed"], "closed"),
        (MODULES[5][0], "malware_logs_details_response_valid.json", STATUS_ENTRIES["resolved"], "resolved"),
        (MODULES[5][0], "malware_logs_details_response_valid.json", STATUS_ENTRIES["disregarded"], "disregarded"),
        (MODULES[5][0], "malware_logs_details_response_valid.json", STATUS_ENTRIES["unconfirmed"], "unconfirmed"),
    ],
)
def test_get_remote_data(mock_module, mock_response, mock_entry, mock_status, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client.
        - Client arguments.
    When:
        -  get-remote-data command is called.
    Then:
        - Ensure result is as expected.
    """
    from copy import deepcopy

    from CyberBlindspot import (
        Instance,
        get_remote_data_command,
        map_and_create_incident,
    )

    mock_args = {"id": "COMX165756654321", "lastUpdate": "2024-01-02T13:30:21.172707565Z"}
    mock_result = load_mock_response(mock_response)
    if type(mock_result) is dict:
        mock_result["status"] = mock_status

    if type(mock_result) is dict:
        mock_result2 = deepcopy(mock_result)
    mocker.patch.object(mock_client, "fetch_incident", return_value=mock_result)

    mock_instance = Instance(module=mock_module)
    with patch("CyberBlindspot.INSTANCE", new=mock_instance):
        result = get_remote_data_command(mock_client, mock_args)
        mock_result = map_and_create_incident(mock_result2) if mock_status else []
        if isinstance(mock_result, dict):
            del mock_result["rawJson"]
        if isinstance(result.mirrored_object, dict):
            del result.mirrored_object["rawJson"]
        assert result.mirrored_object == mock_result
        if mock_status == "":
            assert result.entries == []
        elif mock_status == "closed":
            assert result.entries[0].get("Contents", {}).get("closeReason", "") == mock_entry
        else:
            assert result.entries[0].get("Contents", "") == mock_entry


@pytest.mark.parametrize(
    "mock_input_file",
    [
        ("fetch_incidents_response_valid.json"),
    ],
)
def test_get_modified_remote_data(mock_input_file, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client.
        - Client arguments.
    When:
        - get-modified-remote-data command is called
    Then:
        - Ensure result is as expected
    """
    from CyberBlindspot import get_modified_remote_data_command

    mock_args = {
        "date_field": "last_seen",
        "order": "asc",
        "date_from": "1704183134",
        "max_hits": 50,
        "lastUpdate": "2024-01-02T13:30:21.172707565Z",
    }
    mock_result = load_mock_response(mock_input_file)
    mocker.patch.object(mock_client, "fetch_incidents", return_value=mock_result)
    result = get_modified_remote_data_command(mock_client, mock_args)
    mock_assert = [item["id"] for item in mock_result]
    assert result.modified_incident_ids == mock_assert


@pytest.mark.parametrize(
    "mock_response_file,mock_args,mock_log_asserts",
    [
        (
            "",
            {
                "remoteId": "1",
                "data": {},
                "entries": [],
                "status": IncidentStatus.ACTIVE,
                "incidentChanged": False,
                "delta": {"not_empty": "value"},
            },
            f"{LOGGING_PREFIX} Incident 1 was not modified locally..",
        ),
        (
            "",
            {
                "remoteId": "2",
                "data": {},
                "entries": [],
                "status": IncidentStatus.ARCHIVE,
                "incidentChanged": True,
                "delta": {"not_empty": "value"},
            },
            f"{LOGGING_PREFIX} Modification to 2 is not configured for outgoing mirroring..",
        ),
        (
            "close_incident_response_valid.json",
            {
                "remoteId": "3",
                "data": {},
                "entries": [],
                "status": IncidentStatus.DONE,
                "incidentChanged": True,
                "delta": {"not_empty": "value"},
            },
            f"{LOGGING_PREFIX} Closing incident 3",
        ),
    ],
)
def test_update_remote_system(mock_response_file, mock_args, mock_log_asserts, mock_client, mocker, caplog):
    """
    Given:
        - CyberBlindspot Client.
        - Client arguments.
    When:
        - update-remote-system command is called.
    Then:
        - Ensure result is as expected.
    """
    from CyberBlindspot import update_remote_system_command

    with caplog.at_level(logging.INFO):
        if mock_response_file:
            mocker.patch.object(mock_client, "close_incident", return_value=load_mock_response(mock_response_file))
        result = update_remote_system_command(mock_client, mock_args)
        assert result == mock_args["remoteId"]
        assert mock_log_asserts in caplog.text


@pytest.mark.parametrize(
    "mock_response,mock_params,expected_result",
    [
        (
            {"results": [{"filename": "screenshot1.png", "filedata": {"data": b"test_data"}}]},
            {"ticket_id": "COMX123456"},
            [{"filename": "screenshot1.png", "filedata": {"data": b"test_data"}}],
        ),
        ({"results": []}, {"ticket_id": "COMX123456"}, []),
        ({"results": None}, {"ticket_id": "COMX123456"}, []),
    ],
)
def test_get_screenshot_files(mock_response, mock_params, expected_result, mock_client, mocker):
    """
    Given:
        - CyberBlindspot Client
        - Parameters for the screenshot request
        - Mock API response
    When:
        - get_screenshot_files is called
    Then:
        - Ensure the correct response is returned
    """
    mocker.patch.object(mock_client, "_http_request", return_value=mock_response)
    result = mock_client.get_screenshot_files(mock_params)
    assert result == expected_result


@pytest.mark.parametrize(
    "mock_conf,mock_response,mock_args,existing_files,api_error,expected_result",
    [
        # Test when screenshot retrieval is disabled
        (
            False,
            {"results": [{"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}}]},
            {"ticket_id": "COMX123456"},
            [],
            None,
            CommandResults(readable_output="Screenshot Evidence Retrieval is Disabled in Instance Configuration."),
        ),
        # Test with single screenshot
        (
            True,
            {"results": [{"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}}]},
            {"ticket_id": "COMX123456"},
            [],
            None,
            [{"File": "screenshot1.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE}],
        ),
        # Test with multiple screenshots
        (
            True,
            {
                "results": [
                    {"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}},
                    {"filename": "screenshot2.png", "filedata": {"data": b"test_data2"}},
                ]
            },
            {"ticket_id": "COMX123456"},
            [],
            None,
            [
                {"File": "screenshot1.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE},
                {"File": "screenshot2.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE},
            ],
        ),
        # Test with no results
        (
            True,
            {"results": []},
            {"ticket_id": "COMX123456"},
            [],
            None,
            CommandResults(readable_output="No new screenshots to fetch"),
        ),
        # Test with filtered files (no files to fetch after filtering)
        (
            True,
            {"results": []},
            {"files": [{"filename": "screenshot1.png", "filepath": "/path/to/file"}]},
            [],
            None,
            CommandResults(readable_output="No new screenshots to fetch"),
        ),
        # Test with empty files list
        (
            True,
            {"results": []},
            {"ticket_id": "COMX123456", "files": []},
            [],
            None,
            CommandResults(readable_output="No new screenshots to fetch"),
        ),
        # Test with all files already existing in context
        (
            True,
            {"results": []},
            {"files": [{"filename": "existing.png", "filepath": "/path/to/file"}]},
            [{"Name": "existing.png"}],
            None,
            CommandResults(readable_output="All requested screenshots already exist in context"),
        ),
        # Test with invalid type in files list (should be filtered out)
        (
            True,
            {"results": [{"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}}]},
            {"files": [{"filename": "screenshot1.png"}, "not_a_dict"]},
            [],
            None,
            [{"File": "screenshot1.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE}],
        ),
        # Test with API error
        (
            True,
            {"results": []},
            {"ticket_id": "COMX123456"},
            [],
            Exception("API connection error"),
            CommandResults(readable_output="Failed to fetch screenshots from API: API connection error"),
        ),
        # Test with non-dictionary in results
        (
            True,
            {"results": ["not_a_dict", {"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}}]},
            {"ticket_id": "COMX123456"},
            [],
            None,
            [{"File": "screenshot1.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE}],
        ),
        # Test with non-list InfoFile
        (
            True,
            {"results": [{"filename": "screenshot1.png", "filedata": {"data": b"test_data1"}}]},
            {"ticket_id": "COMX123456"},
            {"Name": "other_file.png"},  # Not a list
            None,
            [{"File": "screenshot1.png", "FileID": "mock_file_id", "Type": EntryType.IMAGE}],
        ),
    ],
)
def test_ctm360_cbs_incident_retrieve_screenshots_command(
    mock_conf, mock_response, mock_args, existing_files, api_error, expected_result, mock_client, mocker, capfd
):
    """
    Given:
        - CyberBlindspot Client
        - Command arguments
        - Mock API response
        - Existing files in context
        - Optional API error to simulate
    When:
        - ctm360_cbs_incident_retrieve_screenshots_command is called
    Then:
        - Ensure the correct file results or command results are returned
        - Verify error handling works as expected
        - Verify critical logs are generated
    """
    from CyberBlindspot import ctm360_cbs_incident_retrieve_screenshots_command

    # Mock fileResult to avoid writing to disk
    def mock_file_result(filename, data, file_type=None):
        return {"File": filename, "FileID": "mock_file_id", "Type": file_type}

    # Create a mock for demisto context
    mock_context = mocker.MagicMock(return_value={"InfoFile": existing_files})

    # Mock log function but keep track of calls
    mock_log = mocker.patch("CyberBlindspot.log")

    # Patch everything with mocker
    mocker.patch("CyberBlindspot.demisto.context", mock_context)
    mocker.patch("CyberBlindspot.RETRIEVE_SCREENSHOTS", new=mock_conf)
    mocker.patch("CyberBlindspot.fileResult", side_effect=mock_file_result)
    mocker.patch("CommonServerPython.fileResult", side_effect=mock_file_result)

    # Configure get_screenshot_files based on whether we want to simulate an error
    if api_error:
        mocker.patch.object(mock_client, "get_screenshot_files", side_effect=api_error)
    else:
        mocker.patch.object(mock_client, "get_screenshot_files", return_value=mock_response.get("results", []))

    # Run the function with capfd disabled to prevent stdout issues
    with capfd.disabled():
        result = ctm360_cbs_incident_retrieve_screenshots_command(mock_client, mock_args)

    # Verify results
    if isinstance(result, list):
        assert len(result) == len(expected_result)
        for r, e in zip(result, expected_result):
            assert r["File"] == e["File"]
            assert r["Type"] == e["Type"]
    elif isinstance(result, CommandResults):
        assert isinstance(expected_result, CommandResults)
        assert result.readable_output == expected_result.readable_output

    # Verify critical logging behavior only
    if api_error:
        # For API errors, verify error is logged
        mock_log.assert_any_call("error", f"Error calling get_screenshot_files: {str(api_error)}")
    elif not mock_conf:
        # For disabled screenshots, verify info message
        mock_log.assert_any_call("info", "Screenshot Evidence Retrieval is Disabled in Instance Configuration.")
    elif result and isinstance(result, list):
        # For successful file retrieval, verify success message
        mock_log.assert_any_call("info", f"Added {len(result)} new screenshot(s) to context")