Collect Workday audit logs

Supported in:

This document explains how to ingest Workday audit logs to Google Security Operations using AWS S3. The parser first identifies the specific event type from the logs based on pattern analysis of the CSV data. Then, it extracts and structures relevant fields according to the identified type, mapping them to a unified data model (UDM) for consistent security analysis.

Before you begin

Make sure you have the following prerequisites:

  • Google SecOps instance
  • Privileged access to AWS
  • Privileged access to Workday

Configure AWS S3 bucket and IAM for Google SecOps

  1. Create Amazon S3 bucket following this user guide: Creating a bucket.
  2. Save bucket Name and Region for future reference (for example, workday-audit-logs).
  3. Create a User following this user guide: Creating an IAM user.
  4. Select the created User.
  5. Select Security credentials tab.
  6. Click Create Access Key in section Access Keys.
  7. Select Third-party service as Use case.
  8. Click Next.
  9. Optional: Add description tag.
  10. Click Create access key.
  11. Click Download CSV file for save the Access Key and Secret Access Key for future reference.
  12. Click Done.
  13. Select Permissions tab.
  14. Click Add permissions in section Permissions policies.
  15. Select Add permissions.
  16. Select Attach policies directly.
  17. Search for and select the AmazonS3FullAccess policy.
  18. Click Next.
  19. Click Add permissions.

Create Workday Integration System User (ISU)

  1. In Workday, search for Create Integration System User > OK.
  2. Fill in the User Name (for example, audit_s3_user).
  3. Click OK.
  4. Reset the password by going to Related Actions > Security > Reset Password.
  5. Select Maintain Password Rules to prevent the password from expiring.
  6. Search for Create Security Group > Integration System Security Group (Unconstrained).
  7. Provide a name (for example, ISU_Audit_S3) and add the ISU to Integration System Users.
  8. Search for Domain Security Policies for Functional Area > System.
  9. For Audit Trail, select Actions > Edit Permissions.
  10. Under Get Only, add the ISU_Audit_S3 group.
  11. Click OK > Activate Pending Security Policy Changes.

Configure Workday Custom Report

  1. In Workday, search Create Custom Report.
  2. Provide the following configuration details:
    • Name: Enter a unique name (for example, Audit_Trail_BP_JSON).
    • Type: Select Advanced.
    • Data Source: Select Audit Trail – Business Process.
    • Click OK.
    • Optional: Add filters on Business Process Type or Effective Date.
  3. Go to Output tab.
  4. Select Enable as Web Service, Optimized for Performance and select JSON Format.
  5. Click OK > Done.
  6. Open the report and click Share > add ISU_Audit_S3 with View permission > OK.
  7. Go to Related Actions > Web Service > View URLs.
  8. Copy the JSON URL (for example, https://wd-services1.workday.com/ccx/service/customreport2/<tenant>/<user>/Audit_Trail_BP_JSON?format=json).

Configure the IAM policy and role for S3 uploads

  1. Policy JSON (replace workday-audit-logs if you entered a different bucket name):

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowPutWorkdayObjects",
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::workday-audit-logs/*"
        }
      ]
    }
    
  2. Go to AWS console > IAM > Policies > Create policy > JSON tab.

  3. Copy and paste the policy.

  4. Click Next > Create policy.

  5. Go to IAM > Roles > Create role > AWS service > Lambda.

  6. Attach the newly created policy.

  7. Name the role WriteWorkdayToS3Role and click Create role.

Create the Lambda function

Setting Value
Name workday_audit_to_s3
Runtime Python 3.13
Architecture x86_64
Execution role WriteWorkdayToS3Role
  1. After the function is created, open the Code tab, delete the stub and paste the code below (workday_audit_to_s3.py).

    #!/usr/bin/env python3
    
    import os, json, gzip, io, uuid, base64, datetime as dt, urllib.request, urllib.error
    import boto3
    
    WD_USER   = os.environ["WD_USER"]
    WD_PASS   = os.environ["WD_PASS"]
    WD_URL    = os.environ["WD_URL"]
    S3_BUCKET = os.environ["S3_BUCKET_NAME"]
    
    def fetch_report() -> bytes:
        credentials = f"{WD_USER}:{WD_PASS}".encode()
        auth_header = b"Basic " + base64.b64encode(credentials)
        req = urllib.request.Request(WD_URL, headers={"Authorization": auth_header.decode()})
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.read()  # raw JSON bytes
    
    def upload(payload: bytes, ts: dt.datetime) -> None:
        key = f"{ts:%Y/%m/%d}/workday-audit-{uuid.uuid4()}.json.gz"
        buf = io.BytesIO()
        with gzip.GzipFile(fileobj=buf, mode="w") as gz:
            gz.write(payload)
        buf.seek(0)
        boto3.client("s3").upload_fileobj(buf, S3_BUCKET, key)
    
    def lambda_handler(event=None, context=None):
        now = dt.datetime.utcnow().replace(microsecond=0)
        data = fetch_report()
        upload(data, now)
        print(f"Uploaded Workday audit report ({len(data)} bytes raw)")
    
    if __name__ == "__main__":
        lambda_handler()
    
  2. Go to Configuration > Environment variables > Edit > Add new environment variable.

  3. Enter the following environment variables, replacing with your value.

    Environment variables

    Key Example Values
    WD_USER audit_s3_user
    WD_PASS Wrokday-Password
    WD_URL https://.../Audit_Trail_BP_JSON?format=json
    S3_BUCKET_NAME workday-audit-logs
  4. After the function is created, stay on its page (or open Lambda > Functions > your‑function).

  5. Select the Configuration tab.

  6. In the General configuration panel click Edit.

  7. Change Timeout to 5 minutes (300 seconds) and click Save.

Schedule the Lambda function (EventBridge Scheduler)

  1. Go to Configuration > Triggers > Add trigger > EventBridge Scheduler > Create rule.
  2. Provide the following configuration details:
    • Name: daily-workday-audit export.
    • Schedule pattern: Cron expression.
    • Expression: 20 2 * * ? * (runs daily at 02:20 UTC).
  3. Leave the rest as default and click Create.

Configure a feed in Google SecOps to ingest Workday Audit logs

  1. Go to SIEM Settings > Feeds.
  2. Click + Add New Feed.
  3. In the Feed name field, enter a name for the feed (for example, Workday Audit Logs).
  4. Select Amazon S3 V2 as the Source type.
  5. Select Workday Audit as the Log type.
  6. Click Get A Service Account.
  7. Click Next.
  8. Specify values for the following input parameters:
    • S3 URI: The bucket URI
      • s3://workday-audit-logs/.
        • Replace workday-audit-logs with the actual name of the bucket.
    • Source deletion options: Select deletion option according to your preference.
    • Maximum File Age: Include files modified in the last number of days. Default is 180 Days.
    • Access Key ID: User access key with access to the s3 bucket.
    • Secret Access Key: User secret key with access to the s3 bucket.
    • Asset namespace: The asset namespace.
    • Ingestion labels: The label to be applied to the events from this feed.
  9. Click Next.
  10. Review your new feed configuration in the Finalize screen, and then click Submit.

UDM Mapping Table

Log Field UDM Mapping Logic
Account metadata.event_type If the "Account" field is not empty, the "metadata.event_type" field is set to "USER_RESOURCE_UPDATE_CONTENT".
Account principal.user.primaryId The userid is extracted from the "Account" field using a grok pattern and mapped to principal.user.primaryId.
Account principal.user.primaryName The user display name is extracted from the "Account" field using a grok pattern and mapped to "principal.user.primaryName".
ActivityCategory metadata.event_type If the "ActivityCategory" field is "READ", the "metadata.event_type" field is set to "RESOURCE_READ". If "WRITE", it's set to "RESOURCE_WRITTEN".
ActivityCategory metadata.product_event_type Directly mapped from the "ActivityCategory" field.
AffectedGroups target.user.group_identifiers Directly mapped from the "AffectedGroups" field.
Area target.resource.attribute.labels.area.value Directly mapped from the "Area" field.
AuthType extensions.auth.auth_details Directly mapped from the "AuthType" field.
AuthType extensions.auth.type Mapped from the "AuthType" field to different authentication types defined in the UDM based on specific values.
CFIPdeConexion src.domain.name If the "CFIPdeConexion" field is not a valid IP address, it's mapped to "src.domain.name".
CFIPdeConexion target.ip If the "CFIPdeConexion" field is a valid IP address, it's mapped to "target.ip".
ChangedRelationship metadata.description Directly mapped from the "ChangedRelationship" field.
ClassOfInstance target.resource.attribute.labels.class_instance.value Directly mapped from the "ClassOfInstance" field.
column18 about.labels.utub.value Directly mapped from the "column18" field.
CreatedBy principal.user.userid The userid is extracted from the "CreatedBy" field using a grok pattern and mapped to "principal.user.userid".
CreatedBy principal.user.user_display_name The user display name is extracted from the "CreatedBy" field using a grok pattern and mapped to "principal.user.user_display_name".
Domain about.domain.name Directly mapped from the "Domain" field.
EffectiveDate @timestamp Parsed to "@timestamp" after converting to "yyyy-MM-dd HH:mm:ss.SSSZ" format.
EntryMoment @timestamp Parsed to "@timestamp" after converting to "ISO8601" format.
EventType security_result.description Directly mapped from the "EventType" field.
Form target.resource.name Directly mapped from the "Form" field.
InstancesAdded about.resource.attribute.labels.instances_added.value Directly mapped from the "InstancesAdded" field.
InstancesAdded target.user.attribute.roles.instances_added.name Directly mapped from the "InstancesAdded" field.
InstancesRemoved about.resource.attribute.labels.instances_removed.value Directly mapped from the "InstancesRemoved" field.
InstancesRemoved target.user.attribute.roles.instances_removed.name Directly mapped from the "InstancesRemoved" field.
IntegrationEvent target.resource.attribute.labels.integration_event.value Directly mapped from the "IntegrationEvent" field.
IntegrationStatus security_result.action_details Directly mapped from the "IntegrationStatus" field.
IntegrationSystem target.resource.name Directly mapped from the "IntegrationSystem" field.
IP src.domain.name If the "IP" field is not a valid IP address, it's mapped to "src.domain.name".
IP src.ip If the "IP" field is a valid IP address, it's mapped to "src.ip".
IsDeviceManaged additional.fields.additional1.value.string_value If the "IsDeviceManaged" field is "N", the value is set to "Successful". Otherwise, it's set to "Failed login occurred".
IsDeviceManaged additional.fields.additional2.value.string_value If the "IsDeviceManaged" field is "N", the value is set to "Successful". Otherwise, it's set to "Invalid Credentials".
IsDeviceManaged additional.fields.additional3.value.string_value If the "IsDeviceManaged" field is "N", the value is set to "Successful". Otherwise, it's set to "Account Locked".
IsDeviceManaged security_result.action_details Directly mapped from the "IsDeviceManaged" field.
OutputFiles about.file.full_path Directly mapped from the "OutputFiles" field.
Person principal.user.primaryId If the "Person" field starts with "INT", the userid is extracted using a grok pattern and mapped to "principal.user.primaryId".
Person principal.user.primaryName If the "Person" field starts with "INT", the user display name is extracted using a grok pattern and mapped to "principal.user.primaryName".
Person principal.user.user_display_name If the "Person" field doesn't start with "INT", it's directly mapped to "principal.user.user_display_name".
Person metadata.event_type If the "Person" field is not empty, the "metadata.event_type" field is set to "USER_RESOURCE_UPDATE_CONTENT".
ProcessedTransaction target.resource.attribute.creation_time Parsed to "target.resource.attribute.creation_time" after converting to "dd/MM/yyyy HH:mm:ss,SSS (ZZZ)", "dd/MM/yyyy, HH:mm:ss,SSS (ZZZ)", or "MM/dd/yyyy, HH:mm:ss.SSS A ZZZ" format.
ProgramBy principal.user.userid Directly mapped from the "ProgramBy" field.
RecurrenceEndDate principal.resource.attribute.last_update_time Parsed to "principal.resource.attribute.last_update_time" after converting to "yyyy-MM-dd" format.
RecurrenceStartDate principal.resource.attribute.creation_time Parsed to "principal.resource.attribute.creation_time" after converting to "yyyy-MM-dd" format.
RequestName metadata.description Directly mapped from the "RequestName" field.
ResponseMessage security_result.summary Directly mapped from the "ResponseMessage" field.
RestrictedToEnvironment security_result.about.hostname Directly mapped from the "RestrictedToEnvironment" field.
RevokedSecurity security_result.outcomes.outcomes.value Directly mapped from the "RevokedSecurity" field.
RunFrequency principal.resource.attribute.labels.run_frequency.value Directly mapped from the "RunFrequency" field.
ScheduledProcess principal.resource.name Directly mapped from the "ScheduledProcess" field.
SecuredTaskExecuted target.resource.name Directly mapped from the "SecuredTaskExecuted" field.
SecureTaskExecuted metadata.event_type If the "SecureTaskExecuted" field contains "Create", the "metadata.event_type" field is set to "USER_RESOURCE_CREATION".
SecureTaskExecuted target.resource.name Directly mapped from the "SecureTaskExecuted" field.
SentTime @timestamp Parsed to "@timestamp" after converting to "ISO8601" format.
SessionId network.session_id Directly mapped from the "SessionId" field.
ShareBy target.user.userid Directly mapped from the "ShareBy" field.
SignOffTime additional.fields.additional4.value.string_value The "AuthFailMessage" field value is placed within the "additional.fields" array with the key "Enterprise Interface Builder".
SignOffTime metadata.description Directly mapped from the "AuthFailMessage" field.
SignOffTime metadata.event_type If the "SignOffTime" field is empty, the "metadata.event_type" field is set to "USER_LOGIN". Otherwise, it's set to "USER_LOGOUT".
SignOffTime principal.user.attribute.last_update_time Parsed to "principal.user.attribute.last_update_time" after converting to "ISO8601" format.
SignOnIp src.domain.name If the "SignOnIp" field is not a valid IP address, it's mapped to "src.domain.name".
SignOnIp src.ip If the "SignOnIp" field is a valid IP address, it's mapped to "src.ip".
Status metadata.product_event_type Directly mapped from the "Status" field.
SystemAccount principal.user.email_addresses The email address is extracted from the "SystemAccount" field using a grok pattern and mapped to "principal.user.email_addresses".
SystemAccount principal.user.primaryId The userid is extracted from the "SystemAccount" field using a grok pattern and mapped to "principal.user.primaryId".
SystemAccount principal.user.primaryName The user display name is extracted from the "SystemAccount" field using a grok pattern and mapped to "principal.user.primaryName".
SystemAccount src.user.userid The secondary userid is extracted from the "SystemAccount" field using a grok pattern and mapped to "src.user.userid".
SystemAccount src.user.user_display_name The secondary user display name is extracted from the "SystemAccount" field using a grok pattern and mapped to "src.user.user_display_name".
SystemAccount target.user.userid The target userid is extracted from the "SystemAccount" field using a grok pattern and mapped to "target.user.userid".
Target target.user.user_display_name Directly mapped from the "Target" field.
Template about.resource.name Directly mapped from the "Template" field.
Tenant target.asset.hostname Directly mapped from the "Tenant" field.
TlsVersion network.tls.version Directly mapped from the "TlsVersion" field.
Transaction security_result.action_details Directly mapped from the "Transaction" field.
TransactionType security_result.summary Directly mapped from the "TransactionType" field.
TypeForm target.resource.resource_subtype Directly mapped from the "TypeForm" field.
UserAgent network.http.parsed_user_agent Parsed from the "UserAgent" field using the "useragent" filter.
UserAgent network.http.user_agent Directly mapped from the "UserAgent" field.
WorkdayAccount target.user.user_display_name The user display name is extracted from the "WorkdayAccount" field using a grok pattern and mapped to "target.user.user_display_name".
WorkdayAccount target.user.userid The userid is extracted from the "WorkdayAccount" field using a grok pattern and mapped to "target.user.userid".
additional.fields.additional1.key Set to "FailedSignOn".
additional.fields.additional2.key Set to "InvalidCredentials".
additional.fields.additional3.key Set to "AccountLocked".
additional.fields.additional4.key Set to "Enterprise Interface Builder".
metadata.event_type Set to "GENERIC_EVENT" initially and then updated based on the logic involving other fields.
metadata.event_type Set to "USER_CHANGE_PERMISSIONS" for specific event types.
metadata.event_type Set to "RESOURCE_WRITTEN" for specific event types.
metadata.log_type Hardcoded to "WORKDAY_AUDIT".
metadata.product_name Hardcoded to "Enterprise Interface Builder".
metadata.vendor_name Hardcoded to "Workday".
principal.asset.category Set to "Phone" if the "DeviceType" field is "Phone".
principal.resource.resource_type Hardcoded to "TASK" if the "ScheduledProcess" field is not empty.
security_result.action Set to "ALLOW" or "FAIL" based on the values of "FailedSignOn", "IsDeviceManaged", "InvalidCredentials", and "AccountLocked" fields.
security_result.summary Set to "Successful" or specific error messages based on the values of "FailedSignOn", "IsDeviceManaged", "InvalidCredentials", and "AccountLocked" fields.
target.resource.resource_type Hardcoded to "TASK" for specific event types.
target.resource.resource_type Hardcoded to "DATASET" if the "TypeForm" field is not empty.
message principal.user.email_addresses Extracts the email address from the "message" field using a grok pattern and merges it into "principal.user.email_addresses" if a specific pattern is matched.
message src.user.userid Clears the field if the "event.idm.read_only_udm.principal.user.userid" field matches the extracted "user_target" from the "message" field.
message src.user.user_display_name Clears the field if the "event.idm.read_only_udm.principal.user.userid" field matches the extracted "user_target" from the "message" field.
message target.user.userid Extracts the userid from the "message" field using a grok pattern and maps it to "target.user.userid" if a specific pattern is matched.

Need more help? Get answers from Community members and Google SecOps professionals.