Workday Event Collector

Use Workday Event Collector integration to get activity loggings from Workday.

Analytics & SIEM · Workday

Details

IDWorkday Event Collector
ProviderWorkday
CategoryAnalytics & SIEM
From Version8.2.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM Cloud Posture Security

README

Use Workday Event Collector integration to get activity loggings from Workday.
This integration was integrated and tested with API v1.

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure Workday Event Collector in Cortex

Parameter Description Required
Server URL (e.g. https://WORKDAY-HOST/ccx/api/privacy/v1/TENANT_NAME) REST API Endpoint of Workday server. Can be obtained from View API Clients report in Workday application True
Token endpoint (e.g. https://WORKDAY-HOST/ccx/oauth2/TENANT_NAME/token) Token endpoint of the Workday server. Can be obtained from View API Clients report in Workday application. True
Client ID Copy the Client ID and Secret from the Register API Client for Integrations stage at Workday. True
Client Secret   True
Refresh Token Non-expiry Workday API refresh token. True
Trust any certificate (not secure)   False
Use system proxy settings   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)   False
Max events per fetch The maximum number of audit logs to retrieve for each event type. For more information about event types see the help section. 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.

workday-get-activity-logging


Returns activity loggings extracted from Workday.

Base Command

workday-get-activity-logging

Input

Argument Name Description Required
limit The maximum number of loggings to return.. Default is 1000. Optional
offset The zero-based index of the first object in a response collection. Default is 0. Optional
from_date The date and time of the earliest log entry. The default timezone is UTC/GMT. The time format is “{yyyy}-{mm}-{dd}T{hh}:{mm}:{ss}Z”. Example: “2021-05-18T13:45:14Z” indicates May 18, 2021, 1:45PM UTC. Possible values are: . Required
to_date The time format is “{yyyy}-{mm}-{dd}T{hh}:{mm}:{ss}Z”. Example: “2021-05-18T13:45:14Z” indicates May 18, 2021, 1:45PM UTC. Possible values are: . Required
should_push_events Set this argument to True in order to create events, otherwise the command will only display them. Possible values are: True, False. Default is False.  

Context Output

There is no context output for this command.

Command example

!workday-get-activity-logging limit=4 from_date=2023-04-24T07:00:00Z to_date=2023-04-24T08:00:00Z

Human Readable Output

Activity Logging List

Activity Action Device Type Ip Address Request Time Session Id System Account Target Task Display Name Task Id User Activity Entry Count User Agent
test_action test_device 1.1.1.1 2023-04-24T07:00:00Z test_session_id 123 id: 1234
descriptor: test_descriptor
href: test_href
test_display 1 1234 test_agent
test_action test_device 1.1.1.1 2023-04-24T07:00:00Z test_session_id 123 id: 1234
descriptor: test_descriptor
href: test_href
test_display 2 1234 test_agent
test_action test_device 1.1.1.1 2023-04-24T07:00:00Z test_session_id 123 id: 1234
descriptor: test_descriptor
href: test_href
test_display 3 1234 test_agent
test_action test_device 1.1.1.1 2023-04-24T07:00:00Z test_session_id 123 id: 1234
descriptor: test_descriptor
href: test_href
test_display 4 1234 test_agent

Configuration parameters

  • base_url — Server URL (e.g. https://WORKDAY-HOST/ccx/api/privacy/v1/TENANT-NAME) (required)
  • token_url — Token endpoint (e.g. https://WORKDAY-HOST/ccx/oauth2/TENANT-NAME/token) (required)
  • credentials — Client ID (required)
  • token — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • max_fetch — Max events per fetch
  • eventFetchInterval — Events Fetch Interval

Commands (1)

  • workday-get-activity-logging

    Returns activity loggings extracted from Workday. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.

import math

import demistomock as demisto
import urllib3
from CommonServerPython import *  # noqa # pylint: disable=unused-wildcard-import

from CommonServerUserPython import *  # noqa

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DEFAULT_MAX_FETCH = 3000
MAX_PAGE_SIZE = 1000
VENDOR = "Workday"
PRODUCT = "Activity"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

""" CLIENT CLASS """


class Client(BaseClient):
    """Client class to interact with the service API

    This Client implements API calls to the Saas Security platform, and does not contain any XSOAR logic.
    Handles the token retrieval.

    :param base_url (str): Workday server url.
    :param client_id (str): Workday client id.
    :param client_secret (str): Workday client_secret.
    :param token_url (str): Workday token url.
    :param refresh_token (str): Workday refresh token.
    :param verify (bool): specifies whether to verify the SSL certificate or not.
    :param proxy (bool): specifies if to use XSOAR proxy settings.
    """

    def __init__(self, base_url, token_url, verify, proxy, headers, client_id, client_secret, refresh_token, max_fetch):
        super().__init__(base_url, verify=verify, proxy=proxy, headers=headers)
        self.client_id = client_id
        self.client_secret = client_secret
        self.refresh_token = refresh_token
        self.token_url = token_url
        self.max_fetch = max_fetch
        self.access_token = self.get_access_token()

    def get_access_token(self):  # pragma: no cover
        """
        Getting access token from Workday API.
        """
        demisto.debug("Fetching access token from Workday API.")
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {"grant_type": "refresh_token", "refresh_token": self.refresh_token}

        workday_resp_token = self._http_request(
            method="POST", full_url=self.token_url, headers=headers, data=data, auth=(self.client_id, self.client_secret)
        )
        if workday_resp_token:
            return workday_resp_token.get("access_token")
        return None

    def http_request(
        self, method: str, url_suffix: str = "", params: dict = None, json_data: dict = None, retries: int = 0
    ) -> dict:  # pragma: no cover
        """
        Overriding BaseClient http request in order to use the access token.
        """
        headers = self._headers
        headers["Authorization"] = f"Bearer {self.access_token}"
        return self._http_request(
            method=method, url_suffix=url_suffix, params=params, json_data=json_data, headers=headers, retries=retries
        )

    def get_activity_logging_request(
        self,
        from_date: str,
        to_date: str,
        offset: Optional[int] = 0,
        user_activity_entry_count: bool = False,
        limit: Optional[int] = 1000,
    ) -> list:
        """Returns a simple python dict with the information provided
        Args:
            offset: The zero-based index of the first object in a response collection.
            limit: The maximum number of loggings to return.
            to_date: date to fetch events from.
            from_date: date to fetch events to.
            user_activity_entry_count: If true, returns only the total count of user activity instances for the params.

        Returns:
            activity loggings returned from Workday API.
        """
        instance_returned = math.ceil(self.max_fetch / 10000)
        params = {
            "from": from_date,
            "to": to_date,
            "limit": limit,
            "instancesReturned": instance_returned,
            "offset": offset,
            "returnUserActivityEntryCount": user_activity_entry_count,
            "type": "userActivity",
        }
        demisto.debug(f"params sent to Workday API are {params!s}")
        res = self.http_request(method="GET", url_suffix="/activityLogging", params=params, retries=3)
        return res.get("data", [])


""" HELPER FUNCTIONS """


def resolve_max_fetch(params: dict) -> int:
    """
    Resolves the max_fetch value from the integration params, falling back to DEFAULT_MAX_FETCH
    when the parameter is missing, empty, or evaluates to a falsy value.

    Args:
        params: The integration parameters.

    Returns:
        The resolved max_fetch value.
    """
    return arg_to_number(params.get("max_fetch")) or DEFAULT_MAX_FETCH


def get_max_fetch_activity_logging(client: Client, logging_to_fetch: int, from_date: str, to_date: str):
    """
    Fetches up to logging_to_fetch activity logging avaiable from Workday.
    Args:
        client: Client object.
        logging_to_fetch: limit of logging to fetch from Workday.
        from_date: loggings from time.
        to_date: loggings to time.

    Returns:
        Activity loggings fetched from Workday.
    """
    activity_loggings: list = []
    offset = 0
    while logging_to_fetch > 0:
        limit = min(MAX_PAGE_SIZE, logging_to_fetch)
        res = client.get_activity_logging_request(from_date=from_date, to_date=to_date, offset=offset, limit=limit)
        demisto.debug(f"Fetched {len(res)} activity loggings.")
        activity_loggings.extend(res)
        offset += len(res)
        logging_to_fetch -= len(res)
        if not res:
            break
        demisto.debug(f"{logging_to_fetch} loggings left to fetch.")
    demisto.debug(f"Found {len(activity_loggings)} activity loggings.")
    return activity_loggings


def remove_duplications(activity_loggings: list, last_run: dict):
    """
    Removes potential duplicated activity loggings.

    Args:
        activity_loggings: activity loggings fetched from Workday.
        last_run: Last run object.
    """
    demisto.debug("Started removing duplications")
    last_log_stored = last_run.get("last_log")
    log_found = False
    final_count = 0
    if last_log_stored:
        for count, log in enumerate(activity_loggings):
            if log == last_log_stored:
                log_found = True
                final_count = count
                break
        if log_found:
            demisto.debug(f"Found duplicated with {last_log_stored}, returning from {final_count}")
            return activity_loggings[final_count + 1 :]
    demisto.debug("Didn't find duplications, returning everything")
    return activity_loggings


def remove_milliseconds_from_time_of_logging(activity_logging: dict):
    """
    Workday API receive from_date only without milliseconds, therefor need to be removed.
    Args:
        activity_logging: activity logging

    Returns:
        The logging with the string in the correct format.

    """
    demisto.debug("Changing timestamp of loggings to match date format.")
    date_format_with_milliseconds = "%Y-%m-%dT%H:%M:%S.%fZ"
    request_time_date_obj = datetime.strptime(activity_logging.get("requestTime"), date_format_with_milliseconds)  # type: ignore
    request_time_date_obj.replace(microsecond=0)
    return datetime.strftime(request_time_date_obj, DATE_FORMAT)


""" COMMAND FUNCTIONS """


def get_activity_logging_command(
    client: Client, from_date: str, to_date: str, limit: Optional[int], offset: Optional[int]
) -> tuple[list, CommandResults]:
    """

    Args:
        offset: The zero-based index of the first object in a response collection.
        limit: The maximum number of loggings to return.
        to_date: date to fetch events from.
        from_date: date to fetch events to.
        client: Client object.

    Returns:
        Activity loggings from Workday.
    """

    activity_loggings = client.get_activity_logging_request(to_date=to_date, from_date=from_date, limit=limit, offset=offset)
    readable_output = tableToMarkdown(
        "Activity Logging List:",
        activity_loggings,
        removeNull=True,
        headerTransform=lambda x: string_to_table_header(camel_case_to_underscore(x)),
    )

    return activity_loggings, CommandResults(readable_output=readable_output)


def fetch_activity_logging(client: Client, max_fetch: int, first_fetch: datetime, last_run: dict):
    """
    Fetches activity loggings from Workday.
    Args:
        first_fetch: first fetch date.
        client: Client object.
        max_fetch: max loggings to fetch set by customer.
        last_run: last run object.

    Returns:
        Activity loggings from Workday.

    """
    from_date = last_run.get("last_fetch_time", first_fetch.strftime(DATE_FORMAT))
    to_date = datetime.now(tz=timezone.utc).strftime(DATE_FORMAT)
    demisto.debug(f"Getting activity loggings {from_date=}, {to_date=}.")
    activity_loggings = get_max_fetch_activity_logging(
        client=client, logging_to_fetch=max_fetch, from_date=from_date, to_date=to_date
    )

    activity_loggings = remove_duplications(activity_loggings=activity_loggings, last_run=last_run)
    if activity_loggings:
        last_log = activity_loggings[-1]
        last_log_time = remove_milliseconds_from_time_of_logging(last_log)
        last_run = {"last_fetch_time": last_log_time, "last_log": last_log}

    return activity_loggings, last_run


def test_module(client: Client) -> str:  # pragma: no cover
    """Tests API connectivity and authentication'

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    :type client: ``Client``
    :param Client: client to use

    :return: 'ok' if test passed, anything else will fail the test.
    :rtype: ``str``
    """

    client.get_access_token()
    return "ok"


""" MAIN FUNCTION """


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

    base_url = params.get("base_url")
    token_url = params.get("token_url")
    client_id = params.get("credentials", {}).get("identifier")
    client_secret = params.get("credentials", {}).get("password")
    token = params.get("token", {}).get("password")

    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    max_fetch = resolve_max_fetch(params)
    first_fetch = arg_to_datetime(arg=params.get("first_fetch", "3 days"), arg_name="First fetch time", required=True)

    demisto.debug(f"Command being called is {command}")
    try:
        client = Client(
            base_url=base_url,
            token_url=token_url,
            client_id=client_id,
            client_secret=client_secret,
            refresh_token=token,
            verify=verify_certificate,
            proxy=proxy,
            headers={"Accept": "application/json", "Content-Type": "application/json"},
            max_fetch=max_fetch,
        )

        if command == "test-module":
            return_results(test_module(client))

        elif command == "workday-get-activity-logging":
            should_push_events = argToBoolean(args.get("should_push_events", "false"))
            activity_loggings, results = get_activity_logging_command(
                client=client,
                from_date=args.get("from_date"),
                to_date=args.get("to_date"),
                limit=arg_to_number(args.get("limit")),
                offset=arg_to_number(args.get("offset")),
            )
            return_results(results)
            if should_push_events:
                send_events_to_xsiam(activity_loggings, vendor=VENDOR, product=PRODUCT)
        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            activity_loggings, new_last_run = fetch_activity_logging(
                client=client,
                max_fetch=max_fetch,
                first_fetch=first_fetch,  # type: ignore
                last_run=last_run,
            )
            send_events_to_xsiam(activity_loggings, vendor=VENDOR, product=PRODUCT)
            if new_last_run:
                # saves next_run for the time fetch-events is invoked
                demisto.info(f"Setting new last_run to {new_last_run}")
                demisto.setLastRun(new_last_run)

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")


""" ENTRY POINT """

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