Cloudflare Zero Trust

Cloudflare provides network and security products for consumers and businesses, utilizing reverse proxies for web traffic, edge computing, and a content distribution network to provide content across its network of servers.

Analytics & SIEM · Cloudflare Zero Trust

Details

IDCloudflare Zero Trust
ProviderCloudflare
CategoryAnalytics & SIEM
From Version8.4.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

Cloudflare provides network and security products for consumers and businesses, utilizing reverse proxies for web traffic, edge computing, and a content distribution network to provide content across its network of servers.
This integration was integrated and tested with version 1 of Cloudflare Zero Trust.

Authorization

Two authorization types are supported:

  • API Token - Requires generating an account or a user API token.
  • Global API Key (Legacy) - Requires retrieving the global API key and finding the associated Email address.

Refer to the integration help section for detailed instructions on how to attain the required credentials.

Token Permissions

The API Token authorization method requires an access token with the following permissions:

  • Account - Account Settings - Read
  • Account - Access: Audit Logs - Read

Note: It is recommended to use an account token (instead of a user token) to set up this integration.

Configure Cloudflare Zero Trust in Cortex

Parameter Required Additional Info
Server URL True The base URL for the Cloudflare API (e.g., https://api.cloudflare.com).
Account ID True Obtain from the Account Overview page.
Trust any certificate (not secure) False  
Use system proxy settings False  
Authorization Type True Possible values are: API Token, Global API Key (Legacy). Default value is Global API Key (Legacy).
API Token False Obtain from the Cloudflare API Tokens page.
API Email False Obtain from the Cloudflare Profile page.
Global API Key False Obtain from the Cloudflare API Tokens page.
Event types to fetch True Specify the types of events to fetch. Possible values are: Account Audit Logs, User Audit Logs, and Access Authentication Logs.
Maximum number of account audit logs per fetch False  
Maximum number of user audit logs per fetch False  
Maximum number of access authentication logs per fetch 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.

cloudflare-zero-trust-get-events


Gets events from Cloudflare Zero Trust.

Base Command

cloudflare-zero-trust-get-events

Input

Argument Name Description Required
limit The number of events to return per type. Default is 10. Optional
should_push_events If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. Optional
start_date The start date from which to filter events. Optional
event_types_to_fetch Comma-separated list of event types to fetch. Possible values are: Account Audit Logs, User Audit Logs, Access Authentication Logs. Default is Account Audit Logs,User Audit Logs. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (required)
  • account_id — Account ID (required)
  • auth_type — Authorization Type (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • event_types_to_fetch — Event types to fetch (required)
  • max_fetch_account_audit_logs — Maximum number of account audit logs per fetch
  • max_fetch_user_audit_logs — Maximum number of user audit logs per fetch
  • max_fetch_access_authentication_logs — Maximum number of access authentication logs per fetch
  • credentials — API Email
  • token_credentials

Commands (1)

  • cloudflare-zero-trust-get-events

    Gets events from Cloudflare Zero Trust.

import hashlib
from typing import Any

import demistomock as demisto
import urllib3
from enum import Enum
from CommonServerPython import *

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

VENDOR = "Cloudflare"
PRODUCT = "ZeroTrust"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
ACCOUNT_AUDIT_PAGE_SIZE = 1000
USER_AUDIT_PAGE_SIZE = 1000
ACCESS_AUTHENTICATION_PAGE_SIZE = 1000
DEFAULT_MAX_FETCH_ACCOUNT_AUDIT = 5000
DEFAULT_MAX_FETCH_USER_AUDIT = 5000
DEFAULT_MAX_FETCH_ACCESS_AUTHENTICATION = 5000
DEFAULT_COMMAND_LIMIT = 10

FETCH_EVENTS_TIMEOUT = 180  # allow up to 3 minutes to fetch events of all types

ACCOUNT_AUDIT_TYPE = "Account Audit Logs"
USER_AUDIT_TYPE = "User Audit Logs"
ACCESS_AUTHENTICATION_TYPE = "Access Authentication Logs"


class AuthTypes(Enum):
    GLOBAL_API_KEY = "Global API Key (Legacy)"
    API_TOKEN = "API Token"


""" CLIENT CLASS """


class Client(BaseClient):
    def __init__(self, base_url: str, verify: bool, proxy: bool, headers: Dict[str, str], account_id: str):
        """
        Initializes the Client with API details.

        Args:
            base_url (str): The base URL of the API.
            verify (bool): Whether to verify SSL certificates.
            proxy (bool): Whether to use a proxy.
            headers (Dict[str, str]): The HTTP headers for authentication and other configurations.
            account_id (str): The Cloudflare account ID to be used for account-specific API requests.
        """
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.account_id = account_id
        self.headers = headers

    def get_events(self, start_date: str, page_size: int, page: int, event_type: str) -> Dict[str, Any]:
        """
        Fetches events from the API for the specified event type.

        Args:
            start_date (str): The start date for fetching events, in ISO 8601 format.
            page_size (int): The maximum number of events to fetch per page.
            page (int): The page number to fetch.
            event_type (str): The type of events to fetch. Supported types include:
                              - ACCOUNT_AUDIT_TYPE
                              - USER_AUDIT_TYPE
                              - ACCESS_AUTHENTICATION_TYPE

        Returns:
            Dict[str, Any]: The API response containing the fetched events.

        Raises:
            ValueError: If the event_type is invalid or unsupported.
        """
        demisto.debug(
            f"Fetching events with start_date={start_date}, page_size={page_size}, page={page}, event_type={event_type}"
        )

        endpoint_urls = {
            ACCOUNT_AUDIT_TYPE: f"/client/v4/accounts/{self.account_id}/audit_logs",
            USER_AUDIT_TYPE: "/client/v4/user/audit_logs",
            ACCESS_AUTHENTICATION_TYPE: f"/client/v4/accounts/{self.account_id}/access/logs/access_requests",
        }
        params = {"per_page": page_size, "page": page, "since": start_date, "direction": "asc"}
        return self._http_request(method="GET", url_suffix=endpoint_urls[event_type], headers=self.headers, params=params)


def test_module(client: Client, event_types: list) -> str:
    """
    Tests API connectivity and authentication.

    When 'ok' is returned, it indicates that the integration is working as expected and the connection to the
    service is successful. If something goes wrong, the function raises exceptions with meaningful error messages.

    Args:
        client (Client): The Cloudflare Zero Trust client to use for testing connectivity.

    Returns:
        str: 'ok' if the test passed, otherwise raises an exception.
    """
    try:
        fetch_events(
            client=client,
            last_run={},
            max_fetch_account_audit=1,
            max_fetch_user_audit=1,
            max_fetch_authentication=1,
            event_types_to_fetch=event_types,
        )

    except Exception as e:
        raise e

    return "ok"


def fetch_events_for_type(
    client: Client,
    last_run: dict[str, Any],
    max_fetch: int,
    max_page_size: int,
    event_type: str,
    start_fetch_date: str = "",
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """
    Fetches events for a specific event type and returns the fetched events and updated last run details.

    Args:
        client (Client): The API client instance.
        last_run (dict): A dictionary containing the last fetch timestamp and event IDs to avoid duplicates.
        max_fetch (int): The maximum number of events to fetch.
        max_page_size (int): The maximum number of events to fetch per page.
        event_type (str): The type of events to fetch.
        start_fetch_date (str, optional): The starting date for fetching events. Defaults to "".

    Returns:
        tuple: A tuple containing:
            - list[dict[str, Any]]: The list of fetched events.
            - dict[str, Any]: The updated last run data with new timestamps and event IDs.
    """
    demisto.debug(f"Fetching events for event_type={event_type} with last_run={last_run}")

    start_date = calculate_fetch_dates(last_run, start_fetch_date)
    previous_event_ids = last_run.get("events_ids", [])
    events_to_fetch = max_fetch + len(previous_event_ids)
    page_size = min(events_to_fetch, max_page_size)

    page = 1
    events: list[dict[str, Any]] = []
    while len(events) < events_to_fetch:
        response = client.get_events(start_date, page_size, page, event_type)
        result = response.get("result", [])
        demisto.debug(f"Fetched {len(result)} events of {event_type=} on {page=}.")
        events.extend(result)
        if len(result) < page_size:
            break
        page += 1

    generate_event_id_if_not_exists(events)
    unique_events = handle_duplicates(events, previous_event_ids)[:max_fetch]
    demisto.debug(f"{event_type=} has {len(unique_events)} events after deduplication.")

    if unique_events:
        format_events(event_type, unique_events)
        start_date, previous_event_ids = prepare_next_run(unique_events)

    new_last_run = {"last_fetch": start_date, "events_ids": previous_event_ids}
    demisto.debug(f"{event_type=} has {new_last_run=}.")
    return unique_events, new_last_run


def fetch_events(
    client: Client,
    last_run: dict[str, Any],
    max_fetch_account_audit: int,
    max_fetch_user_audit: int,
    max_fetch_authentication: int,
    event_types_to_fetch: list[str],
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    """
    Fetches events for multiple event types and aggregates them.

    Args:
        client (Client): The API client instance.
        last_run (dict[str, Any]): A dictionary containing the last run data for all event types.
        max_fetch_account_audit (int): Maximum number of account audit events to fetch.
        max_fetch_user_audit (int): Maximum number of user audit events to fetch.
        max_fetch_authentication (int): Maximum number of authentication events to fetch.
        event_types_to_fetch (list[str]): List of event types to fetch.

    Returns:
        tuple: A tuple containing:
            - dict[str, Any]: The updated last run data for all event types.
            - list[dict[str, Any]]: The aggregated list of fetched events.
    """
    demisto.debug(f"Starting to fetch events. Got {last_run=}.")
    events: list[dict[str, Any]] = []
    next_run: dict[str, Any] = {}

    account_audit_last_run = last_run.get(ACCOUNT_AUDIT_TYPE, {})
    user_audit_last_run = last_run.get(USER_AUDIT_TYPE, {})
    access_authentication_last_run = last_run.get(ACCESS_AUTHENTICATION_TYPE, {})

    event_type_kwargs = {
        ACCOUNT_AUDIT_TYPE: {
            "last_run": account_audit_last_run,
            "max_fetch": account_audit_last_run.pop("max_fetch", max_fetch_account_audit),
            "event_type": ACCOUNT_AUDIT_TYPE,
            "max_page_size": ACCOUNT_AUDIT_PAGE_SIZE,
        },
        USER_AUDIT_TYPE: {
            "last_run": user_audit_last_run,
            "max_fetch": user_audit_last_run.pop("max_fetch", max_fetch_user_audit),
            "event_type": USER_AUDIT_TYPE,
            "max_page_size": USER_AUDIT_PAGE_SIZE,
        },
        ACCESS_AUTHENTICATION_TYPE: {
            "last_run": access_authentication_last_run,
            "max_fetch": access_authentication_last_run.pop("max_fetch", max_fetch_authentication),
            "event_type": ACCESS_AUTHENTICATION_TYPE,
            "max_page_size": ACCESS_AUTHENTICATION_PAGE_SIZE,
        },
    }
    event_type_is_finished: dict[str, bool] = {}

    for event_type in event_types_to_fetch:
        event_type_is_finished[event_type] = False
        event_type_timeout = FETCH_EVENTS_TIMEOUT // len(event_types_to_fetch)
        event_type_max_fetch = event_type_kwargs[event_type]["max_fetch"]

        with ExecutionTimeout(event_type_timeout):
            demisto.debug(f"Starting to fetch {event_type=} with {event_type_max_fetch=} and {event_type_timeout=}.")
            fetched_events, event_type_next_run = fetch_events_for_type(client=client, **event_type_kwargs[event_type])
            event_type_is_finished[event_type] = True

        if event_type_is_finished[event_type]:
            demisto.debug(
                f"Completed fetching {event_type=} with {event_type_max_fetch=} and {event_type_timeout=}. "
                f"Adding {len(fetched_events)} events to the list of all events."
            )
            next_run[event_type] = event_type_next_run
            events.extend(fetched_events)

        else:
            demisto.debug(
                f"Timed out fetching {event_type=} with {event_type_max_fetch=} and {event_type_timeout=}. "
                f"Setting next run for {event_type=} with reduced limit."
            )
            # If timed out, keep event type last run and reduce its max fetch limit to ensure it completes in the next iteration
            event_type_last_run = event_type_kwargs[event_type]["last_run"]
            next_run[event_type] = {**event_type_last_run, "max_fetch": max(event_type_max_fetch // 2, 1)}

    event_types_finished = event_type_is_finished.values()
    # If at least one event type timed out and at least one finished in time, trigger instant next run
    if False in event_types_finished and True in event_types_finished:
        demisto.debug("Some event types timed out. Next fetch triggered immediately.")
        next_run["nextTrigger"] = "0"
    else:
        demisto.debug("All event types timed out or finished in time. Next fetch triggered based on the configured interval.")

    demisto.debug(f"Finished fetching {len(events)} events. Setting {next_run=}.")
    return next_run, events


def get_events_command(client: Client, args: dict[str, Any]) -> tuple[list[dict[str, Any]], list[CommandResults]]:
    """
    Fetches events for specified event types and prepares results for display.

    Args:
        client (Client): The API client instance.
        args (dict[str, Any]): Command arguments containing:
            - limit (int): The maximum number of events to fetch per event type.
            - event_types_to_fetch (list[str]): The list of event types to fetch.
            - start_date (str): The start date for fetching events.

    Returns:
        tuple: A tuple containing:
            - list[dict[str, Any]]: A list of all fetched events across event types.
            - list[CommandResults]: A list of CommandResults for displaying fetched events.
    """
    event_types, start_date = validate_args(args)
    limit = arg_to_number(args.get("limit", DEFAULT_COMMAND_LIMIT)) or DEFAULT_COMMAND_LIMIT

    all_fetched_events: list[dict[str, Any]] = []
    command_results: list[CommandResults] = []

    for event_type in event_types:
        events, _ = fetch_events_for_type(
            client=client, last_run={}, max_fetch=limit, max_page_size=100, event_type=event_type, start_fetch_date=start_date
        )
        all_fetched_events.extend(events)

        if events:
            command_results.append(
                CommandResults(
                    readable_output=tableToMarkdown(f"Cloudflare Zero Trust {event_type} Events", events), raw_response=events
                )
            )

    if not all_fetched_events:
        command_results.append(CommandResults(readable_output="No events found."))

    return all_fetched_events, command_results


def calculate_fetch_dates(next_run: dict[str, Any], start_date: str = "") -> str:
    """
    Calculates the start date for fetching events. If no start date is provided, it uses the last fetched date or,
    if that is also unavailable, the current time minus 1 minute.

    Args:
        next_run (dict[str, Any]): A dictionary containing the last run timestamp.
        start_date (str): The provided start date for fetching events in '%Y-%m-%dT%H:%M:%SZ' format.

    Returns:
        str: The calculated start date in '%Y-%m-%dT%H:%M:%SZ' format.
    """
    now_utc_time = get_current_time()
    start_date = start_date or next_run.get("last_fetch") or ((now_utc_time - timedelta(minutes=1)).strftime(DATE_FORMAT))
    return start_date


def prepare_next_run(events: list[dict[str, Any]]) -> tuple[str, list[str]]:
    """
    Prepares the next run data by extracting the latest timestamp and event IDs.

    Args:
        events (list[dict[str, Any]]): A list of fetched events.

    Returns:
        tuple: A tuple containing:
            - str: The latest timestamp (up to seconds) of the fetched events.
            - list[str]: A list of IDs for the events with the latest timestamp.
    """
    latest_time = events[-1].get("when") or events[-1].get("created_at") or ""
    latest_time_obj = datetime.fromisoformat(latest_time.rstrip("Z"))
    latest_time_truncated = latest_time_obj.replace(microsecond=0).isoformat() + "Z"

    latest_ids = [
        event["id"]
        for event in events
        if (
            datetime.fromisoformat((event.get("when") or event.get("created_at") or "").rstrip("Z"))
            .replace(microsecond=0)
            .isoformat()
            + "Z"
        )
        == latest_time_truncated
    ]

    return latest_time_truncated, latest_ids


def generate_event_id_if_not_exists(events: list[dict[str, Any]]):
    """
    Generates a unique SHA256 hash as the event ID if the `id` field does not exist in the event JSON.

    Args:
        events (list[dict[str, Any]]): The list of events to process.
    """
    for event in events:
        if "id" in event:
            continue
        # Access authentication logs do *not* have an "id" field, so we need to generate a unique hash for deduplication
        # https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/logs/subresources/access_requests/
        encoded_event: bytes = json.dumps(event, sort_keys=True).encode("utf-8")
        event_id = str(hashlib.sha256(encoded_event).hexdigest())
        event["id"] = event_id
        demisto.debug(f"Generated a unique SHA256 {event_id=} using the contents of {event=}.")


def handle_duplicates(events: list[dict[str, Any]], previous_event_ids: list[str]) -> list[dict[str, Any]]:
    """
    Filters out events that have already been fetched.

    Args:
        events (list[dict[str, Any]]): The list of events to process.
        previous_event_ids (list[str]): A list of IDs of previously fetched events.

    Returns:
        list[dict[str, Any]]: A list of events excluding duplicates.
    """
    unique_events = [event for event in events if event["id"] not in previous_event_ids]
    demisto.debug(f"Deduplicated events: {len(unique_events)} (removed {len(events) - len(unique_events)} duplicates)")
    return unique_events


def format_events(event_type: str, events: list[dict[str, Any]]):
    """
    Formats events by adding `_time` and `SOURCE_LOG_TYPE` fields.
    The `_time` value is based on on the event creation or occurrence timestamp.
    The `SOURCE_LOG_TYPE` value is the event type.

    Args:
        event_type (str): The type of fetched events.
        events (list[dict[str, Any]]): The list of events to process.
    """
    for event in events:
        create_time = arg_to_datetime(arg=event.get("when") or event.get("created_at"))
        event["_time"] = create_time.strftime(DATE_FORMAT) if create_time else None
        event["SOURCE_LOG_TYPE"] = event_type


def validate_headers(params: dict) -> dict:
    """
    Validates the provided the configuration parameters and returns the authorization headers.

    Args:
        params (dict): Configuration parameters to validate.

    Raises:
        DemistoException: If the credentials do not match the selected authorization type.

    Returns:
        dict: Validated request authorization headers.
    """
    auth_type = params.get("auth_type")
    demisto.debug(f"Starting to validate parameters for {auth_type=}.")

    # API Token credentials
    token = params.get("token_credentials", {}).get("password")
    # Global API Key credentials
    auth_email = params.get("credentials", {}).get("identifier")
    auth_key = params.get("credentials", {}).get("password")

    if auth_type == AuthTypes.API_TOKEN.value:
        if not token:
            raise DemistoException(f"API Token is required for the {auth_type} authorization type.")
        if auth_email or auth_key:
            raise DemistoException(f"API Email and Global API Key should be left blank for the {auth_type} authorization type.")

        demisto.debug(f"Found API token matching {auth_type=}. Creating request headers.")
        return {"Authorization": f"Bearer {token}"}

    elif auth_type == AuthTypes.GLOBAL_API_KEY.value:
        if not (auth_email and auth_key):
            raise DemistoException(f"API Email and Global API Key are required for the {auth_type} authorization type.")
        if token:
            raise DemistoException(f"API Token should be left blank for the {auth_type} authorization type.")

        demisto.debug(f"Found API email and global key matching {auth_type=}. Creating request headers.")
        return {"X-Auth-Email": auth_email, "X-Auth-Key": auth_key}

    else:
        raise DemistoException(f"Invalid authorization type: {auth_type!r}.")


def validate_args(args: dict):
    """
    Validates the provided arguments for fetch events command.

    Args:
        args (dict): Arguments to validate.

    Raises:
        DemistoException: If an invalid event type or start date is provided.
    """
    start_date_str = ""
    event_types = argToList(args.get("event_types_to_fetch", []))
    valid_types = [ACCOUNT_AUDIT_TYPE, USER_AUDIT_TYPE, ACCESS_AUTHENTICATION_TYPE]

    invalid_types = [event_type for event_type in event_types if event_type not in valid_types]
    if invalid_types:
        raise DemistoException(
            f"Invalid event types provided: {', '.join(invalid_types)}. Valid options are: {', '.join(valid_types)}."
        )

    if start_date := arg_to_datetime(args.get("start_date", "")):
        start_date_str = start_date.strftime(DATE_FORMAT)

    return event_types, start_date_str


""" MAIN FUNCTION """


def main() -> None:  # pragma: no cover
    """
    main function, parses params and runs command functions
    """
    params = demisto.params()
    command = demisto.command()
    args = demisto.args()

    demisto.debug(f"Command being called is {command}")

    max_fetch_account_audit = arg_to_number(params.get("max_fetch_account_audit_logs")) or DEFAULT_MAX_FETCH_ACCOUNT_AUDIT
    max_fetch_user_audit = arg_to_number(params.get("max_fetch_user_audit_logs")) or DEFAULT_MAX_FETCH_USER_AUDIT
    max_fetch_authentication = (
        arg_to_number(params.get("max_fetch_access_authentication_logs")) or DEFAULT_MAX_FETCH_ACCESS_AUTHENTICATION
    )
    event_types_to_fetch = argToList(params.get("event_types_to_fetch"), transform=lambda event_type: event_type.strip())

    try:
        headers = validate_headers(params)
        client = Client(
            base_url=params.get("url", ""),
            verify=not params.get("insecure", False),
            proxy=params.get("proxy", False),
            account_id=params.get("account_id", ""),
            headers=headers,
        )

        if command == "test-module":
            result = test_module(client=client, event_types=event_types_to_fetch)
            return_results(result)

        elif command == "cloudflare-zero-trust-get-events":
            events, results = get_events_command(client=client, args=args)
            return_results(results)
            if events and argToBoolean(args.get("should_push_events")):
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)

        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            next_run, events = fetch_events(
                client=client,
                last_run=last_run,
                max_fetch_account_audit=max_fetch_account_audit,
                max_fetch_user_audit=max_fetch_user_audit,
                max_fetch_authentication=max_fetch_authentication,
                event_types_to_fetch=event_types_to_fetch,
            )
            send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            demisto.setLastRun(next_run)

    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


""" ENTRY POINT """

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