Recopila registros de auditoría de Workday
En este documento, se explica cómo transferir registros de auditoría de Workday a Google Security Operations con AWS S3. Primero, el analizador identifica el tipo de evento específico en los registros según el análisis de patrones de los datos CSV. Luego, extrae y estructura los campos pertinentes según el tipo identificado, y los asigna a un modelo de datos unificado (UDM) para realizar un análisis de seguridad coherente.
Antes de comenzar
Asegúrate de cumplir con los siguientes requisitos previos:
- Instancia de Google SecOps
- Acceso privilegiado a AWS
- Acceso con privilegios a Workday
Configura el bucket de AWS S3 y el IAM para Google SecOps
- Crea un bucket de Amazon S3 siguiendo esta guía del usuario: Crea un bucket.
- Guarda el Nombre y la Región del bucket para futuras referencias (por ejemplo,
workday-audit-logs
). - Crea un usuario siguiendo esta guía del usuario: Cómo crear un usuario de IAM.
- Selecciona el usuario creado.
- Selecciona la pestaña Credenciales de seguridad.
- Haz clic en Crear clave de acceso en la sección Claves de acceso.
- Selecciona Servicio de terceros como Caso de uso.
- Haz clic en Siguiente.
- Opcional: Agrega una etiqueta de descripción.
- Haz clic en Crear clave de acceso.
- Haz clic en Descargar archivo CSV para guardar la clave de acceso y la clave de acceso secreta para consultarlas en el futuro.
- Haz clic en Listo.
- Selecciona la pestaña Permisos.
- Haz clic en Agregar permisos en la sección Políticas de permisos.
- Selecciona Agregar permisos.
- Selecciona Adjuntar políticas directamente.
- Busca y selecciona la política AmazonS3FullAccess.
- Haz clic en Siguiente.
- Haz clic en Agregar permisos.
Crea un usuario del sistema de integración (ISU) de Workday
- En Workday, busca Create Integration System User > OK.
- Completa el Nombre de usuario (por ejemplo,
audit_s3_user
). - Haz clic en Aceptar.
- Para restablecer la contraseña, ve a Acciones relacionadas > Seguridad > Restablecer contraseña.
- Selecciona Mantener reglas de contraseñas para evitar que la contraseña venza.
- Busca Create Security Group > Integration System Security Group (Unconstrained).
- Proporciona un nombre (por ejemplo,
ISU_Audit_S3
) y agrega el ISU a Integration System Users. - Busca Políticas de seguridad del dominio para el área funcional > Sistema.
- En Registro de auditoría, selecciona Acciones > Editar permisos.
- En Get Only, agrega el grupo
ISU_Audit_S3
. - Haz clic en Aceptar > Activar cambios pendientes en la política de seguridad.
Configura el informe personalizado de Workday
- En Workday, busca Create Custom Report.
- Proporciona los siguientes detalles de configuración:
- Nombre: Ingresa un nombre único (por ejemplo,
Audit_Trail_BP_JSON
). - Tipo: Selecciona Avanzado.
- Fuente de datos: Selecciona Registro de auditoría: Proceso comercial.
- Haz clic en Aceptar.
- Opcional: Agrega filtros en Tipo de proceso comercial o Fecha de entrada en vigencia.
- Nombre: Ingresa un nombre único (por ejemplo,
- Ve a la pestaña Salida.
- Selecciona Enable as Web Service, Optimized for Performance y JSON Format.
- Haz clic en Aceptar > Listo.
- Abre el informe y haz clic en Compartir > agregar
ISU_Audit_S3
con permiso de visualización > Aceptar. - Ve a Acciones relacionadas > Servicio web > Ver URLs.
- Copia la URL de JSON (por ejemplo,
https://wd-services1.workday.com/ccx/service/customreport2/<tenant>/<user>/Audit_Trail_BP_JSON?format=json
).
Configura la política y el rol de IAM para las cargas de S3
JSON de la política (reemplaza
workday-audit-logs
si ingresaste un nombre de bucket diferente):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutWorkdayObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::workday-audit-logs/*" } ] }
Ve a la consola de AWS > IAM > Políticas > Crear política > pestaña JSON.
Copia y pega la política.
Haz clic en Siguiente > Crear política.
Ve a IAM > Roles > Crear rol > Servicio de AWS > Lambda.
Adjunta la política recién creada.
Asigna el nombre
WriteWorkdayToS3Role
al rol y haz clic en Crear rol.
Crea la función Lambda
Configuración | Valor |
---|---|
Nombre | workday_audit_to_s3 |
Tiempo de ejecución | Python 3.13 |
Arquitectura | x86_64 |
Rol de ejecución | WriteWorkdayToS3Role |
Después de crear la función, abre la pestaña Code, borra el código auxiliar y pega el siguiente código (
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()
Ve a Configuración > Variables de entorno > Editar > Agregar nueva variable de entorno.
Ingresa las siguientes variables de entorno y reemplaza los marcadores de posición por tus valores.
Variables de entorno
Clave Valores de ejemplo WD_USER
audit_s3_user
WD_PASS
Wrokday-Password
WD_URL
https://.../Audit_Trail_BP_JSON?format=json
S3_BUCKET_NAME
workday-audit-logs
Después de crear la función, permanece en su página (o abre Lambda > Functions > tu‑función).
Selecciona la pestaña Configuración.
En el panel Configuración general, haz clic en Editar.
Cambia Tiempo de espera a 5 minutos (300 segundos) y haz clic en Guardar.
Programa la función de Lambda (EventBridge Scheduler)
- Ve a Configuración > Activadores > Agregar activador > EventBridge Scheduler > Crear regla.
- Proporciona los siguientes detalles de configuración:
- Nombre:
daily-workday-audit export
. - Patrón de programación: Expresión cron.
- Expresión:
20 2 * * ? *
(se ejecuta a diario a las 02:20 UTC).
- Nombre:
- Deja el resto con la configuración predeterminada y haz clic en Crear.
Configura un feed en Google SecOps para transferir registros de auditoría de Workday
- Ve a SIEM Settings > Feeds.
- Haz clic en + Agregar feed nuevo.
- En el campo Nombre del feed, ingresa un nombre para el feed (por ejemplo,
Workday Audit Logs
). - Selecciona Amazon S3 V2 como el Tipo de fuente.
- Selecciona Auditoría de jornada laboral como el Tipo de registro.
- Haz clic en Obtener una cuenta de servicio.
- Haz clic en Siguiente.
- Especifica valores para los siguientes parámetros de entrada:
- URI de S3: Es el URI del bucket.
s3://workday-audit-logs/
.- Reemplaza
workday-audit-logs
por el nombre real del bucket.
- Reemplaza
- Opciones de borrado de la fuente: Selecciona la opción de borrado según tu preferencia.
- Antigüedad máxima del archivo: Incluye los archivos modificados en la cantidad de días especificada. El valor predeterminado es 180 días.
- ID de clave de acceso: Clave de acceso del usuario con acceso al bucket de S3.
- Clave de acceso secreta: Clave secreta del usuario con acceso al bucket de S3.
- Espacio de nombres del recurso: Es el espacio de nombres del recurso.
- Etiquetas de transmisión: Es la etiqueta que se aplicará a los eventos de este feed.
- URI de S3: Es el URI del bucket.
- Haz clic en Siguiente.
- Revisa la nueva configuración del feed en la pantalla Finalizar y, luego, haz clic en Enviar.
Tabla de asignación de UDM
Campo de registro | Asignación de UDM | Lógica |
---|---|---|
Account |
metadata.event_type | Si el campo "Account" no está vacío, el campo "metadata.event_type" se establece en "USER_RESOURCE_UPDATE_CONTENT". |
Account |
principal.user.primaryId | El ID de usuario se extrae del campo "Cuenta" con un patrón de Grok y se asigna a principal.user.primaryId . |
Account |
principal.user.primaryName | El nombre visible del usuario se extrae del campo "Cuenta" con un patrón de grok y se asigna a "principal.user.primaryName". |
ActivityCategory |
metadata.event_type | Si el campo "ActivityCategory" es "READ", el campo "metadata.event_type" se establece en "RESOURCE_READ". Si es "WRITE", se establece en "RESOURCE_WRITTEN". |
ActivityCategory |
metadata.product_event_type | Se asigna directamente desde el campo "ActivityCategory". |
AffectedGroups |
target.user.group_identifiers | Se asigna directamente desde el campo "AffectedGroups". |
Area |
target.resource.attribute.labels.area.value | Se asigna directamente desde el campo "Área". |
AuthType |
extensions.auth.auth_details | Se asigna directamente desde el campo "AuthType". |
AuthType |
extensions.auth.type | Se asigna desde el campo "AuthType" a diferentes tipos de autenticación definidos en el UDM según valores específicos. |
CFIPdeConexion |
src.domain.name | Si el campo "CFIPdeConexion" no es una dirección IP válida, se asigna a "src.domain.name". |
CFIPdeConexion |
target.ip | Si el campo "CFIPdeConexion" es una dirección IP válida, se asigna a "target.ip". |
ChangedRelationship |
metadata.description | Se asigna directamente desde el campo "ChangedRelationship". |
ClassOfInstance |
target.resource.attribute.labels.class_instance.value | Se asigna directamente desde el campo "ClassOfInstance". |
column18 |
about.labels.utub.value | Se asigna directamente desde el campo "column18". |
CreatedBy |
principal.user.userid | El ID de usuario se extrae del campo "CreatedBy" con un patrón grok y se asigna a "principal.user.userid". |
CreatedBy |
principal.user.user_display_name | El nombre visible del usuario se extrae del campo "CreatedBy" con un patrón grok y se asigna a "principal.user.user_display_name". |
Domain |
about.domain.name | Se asigna directamente desde el campo "Dominio". |
EffectiveDate |
@timestamp | Se analizó como "@timestamp" después de convertirse al formato "aaaa-MM-dd HH:mm:ss.SSSZ". |
EntryMoment |
@timestamp | Se analizó como "@timestamp" después de la conversión al formato "ISO8601". |
EventType |
security_result.description | Se asigna directamente desde el campo "EventType". |
Form |
target.resource.name | Se asigna directamente desde el campo "Formulario". |
InstancesAdded |
about.resource.attribute.labels.instances_added.value | Se asigna directamente desde el campo "InstancesAdded". |
InstancesAdded |
target.user.attribute.roles.instances_added.name | Se asigna directamente desde el campo "InstancesAdded". |
InstancesRemoved |
about.resource.attribute.labels.instances_removed.value | Se asigna directamente desde el campo "InstancesRemoved". |
InstancesRemoved |
target.user.attribute.roles.instances_removed.name | Se asigna directamente desde el campo "InstancesRemoved". |
IntegrationEvent |
target.resource.attribute.labels.integration_event.value | Se asigna directamente desde el campo "IntegrationEvent". |
IntegrationStatus |
security_result.action_details | Se asigna directamente desde el campo "IntegrationStatus". |
IntegrationSystem |
target.resource.name | Se asigna directamente desde el campo "IntegrationSystem". |
IP |
src.domain.name | Si el campo "IP" no es una dirección IP válida, se asigna a "src.domain.name". |
IP |
src.ip | Si el campo "IP" es una dirección IP válida, se asigna a "src.ip". |
IsDeviceManaged |
additional.fields.additional1.value.string_value | Si el campo "IsDeviceManaged" es "N", el valor se establece en "Successful". De lo contrario, se establece como "Se produjo un acceso fallido". |
IsDeviceManaged |
additional.fields.additional2.value.string_value | Si el campo "IsDeviceManaged" es "N", el valor se establece en "Successful". De lo contrario, se establece como "Invalid Credentials". |
IsDeviceManaged |
additional.fields.additional3.value.string_value | Si el campo "IsDeviceManaged" es "N", el valor se establece en "Successful". De lo contrario, se establece como "Cuenta bloqueada". |
IsDeviceManaged |
security_result.action_details | Se asigna directamente desde el campo "IsDeviceManaged". |
OutputFiles |
about.file.full_path | Se asigna directamente desde el campo "OutputFiles". |
Person |
principal.user.primaryId | Si el campo "Persona" comienza con "INT", el ID de usuario se extrae con un patrón de Grok y se asigna a "principal.user.primaryId". |
Person |
principal.user.primaryName | Si el campo "Persona" comienza con "INT", el nombre visible del usuario se extrae con un patrón de Grok y se asigna a "principal.user.primaryName". |
Person |
principal.user.user_display_name | Si el campo "Persona" no comienza con "INT", se asigna directamente a "principal.user.user_display_name". |
Person |
metadata.event_type | Si el campo "Person" no está vacío, el campo "metadata.event_type" se establece en "USER_RESOURCE_UPDATE_CONTENT". |
ProcessedTransaction |
target.resource.attribute.creation_time | Se analiza como "target.resource.attribute.creation_time" después de convertirse al formato "dd/MM/aaaa HH:mm:ss,SSS (ZZZ)", "dd/MM/aaaa, HH:mm:ss,SSS (ZZZ)" o "MM/dd/aaaa, HH:mm:ss.SSS A ZZZ". |
ProgramBy |
principal.user.userid | Se asigna directamente desde el campo "ProgramBy". |
RecurrenceEndDate |
principal.resource.attribute.last_update_time | Se analizó como "principal.resource.attribute.last_update_time" después de la conversión al formato "aaaa-MM-dd". |
RecurrenceStartDate |
principal.resource.attribute.creation_time | Se analiza y se convierte a "principal.resource.attribute.creation_time" después de convertirlo al formato "aaaa-MM-dd". |
RequestName |
metadata.description | Se asigna directamente desde el campo "RequestName". |
ResponseMessage |
security_result.summary | Se asigna directamente desde el campo "ResponseMessage". |
RestrictedToEnvironment |
security_result.about.hostname | Se asigna directamente desde el campo "RestrictedToEnvironment". |
RevokedSecurity |
security_result.outcomes.outcomes.value | Se asigna directamente desde el campo "RevokedSecurity". |
RunFrequency |
principal.resource.attribute.labels.run_frequency.value | Se asigna directamente desde el campo "RunFrequency". |
ScheduledProcess |
principal.resource.name | Se asigna directamente desde el campo "ScheduledProcess". |
SecuredTaskExecuted |
target.resource.name | Se asigna directamente desde el campo "SecuredTaskExecuted". |
SecureTaskExecuted |
metadata.event_type | Si el campo "SecureTaskExecuted" contiene "Create", el campo "metadata.event_type" se establece en "USER_RESOURCE_CREATION". |
SecureTaskExecuted |
target.resource.name | Se asigna directamente desde el campo "SecureTaskExecuted". |
SentTime |
@timestamp | Se analizó como "@timestamp" después de la conversión al formato "ISO8601". |
SessionId |
network.session_id | Se asigna directamente desde el campo "SessionId". |
ShareBy |
target.user.userid | Se asigna directamente desde el campo "ShareBy". |
SignOffTime |
additional.fields.additional4.value.string_value | El valor del campo "AuthFailMessage" se coloca dentro del array "additional.fields" con la clave "Enterprise Interface Builder". |
SignOffTime |
metadata.description | Se asigna directamente desde el campo "AuthFailMessage". |
SignOffTime |
metadata.event_type | Si el campo "SignOffTime" está vacío, el campo "metadata.event_type" se establece en "USER_LOGIN". De lo contrario, se establece como "USER_LOGOUT". |
SignOffTime |
principal.user.attribute.last_update_time | Se analiza como "principal.user.attribute.last_update_time" después de convertirse al formato "ISO8601". |
SignOnIp |
src.domain.name | Si el campo "SignOnIp" no es una dirección IP válida, se asigna a "src.domain.name". |
SignOnIp |
src.ip | Si el campo "SignOnIp" es una dirección IP válida, se asigna a "src.ip". |
Status |
metadata.product_event_type | Se asigna directamente desde el campo "Estado". |
SystemAccount |
principal.user.email_addresses | La dirección de correo electrónico se extrae del campo "SystemAccount" con un patrón de Grok y se asigna a "principal.user.email_addresses". |
SystemAccount |
principal.user.primaryId | El ID de usuario se extrae del campo "SystemAccount" con un patrón grok y se asigna a "principal.user.primaryId". |
SystemAccount |
principal.user.primaryName | El nombre visible del usuario se extrae del campo "SystemAccount" con un patrón de Grok y se asigna a "principal.user.primaryName". |
SystemAccount |
src.user.userid | El ID de usuario secundario se extrae del campo "SystemAccount" con un patrón de Grok y se asigna a "src.user.userid". |
SystemAccount |
src.user.user_display_name | El nombre visible del usuario secundario se extrae del campo "SystemAccount" con un patrón de Grok y se asigna a "src.user.user_display_name". |
SystemAccount |
target.user.userid | El ID de usuario objetivo se extrae del campo "SystemAccount" con un patrón de Grok y se asigna a "target.user.userid". |
Target |
target.user.user_display_name | Se asigna directamente desde el campo "Objetivo". |
Template |
about.resource.name | Se asigna directamente desde el campo "Plantilla". |
Tenant |
target.asset.hostname | Se asigna directamente desde el campo "Tenant". |
TlsVersion |
network.tls.version | Se asigna directamente desde el campo "TlsVersion". |
Transaction |
security_result.action_details | Se asigna directamente desde el campo "Transacción". |
TransactionType |
security_result.summary | Se asigna directamente desde el campo "TransactionType". |
TypeForm |
target.resource.resource_subtype | Se asigna directamente desde el campo "TypeForm". |
UserAgent |
network.http.parsed_user_agent | Se analizó a partir del campo "UserAgent" con el filtro "useragent". |
UserAgent |
network.http.user_agent | Se asigna directamente desde el campo "UserAgent". |
WorkdayAccount |
target.user.user_display_name | El nombre visible del usuario se extrae del campo "WorkdayAccount" con un patrón de Grok y se asigna a "target.user.user_display_name". |
WorkdayAccount |
target.user.userid | El ID de usuario se extrae del campo "WorkdayAccount" con un patrón de Grok y se asigna a "target.user.userid". |
additional.fields.additional1.key | Se establece en "FailedSignOn". | |
additional.fields.additional2.key | Se establece en "InvalidCredentials". | |
additional.fields.additional3.key | Se establece en "AccountLocked". | |
additional.fields.additional4.key | Establece el valor en "Enterprise Interface Builder". | |
metadata.event_type | Se establece en "GENERIC_EVENT" inicialmente y, luego, se actualiza según la lógica que involucra otros campos. | |
metadata.event_type | Se establece en "USER_CHANGE_PERMISSIONS" para tipos de eventos específicos. | |
metadata.event_type | Se establece en "RESOURCE_WRITTEN" para tipos de eventos específicos. | |
metadata.log_type | Se codifica como "WORKDAY_AUDIT". | |
metadata.product_name | Está codificado como "Enterprise Interface Builder". | |
metadata.vendor_name | Está codificado como "Día laboral". | |
principal.asset.category | Se establece en "Teléfono" si el campo "DeviceType" es "Teléfono". | |
principal.resource.resource_type | Se codifica como "TASK" si el campo "ScheduledProcess" no está vacío. | |
security_result.action | Se establece en "ALLOW" o "FAIL" según los valores de los campos "FailedSignOn", "IsDeviceManaged", "InvalidCredentials" y "AccountLocked". | |
security_result.summary | Se establece en "Correcto" o en mensajes de error específicos según los valores de los campos "FailedSignOn", "IsDeviceManaged", "InvalidCredentials" y "AccountLocked". | |
target.resource.resource_type | Se codifica como "TASK" para tipos de eventos específicos. | |
target.resource.resource_type | Se codifica como "DATASET" si el campo "TypeForm" no está vacío. | |
message |
principal.user.email_addresses | Extrae la dirección de correo electrónico del campo "message" con un patrón de Grok y la combina en "principal.user.email_addresses" si coincide con un patrón específico. |
message |
src.user.userid | Borra el campo si el campo "event.idm.read_only_udm.principal.user.userid" coincide con el "user_target" extraído del campo "message". |
message |
src.user.user_display_name | Borra el campo si el campo "event.idm.read_only_udm.principal.user.userid" coincide con el "user_target" extraído del campo "message". |
message |
target.user.userid | Extrae el ID de usuario del campo "message" con un patrón de Grok y lo asigna a "target.user.userid" si coincide con un patrón específico. |
¿Necesitas más ayuda? Obtén respuestas de miembros de la comunidad y profesionales de Google SecOps.