Workday IAM
Use the Workday IAM Integration as part of the IAM premium pack.
Identity and Access Management · Workday
Details
| ID | Workday IAM |
|---|---|
| Provider | Workday |
| Category | Identity and Access Management |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM Cloud Posture Security |
README
Use the Workday IAM Integration as part of the IAM premium pack.
For more information, please refer to the Identity Lifecycle Management article.
Configure Workday IAM in Cortex
| Parameter | Description | Required |
|---|---|---|
| Username | False | |
| Password | False | |
| Workday Report URL | True | |
| Fetch Limit (Recommended less than 200) | False | |
| Fetch incidents | Whether or not to fetch events from Workday report. Enable only when all required configurations are set properly. | False |
| Incident type | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Mapper (incoming) | Used to map Workday report entries to XSOAR indicators format. | False |
| Sync user profiles on first run | If checked, the first fetch won’t trigger incidents but all of the User Profile indicators will be created. | False |
| Fetch Samples | If checked, the fetch incidents command will sample (at most) five incidents. Use only to sample incidents for classification & mapping. | False |
| Date Format in Workday Report | False | |
| Deactivation date field | Select the field that determines when to trigger a termination incident for deactivated employees. | False |
| Number of days before hire date to sync hires | Determines when employees are synced from Workday, i.e., when are the User Profile in XSOAR, and the users in the applications, created. Set to 0 to sync hires on their hire date. Leave empty to sync the hires immediately. | False |
| Number of days before hire date to enable Active Directory account | Determines when to enable the Active Directory accounts of employees. Set to 0 to enable the Active Directory accounts on their hire date. Leave empty to enable the accounts immediately. Note that this is effective only when the employees are already synced to XSOAR, so you should set a number lower, or equal to, the value in the *Number of days before hire date to sync hires* parameter. | False |
| Source Priority Level | Events will be fetched only for User Profiles with a Source Priority value less than or equal to the value of this parameter. | 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-iam-get-full-report
Gets the report entries from Workday.
Base Command
workday-iam-get-full-report
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| WorkdayIAM.ReportEntry.email | String | Email address of the employee in Workday. |
| WorkdayIAM.ReportEntry.employeeid | String | Employee ID in Workday. |
| WorkdayIAM.ReportEntry.username | String | Username of the employee in Workday. |
| WorkdayIAM.ReportEntry.displayname | String | Display name of the employee. |
| WorkdayIAM.ReportEntry.locationregion | String | Location of the employee in Workday. |
Command Example
!workday-iam-get-full-report
Human Readable Output
Results
| city | costcenter | costcentercode | countryname | department | displayname | employeeid | employeetype | employmentstatus | givenname | hiredate | jobcode | jobfamily | jobfunction | lastdayofwork | leadership | location | locationregion | manageremailaddress | personalemail | prehireflag | rehiredemployee | sourceoftruth | sourcepriority | state | streetaddress | surname | terminationdate | title | username | zipcode | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Santa Clara | Channel Sales | 120100 | United States Of America | Sales NAM:NAM Channel Sales | Ronny Rahardjo | rrahardjo@test.com | 100122 | Regular | Ronny | 03/25/2021 | 2245 | Product Management | Product Management Function | 02/15/2032 | Yes-HQ | Office - USA - CA - Headquarters | Americas | test@test.com | ronnyrahardjo@test.com | True | Yes | Workday IAM | 1 | California | 3000 Tannery Way | Rahardjo | 02/15/2032 | Dir, Product Line Manager | rrahardjo@test.com | 95054 | |
| San Francisco | Magnifier Sales Inc | 101100 | United States Of America | WW Sales Functions:Cortex Sales | Steve Testman | sarnold@test.com | 101351 | Regular | Stephen | 03/26/2021 | 2163 | Software Engineering | Engineering Function | 02/15/2032 | No | Office - USA - CA - San Francisco | Americas | test@test.com | stevearnoldtstc@test.com | True | Yes | Workday IAM | 1 | California | WeWork Embarcadero Center | Arnold | 02/15/2032 | Mgr, SW Engineering | sarnold@test.com | 94111 |
Configuration parameters
credentials— Usernamereport_url— Workday Report URL (required)max_fetch— Fetch Limit (Recommended less than 200)isFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsmapper_in— Mapper (incoming)first_run— Sync user profiles on first runfetch_samples— Fetch Samplesworkday_date_format— Date Format in Workday Reportdeactivation_date_field— Deactivation date fielddays_before_hire_to_sync— Number of days before hire date to sync hiresdays_before_hire_to_enable_ad— Number of days before hire date to enable Active Directory accountsource_priority— Source Priority Level
Commands (1)
-
workday-iam-get-full-reportGets the report entries from Workday.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 """ IMPORTS """ import re import traceback import dateparser import requests import urllib3 EMAIL_ADDRESS_FIELD = "email" EMPLOYEE_ID_FIELD = "employeeid" IS_PROCESSED_FIELD = "isprocessed" DISPLAY_NAME_FIELD = "displayname" LAST_DAY_OF_WORK_FIELD = "lastdayofwork" TERMINATION_DATE_FIELD = "terminationdate" TERMINATION_TRIGGER_FIELD = "terminationtrigger" EMPLOYMENT_STATUS_FIELD = "employmentstatus" PREHIRE_FLAG_FIELD = "prehireflag" REHIRED_EMPLOYEE_FIELD = "rehiredemployee" HIRE_DATE_FIELD = "hiredate" AD_ACCOUNT_STATUS_FIELD = "adaccountstatus" OLD_USER_DATA_FIELD = "olduserdata" SOURCE_PRIORITY_FIELD = "sourcepriority" SOURCE_OF_TRUTH_FIELD = "sourceoftruth" CONVERSION_HIRE_FIELD = "conversionhire" USER_PROFILE_INC_FIELD = "UserProfile" USER_PROFILE_INDICATOR = "User Profile" DATE_FORMAT = "%m/%d/%Y" READ_TIME_OUT_IN_SECONDS = 300 DEFAULT_MAPPER_IN = "IAM Sync User - Workday" BATCH_SIZE = 2000 NEW_HIRE_EVENT_TYPE = "IAM - New Hire" UPDATE_USER_EVENT_TYPE = "IAM - Update User" REHIRE_USER_EVENT_TYPE = "IAM - Rehire User" TERMINATE_USER_EVENT_TYPE = "IAM - Terminate User" ACTIVATE_AD_EVENT_TYPE = "IAM - AD User Activation" DEACTIVATE_AD_EVENT_TYPE = "IAM - AD User Deactivation" DEFAULT_INCIDENT_TYPE = "IAM - Sync User" # Disable insecure warnings urllib3.disable_warnings() class Client(BaseClient): """ Client will implement the service API, should not contain Cortex XSOAR logic. Should do requests and return data """ # Getting Workday Full User Report with a given report URL. This uses RaaS def get_full_report(self, report_url): res = self._http_request(method="GET", full_url=report_url, url_suffix="", timeout=READ_TIME_OUT_IN_SECONDS) return res.get("Report_Entry", []) """ HELPER FUNCTIONS """ def report_to_indicators( report_entries, mapper_in, workday_date_format, deactivation_date_field, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ): indicators = [] email_to_user_profile: Dict[str, Dict] = {} for entry in report_entries: workday_user = get_workday_user_from_entry(entry, mapper_in, workday_date_format, source_priority) if ( not has_reached_threshold_date(days_before_hire_to_sync, workday_user) or new_hire_email_already_taken(workday_user, None, email_to_user_profile) or is_report_missing_required_user_data(workday_user) or is_termination_event(workday_user, None, deactivation_date_field, first_run=True) ): continue indicator = workday_user_to_indicator(workday_user, days_before_hire_to_enable_ad) indicators.append(indicator) email_to_user_profile[workday_user.get(EMAIL_ADDRESS_FIELD)] = indicator return indicators def workday_user_to_indicator(workday_user, days_before_hire_to_enable_ad): if not has_reached_threshold_date(days_before_hire_to_enable_ad, workday_user): workday_user[AD_ACCOUNT_STATUS_FIELD] = "Pending" raw_json = workday_user.copy() raw_json["value"] = workday_user.get(EMAIL_ADDRESS_FIELD) raw_json["type"] = USER_PROFILE_INDICATOR indicator = { "value": workday_user.get(EMAIL_ADDRESS_FIELD), "type": USER_PROFILE_INDICATOR, "rawJSON": raw_json, "fields": workday_user, } return indicator def convert_incident_fields_to_cli_names(data): converted_data = {} for k, v in data.items(): key_machine_name = re.sub(r"[^a-zA-Z0-9]", "", k).lower() converted_data[key_machine_name] = v return converted_data def reformat_date_fields(user_profile, workday_date_format): for date_field in [TERMINATION_DATE_FIELD, LAST_DAY_OF_WORK_FIELD, HIRE_DATE_FIELD]: if user_profile.get(date_field): datetime_obj = datetime.strptime(user_profile[date_field], workday_date_format) user_profile[date_field] = datetime_obj.strftime(DATE_FORMAT) def get_workday_user_from_entry(entry, mapper_in, workday_date_format, source_priority): workday_user = demisto.mapObject(entry, mapper_in, DEFAULT_INCIDENT_TYPE) workday_user = convert_incident_fields_to_cli_names(workday_user) reformat_date_fields(workday_user, workday_date_format) workday_user[SOURCE_PRIORITY_FIELD] = source_priority workday_user[SOURCE_OF_TRUTH_FIELD] = "Workday IAM" return workday_user def has_reached_threshold_date(num_of_days_before_hire, workday_user): if not num_of_days_before_hire and num_of_days_before_hire != 0: return True hire_date = dateparser.parse(workday_user.get(HIRE_DATE_FIELD)) assert hire_date is not None hire_date = hire_date.date() today = datetime.today().date() delta = (hire_date - today).days if delta > num_of_days_before_hire: demisto.debug( f"Skipped creating an incident for user " f"with email address {workday_user.get(EMAIL_ADDRESS_FIELD)} - " f"the hire date of the employee is in more than {num_of_days_before_hire} days." ) return False return True def new_hire_email_already_taken(workday_user, demisto_user, email_to_user_profile): if demisto_user is None and email_to_user_profile.get(workday_user.get(EMAIL_ADDRESS_FIELD)) is not None: demisto.debug( f"Skipped creating a user profile for the following employee:\n{workday_user}\n\n" f"The user profile doesn't exist but its email is already being used by another user." ) return True return False def is_report_missing_required_user_data(workday_user): if ( not workday_user.get(EMAIL_ADDRESS_FIELD) or not workday_user.get(EMPLOYEE_ID_FIELD) or not workday_user.get(HIRE_DATE_FIELD) ): demisto.debug( f"Skipped creating an incident for the following user profile:\n{workday_user}\n\n" f"The user profile does not contain email address/employee ID/hire date, " f"to fix please add the missing data to the report." ) return True return False def is_tufe_user(demisto_user): if demisto_user is not None and demisto_user.get(TERMINATION_TRIGGER_FIELD) == "TUFE": demisto.debug(f"Dropping event for user with email {demisto_user.get(EMAIL_ADDRESS_FIELD)} as it is a TUFE user.") return True return False def is_event_processed(demisto_user): if demisto_user is not None and demisto_user.get(IS_PROCESSED_FIELD) is True: demisto.debug( f"Dropping event for user with email {demisto_user.get(EMAIL_ADDRESS_FIELD)} as it is currently being processed." ) return True return False def is_termination_event(workday_user, demisto_user, deactivation_date_field, first_run=False): if not first_run and (demisto_user is None or demisto_user.get(AD_ACCOUNT_STATUS_FIELD) == "Disabled"): # skipping termination check - user does not exist or already terminated return False prehire_flag = workday_user.get(PREHIRE_FLAG_FIELD, "").lower() == "true" employment_status = workday_user.get(EMPLOYMENT_STATUS_FIELD, "").lower() if deactivation_date := workday_user.get(deactivation_date_field): deactivation_date = dateparser.parse(workday_user.get(deactivation_date_field)) today = datetime.today() if (employment_status == "terminated" and prehire_flag is False) or (deactivation_date and deactivation_date <= today): demisto.debug(f"A termination event was detected for user with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}.") return True return False def is_display_name_already_taken(demisto_user, workday_user, display_name_to_user_profile): user_display_name = workday_user.get(DISPLAY_NAME_FIELD) demisto_users_by_display_name = display_name_to_user_profile.get(user_display_name) if demisto_users_by_display_name is None: return False if demisto_user is None: demisto.debug( f"Detected a potential new hire for user with email address " f"{workday_user.get(EMAIL_ADDRESS_FIELD)}, but display name is already taken. " f"Please review the incident." ) return True display_name_is_taken_by_another_user = False is_display_name_change = True for user in demisto_users_by_display_name: if user.get(EMPLOYEE_ID_FIELD) == demisto_user.get(EMPLOYEE_ID_FIELD): # user already exists with this display name in XSOAR is_display_name_change = False if ( user.get(EMPLOYEE_ID_FIELD) != demisto_user.get(EMPLOYEE_ID_FIELD) and user.get(AD_ACCOUNT_STATUS_FIELD).lower() != "terminated" ): display_name_is_taken_by_another_user = True if display_name_is_taken_by_another_user and is_display_name_change: demisto.debug( f"Detected an event for user with email address " f"{workday_user.get(EMAIL_ADDRESS_FIELD)}, but its display name is already taken. " f"Please review the incident." ) return True return False def is_new_hire_event(demisto_user, workday_user, deactivation_date_field): if demisto_user is not None: return False prehire_flag = workday_user.get(PREHIRE_FLAG_FIELD, "").lower() == "true" employment_status = workday_user.get(EMPLOYMENT_STATUS_FIELD, "") if prehire_flag is True and not employment_status: demisto.debug(f"A new hire event was detected for user with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}.") return True if deactivation_date := workday_user.get(deactivation_date_field): deactivation_date = dateparser.parse(workday_user.get(deactivation_date_field)) today = datetime.today() if employment_status.lower() != "terminated" and (not deactivation_date or today <= deactivation_date): demisto.debug( f"A non-terminated user with an email address {workday_user.get(EMAIL_ADDRESS_FIELD)} " f"was not found in XSOAR, even though a pre-hire was not detected. Syncing anyway." ) return True return False def is_rehire_event(demisto_user, workday_user, changed_fields): if demisto_user is None or demisto_user.get(AD_ACCOUNT_STATUS_FIELD) != "Disabled": # skipping rehire check - user does not exist or already active/pending return False prehire_flag = workday_user.get(PREHIRE_FLAG_FIELD, "").lower() == "true" is_rehired_employee = workday_user.get(REHIRED_EMPLOYEE_FIELD, "").lower() == "yes" if prehire_flag is True and is_rehired_employee and changed_fields is not None: demisto.debug(f"A rehire event was detected for user with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}.") return True return False def is_ad_activation_event(demisto_user, workday_user, days_before_hire_to_enable_ad): if ( demisto_user and demisto_user.get(AD_ACCOUNT_STATUS_FIELD, "") == "Pending" and has_reached_threshold_date(days_before_hire_to_enable_ad, workday_user) ): demisto.debug( f"An Active Directory activation event was detected for user " f"with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}." ) return True return False def is_ad_deactivation_event(demisto_user, workday_user, days_before_hire_to_enable_ad, source_priority): """ Checks whether the event is IAM - Deactivate User in Active Directory. Note: To avoid misdetection of deactivation events for conversion hires, we check that: 1. The current SOURCE_PRIORITY_FIELD is Workday's - otherwise it's a conversion hire in its first fetch. 2. CONVERSION_HIRE_FIELD is not True - otherwise it's a conversion hire. Args: demisto_user: The user profile in XSOAR. workday_user: Workday user in XSOAR format. days_before_hire_to_enable_ad: Number of days before hire date to enable Active Directory account, `None` if should sync instantly. source_priority: Source priority level. Returns: (bool). True iff the event is an AD deactivation. """ if ( not demisto_user or demisto_user.get(SOURCE_PRIORITY_FIELD) != source_priority or demisto_user.get(CONVERSION_HIRE_FIELD) is True ): return False if demisto_user.get(AD_ACCOUNT_STATUS_FIELD, "") == "Enabled" and not has_reached_threshold_date( days_before_hire_to_enable_ad, workday_user ): demisto.debug( f"An Active Directory deactivation event was detected for user " f"with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}." ) return True return False def is_update_event(workday_user, changed_fields): if changed_fields and workday_user.get(EMPLOYMENT_STATUS_FIELD, "").lower() != "terminated": demisto.debug(f"An update event was detected for user with email address {workday_user.get(EMAIL_ADDRESS_FIELD)}.") return True return False def get_all_user_profiles(): query = f'type:"{USER_PROFILE_INDICATOR}"' display_name_to_user_profile: Dict[str, List[Dict]] = {} employee_id_to_user_profile: Dict[str, Dict] = {} email_to_user_profile: Dict[str, Dict] = {} def handle_batch(user_profiles): for user_profile in user_profiles: user_profile = user_profile.get("CustomFields", {}) display_name = user_profile.get(DISPLAY_NAME_FIELD) employee_id = user_profile.get(EMPLOYEE_ID_FIELD) email = user_profile.get(EMAIL_ADDRESS_FIELD) display_name_to_user_profile.setdefault(display_name, []).append(user_profile) employee_id_to_user_profile[employee_id] = user_profile email_to_user_profile[email] = user_profile search_indicators = IndicatorsSearcher(query=query, size=BATCH_SIZE) for ioc_res in search_indicators: fetched_iocs = ioc_res.get("iocs") or [] handle_batch(fetched_iocs) return display_name_to_user_profile, employee_id_to_user_profile, email_to_user_profile def get_demisto_user(email_to_user_profile, employee_id_to_user_profile, workday_user): demisto_user = None if employee_id := workday_user.get(EMPLOYEE_ID_FIELD): demisto_user = employee_id_to_user_profile.get(employee_id) if not demisto_user and (email := workday_user.get(EMAIL_ADDRESS_FIELD)): demisto_user = email_to_user_profile.get(email) return demisto_user def get_orphan_users(email_to_user_profile, user_emails, source_priority): """Gets all users that don't exist in the Workday report anymore and terminate them in XSOAR.""" events = [] orphan_users = [ email for email, user in email_to_user_profile.items() if email not in user_emails and user.get(EMPLOYMENT_STATUS_FIELD) != "Terminated" and user.get(SOURCE_PRIORITY_FIELD) == source_priority and user.get(IS_PROCESSED_FIELD) is False ] if orphan_users: demisto.debug(f"Found orphan users: {orphan_users}") for email in orphan_users: email_to_user_profile[email][EMPLOYMENT_STATUS_FIELD] = "Terminated" entry = { "Email_Address": email, "UserProfile": email_to_user_profile[email], "Emp_ID": email_to_user_profile[email].get(EMPLOYEE_ID_FIELD), } event = { "name": email, "rawJSON": json.dumps(entry), "type": DEFAULT_INCIDENT_TYPE, "details": "An orphan user was detected (could not find the user in Workday report). " "Please review and terminate if necessary.", } events.append(event) return events def get_profile_changed_fields_str(demisto_user, workday_user): if not demisto_user: return None profile_changed_fields = [] for field, workday_value in workday_user.items(): if (workday_value and not demisto_user.get(field)) or workday_value != demisto_user.get(field): profile_changed_fields.append([field, workday_value]) return "\n".join([f'{field[0]} field was updated to "{field[1]}".' for field in profile_changed_fields]) def is_valid_source_of_truth(demisto_user, source_priority): if demisto_user is not None and demisto_user.get(SOURCE_PRIORITY_FIELD, 1) < source_priority: demisto.debug( f"Skipped creating an incident for user profile {demisto_user.get(EMAIL_ADDRESS_FIELD)}: " f"The user profile in XSOAR has a higher source priority level." ) return False return True def get_event_details( entry, workday_user, demisto_user, days_before_hire_to_sync, days_before_hire_to_enable_ad, deactivation_date_field, display_name_to_user_profile, email_to_user_profile, employee_id_to_user_profile, source_priority, ): """ This function detects the event type and creates a dictionary which holds the event details. If the event should not be created, None is returned. Args: entry: The employee's report entry. workday_user: Workday user in XSOAR format. demisto_user: The user profile in XSOAR. deactivation_date_field: Deactivation date field - "lastdayofwork" or "terminationdate". days_before_hire_to_sync: Number of days before hire date to sync hires, -1 if should sync instantly. days_before_hire_to_enable_ad: Number of days before hire date to enable Active Directory account, -1 if should sync instantly. display_name_to_user_profile: A dictionary that maps display names to user profile indicators in XSOAR. email_to_user_profile: A dictionary that maps email addresses to user profile indicators in XSOAR. employee_id_to_user_profile: A dictionary that maps employee ids to user profile indicators in XSOAR. source_priority: The source priority number. Returns: event: The event details. """ user_email = workday_user.get(EMAIL_ADDRESS_FIELD) changed_fields = get_profile_changed_fields_str(demisto_user, workday_user) demisto.debug(f"{changed_fields=}") # type: ignore if ( not has_reached_threshold_date(days_before_hire_to_sync, workday_user) or new_hire_email_already_taken(workday_user, demisto_user, email_to_user_profile) or is_report_missing_required_user_data(workday_user) or not is_valid_source_of_truth(demisto_user, source_priority) or is_event_processed(demisto_user) ): return None if is_new_hire_event(demisto_user, workday_user, deactivation_date_field): event_type = NEW_HIRE_EVENT_TYPE event_details = "The user has been hired." elif is_ad_activation_event(demisto_user, workday_user, days_before_hire_to_enable_ad): event_type = ACTIVATE_AD_EVENT_TYPE event_details = "Active Directory user account was enabled." elif is_ad_deactivation_event(demisto_user, workday_user, days_before_hire_to_enable_ad, source_priority): event_type = DEACTIVATE_AD_EVENT_TYPE event_details = "Active Directory user account was disabled due to hire date postponement." elif is_rehire_event(demisto_user, workday_user, changed_fields): event_type = REHIRE_USER_EVENT_TYPE event_details = "The user has been rehired." elif is_termination_event(workday_user, demisto_user, deactivation_date_field): event_type = TERMINATE_USER_EVENT_TYPE event_details = "The user has been terminated." elif is_update_event(workday_user, changed_fields): event_type = UPDATE_USER_EVENT_TYPE event_details = f"The user has been updated:\n{changed_fields}" workday_user[OLD_USER_DATA_FIELD] = demisto_user if demisto_user.get(SOURCE_PRIORITY_FIELD) != source_priority: workday_user[CONVERSION_HIRE_FIELD] = True event_details = f"A conversion hire was detected:\n{changed_fields}" else: demisto.debug(f"Could not detect changes in report for user with email address {user_email} - skipping.") return None if is_tufe_user(demisto_user) and event_type != REHIRE_USER_EVENT_TYPE: return None if is_display_name_already_taken(demisto_user, workday_user, display_name_to_user_profile) and event_type in [ NEW_HIRE_EVENT_TYPE, REHIRE_USER_EVENT_TYPE, UPDATE_USER_EVENT_TYPE, ]: event_details = f'Detected an "{event_type}" event, but display name already exists. Please review.' if changed_fields: event_details += f"\n{changed_fields}" event_type = DEFAULT_INCIDENT_TYPE entry[USER_PROFILE_INC_FIELD] = workday_user return {"name": user_email, "rawJSON": json.dumps(entry), "type": event_type, "details": event_details} """ INTEGRATION COMMANDS """ def fetch_samples(client, mapper_in, report_url, workday_date_format): """ This function returns a list of (at most) five sample events (used for classification and mapping only). Args: client: Workday client mapper_in: Incoming mapper's name report_url: The report full URL workday_date_format: Date format in Workday Returns: events: Incidents/events that will be used as samples for classification and mapping. """ events = [] num_of_samples = 5 try: report_entries = client.get_full_report(report_url) num_of_samples = min(num_of_samples, len(report_entries)) report_entries = report_entries[:num_of_samples] for entry in report_entries: workday_user = demisto.mapObject(entry, mapper_in, DEFAULT_INCIDENT_TYPE) workday_user = convert_incident_fields_to_cli_names(workday_user) reformat_date_fields(workday_user, workday_date_format) entry["UserProfile"] = workday_user event = { "name": workday_user.get(EMAIL_ADDRESS_FIELD), "rawJSON": json.dumps(entry), "details": "This is a sample event.", } events.append(event) except Exception as e: demisto.error("Failed to fetch events. Reason: " + str(e)) raise e return events def get_full_report_command(client, mapper_in, report_url, workday_date_format, source_priority): report_entries = client.get_full_report(report_url) outputs = [get_workday_user_from_entry(entry, mapper_in, workday_date_format, source_priority) for entry in report_entries] results = CommandResults(outputs_prefix="WorkdayIAM.ReportEntry", outputs_key_field=EMPLOYEE_ID_FIELD, outputs=outputs) return results def fetch_incidents( client, mapper_in, report_url, workday_date_format, deactivation_date_field, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ): """ This function will execute each interval (default is 1 minute). Args: client: Workday client mapper_in: Incoming mapper's name report_url: The report full URL. workday_date_format: Date format in Workday report. deactivation_date_field: Deactivation date field - "lastdayofwork" or "terminationdate". days_before_hire_to_sync: Number of days before hire date to sync hires, `None` if should sync instantly. days_before_hire_to_enable_ad: Number of days before hire date to enable Active Directory account, `None` if should sync instantly. source_priority: Source priority level. Returns: events: Incidents/Events that will be created in Cortex XSOAR """ events = [] user_emails = [] try: display_name_to_user_profile, employee_id_to_user_profile, email_to_user_profile = get_all_user_profiles() report_entries = client.get_full_report(report_url) for entry in report_entries: # get the user event (if exists) according to workday report workday_user = get_workday_user_from_entry(entry, mapper_in, workday_date_format, source_priority) demisto_user = get_demisto_user(email_to_user_profile, employee_id_to_user_profile, workday_user) demisto.debug( f'Getting event details for user with email address {workday_user.get("email")}.\n' f'Current user data in XSOAR: {demisto_user=}\nData in Workday: {workday_user=}' ) event = get_event_details( entry, workday_user, demisto_user, days_before_hire_to_sync, days_before_hire_to_enable_ad, deactivation_date_field, display_name_to_user_profile, email_to_user_profile, employee_id_to_user_profile, source_priority, ) if event is not None: events.append(event) # we store all emails of user profiles for later use if demisto_user is not None: user_emails.append(demisto_user.get(EMAIL_ADDRESS_FIELD)) # terminate users in XSOAR which are not on workday report orphan_users_events = get_orphan_users(email_to_user_profile, user_emails, source_priority) events.extend(orphan_users_events) if not events: demisto.info("Did not detect any changes in Workday report.") except Exception as e: demisto.error("Failed to fetch events. Reason: " + str(e)) raise e return events def workday_first_run_command( client, mapper_in, report_url, workday_date_format, deactivation_date_field, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ): report_entries = client.get_full_report(report_url) indicators = report_to_indicators( report_entries, mapper_in, workday_date_format, deactivation_date_field, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ) for b in batch(indicators, batch_size=BATCH_SIZE): demisto.createIndicators(b) def test_module( client, is_fetch, report_url, mapper_in, workday_date_format, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ): """ Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Anything else will fail the test. """ # test API connectivity client.get_full_report(report_url) if is_fetch: fetch_incidents( client=client, mapper_in=mapper_in, report_url=report_url, workday_date_format=workday_date_format, days_before_hire_to_sync=days_before_hire_to_sync, days_before_hire_to_enable_ad=days_before_hire_to_enable_ad, deactivation_date_field=TERMINATION_DATE_FIELD, source_priority=source_priority, ) return "ok" def main(): command = demisto.command() params = demisto.params() is_fetch = params.get("isFetch") report_url = params.get("report_url") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) mapper_in = params.get("mapper_in", DEFAULT_MAPPER_IN) workday_username = params.get("credentials", {}).get("identifier") workday_password = params.get("credentials", {}).get("password") workday_date_format = params.get("workday_date_format", DATE_FORMAT) deactivation_date_field = params.get("deactivation_date_field").lower().replace("_", "") source_priority = int(params.get("source_priority", "1")) days_before_hire_to_sync = params.get("days_before_hire_to_sync") if days_before_hire_to_sync: days_before_hire_to_sync = int(days_before_hire_to_sync) days_before_hire_to_enable_ad = params.get("days_before_hire_to_enable_ad") if days_before_hire_to_enable_ad: days_before_hire_to_enable_ad = int(days_before_hire_to_enable_ad) demisto.debug(f"Command being called is {command}") client = Client( base_url="", # using report_url in _http_request verify=verify_certificate, headers={"Accept": "application/json", "Content-Type": "application/json"}, proxy=proxy, ok_codes=(200, 204), auth=requests.auth.HTTPBasicAuth(workday_username, workday_password), ) try: if command == "test-module": return_results( test_module( client, is_fetch, report_url, mapper_in, workday_date_format, days_before_hire_to_sync, days_before_hire_to_enable_ad, source_priority, ) ) if command == "workday-iam-get-full-report": return_results(get_full_report_command(client, mapper_in, report_url, workday_date_format, source_priority)) if command == "fetch-incidents": """ Checks if there are events are stored in the integration context. If yes, it gets it from there. Else, it makes a call to Workday to get a new report Returns the first x events (x being the fetch limit) and stores the remaining in integration context """ last_run = demisto.getLastRun() events = last_run.get("events", []) report_url = params.get("report_url") if params.get("fetch_samples") and not last_run.get("fetched_samples"): sample_events = fetch_samples( client=client, mapper_in=mapper_in, report_url=report_url, workday_date_format=workday_date_format ) demisto.incidents(sample_events) demisto.setLastRun({"fetched_samples": True}) else: if not last_run.get("synced_users") and params.get("first_run"): workday_first_run_command( client=client, mapper_in=mapper_in, report_url=report_url, workday_date_format=workday_date_format, deactivation_date_field=deactivation_date_field, days_before_hire_to_sync=days_before_hire_to_sync, days_before_hire_to_enable_ad=days_before_hire_to_enable_ad, source_priority=source_priority, ) elif not events: # Get the events from Workday by making an API call. Last run is updated only when API call is made events = fetch_incidents( client=client, mapper_in=mapper_in, report_url=report_url, workday_date_format=workday_date_format, deactivation_date_field=deactivation_date_field, days_before_hire_to_sync=days_before_hire_to_sync, days_before_hire_to_enable_ad=days_before_hire_to_enable_ad, source_priority=source_priority, ) fetch_limit = int(params.get("max_fetch")) demisto.incidents(events[:fetch_limit]) demisto.setLastRun({"synced_users": True, "events": events[fetch_limit:]}) except Exception as e: return_error(f"Failed to execute {demisto.command()} command, Error: {e}. Traceback: {traceback.format_exc()}") if __name__ in ["__main__", "builtin", "builtins"]: main()