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 json
from datetime import datetime, timedelta

import pytest
from freezegun import freeze_time
from WorkdayEventCollector import DATE_FORMAT, DEFAULT_MAX_FETCH, MAX_PAGE_SIZE, Client


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


class TestFetchActivity:
    @pytest.fixture(autouse=True)
    def create_client(self, mocker):
        self.base_url = "https://test.com"
        self.tenant_name = "test"
        self.token_url = f"{self.base_url}/ccx/oauth2/{self.tenant_name}/token"
        mocker.patch.object(Client, "get_access_token", return_value="1234")
        self.client = Client(
            base_url=self.base_url,
            refresh_token="refresh_token",
            client_id="test12",
            client_secret="test_sec1234",
            token_url=self.token_url,
            verify=False,
            proxy=False,
            headers={"Accept": "application/json", "Content-Type": "application/json"},
            max_fetch=25000,
        )

    @staticmethod
    def create_response_by_limit(from_date, to_date, offset, user_activity_entry_count=False, limit=1):
        single_response = util_load_json("test_data/single_loggings_response.json")
        return [single_response.copy() for i in range(limit)]

    @staticmethod
    def create_response_with_duplicates(request_time, limit, number_of_different_time, id_to_start_from):
        """
        Creates response with different requestTime and ids.
        Args:
            request_time: request time to start from.
            limit: number of responses
            number_of_different_time: number of responses with different requestTime
            id_to_start_from: id to start from

        """
        single_response = util_load_json("test_data/single_loggings_response.json")
        request_time_date_time = datetime.strptime(request_time, DATE_FORMAT)
        output = []

        def create_single(single_response, time, id, output):
            single_response["requestTime"] = time
            single_response["taskId"] = str(id)
            output.append(single_response)
            id += 1
            return output, id

        for _i in range(limit - number_of_different_time):
            output, id_to_start_from = create_single(single_response.copy(), request_time, id_to_start_from, output)
        for _i in range(limit - number_of_different_time, limit):
            new_time = datetime.strftime(request_time_date_time + timedelta(seconds=10), DATE_FORMAT)
            output, id_to_start_from = create_single(single_response.copy(), new_time, id_to_start_from, output)
        return output

    @pytest.mark.parametrize("loggings_to_fetch", [1, 4, 6], ids=["Single", "Part", "All"])
    def test_get_max_fetch_activity_logging(self, loggings_to_fetch, requests_mock, mocker):
        """
        Given: number of logging to fetch.
        When: running get activity logging command or fetch.
        Then: return the correct number of loggings.

        """
        from WorkdayEventCollector import get_max_fetch_activity_logging

        mocker.patch.object(Client, "get_activity_logging_request", side_effect=self.create_response_by_limit)
        res = get_max_fetch_activity_logging(
            client=self.client,
            logging_to_fetch=loggings_to_fetch,
            from_date="2023-04-15T07:00:00.000Z",
            to_date="2023-04-16T07:00:00.000Z",
        )
        assert len(res) == loggings_to_fetch

    def test_get_max_fetch_activity_logging_paginates_above_page_size(self, mocker):
        """
        Given: a max_fetch (logging_to_fetch) larger than the per-call page cap (3 * MAX_PAGE_SIZE).
        When: running get_max_fetch_activity_logging.
        Then: the client is called multiple times (pagination), each call is capped at MAX_PAGE_SIZE,
              the offset advances by the number of events returned, and all events are collected.
        """
        from WorkdayEventCollector import get_max_fetch_activity_logging

        logging_to_fetch = 3 * MAX_PAGE_SIZE

        def response_by_requested_limit(from_date, to_date, offset, limit):
            # Workday returns up to `limit` events (page cap enforced by caller at MAX_PAGE_SIZE).
            single_response = util_load_json("test_data/single_loggings_response.json")
            return [single_response.copy() for _ in range(limit)]

        request_mock = mocker.patch.object(Client, "get_activity_logging_request", side_effect=response_by_requested_limit)

        res = get_max_fetch_activity_logging(
            client=self.client,
            logging_to_fetch=logging_to_fetch,
            from_date="2023-04-15T07:00:00.000Z",
            to_date="2023-04-16T07:00:00.000Z",
        )

        # logging_to_fetch / MAX_PAGE_SIZE => exactly 3 paginated API calls.
        assert request_mock.call_count == 3
        # Every call must be capped at the per-call page limit.
        assert [call.kwargs["limit"] for call in request_mock.call_args_list] == [MAX_PAGE_SIZE, MAX_PAGE_SIZE, MAX_PAGE_SIZE]
        # Offset must advance by the number of events collected so far.
        assert [call.kwargs["offset"] for call in request_mock.call_args_list] == [0, MAX_PAGE_SIZE, 2 * MAX_PAGE_SIZE]
        # All events are accumulated across the pages.
        assert len(res) == logging_to_fetch

    def test_get_max_fetch_activity_logging_stops_on_empty_page(self, mocker):
        """
        Given: a large max_fetch but the source runs out of events mid-pagination.
        When: running get_max_fetch_activity_logging.
        Then: pagination stops as soon as an empty page is returned (no infinite loop),
              and only the available events are collected.
        """
        from WorkdayEventCollector import get_max_fetch_activity_logging

        single_response = util_load_json("test_data/single_loggings_response.json")
        first_page = [single_response.copy() for _ in range(MAX_PAGE_SIZE)]
        second_page = [single_response.copy() for _ in range(MAX_PAGE_SIZE // 2)]
        request_mock = mocker.patch.object(
            Client,
            "get_activity_logging_request",
            side_effect=[first_page, second_page, []],
        )

        res = get_max_fetch_activity_logging(
            client=self.client,
            logging_to_fetch=3 * MAX_PAGE_SIZE,
            from_date="2023-04-15T07:00:00.000Z",
            to_date="2023-04-16T07:00:00.000Z",
        )

        available_events = MAX_PAGE_SIZE + MAX_PAGE_SIZE // 2
        # Third call returns empty -> loop breaks. Only the available events are collected.
        assert request_mock.call_count == 3
        assert [call.kwargs["offset"] for call in request_mock.call_args_list] == [0, MAX_PAGE_SIZE, available_events]
        assert len(res) == available_events

    def test_resolve_max_fetch_uses_provided_value(self):
        """
        Given: a max_fetch value is provided in the params.
        When: running resolve_max_fetch.
        Then: the provided value is returned (as an int).
        """
        from WorkdayEventCollector import resolve_max_fetch

        assert resolve_max_fetch({"max_fetch": "500"}) == 500

    @pytest.mark.parametrize("params", [{}, {"max_fetch": None}, {"max_fetch": ""}], ids=["missing", "none", "empty"])
    def test_resolve_max_fetch_falls_back_to_default(self, params):
        """
        Given: max_fetch is missing, None, or empty in the params.
        When: running resolve_max_fetch.
        Then: DEFAULT_MAX_FETCH is returned.
        """
        from WorkdayEventCollector import resolve_max_fetch

        assert resolve_max_fetch(params) == DEFAULT_MAX_FETCH

    DUPLICATED_ACTIVITY_LOGGINGS = [
        (("2023-04-15T07:00:00Z", 5, 2, 0), 5, {}),
        (("2023-04-15T07:00:00Z", 5, 1, 0), 4, {"last_log": 0}),
    ]

    @pytest.mark.parametrize("args, len_of_activity_loggings, last_run", DUPLICATED_ACTIVITY_LOGGINGS)
    def test_remove_duplicated_activity_logging(self, args, len_of_activity_loggings, last_run):
        """
        Given: responses with potential duplications from last fetch.
        When: running fetch command.
        Then: return last responses with the latest requestTime to check if there are duplications.

        """
        from WorkdayEventCollector import remove_duplications

        loggings = self.create_response_with_duplicates(*args)
        if "last_log" in last_run:
            last_run["last_log"] = loggings[last_run.get("last_log")]
        activity_loggings = remove_duplications(loggings, last_run)
        assert len(activity_loggings) == len_of_activity_loggings

    def test_remove_milliseconds_from_time_of_logging(self):
        """
        Given: loggings with time string with milliseconds
        When: Fetching loggings from Workday
        Then: Remove the milliseconds.

        """
        from WorkdayEventCollector import remove_milliseconds_from_time_of_logging

        activity_logging: dict = util_load_json("test_data/single_loggings_response.json")
        requests_time = "2023-04-24T07:00:00.123Z"
        final_time = "2023-04-24T07:00:00Z"
        activity_logging["requestTime"] = requests_time

        assert remove_milliseconds_from_time_of_logging(activity_logging) == final_time

    def test_get_activity_logging_command(self, mocker):
        """
        Given: params to run get_activity_logging_command
        When: running the command
        Then: Accurate response and readable output is returned.
        """
        from WorkdayEventCollector import get_activity_logging_command

        mocker.patch.object(Client, "get_activity_logging_request", side_effect=self.create_response_by_limit)
        activity_loggings, res = get_activity_logging_command(
            client=self.client, from_date="2023-04-15T07:00:00Z", to_date="2023-04-16T07:00:00Z", limit=4, offset=0
        )
        assert len(activity_loggings) == 4
        assert "Activity Logging List" in res.readable_output

    @freeze_time("2023-04-15 08:00:00")
    def test_fetch_activity_logging(self, mocker):
        """
        Tests the fetch_events function

        Given:
            - first_fetch_time
        When:
            - Running the 'fetch_activity_logging' function.
        Then:
            - Validates that the function generates the correct API requests with the expected parameters.
            - Validates that the function returns the expected events and next_run timestamps.
        """
        from WorkdayEventCollector import fetch_activity_logging

        first_fetch_time = datetime.strptime("2023-04-12T07:00:00Z", DATE_FORMAT)
        fetched_events = util_load_json("test_data/fetch_activity_loggings.json")
        http_responses = mocker.patch.object(
            Client,
            "get_activity_logging_request",
            side_effect=[
                fetched_events.get("fetch_loggings_before"),
                fetched_events.get("fetch_loggings"),
            ],
        )

        activity_loggings, new_last_run = fetch_activity_logging(
            self.client, last_run={}, first_fetch=first_fetch_time, max_fetch=3
        )

        assert http_responses.call_args_list[0][1] == {
            "limit": 3,
            "offset": 0,
            "from_date": "2023-04-12T07:00:00Z",
            "to_date": "2023-04-15T08:00:00Z",
        }
        assert http_responses.call_args_list[1][1] == {
            "limit": 2,
            "offset": 1,
            "from_date": "2023-04-12T07:00:00Z",
            "to_date": "2023-04-15T08:00:00Z",
        }

        assert activity_loggings == fetched_events.get("fetched_events")
        assert new_last_run.get("last_fetch_time") == "2023-04-15T07:00:00Z"
        assert new_last_run.get("last_log").get("taskId") == "3"

        # assert no new results when given the last_run:
        fetched_events = util_load_json("test_data/fetch_activity_loggings.json")
        http_responses = mocker.patch.object(
            Client, "get_activity_logging_request", side_effect=[fetched_events.get("fetch_loggings"), []]
        )

        activity_loggings, new_last_run = fetch_activity_logging(
            self.client, last_run=new_last_run, first_fetch=first_fetch_time, max_fetch=3
        )
        assert http_responses.call_args_list[0][1] == {
            "limit": 3,
            "offset": 0,
            "from_date": "2023-04-15T07:00:00Z",
            "to_date": "2023-04-15T08:00:00Z",
        }
        assert activity_loggings == []
        assert new_last_run.get("last_fetch_time") == "2023-04-15T07:00:00Z"
        assert new_last_run.get("last_log").get("taskId") == "3"

    @pytest.mark.parametrize("max_fetch, instance_returned", [(6000, 1), (15000, 2), (60000, 6)])
    def test_instance_returned_request(self, mocker, max_fetch, instance_returned):
        self.client.max_fetch = max_fetch
        http_request = mocker.patch.object(Client, "http_request")
        self.client.get_activity_logging_request(from_date="2023-04-15T07:00:00Z", to_date="2023-04-15T08:00:00Z")
        params_sent = http_request.call_args_list[0][1].get("params", {})
        assert params_sent.get("instancesReturned") == instance_returned