SOCRadarIncidentsV4

SOCRadar Incidents v4 API integration with advanced incident management, status reasons, and compliance tracking. Fetches security incidents from SOCRadar platform with proper deduplication and date handling. Supports filtering by alarm type IDs for granular control.

Analytics & SIEM · SOCRadar

Details

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

README

SOCRadar Incidents v4.0

Fetch and manage security incidents from SOCRadar’s Incident API v4 with advanced filtering, multi-status selection, and comprehensive incident enrichment.

Overview

SOCRadar is a digital risk protection platform that provides extended threat intelligence and brand protection capabilities. This integration enables XSOAR to ingest security incidents from SOCRadar’s Incident API v4, 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

What’s New in v4.0

Major Enhancements

  • 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
  • Parametric Company ID Display: Control whether company ID appears in incidents
  • Enhanced Deduplication: Two-layer protection prevents duplicate incidents
  • New Commands: Ask analyst, change severity, and more

Technical Improvements

  • Interval-based fetching with overlap protection
  • Configurable content and entity inclusion
  • Integer company ID with validation
  • Comprehensive debug logging
  • Better error handling and recovery

Key Features

Incident Fetching

  • Fetch incidents with configurable interval (default: 1 minute)
  • Support for date ranges and epoch timestamps
  • Automatic deduplication (tracks last 1000 alarm IDs)
  • Reverse pagination for optimal performance

Flexible Filtering

  • Multi-Status Selection: OPEN, CLOSED, ON_HOLD (multi-select)
  • Severity Levels: LOW, MEDIUM, HIGH, CRITICAL
  • Alarm Types: Filter by main type, sub-type, or type ID
  • Custom Filters: Tags, assignees, date ranges

Rich Incident Data

  • Dynamic CustomFields based on alarm type
  • Configurable content extraction (varies by alarm type)
  • Optional entity details inclusion
  • Optional company ID visibility

Incident Management

  • Change alarm status (11 status options)
  • Add comments to alarms
  • Change assignees
  • Add/remove tags
  • Request analyst assistance
  • Modify severity levels
  • Mark as false positive/resolved

Prerequisites

Required

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

API Access

To obtain your API credentials:

  1. Log in to SOCRadar Platform
  2. Navigate to Settings → API & Integrations
  3. Go to API Options page
  4. Copy your Company API Key (for Incident API)
  5. Note your Company ID

Configuration

Integration Settings

Parameter Required Default Description
Server URL Yes https://platform.socradar.com/api SOCRadar API base URL
API Key Yes - Your Company API Key from SOCRadar
Company ID Yes - Your Company 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

Content Options

Parameter Default Description
Include Alarm Content True Extract content fields to CustomFields
Include Related Entities True Include entity details in CustomFields
Include Company ID False Show company ID in incidents (for multi-tenant)

Installation

From XSOAR Marketplace

  1. Navigate to Settings → Marketplace
  2. Search for “SOCRadar”
  3. Click Install
  4. Configure integration instance

From GitHub (Manual)

# Clone the content repository
git clone https://github.com/demisto/content.git
cd content/Packs/SOCRadar

# Copy to your XSOAR instance
# Upload via Settings → Integrations → Upload

Setup Guide

Step 1: Create Integration Instance

  1. Navigate to Settings → Integrations → Servers & Services
  2. Search for “SOCRadar Incidents v4”
  3. Click Add instance

Step 2: Configure Basic Settings

Name: SOCRadar Production
Server URL: https://platform.socradar.com/api
API Key: [Your API Key from SOCRadar]
Company ID: [Your Company ID]

Step 3: Configure Fetching (Optional)

✓ Fetches incidents
Incident type: SOCRadar Incident
Max incidents per fetch: 10000
First fetch time: 3 days
Fetch Interval: 1 (minutes)

Step 4: Configure Filters (Optional)

Status Filter:
  ☑ OPEN
  ☐ CLOSED
  ☐ ON_HOLD

Content Options:
  ☑ Include Alarm Content in Custom Fields
  ☑ Include Related Entities Details
  ☐ Include Company ID in Incidents

Step 5: Test Connection

Click Test button to verify:

  • API connectivity
  • Authentication
  • Company ID validity

Commands

Incident Management

socradar-change-alarm-status

Change the status of one or more alarms.

Arguments:

  • alarm_ids (Required): Comma-separated alarm IDs (e.g., “12345,67890”)
  • status_reason (Required): New status
    • OPEN, INVESTIGATING, RESOLVED, PENDING_INFO
    • LEGAL_REVIEW, VENDOR_ASSESSMENT, FALSE_POSITIVE
    • DUPLICATE, PROCESSED_INTERNALLY, MITIGATED, NOT_APPLICABLE
  • comments (Optional): Status change comments
  • company_id (Optional): Override default company ID

Example:

!socradar-change-alarm-status alarm_ids="81171696" status_reason="INVESTIGATING" comments="Under review"

From Incident Context:

!socradar-change-alarm-status alarm_ids=${incident.dbotMirrorId} status_reason="RESOLVED"

socradar-mark-false-positive

Mark alarm as false positive (shortcut command).

Arguments:

  • alarm_id (Required): Alarm ID
  • comments (Optional): Reason for false positive
  • company_id (Optional): Override default company ID

Example:

!socradar-mark-false-positive alarm_id="81171696" comments="Not a real threat"

socradar-mark-resolved

Mark alarm as resolved (shortcut command).

Arguments:

  • alarm_id (Required): Alarm ID
  • comments (Optional): Resolution notes
  • company_id (Optional): Override default company ID

Example:

!socradar-mark-resolved alarm_id="81171696" comments="Issue fixed"

For Notes

socradar-add-comment

Add a comment to an alarm.

Arguments:

  • alarm_id (Required): Alarm ID
  • user_email (Required): Email of user posting comment
  • comment (Required): Comment text
  • company_id (Optional): Override default company ID

Example:

!socradar-add-comment alarm_id="81171696" user_email="useremail" comment="Investigating with security team"

socradar-ask-analyst

Request assistance from SOCRadar analyst.

Arguments:

  • alarm_id (Required): Alarm ID
  • comment (Required): Message for analyst
  • company_id (Optional): Override default company ID

Example:

!socradar-ask-analyst alarm_id="81171696" comment="Need help analyzing this credential leak"

Assignment & Organization

socradar-add-assignee

Change alarm assignee(s).(User must be defined the same company)

Arguments:

  • alarm_id (Required): Alarm ID
  • user_emails (Required): Comma-separated email addresses
  • company_id (Optional): Override default company ID

Example:

!socradar-add-assignee alarm_id="81171696" user_emails="useremail"

socradar-add-tag

Add or remove a tag from alarm.

Arguments:

  • alarm_id (Required): Alarm ID
  • tag (Required): Tag name
  • company_id (Optional): Override default company ID

Example:

!socradar-add-tag alarm_id="81171696" tag="reviewed"

socradar-change-severity

Modify alarm severity level.

Arguments:

  • alarm_id (Required): Alarm ID
  • severity (Required): New severity (LOW, MEDIUM, HIGH, CRITICAL)
  • company_id (Optional): Override default company ID

Example:

!socradar-change-severity alarm_id="81171696" severity="HIGH"

Testing

socradar-test-fetch

Test the fetch incidents functionality without creating incidents.

Arguments:

  • first_fetch (Optional): Time range to test (default: “3 days”)
  • limit (Optional): Number of incidents to fetch (default: 5)

Example:

!socradar-test-fetch first_fetch="1 hour" limit="10"

Output:

Found 150 incident(s) from 2024-12-20!
Total available: 1000 records across 10 pages
Using REVERSE PAGINATION (page 10 → page 1)

Sample incidents:
- [81171696] HIGH | OPEN | example.com
  Company: 789 | Type: Brand Protection / Impersonating Domain
  


Custom Fields

The integration creates these CustomFields in XSOAR incidents:

Standard Fields (Always Present)

  • socradaralarmid: Alarm ID
  • socradarstatus: Current status
  • socradarasset: Affected asset
  • socradaralarmtype: Main alarm type
  • socradaralarmsubtype: Alarm sub-type
  • socradaralarmtypeid: Type ID
  • socradartags: Comma-separated tags
  • socradarrisklevel: Risk level
  • socradaralarmtext: Alarm description (truncated to 1000 chars)

Optional Fields

  • socradarcompanyid: Company ID (if “Include Company ID” enabled)
  • socradarentities: Related entities (if “Include Entities” enabled)

Dynamic Content Fields (if “Include Content” enabled)

Content structure varies by alarm type. Examples:

Impersonating Domain:

  • socradarcontentdns_information
  • socradarcontentwhois_information
  • socradarcontentdomain_status

Stolen Credentials:

  • socradarcontentcredential_details
  • socradarcontentlog_content_link
  • socradarcontentmalware_family

Data Leak:

  • socradarcontentsource_full_content
  • socradarcontentcompromised_emails
  • socradarcontentcompromised_domains

Use Cases

1. Brand Protection

Scenario: Detect and respond to phishing domains impersonating your brand.

Configuration:

Main Alarm Types: Brand Protection
Status Filter: OPEN
Severity: HIGH, CRITICAL

Automation:

# Get incident
incident = demisto.incident()
alarm_id = incident.get('dbotMirrorId')

# Check domain details
domain_info = incident.get('CustomFields', {}).get('socradarcontentdns_information')

# Take action
demisto.executeCommand('socradar-add-assignee', {
    'alarm_id': alarm_id,
    'user_emails': 'useremail@usermail.com'
})

2. Multi-Tenant Operations (MSSP)

Scenario: Manage incidents for multiple customers.

Configuration:

Instance 1:
- Company ID: 789
- Include Company ID: ✓ YES

Instance 2:
- Company ID: 456
- Include Company ID: ✓ YES

Automation:

# Company ID is visible in CustomFields
company_id = incident.get('CustomFields', {}).get('socradarcompanyid')

# Route to appropriate team
if company_id == "789":
    assign_to = "useremail"
else:
    assign_to = "useremail"

demisto.executeCommand('socradar-add-assignee', {
    'alarm_id': incident.get('dbotMirrorId'),
    'user_emails': assign_to
})

Troubleshooting

No Incidents Fetched

Check:

  1. Test connection: Click “Test” button
  2. Date range: Increase “First fetch time” to “7 days”
  3. Filters: Remove status/severity filters temporarily
  4. Debug logs: Check XSOAR server logs

Debug Command:

!socradar-test-fetch first_fetch="7 days" limit="10"

Look for:

[SOCRadar V4.0] Total available: 0 records

If 0 records, no alarms match your filters.

Duplicate Incidents

This should NOT happen in v4.0! If you see duplicates:

  1. Check fetch interval: Should be ≥ 1 minute
  2. Check logs for deduplication stats:

    [SOCRadar V4.0] Page 1: Created 5 NEW incidents, skipped 95 duplicates
    
  3. Verify epoch time usage:

    [SOCRadar V4.0] Time window (epoch seconds): 1734465600 to 1734465660
    

If duplicates persist, contact support with debug logs.

API Errors

401 Unauthorized:

  • Verify API Key is correct

404 Not Found:

  • Verify Company ID is correct (integer, not string)
  • Check endpoint URLs in debug logs

Rate Limiting:

  • Reduce fetch frequency
  • Decrease “Max incidents per fetch”

Missing CustomFields

If dynamic content fields not appearing:

  1. Verify “Include Alarm Content” is enabled
  2. Create CustomFields manually in XSOAR:
    • Settings → Advanced → Fields
    • Add fields with type “Short Text”
    • Prefix: socradarcontent

If entities not appearing:

  1. Verify “Include Related Entities Details” is enabled

Performance Optimization

High-Volume Environments

Recommended Settings:

Max incidents per fetch: 10000
Fetch Interval: 5 (minutes)
Include Alarm Content: ✓ (enabled)
Include Related Entities: ✓ (enabled)

With Filters:

Status Filter: OPEN only
Severity: HIGH, CRITICAL only

This reduces data volume while capturing critical alerts.

Low-Volume Environments

Recommended Settings:

Max incidents per fetch: 1000
Fetch Interval: 1 (minutes)
Include Alarm Content: ✓ (enabled)
Include Related Entities: ✓ (enabled)

Faster response to new incidents.

Support

Official Support

Feature Requests

Submit feature requests through:

  1. SOCRadar platform Request
  2. XSOAR content repository issues

Version History

v4.0.0 (December 2024)

  • Initial release of SOCRadar Incidents v4.0
  • Multi-status filtering support
  • Epoch time precision for zero duplicates
  • Reverse pagination implementation
  • Dynamic content extraction
  • Enhanced deduplication (2-layer)
  • New commands: ask-analyst, change-severity
  • Parametric company ID display
  • Comprehensive debug logging

License

This integration is provided under the MIT License.

About SOCRadar

SOCRadar is a leading Extended Threat Intelligence (XTI) platform that helps organizations:

  • Monitor and protect their digital assets
  • Detect brand abuse and phishing attacks
  • Identify stolen credentials and data leaks
  • Track dark web activities
  • Manage attack surface exposure
  • Ensure supply chain security

Configuration parameters

  • apikey — (required)
  • company_id — Company 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)
  • include_company_id — Include Company ID
  • include_incident_link — Include Incident Link
  • include_mitigation — Include Mitigation
  • include_response — Include Response
  • include_detection_and_analysis — Include Detection And Analysis
  • include_post_incident_analysis — Include Post Incident Analysis
  • include_related_assets — Include Related Assets
  • include_related_entities — Include Related Entities
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (8)

  • 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-change-assignee Deprecated

    add the assignee(s) of an alarm.

  • 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, company_id: str, verify: bool, proxy: bool):
        super().__init__(base_url, verify=verify, proxy=proxy)
        self.api_key = api_key
        self.company_id = company_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. When True, the API returns
        # total_pages and total_records in the response. Previously this was
        # hardcoded to "true" and test_fetch_command passed it as a kwarg which
        # caused a TypeError since search_incidents did not accept it.
        include_total_records: bool = True,
        include_company_id: bool = False,
    ) -> dict[str, Any]:
        """
        Search incidents from SOCRadar API

        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
            include_company_id: Whether to request company_id in alarm data from the API

        API Response Structure:
        {
          "data": {
            "alarms": [...],
            "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"] = status
        if severities:
            params["severities"] = 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
        if include_company_id:
            params["include_company_id"] = "true"

        url_suffix = f"/company/{self.company_id}/incidents/v4"

        demisto.debug(f"[SOCRadar] Requesting incidents from: {url_suffix}")
        demisto.debug(f"[SOCRadar] 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] Response type: {type(response)}")

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

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

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

                # When include_total_records is false, API returns data as a list
                # instead of a dict with "alarms", "total_pages", "total_records" keys.
                if isinstance(data_obj, list):
                    alarms = data_obj
                    total_pages = 1
                    total_records = len(alarms)
                else:
                    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] Received {len(alarms)} alarms from page {page}")
                demisto.debug(f"[SOCRadar] Total records: {total_records}, Total pages: {total_pages}")

                if alarms and len(alarms) > 0:
                    sample = alarms[0]
                    demisto.debug(f"[SOCRadar] Sample alarm keys: {list(sample.keys())}")
                    demisto.debug(f"[SOCRadar] Sample alarm_id: {sample.get('alarm_id')}")
                    demisto.debug(f"[SOCRadar] 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] Unexpected response type: {type(response)}")
                raise DemistoException("Unexpected response format from API")

        except Exception as e:
            demisto.error(f"[SOCRadar] HTTP Request failed: {str(e)}")
            demisto.error(f"[SOCRadar] 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}")

        effective_company_id = company_id or self.company_id
        if not effective_company_id:
            raise ValueError("company_id must be provided either as a parameter or set on the client")

        url_suffix = f"/company/{effective_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"""
        effective_company_id = company_id or self.company_id
        if not effective_company_id:
            raise ValueError("company_id must be provided either as a parameter or set on the client")

        url_suffix = f"/company/{effective_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"""
        effective_company_id = company_id or self.company_id
        if not effective_company_id:
            raise ValueError("company_id must be provided either as a parameter or set on the client")

        url_suffix = f"/company/{effective_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"""
        effective_company_id = company_id or self.company_id
        if not effective_company_id:
            raise ValueError("company_id must be provided either as a parameter or set on the client")

        url_suffix = f"/company/{effective_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 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'. 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 a
# variable 'show_content' that was never defined in scope, causing a NameError
# at runtime. Now it is explicitly passed in from the YAML checkbox parameter.
def alarm_to_incident(
    alarm: dict[str, Any],
    show_content: bool = True,
    include_mitigation: bool = False,
    include_response: bool = False,
    include_detection_and_analysis: bool = False,
    include_post_incident_analysis: bool = False,
    include_related_assets: bool = False,
    include_related_entities: bool = False,
    include_company_id: bool = False,
    include_incident_link: bool = False,
    configured_company_id: str = "",
) -> 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)
        include_mitigation: Whether to include mitigation plan in custom fields
        include_response: Whether to include response steps in custom fields
        include_detection_and_analysis: Whether to include detection & analysis in custom fields
        include_post_incident_analysis: Whether to include post-incident analysis in custom fields
        include_related_assets: Whether to include related assets in custom fields
        include_related_entities: Whether to include related entities in custom fields
        include_company_id: Whether to include company ID in custom fields
        configured_company_id: Company ID from integration configuration (fallback when alarm has no company_id)

    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") or configured_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)
    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}]"

    # Build related entities string
    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}")

    related_entities_str = "\n".join(entity_info)

    # Build related assets string
    related_assets = alarm.get("alarm_related_assets", [])
    if not isinstance(related_assets, list):
        related_assets = []

    asset_info = []
    for asset in related_assets:
        if isinstance(asset, dict):
            asset_key = asset.get("key", "")
            asset_value = asset.get("value", "")
            if asset_key and asset_value:
                if isinstance(asset_value, list):
                    asset_value = [str(v) for v in asset_value if v]
                    if asset_value:
                        asset_info.append(f"{asset_key}: {' || '.join(asset_value)}")
                else:
                    asset_info.append(f"{asset_key}: {asset_value}")

    related_assets_str = "\n".join(asset_info)

    # Extract enrichment fields from alarm_type_details
    alarm_mitigation = alarm_type_details.get("alarm_default_mitigation_plan", "")
    alarm_response = alarm.get("alarm_response", "")
    alarm_detection_analysis = alarm_type_details.get("alarm_detection_and_analysis", "")
    alarm_post_incident = alarm_type_details.get("alarm_post_incident_analysis", "")

    # Promote to top-level rawJSON only when include is enabled
    if include_mitigation and alarm_mitigation:
        alarm["alarm_mitigation"] = alarm_mitigation
    if include_response and alarm_response:
        alarm["alarm_response_plan"] = alarm_response
    if include_detection_and_analysis and alarm_detection_analysis:
        alarm["alarm_detection_and_analysis"] = alarm_detection_analysis
    if include_post_incident_analysis and alarm_post_incident:
        alarm["alarm_post_incident_analysis"] = alarm_post_incident

    # Build incident content string from content dict
    incident_content = ""
    if show_content and content:
        try:
            content_parts = format_value(content)
            incident_content = "\n".join(content_parts)
        except Exception as e:
            incident_content = f"Content parsing error: {str(e)}"

    # Build incident link
    incident_link = ""
    if company_id and alarm_id:
        incident_link = (
            f"https://platform.socradar.com/app/company/{company_id}"
            f"/alarm-management?tab=approved&field=alarmId&operator=equals&value={alarm_id}"
        )

    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 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])

    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)

    custom_fields: dict[str, Any] = {
        "socradaralarmid": str(alarm_id) if alarm_id else "unknown",
        "socradarstatus": alarm_status,
        "socradarasset": alarm_asset,
        "socradaralarmtype": alarm_main_type,
        "socradartags": tags_str,
    }

    if include_mitigation and alarm_mitigation:
        custom_fields["socradarmitigation"] = alarm_mitigation
    if include_response and alarm_response:
        custom_fields["socradarresponse"] = alarm_response
    if include_detection_and_analysis and alarm_detection_analysis:
        custom_fields["socradardetectionandanalysis"] = alarm_detection_analysis
    if include_post_incident_analysis and alarm_post_incident:
        custom_fields["socradarpostincidentanalysis"] = alarm_post_incident
    if include_related_assets and related_assets_str:
        custom_fields["socradarrelatedassets"] = related_assets_str
    if include_related_entities and related_entities_str:
        custom_fields["socradarrelatedentities"] = related_entities_str
    if include_company_id:
        if "company_id" not in alarm:
            alarm["company_id"] = str(company_id) if company_id else ""
        custom_fields["socradarcompanyid"] = str(company_id) if company_id else ""
    if include_incident_link:
        if incident_link:
            alarm["incident_link"] = incident_link
        custom_fields["socradarincidentlink"] = incident_link
        custom_fields["socradarincidentcontent"] = incident_content

    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": custom_fields,
    }

    demisto.debug(f"[SOCRadar] 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,
    include_mitigation: bool = False,
    include_response: bool = False,
    include_detection_and_analysis: bool = False,
    include_post_incident_analysis: bool = False,
    include_related_assets: bool = False,
    include_related_entities: bool = False,
    include_company_id: bool = False,
    include_incident_link: bool = False,
    configured_company_id: str = "",
    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] Starting fetch_incidents")
    demisto.debug(f"[SOCRadar] max_results: {max_results}")
    demisto.debug(f"[SOCRadar] fetch_interval_minutes: {fetch_interval_minutes}")
    demisto.debug(f"[SOCRadar] alarm_type_ids: {alarm_type_ids}")
    demisto.debug(f"[SOCRadar] excluded_alarm_type_ids: {excluded_alarm_type_ids}")
    demisto.debug(f"[SOCRadar] excluded_alarm_sub_types: {excluded_alarm_sub_types}")
    demisto.debug(f"[SOCRadar] 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] Last fetch: {last_fetch_str}")

    if last_fetch_str:
        try:
            start_datetime = datetime.fromisoformat(last_fetch_str) - timedelta(minutes=fetch_interval_minutes)
            demisto.debug(f"[SOCRadar] Subsequent fetch: Using last_fetch datetime {start_datetime.isoformat()}")
        except Exception:
            start_datetime = current_time - timedelta(minutes=fetch_interval_minutes)
            demisto.debug("[SOCRadar] 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] First fetch: Using first_fetch_time")

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

    demisto.debug(f"[SOCRadar] Time window: {start_date} to {end_date}")
    demisto.debug("[SOCRadar] 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] 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,
                include_company_id=include_company_id,
            )

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

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

            total_pages_fetched += 1

            if not alarms:
                demisto.debug(f"[SOCRadar] 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,
                        include_mitigation=include_mitigation,
                        include_response=include_response,
                        include_detection_and_analysis=include_detection_and_analysis,
                        include_post_incident_analysis=include_post_incident_analysis,
                        include_related_assets=include_related_assets,
                        include_related_entities=include_related_entities,
                        include_company_id=include_company_id,
                        include_incident_link=include_incident_link,
                        configured_company_id=configured_company_id,
                    )
                    page_incidents.append(incident)
                    total_incidents_created += 1
                    last_alarm_ids.add(alarm.get("alarm_id"))
                    page_new += 1

            demisto.debug(f"[SOCRadar] 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] 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] Reached last page ({current_page}/{total_pages})")
                break

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

            current_page += 1

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

        # 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] Returning {len(all_incidents)} incidents to XSOAR")

        return next_run, all_incidents

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

        return {
            "last_alarm_ids": list(last_alarm_ids_backup)[:10000],
            "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(",")]
    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")

    response = client.change_alarm_status(
        [int(alarm_id)],
        "FALSE_POSITIVE",
        args.get("comments", "Marked as false positive"),
        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")

    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")

    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")

    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")

    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:
            message = "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,
                }
            )

        message = f"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())
                message += f"\n📋 Content 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)
        message = f"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_param = params.get("apikey")
    if isinstance(api_key_param, dict):
        api_key = api_key_param.get("password", "")
    else:
        api_key = api_key_param
    company_id = params.get("company_id")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    demisto.debug(f"[SOCRadar] Starting command: {demisto.command()}")
    demisto.debug(f"[SOCRadar] Company ID: {company_id}")

    try:
        client = Client(
            base_url=SOCRADAR_API_ENDPOINT,
            api_key=api_key,
            company_id=company_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")

            include_mitigation = argToBoolean(params.get("include_mitigation", False))
            include_response = argToBoolean(params.get("include_response", False))
            include_detection_and_analysis = argToBoolean(params.get("include_detection_and_analysis", False))
            include_post_incident_analysis = argToBoolean(params.get("include_post_incident_analysis", False))
            include_related_assets = argToBoolean(params.get("include_related_assets", False))
            include_related_entities = argToBoolean(params.get("include_related_entities", False))
            include_company_id = argToBoolean(params.get("include_company_id", False))
            include_incident_link = argToBoolean(params.get("include_incident_link", False))

            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] 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] 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] 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] Invalid excluded_alarm_main_types format: {excluded_alarm_main_types_str}")

            demisto.debug(
                f"[SOCRadar] Fetch config - max_fetch: {max_fetch}, "
                f"first_fetch: {params.get('first_fetch')}, "
                f"fetch_interval: {fetch_interval_minutes} minutes"
            )
            demisto.debug(
                f"[SOCRadar] 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,
                include_mitigation=include_mitigation,
                include_response=include_response,
                include_detection_and_analysis=include_detection_and_analysis,
                include_post_incident_analysis=include_post_incident_analysis,
                include_related_assets=include_related_assets,
                include_related_entities=include_related_entities,
                include_company_id=include_company_id,
                include_incident_link=include_incident_link,
                configured_company_id=company_id or "",
                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] Setting last run to: {next_run}")
            demisto.debug(f"[SOCRadar] 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] Error: {str(e)}")
        demisto.error(f"[SOCRadar] Traceback: {traceback.format_exc()}")
        return_error(f"Failed to execute {demisto.command()}.\nError: {str(e)}")


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