Collecter les journaux d'audit Workday
Ce document explique comment ingérer des journaux d'audit Workday dans Google Security Operations à l'aide d'AWS S3. L'analyseur identifie d'abord le type d'événement spécifique à partir des journaux en se basant sur l'analyse des modèles des données CSV. Il extrait et structure ensuite les champs pertinents en fonction du type identifié, en les mappant à un modèle de données unifié (UDM) pour une analyse de sécurité cohérente.
Avant de commencer
Assurez-vous de remplir les conditions suivantes :
- Instance Google SecOps
- Accès privilégié à AWS
- Accès privilégié à Workday
Configurer un bucket AWS S3 et IAM pour Google SecOps
- Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
- Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple,
workday-audit-logs
). - Créez un utilisateur en suivant ce guide de l'utilisateur : Créer un utilisateur IAM.
- Sélectionnez l'utilisateur créé.
- Sélectionnez l'onglet Informations d'identification de sécurité.
- Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
- Sélectionnez Service tiers comme Cas d'utilisation.
- Cliquez sur Suivant.
- Facultatif : Ajoutez une balise de description.
- Cliquez sur Créer une clé d'accès.
- Cliquez sur Télécharger le fichier CSV pour enregistrer la clé d'accès et la clé d'accès secrète pour référence ultérieure.
- Cliquez sur OK.
- Sélectionnez l'onglet Autorisations.
- Cliquez sur Ajouter des autorisations dans la section Règles relatives aux autorisations.
- Sélectionnez Ajouter des autorisations.
- Sélectionnez Joindre directement des règles.
- Recherchez et sélectionnez la règle AmazonS3FullAccess.
- Cliquez sur Suivant.
- Cliquez sur Ajouter des autorisations.
Créer un utilisateur du système d'intégration Workday
- Dans Workday, recherchez Create Integration System User > OK.
- Renseignez le champ Nom d'utilisateur (par exemple,
audit_s3_user
). - Cliquez sur OK.
- Pour réinitialiser le mot de passe, accédez à Actions associées > Sécurité > Réinitialiser le mot de passe.
- Sélectionnez Conserver les règles relatives aux mots de passe pour empêcher l'expiration du mot de passe.
- Recherchez Create Security Group> Integration System Security Group (Unconstrained) (Créer un groupe de sécurité > Groupe de sécurité du système d'intégration (sans restriction)).
- Attribuez un nom (par exemple,
ISU_Audit_S3
) et ajoutez l'utilisateur du système d'intégration à Utilisateurs du système d'intégration. - Recherchez Domain Security Policies for Functional Area > System (Règles de sécurité du domaine pour la zone fonctionnelle > Système).
- Pour Audit Trail (Journal d'audit), sélectionnez Actions > Modifier les autorisations.
- Sous Get Only, ajoutez le groupe
ISU_Audit_S3
. - Cliquez sur OK> Activer les modifications en attente de la stratégie de sécurité.
Configurer un rapport personnalisé Workday
- Dans Workday, recherchez Create Custom Report (Créer un rapport personnalisé).
- Fournissez les informations de configuration suivantes :
- Nom : saisissez un nom unique (par exemple,
Audit_Trail_BP_JSON
). - Type : sélectionnez Avancé.
- Source de données : sélectionnez Piste d'audit – Processus métier.
- Cliquez sur OK.
- Facultatif : Ajoutez des filtres sur Type de processus métier ou Date d'entrée en vigueur.
- Nom : saisissez un nom unique (par exemple,
- Accédez à l'onglet Sortie.
- Sélectionnez Activer en tant que service Web, Optimisé pour les performances, puis Format JSON.
- Cliquez sur OK > OK.
- Ouvrez le rapport, puis cliquez sur Partager > ajoutez
ISU_Audit_S3
avec l'autorisation Afficher > OK. - Accédez à Actions associées > Service Web > Afficher les URL.
- Copiez l'URL JSON (par exemple,
https://wd-services1.workday.com/ccx/service/customreport2/<tenant>/<user>/Audit_Trail_BP_JSON?format=json
).
Configurer la stratégie et le rôle IAM pour les importations S3
JSON de la règle (remplacez
workday-audit-logs
si vous avez saisi un autre nom de bucket) :{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutWorkdayObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::workday-audit-logs/*" } ] }
Accédez à la console AWS> IAM> Policies> Create policy> onglet JSON.
Copiez et collez le règlement.
Cliquez sur Suivant > Créer une règle.
Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.
Associez la règle nouvellement créée.
Nommez le rôle
WriteWorkdayToS3Role
, puis cliquez sur Créer un rôle.
Créer la fonction Lambda
Paramètre | Valeur |
---|---|
Nom | workday_audit_to_s3 |
Durée d'exécution | Python 3.13 |
Architecture | x86_64 |
Rôle d'exécution | WriteWorkdayToS3Role |
Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et collez le code ci-dessous (
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()
Accédez à Configuration > Variables d'environnement > Modifier > Ajouter une variable d'environnement.
Saisissez les variables d'environnement suivantes en remplaçant par votre valeur.
Variables d'environnement
Clé Exemples de valeurs WD_USER
audit_s3_user
WD_PASS
Wrokday-Password
WD_URL
https://.../Audit_Trail_BP_JSON?format=json
S3_BUCKET_NAME
workday-audit-logs
Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre‑fonction).
Accédez à l'onglet Configuration.
Dans le panneau Configuration générale, cliquez sur Modifier.
Définissez le délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.
Planifier la fonction Lambda (EventBridge Scheduler)
- Accédez à Configuration> Déclencheurs> Ajouter un déclencheur> EventBridge Scheduler> Créer une règle.
- Fournissez les informations de configuration suivantes :
- Nom :
daily-workday-audit export
. - Modèle de programmation : expression Cron.
- Expression :
20 2 * * ? *
(s'exécute tous les jours à 02h20 UTC).
- Nom :
- Conservez les autres valeurs par défaut, puis cliquez sur Créer.
Configurer un flux dans Google SecOps pour ingérer les journaux d'audit Workday
- Accédez à Paramètres SIEM> Flux.
- Cliquez sur + Ajouter un flux.
- Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple,
Workday Audit Logs
). - Sélectionnez Amazon S3 V2 comme type de source.
- Sélectionnez Audit Workday comme Type de journal.
- Cliquez sur Créer un compte de service.
- Cliquez sur Suivant.
- Spécifiez les valeurs des paramètres d'entrée suivants :
- URI S3 : URI du bucket
s3://workday-audit-logs/
.- Remplacez
workday-audit-logs
par le nom réel du bucket.
- Remplacez
- Options de suppression de la source : sélectionnez l'option de suppression de votre choix.
- Âge maximal des fichiers : incluez les fichiers modifiés au cours des derniers jours. La valeur par défaut est de 180 jours.
- ID de clé d'accès : clé d'accès utilisateur ayant accès au bucket S3.
- Clé d'accès secrète : clé secrète de l'utilisateur ayant accès au bucket S3.
- Espace de noms de l'élément : espace de noms de l'élément.
- Libellés d'ingestion : libellé à appliquer aux événements de ce flux.
- URI S3 : URI du bucket
- Cliquez sur Suivant.
- Vérifiez la configuration de votre nouveau flux sur l'écran Finaliser, puis cliquez sur Envoyer.
Table de mappage UDM
Champ de journal | Mappage UDM | Logique |
---|---|---|
Account |
metadata.event_type | Si le champ "Compte" n'est pas vide, le champ "metadata.event_type" est défini sur "USER_RESOURCE_UPDATE_CONTENT". |
Account |
principal.user.primaryId | L'ID utilisateur est extrait du champ "Compte" à l'aide d'un modèle Grok et mappé à principal.user.primaryId . |
Account |
principal.user.primaryName | Le nom à afficher de l'utilisateur est extrait du champ "Compte" à l'aide d'un modèle Grok et mappé sur "principal.user.primaryName". |
ActivityCategory |
metadata.event_type | Si le champ "ActivityCategory" est défini sur "READ", le champ "metadata.event_type" est défini sur "RESOURCE_READ". Si la valeur est "WRITE", elle est définie sur "RESOURCE_WRITTEN". |
ActivityCategory |
metadata.product_event_type | Directement mappé à partir du champ "ActivityCategory". |
AffectedGroups |
target.user.group_identifiers | Mappé directement à partir du champ "AffectedGroups". |
Area |
target.resource.attribute.labels.area.value | Mappé directement à partir du champ "Zone". |
AuthType |
extensions.auth.auth_details | Mappé directement à partir du champ "AuthType". |
AuthType |
extensions.auth.type | Mappé à partir du champ "AuthType" vers différents types d'authentification définis dans l'UDM en fonction de valeurs spécifiques. |
CFIPdeConexion |
src.domain.name | Si le champ "CFIPdeConexion" n'est pas une adresse IP valide, il est mappé sur "src.domain.name". |
CFIPdeConexion |
target.ip | Si le champ "CFIPdeConexion" est une adresse IP valide, il est mappé à "target.ip". |
ChangedRelationship |
metadata.description | Mappé directement à partir du champ "ChangedRelationship". |
ClassOfInstance |
target.resource.attribute.labels.class_instance.value | Mappé directement à partir du champ "ClassOfInstance". |
column18 |
about.labels.utub.value | Mappé directement à partir du champ "column18". |
CreatedBy |
principal.user.userid | L'ID utilisateur est extrait du champ "CreatedBy" (Créé par) à l'aide d'un modèle Grok et mappé sur "principal.user.userid". |
CreatedBy |
principal.user.user_display_name | Le nom à afficher de l'utilisateur est extrait du champ "CreatedBy" à l'aide d'un modèle Grok et mappé à "principal.user.user_display_name". |
Domain |
about.domain.name | Mappé directement à partir du champ "Domaine". |
EffectiveDate |
@timestamp | Analysé dans "@timestamp" après conversion au format "aaaa-MM-jj HH:mm:ss.SSSZ". |
EntryMoment |
@timestamp | Analysé dans "@timestamp" après conversion au format "ISO8601". |
EventType |
security_result.description | Mappé directement à partir du champ "EventType". |
Form |
target.resource.name | Mappé directement à partir du champ "Formulaire". |
InstancesAdded |
about.resource.attribute.labels.instances_added.value | Mappé directement à partir du champ "InstancesAdded". |
InstancesAdded |
target.user.attribute.roles.instances_added.name | Mappé directement à partir du champ "InstancesAdded". |
InstancesRemoved |
about.resource.attribute.labels.instances_removed.value | Mappé directement à partir du champ "InstancesRemoved". |
InstancesRemoved |
target.user.attribute.roles.instances_removed.name | Mappé directement à partir du champ "InstancesRemoved". |
IntegrationEvent |
target.resource.attribute.labels.integration_event.value | Mappé directement à partir du champ "IntegrationEvent". |
IntegrationStatus |
security_result.action_details | Mappé directement à partir du champ "IntegrationStatus". |
IntegrationSystem |
target.resource.name | Mappé directement à partir du champ "IntegrationSystem". |
IP |
src.domain.name | Si le champ "IP" n'est pas une adresse IP valide, il est mappé sur "src.domain.name". |
IP |
src.ip | Si le champ "IP" est une adresse IP valide, il est mappé sur "src.ip". |
IsDeviceManaged |
additional.fields.additional1.value.string_value | Si le champ "IsDeviceManaged" est défini sur "N", la valeur est définie sur "Successful" (Réussie). Sinon, la valeur est définie sur "Échec de la connexion". |
IsDeviceManaged |
additional.fields.additional2.value.string_value | Si le champ "IsDeviceManaged" est défini sur "N", la valeur est définie sur "Successful" (Réussie). Sinon, elle est définie sur "Invalid Credentials" (Identifiants non valides). |
IsDeviceManaged |
additional.fields.additional3.value.string_value | Si le champ "IsDeviceManaged" est défini sur "N", la valeur est définie sur "Successful" (Réussie). Sinon, il est défini sur "Compte verrouillé". |
IsDeviceManaged |
security_result.action_details | Mappé directement à partir du champ "IsDeviceManaged". |
OutputFiles |
about.file.full_path | Mappé directement à partir du champ "OutputFiles". |
Person |
principal.user.primaryId | Si le champ "Personne" commence par "INT", l'ID utilisateur est extrait à l'aide d'un modèle Grok et mappé sur "principal.user.primaryId". |
Person |
principal.user.primaryName | Si le champ "Personne" commence par "INT", le nom à afficher de l'utilisateur est extrait à l'aide d'un modèle Grok et mappé sur "principal.user.primaryName". |
Person |
principal.user.user_display_name | Si le champ "Personne" ne commence pas par "INT", il est directement mappé à "principal.user.user_display_name". |
Person |
metadata.event_type | Si le champ "Personne" n'est pas vide, le champ "metadata.event_type" est défini sur "USER_RESOURCE_UPDATE_CONTENT". |
ProcessedTransaction |
target.resource.attribute.creation_time | Analysé en "target.resource.attribute.creation_time" après conversion au format "jj/MM/aaaa HH:mm:ss,SSS (ZZZ)", "jj/MM/aaaa, HH:mm:ss,SSS (ZZZ)" ou "MM/jj/aaaa, HH:mm:ss.SSS A ZZZ". |
ProgramBy |
principal.user.userid | Mappé directement à partir du champ "ProgramBy". |
RecurrenceEndDate |
principal.resource.attribute.last_update_time | Analysé en "principal.resource.attribute.last_update_time" après conversion au format "yyyy-MM-dd". |
RecurrenceStartDate |
principal.resource.attribute.creation_time | Analysé en "principal.resource.attribute.creation_time" après conversion au format "aaaa-MM-jj". |
RequestName |
metadata.description | Mappé directement à partir du champ "RequestName". |
ResponseMessage |
security_result.summary | Mappé directement à partir du champ "ResponseMessage". |
RestrictedToEnvironment |
security_result.about.hostname | Mappé directement à partir du champ "RestrictedToEnvironment". |
RevokedSecurity |
security_result.outcomes.outcomes.value | Directement mappé à partir du champ "RevokedSecurity". |
RunFrequency |
principal.resource.attribute.labels.run_frequency.value | Mappé directement à partir du champ "RunFrequency". |
ScheduledProcess |
principal.resource.name | Mappé directement à partir du champ "ScheduledProcess". |
SecuredTaskExecuted |
target.resource.name | Mappé directement à partir du champ "SecuredTaskExecuted". |
SecureTaskExecuted |
metadata.event_type | Si le champ "SecureTaskExecuted" contient "Create", le champ "metadata.event_type" est défini sur "USER_RESOURCE_CREATION". |
SecureTaskExecuted |
target.resource.name | Mappé directement à partir du champ "SecureTaskExecuted". |
SentTime |
@timestamp | Analysé dans "@timestamp" après conversion au format "ISO8601". |
SessionId |
network.session_id | Mappé directement à partir du champ "SessionId". |
ShareBy |
target.user.userid | Mappé directement à partir du champ "ShareBy". |
SignOffTime |
additional.fields.additional4.value.string_value | La valeur du champ "AuthFailMessage" est placée dans le tableau "additional.fields" avec la clé "Enterprise Interface Builder". |
SignOffTime |
metadata.description | Mappé directement à partir du champ "AuthFailMessage". |
SignOffTime |
metadata.event_type | Si le champ "SignOffTime" est vide, le champ "metadata.event_type" est défini sur "USER_LOGIN". Sinon, la valeur est définie sur "USER_LOGOUT". |
SignOffTime |
principal.user.attribute.last_update_time | Analysé dans "principal.user.attribute.last_update_time" après conversion au format "ISO8601". |
SignOnIp |
src.domain.name | Si le champ "SignOnIp" n'est pas une adresse IP valide, il est mappé sur "src.domain.name". |
SignOnIp |
src.ip | Si le champ "SignOnIp" est une adresse IP valide, il est mappé sur "src.ip". |
Status |
metadata.product_event_type | Mappé directement à partir du champ "État". |
SystemAccount |
principal.user.email_addresses | L'adresse e-mail est extraite du champ "SystemAccount" à l'aide d'un modèle grok et mappée sur "principal.user.email_addresses". |
SystemAccount |
principal.user.primaryId | L'ID utilisateur est extrait du champ "SystemAccount" à l'aide d'un modèle Grok et mappé sur "principal.user.primaryId". |
SystemAccount |
principal.user.primaryName | Le nom à afficher de l'utilisateur est extrait du champ "SystemAccount" à l'aide d'un modèle Grok et mappé sur "principal.user.primaryName". |
SystemAccount |
src.user.userid | L'ID utilisateur secondaire est extrait du champ "SystemAccount" à l'aide d'un modèle Grok et mappé à "src.user.userid". |
SystemAccount |
src.user.user_display_name | Le nom à afficher de l'utilisateur secondaire est extrait du champ "SystemAccount" à l'aide d'un modèle Grok et mappé sur "src.user.user_display_name". |
SystemAccount |
target.user.userid | L'ID utilisateur cible est extrait du champ "SystemAccount" à l'aide d'un modèle Grok et mappé sur "target.user.userid". |
Target |
target.user.user_display_name | Mappé directement à partir du champ "Cible". |
Template |
about.resource.name | Mappé directement à partir du champ "Modèle". |
Tenant |
target.asset.hostname | Mappé directement à partir du champ "Locataire". |
TlsVersion |
network.tls.version | Mappé directement à partir du champ "TlsVersion". |
Transaction |
security_result.action_details | Directement mappé à partir du champ "Transaction". |
TransactionType |
security_result.summary | Directement mappé à partir du champ "TransactionType". |
TypeForm |
target.resource.resource_subtype | Mappé directement à partir du champ "TypeForm". |
UserAgent |
network.http.parsed_user_agent | Analysé à partir du champ "UserAgent" à l'aide du filtre "useragent". |
UserAgent |
network.http.user_agent | Mappé directement à partir du champ "UserAgent". |
WorkdayAccount |
target.user.user_display_name | Le nom à afficher de l'utilisateur est extrait du champ "WorkdayAccount" à l'aide d'un modèle Grok et mappé sur "target.user.user_display_name". |
WorkdayAccount |
target.user.userid | L'ID utilisateur est extrait du champ "WorkdayAccount" à l'aide d'un modèle Grok et mappé sur "target.user.userid". |
additional.fields.additional1.key | Défini sur "FailedSignOn". | |
additional.fields.additional2.key | Défini sur "InvalidCredentials". | |
additional.fields.additional3.key | Défini sur "AccountLocked". | |
additional.fields.additional4.key | Définissez-le sur "Enterprise Interface Builder". | |
metadata.event_type | Défini sur "GENERIC_EVENT" au départ, puis mis à jour en fonction de la logique impliquant d'autres champs. | |
metadata.event_type | Définissez la valeur sur "USER_CHANGE_PERMISSIONS" pour des types d'événements spécifiques. | |
metadata.event_type | Définissez la valeur sur "RESOURCE_WRITTEN" pour des types d'événements spécifiques. | |
metadata.log_type | Codé en dur sur "WORKDAY_AUDIT". | |
metadata.product_name | Codé en dur sur "Enterprise Interface Builder". | |
metadata.vendor_name | Codé en dur sur "Workday". | |
principal.asset.category | Définissez la valeur sur "Téléphone" si le champ "DeviceType" est défini sur "Téléphone". | |
principal.resource.resource_type | Codé en dur sur "TASK" si le champ "ScheduledProcess" n'est pas vide. | |
security_result.action | Définissez la valeur sur "ALLOW" ou "FAIL" en fonction des valeurs des champs "FailedSignOn", "IsDeviceManaged", "InvalidCredentials" et "AccountLocked". | |
security_result.summary | Définissez la valeur sur "Traitement effectué" ou sur des messages d'erreur spécifiques en fonction des valeurs des champs "FailedSignOn", "IsDeviceManaged", "InvalidCredentials" et "AccountLocked". | |
target.resource.resource_type | Codé en dur sur "TASK" pour certains types d'événements. | |
target.resource.resource_type | Codé en dur sur "DATASET" si le champ "TypeForm" n'est pas vide. | |
message |
principal.user.email_addresses | Extrait l'adresse e-mail du champ "message" à l'aide d'un modèle Grok et la fusionne dans "principal.user.email_addresses" si un modèle spécifique est trouvé. |
message |
src.user.userid | Efface le champ si le champ "event.idm.read_only_udm.principal.user.userid" correspond à "user_target" extrait du champ "message". |
message |
src.user.user_display_name | Efface le champ si le champ "event.idm.read_only_udm.principal.user.userid" correspond à "user_target" extrait du champ "message". |
message |
target.user.userid | Extrait l'ID utilisateur du champ "message" à l'aide d'un modèle Grok et le mappe à "target.user.userid" si un modèle spécifique est trouvé. |
Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.