Workday Event Collector
Use Workday Event Collector integration to get activity loggings from Workday.
Analytics & SIEM · Workday
Details
| ID | Workday Event Collector |
|---|---|
| Provider | Workday |
| Category | Analytics & SIEM |
| From Version | 8.2.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | Agentix 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 settingsfirst_fetch— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)max_fetch— Max events per fetcheventFetchInterval— Events Fetch Interval
Commands (1)
-
workday-get-activity-loggingReturns 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()