SOCRadarIncidentsMultiTenant

SOCRadar Multi-Tenant Incidents integration with advanced incident management, status reasons, and compliance tracking. Fetches security incidents from SOCRadar platform across all tenant companies with proper deduplication and date handling. Uses Multi-Tenant API for incident fetching and company-specific APIs for alarm management.

Analytics & SIEM · SOCRadar

Details

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

README

SOCRadar Incidents v4.0 Multi-Tenant

Fetch and manage security incidents from multiple companies using SOCRadar’s Multi-Tenant Incident API. Designed for MSPs, MSSPs, and organizations managing multiple subsidiaries.

Overview

SOCRadar is a digital risk protection platform that provides extended threat intelligence and brand protection capabilities. This Multi-Tenant integration enables XSOAR to ingest security incidents from multiple companies through a single integration instance, including:

  • Brand Protection: Impersonating domains, phishing attacks, brand abuse
  • Cyber Threat Intelligence: Stolen credentials, data leaks, malware infections
  • Attack Surface Management: External exposure findings, misconfigurations
  • Dark Web Intelligence: Compromised credentials, leaked data from dark web sources
  • Supply Chain Security: Third-party risks and vendor security issues

Multi-Tenant Features

Centralized Multi-Company Management

  • Single Integration: Monitor incidents from all your companies through one integration instance
  • Company Tracking: Each alarm automatically includes company ID and company name
  • Smart Filtering: Filter and manage incidents across companies or focus on specific ones

Automatic Company ID Handling

  • Auto-Extraction: When taking actions, company ID is automatically extracted from alarm data
  • No Manual Input: You don’t need to remember or specify company IDs for most operations
  • Override Capability: Manually specify company ID when needed (advanced use cases)

Company Visibility Control

  • Configurable Display: Choose whether to show company information in incident details
  • Custom Fields: Company ID and company name available in custom fields
  • Incident Naming: Company information included in incident names for quick identification

What’s New in Multi-Tenant v4.0

Multi-Tenant Specific

  • Multi-Tenant API Endpoint: Uses /multi_tenant/{multi-tenant-id}/incidents for fetching
  • Company Information: Each alarm includes both company_id and company_name
  • Smart Action Handling: Automatically determines which company to act upon
  • Default Company Visibility: Company info shown by default (can be disabled)

Core Features (from v4.0)

  • Multi-Status Filtering: Select multiple statuses (OPEN, CLOSED, ON_HOLD) simultaneously
  • Epoch Time Precision: Second-level accuracy for incident fetching - zero duplicates
  • Reverse Pagination: Fetches newest incidents first for better performance
  • Dynamic Content Extraction: Automatically extracts alarm-specific fields regardless of type
  • Enhanced Deduplication: Two-layer protection prevents duplicate incidents

Technical Improvements

  • Interval-based fetching with overlap protection
  • Configurable content and entity inclusion
  • Comprehensive debug logging
  • Better error handling and recovery
  • Intelligent company ID extraction from incident context

Key Differences: Standard vs Multi-Tenant

Feature Standard v4.0 Multi-Tenant v4.0
Configuration Company ID + API Key Multi-Tenant ID + API Key
Fetch Endpoint /company/{id}/incidents/v4 /multi_tenant/{id}/incidents
Company Data Single company (implicit) Multiple companies (explicit)
Company ID in Actions Uses configured company ID Auto-extracted from alarm
Company Visibility Optional (default: hidden) Optional (default: visible)
Use Case Single organization MSPs, MSSPs, multi-subsidiary

Prerequisites

Required

  • SOCRadar account with Multi-Tenant Incident API access
  • Multi-Tenant ID from SOCRadar platform
  • API Key from SOCRadar platform
  • XSOAR 6.x or later

API Access

To obtain your API credentials:

  1. Log in to SOCRadar Platform
  2. Reach out support team to get MSSP API Key

Configuration

Integration Settings

Parameter Required Default Description
Server URL Yes https://platform.socradar.com/api SOCRadar API base URL
API Key Yes - Your Multi-Tenant API Key from SOCRadar
Multi-Tenant ID Yes - Your Multi-Tenant ID (integer)
Fetch incidents No False Enable automatic incident fetching
Incident type No - XSOAR incident type to create
Max incidents per fetch No 10000 Maximum incidents per fetch cycle
First fetch time No 3 days Initial time range for first fetch
Fetch Interval (Minutes) No 1 Time window for subsequent fetches

Filtering Options

Parameter Type Description
Status Filter Multi-select Select one or more: OPEN, CLOSED, ON_HOLD
Severity Multi-select Filter by: LOW, MEDIUM, HIGH, CRITICAL
Alarm Type IDs Text Comma-separated list of type IDs to include
Excluded Alarm Type IDs Text Comma-separated list of type IDs to exclude
Main Alarm Types Text Comma-separated main types (e.g., “Brand Protection”)
Alarm Sub Types Text Comma-separated sub types

Commands

You can execute these commands from the Cortex XSOAR 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.

socradar-change-alarm-status


Change the status of one or more alarms.

Base Command

socradar-change-alarm-status

Input

Argument Name Description Required
alarm_ids Comma-separated list of alarm IDs to update. Required
status_reason New status reason for the alarms. Possible values are: OPEN, INVESTIGATING, RESOLVED, PENDING_INFO, LEGAL_REVIEW, VENDOR_ASSESSMENT, FALSE_POSITIVE, DUPLICATE, PROCESSED_INTERNALLY, MITIGATED, NOT_APPLICABLE. Required
comments Optional comments explaining the status change. Optional
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.
SOCRadar.Alarm.Status String New alarm status.

socradar-mark-false-positive


Mark an alarm as false positive.

Base Command

socradar-mark-false-positive

Input

Argument Name Description Required
alarm_id Alarm ID to mark as false positive. Required
comments Optional comments explaining why this is a false positive. Default is False positive. Optional
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.
SOCRadar.Alarm.Status String New alarm status.

socradar-mark-resolved


Mark an alarm as resolved.

Base Command

socradar-mark-resolved

Input

Argument Name Description Required
alarm_id Alarm ID to mark as resolved. Required
comments Optional comments explaining the resolution. Default is Resolved. Optional
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.
SOCRadar.Alarm.Status String New alarm status.

socradar-add-comment


Add a comment to an alarm.

Base Command

socradar-add-comment

Input

Argument Name Description Required
alarm_id Alarm ID to add comment to. Required
user_email Email address of the user adding the comment. Required
comment Comment text to add. Required
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.

socradar-add-assignee


Add the assignee(s) of an alarm.

Base Command

socradar-add-assignee

Input

Argument Name Description Required
alarm_id Alarm ID to add assignee for. Required
user_emails Comma-separated list of user email addresses to assign. Required
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.
SOCRadar.Alarm.Assignees String New assignees.

socradar-add-tag


Add or remove a tag from an alarm.

Base Command

socradar-add-tag

Input

Argument Name Description Required
alarm_id Alarm ID to add/remove tag for. Required
tag Tag name to add or remove. Required
company_id Company ID for the alarm. If not provided, will be auto-fetched from alarm data. Can also use ${incident.socradarcompanyid} from incident fields. Optional

Context Output

Path Type Description
SOCRadar.Alarm.ID String Alarm ID.
SOCRadar.Alarm.Tags String Alarm tags.

socradar-test-fetch


Test incident fetching to verify alarms are available and date parsing works correctly.

Base Command

socradar-test-fetch

Input

Argument Name Description Required
limit Number of incidents to test fetch (default 5). Default is 5. Optional
first_fetch Test date range (e.g., “3 days”, “7 days”). Default is 3 days. Optional

Context Output

Path Type Description
SOCRadar.TestFetch.TotalCount Number Total number of incidents found.
SOCRadar.TestFetch.SampleIncidents Unknown Sample incidents for testing.
SOCRadar.TestFetch.StartDate String Parsed start date used for the test.
SOCRadar.TestFetch.TotalRecords Number Total number of incidents from service.
SOCRadar.TestFetch.TotalPages Number Total number of pages of incidents from service.

License

This integration is provided as part of the Cortex XSOAR content pack.

Configuration parameters

  • apikey — API Key (required)
  • multi_tenant_id — Multi-Tenant ID (required)
  • isFetch — Fetch incidents
  • first_fetch — First Fetch Time
  • fetch_interval_minutes — Fetch Interval (Minutes)
  • max_fetch — Max incidents per fetch
  • show_content — Show Alarm Content
  • status — Status Filter
  • severities — Severity Levels
  • alarm_type_ids — Alarm Type IDs (Include)
  • excluded_alarm_type_ids — Alarm Type IDs (Exclude)
  • alarm_main_types — Alarm Main Types
  • excluded_alarm_main_types — Alarm Main Types (Exclude)
  • alarm_sub_types — Alarm Sub Types
  • excluded_alarm_sub_types — Alarm Sub Types (Exclude)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (7)

  • socradar-add-assignee

    Add the assignee(s) of an alarm.

  • socradar-add-comment

    Add a comment to an alarm.

  • socradar-add-tag

    Add or remove a tag from an alarm.

  • socradar-change-alarm-status

    Change the status of one or more alarms.

  • socradar-mark-false-positive

    Mark an alarm as false positive.

  • socradar-mark-resolved

    Mark an alarm as resolved.

  • socradar-test-fetch

    Test incident fetching to verify alarms are available and date parsing works correctly.

import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *

import urllib3
import traceback
from typing import Any
from datetime import datetime, timedelta

urllib3.disable_warnings()

SOCRADAR_API_ENDPOINT = "https://platform.socradar.com/api"
SOCRADAR_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
MAX_INCIDENTS_TO_FETCH = 100000
MAX_INCIDENTS_PER_PAGE = 100

STATUS_REASON_MAP = {
    "OPEN": 0,
    "INVESTIGATING": 1,
    "RESOLVED": 2,
    "PENDING_INFO": 4,
    "LEGAL_REVIEW": 5,
    "VENDOR_ASSESSMENT": 6,
    "FALSE_POSITIVE": 9,
    "DUPLICATE": 10,
    "PROCESSED_INTERNALLY": 11,
    "MITIGATED": 12,
    "NOT_APPLICABLE": 13,
}

MESSAGES = {
    "BAD_REQUEST_ERROR": "An error occurred while fetching the data.",
    "AUTHORIZATION_ERROR": "Authorization Error: make sure API Key is correctly set.",
    "RATE_LIMIT_EXCEED_ERROR": "Rate limit exceeded.",
}


def convert_to_demisto_severity(severity: str) -> int | float:
    """Convert SOCRadar severity to Demisto severity level"""
    return {
        "LOW": IncidentSeverity.LOW,
        "MEDIUM": IncidentSeverity.MEDIUM,
        "HIGH": IncidentSeverity.HIGH,
        "CRITICAL": IncidentSeverity.CRITICAL,
        "INFO": IncidentSeverity.INFO,
    }.get(severity.upper(), IncidentSeverity.UNKNOWN)


class Client(BaseClient):
    def __init__(
        self,
        base_url: str,
        api_key: str,
        multi_tenant_id: str,
        verify: bool,
        proxy: bool,
    ):
        super().__init__(base_url, verify=verify, proxy=proxy)
        self.api_key = api_key
        self.multi_tenant_id = multi_tenant_id

    def _get_headers(self) -> dict[str, str]:
        """Get API request headers"""
        return {"API-Key": self.api_key}

    def search_incidents(
        self,
        status: list[str] | None = None,
        severities: list[str] | None = None,
        alarm_main_types: list[str] | None = None,
        alarm_sub_types: list[str] | None = None,
        alarm_type_ids: list[int] | None = None,
        excluded_alarm_type_ids: list[int] | None = None,
        excluded_alarm_main_types: list[str] | None = None,
        excluded_alarm_sub_types: list[str] | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        limit: int = 20,
        page: int = 1,
        # FIX: Added include_total_records parameter to the method signature.
        # Previously test_fetch_command passed this as a kwarg but the method
        # did not accept it, causing a TypeError at runtime.
        # When True the API returns total_pages/total_records in the response.
        include_total_records: bool = True,
    ) -> dict[str, Any]:
        """
        Search incidents from SOCRadar Multi-Tenant API

        CHANGED: Now uses /v1/multi-tenant/{multiTenantId}/incidents endpoint
        instead of /company/{company_id}/incidents/v4

        Args:
            status: List of statuses to filter (OPEN, CLOSED, ON_HOLD)
            severities: List of severity levels to filter
            alarm_main_types: List of main alarm types to filter
            alarm_sub_types: List of alarm subtypes to filter
            alarm_type_ids: List of alarm type IDs to include
            excluded_alarm_type_ids: List of alarm type IDs to exclude
            excluded_alarm_sub_types: List of alarm sub types to exclude
            excluded_alarm_main_types: List of alarm main types to exclude
            start_date: Start date for filtering (YYYY-MM-DD)
            end_date: End date for filtering (YYYY-MM-DD)
            limit: Number of results per page (max 100)
            page: Page number for pagination
            include_total_records: Whether to request total record counts from the API

        API Response Structure:
        {
          "data": {
            "alarms": [...],  # Each alarm contains company_id
            "total_pages": 3077,
            "total_records": 6153
          },
          "is_success": true,
          "message": "Success"
        }
        """
        # FIX: Use the include_total_records parameter instead of hardcoding "true".
        # This allows callers like test_fetch_command to disable counting for performance.
        params: dict[str, Any] = {
            "limit": min(limit, 100),
            "page": page,
            "include_total_records": "true" if include_total_records else "false",
        }

        if status:
            params["status_list[]"] = status
        if severities:
            params["severity"] = ",".join(severities)
        if alarm_main_types:
            params["alarm_main_types"] = alarm_main_types
        if alarm_sub_types:
            params["alarm_sub_types"] = alarm_sub_types
        if alarm_type_ids:
            params["alarm_type_ids"] = alarm_type_ids
        if excluded_alarm_type_ids:
            params["excluded_alarm_type_ids"] = excluded_alarm_type_ids
        if start_date:
            params["start_date"] = start_date
        if end_date:
            params["end_date"] = end_date
        if excluded_alarm_main_types:
            params["excluded_alarm_main_types"] = excluded_alarm_main_types
        if excluded_alarm_sub_types:
            params["excluded_alarm_sub_types"] = excluded_alarm_sub_types

        # CHANGED: Multi-tenant endpoint
        url_suffix = f"/v1/multi-tenant/{self.multi_tenant_id}/incidents"

        demisto.debug(f"[SOCRadar-MT] Requesting incidents from: {url_suffix}")
        demisto.debug(f"[SOCRadar-MT] Request params: {params}")

        try:
            response = self._http_request(
                method="GET",
                url_suffix=url_suffix,
                params=params,
                headers=self._get_headers(),
                timeout=60,
                resp_type="json",
            )

            demisto.debug(f"[SOCRadar-MT] Response type: {type(response)}")

            if isinstance(response, dict):
                demisto.debug(f"[SOCRadar-MT] Response keys: {list(response.keys())}")

                if not response.get("is_success", True):
                    error_msg = response.get("message", "Unknown error")
                    demisto.error(f"[SOCRadar-MT] API Error: {error_msg}")
                    raise DemistoException(f"API Error: {error_msg}")

                data_obj = response.get("data", {})

                alarms = data_obj.get("alarms", [])
                total_pages = data_obj.get("total_pages", 1)
                total_records = data_obj.get("total_records", len(alarms))

                demisto.debug(f"[SOCRadar-MT] Received {len(alarms)} alarms from page {page}")
                demisto.debug(f"[SOCRadar-MT] Total records: {total_records}, Total pages: {total_pages}")

                if alarms and len(alarms) > 0:
                    sample = alarms[0]
                    demisto.debug(f"[SOCRadar-MT] Sample alarm keys: {list(sample.keys())}")
                    demisto.debug(f"[SOCRadar-MT] Sample alarm_id: {sample.get('alarm_id')}")
                    demisto.debug(f"[SOCRadar-MT] Sample company_id: {sample.get('company_id')}")
                    demisto.debug(f"[SOCRadar-MT] Sample status: {sample.get('status')}")

                return {
                    "is_success": response.get("is_success"),
                    "message": response.get("message"),
                    "response_code": response.get("response_code"),
                    "data": alarms,
                    "total_pages": int(total_pages),
                    "total_records": int(total_records),
                    "current_page": page,
                }
            else:
                demisto.error(f"[SOCRadar-MT] Unexpected response type: {type(response)}")
                raise DemistoException("Unexpected response format from API")

        except Exception as e:
            demisto.error(f"[SOCRadar-MT] HTTP Request failed: {str(e)}")
            demisto.error(f"[SOCRadar-MT] Traceback: {traceback.format_exc()}")
            raise

    def change_alarm_status(
        self,
        alarm_ids: list[int],
        status_reason: str,
        comments: str | None = None,
        company_id: str | None = None,
        update_related_finding_status: bool | None = None,
        email: str | None = None,
    ) -> dict[str, Any]:
        """Change status of alarms"""
        if status_reason not in STATUS_REASON_MAP:
            raise ValueError(f"Invalid status reason: {status_reason}")

        if not company_id:
            raise ValueError("company_id must be provided for alarm operations")

        # Uses company-specific endpoint
        url_suffix = f"/company/{company_id}/alarms/status/change"
        json_data = {
            "alarm_ids": [str(aid) for aid in alarm_ids],
            "status": STATUS_REASON_MAP[status_reason],
            "comments": comments or "",
        }

        if update_related_finding_status is not None:
            json_data["update_related_finding_status"] = update_related_finding_status
        if email is not None:
            json_data["email"] = email

        response = self._http_request(
            method="POST",
            url_suffix=url_suffix,
            json_data=json_data,
            headers=self._get_headers(),
            timeout=60,
        )

        if not response.get("is_success"):
            raise DemistoException(f"API Error: {response.get('message')}")
        return response

    def add_alarm_comment(
        self,
        alarm_id: int,
        user_email: str,
        comment: str,
        company_id: str | None = None,
    ) -> dict[str, Any]:
        """Add comment to an alarm"""
        if not company_id:
            raise ValueError("company_id must be provided for alarm operations")

        # Uses company-specific endpoint
        url_suffix = f"/company/{company_id}/alarm/add/comment/v2"
        json_data = {"alarm_id": alarm_id, "user_email": user_email, "comment": comment}
        return self._http_request(
            method="POST",
            url_suffix=url_suffix,
            json_data=json_data,
            headers=self._get_headers(),
            timeout=60,
        )

    def add_alarm_assignee(
        self,
        alarm_id: int,
        user_ids: list[int] | None = None,
        user_emails: list[str] | None = None,
        company_id: str | None = None,
    ) -> dict[str, Any]:
        """Add assignee(s) to an alarm"""
        if not company_id:
            raise ValueError("company_id must be provided for alarm operations")

        # Uses company-specific endpoint
        url_suffix = f"/company/{company_id}/alarm/{alarm_id}/assignee"
        json_data: dict[str, Any] = {}

        if user_ids:
            json_data["user_ids"] = user_ids
        if user_emails:
            json_data["user_emails"] = user_emails

        return self._http_request(
            method="POST",
            url_suffix=url_suffix,
            json_data=json_data,
            headers=self._get_headers(),
            timeout=60,
        )

    def add_remove_tag(self, alarm_id: int, tag: str, company_id: str | None = None) -> dict[str, Any]:
        """Add or remove a tag from an alarm"""
        if not company_id:
            raise ValueError("company_id must be provided for alarm operations")

        # Uses company-specific endpoint
        url_suffix = f"/company/{company_id}/alarm/tag"
        json_data = {"alarm_id": alarm_id, "tag": tag}
        return self._http_request(
            method="POST",
            url_suffix=url_suffix,
            json_data=json_data,
            headers=self._get_headers(),
            timeout=60,
        )

    def get_company_id_for_alarm(self, alarm_id: int) -> str | None:
        """
        Fetch company_id for a given alarm_id from multi-tenant API

        This is used when company_id is not provided in commands,
        allowing automatic lookup from the alarm data.
        """
        demisto.debug(f"[SOCRadar-MT] Looking up company_id for alarm_id: {alarm_id}")

        try:
            # Search for the specific alarm using multi-tenant endpoint
            # We use a broad date range to find the alarm
            response = self.search_incidents(limit=1, page=1)

            # If we got results, search through pages to find our alarm
            total_pages = response.get("total_pages", 1)

            for page in range(1, min(total_pages + 1, 100)):  # Limit to 100 pages for safety
                if page > 1:
                    response = self.search_incidents(limit=100, page=page)

                alarms = response.get("data", [])
                for alarm in alarms:
                    if str(alarm.get("alarm_id")) == str(alarm_id):
                        company_id = alarm.get("company_id")
                        demisto.debug(f"[SOCRadar-MT] Found company_id {company_id} for alarm_id {alarm_id}")
                        return str(company_id) if company_id else None

                if len(alarms) < 100:
                    break

            demisto.debug(f"[SOCRadar-MT] Could not find alarm_id {alarm_id} in recent alarms")
            return None

        except Exception as e:
            demisto.error(f"[SOCRadar-MT] Error looking up company_id for alarm {alarm_id}: {str(e)}")
            return None


def test_module(client: Client) -> str:
    """
    Test API connectivity and credentials.

    Uses a narrow time window (last 1 day) and disables total record counting
    to avoid timeout issues on large datasets.
    """
    try:
        demisto.debug("[SOCRadar] Running test module...")

        start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        demisto.debug(f"[SOCRadar] Test module using start_date: {start_date}")

        response = client.search_incidents(limit=1, page=1, start_date=start_date)

        demisto.debug(f"[SOCRadar] Test response: {response}")

        if response.get("is_success"):
            demisto.debug("[SOCRadar] Test module successful")
            return "ok"
        else:
            error_msg = response.get("message", "Unknown error")
            demisto.error(f"[SOCRadar] Test module failed: {error_msg}")
            return f"Test failed: {error_msg}"

    except DemistoException as e:
        error_str = str(e)
        demisto.error(f"[SOCRadar] Test module error: {error_str}")

        if "401" in error_str or "Unauthorized" in error_str:
            return "Authorization Error: Invalid API Key"
        elif "403" in error_str or "Forbidden" in error_str:
            return "Access Denied: Check API Key permissions and Company ID"
        elif "404" in error_str:
            return "API Endpoint Not Found: Check Company ID"
        else:
            return f"Connection failed: {error_str}"
    except Exception as e:
        demisto.error(f"[SOCRadar] Unexpected test error: {str(e)}")
        return f"Unexpected error: {str(e)}"


# FIX: Completely rewritten parse_alarm_date. Previously each try/except block
# could overwrite a successful parse from a prior block, and the final fallback
# referenced an undefined variable 'e' (NameError). Now uses early-return on
# first successful parse and catches only ValueError/IndexError instead of
# bare except.
def parse_alarm_date(date_str: str | None) -> datetime | None:
    """Parse alarm date with multiple format support"""
    if not date_str:
        return None

    formats = [
        ("%Y-%m-%d %H:%M:%S", 19),
        ("%Y-%m-%dT%H:%M:%S.%f", 26),
        ("%Y-%m-%dT%H:%M:%S", 19),
        ("%Y-%m-%d", 10),
    ]

    for fmt, slice_end in formats:
        try:
            return datetime.strptime(date_str[:slice_end], fmt)
        except (ValueError, IndexError):
            continue

    demisto.debug(f"[SOCRadar] Could not parse date: '{date_str}'")
    return None


def format_value(value, indent=0):
    lines = []
    prefix = "  " * indent

    if isinstance(value, dict):
        for k, v in value.items():
            if isinstance(v, dict | list):
                lines.append(f"{prefix}{k}:")
                lines.extend(format_value(v, indent + 1))
            else:
                lines.append(f"{prefix}{k}: {v}")

    elif isinstance(value, list):
        for _, item in enumerate(value):
            if isinstance(item, dict | list):
                lines.append(f"{prefix}-")
                lines.extend(format_value(item, indent + 1))
            else:
                lines.append(f"{prefix}- {item}")

    else:
        lines.append(f"{prefix}{value}")

    return lines


# FIX: Added show_content parameter. Previously the function referenced
# 'show_content' and 'content' variables that were never defined in scope,
# causing NameError at runtime. Now show_content is explicitly passed in
# from the YAML checkbox parameter, and content is extracted from the alarm.
# Also fixed the indentation of the try/except block for format_value —
# it was outside the 'if show_content' guard, so it would always run and
# crash on the undefined 'content' variable even when show_content was False.
def alarm_to_incident(alarm: dict[str, Any], show_content: bool = True) -> dict[str, Any]:
    """
    Convert SOCRadar alarm to Demisto incident

    Args:
        alarm: Raw alarm dict from the SOCRadar API
        show_content: Whether to include full alarm content in incident details
                      (controlled by the show_content checkbox in YAML config)

    IMPORTANT: The 'content' field structure varies by alarm type:
    - Impersonating Domain: has dns_information, whois_information, domain_status
    - Stolen Credentials: has credential_details, log_content_link, asset_match
    - Bad Reputation: may have different fields
    - Each alarm type has its own unique content structure

    We safely extract common fields and include full content in rawJSON.
    """
    company_id = alarm.get("company_id")
    alarm_id = alarm.get("alarm_id")
    alarm_risk_level = alarm.get("alarm_risk_level", "UNKNOWN")
    alarm_asset = alarm.get("alarm_asset", "N/A")
    alarm_status = alarm.get("status", "UNKNOWN")

    alarm_type_details = alarm.get("alarm_type_details", {})
    if not isinstance(alarm_type_details, dict):
        alarm_type_details = {}

    alarm_main_type = alarm_type_details.get("alarm_main_type", "Unknown")
    alarm_sub_type = alarm_type_details.get("alarm_sub_type", "")

    date_str = alarm.get("date")
    occurred_time = parse_alarm_date(date_str)

    # FIX: Added content extraction from alarm. Previously this line was
    # missing entirely in the MT version, causing NameError when 'content'
    # was referenced later in format_value(content) and the if-guard.
    content = alarm.get("content", {})
    if not isinstance(content, dict):
        content = {}

    tags = alarm.get("tags", [])
    if not isinstance(tags, list):
        tags = []
    tags_str = ",".join(str(tag) for tag in tags)

    incident_name = f"SOCRadar Alarm {alarm_id}: {alarm_main_type}"
    if alarm_sub_type:
        incident_name += f" - {alarm_sub_type}"
    incident_name += f" [{alarm_asset}]"

    related_entities = alarm.get("alarm_related_entities", [])
    if not isinstance(related_entities, list):
        related_entities = []

    entity_info = []
    for entity in related_entities:
        if isinstance(entity, dict):
            key = entity.get("key", "")
            value = entity.get("value", "")
            if key and value:
                entity_info.append(f"{key}: {value}")

    alarm_text = alarm.get("alarm_text", "")

    details_parts = []

    details_parts.append(f"Alarm ID: {alarm_id}")
    details_parts.append(f"Risk Level: {alarm_risk_level}")
    details_parts.append(f"Asset: {alarm_asset}")
    details_parts.append(f"Status: {alarm_status}")
    details_parts.append(f"Company ID: {company_id}")

    if alarm_main_type:
        details_parts.append(f"Type: {alarm_main_type}")
    if alarm_sub_type:
        details_parts.append(f"   Sub-Type: {alarm_sub_type}")

    if entity_info:
        details_parts.append("\n Related Entities:")
        for info in entity_info:
            # FIX: Replaced Unicode bullet U+2022 with ASCII dash '-'.
            # XSOAR uses Latin-1 encoding for HTTP responses which cannot
            # encode U+2022, causing 'latin-1 codec can't encode character' error.
            details_parts.append(f"  - {info}")

    if alarm_text:
        details_parts.append("\n Alarm Description:")
        details_parts.append(alarm_text[:3072])

    # FIX: Moved the try/except block for format_value INSIDE the if-guard.
    # Previously the try block was outside 'if show_content', meaning
    # format_value(content) would always execute — and since 'content' was
    # undefined, it would always crash with NameError.
    if show_content and content is not None:
        details_parts.append("\nAlarm Content:")
        try:
            details_parts.extend(format_value(content))
        except Exception as e:
            details_parts.append(f"Content parsing error: {str(e)}")

    if tags:
        details_parts.append(f"\n Tags: {', '.join(str(tag) for tag in tags)}")

    full_details = "\n".join(details_parts)

    incident = {
        "name": incident_name,
        "occurred": (occurred_time.isoformat() + "Z" if occurred_time else datetime.now().isoformat() + "Z"),
        "rawJSON": json.dumps(alarm),
        "severity": convert_to_demisto_severity(alarm_risk_level),
        "details": full_details,
        "dbotMirrorId": str(alarm_id) if alarm_id else None,
        "CustomFields": {
            "socradaralarmid": str(alarm_id) if alarm_id else "unknown",
            "socradarcompanyid": str(company_id) if company_id else "unknown",
            "socradarstatus": alarm_status,
            "socradarasset": alarm_asset,
            "socradaralarmtype": alarm_main_type,
            "socradartags": tags_str,
        },
    }

    demisto.debug(f"[SOCRadar-MT] Created incident: Alarm {alarm_id} - {alarm_main_type} (Risk: {alarm_risk_level})")

    return incident


# FIX: Added show_content parameter to fetch_incidents so it can be passed
# through to alarm_to_incident from the YAML checkbox configuration.
def fetch_incidents(
    client: Client,
    max_results: int,
    last_run: dict[str, Any],
    first_fetch_time: str,
    fetch_interval_minutes: int = 1,
    show_content: bool = True,
    status: list[str] | None = None,
    severities: list[str] | None = None,
    alarm_main_types: list[str] | None = None,
    excluded_alarm_main_types: list[str] | None = None,
    alarm_sub_types: list[str] | None = None,
    excluded_alarm_sub_types: list[str] | None = None,
    alarm_type_ids: list[int] | None = None,
    excluded_alarm_type_ids: list[int] | None = None,
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    """
    Fetch incidents from SOCRadar with time-window based fetching

    Args:
        client: SOCRadar API client
        max_results: Maximum number of incidents to fetch
        last_run: Last run information from previous fetch
        first_fetch_time: Time range for first fetch (e.g., "30 days")
        fetch_interval_minutes: Time window for subsequent fetches in minutes
        show_content: Whether to include full alarm content in incident details
        status: Filter by status (OPEN, CLOSED, ON_HOLD)
        severities: List of severity levels to filter
        alarm_main_types: List of main alarm types to filter
        alarm_sub_types: List of alarm subtypes to filter
        alarm_type_ids: List of alarm type IDs to include
        excluded_alarm_type_ids: List of alarm type IDs to exclude
        excluded_alarm_sub_types: List of alarm sub types to exclude
        excluded_alarm_main_types: List of alarm main types to exclude

    Strategy:
    - First fetch: Use first_fetch_time (e.g., "30 days ago")
    - Subsequent fetches: Use fetch_interval_minutes (e.g., last 1 minute)
    - Fetch ALL pages for the time window
    - Each page sent to XSOAR immediately (max 100 alarms per page)

    Example:
    First fetch (first_fetch_time = "30 days"):
      start_date: 30 days ago
      end_date: now
      Fetch ALL pages, each page -> XSOAR

    Subsequent fetches (fetch_interval = 1 minute):
      start_date: 1 minute ago
      end_date: now
      If 500 new alarms -> 5 pages -> 5 separate sends to XSOAR
    """
    demisto.debug("[SOCRadar-MT] Starting fetch_incidents")
    demisto.debug(f"[SOCRadar-MT] max_results: {max_results}")
    demisto.debug(f"[SOCRadar-MT] fetch_interval_minutes: {fetch_interval_minutes}")
    demisto.debug(f"[SOCRadar-MT] alarm_type_ids: {alarm_type_ids}")
    demisto.debug(f"[SOCRadar-MT] excluded_alarm_type_ids: {excluded_alarm_type_ids}")
    demisto.debug(f"[SOCRadar-MT] excluded_alarm_sub_types: {excluded_alarm_sub_types}")
    demisto.debug(f"[SOCRadar-MT] excluded_alarm_main_types: {excluded_alarm_main_types}")

    # FIX: Renamed to last_fetch_str to avoid variable name collision.
    # Previously 'last_fetch' was reused later as a datetime object which
    # shadowed this string value and could cause confusion.
    last_fetch_str = last_run.get("last_fetch")
    last_alarm_ids = set(last_run.get("last_alarm_ids", []))
    last_alarm_ids_backup = last_alarm_ids.copy()
    current_time = datetime.now()

    demisto.debug(f"[SOCRadar-MT] Last fetch: {last_fetch_str}")
    demisto.debug(f"[SOCRadar-MT] Previously fetched alarm IDs: {len(last_alarm_ids)}")

    if last_fetch_str:
        try:
            start_datetime = datetime.fromisoformat(last_fetch_str) - timedelta(minutes=fetch_interval_minutes)
            demisto.debug(f"[SOCRadar-MT] Subsequent fetch: Using last_fetch datetime {start_datetime.isoformat()}")
        except Exception:
            start_datetime = current_time - timedelta(minutes=fetch_interval_minutes)
            demisto.debug("[SOCRadar-MT] Failed to parse last_fetch, fallback to interval")
    else:
        start_datetime_temp = arg_to_datetime(first_fetch_time, arg_name="first_fetch", required=True)
        if not start_datetime_temp:
            raise ValueError("Failed to parse first_fetch_time")
        start_datetime = start_datetime_temp
        demisto.debug("[SOCRadar-MT] First fetch: Using first_fetch_time")

    start_date = start_datetime.isoformat()
    end_date = current_time.isoformat()

    demisto.debug(f"[SOCRadar-MT] Time window: {start_date} to {end_date}")
    demisto.debug("[SOCRadar-MT] Will fetch ALL pages for this time window")

    all_incidents = []
    reached_max_results = False
    per_page = 100
    latest_page = last_run.get("latest_page", 1)
    current_page = latest_page
    next_run_incidents_to_skip = last_run.get("next_run_incidents_to_skip", 0)
    total_pages = None
    total_incidents_created = 0
    total_pages_fetched = 0

    try:
        while True:
            demisto.debug(f"[SOCRadar-MT] Fetching page {current_page}/{total_pages if total_pages else '?'}")

            response = client.search_incidents(
                status=status,
                severities=severities,
                alarm_main_types=alarm_main_types,
                alarm_sub_types=alarm_sub_types,
                alarm_type_ids=alarm_type_ids,
                excluded_alarm_type_ids=excluded_alarm_type_ids,
                excluded_alarm_main_types=excluded_alarm_main_types,
                excluded_alarm_sub_types=excluded_alarm_sub_types,
                start_date=start_date,
                end_date=end_date,
                limit=per_page,
                page=current_page,
            )

            alarms = response.get("data", [])
            total_records = response.get("total_records", 0)
            total_pages = response.get("total_pages", 0)

            demisto.debug(f"[SOCRadar-MT] Page {current_page}: Received {len(alarms)} alarms")
            if current_page == 1:
                demisto.debug(f"[SOCRadar-MT] Total available in time window: {total_records} records across {total_pages} pages")
                demisto.debug(f"[SOCRadar-MT] Will fetch ALL {total_pages} pages")

            total_pages_fetched += 1

            if not alarms:
                demisto.debug(f"[SOCRadar-MT] No alarms on page {current_page}")
                break

            page_incidents = []
            page_new = 0
            page_dup = 0

            for alarm in alarms[next_run_incidents_to_skip:]:
                if alarm.get("alarm_id") in last_alarm_ids:
                    page_dup += 1
                    continue
                if total_incidents_created < max_results:
                    # FIX: Pass show_content parameter to alarm_to_incident
                    incident = alarm_to_incident(alarm, show_content=show_content)
                    page_incidents.append(incident)
                    total_incidents_created += 1
                    last_alarm_ids.add(alarm.get("alarm_id"))
                    page_new += 1

            demisto.debug(f"[SOCRadar-MT] Page {current_page}: Created {page_new} incidents, skipped {page_dup} duplicates")

            all_incidents.extend(page_incidents)

            if total_incidents_created >= max_results:
                reached_max_results = True
                if current_page == latest_page:
                    next_run_incidents_to_skip += len(page_incidents)
                else:
                    next_run_incidents_to_skip = len(page_incidents)
                demisto.debug(
                    f"[SOCRadar-MT] Reached max_results ({max_results}), fetch pages is stopped. "
                    f"Next run will start from the date of the latest run with skipping {next_run_incidents_to_skip} incidents"
                )
                break
            next_run_incidents_to_skip = 0
            if current_page >= total_pages:
                demisto.debug(f"[SOCRadar-MT] Reached last page ({current_page}/{total_pages})")
                break

            if len(alarms) < per_page:
                demisto.debug(f"[SOCRadar-MT] Partial page ({len(alarms)} < {per_page}), probably last page")
                break

            current_page += 1

        demisto.debug("[SOCRadar-MT] ========== FETCH SUMMARY ==========")
        demisto.debug(f"[SOCRadar-MT] Time window: {start_date} to {end_date}")
        demisto.debug(f"[SOCRadar-MT] Fetch interval: {fetch_interval_minutes} minutes")
        demisto.debug(f"[SOCRadar-MT] Pages fetched: {total_pages_fetched}/{total_pages if total_pages else 'unknown'}")
        demisto.debug(f"[SOCRadar-MT] Incidents created: {total_incidents_created} (max: {max_results})")
        demisto.debug(f"[SOCRadar-MT] Alarm Type IDs filter: {alarm_type_ids}")
        demisto.debug(f"[SOCRadar-MT] Excluded Alarm Type IDs: {excluded_alarm_type_ids}")
        demisto.debug(f"[SOCRadar-MT] Excluded Alarm Sub Types: {excluded_alarm_sub_types}")
        demisto.debug(f"[SOCRadar-MT] Excluded Alarm Main Types: {excluded_alarm_main_types}")
        demisto.debug("[SOCRadar-MT] ====================================")

        # FIX: Use a new variable name 'next_fetch_time' to avoid shadowing
        # the 'last_fetch_str' string from the top of the function.
        if reached_max_results:
            next_fetch_time = start_datetime
        else:
            next_fetch_time = current_time + timedelta(seconds=1)
        combined = list(last_alarm_ids)[:10000]
        next_run = {
            "last_fetch": next_fetch_time.isoformat(),
            "start_date": start_date,
            "end_date": end_date,
            "next_run_incidents_to_skip": next_run_incidents_to_skip,
            "latest_page": current_page if next_run_incidents_to_skip else 1,
            "last_alarm_ids": combined,
        }
        demisto.debug(f"[SOCRadar-MT] Next fetch will use time window: last {fetch_interval_minutes} minutes")
        demisto.debug(f"[SOCRadar-MT] Returning {len(all_incidents)} incidents to XSOAR")

        return next_run, all_incidents

    except Exception as e:
        demisto.error(f"[SOCRadar-MT] Error in fetch_incidents: {str(e)}")
        demisto.error(f"[SOCRadar-MT] Traceback: {traceback.format_exc()}")

        return {
            "last_alarm_ids": list(last_alarm_ids_backup)[:1000],
            "last_fetch": start_date,
            "start_date": start_date,
            "end_date": end_date,
            "exception": str(e),
        }, []


def change_status_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Change status of alarms"""
    alarm_ids_str = args.get("alarm_ids", "")
    status_reason = args.get("status_reason", "")
    comments = args.get("comments")
    company_id = args.get("company_id")
    update_related_finding_status_str = args.get("update_related_finding_status")
    update_related_finding_status: bool | None = (
        argToBoolean(update_related_finding_status_str) if update_related_finding_status_str else None
    )
    email = args.get("email")

    if not alarm_ids_str or not status_reason:
        raise ValueError("alarm_ids and status_reason are required")

    if update_related_finding_status and not email:
        raise ValueError("Argument 'email' is required when 'update_related_finding_status' is set to true.")

    alarm_ids = [int(aid.strip()) for aid in alarm_ids_str.split(",")]

    # Auto-fetch company_id from first alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_ids[0]}")
        company_id = client.get_company_id_for_alarm(alarm_ids[0])
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_ids[0]}. Please provide company_id parameter.")

    response = client.change_alarm_status(
        alarm_ids,
        status_reason,
        comments,
        company_id,
        update_related_finding_status,
        email,
    )

    return CommandResults(
        readable_output=f"Status changed for {len(alarm_ids)} alarm(s)",
        raw_response=response,
    )


def mark_as_false_positive_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Mark alarm as false positive"""
    alarm_id = args.get("alarm_id")
    company_id = args.get("company_id")

    if not alarm_id:
        raise ValueError("alarm_id is required")

    # Auto-fetch company_id from alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_id}")
        company_id = client.get_company_id_for_alarm(int(alarm_id))
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_id}. Please provide company_id parameter.")

    comments = args.get("comments", "Marked as false positive")
    response = client.change_alarm_status([int(alarm_id)], "FALSE_POSITIVE", comments, company_id)

    return CommandResults(
        readable_output=f"Alarm {alarm_id} marked as false positive",
        raw_response=response,
    )


def mark_as_resolved_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Mark alarm as resolved"""
    alarm_id = args.get("alarm_id")
    company_id = args.get("company_id")

    if not alarm_id:
        raise ValueError("alarm_id is required")

    # Auto-fetch company_id from alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_id}")
        company_id = client.get_company_id_for_alarm(int(alarm_id))
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_id}. Please provide company_id parameter.")

    response = client.change_alarm_status(
        [int(alarm_id)],
        "RESOLVED",
        args.get("comments", "Marked as resolved"),
        company_id,
    )

    return CommandResults(readable_output=f"Alarm {alarm_id} marked as resolved", raw_response=response)


def add_comment_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Add comment to alarm"""
    alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True)
    company_id = args.get("company_id")

    if alarm_id is None:
        raise ValueError("alarm_id is required")

    user_email = args.get("user_email", "")
    comment = args.get("comment", "")

    if not user_email or not comment:
        raise ValueError("user_email and comment are required")

    # Auto-fetch company_id from alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_id}")
        company_id = client.get_company_id_for_alarm(alarm_id)
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_id}. Please provide company_id parameter.")

    response = client.add_alarm_comment(alarm_id, user_email, comment, company_id)

    return CommandResults(readable_output=f"Comment added to alarm {alarm_id}", raw_response=response)


def add_assignee_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Add assignee(s) to an alarm."""
    alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True)
    company_id = args.get("company_id")

    if alarm_id is None:
        raise ValueError("alarm_id is required")

    user_emails = argToList(args.get("user_emails"))

    if not user_emails:
        raise ValueError("user_emails is required")

    # Auto-fetch company_id from alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_id}")
        company_id = client.get_company_id_for_alarm(alarm_id)
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_id}. Please provide company_id parameter.")

    response = client.add_alarm_assignee(alarm_id, user_emails=user_emails, company_id=company_id)

    return CommandResults(readable_output=f"Assignee added for alarm {alarm_id}", raw_response=response)


def add_tag_command(client: Client, args: dict[str, str]) -> CommandResults:
    """Add or remove tag from alarm"""
    alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True)
    company_id = args.get("company_id")

    if alarm_id is None:
        raise ValueError("alarm_id is required")

    tag = args.get("tag", "")

    if not tag:
        raise ValueError("tag is required")

    # Auto-fetch company_id from alarm if not provided
    if not company_id:
        demisto.debug(f"[SOCRadar-MT] company_id not provided, fetching from alarm {alarm_id}")
        company_id = client.get_company_id_for_alarm(alarm_id)
        if not company_id:
            raise ValueError(f"Could not find company_id for alarm {alarm_id}. Please provide company_id parameter.")

    response = client.add_remove_tag(alarm_id, tag, company_id)

    return CommandResults(
        readable_output=f"Tag '{tag}' added/removed for alarm {alarm_id}",
        raw_response=response,
    )


def test_fetch_command(client: Client, args: dict[str, str]) -> CommandResults:
    """
    Test incident fetching with safe handling of variable content structures.

    Disables total record counting for faster response times.
    Shows sample incidents to verify API connectivity and data format.
    """
    limit = arg_to_number(args.get("limit", "5"), "limit") or 5

    try:
        first_fetch = args.get("first_fetch", "3 days")
        first_fetch_datetime = arg_to_datetime(first_fetch, arg_name="first_fetch", required=True)
        if not first_fetch_datetime:
            raise ValueError("Failed to parse first_fetch")
        start_date = first_fetch_datetime.strftime("%Y-%m-%d")

        demisto.debug(f"[SOCRadar Test] Testing fetch from {start_date}")

        # FIX: include_total_records is now a proper parameter of search_incidents.
        # Previously passing it as a kwarg caused TypeError because the method
        # did not have this in its signature.
        response = client.search_incidents(
            limit=limit,
            start_date=start_date,
            page=1,
            include_total_records=False,
        )

        data = response.get("data", [])

        if not data:
            # FIX: Replaced emoji characters with ASCII text. XSOAR uses Latin-1
            # encoding which cannot handle Unicode emoji codepoints.
            message = "[X] No incidents found. Possible reasons:\n"
            message += f"- No active alarms in SOCRadar from {start_date}\n"
            message += "- Filters are too restrictive\n"
            message += "- Date range is too narrow\n\n"
            message += f"Tested with start_date: {start_date}\n"

            return CommandResults(readable_output=message, raw_response=response)

        incidents_info = []
        for incident in data[:5]:
            alarm_type_details = incident.get("alarm_type_details")
            if isinstance(alarm_type_details, dict):
                main_type = alarm_type_details.get("alarm_main_type", "Unknown")
                sub_type = alarm_type_details.get("alarm_sub_type", "")
            else:
                main_type = "Unknown"
                sub_type = ""

            alarm_type_display = main_type
            if sub_type:
                alarm_type_display += f" / {sub_type}"

            related_entities = incident.get("alarm_related_entities", [])
            entity_summary = ""
            if isinstance(related_entities, list) and related_entities:
                first_entity = related_entities[0]
                if isinstance(first_entity, dict):
                    entity_value = first_entity.get("value", "")
                    if entity_value:
                        entity_summary = f" | Entity: {entity_value[:30]}"

            incidents_info.append(
                {
                    "Alarm ID": incident.get("alarm_id", "N/A"),
                    "Risk Level": incident.get("alarm_risk_level", "UNKNOWN"),
                    "Status": incident.get("status", "UNKNOWN"),
                    "Asset": incident.get("alarm_asset", "N/A"),
                    "Type": alarm_type_display,
                    "Date": (incident.get("date", "")[:19] if incident.get("date") else "N/A"),
                    "Extra": entity_summary,
                }
            )

        # FIX: Replaced emoji characters with ASCII text to avoid Latin-1 encoding errors.
        message = f"[OK] Found {len(data)} incident(s) on page 1 from {start_date}!\n\n"
        message += "Sample incidents:\n"
        for info in incidents_info:
            message += f"- [{info['Alarm ID']}] {info['Risk Level']} | {info['Status']} | {info['Asset']}\n"
            message += f"  Type: {info['Type']}{info['Extra']}\n"

        if data:
            first_alarm = data[0]
            content = first_alarm.get("content")
            if isinstance(content, dict):
                content_keys = list(content.keys())
                # FIX: Replaced clipboard emoji with ASCII text
                message += f"\nContent structure example (keys): {', '.join(content_keys[:5])}"
                if len(content_keys) > 5:
                    message += f" ... and {len(content_keys) - 5} more"

        return CommandResults(
            readable_output=message,
            outputs_prefix="SOCRadar.TestFetch",
            outputs={
                "TotalCount": len(data),
                "SampleIncidents": incidents_info,
                "StartDate": start_date,
            },
            raw_response=response,
        )

    except Exception as e:
        error_msg = str(e)
        # FIX: Replaced emoji with ASCII text
        message = f"[X] Error testing fetch: {error_msg}\n\n"
        message += "Check:\n"
        message += "- API key validity\n"
        message += "- Network connectivity\n"
        message += "- Company ID correctness\n"
        message += f"- Date parsing (tried to parse: '{args.get('first_fetch', '3 days')}')\n\n"
        message += f"Full error:\n{traceback.format_exc()}"

        return CommandResults(
            readable_output=message,
            raw_response={"error": error_msg, "traceback": traceback.format_exc()},
        )


def main() -> None:
    """Main execution function"""
    params = demisto.params()
    api_key = params.get("apikey")
    multi_tenant_id = params.get("multi_tenant_id")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    demisto.debug(f"[SOCRadar-MT] Starting command: {demisto.command()}")
    demisto.debug(f"[SOCRadar-MT] Multi-Tenant ID: {multi_tenant_id}")

    try:
        client = Client(
            base_url=SOCRADAR_API_ENDPOINT,
            api_key=api_key,
            multi_tenant_id=multi_tenant_id,
            verify=verify_certificate,
            proxy=proxy,
        )

        command = demisto.command()

        if command == "test-module":
            return_results(test_module(client))

        elif command == "fetch-incidents":
            max_fetch = arg_to_number(params.get("max_fetch", 100000)) or 100000
            max_fetch = min(max_fetch, MAX_INCIDENTS_TO_FETCH)
            fetch_interval_minutes = arg_to_number(params.get("fetch_interval_minutes", 1)) or 1

            # FIX: Read show_content from YAML checkbox parameter and pass it
            # through to fetch_incidents -> alarm_to_incident. Previously the
            # show_content variable was referenced inside alarm_to_incident but
            # never defined, causing NameError.
            show_content = params.get("show_content", True)
            if isinstance(show_content, str):
                show_content = show_content.lower() in ("true", "1", "yes")

            alarm_type_ids_str = params.get("alarm_type_ids", "")
            alarm_type_ids = None
            if alarm_type_ids_str:
                try:
                    alarm_type_ids = [int(x.strip()) for x in alarm_type_ids_str.split(",") if x.strip()]
                except ValueError:
                    demisto.error(f"[SOCRadar-MT] Invalid alarm_type_ids format: {alarm_type_ids_str}")

            excluded_alarm_type_ids_str = params.get("excluded_alarm_type_ids", "")
            excluded_alarm_type_ids = None
            if excluded_alarm_type_ids_str:
                try:
                    # FIX: Parse excluded_alarm_type_ids as integers instead of strings.
                    # Previously this used [x.strip() for x in ...] which produced
                    # string values, inconsistent with alarm_type_ids (int list)
                    # and the API expectation of integer IDs.
                    excluded_alarm_type_ids = [int(x.strip()) for x in excluded_alarm_type_ids_str.split(",") if x.strip()]
                except ValueError:
                    demisto.error(f"[SOCRadar-MT] Invalid excluded_alarm_type_ids format: {excluded_alarm_type_ids_str}")

            excluded_alarm_sub_types_str = params.get("excluded_alarm_sub_types", "")
            excluded_alarm_sub_types = None
            if excluded_alarm_sub_types_str:
                try:
                    excluded_alarm_sub_types = [x.strip() for x in excluded_alarm_sub_types_str.split(",") if x.strip()]
                except ValueError:
                    demisto.error(f"[SOCRadar-MT] Invalid excluded_alarm_sub_types format: {excluded_alarm_sub_types_str}")

            excluded_alarm_main_types_str = params.get("excluded_alarm_main_types", "")
            excluded_alarm_main_types = None
            if excluded_alarm_main_types_str:
                try:
                    excluded_alarm_main_types = [x.strip() for x in excluded_alarm_main_types_str.split(",") if x.strip()]
                except ValueError:
                    demisto.error(f"[SOCRadar-MT] Invalid excluded_alarm_main_types format: {excluded_alarm_main_types_str}")

            demisto.debug(
                f"[SOCRadar-MT] Fetch config - max_fetch: {max_fetch}, "
                f"first_fetch: {params.get('first_fetch')}, "
                f"fetch_interval: {fetch_interval_minutes} minutes"
            )
            # FIX: Added missing commas between f-string segments. Previously the
            # four f-strings were concatenated without separators, producing a
            # single unreadable debug line like "...alarm_type_ids: [1]excluded...".
            demisto.debug(
                f"[SOCRadar-MT] Fetch config - alarm_type_ids: {alarm_type_ids}, "
                f"excluded_alarm_type_ids: {excluded_alarm_type_ids}, "
                f"excluded_alarm_main_types: {excluded_alarm_main_types}, "
                f"excluded_alarm_sub_types: {excluded_alarm_sub_types}"
            )

            next_run, incidents = fetch_incidents(
                client=client,
                max_results=max_fetch,
                last_run=demisto.getLastRun(),
                first_fetch_time=params.get("first_fetch", "3 days"),
                fetch_interval_minutes=fetch_interval_minutes,
                show_content=show_content,
                status=argToList(params.get("status")),
                severities=argToList(params.get("severities")),
                alarm_main_types=argToList(params.get("alarm_main_types")),
                alarm_sub_types=argToList(params.get("alarm_sub_types")),
                alarm_type_ids=alarm_type_ids,
                excluded_alarm_type_ids=excluded_alarm_type_ids,
                excluded_alarm_sub_types=excluded_alarm_sub_types,
                excluded_alarm_main_types=excluded_alarm_main_types,
            )

            demisto.debug(f"[SOCRadar-MT] Setting last run to: {next_run}")
            demisto.debug(f"[SOCRadar-MT] Returning {len(incidents)} incidents")

            demisto.setLastRun(next_run)
            demisto.incidents(incidents)

        elif command == "socradar-change-alarm-status":
            return_results(change_status_command(client, demisto.args()))
        elif command == "socradar-mark-false-positive":
            return_results(mark_as_false_positive_command(client, demisto.args()))
        elif command == "socradar-mark-resolved":
            return_results(mark_as_resolved_command(client, demisto.args()))
        elif command == "socradar-add-comment":
            return_results(add_comment_command(client, demisto.args()))
        elif command == "socradar-add-assignee" or command == "socradar-change-assignee":
            return_results(add_assignee_command(client, demisto.args()))
        elif command == "socradar-add-tag":
            return_results(add_tag_command(client, demisto.args()))
        elif command == "socradar-test-fetch":
            return_results(test_fetch_command(client, demisto.args()))
        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    except Exception as e:
        demisto.error(f"[SOCRadar-MT] Error: {str(e)}")
        demisto.error(f"[SOCRadar-MT] Traceback: {traceback.format_exc()}")
        return_error(f"Failed to execute {demisto.command()}.\nError: {str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()