Workday IAM

Use the Workday IAM Integration as part of the IAM premium pack.

Identity and Access Management · Workday

Details

IDWorkday IAM
ProviderWorkday
CategoryIdentity and Access Management
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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 email 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 — Username
  • report_url — Workday Report URL (required)
  • max_fetch — Fetch Limit (Recommended less than 200)
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • mapper_in — Mapper (incoming)
  • first_run — Sync user profiles on first run
  • fetch_samples — Fetch Samples
  • workday_date_format — Date Format in Workday Report
  • deactivation_date_field — Deactivation date field
  • days_before_hire_to_sync — Number of days before hire date to sync hires
  • days_before_hire_to_enable_ad — Number of days before hire date to enable Active Directory account
  • source_priority — Source Priority Level

Commands (1)

  • workday-iam-get-full-report

    Gets the report entries from Workday.

import json

import demistomock as demisto  # noqa: F401
import pytest
from test_data.event_results import events_result
from Workday_IAM import (
    AD_ACCOUNT_STATUS_FIELD,
    CONVERSION_HIRE_FIELD,
    EMAIL_ADDRESS_FIELD,
    EMPLOYEE_ID_FIELD,
    EMPLOYMENT_STATUS_FIELD,
    HIRE_DATE_FIELD,
    LAST_DAY_OF_WORK_FIELD,
    PREHIRE_FLAG_FIELD,
    REHIRE_USER_EVENT_TYPE,
    REHIRED_EMPLOYEE_FIELD,
    SOURCE_PRIORITY_FIELD,
    TERMINATION_TRIGGER_FIELD,
    Client,
    fetch_incidents,
)

EVENT_RESULTS = events_result


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


def test_fetch_incidents(mocker):
    """Unit test
    Given
    - raw response of report results
    When
    - mock the demisto map object
    - mock getting demisto indicators
    Then
    - validate the incidents values
    """
    client_response = util_load_json("test_data/json_raw_response.json")
    mapped_user = util_load_json("test_data/mapped_user.json")

    mocker.patch.object(Client, "get_full_report", return_value=client_response.get("Report_Entry"))
    mocker.patch("Workday_IAM.get_all_user_profiles", return_value=({}, {}, {}))
    mocker.patch.object(demisto, "mapObject", return_value=mapped_user)
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(client, {}, "", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, 1)
    assert fetch_events == EVENT_RESULTS


def test_fetch_incidents_email_change(requests_mock, mocker):
    """
    Given
    - A workday full report of employees.
    When
    - An email address change is detected for the user rrahardj@paloaltonetworks.com.
    Then
    - Make sure the IAM - Update User event is returned as expected.
    """
    from test_data.fetch_incidents_email_change_mock_data import (
        email_to_user_profile,
        employee_id_to_user_profile,
        event_data,
        full_report,
        mapped_workday_user,
    )

    requests_mock.get("https://test.com", json=full_report)
    mocker.patch("Workday_IAM.get_all_user_profiles", return_value=({}, employee_id_to_user_profile, email_to_user_profile))
    mocker.patch.object(demisto, "mapObject", return_value=mapped_workday_user)
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(client, {}, "https://test.com", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, 1)
    assert fetch_events == event_data


def test_fetch_incidents_employee_id_change(requests_mock, mocker):
    """
    Given
    - A workday full report of employees.
    When
    - An employee id change is detected for the user rrahardj@paloaltonetworks.com.
    Then
    - Make sure the IAM - Update User event is returned as expected.
    """
    from test_data.fetch_incidents_employee_id_change_mock_data import (
        email_to_user_profile,
        employee_id_to_user_profile,
        event_data,
        full_report,
        mapped_workday_user,
    )

    requests_mock.get("https://test.com", json=full_report)
    mocker.patch("Workday_IAM.get_all_user_profiles", return_value=({}, employee_id_to_user_profile, email_to_user_profile))
    mocker.patch.object(demisto, "mapObject", return_value=mapped_workday_user)
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(client, {}, "https://test.com", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, 1)
    assert fetch_events == event_data


def test_fetch_incidents_orphan_user(requests_mock, mocker):
    """
    Given
    - An empty workday report of employees.
    When
    - A user profile with email rrahardjo@paloaltonetworks.com exists on XSOAR.
    Then
    - Ensure an IAM - Terminate User event is returned for this user.
    """
    from test_data.fetch_incidents_orphan_user_mock_data import email_to_user_profile, event_data, full_report

    requests_mock.get("https://test.com", json=full_report)
    mocker.patch("Workday_IAM.get_all_user_profiles", return_value=({}, {}, email_to_user_profile))
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(client, {}, "https://test.com", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, 1)
    assert fetch_events == event_data


def test_fetch_incidents_source_priority(requests_mock, mocker):
    """
    Given
    - A workday full report of employees.
    When
    - Workday IAM configured source priority is 2.
    - A user profile with email rrahardjo@paloaltonetworks.com has a source priority 1.
    Then
    - Ensure the event for rrahardjo@paloaltonetworks.com is dropped.
    """
    from test_data.fetch_incidents_source_priority_mock_data import (
        email_to_user_profile,
        employee_id_to_user_profile,
        event_data,
        full_report,
        mapped_workday_user,
    )

    requests_mock.get("https://test.com", json=full_report)
    mocker.patch.object(demisto, "mapObject", return_value=mapped_workday_user)
    mocker.patch("Workday_IAM.get_all_user_profiles", return_value=({}, employee_id_to_user_profile, email_to_user_profile))
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(
        client, {}, "https://test.com", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, source_priority=2
    )
    assert fetch_events == event_data


def test_fetch_incidents_partial_name_match(requests_mock, mocker):
    """
    Given
    - A workday full report of employees.
    When
    - A new hire is detected with the same display name as an existing active user.
    Then
    - Ensure an "IAM - Sync user" event is detected with the partial name match details.
    """
    from test_data.fetch_incidents_partial_name_match_mock_data import (
        display_name_to_user_profile,
        email_to_user_profile,
        employee_id_to_user_profile,
        event_data,
        full_report,
        mapped_workday_user,
    )

    requests_mock.get("https://test.com", json=full_report)
    mocker.patch.object(demisto, "mapObject", return_value=mapped_workday_user)
    mocker.patch(
        "Workday_IAM.get_all_user_profiles",
        return_value=(display_name_to_user_profile, employee_id_to_user_profile, email_to_user_profile),
    )
    mocker.patch("Workday_IAM.get_orphan_users", return_value=[])  # skip the orphan user detection
    client = Client(base_url="", verify="verify", headers={}, proxy=False, ok_codes=(200, 204), auth=None)

    fetch_events = fetch_incidents(client, {}, "https://test.com", "%m/%d/%Y", LAST_DAY_OF_WORK_FIELD, None, None, 1)
    assert fetch_events == event_data


@pytest.mark.parametrize(
    "demisto_user, workday_user, expected_result",
    [
        # a pre-hired employee with no employment status, not synced into XSOAR - should return True
        (None, {PREHIRE_FLAG_FIELD: "True", EMPLOYMENT_STATUS_FIELD: ""}, True),
        # non-terminated, active employee, not synced into XSOAR - should return True
        (None, {EMPLOYMENT_STATUS_FIELD: "active", LAST_DAY_OF_WORK_FIELD: "12/12/2100"}, True),
        # non-empty demisto_user - should return False
        ("mocked_non_empty_demisto_user", "mocked_workday_user", False),
        # non pre-hired, terminated employee - should return False
        (None, {PREHIRE_FLAG_FIELD: "False", EMPLOYMENT_STATUS_FIELD: "Terminated", LAST_DAY_OF_WORK_FIELD: "12/12/2020"}, False),
    ],
)
def test_is_new_hire_event(demisto_user, workday_user, expected_result):
    from Workday_IAM import is_new_hire_event

    assert is_new_hire_event(demisto_user, workday_user, LAST_DAY_OF_WORK_FIELD) == expected_result


@pytest.mark.parametrize(
    "demisto_user, workday_user, expected_result",
    [
        # not a pre-hire, employment status is "terminated" - should return True
        (
            {"mocked_demisto_user": ""},
            {PREHIRE_FLAG_FIELD: "False", EMPLOYMENT_STATUS_FIELD: "Terminated", LAST_DAY_OF_WORK_FIELD: "12/12/2100"},
            True,
        ),
        # non terminated but last day of work is in the past - should return True
        ({"mocked_demisto_user": ""}, {EMPLOYMENT_STATUS_FIELD: "active", LAST_DAY_OF_WORK_FIELD: "12/12/2020"}, True),
        # no demisto_user - should return False
        (None, "mocked_workday_user", False),
        # demisto_user is already disabled in AD - should return False
        ({"adaccountstatus": "Disabled"}, "mocked_workday_user", False),
        # active, non pre-hired user with future last day of work - should return False
        (
            {"mocked_demisto_user": ""},
            {PREHIRE_FLAG_FIELD: "False", EMPLOYMENT_STATUS_FIELD: "Active", LAST_DAY_OF_WORK_FIELD: "12/12/2100"},
            False,
        ),
        # active, pre-hired user with future last day of work - should return False
        (
            {"mocked_demisto_user": ""},
            {PREHIRE_FLAG_FIELD: "True", EMPLOYMENT_STATUS_FIELD: "Active", LAST_DAY_OF_WORK_FIELD: "12/12/2100"},
            False,
        ),
        # pre-hired user with future last day of work - should return False
        (
            {"mocked_demisto_user": ""},
            {PREHIRE_FLAG_FIELD: "True", EMPLOYMENT_STATUS_FIELD: "Terminated", LAST_DAY_OF_WORK_FIELD: "12/12/2100"},
            False,
        ),
    ],
)
def test_is_termination_event(demisto_user, workday_user, expected_result):
    from Workday_IAM import is_termination_event

    assert is_termination_event(workday_user, demisto_user, LAST_DAY_OF_WORK_FIELD) == expected_result


@pytest.mark.parametrize(
    "demisto_user, workday_user, changed_fields, expected_result",
    [
        # a pre-hired + rehired employee, non empty changed_fields - should return True
        (
            {"adaccountstatus": "Disabled"},
            {PREHIRE_FLAG_FIELD: "True", REHIRED_EMPLOYEE_FIELD: "Yes"},
            "mocked_changed_fields",
            True,
        ),
        # a pre-hired + rehired employee, no changed_fields (already synced into XSOAR) - should return False
        ({"adaccountstatus": "Disabled"}, {PREHIRE_FLAG_FIELD: "True", EMPLOYMENT_STATUS_FIELD: ""}, None, False),
        # no demisto_user - should return False
        (None, "mocked_workday_user", None, False),
        # demisto_user AD status is not disabled - should return False
        ({"adaccountstatus": "Pending"}, "mocked_workday_user", None, False),
        # non pre-hired / non rehired employee, non empty changed_fields - should return False
        (
            {"adaccountstatus": "Disabled"},
            {PREHIRE_FLAG_FIELD: "False", REHIRED_EMPLOYEE_FIELD: "No"},
            "mocked_changed_fields",
            False,
        ),
    ],
)
def test_is_rehire_event(demisto_user, workday_user, changed_fields, expected_result):
    from Workday_IAM import is_rehire_event

    assert is_rehire_event(demisto_user, workday_user, LAST_DAY_OF_WORK_FIELD) == expected_result


@pytest.mark.parametrize(
    "demisto_user, workday_user, days_before_hire_to_enable_ad, expected_result",
    [
        # a pending demisto user, workday user exceeded threshold to enable AD - should return True
        ({AD_ACCOUNT_STATUS_FIELD: "Pending"}, {HIRE_DATE_FIELD: "12/12/2020"}, 2, True),
        # a pending demisto user, no threshold to enable AD - should return True
        ({AD_ACCOUNT_STATUS_FIELD: "Pending"}, {}, None, True),
        # no demisto_user - should return False
        (None, "mocked_workday_user", None, False),
        # did not exceed threshold date - should return False
        ({AD_ACCOUNT_STATUS_FIELD: "Pending"}, {HIRE_DATE_FIELD: "12/12/2200"}, 2, False),
        # demisto_user AD status is not pending - should return False
        ({AD_ACCOUNT_STATUS_FIELD: "Disabled"}, "mocked_workday_user", None, False),
    ],
)
def test_is_ad_activation_event(demisto_user, workday_user, days_before_hire_to_enable_ad, expected_result):
    from Workday_IAM import is_ad_activation_event

    assert is_ad_activation_event(demisto_user, workday_user, days_before_hire_to_enable_ad) == expected_result


@pytest.mark.parametrize(
    "demisto_user, workday_user, days_before_hire_to_enable_ad, source_priority, expected_result",
    [
        # source priority field is different than Workday's (conversion hire) - should return False
        ({AD_ACCOUNT_STATUS_FIELD: "Enabled", SOURCE_PRIORITY_FIELD: 2}, {HIRE_DATE_FIELD: "12/12/2035"}, 2, 1, False),
        # A conversion hire field is True - should return False
        (
            {AD_ACCOUNT_STATUS_FIELD: "Enabled", CONVERSION_HIRE_FIELD: True, SOURCE_PRIORITY_FIELD: 1},
            {HIRE_DATE_FIELD: "12/12/2035"},
            2,
            1,
            False,
        ),
        # Active AD account and hire date did not exceed threshold date - should return True
        ({AD_ACCOUNT_STATUS_FIELD: "Enabled", SOURCE_PRIORITY_FIELD: 1}, {HIRE_DATE_FIELD: "12/12/2035"}, 2, 1, True),
        # Active AD account, past hire date - should return False
        ({AD_ACCOUNT_STATUS_FIELD: "Enabled", SOURCE_PRIORITY_FIELD: 1}, {HIRE_DATE_FIELD: "12/12/2020"}, 2, 1, False),
        # Non active AD account - should return False
        ({AD_ACCOUNT_STATUS_FIELD: "Pending", SOURCE_PRIORITY_FIELD: 1}, {HIRE_DATE_FIELD: "12/12/2035"}, 2, 1, False),
    ],
)
def test_is_ad_deactivation_event(demisto_user, workday_user, days_before_hire_to_enable_ad, source_priority, expected_result):
    from Workday_IAM import is_ad_deactivation_event

    assert is_ad_deactivation_event(demisto_user, workday_user, days_before_hire_to_enable_ad, source_priority) == expected_result


@pytest.mark.parametrize(
    "workday_user, changed_fields, expected_result",
    [
        # a non terminated workday_user with changed fields (hasn't been synced to XSOAR yet) - should return True
        ({EMPLOYMENT_STATUS_FIELD: "Leave of Absence"}, "mock_changed_fields", True),
        # a terminated workday_user with changed fields (hasn't been synced to XSOAR yet) - should return False
        ({EMPLOYMENT_STATUS_FIELD: "Terminated"}, "mock_changed_fields", False),
        # a non terminated workday_user with no changed fields (already synced to XSOAR) - should return False
        ({EMPLOYMENT_STATUS_FIELD: "Leave of Absence"}, None, False),
    ],
)
def test_is_update_event(workday_user, changed_fields, expected_result):
    from Workday_IAM import is_update_event

    assert is_update_event(workday_user, changed_fields) == expected_result


@pytest.mark.parametrize(
    "demisto_user, expected_result",
    [(None, False), ({TERMINATION_TRIGGER_FIELD: "TUFE"}, True), ({TERMINATION_TRIGGER_FIELD: "Workday IAM"}, False)],
)
def test_is_tufe_user(demisto_user, expected_result):
    from Workday_IAM import is_tufe_user

    assert is_tufe_user(demisto_user) == expected_result


@pytest.mark.parametrize(
    "workday_user, demisto_user, expected_event_type",
    [
        # case 1: non-rehired TUFE user
        (
            {EMAIL_ADDRESS_FIELD: "test@example.com", EMPLOYEE_ID_FIELD: "123", HIRE_DATE_FIELD: "02/02/2040"},
            {
                EMAIL_ADDRESS_FIELD: "test@example.com",
                EMPLOYEE_ID_FIELD: "123",
                HIRE_DATE_FIELD: "02/02/2040",
                AD_ACCOUNT_STATUS_FIELD: "Enabled",
            },
            None,
        ),
        # case 2: a rehired tufe user
        (
            {
                EMAIL_ADDRESS_FIELD: "test@example.com",
                EMPLOYEE_ID_FIELD: "123",
                HIRE_DATE_FIELD: "mock_date_field",
                PREHIRE_FLAG_FIELD: "True",
                REHIRED_EMPLOYEE_FIELD: "Yes",
            },
            {
                EMAIL_ADDRESS_FIELD: "test@example.com",
                EMPLOYEE_ID_FIELD: "123",
                HIRE_DATE_FIELD: "mock_date_field",
                AD_ACCOUNT_STATUS_FIELD: "Disabled",
            },
            REHIRE_USER_EVENT_TYPE,
        ),
    ],
)
def test_get_event_details__tufe_user(workday_user, demisto_user, expected_event_type):
    """
    Given
    - A TUFE user event process.
    When
    - Calling get_event_details() method.
    Then
    - Ensure None is returned, unless a rehire event is detected.
    """
    from Workday_IAM import get_event_details

    event = get_event_details(
        entry={},
        workday_user=workday_user,
        demisto_user=demisto_user,
        days_before_hire_to_sync=None,
        days_before_hire_to_enable_ad=None,
        deactivation_date_field=LAST_DAY_OF_WORK_FIELD,
        display_name_to_user_profile={},
        email_to_user_profile={},
        employee_id_to_user_profile={},
        source_priority=1,
    )
    event_type = event.get("type") if isinstance(event, dict) else None
    assert event_type is expected_event_type