Recoger registros de auditoría de Workday
En este documento se explica cómo ingerir registros de auditoría de Workday en Google Security Operations mediante AWS S3. En primer lugar, el analizador identifica el tipo de evento específico de los registros en función del análisis de patrones de los datos CSV. A continuación, extrae y estructura los campos relevantes según el tipo identificado, asignándolos a un modelo de datos unificado (UDM) para realizar un análisis de seguridad coherente.
Antes de empezar
Asegúrate de que cumples los siguientes requisitos previos:
- Instancia de Google SecOps
- Acceso privilegiado a AWS
- Acceso privilegiado a Workday
Configurar un segmento de AWS S3 y IAM para Google SecOps
- Crea un segmento de Amazon S3 siguiendo esta guía de usuario: Crear un segmento.
- Guarda el nombre y la región del segmento para consultarlos más adelante (por ejemplo,
workday-audit-logs
). - Crea un usuario siguiendo esta guía: Crear un usuario de gestión de identidades y accesos.
- Selecciona el usuario creado.
- Selecciona la pestaña Credenciales de seguridad.
- En la sección Claves de acceso, haz clic en Crear clave de acceso.
- Selecciona Servicio de terceros como Caso práctico.
- Haz clic en Siguiente.
- Opcional: añade 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 futuras consultas.
- Haz clic en Listo.
- Selecciona la pestaña Permisos.
- Haz clic en Añadir permisos en la sección Políticas de permisos.
- Selecciona Añadir permisos.
- Seleccione Adjuntar políticas directamente.
- Busca y selecciona la política AmazonS3FullAccess.
- Haz clic en Siguiente.
- Haz clic en Añadir permisos.
Crear un usuario del sistema de integración de Workday
- En Workday, busca Create Integration System User (Crear usuario del sistema de integración) > OK (Aceptar).
- Rellena el campo 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 caduque.
- Busca Create Security Group > Integration System Security Group (Unconstrained) (Crear grupo de seguridad > Grupo de seguridad del sistema de integración [sin restricciones]).
- Proporciona un nombre (por ejemplo,
ISU_Audit_S3
) y añade el usuario del sistema de integración a Integration System Users (Usuarios del sistema de integración). - Busca Políticas de seguridad de dominio para el área funcional > Sistema.
- En Registro de auditoría, selecciona Acciones > Editar permisos.
- En Obtener solo, añade el grupo
ISU_Audit_S3
. - Haz clic en Aceptar > Activar cambios pendientes en la política de seguridad.
Configurar un informe personalizado de Workday
- En Workday, busca Create Custom Report (Crear informe personalizado).
- Proporcione los siguientes detalles de configuración:
- Nombre: introduzca un nombre único (por ejemplo,
Audit_Trail_BP_JSON
). - Tipo: selecciona Avanzado.
- Fuente de datos: selecciona Registro de auditoría: proceso empresarial.
- Haz clic en Aceptar.
- Opcional: Añade filtros en Tipo de proceso de empresa o Fecha de entrada en vigor.
- Nombre: introduzca un nombre único (por ejemplo,
- Ve a la pestaña Salida.
- Selecciona Habilitar como servicio web y Optimizado para el rendimiento y, a continuación, Formato JSON.
- Haz clic en Aceptar > Hecho.
- Abre el informe y haz clic en Compartir > añade
ISU_Audit_S3
con permiso de lectura > Aceptar. - Vaya a Acciones relacionadas > Servicio web > Ver URLs.
- Copia la URL JSON (por ejemplo,
https://wd-services1.workday.com/ccx/service/customreport2/<tenant>/<user>/Audit_Trail_BP_JSON?format=json
).
Configurar la política y el rol de gestión de identidades y accesos para las subidas de S3
JSON de la política (sustituye
workday-audit-logs
si has introducido otro nombre de contenedor):{ "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.
Adjunte la política que acaba de crear.
Dale el nombre
WriteWorkdayToS3Role
al rol y haz clic en Crear rol.
Crear la función Lambda
Ajuste | Valor |
---|---|
Nombre | workday_audit_to_s3 |
Tiempo de ejecución | Python 3.13 |
Arquitectura | x86_64 |
Rol de ejecución | WriteWorkdayToS3Role |
Una vez creada la función, abre la pestaña Código, elimina el stub y pega el código que aparece más abajo (
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 > Añadir nueva variable de entorno.
Introduce las siguientes variables de entorno y sustituye los valores por los tuyos.
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
Una vez creada la función, permanece en su página (o abre Lambda > Funciones > tu‑función).
Seleccione 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.
Programar la función Lambda (EventBridge Scheduler)
- Ve a Configuración > Activadores > Añadir activador > EventBridge Scheduler > Crear regla.
- Proporcione 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 todos los días a las 02:20 UTC).
- Nombre:
- Deja el resto de los ajustes como están y haz clic en Crear.
Configurar un feed en Google SecOps para ingerir registros de auditoría de Workday
- Ve a Configuración de SIEM > Feeds.
- Haz clic en + Añadir nuevo feed.
- En el campo Nombre del feed, introduce un nombre para el feed (por ejemplo,
Workday Audit Logs
). - Selecciona Amazon S3 V2 como Tipo de fuente.
- Seleccione Auditoría de Workday como Tipo de registro.
- Haz clic en Obtener una cuenta de servicio.
- Haz clic en Siguiente.
- Especifique valores para los siguientes parámetros de entrada:
- URI de S3: el URI del contenedor
s3://workday-audit-logs/
.- Sustituye
workday-audit-logs
por el nombre real del segmento.
- Sustituye
- Opciones de eliminación de la fuente: selecciona la opción de eliminación que prefieras.
- Antigüedad máxima del archivo: incluye los archivos modificados en los últimos días. El valor predeterminado es 180 días.
- ID de clave de acceso: clave de acceso de usuario con acceso al segmento de S3.
- Clave de acceso secreta: clave secreta del usuario con acceso al segmento de S3.
- Espacio de nombres de recursos: el espacio de nombres de recursos.
- Etiquetas de ingestión: etiqueta que se aplicará a los eventos de este feed.
- URI de S3: el URI del contenedor
- Haz clic en Siguiente.
- Revise la configuración de la nueva fuente en la pantalla Finalizar y, a continuación, haga 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 define como "USER_RESOURCE_UPDATE_CONTENT". |
Account |
principal.user.primaryId | El ID de usuario se extrae del campo "Account" (Cuenta) mediante un patrón grok y se asigna a principal.user.primaryId . |
Account |
principal.user.primaryName | El nombre visible del usuario se extrae del campo "Account" mediante un patrón grok y se asigna a "principal.user.primaryName". |
ActivityCategory |
metadata.event_type | Si el valor del campo "ActivityCategory" es "READ", el valor del campo "metadata.event_type" es "RESOURCE_READ". Si es "WRITE", se asigna el valor "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 del campo "AuthType" a diferentes tipos de autenticación definidos en UDM en función de 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" mediante 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" mediante 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 analiza en "@timestamp" después de convertirlo al formato "aaaa-MM-dd HH:mm:ss.SSSZ". |
EntryMoment |
@timestamp | Se analiza en "@timestamp" después de convertirlo al formato "ISO8601". |
EventType |
security_result.description | Se asigna directamente desde el campo "EventType". |
Form |
target.resource.name | Se asigna directamente desde el campo "Form" (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 define como "Successful". De lo contrario, se asigna el valor "Se ha producido un error al iniciar sesión". |
IsDeviceManaged |
additional.fields.additional2.value.string_value | Si el campo "IsDeviceManaged" es "N", el valor se define como "Successful". De lo contrario, se le asigna el valor "Credenciales no válidas". |
IsDeviceManaged |
additional.fields.additional3.value.string_value | Si el campo "IsDeviceManaged" es "N", el valor se define como "Successful". De lo contrario, se le asigna el valor "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 "Person" empieza por "INT", el ID de usuario se extrae mediante un patrón grok y se asigna a "principal.user.primaryId". |
Person |
principal.user.primaryName | Si el campo "Person" empieza por "INT", el nombre visible del usuario se extrae mediante un patrón grok y se asigna a "principal.user.primaryName". |
Person |
principal.user.user_display_name | Si el campo "Person" no empieza por "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 define como "USER_RESOURCE_UPDATE_CONTENT". |
ProcessedTransaction |
target.resource.attribute.creation_time | Se analiza como "target.resource.attribute.creation_time" después de convertirlo al formato "dd/MM/yyyy HH:mm:ss,SSS (ZZZ)", "dd/MM/yyyy, HH:mm:ss,SSS (ZZZ)" o "MM/dd/yyyy, 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 ha analizado como "principal.resource.attribute.last_update_time" después de convertirlo al formato "aaaa-MM-dd". |
RecurrenceStartDate |
principal.resource.attribute.creation_time | Se ha analizado como "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 define como "USER_RESOURCE_CREATION". |
SecureTaskExecuted |
target.resource.name | Se asigna directamente desde el campo "SecureTaskExecuted". |
SentTime |
@timestamp | Se analiza en "@timestamp" después de convertirlo 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 en la matriz "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 define como "USER_LOGIN". De lo contrario, se le asigna el valor "USER_LOGOUT". |
SignOffTime |
principal.user.attribute.last_update_time | Se ha analizado como "principal.user.attribute.last_update_time" después de convertirlo 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 se extrae del campo "SystemAccount" mediante un patrón grok y se asigna a "principal.user.email_addresses". |
SystemAccount |
principal.user.primaryId | El ID de usuario se extrae del campo "SystemAccount" mediante 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" mediante un patrón grok y se asigna a "principal.user.primaryName". |
SystemAccount |
src.user.userid | El ID de usuario secundario se extrae del campo "SystemAccount" mediante un patrón 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" mediante un patrón grok y se asigna a "src.user.user_display_name". |
SystemAccount |
target.user.userid | El ID de usuario de destino se extrae del campo "SystemAccount" mediante un patrón grok y se asigna a "target.user.userid". |
Target |
target.user.user_display_name | Se asigna directamente desde el campo "Target" (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 ha analizado a partir del campo "UserAgent" mediante 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" mediante un patrón grok y se asigna a "target.user.user_display_name". |
WorkdayAccount |
target.user.userid | El ID de usuario se extrae del campo "WorkdayAccount" mediante un patrón grok y se asigna a "target.user.userid". |
additional.fields.additional1.key | Se ha definido como "FailedSignOn". | |
additional.fields.additional2.key | Se establece en "InvalidCredentials". | |
additional.fields.additional3.key | Se ha asignado el valor "AccountLocked". | |
additional.fields.additional4.key | Selecciona "Enterprise Interface Builder". | |
metadata.event_type | Se define como "GENERIC_EVENT" al principio y, después, se actualiza en función de la lógica que implican otros campos. | |
metadata.event_type | Se asigna el valor "USER_CHANGE_PERMISSIONS" a tipos de eventos específicos. | |
metadata.event_type | Se establece en "RESOURCE_WRITTEN" para tipos de eventos específicos. | |
metadata.log_type | Codificado como "WORKDAY_AUDIT". | |
metadata.product_name | Codificado como "Enterprise Interface Builder". | |
metadata.vendor_name | Codificado como "Workday". | |
principal.asset.category | Selecciona "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 define como "ALLOW" o "FAIL" en función de los valores de los campos "FailedSignOn", "IsDeviceManaged", "InvalidCredentials" y "AccountLocked". | |
security_result.summary | Se define como "Successful" (Completado) o como mensajes de error específicos en función de los valores de los campos "FailedSignOn" (Inicio de sesión fallido), "IsDeviceManaged" (¿El dispositivo está gestionado?), "InvalidCredentials" (Credenciales no válidas) y "AccountLocked" (Cuenta bloqueada). | |
target.resource.resource_type | Se ha codificado 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 del campo "message" mediante un patrón grok y la combina en "principal.user.email_addresses" si se encuentra 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 campo "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 campo "user_target" extraído del campo "message". |
message |
target.user.userid | Extrae el ID de usuario del campo "message" mediante un patrón grok y lo asigna a "target.user.userid" si se encuentra un patrón específico. |
¿Necesitas más ayuda? Recibe respuestas de los miembros de la comunidad y de los profesionales de Google SecOps.