iZOOlogic

Fetches and manages incidents from iZOOlogic, enabling automated ingestion, incident creation, and advanced filtering for brand protection and threat management.

Network Security · iZOOlogic

Details

IDiZOOlogic
ProvideriZOOlogic
CategoryNetwork Security
From Version8.2.0
Docker Imagedemisto/fastapi:0.125.0.10158186

README

Fetches and manages incidents from iZOOlogic, enabling automated ingestion, incident creation, and advanced filtering for brand protection and threat management.

Configure iZOOlogic in Cortex

Parameter Description Required
Server URL The iZOOlogic API server URL. True
API Key The API key provided by iZOOlogic for authentication. True
Secret Key The secret key corresponding to the API key. True
Trust any certificate (not secure) Whether to trust any certificate (not secure). False
Use system proxy settings Whether to use the system proxy settings. False
Fetch incidents Whether to fetch incidents from iZOOlogic. False
Fetch incident types A comma-separated list of incident types to fetch from iZOOlogic. True
Maximum incidents per fetch per type The maximum number of incidents to fetch per type per fetch cycle. True

Commands

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

izoologic-get-events


Gets events from iZOOlogic. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.

Base Command

izoologic-get-events

Input

Argument Name Description Required
limit The maximum number of events to return per type. Default is 50. Optional
start_time The time to filter events detected at or after. Supports ISO 8601 format or relative time expressions (e.g., “3 days ago”, “2024-01-01T00:00:00Z”). Optional
end_time The time to filter events detected at or before. Supports ISO 8601 format or relative time expressions (e.g., “now”, “2024-01-01T00:00:00Z”). Optional
event_type The event types to filter by, as a comma-separated list. If not specified, the command uses the types configured in the integration parameters. Possible values are: brand abuse, phishing, malware, pharming, smishing, vishing, mobile apps, social media, other, email. Optional
should_push_events The flag that indicates whether to push events to Cortex XSIAM. Pushing events is supported on Cortex XSIAM only. When set to false, or on non-Cortex XSIAM platforms, events are displayed without being pushed. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
iZOOlogic.Incident.incidentID String The unique identifier of the incident.
iZOOlogic.Incident.incidentType String The type and subtype of the incident.
iZOOlogic.Incident.subIncidentType String The subtype of the incident.
iZOOlogic.Incident.detectionDate String The detection date of the incident as a Unix timestamp.
iZOOlogic.Incident.url String The URL associated with the incident.
iZOOlogic.Incident.status String The current status of the incident.
iZOOlogic.Incident.statusCode Number The numeric status code of the incident.
iZOOlogic.Incident.brand String The brand associated with the incident.
iZOOlogic.Incident.threatType String The threat level of the incident.
iZOOlogic.Incident.createdOn String The creation date of the incident as a Unix timestamp.
iZOOlogic.Incident.closedOn String The closing date of the incident as a Unix timestamp.
iZOOlogic.Incident.detectedBy String The entity that detected the incident.

Command example

!izoologic-get-events limit=3

Human Readable Output

iZOOlogic Events

Incident ID Incident Type Sub Incident Type Brand Url Status Status Code Threat Type Detection Date Created On Closed On Detected By
uVJxla1s1 Brand Abuse - Fake Website Fake Website TVS Motor https://tvsmotor.com.mt Waiting 17 Substantial Threat 1760941801 1769509374   Reported By iZOOLogic
1JrJzZBip Phishing   TVS Credit https://tvs-credit.dev.veefin.in Closed 16 High Threat 1769792260 1769792260 1770180062 Reported By iZOOLogic
KIks8sE3U Social Media - Facebook Facebook TVS King https://www.facebook.com/ads/library/?id=917334661007536 Waiting 17 Substantial Threat 1769626014 1769626014   Reported By iZOOLogic

izoologic-incident-create


Creates a new security incident in iZOOlogic.

Base Command

izoologic-incident-create

Input

Argument Name Description Required
incident_url The URL, email, or target of the security incident (max 1000 characters). Required
incident_type The type of incident. Possible values are: brand abuse, phishing, malware, pharming, smishing, vishing, mobile apps, social media, other, email, executive. Required
brand_code The brand identifier associated with the incident. Required
threat_type The threat level. Possible values are: low threat, moderate threat, substantial threat, high threat, critical threat, redirect to whitelist. Default is moderate threat. Optional
case_type The preferred case type for processing. All new incidents are initially created as “Reported Incident” and may be reclassified during review. Possible values are: incident, brand abuse monitoring, domain monitoring, social media monitoring, mobile app monitoring, executive monitoring. Default is incident. Optional
comment The comments about the incident (max 2500 characters). Optional
executive_name The executive name. Required for executive-related incidents (max 2500 characters). Optional
client_code The client identifier for validation and access control. Optional

Context Output

Path Type Description
iZOOlogic.Incident.reportedIncidentId String The unique identifier for the created incident case.
iZOOlogic.Incident.statusCode Number The numeric status code (1 = under review).
iZOOlogic.Incident.statusDescription String The human-readable status description.
iZOOlogic.Incident.caseType Number The case type code (9 = reported incident).
iZOOlogic.Incident.caseTypeDescription String The human-readable case type description.

Command example

!izoologic-incident-create incident_url="https://test-malicious-site.example.com" incident_type="phishing" brand_code="QnjggfvwlW"

Human Readable Output

iZOOlogic - New Incident Created

Reported Incident Id Status Code Status Description Case Type Case Type Description
ycB2E7gPQ 1 Under Review 9 Reported Incident

izoologic-incident-fetch


Fetches incidents from iZOOlogic based on specified filters including date range, brand, incident type, and other criteria.

Base Command

izoologic-incident-fetch

Input

Argument Name Description Required
from_date The start date for filtering incidents. Supports ISO 8601 format or relative time expressions (e.g., “1 day ago”, “2024-01-01T00:00:00Z”). Maximum date range is 31 days. Default is 1 day ago. Optional
to_date The end date for filtering incidents. Supports ISO 8601 format or relative time expressions (e.g., “now”, “2024-01-01T00:00:00Z”). Maximum date range is 31 days. Default is now. Optional
incident_type The type of incident to filter by. Possible values are: brand abuse, phishing, malware, pharming, smishing, vishing, mobile apps, social media, other, email, executive. Optional
threat_type The threat level to filter by. Possible values are: low threat, moderate threat, substantial threat, high threat, critical threat, redirect to whitelist. Optional
brand_code The brand identifier to filter incidents by. Optional
executive_name The executive name for filtering executive-related incidents (max 100 characters). Optional
client_ref_id The client reference ID for specific incident lookup. Optional
client_code The client identifier for filtering incidents. Optional

Context Output

Path Type Description
iZOOlogic.Incident.incidentID String The unique identifier of the incident.
iZOOlogic.Incident.incidentType String The type and subtype of the incident.
iZOOlogic.Incident.subIncidentType String The subtype of the incident.
iZOOlogic.Incident.detectionDate String The detection date of the incident as a Unix timestamp (e.g., 1704067200).
iZOOlogic.Incident.url String The URL associated with the incident.
iZOOlogic.Incident.status String The current status of the incident.
iZOOlogic.Incident.statusCode Number The numeric status code of the incident.
iZOOlogic.Incident.brand String The brand associated with the incident.
iZOOlogic.Incident.threatType String The threat level of the incident.
iZOOlogic.Incident.createdOn String The creation date of the incident as a Unix timestamp (e.g., 1704067200).
iZOOlogic.Incident.closedOn String The closing date of the incident as a Unix timestamp (e.g., 1704067200).
iZOOlogic.Incident.detectedBy String The entity that detected the incident.

Command example

!izoologic-incident-fetch from_date="1 day ago" incident_type="phishing"

Human Readable Output

iZOOlogic Incidents

Incident ID Incident Type Brand Url Status Status Code Threat Type Detection Date Created On Detected By
abc123 Phishing ExampleBrand https://example.com Active 1 High Threat 1700000000 1700000200 Reported By iZOOLogic

Configuration parameters

  • url — Server URL (required)
  • api_key — (required)
  • secret_key — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchEvents
  • events_types_filter — Fetch incident types (required)
  • max_fetch — Maximum incidents per fetch per type (required)

Commands (3)

  • izoologic-get-events

    Gets events from iZOOlogic. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.

  • izoologic-incident-create

    Creates a new security incident in iZOOlogic.

  • izoologic-incident-fetch

    Fetches incidents from iZOOlogic based on specified filters including date range, brand, incident type, and other criteria.

import threading
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, UTC
from typing import Any

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from CommonServerUserPython import *  # noqa
from ContentClientApiModule import *

"""
iZOOlogic
Integration for fetching threat events from the iZOOlogic API.
"""

# region Constants and helpers
# =================================
# Constants and helpers
# =================================
INTEGRATION_NAME = "iZOOlogic"
VENDOR = "iZOOlogic"
PRODUCT = "iZOOlogic"


class ApiPaths:
    """Centralized iZOOlogic API endpoint paths."""

    AUTHENTICATE = "/api/Token/Authenticate"
    FETCH_EVENTS = "/api/ThreatManagement/FetchIncidents"
    REPORT_NEW_INCIDENT = "/api/ThreatManagement/ReportNewIncident"


class Config:
    """Global static configuration."""

    # Pagination
    DEFAULT_LIMIT = 50
    DEFAULT_MAX_FETCH_PER_TYPE = 5000

    # Fetch defaults
    DEFAULT_FROM_TIME = "1 minute ago"
    DEFAULT_FETCH_COMMAND_FROM_TIME = "1 day ago"

    # Max date range for API (31 days)
    MAX_DATE_RANGE_DAYS = 31


class TableHeaders:
    """Human-readable table header mappings (API key → display name)."""

    INCIDENT: dict[str, str] = {
        "incidentID": "Incident ID",
        "incidentType": "Incident Type",
        "subIncidentType": "Sub Incident Type",
        "brand": "Brand",
        "url": "URL",
        "status": "Status",
        "statusCode": "Status Code",
        "threatType": "Threat Type",
        "detectionDate": "Detection Date",
        "createdOn": "Created On",
        "closedOn": "Closed On",
        "detectedBy": "Detected By",
    }

    CREATE_INCIDENT: dict[str, str] = {
        "reportedIncidentId": "Reported Incident ID",
        "statusCode": "Status Code",
        "statusDescription": "Status Description",
        "caseType": "Case Type",
        "caseTypeDescription": "Case Type Description",
    }


class ApiCodes:
    """Centralized API code mappings used across multiple commands."""

    # API error codes
    NO_DATA_FOUND = "iZOO2011"

    # Mapping of incident type display names to API integer codes
    EVENT_TYPE: dict[str, int] = {
        "brand abuse": 1,
        "phishing": 2,
        "malware": 3,
        "pharming": 4,
        "smishing": 5,
        "vishing": 6,
        "mobile apps": 7,
        "social media": 8,
        "other": 9,
        "email": 23,
        "executive": 56,
    }

    # Mapping of threat type display names to API integer codes
    THREAT_TYPE: dict[str, int] = {
        "low threat": 10,
        "moderate threat": 11,
        "substantial threat": 12,
        "high threat": 13,
        "critical threat": 14,
        "redirect to whitelist": 48,
    }

    # Mapping of case type display names to API integer codes
    CASE_TYPE: dict[str, int] = {
        "incident": 6,
        "brand abuse monitoring": 2,
        "domain monitoring": 1,
        "social media monitoring": 4,
        "mobile app monitoring": 3,
        "executive monitoring": 5,
    }


def date_to_unix_timestamp(date_input: str) -> str:
    """Parse a date string and return a Unix timestamp string for the iZOOlogic API.

    Args:
        date_input: Date string to parse (e.g., '3 days ago', '2024-01-01T00:00:00Z').

    Returns:
        Unix timestamp as a string.
    """
    parsed_dt = parse_date(date_input)
    timestamp = str(int(parsed_dt.timestamp()))
    demisto.debug(f"[Date Helper] Input: '{date_input}' -> Unix timestamp: '{timestamp}'")
    return timestamp


def get_current_unix_timestamp() -> str:
    """Return the current UTC time as a Unix timestamp string.

    Returns:
        Current Unix timestamp as a string.
    """
    timestamp = str(int(datetime.now(UTC).timestamp()))
    demisto.debug(f"[Date Helper] Current UTC Unix timestamp: '{timestamp}'")
    return timestamp


def snap_to_day_boundary_utc(unix_timestamp: str, boundary: str = "start") -> str:
    """Snap a Unix timestamp to a UTC day boundary.

    The iZOOlogic API uses day-level filtering with the following rules:

    * ``fromdate`` is **rounded up** to the next UTC midnight (unless already
      at midnight).  The resulting day is **inclusive**.
    * ``todate`` is **floored** to its UTC midnight.  The resulting day is
      **inclusive**.
    * The effective range is ``[ceil(fromdate)_day, floor(todate)_day]``.

    Because ``fromdate`` rounds *up*, a non-midnight value skips the current
    day entirely.  Snapping to ``"start"`` (midnight) prevents this.

    For same-day queries where ``fromdate == todate`` (both at midnight), the
    API rejects the request.  Snapping ``todate`` to ``"end"`` (23:59:59)
    keeps it on the same day after flooring, producing ``[Day, Day]``.

    Args:
        unix_timestamp: Unix timestamp as a string.
        boundary: Either ``"start"`` (00:00:00) or ``"end"`` (23:59:59).

    Returns:
        Unix timestamp string snapped to the requested boundary of the same UTC day.
    """
    dt = datetime.fromtimestamp(int(unix_timestamp), tz=UTC)
    if boundary == "end":
        snapped = dt.replace(hour=23, minute=59, second=59, microsecond=0)
    else:
        snapped = dt.replace(hour=0, minute=0, second=0, microsecond=0)
    result = str(int(snapped.timestamp()))
    demisto.debug(f"[DayBoundary:{boundary}] {unix_timestamp} -> {result}")
    return result


def parse_date(date_string: str) -> datetime:
    """Parse a date string into a datetime object.

    Args:
        date_string: Date string to parse (e.g., '3 days ago', '2024-01-01T00:00:00Z').

    Returns:
        Parsed datetime object in UTC.

    Raises:
        DemistoException: If the date string cannot be parsed.
    """
    demisto.debug(f"[Date Helper] Attempting to parse date string: '{date_string}'")
    parsed_datetime = arg_to_datetime(arg=date_string, is_utc=True)

    if not parsed_datetime:
        raise DemistoException(
            f"Failed to parse date string: '{date_string}'. "
            "Please provide a valid date in ISO 8601 format (e.g., '2024-01-01T00:00:00Z') "
            "or a relative time expression (e.g., '3 days ago')."
        )

    demisto.debug(f"[Date Helper] Final parsed date: {parsed_datetime.isoformat()}")
    return parsed_datetime


def enrich_events(events: list[dict]) -> None:
    """Enrich events with the _time and source_log_type fields for XSIAM ingestion.

    Converts the ``createdOn`` Unix timestamp to ISO 8601 format and sets it as ``_time``.
    If ``createdOn`` is missing or cannot be parsed, ``_time`` falls back to the current
    UTC time so that every event is guaranteed to have a ``_time`` value.
    Also sets ``source_log_type`` to the event type for each event.

    Args:
        events: List of event dictionaries from the API. Modified in place.
    """
    time_format = "%Y-%m-%dT%H:%M:%SZ"
    for event in events:
        created_on = event.get("createdOn", "")
        event_time = datetime.now(tz=UTC)
        if created_on:
            try:
                event_time = datetime.fromtimestamp(int(created_on), tz=UTC)
            except (ValueError, TypeError, OSError):
                demisto.debug(f"[Time] Failed to parse createdOn: {created_on}. Falling back to current time.")

        event["_time"] = event_time.strftime(time_format)
        event["source_log_type"] = event.get("incidentType", "Unknown")


def create_events(events: list[dict]) -> None:
    """Send already-normalized events to XSIAM.

    Callers must normalize events with ``enrich_events`` beforehand so that
    each event has the ``_time`` and ``source_log_type`` fields set.

    Args:
        events: List of normalized event dictionaries.
    """
    demisto.debug(f"[Create Events] Sending {len(events)} XSIAM events.")
    send_events_to_xsiam(
        events=events,
        vendor=VENDOR,
        product=PRODUCT,
    )


def filter_by_ids(raw_events: list[dict], ids_to_skip: list[str]) -> list[dict]:
    """Filter out events whose IDs are in the given skip set.

    Args:
        raw_events: List of event dictionaries from the API.
        ids_to_skip: List of event IDs to filter out.

    Returns:
        List of events not in the skip set.
    """
    if not raw_events or not ids_to_skip:
        return raw_events

    skip_set = set(ids_to_skip)
    filtered = [inc for inc in raw_events if inc.get("incidentID") not in skip_set]

    skipped = len(raw_events) - len(filtered)
    if skipped > 0:
        demisto.debug(f"[Filter] Skipped {skipped} events by ID. {len(filtered)} remain.")

    return filtered


def _validate_api_response(response: dict) -> dict:
    """Validate the API response and return the 'result' object.

    Handles known non-error codes like 'no data found' gracefully.

    Args:
        response: The full API response dictionary.

    Returns:
        The 'result' object from the response, or empty dict if no data found.

    Raises:
        DemistoException: If the API returned a real error.
    """
    if not response.get("success", True):
        error_code = response.get("errorCode", "")
        message = response.get("message", "Unknown error")

        if error_code == ApiCodes.NO_DATA_FOUND:
            demisto.debug(f"[API] No data found for the given time range (errorCode: {error_code})")
            return {}

        raise DemistoException(f"API error: {message} (errorCode: {error_code})")

    result = response.get("result", {})
    demisto.debug(
        f"[API Response] currentPage={result.get('currentPage')}, "
        f"totalRecords={result.get('totalRecords')}, "
        f"message={response.get('message')}, "
        f"errorCode={response.get('errorCode')}"
    )
    return result


def parse_integration_params(params: dict[str, Any]) -> dict[str, Any]:
    """Parse and validate integration configuration parameters.

    Args:
        params: Raw parameters from demisto.params().

    Returns:
        Validated configuration dictionary.

    Raises:
        DemistoException: If required parameters are missing or invalid.
    """
    base_url = params.get("url", "").rstrip("/")
    if not base_url:
        raise DemistoException("Server URL is required.")

    api_key_param = params.get("api_key", {})
    api_key = api_key_param.get("password", "") if isinstance(api_key_param, dict) else api_key_param
    if not api_key:
        raise DemistoException("API Key is required.")

    secret_key_param = params.get("secret_key", {})
    secret_key = secret_key_param.get("password", "") if isinstance(secret_key_param, dict) else secret_key_param
    if not secret_key:
        raise DemistoException("Secret Key is required.")

    verify_certificate = not argToBoolean(params.get("insecure", False))
    proxy = argToBoolean(params.get("proxy", False))
    is_fetch_events = argToBoolean(params.get("isFetchEvents", False))

    # Parse and validate event types filter — default to all types if none specified
    event_types_filter = argToList(params.get("events_types_filter"))
    event_type_codes = resolve_type_codes(event_types_filter) if event_types_filter else list(ApiCodes.EVENT_TYPE.values())

    max_fetch = int(params.get("max_fetch", Config.DEFAULT_MAX_FETCH_PER_TYPE))
    if max_fetch <= 0:
        raise DemistoException(f"Invalid max_fetch value: {params.get('max_fetch')}. Must be a positive integer.")

    demisto.debug(f"[Config] URL: {base_url}")

    return {
        "base_url": base_url,
        "api_key": api_key,
        "secret_key": secret_key,
        "verify": verify_certificate,
        "proxy": proxy,
        "event_type_codes": event_type_codes,
        "max_fetch": max_fetch,
        "is_fetch_events": is_fetch_events,
    }


def validate_date_range(from_date: str, to_date: str) -> None:
    """Validate that the date range is valid and does not exceed the API maximum of 31 days.

    Checks:
    1. ``to_date`` must not be earlier than ``from_date`` (different-day inversion).
    2. The span must not exceed ``Config.MAX_DATE_RANGE_DAYS`` (31 days).

    Note: same-day cases where ``to_date == from_date`` after snapping to midnight
    are handled separately by callers (e.g. ``get_events_command``).

    Args:
        from_date: Start date as Unix timestamp string.
        to_date: End date as Unix timestamp string.

    Raises:
        DemistoException: If the date range is inverted or exceeds 31 days.
    """
    from_dt = datetime.fromtimestamp(int(from_date), tz=UTC)
    to_dt = datetime.fromtimestamp(int(to_date), tz=UTC)

    if to_dt.date() < from_dt.date():
        raise DemistoException(
            f"'end_time' ({to_dt.isoformat()}) is before 'start_time' ({from_dt.isoformat()}). "
            "Please provide a valid date range where end_time >= start_time."
        )

    if (to_dt - from_dt).days > Config.MAX_DATE_RANGE_DAYS:
        raise DemistoException(
            f"Date range exceeds the maximum of {Config.MAX_DATE_RANGE_DAYS} days. "
            f"From: {from_dt.isoformat()}, To: {to_dt.isoformat()}."
        )


def resolve_type_codes(type_names: list[str]) -> list[int]:
    """Resolve event type display names to API codes.

    Args:
        type_names: List of event type display names (e.g., ['phishing', 'malware']).

    Returns:
        List of API integer codes.

    Raises:
        DemistoException: If any type name is invalid.
    """
    codes: list[int] = []
    for name in type_names:
        code = ApiCodes.EVENT_TYPE.get(name.lower().strip())
        if code is None:
            raise DemistoException(f"Invalid event type: '{name}'. Valid types: {list(ApiCodes.EVENT_TYPE.keys())}")
        codes.append(code)
    return codes


# endregion

# region Auth Handler
# =================================
# Auth Handler
# =================================


class IZOOlogicAuthHandler(AuthHandler):
    """Custom authentication handler for iZOOlogic two-step token auth.

    Flow:
    1. POST to /api/Token/Authenticate with apikey + secretkey → get accesstoken
    2. Set Authorization: Bearer <accesstoken> on each request
    3. On 401, re-authenticate and retry
    """

    name = "izoologic_token"

    def __init__(self, api_key: str, secret_key: str):
        self._api_key = api_key
        self._secret_key = secret_key
        self._token: str | None = None
        self._authenticating: bool = False
        self._auth_lock = threading.Lock()

    async def on_request(self, client: ContentClient, request: Any) -> None:
        """Add Bearer token to each request, authenticating if needed."""
        if self._authenticating:
            return  # Skip auth for the auth request itself to prevent recursion
        if not self._token:
            await self._authenticate(client)
        request.headers["Authorization"] = f"Bearer {self._token}"

    async def on_auth_failure(self, client: ContentClient, response: Any) -> bool:
        """Re-authenticate on 401 and retry the request."""
        demisto.debug("[Auth] Authentication failed (401). Re-authenticating...")
        self._token = None  # Clear expired token so _authenticate doesn't skip
        await self._authenticate(client)
        return True  # Retry the request with new token

    async def _authenticate(self, client: ContentClient) -> None:
        """Authenticate with the iZOOlogic API and store the token.

        Uses a threading.Lock to ensure thread safety when concurrent fetches
        run via the ThreadPoolExecutor in fetch_events_command. The double-check
        on self._token prevents redundant auth calls when multiple threads queue
        up on the lock.
        """
        with self._auth_lock:
            if self._token:
                demisto.debug("[Auth] Token already obtained by another thread, skipping.")
                return

            demisto.debug("[Auth] Authenticating with iZOOlogic API...")
            self._authenticating = True

            try:
                body = {
                    "apikey": self._api_key,
                    "secretkey": self._secret_key,
                }

                raw_response = await client._request(
                    method="POST",
                    url_suffix=ApiPaths.AUTHENTICATE,
                    json_data=body,
                )
                response: dict = raw_response.json()

                if not response.get("success", True):
                    error_code = response.get("errorCode", "")
                    message = response.get("message", "Unknown error")
                    raise DemistoException(
                        f"Authentication failed: {message} (errorCode: {error_code}). "
                        "Verify your API Key and Secret Key are correct."
                    )

                result = response.get("result", {})
                token = result.get("accessToken") if isinstance(result, dict) else None
                if not token:
                    raise DemistoException(
                        "Authentication failed: No token received from the API. "
                        "Verify your API Key and Secret Key are correct."
                    )

                self._token = token
                demisto.debug("[Auth] Successfully authenticated")
            finally:
                self._authenticating = False


# endregion

# region Client
# =================================
# Client
# =================================


class Client(ContentClient):
    """iZOOlogic API client.

    Extends ContentClient with iZOOlogic-specific API methods.
    Authentication is handled automatically by IZOOlogicAuthHandler.
    """

    def __init__(
        self,
        base_url: str,
        api_key: str,
        secret_key: str,
        verify: bool,
        proxy: bool,
    ):
        """Initialize the iZOOlogic client.

        Args:
            base_url: iZOOlogic API server URL.
            api_key: API key for authentication.
            secret_key: Secret key for authentication.
            verify: Whether to verify SSL certificates.
            proxy: Whether to use proxy settings.
        """
        auth_handler = IZOOlogicAuthHandler(api_key, secret_key)
        super().__init__(
            base_url=base_url,
            verify=verify,
            proxy=proxy,
            auth_handler=auth_handler,
            client_name="iZOOlogic",
        )

    def fetch_events_page(
        self,
        from_date: str,
        to_date: str,
        event_type: int | None = None,
        page_token: str | None = None,
        threat_type: int | None = None,
        brand_code: str | None = None,
        executive_name: str | None = None,
        client_ref_id: str | None = None,
        client_code: str | None = None,
    ) -> dict[str, Any]:
        """Fetch a single page of events from the iZOOlogic API.

        The API uses day-level filtering: ``fromdate`` is rounded up to the
        next UTC midnight (inclusive), ``todate`` is floored to its UTC
        midnight (inclusive).  The effective range is
        ``[ceil(fromdate)_day, floor(todate)_day]``.

        Args:
            from_date: Start date as Unix timestamp string (must be midnight UTC
                to avoid the round-up skipping the intended day).
            to_date: End date as Unix timestamp string (floored to its day by the API).
            event_type: Optional event type code to filter by.
            page_token: Pagination token for retrieving the next page.
            threat_type: Optional threat level code to filter by.
            brand_code: Optional brand identifier to filter by.
            executive_name: Optional executive name to filter by.
            client_ref_id: Optional client reference ID for specific incident lookup.
            client_code: Optional client identifier to filter by.

        Returns:
            The 'result' object from the API response containing events and pagination info.
        """
        body: dict[str, Any] = assign_params(
            fromdate=from_date,
            todate=to_date,
            incidenttype=event_type,
            token=page_token,
            threattype=threat_type,
            brandcode=brand_code,
            executivename=executive_name,
            clientrefid=client_ref_id,
            clientcode=client_code,
        )

        demisto.debug(f"[API Fetch] Fetching events | Params: {body}")

        response = self._http_request(
            method="POST",
            url_suffix=ApiPaths.FETCH_EVENTS,
            json_data=body,
        )

        return _validate_api_response(response)

    def report_new_incident(
        self,
        incident_url: str,
        incident_type: int,
        brand_code: str,
        threat_type: int | None = None,
        case_type: int | None = None,
        comment: str | None = None,
        executive_name: str | None = None,
        client_code: str | None = None,
    ) -> dict[str, Any]:
        """Report a new security incident to the iZOOlogic API.

        Args:
            incident_url: URL, email, or target of the security incident (max 1000 chars).
            incident_type: Type of incident (API integer code).
            brand_code: Brand identifier associated with the incident.
            threat_type: Optional threat level code. Defaults to moderate threat if not specified.
            case_type: Optional case type code. Defaults to incident (6) if not specified.
            comment: Optional comments about the incident (max 2500 chars).
            executive_name: Optional executive name for executive-related incidents (max 2500 chars).
            client_code: Optional client identifier for validation and access control.

        Returns:
            The full API response dictionary.
        """
        body: dict[str, Any] = assign_params(
            incidenturl=incident_url,
            incidenttype=incident_type,
            brandcode=brand_code,
            threattype=threat_type,
            casetype=case_type,
            comment=comment,
            executivename=executive_name,
            clientcode=client_code,
        )

        log_body = {**body, "executivename": "***"} if "executivename" in body else body
        demisto.debug(f"[API ReportNewIncident] Creating incident | Params: {log_body}")

        response = self._http_request(
            method="POST",
            url_suffix=ApiPaths.REPORT_NEW_INCIDENT,
            json_data=body,
        )

        return response


# endregion

# region Command implementations
# =================================
# Command implementations
# =================================


def test_module(client: Client, event_type_codes: list[int], is_fetch_events: bool) -> str:
    """Test API connectivity, the event collector, and command functionality.

    Always validates the command path (the incident-search flow) so the
    integration's commands are verified regardless of configuration. The event
    collector is validated only when ``is_fetch_events`` is enabled — fetching a
    single event (``max_results=1``) per configured type via the same
    ``_fetch_all_pages`` code path as production. An empty result (no events)
    still proves connectivity. This catches a type the configured credentials
    cannot access, not just generic connectivity.

    Args:
        client: The iZOOlogic client.
        event_type_codes: Configured event type codes to validate.
        is_fetch_events: Whether event collection is enabled.

    Returns:
        'ok' if test passed, otherwise raises an exception.
    """
    demisto.debug(f"[Test Module] Starting (is_fetch_events={is_fetch_events})...")
    try:
        from_date = snap_to_day_boundary_utc(get_current_unix_timestamp(), "start")
        to_date = get_current_unix_timestamp()

        # Always validate command functionality by running the incident-search command,
        # using a narrow window (DEFAULT_FROM_TIME) to keep the test lightweight.
        demisto.debug("[Test Module] Validating command functionality (incident search)...")
        search_incidents_command(client, {"from_date": Config.DEFAULT_FROM_TIME})

        # Validate the collector only when event collection is enabled.
        if is_fetch_events:
            demisto.debug(f"[Test Module] Validating collector for {len(event_type_codes)} configured type(s)...")
            for type_code in event_type_codes:
                _fetch_all_pages(client, from_date=from_date, to_date=to_date, event_type=type_code, max_results=1)

        demisto.debug("[Test Module] Success")
        return "ok"

    except Exception as error:
        error_msg = str(error)
        demisto.debug(f"[Test Module] Failed: {error_msg}")
        if "401" in error_msg or "403" in error_msg or "unauthorized" in error_msg.lower():
            return "Authorization Error: Verify your API Key and Secret Key."
        raise


def _fetch_all_pages(
    client: Client,
    from_date: str,
    to_date: str,
    event_type: int | None = None,
    threat_type: int | None = None,
    brand_code: str | None = None,
    executive_name: str | None = None,
    client_ref_id: str | None = None,
    client_code: str | None = None,
    max_results: int | None = None,
    from_ts: str | None = None,
    to_ts: str | None = None,
    last_ids: list[str] | None = None,
) -> list[dict]:
    """Fetch events for a single type: page, filter, dedup, sort, and cap.

    The API filters by whole UTC days and returns events newest-first. This
    function performs the full shared pipeline so all commands behave the same:
      1. Page through the API, trimming each page to the precise ``[from_ts, to_ts]``
         range (the API only filters by day).
      2. Stop paging early once an event strictly older than ``from_ts`` appears
         (everything later is older too); events exactly at ``from_ts`` are kept.
      3. Drop events whose IDs are in ``last_ids`` (already consumed at the
         watermark) — only when ``last_ids`` is provided.
      4. Sort ascending by ``createdOn`` and keep the oldest ``max_results``, so
         callers always get the *earliest* events in the range (never the latest).

    Used by all commands: test_module, get_events_command, fetch_events_command,
    and search_incidents_command.

    Args:
        client: The iZOOlogic client.
        from_date: Start date as Unix timestamp string (must be midnight UTC).
        to_date: End date as Unix timestamp string.
        event_type: Optional event type code.
        threat_type: Optional threat level code to filter by.
        brand_code: Optional brand identifier to filter by.
        executive_name: Optional executive name to filter by.
        client_ref_id: Optional client reference ID for specific event lookup.
        client_code: Optional client identifier to filter by.
        max_results: Optional cap on the number of (oldest) events to return.
        from_ts: Optional precise lower bound (inclusive) as a Unix timestamp.
        to_ts: Optional precise upper bound (inclusive) as a Unix timestamp.
        last_ids: Optional IDs already consumed at ``from_ts`` to deduplicate.

    Returns:
        In-range events, sorted ascending by createdOn, capped to max_results.
    """
    all_events: list[dict] = []
    page_token: str | None = None
    page_count = 0
    from_threshold = int(from_ts) if from_ts else None
    to_threshold = int(to_ts) if to_ts else None
    should_continue = True

    while should_continue:
        result_obj = client.fetch_events_page(
            from_date=from_date,
            to_date=to_date,
            event_type=event_type,
            page_token=page_token,
            threat_type=threat_type,
            brand_code=brand_code,
            executive_name=executive_name,
            client_ref_id=client_ref_id,
            client_code=client_code,
        )

        page_events = result_obj.get("incidents") or []
        if not page_events:
            break

        # Trim to the precise range (the API only filters by day).
        in_range = [event for event in page_events if _is_in_range(event, from_threshold, to_threshold)]
        all_events.extend(in_range)
        page_count += 1
        demisto.debug(
            f"[FetchAll] Type {event_type or 'all'} | Page {page_count}: "
            f"+{len(in_range)} in-range events (total: {len(all_events)})"
        )

        # Stop once an event is older than from_ts (newest-first). max_results is
        # applied after sorting, not here, so we get the oldest events.
        reached_watermark = from_threshold is not None and any(
            int(event.get("createdOn", "0")) < from_threshold for event in page_events
        )
        if reached_watermark:
            demisto.debug(f"[FetchAll] Type {event_type or 'all'} | Reached lower-bound watermark, stopping pagination.")

        page_token = result_obj.get("nextPage")
        should_continue = bool(page_token) and not reached_watermark

    if last_ids:  # drop IDs already consumed at the watermark (stateful callers)
        all_events = filter_by_ids(all_events, last_ids)

    all_events.sort(key=lambda event: int(event.get("createdOn", "0")))  # oldest first
    if max_results is not None:
        all_events = all_events[:max_results]

    demisto.debug(f"[FetchAll] Type {event_type or 'all'} | Done: {len(all_events)} events " f"across {page_count} pages")
    return all_events


def _is_in_range(event: dict, from_threshold: int | None, to_threshold: int | None) -> bool:
    created_on = int(event.get("createdOn", "0"))
    if from_threshold is not None and created_on < from_threshold:
        return False
    return not (to_threshold is not None and created_on > to_threshold)


def _resolve_fetch_window(from_input: str, to_input: str | None) -> tuple[str, str, str]:
    """Resolve a fetch time window shared by get-events and incident-fetch.

    Converts the inputs to Unix timestamps, snaps ``from_date`` to the start of
    its UTC day (the API filters by day), defaults ``to_date`` to now, validates
    the range, and snaps ``to_date`` to end-of-day when both land on the same day
    (otherwise the API rejects an equal from/to range).

    Args:
        from_input: Start time (ISO 8601 or relative).
        to_input: End time (ISO 8601 or relative), or None for now.

    Returns:
        Tuple of (from_ts, from_date, to_date) as Unix timestamp strings, where
        from_ts is the precise requested start (before the day snap).
    """
    from_ts = date_to_unix_timestamp(from_input)
    from_date = snap_to_day_boundary_utc(from_ts, "start")
    to_date = date_to_unix_timestamp(to_input) if to_input else get_current_unix_timestamp()

    validate_date_range(from_date, to_date)

    # When from_date and to_date land on the same UTC day, snap to_date to
    # end-of-day so the API accepts the [Day, Day] range.
    if int(to_date) == int(from_date):
        to_date = snap_to_day_boundary_utc(to_date, "end")

    return from_ts, from_date, to_date


def get_events_command(
    client: Client,
    args: dict,
    default_type_codes: list[int],
) -> CommandResults:
    """Manual command to get events for debugging/development.

    Args:
        client: The iZOOlogic client.
        args: Command arguments.
        default_type_codes: Default event type codes from integration config.

    Returns:
        CommandResults with the retrieved events.
    """
    demisto.debug("[Command] izoologic-get-events triggered")

    should_push_events = resolve_should_push_events(args)

    limit = arg_to_number(args.get("limit", Config.DEFAULT_LIMIT))
    if not limit or limit <= 0:
        raise DemistoException(f"Invalid limit value: {args.get('limit')}. Must be a positive integer.")
    limit = int(limit)

    start_time_input = args.get("start_time", Config.DEFAULT_FROM_TIME)
    end_time_input = args.get("end_time")

    # Use event_type from command args if provided, otherwise fall back to config
    event_type_arg = argToList(args.get("event_type"))
    type_codes = resolve_type_codes(event_type_arg) if event_type_arg else default_type_codes

    from_ts, from_date, to_date = _resolve_fetch_window(start_time_input, end_time_input)

    demisto.debug(
        f"[Command Params] From: {from_date} (requested: {from_ts}), To: {to_date}, Limit: {limit}, Types: {type_codes}"
    )

    all_events: list[dict] = []
    for type_code in type_codes:
        type_events = _fetch_all_pages(
            client,
            from_date=from_date,
            to_date=to_date,
            event_type=type_code,
            from_ts=from_ts,
            to_ts=to_date,
            max_results=limit,
        )
        all_events.extend(type_events)

    demisto.debug(f"[Command Result] Total events retrieved: {len(all_events)} (limit per type: {limit})")

    # Normalize events (adds _time and source_log_type) so the command outputs
    # the same parsed events that are ingested into XSIAM.
    enrich_events(all_events)

    if should_push_events and all_events:
        create_events(list(all_events))

    readable_output = tableToMarkdown(
        f"{INTEGRATION_NAME} Events",
        all_events,
        headers=list(TableHeaders.INCIDENT.keys()),
        headerTransform=TableHeaders.INCIDENT.get,
        removeNull=True,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="iZOOlogic.Incident",
        outputs_key_field="incidentID",
        outputs=all_events,
    )


def _compute_new_state(
    consumed: list[dict],
    type_key: str,
) -> dict:
    """Compute the new last_run state from consumed events.

    Sets ``last_created_on`` to the maximum ``createdOn`` among the consumed
    events and collects all event IDs at that timestamp into ``last_ids``
    for deduplication on the next run.

    Args:
        consumed: Events to consume (already sorted ascending and sliced to max_fetch).
        type_key: Type key string used for debug logging.

    Returns:
        Updated state dict with ``last_created_on`` and ``last_ids``.
    """
    # Normalize createdOn to int to stay consistent with the int-based pipeline.
    max_created_on = int(consumed[-1].get("createdOn", "0"))
    new_last_ids = [inc.get("incidentID", "") for inc in consumed if int(inc.get("createdOn", "0")) == max_created_on]

    demisto.debug(
        f"[Fetch] Type {type_key}: {len(consumed)} events consumed. "
        f"New last_created_on={max_created_on}, dedup IDs={new_last_ids}."
    )

    return {
        "last_created_on": max_created_on,
        "last_ids": new_last_ids,
    }


def _fetch_for_type(
    client: Client,
    type_code: int,
    type_state: dict,
    max_fetch_per_type: int,
) -> tuple[str, list[dict], dict]:
    """Fetch events for a single type within a ≤31-day window.

    Uses ``Config.DEFAULT_FROM_TIME`` as the starting point on first fetch.
    The date range must not exceed 31 days (enforced by ``validate_date_range``).

    Args:
        client: The iZOOlogic client.
        type_code: Event type code.
        type_state: Per-type state from last_run.
        max_fetch_per_type: Maximum events to return for this type.

    Returns:
        Tuple of (type_key, consumed_events, updated_state).
    """
    type_key = str(type_code)
    last_created_on: str | None = type_state.get("last_created_on")
    last_ids: list[str] = type_state.get("last_ids", [])

    effective_first_fetch = date_to_unix_timestamp(Config.DEFAULT_FROM_TIME)
    from_date = snap_to_day_boundary_utc(
        last_created_on if last_created_on else effective_first_fetch,
        "start",
    )
    to_date = get_current_unix_timestamp()

    demisto.debug(
        f"[Fetch] Type {type_key}: from_date={from_date}, to_date={to_date}, "
        f"last_created_on={last_created_on}, last_ids({len(last_ids)})={last_ids}"
    )

    from_ts = int(from_date)
    to_ts = int(to_date)

    if from_ts >= to_ts:
        demisto.debug(f"[Fetch] Type {type_key}: from_date >= to_date. Skipping.")
        return type_key, [], type_state

    validate_date_range(from_date, to_date)

    # _fetch_all_pages filters to >= last_created_on, dedups last_ids, sorts
    # ascending, and caps to the oldest max_fetch_per_type events.
    consumed = _fetch_all_pages(
        client,
        from_date,
        to_date,
        type_code,
        from_ts=last_created_on,
        last_ids=last_ids,
        max_results=max_fetch_per_type,
    )

    if not consumed:
        demisto.debug(f"[Fetch] Type {type_key}: No new events.")
        return type_key, [], {"last_created_on": int(to_date), "last_ids": []}

    demisto.debug(f"[Fetch] Type {type_key}: Consuming {len(consumed)} events")
    updated_state = _compute_new_state(consumed, type_key)
    return type_key, consumed, updated_state


def fetch_events_command(
    client: Client,
    max_fetch_per_type: int,
    event_type_codes: list[int],
) -> None:
    """Scheduled command to fetch events from iZOOlogic and send to XSIAM.

    Fetches all event types concurrently using a ThreadPoolExecutor (the API
    calls are blocking I/O, so threads parallelize them effectively).
    Each type maintains its own last_created_on and last_ids in last_run.

    For each type, ``_fetch_for_type`` delegates to ``_fetch_all_pages`` which
    pages the API, filters to the precise time window, stops early at the
    last_run watermark, dedups by last_ids, sorts ascending by createdOn, and
    caps to max_fetch_per_type. This command then enriches and pushes the
    consumed events and advances each type's last_run state.

    Args:
        client: The iZOOlogic client.
        max_fetch_per_type: Maximum number of events to fetch per type per run.
        event_type_codes: Event type codes from integration config.
    """
    last_run = demisto.getLastRun()
    demisto.debug(f"[Fetch Events] Starting with last_run keys: {list(last_run.keys())}")

    # Launch concurrent fetches for all types. The client makes blocking HTTP
    # calls, so a thread pool parallelizes the per-type fetches effectively.
    all_events: list[dict] = []
    updated_last_run: dict = dict(last_run)

    with ThreadPoolExecutor(max_workers=len(event_type_codes) or 1) as executor:
        future_to_type = {
            executor.submit(
                _fetch_for_type,
                client,
                type_code,
                last_run.get(str(type_code), {}),  # type: ignore[arg-type]
                max_fetch_per_type,
            ): type_code
            for type_code in event_type_codes
        }

        for future in as_completed(future_to_type):
            try:
                type_key, consumed_events, updated_state = future.result()
            except Exception as error:
                # Isolate per-type failures so other types still succeed.
                demisto.error(
                    f"[Fetch Events] Error fetching type {future_to_type[future]}: {error!s}\n" f"{traceback.format_exc()}"
                )
                continue

            all_events.extend(consumed_events)
            updated_last_run[type_key] = updated_state

    if all_events:
        enrich_events(all_events)
        create_events(all_events)

    demisto.setLastRun(updated_last_run)
    demisto.debug(f"[Fetch Events] Done. Total events: {len(all_events)}. Last run updated.")


def _resolve_code_by_name(raw_value: str, code_map: dict[str, int], field_name: str) -> int:
    """Resolve a display name to an API integer code.

    Args:
        raw_value: The user-provided display name.
        code_map: Mapping of display names to integer codes.
        field_name: Argument name used in error messages.

    Returns:
        The resolved integer code.

    Raises:
        DemistoException: If the name is not found in the code map.
    """
    normalized = raw_value.lower().strip()
    code = code_map.get(normalized)
    if code is None:
        raise DemistoException(f"Invalid '{field_name}': '{raw_value}'. " f"Valid values: {list(code_map.keys())}.")
    return code


def _validate_incident_creation_args(args: dict[str, Any]) -> dict[str, Any]:
    """Validate and parse arguments for the izoologic-incident-create command.

    Args:
        args: Raw command arguments from demisto.args().

    Returns:
        Validated and parsed arguments dictionary ready for the API call.

    Raises:
        DemistoException: If required arguments are missing or invalid.
    """
    # Required: incident_url
    incident_url = args.get("incident_url", "")
    if not incident_url:
        raise DemistoException("'incident_url' is a required argument.")

    # Required: incident_type (name only, mapped to integer code)
    incident_type_raw = args.get("incident_type", "")
    if not incident_type_raw:
        raise DemistoException("'incident_type' is a required argument.")
    incident_type = _resolve_code_by_name(str(incident_type_raw), ApiCodes.EVENT_TYPE, "incident_type")

    # Required: brand_code
    brand_code = args.get("brand_code", "")
    if not brand_code:
        raise DemistoException("'brand_code' is a required argument.")

    # Optional: threat_type (name only, mapped to integer code)
    threat_type: int | None = None
    threat_type_raw = args.get("threat_type")
    if threat_type_raw:
        threat_type = _resolve_code_by_name(str(threat_type_raw), ApiCodes.THREAT_TYPE, "threat_type")

    # Optional: case_type (name only, mapped to integer code)
    case_type: int | None = None
    case_type_raw = args.get("case_type")
    if case_type_raw:
        case_type = _resolve_code_by_name(str(case_type_raw), ApiCodes.CASE_TYPE, "case_type")

    # Optional: comment
    comment = args.get("comment")

    # Optional: executive_name
    executive_name = args.get("executive_name")

    # Optional: client_code
    client_code = args.get("client_code")

    return {
        "incident_url": incident_url,
        "incident_type": incident_type,
        "brand_code": brand_code,
        "threat_type": threat_type,
        "case_type": case_type,
        "comment": comment,
        "executive_name": executive_name,
        "client_code": client_code,
    }


def create_incident_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Create a new security incident in iZOOlogic.

    Args:
        client: The iZOOlogic client.
        args: Command arguments from demisto.args().

    Returns:
        CommandResults with the API response.
    """
    demisto.debug("[Command] izoologic-incident-create triggered")

    validated_args = _validate_incident_creation_args(args)

    response = client.report_new_incident(
        incident_url=validated_args["incident_url"],
        incident_type=validated_args["incident_type"],
        brand_code=validated_args["brand_code"],
        threat_type=validated_args["threat_type"],
        case_type=validated_args["case_type"],
        comment=validated_args["comment"],
        executive_name=validated_args["executive_name"],
        client_code=validated_args["client_code"],
    )

    if not response.get("success", False):
        error_code = response.get("errorCode", "")
        message = response.get("message", "Unknown error")
        raise DemistoException(f"Failed to create incident: {message} (errorCode: {error_code})")

    result = response.get("result", {})

    readable_output = tableToMarkdown(
        f"{INTEGRATION_NAME} - New Incident Created",
        result,
        headers=list(TableHeaders.CREATE_INCIDENT.keys()),
        headerTransform=TableHeaders.CREATE_INCIDENT.get,
        removeNull=True,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="iZOOlogic.Incident",
        outputs_key_field="reportedIncidentId",
        outputs=result,
        raw_response=response,
    )


def search_incidents_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Fetch incidents from iZOOlogic with advanced filtering.

    Retrieves incidents based on specified filters including date range, brand,
    incident type, threat type, and other criteria. Supports pagination for
    large result sets.

    Args:
        client: The iZOOlogic client.
        args: Command arguments from demisto.args().

    Returns:
        CommandResults with the retrieved incidents.
    """
    demisto.debug("[Command] izoologic-incident-fetch triggered")

    # Date range — default: 1 day ago to now
    from_date_input = args.get("from_date", Config.DEFAULT_FETCH_COMMAND_FROM_TIME)
    to_date_input = args.get("to_date")

    from_ts, from_date, to_date = _resolve_fetch_window(from_date_input, to_date_input)

    # Optional: incident_type (name only, mapped to integer code)
    incident_type: int | None = None
    incident_type_raw = args.get("incident_type")
    if incident_type_raw:
        incident_type = _resolve_code_by_name(str(incident_type_raw), ApiCodes.EVENT_TYPE, "incident_type")

    # Optional: threat_type (name only, mapped to integer code)
    threat_type: int | None = None
    threat_type_raw = args.get("threat_type")
    if threat_type_raw:
        threat_type = _resolve_code_by_name(str(threat_type_raw), ApiCodes.THREAT_TYPE, "threat_type")

    # Optional string filters
    brand_code = args.get("brand_code")
    executive_name = args.get("executive_name")
    client_ref_id = args.get("client_ref_id")
    client_code = args.get("client_code")

    demisto.debug(
        f"[Command Params] From: {from_date}, To: {to_date}, "
        f"IncidentType: {incident_type}, ThreatType: {threat_type}, "
        f"BrandCode: {brand_code}, ExecutiveName: {executive_name}, "
        f"ClientRefId: {client_ref_id}, ClientCode: {client_code}"
    )

    all_incidents = _fetch_all_pages(
        client,
        from_date=from_date,
        to_date=to_date,
        event_type=incident_type,
        threat_type=threat_type,
        brand_code=brand_code,
        executive_name=executive_name,
        client_ref_id=client_ref_id,
        client_code=client_code,
        from_ts=from_ts,
        to_ts=to_date,
    )

    demisto.debug(f"[Command Result] Total incidents retrieved: {len(all_incidents)}")

    readable_output = tableToMarkdown(
        f"{INTEGRATION_NAME} Incidents",
        all_incidents,
        headers=list(TableHeaders.INCIDENT.keys()),
        headerTransform=TableHeaders.INCIDENT.get,
        removeNull=True,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="iZOOlogic.Incident",
        outputs_key_field="incidentID",
        outputs=all_incidents,
        raw_response=all_incidents,
    )


# endregion

# region Command Map and Main
# =================================
# Command Map and Main
# =================================

COMMAND_MAP: dict[str, Any] = {
    "test-module": test_module,
    "izoologic-get-events": get_events_command,
    "fetch-events": fetch_events_command,
    "izoologic-incident-create": create_incident_command,
    "izoologic-incident-fetch": search_incidents_command,
}


def main() -> None:
    """Main entry point for iZOOlogic integration."""
    demisto.debug(f"{INTEGRATION_NAME} integration started")
    command = demisto.command()

    try:
        if command not in COMMAND_MAP:
            raise DemistoException(f"Command '{command}' is not implemented")

        params = demisto.params()
        args = demisto.args()
        config = parse_integration_params(params)

        client = Client(
            base_url=config["base_url"],
            api_key=config["api_key"],
            secret_key=config["secret_key"],
            verify=config["verify"],
            proxy=config["proxy"],
        )

        command_func = COMMAND_MAP[command]

        if command == "test-module":
            result = command_func(client, config["event_type_codes"], config["is_fetch_events"])
            return_results(result)
        elif command == "fetch-events":
            command_func(client, config["max_fetch"], config["event_type_codes"])
        elif command == "izoologic-get-events":
            result = command_func(client, args, config["event_type_codes"])
            return_results(result)
        elif command in ("izoologic-incident-create", "izoologic-incident-fetch"):
            result = command_func(client, args)
            return_results(result)

    except Exception as error:
        error_msg = f"Failed to execute {command}. Error: {error!s}"
        demisto.error(f"{error_msg}\n{traceback.format_exc()}")
        return_error(error_msg)

    demisto.debug(f"{INTEGRATION_NAME} integration finished")


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

# endregion