AkamaiProlexic

Collects DDoS detection critical events and general events from Akamai Prolexic Analytics for Cortex XSIAM.

Analytics & SIEM · Akamai Prolexic

Details

IDAkamaiProlexic
ProviderAkamai Technologies
CategoryAnalytics & SIEM
From Version8.4.0
Docker Imagedemisto/auth-utils:1.0.0.11671917
Supported ModulesXSIAM

README

Collects DDoS detection critical events and general events from Akamai Prolexic Analytics for Cortex XSIAM.
This integration was integrated and tested with version v2 of the Akamai Prolexic Analytics API.

Configure Akamai Prolexic in Cortex

Parameter Description Required
Server URL The Akamai API host (the value of the “host” field in your .edgerc file). Example: https://akab-h05tnam3wl42son7nktnlnnx-kbob3i3v.luna.akamaiapis.net True
Contract ID The policy domain name of the data center or proxy that the events belong to. True
Client Token The EdgeGrid client token, taken from the “client_token” field of your .edgerc file. True
Client Secret The EdgeGrid client secret, taken from the “client_secret” field of your .edgerc file. True
Access Token The EdgeGrid access token, taken from the “access_token” field of your .edgerc file. True
Account Switch Key The account switch key used to run operations against a managed account, for customers managing more than one account. The Identity and Access Management API provides a list of available account switch keys. False
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch events   False
Event types to fetch The Akamai Prolexic event sources to collect. Each selected source is fetched and deduplicated independently. True
First fetch time The point in time from which to start fetching events on the first run. Examples: “1 day”, “12 hours”. False
Maximum events per fetch The maximum number of events to fetch per source, per fetch. Maximum allowed: 10000. False

Commands

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

akamai-prolexic-get-events


Gets events from Akamai Prolexic. This command is used for developing and debugging and is to be used with caution, as it can create duplicate events in the dataset.

Base Command

akamai-prolexic-get-events

Input

Argument Name Description Required
limit The maximum number of events to retrieve per source. Default is 50. Optional
event_type A comma-separated list of event types to retrieve. If empty, uses the integration configuration. Possible values are: Critical Events, Events. Optional
start_time The lower-bound timestamp for events to retrieve. Supports ISO 8601 (e.g., “2026-04-20T10:00:00Z”) or relative time expressions (e.g., “3 days ago”). If omitted, the integration’s “First fetch time” value is used. Optional
end_time The upper-bound timestamp for events to retrieve. Supports ISO 8601 (e.g., “2026-04-20T18:00:00Z”) or relative time expressions (e.g., “1 hour ago”). If omitted, no upper bound is applied. Optional
should_push_events Whether to push the retrieved events to Cortex XSIAM. If false, the events are only displayed. Possible values are: true, false. Default is false. Required

Context Output

There is no context output for this command.

Command example

!akamai-prolexic-get-events limit=2 event_type="Critical Events" should_push_events=false

Context Example

{}

Human Readable Output

Akamai Prolexic Events

_time event_type source_log_type _ENTRY_STATUS id firstOccur recentOccur severity description
2026-04-20T10:00:00.000000Z Critical Events CRITICAL_EVENTS new ce-1 2026-04-20T10:00:00Z 2026-04-20T10:00:00Z high DDoS detected on policy A
2026-04-20T11:30:00.000000Z Critical Events CRITICAL_EVENTS updated ce-2 2026-04-20T11:30:00Z 2026-04-20T12:00:00Z critical Volumetric attack on policy B

Configuration parameters

  • url — Server URL (required)
  • contract_id — Contract ID (required)
  • client_token_creds — (required)
  • client_secret_creds — (required)
  • access_token_creds — (required)
  • account_switch_key — Account Switch Key
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchEvents — Fetch events
  • event_types_to_fetch — Event types to fetch (required)
  • first_fetch — First fetch time
  • max_events_per_fetch — Maximum events per fetch

Commands (1)

  • akamai-prolexic-get-events

    Gets events from Akamai Prolexic. This command is used for developing and debugging and is to be used with caution, as it can create duplicate events in the dataset.

"""Akamai Prolexic Event Collector for Cortex XSIAM.

Fetches DDoS-related events from the Akamai Prolexic Analytics API and forwards
them to Cortex XSIAM as a single ``akamai_prolexic_raw`` dataset
(vendor=``akamai``, product=``prolexic``).

Two event sources are supported:

* **Critical Events** — ``GET /prolexic-analytics/v2/critical-events/contract/{contract}``
* **Events**          — ``GET /prolexic-analytics/v2/events/contract/{contract}?extended=true``

Authentication is Akamai EdgeGrid HMAC-SHA-256, performed by the
``akamai-edgegrid`` library and attached to every HTTP request via the
``requests`` ``auth`` mechanism.
"""

import traceback
from collections.abc import Iterable
from datetime import UTC, datetime
from typing import Any

import demistomock as demisto  # noqa: F401
import urllib3
from akamai.edgegrid import EdgeGridAuth
from CommonServerPython import *  # noqa: F401

urllib3.disable_warnings()

# --------------------------------------------------------------------------- #
# Constants
# --------------------------------------------------------------------------- #

VENDOR = "akamai"
PRODUCT = "prolexic"

CRITICAL_EVENTS = "Critical Events"
EVENTS = "Events"

# Per-source configuration — exact endpoints/timestamp fields come from the
# Akamai Prolexic Analytics design document (CIAC-16080).
#
# ``revision_field`` names a secondary timestamp that changes when the record is
# updated in place. It is folded into the dedup key so that an updated record is
# treated as a distinct entry rather than a duplicate of the original.
SOURCE_CONFIG: dict[str, dict[str, str]] = {
    CRITICAL_EVENTS: {
        "endpoint_template": "/prolexic-analytics/v2/critical-events/contract/{contract}",
        "time_field": "firstOccur",
        "revision_field": "recentOccur",
        "last_run_key": "critical_events",
    },
    EVENTS: {
        "endpoint_template": "/prolexic-analytics/v2/events/contract/{contract}",
        "time_field": "eventStartTime",
        "last_run_key": "events",
    },
}

# Default to "now" so the first fetch does not back-fill historical events
# (matches the XSIAM Event-Collector convention used by OnePassword and others).
DEFAULT_FIRST_FETCH = "now"
DEFAULT_MAX_EVENTS_PER_FETCH = 1000
MAX_EVENTS_PER_FETCH_CEILING = 10000
DEFAULT_GET_EVENTS_LIMIT = 50
# Upper bound on how many dedup ids are carried in ``last_run``. Only ids sharing
# the high-water-mark timestamp are retained, but a source that emits a large
# burst at one timestamp could still grow the set without limit.
MAX_RETAINED_DEDUP_IDS = 5000
ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"


# --------------------------------------------------------------------------- #
# Client
# --------------------------------------------------------------------------- #


class Client(BaseClient):
    """Thin wrapper around ``BaseClient`` that issues EdgeGrid-signed requests
    to the Akamai Prolexic Analytics API.

    ``BaseClient`` is intentionally used (instead of ``ContentClient``) because
    the Akamai EdgeGrid library ships a ``requests.auth.AuthBase`` implementation
    that signs each call synchronously via ``self._session.auth`` — this is
    natively supported by ``BaseClient`` but is not directly compatible with
    the ``httpx``-based ``ContentClient``. This mirrors the pattern used by
    the existing ``Akamai_SIEM`` pack.
    """

    def __init__(
        self,
        base_url: str,
        verify: bool,
        proxy: bool,
        client_token: str,
        client_secret: str,
        access_token: str,
        account_switch_key: str | None = None,
    ) -> None:
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        # ``EdgeGridAuth`` is a ``requests.auth.AuthBase`` implementation that
        # signs the request with HMAC-SHA-256 immediately before it is sent.
        self._session.auth = EdgeGridAuth(
            client_token=client_token,
            client_secret=client_secret,
            access_token=access_token,
        )
        self._account_switch_key = account_switch_key or None

    def _build_params(self, extra: dict[str, Any] | None = None) -> dict[str, Any]:
        """Always merge in ``accountSwitchKey`` when configured."""
        params: dict[str, Any] = dict(extra or {})
        if self._account_switch_key:
            params["accountSwitchKey"] = self._account_switch_key
        return params

    def get_critical_events(self, contract_id: str) -> dict[str, Any]:
        """Calls ``GET /v2/critical-events/contract/{contract}``."""
        url_suffix = SOURCE_CONFIG[CRITICAL_EVENTS]["endpoint_template"].format(contract=contract_id)
        return self._http_request(
            method="GET",
            url_suffix=url_suffix,
            params=self._build_params(),
            headers={"Accept": "application/json"},
        )

    def get_events(self, contract_id: str) -> dict[str, Any]:
        """Calls ``GET /v2/events/contract/{contract}?extended=true``."""
        url_suffix = SOURCE_CONFIG[EVENTS]["endpoint_template"].format(contract=contract_id)
        return self._http_request(
            method="GET",
            url_suffix=url_suffix,
            params=self._build_params({"extended": "true"}),
            headers={"Accept": "application/json"},
        )


# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #


def _parse_positive_int_arg(raw: Any, display_name: str, default: int, ceiling: int | None = None) -> int:
    """Parse a user-supplied positive integer, rejecting invalid values loudly.

    Distinguishes between "not supplied" (use ``default``) and "explicitly
    0/negative" (reject). The ``arg_to_number(...) or DEFAULT`` idiom silently
    rewrites ``0`` to the default, masking misconfiguration.
    """
    if raw in (None, ""):
        return default
    parsed = arg_to_number(raw)
    if parsed is None:
        raise DemistoException(f"{display_name} must be an integer; got {raw!r}.")
    if parsed <= 0:
        raise DemistoException(f"{display_name} must be greater than 0; got {parsed}.")
    if ceiling is not None and parsed > ceiling:
        raise DemistoException(f"{display_name} must be between 1 and {ceiling}; got {parsed}.")
    return parsed


def parse_first_fetch(first_fetch: str) -> str:
    """Convert the human-friendly ``first_fetch`` parameter into an ISO string.

    Falls back to :data:`DEFAULT_FIRST_FETCH` if the value is empty/invalid.
    """
    parsed: datetime | None = arg_to_datetime(first_fetch or DEFAULT_FIRST_FETCH, required=False)
    if parsed is None:
        parsed = arg_to_datetime(DEFAULT_FIRST_FETCH, required=True)  # type: ignore[assignment]
    assert parsed is not None  # for mypy
    return parsed.astimezone(UTC).strftime(ISO_FORMAT)


def normalize_event_timestamp(value: Any) -> str | None:
    """Normalise a raw timestamp value (string, int, float) to ISO-8601 UTC.

    Returns ``None`` if the value cannot be parsed.
    """
    if value is None or value == "":
        return None
    try:
        parsed = arg_to_datetime(value, required=False)
    except (ValueError, TypeError):
        return None
    if parsed is None:
        return None
    return parsed.astimezone(UTC).strftime(ISO_FORMAT)


def _event_is_at_or_before(event: dict[str, Any], end_dt: datetime) -> bool:
    """Return ``True`` when the enriched event's ``_time`` is at or before ``end_dt``."""
    event_dt = arg_to_datetime(event.get("_time"))
    return event_dt is None or event_dt <= end_dt


def make_event_id(event_type: str, raw: dict[str, Any], time_field: str) -> str:
    """Build a stable dedup key.

    The Prolexic API does not return a single canonical ``id`` field, so we
    use the documented timestamp field together with any provided ``id``-like
    field to form a composite key. This is robust against re-emission of the
    same record across overlapping fetch windows.

    For sources that declare a ``revision_field`` (Critical Events use
    ``recentOccur``) that value is appended to the key. Prolexic updates a
    critical event in place while leaving ``firstOccur`` untouched, so without
    the revision component every subsequent update would collide with the
    original key and be discarded as a duplicate.
    """
    candidate_id_fields = ("id", "eventId", "incidentId", "alertId", "uuid")
    raw_id = next((str(raw[k]) for k in candidate_id_fields if raw.get(k) is not None), "")
    raw_time = str(raw.get(time_field, ""))
    key = f"{event_type}:{raw_id}:{raw_time}"

    revision_field = SOURCE_CONFIG.get(event_type, {}).get("revision_field")
    if revision_field:
        key = f"{key}:{raw.get(revision_field, '')!s}"
    return key


def annotate_critical_event(event: dict[str, Any]) -> None:
    """Add the ``_ENTRY_STATUS`` field per the design doc.

    * ``_ENTRY_STATUS = "new"``     when ``recentOccur == firstOccur``
    * ``_ENTRY_STATUS = "updated"`` when ``recentOccur >  firstOccur``
    """
    first_occur = event.get("firstOccur")
    recent_occur = event.get("recentOccur")
    if first_occur is None:
        return
    if recent_occur is None or recent_occur == first_occur:
        event["_ENTRY_STATUS"] = "new"
    else:
        event["_ENTRY_STATUS"] = "updated"


def extract_event_list(response: dict[str, Any]) -> list[dict[str, Any]]:
    """Extract the list of event dicts from a Prolexic API response.

    The Akamai Prolexic Analytics API typically returns a wrapper dict whose
    payload key varies (``events``, ``criticalEvents``, ``data``, ``items``).
    We probe each candidate and fall back to a top-level list if present.
    """
    if isinstance(response, list):
        return [e for e in response if isinstance(e, dict)]
    if not isinstance(response, dict):
        return []
    for key in ("events", "criticalEvents", "data", "items", "results"):
        value = response.get(key)
        if isinstance(value, list):
            return [e for e in value if isinstance(e, dict)]
    return []


def filter_and_dedup(
    raw_events: list[dict[str, Any]],
    event_type: str,
    last_fetch_iso: str,
    fetched_ids: set[str],
    max_events: int,
) -> tuple[list[dict[str, Any]], str, set[str]]:
    """Filter out previously-seen events and cap by ``max_events``.

    Returns the selected events, the new high-water-mark timestamp, and the
    set of dedup ids that share that high-water-mark (to be persisted in
    ``last_run`` so the next fetch can skip them even if the API re-emits them
    at exactly the same timestamp).

    Note: The returned id-set is computed defensively. When the high-water
    mark does NOT advance (i.e. ``new_high_water == last_fetch_iso``) we
    UNION with the input ``fetched_ids`` so that previously-seen ids at the
    same boundary timestamp survive into the next ``last_run``. Replacing
    them would risk re-ingestion if the API re-emitted the same event.
    The union is capped at :data:`MAX_RETAINED_DEDUP_IDS` so a source that
    keeps emitting new records at a single timestamp cannot grow ``last_run``
    without bound.
    """
    time_field = SOURCE_CONFIG[event_type]["time_field"]
    selected: list[dict[str, Any]] = []
    last_fetch_dt = arg_to_datetime(last_fetch_iso)
    new_high_water = last_fetch_iso
    new_high_water_dt = last_fetch_dt
    skipped_invalid_ts = 0
    skipped_old = 0
    skipped_seen = 0

    # Normalise once up front: the raw timestamp may be an ISO string or epoch
    # millis, and sorting those by ``str()`` is not chronological (e.g.
    # "999999999999" sorts after "1767225600000"). Because the loop below stops
    # at ``max_events`` and only ever moves the cursor forward, a mis-ordered
    # walk would skip past events that are then never fetched again.
    normalized_pairs: list[tuple[datetime, str, dict[str, Any]]] = []
    for raw in raw_events:
        normalized_ts = normalize_event_timestamp(raw.get(time_field))
        event_dt = arg_to_datetime(normalized_ts) if normalized_ts is not None else None
        if normalized_ts is None or event_dt is None:
            skipped_invalid_ts += 1
            continue
        normalized_pairs.append((event_dt, normalized_ts, raw))

    # Sort ascending by the parsed datetime so we walk the time window forward.
    for event_dt, normalized_ts, raw in sorted(normalized_pairs, key=lambda pair: pair[0]):
        if last_fetch_dt is not None and event_dt < last_fetch_dt:
            skipped_old += 1
            continue

        dedup_id = make_event_id(event_type, raw, time_field)
        if dedup_id in fetched_ids:
            skipped_seen += 1
            continue

        enriched: dict[str, Any] = dict(raw)
        enriched["_time"] = normalized_ts
        enriched["event_type"] = event_type
        enriched["source_log_type"] = event_type.upper().replace(" ", "_")
        if event_type == CRITICAL_EVENTS:
            annotate_critical_event(enriched)

        selected.append(enriched)
        fetched_ids.add(dedup_id)

        if new_high_water_dt is None or event_dt > new_high_water_dt:
            new_high_water_dt = event_dt
            new_high_water = normalized_ts

        if len(selected) >= max_events:
            break

    if skipped_invalid_ts or skipped_old or skipped_seen:
        demisto.debug(
            f"{event_type}: skipped {skipped_invalid_ts} invalid-timestamp, "
            f"{skipped_old} pre-cursor and {skipped_seen} previously-seen events."
        )
    if len(selected) >= max_events:
        demisto.debug(f"Reached max_events={max_events} for {event_type}; truncating.")

    # Compute retained ids for next run.
    new_run_ids = {make_event_id(event_type, ev, time_field) for ev in selected if ev.get("_time") == new_high_water}
    if new_high_water == last_fetch_iso:
        # Cursor did not advance — keep prior boundary ids so they cannot
        # be re-ingested next run.
        retained_ids = fetched_ids | new_run_ids
    else:
        retained_ids = new_run_ids

    if len(retained_ids) > MAX_RETAINED_DEDUP_IDS:
        # Keep the newest ids (deterministically ordered) and drop the oldest.
        # Trimming can only ever cause a re-fetch of an already-delivered event,
        # never a permanent loss, so bounding state is the safer trade-off.
        demisto.debug(
            f"{event_type}: retained dedup ids ({len(retained_ids)}) exceeded "
            f"{MAX_RETAINED_DEDUP_IDS}; trimming the oldest entries."
        )
        retained_ids = set(sorted(retained_ids)[-MAX_RETAINED_DEDUP_IDS:])
    return selected, new_high_water, retained_ids


# --------------------------------------------------------------------------- #
# Source dispatch
# --------------------------------------------------------------------------- #


def fetch_source_events(
    client: Client,
    event_type: str,
    contract_id: str,
    source_last_run: dict[str, Any],
    max_events: int,
    first_fetch_iso: str,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """Fetch + dedup events for a single source. Returns ``(events, next_run)``."""
    last_fetch_iso = source_last_run.get("last_fetch_ts") or first_fetch_iso
    fetched_ids = set(source_last_run.get("fetched_ids") or [])

    if event_type == CRITICAL_EVENTS:
        response = client.get_critical_events(contract_id)
    elif event_type == EVENTS:
        response = client.get_events(contract_id)
    else:
        raise DemistoException(f"Unsupported event type: {event_type}")

    raw_events = extract_event_list(response)
    demisto.debug(f"Fetched {len(raw_events)} raw {event_type} from Prolexic API.")

    events, new_high_water, retained_ids = filter_and_dedup(
        raw_events=raw_events,
        event_type=event_type,
        last_fetch_iso=last_fetch_iso,
        fetched_ids=fetched_ids,
        max_events=max_events,
    )
    next_run = {"last_fetch_ts": new_high_water, "fetched_ids": sorted(retained_ids)}
    return events, next_run


def push_events(events: list[dict[str, Any]]) -> None:
    """Push events to XSIAM with the documented vendor/product."""
    if not events:
        demisto.debug("No events to push to XSIAM.")
        return
    demisto.debug(f"Pushing {len(events)} events to XSIAM as {VENDOR}/{PRODUCT}.")
    send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT)


# --------------------------------------------------------------------------- #
# Commands
# --------------------------------------------------------------------------- #


def run_test_module(client: Client, contract_id: str, event_types: Iterable[str]) -> str:
    """Run a lightweight call against each configured source to confirm
    credentials and contract id.

    Named ``run_test_module`` (not ``test_module``) so that pytest does not
    collect it as a test case.
    """
    selected_types = list(event_types) or [CRITICAL_EVENTS, EVENTS]
    for event_type in selected_types:
        if event_type not in SOURCE_CONFIG:
            return f"Unknown event type configured: {event_type}"
        try:
            if event_type == CRITICAL_EVENTS:
                client.get_critical_events(contract_id)
            else:
                client.get_events(contract_id)
        except DemistoException as exc:
            # Use duck-typing on ``exc.res`` so that any object exposing
            # ``status_code`` (real ``requests.Response`` or stub) works.
            status = getattr(getattr(exc, "res", None), "status_code", None)
            if status in (401, 403):
                return (
                    "Authorization Error: verify the EdgeGrid Client Token, Client Secret, "
                    "Access Token and that the API client has access to the configured Contract ID."
                )
            if status == 404:
                return "Endpoint not found: verify the Server URL and Contract ID."
            raise
    return "ok"


def get_events_command(
    client: Client,
    args: dict[str, Any],
    contract_id: str,
    configured_types: list[str],
    first_fetch_iso: str,
) -> tuple[list[dict[str, Any]], CommandResults]:
    """Manual fetch (``akamai-prolexic-get-events``) used for development."""
    limit = _parse_positive_int_arg(args.get("limit"), "limit", DEFAULT_GET_EVENTS_LIMIT)
    requested_types = argToList(args.get("event_type")) or configured_types or [CRITICAL_EVENTS, EVENTS]

    # ``start_time`` lets the caller override the lower-bound timestamp used
    # for client-side filtering. If omitted we fall back to ``first_fetch``.
    start_time_arg = args.get("start_time")
    if start_time_arg:
        start_iso = parse_first_fetch(str(start_time_arg))
    else:
        start_iso = first_fetch_iso

    # ``end_time`` is the standard upper bound for the manual command. The
    # Prolexic endpoints do not accept a time window, so it is applied
    # client-side after normalisation.
    end_time_arg = args.get("end_time")
    end_dt = arg_to_datetime(end_time_arg) if end_time_arg else None
    start_dt = arg_to_datetime(start_iso)
    if end_dt is not None and start_dt is not None and end_dt < start_dt:
        raise DemistoException("end_time must be later than start_time.")

    all_events: list[dict[str, Any]] = []
    for event_type in requested_types:
        events, _ = fetch_source_events(
            client=client,
            event_type=event_type,
            contract_id=contract_id,
            source_last_run={"last_fetch_ts": start_iso, "fetched_ids": []},
            max_events=limit,
            first_fetch_iso=start_iso,
        )
        if end_dt is not None:
            events = [ev for ev in events if _event_is_at_or_before(ev, end_dt)]
        all_events.extend(events)

    human_readable = tableToMarkdown(
        name=f"{VENDOR.title()} {PRODUCT.title()} Events",
        t=all_events,
        removeNull=True,
    )
    return all_events, CommandResults(readable_output=human_readable)


def fetch_events(
    client: Client,
    contract_id: str,
    event_types: list[str],
    max_events_per_fetch: int,
    first_fetch_iso: str,
    last_run: dict[str, Any],
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """Top-level fetch. Returns ``(events, next_run)``."""
    next_run: dict[str, Any] = dict(last_run or {})
    all_events: list[dict[str, Any]] = []
    for event_type in event_types:
        source_key = SOURCE_CONFIG[event_type]["last_run_key"]
        source_last_run = (last_run or {}).get(source_key, {})
        events, source_next_run = fetch_source_events(
            client=client,
            event_type=event_type,
            contract_id=contract_id,
            source_last_run=source_last_run,
            max_events=max_events_per_fetch,
            first_fetch_iso=first_fetch_iso,
        )
        all_events.extend(events)
        next_run[source_key] = source_next_run
    return all_events, next_run


# --------------------------------------------------------------------------- #
# Entry point
# --------------------------------------------------------------------------- #


def _parse_max_events_per_fetch(raw: Any) -> int:
    """Parse and validate the ``max_events_per_fetch`` parameter."""
    return _parse_positive_int_arg(
        raw,
        "Maximum events per fetch",
        DEFAULT_MAX_EVENTS_PER_FETCH,
        ceiling=MAX_EVENTS_PER_FETCH_CEILING,
    )


def main() -> None:  # pragma: no cover
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()

    base_url: str = (params.get("url") or "").rstrip("/")
    contract_id: str = (params.get("contract_id") or "").strip()
    client_token: str = (params.get("client_token_creds") or {}).get("password", "") or params.get("client_token", "")
    client_secret: str = (params.get("client_secret_creds") or {}).get("password", "") or params.get("client_secret", "")
    access_token: str = (params.get("access_token_creds") or {}).get("password", "") or params.get("access_token", "")
    account_switch_key: str = (params.get("account_switch_key") or "").strip() or ""

    event_types: list[str] = argToList(params.get("event_types_to_fetch")) or [CRITICAL_EVENTS, EVENTS]

    # Collect every configuration problem up front so the user sees all of them
    # at once, and report through a single ``return_error`` call (W9011).
    config_errors: list[str] = []
    unsupported = [event_type for event_type in event_types if event_type not in SOURCE_CONFIG]
    if unsupported:
        config_errors.append(f"Unsupported event type(s) configured: {', '.join(unsupported)}.")
    if not base_url:
        config_errors.append("Server URL is required.")
    if not contract_id:
        config_errors.append("Contract ID is required.")
    if not (client_token and client_secret and access_token):
        config_errors.append("Client Token, Client Secret and Access Token are all required.")
    if config_errors:
        return_error(" ".join(config_errors))
        return

    first_fetch_iso = parse_first_fetch(params.get("first_fetch") or DEFAULT_FIRST_FETCH)
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    demisto.debug(f"Command being called is {command!r}")
    try:
        max_events_per_fetch = _parse_max_events_per_fetch(params.get("max_events_per_fetch"))

        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            proxy=proxy,
            client_token=client_token,
            client_secret=client_secret,
            access_token=access_token,
            account_switch_key=account_switch_key,
        )

        if command == "test-module":
            return_results(run_test_module(client, contract_id, event_types))
            return

        if command == "akamai-prolexic-get-events":
            should_push = argToBoolean(args.get("should_push_events", "false"))
            events, results = get_events_command(
                client=client,
                args=args,
                contract_id=contract_id,
                configured_types=event_types,
                first_fetch_iso=first_fetch_iso,
            )
            return_results(results)
            if should_push:
                push_events(events)
            return

        if command == "fetch-events":
            last_run = demisto.getLastRun() or {}
            events, next_run = fetch_events(
                client=client,
                contract_id=contract_id,
                event_types=event_types,
                max_events_per_fetch=max_events_per_fetch,
                first_fetch_iso=first_fetch_iso,
                last_run=last_run,
            )
            push_events(events)
            demisto.setLastRun(next_run)
            return

        raise NotImplementedError(f"Command {command!r} is not implemented.")

    except Exception as exc:  # noqa: BLE001
        demisto.error(traceback.format_exc())
        return_error(f"Failed to execute {command!r} command. Error: {exc!s}")


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()