CelonisEventCollector

The Celonis Platform offers you a suite of process mining and intelligence features, helping you to integrate your data and then use that data to analyze, improve, and monitor your business performance across key metrics.

Analytics & SIEM · Celonis

Details

IDCelonisEventCollector
ProviderCelonis
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

Celonis Event Collector is an integration that supports fetching audit log events.
This integration was integrated and tested with version 4.0 of Celonis.

Configure Celonis in Cortex

Parameter Description Required
Server URL The endpoint URL is constructed using the team name and realm in the format: https://<teamname>.<realm>.celonis.cloud. True
Server URL The endpoint URL is constructed using the team name and realm in the format: https://<teamname>.<realm>.celonis.cloud. True
Client ID The Client ID to use for connection. True
Client Secret The Client Secret to use for connection. True
Trust any certificate (not secure)   False
Use system proxy settings   False
Maximum number of events per fetch Defines the maximum number of audits events per fetch cycle. Default value: 600. True

API keys, passed in an HTTP header like this: Authorization: Bearer API_KEY.

How to create an OAuth client and generate client ID and Client Secret

  1. To start, you need to create an OAuth client in your team and then grant this client API permissions.
  2. Click Admin & Settings and select Applications.
  3. Click Add New Application - OAuth client and create your OAuth client.
    When creating your OAuth client, use the following configurations: Authentication method: Client secret post.
  4. Select the following scopes:
    • audit.log:read (For the Audit Log API).
    • platform-adoption.tracking-events:read (For the Studio Adoption API).
    • team.login-history:read (For the Login History API).
  5. Click Create and then copy the client ID and client secret to your clipboard for later use.
  6. Click Permissions and edit Team permissions.
  7. Assign Audit Log API, *Login History API, and Studio Adoption APIs permissions to your newly created application as required.
  8. Click Save.
    The OAuth client now has the relevant API permissions.

For more information visit Celonis Audit Logs Documentation.

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.

celonis-get-events


Retrieves a list of audit logs events from the Celonis instance.

Base Command

celonis-get-events

Input

Argument Name Description Required
should_push_events Set this argument to true in order to create events, otherwise it will only display them. Possible values are: true, false. Default is false. Required
limit Maximum number of events to return. Required
start_date The starting date from which events should be fetched. The date should be in the format “YYYY-MM-DDTHH:MM:SS.sssZ”. Example: 2025-02-04T10:33:24.647Z. Required
end_date The date up to which events should be fetched. The date should be in the format “YYYY-MM-DDTHH:MM:SS.sssZ”. Example: 2025-02-04T10:33:24.647Z. Required

Context Output

Path Type Description
Celonis.Audit List The list of audit logs events.

Command example

!celonis-get-events should_push_events=false limit=10 end_date=2025-02-04T10:33:24.647Z start_date=2025-02-10T10:33:24.647Z

Configuration parameters

  • url — Server URL (required)
  • credentials — Client ID (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchEvents — Fetch events
  • max_events_per_fetch — Maximum number of events per fetch (required)

Commands (1)

  • celonis-get-events

    Retrieves a list of audit logs events from the Celonis instance.

import demistomock as demisto
import urllib3
from CommonServerPython import *

from CommonServerUserPython import *

urllib3.disable_warnings()

""" CONSTANTS """

VENDOR = "Celonis"
PRODUCT = "Celonis"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
PAGE_SIZE = 200
PAGE_NUMBER = 0
DEFAULT_FETCH_LIMIT = 600

""" CLIENT CLASS """


class Client(BaseClient):
    def __init__(self, base_url: str, verify: bool, client_id: str, client_secret: str):
        self.client_id = client_id
        self.client_secret = client_secret
        super().__init__(base_url=base_url, verify=verify)
        self.token: str = ""

    def set_token(self, token: str):
        """
        Sets the client token.
        """
        self.token = token

    def create_access_token_for_audit(self) -> None:
        """
        Creates an access token for audit log access using a specific scope and client credentials.
        """
        data = {"grant_type": "client_credentials", "scope": "audit.log:read"}
        results = self._http_request(
            method="POST", url_suffix="/oauth2/token", data=data, auth=(self.client_id, self.client_secret), retries=3
        )
        self.token = results.get("access_token", "")

    def get_audit_logs(self, start_date: str, end_date: str) -> requests.Response:
        """
        Retrieves audit logs for the given date range using the access token.
        Args:
            start_date (str): The start date of the logs in ISO 8601 format (e.g., "2025-02-05T14:30:00Z").
            end_date (str): The end date of the logs in ISO 8601 format (e.g., "2025-02-05T15:00:00Z").
        Returns:
            dict: The raw response.
        """
        results = self._http_request(
            method="GET",
            url_suffix=f"/log/api/external/audit?pageNumber={PAGE_NUMBER}&pageSize={PAGE_SIZE}&from={start_date}&to={end_date}",
            headers={
                "Authorization": f"Bearer {self.token}",
            },
            resp_type="response",
            retries=3,
        )
        return results


""" HELPER FUNCTIONS """


def sort_events_by_timestamp(events: list) -> list:
    """
    Sorts a list of events by their date in ascending order.
    Args:
        events (list): A list of dictionaries.
    Returns:
        list: The sorted list of events based on the 'timestamp' field.
    """
    return sorted(events, key=lambda x: datetime.strptime(x["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z"))


def add_millisecond(timestamp: str) -> str:
    """
    Adds one millisecond to a given timestamp.
    Args:
        timestamp (str): The timestamp in ISO 8601 format (e.g., "2025-02-05T14:30:00.123Z").
    Returns:
        str: The new timestamp with one millisecond added, formatted in ISO 8601.
    """
    dt = datetime.strptime(timestamp, DATE_FORMAT)
    dt += timedelta(milliseconds=1)
    return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


""" COMMAND FUNCTIONS """


def test_module(client: Client) -> str:
    """
    Tests the connection to the service by creating an access token.
    Args:
        client (Client): The client object used to interact with the service.
    Returns:
        str: 'ok' if the connection is successful. If an authorization error occurs, an appropriate error message is returned.
    """
    current_time = get_current_time()
    start_date = (current_time - timedelta(minutes=1)).strftime(DATE_FORMAT)
    end_date = current_time.strftime(DATE_FORMAT)
    fetch_events(client, 1, {"start_date": start_date, "end_date": end_date})
    return "ok"


def fetch_events(client: Client, fetch_limit: int, get_events_args: dict = None) -> tuple[list, dict]:
    last_run = demisto.getLastRun() or {}
    start_time = (get_events_args or last_run).get("start_date", "") or get_current_time().strftime(DATE_FORMAT)
    end_time = (get_events_args or {}).get("end_date", get_current_time().strftime(DATE_FORMAT))

    if not get_events_args:  # Only set token for fetch_events case
        client.set_token(last_run.get("audit_token", ""))

    demisto.debug(f"Fetching audit logs events from date={start_time} to date={end_time}.")

    output: list = []
    while True:
        try:
            response = client.get_audit_logs(start_time, end_time)
        except DemistoException as e:
            if e.res.status_code == 429:
                retry_after = int(e.res.headers.get("x-ratelimit-reset", 2))
                demisto.debug(f"Rate limit reached. Waiting {retry_after} seconds before retrying.")
                time.sleep(retry_after)  # pylint: disable=E9003
                continue
            if e.res.status_code == 401:
                demisto.debug("Regenerates token for fetching audit logs.")
                client.create_access_token_for_audit()
                continue
            else:
                raise e

        content: list = response.json().get("content", [])

        if not content:
            break

        events = sort_events_by_timestamp(content)
        for event in events:
            event_date = event.get("timestamp")
            event["_time"] = event_date
            output.append(event)

            if len(output) >= fetch_limit:
                start_time = add_millisecond(event_date)
                # Safe to add a millisecond and fetch since no two events share the same timestamp.
                new_last_run = {"start_date": start_time, "audit_token": client.token}
                return output, new_last_run

        start_time = add_millisecond(event_date)

    new_last_run = {"start_date": start_time, "audit_token": client.token}
    return output, new_last_run


def get_events(client: Client, args: dict) -> tuple[list, CommandResults]:
    start_date = args.get("start_date")
    end_date = args.get("end_date")
    limit: int = arg_to_number(args.get("limit")) or DEFAULT_FETCH_LIMIT

    output, _ = fetch_events(client, limit, {"start_date": start_date, "end_date": end_date})

    filtered_events = []
    for event in output:
        filtered_event = {
            "User ID": event.get("userId"),
            "User Role": event.get("userRole"),
            "Event": event.get("event"),
            "Timestamp": event.get("timestamp"),
        }
        filtered_events.append(filtered_event)

    human_readable = tableToMarkdown(name="Audit Logs Events", t=filtered_events, removeNull=True)
    command_results = CommandResults(
        readable_output=human_readable,
        outputs=output,
        outputs_prefix="Celonis.Audit",
    )
    return output, command_results


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

    demisto.debug(f"Command being called is {command}")
    try:
        base_url = params.get("url")
        verify_certificate = not argToBoolean(params.get("insecure", False))
        client_id = params.get("credentials", {}).get("identifier")
        client_secret = params.get("credentials", {}).get("password")
        fetch_limit = arg_to_number(params.get("max_events_per_fetch")) or DEFAULT_FETCH_LIMIT

        client = Client(base_url=base_url, verify=verify_certificate, client_id=client_id, client_secret=client_secret)
        if command == "test-module":
            result = test_module(client)
            return_results(result)
        elif command == "fetch-events":
            events, new_last_run_dict = fetch_events(client, fetch_limit)
            if events:
                demisto.debug(f"Sending {len(events)} events.")
                send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT)
            demisto.setLastRun(new_last_run_dict)
            demisto.debug(f"Successfully saved last_run= {demisto.getLastRun()}")
        elif command == "celonis-get-events":
            events, command_results = get_events(client, args)
            if events and argToBoolean(args.get("should_push_events")):
                demisto.debug(f"Sending {len(events)} events.")
                send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT)
            return_results(command_results)
        else:
            raise NotImplementedError(f"Command {command} is not implemented")
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


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