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
| ID | Cloudflare Zero Trust |
|---|---|
| Provider | Cloudflare |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
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 settingsevent_types_to_fetch— Event types to fetch (required)max_fetch_account_audit_logs— Maximum number of account audit logs per fetchmax_fetch_user_audit_logs— Maximum number of user audit logs per fetchmax_fetch_access_authentication_logs— Maximum number of access authentication logs per fetchcredentials— API Emailtoken_credentials—
Commands (1)
-
cloudflare-zero-trust-get-eventsGets events from Cloudflare Zero Trust.
import datetime import re import dateparser import pytest from CloudflareZeroTrustEventCollector import ( ACCOUNT_AUDIT_TYPE, ACCESS_AUTHENTICATION_TYPE, AuthTypes, Client, calculate_fetch_dates, DATE_FORMAT, DemistoException, fetch_events, format_events, get_events_command, generate_event_id_if_not_exists, handle_duplicates, prepare_next_run, SignalTimeoutError, ) from freezegun import freeze_time MOCK_BASE_URL = "https://api.cloudflare.com" MOCK_ACCOUNT_ID = "mock_account_id" MOCK_API_TOKEN = "test_token_123" MOCK_GLOBAL_API_KEY = "mock_api_key" MOCK_EMAIL = "test@example.com" MOCK_API_TOKEN_HEADERS = {"Authorization": f"Bearer {MOCK_API_TOKEN}"} MOCK_GLOBAL_API_KEY_HEADERS = {"X-Auth-Email": MOCK_EMAIL, "X-Auth-Key": MOCK_GLOBAL_API_KEY} MOCK_TIME_UTC_NOW = "2024-01-01T00:00:00.000000Z" # Sample event data for testing SAMPLE_EVENTS = [ {"id": "4", "created_at": "2024-01-01T11:59:58Z"}, {"id": "3", "when": "2024-01-01T11:59:59Z"}, {"id": "2", "when": "2024-01-01T12:00:00Z"}, {"id": "1", "when": "2024-01-01T12:00:00Z"}, ] @pytest.fixture def mock_client() -> Client: """Fixture to create a mock client for testing.""" return Client( base_url=MOCK_BASE_URL, verify=False, proxy=False, headers=MOCK_GLOBAL_API_KEY_HEADERS, account_id=MOCK_ACCOUNT_ID, ) @freeze_time(MOCK_TIME_UTC_NOW) def test_test_module(mock_client: Client, mocker): """Test the test_module function.""" from CloudflareZeroTrustEventCollector import test_module mocker.patch("CloudflareZeroTrustEventCollector.fetch_events", return_value=({}, [])) events_types = ["Account Audit Logs", "User Audit Logs", "Access Authentication Logs"] result = test_module(mock_client, events_types) assert result == "ok" @freeze_time(MOCK_TIME_UTC_NOW) def test_fetch_events_completes(mock_client: Client, mocker): """ Given: Event types to fetch with their `max_fetch` limits. When: Calling `fetch_events` and the `fetch_events_for_type` function completes in time. Then: Ensure the `next_run` has the right `last_fetch` timestamp and the events are returned. """ mocker.patch( "CloudflareZeroTrustEventCollector.Client.get_events", return_value={"result": [{"id": "event1", "created_at": "2024-01-01T00:00:00Z"}]}, ) last_run = {} max_fetch_account_audit = 5 max_fetch_user_audit = 5 max_fetch_authentication = 5 event_types_to_fetch = ["Account Audit Logs", "User Audit Logs"] next_run, events = fetch_events( client=mock_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, ) assert len(events) == 2 # one for each type, since the len(result) < page_size: break condition. assert events[0]["id"] == "event1" assert events[0].get("SOURCE_LOG_TYPE") assert next_run["Account Audit Logs"]["last_fetch"] == "2024-01-01T00:00:00Z" def test_fetch_events_times_out(mock_client: Client, mocker): """ Given: Event types to fetch with their `max_fetch` limits. When: Calling `fetch_events` and the `fetch_events_for_type` function times out. Then: Ensure the timeout logic is executed; the `next_run` has the right `max_fetch` value and no events are returned. """ from CloudflareZeroTrustEventCollector import ACCOUNT_AUDIT_TYPE max_fetch_account_audit = 20 max_fetch_user_audit = 5 max_fetch_authentication = 5 last_run = {ACCOUNT_AUDIT_TYPE: {"last_fetch": "2024-01-01T00:00:00Z"}} event_types_to_fetch = [ACCOUNT_AUDIT_TYPE] mocker.patch("CloudflareZeroTrustEventCollector.fetch_events_for_type", side_effect=SignalTimeoutError) next_run, events = fetch_events( client=mock_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, ) # Timeout handler should have been called, assert next_run[ACCOUNT_AUDIT_TYPE]["max_fetch"] == max_fetch_account_audit // 2 # Reduce max_fetch limit assert "nextTrigger" not in next_run # Do not set nextTrigger since all event types timed out assert events == [] # No events returned on timeout @freeze_time(MOCK_TIME_UTC_NOW) def test_get_events_command(mock_client: Client, mocker): """Test the get_events_command function.""" mocker.patch( "CloudflareZeroTrustEventCollector.Client.get_events", return_value={ "result": [ {"id": "event1", "created_at": "2024-01-01T00:00:00Z"}, {"id": "event2", "created_at": "2024-01-01T00:00:01Z"}, ] }, ) args = {"limit": "2", "event_types_to_fetch": "Account Audit Logs", "start_date": "2024-01-01T00:00:00Z"} events, command_results = get_events_command(mock_client, args) assert len(events) == 2 assert events[0]["id"] == "event1" assert events[1]["id"] == "event2" assert len(command_results) == 1 assert "Cloudflare Zero Trust Account Audit Logs Events" in command_results[0].readable_output @freeze_time(MOCK_TIME_UTC_NOW) def test_calculate_fetch_dates_with_last_run(): """ Given: A mock Cloudflare API client and last run key. When: Running CalculateFetchDates with last run. Then: Ensure the returned start date is the last fetch time, and the end date is the current time. """ last_fetch_time = (dateparser.parse(MOCK_TIME_UTC_NOW) - datetime.timedelta(minutes=1)).strftime(DATE_FORMAT) next_run = {"last_fetch": last_fetch_time, "events_ids": "event1"} start_date = calculate_fetch_dates(next_run=next_run) assert start_date == last_fetch_time @freeze_time(MOCK_TIME_UTC_NOW) def test_calculate_fetch_dates_without_arguments(): """ Given: A mock Cloudflare API client. When: Running CalculateFetchDates with no arguments. Then: Ensure the returned start date is 1 minute before the current time, and the end date is the current time. """ start_date = calculate_fetch_dates(next_run={}) assert start_date == (dateparser.parse(MOCK_TIME_UTC_NOW) - datetime.timedelta(minutes=1)).strftime(DATE_FORMAT) def test_prepare_next_run(): """Test the prepare_next_run function.""" latest_time, latest_ids = prepare_next_run(SAMPLE_EVENTS) assert latest_time == "2024-01-01T12:00:00Z" assert latest_ids == ["2", "1"] def test_generate_event_id_if_not_exists(): """ Given: Two events (one with an `id` field and one without). When: Calling `generate_event_id_if_not_exists`. Then: Ensure the `id` is preserved for the first event and generated for the second. """ original_event_id = "187d944c61940c77" test_events = [ { # With ID "id": original_event_id, "when": "2025-01-01T05:20:00.12345Z", "ip_address": "1.2.3.4", "user_email": "user@example.com", "action": "logout", }, { # Without ID "action": "login", "allowed": True, "connection": "saml", "user_email": "user@example.com", "created_at": "2025-01-01T05:20:00.12345Z", }, ] generate_event_id_if_not_exists(test_events) assert test_events[0]["id"] == original_event_id assert test_events[1]["id"] == "ffc4ff957a3d1a39ebc27580b26e7b135d0b2b511f0d786da257ed9a607d7b57" @pytest.mark.parametrize( "event, event_type, expected_time", [ pytest.param( {"id": "A", "created_at": "2025-01-01T05:20:24.12345Z"}, ACCOUNT_AUDIT_TYPE, "2025-01-01T05:20:24Z", id="Account audit event with `created_id` field", ), pytest.param( {"when": "2025-01-01T23:03:12.12345Z"}, ACCESS_AUTHENTICATION_TYPE, "2025-01-01T23:03:12Z", id="Access authentication event with `when` field", ), ], ) def test_format_events(event: dict, event_type: str, expected_time: str): """ Given: An event of a specific type. When: Calling `format_events`. Then: Ensure the event has the correct `_time` and `SOURCE_LOG_TYPE` values. """ events = [event] format_events(event_type, events) assert events[0]["_time"] == expected_time assert events[0]["SOURCE_LOG_TYPE"] == event_type def test_handle_duplicates(): """Test the handle_duplicates function.""" previous_ids = ["1", "3"] filtered_events = handle_duplicates(SAMPLE_EVENTS, previous_ids) assert len(filtered_events) == 2 # IDs "2" and "4" remain assert filtered_events[0]["id"] == "4" assert filtered_events[1]["id"] == "2" @pytest.mark.parametrize( "params, expected_headers", [ pytest.param( { "auth_type": AuthTypes.API_TOKEN.value, "token_credentials": {"password": MOCK_API_TOKEN}, }, MOCK_API_TOKEN_HEADERS, id="API token headers", ), pytest.param( { "auth_type": AuthTypes.GLOBAL_API_KEY.value, "credentials": {"identifier": MOCK_EMAIL, "password": MOCK_GLOBAL_API_KEY}, }, MOCK_GLOBAL_API_KEY_HEADERS, id="Global API key headers", ), ], ) def test_validate_headers_returns_correct_headers(params: dict, expected_headers: dict): """ Given: Valid configuration parameters of an integration instance. When: Calling validate_headers. Then: Ensure the returned authorization headers are as expected. """ from CloudflareZeroTrustEventCollector import validate_headers assert validate_headers(params) == expected_headers @pytest.mark.parametrize( "params, expected_error_message", [ pytest.param( { "auth_type": AuthTypes.API_TOKEN.value, "token_credentials": {}, }, f"API Token is required for the {AuthTypes.API_TOKEN.value} authorization type.", id="API Token type chosen with empty token credentials", ), pytest.param( { "auth_type": AuthTypes.GLOBAL_API_KEY.value, "credentials": {"identifier": MOCK_EMAIL}, }, f"API Email and Global API Key are required for the {AuthTypes.GLOBAL_API_KEY.value} authorization type.", id="Global API Key type chosen with partial credentials", ), pytest.param( { "auth_type": AuthTypes.GLOBAL_API_KEY.value, "credentials": {"identifier": MOCK_EMAIL, "password": MOCK_GLOBAL_API_KEY}, "token_credentials": {"password": MOCK_API_TOKEN}, }, f"API Token should be left blank for the {AuthTypes.GLOBAL_API_KEY.value} authorization type.", id="Global API Key type chosen with API Token credentials", ), pytest.param( {"auth_type": "Hello!"}, "Invalid authorization type: 'Hello!'.", id="Invalid authorization type", ), ], ) def test_validate_headers_raises_exception(params: dict, expected_error_message: str): """ Given: Invalid configuration parameters of an integration instance. When: Calling `validate_headers`. Then: Ensure the correct exception is raised with the expected error message. """ from CloudflareZeroTrustEventCollector import validate_headers with pytest.raises(DemistoException, match=re.escape(expected_error_message)): validate_headers(params)