Collecter les journaux d'opération Zoom
Ce document explique comment ingérer des journaux d'opération Zoom dans Google Security Operations à l'aide d'Amazon S3. L'analyseur transforme les journaux bruts en modèle de données unifié (UDM). Il extrait les champs du message de journal brut, nettoie et normalise les données, puis mappe les informations extraites aux champs UDM correspondants. Il enrichit ainsi les données pour l'analyse et la corrélation dans un système SIEM.
Avant de commencer
Assurez-vous de remplir les conditions suivantes :
- Instance Google SecOps
- Accès privilégié à Zoom
- Accès privilégié à AWS (S3, IAM, Lambda, EventBridge)
Collecter les journaux d'opération Zoom (ID, clés API, ID d'organisation, jetons)
- Connectez-vous à la Zoom App Marketplace.
- Accédez à Développer > Créer une application > OAuth serveur à serveur.
- Créez l'application et ajoutez le champ d'application suivant :
report:read:operation_logs:admin
(oureport:read:admin
). - Dans Identifiants de l'application, copiez et enregistrez les informations suivantes dans un emplacement sécurisé :
- Numéro de compte :
- ID client.
- Code secret du client.
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,
zoom-operation-logs
). - Créez un utilisateur en suivant ce guide : 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 un tag 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 une utilisation ultérieure.
- Cliquez sur OK.
- Sélectionnez l'onglet Autorisations.
- Cliquez sur Ajouter des autorisations dans la section Règles d'autorisation.
- 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.
Configurer la stratégie et le rôle IAM pour les importations S3
- Dans la console AWS, accédez à IAM > Stratégies > Créer une stratégie > Onglet JSON.
Saisissez la règle suivante :
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutZoomOperationLogs", "Effect": "Allow", "Action": ["s3:PutObject"], "Resource": "arn:aws:s3:::zoom-operation-logs/zoom/operationlogs/*" }, { "Sid": "AllowStateReadWrite", "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::zoom-operation-logs/zoom/operationlogs/state.json" } ] }
- Remplacez
zoom-operation-logs
si vous avez saisi un autre nom de bucket.
- Remplacez
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 que vous venez de créer.
Nommez le rôle
WriteZoomOperationLogsToS3Role
, puis cliquez sur Créer un rôle.
Créer la fonction Lambda
- Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
- Cliquez sur Créer à partir de zéro.
- Fournissez les informations de configuration suivantes :
Paramètre | Valeur |
---|---|
Nom | zoom_operationlogs_to_s3 |
Durée d'exécution | Python 3.13 |
Architecture | x86_64 |
Rôle d'exécution | WriteZoomOperationLogsToS3Role |
Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et saisissez le code suivant(
zoom_operationlogs_to_s3.py
) :#!/usr/bin/env python3 import os, json, gzip, io, uuid, datetime as dt, base64, urllib.parse, urllib.request import boto3 # ---- Environment ---- S3_BUCKET = os.environ["S3_BUCKET"] S3_PREFIX = os.environ.get("S3_PREFIX", "zoom/operationlogs/") STATE_KEY = os.environ.get("STATE_KEY", S3_PREFIX + "state.json") ZOOM_ACCOUNT_ID = os.environ["ZOOM_ACCOUNT_ID"] ZOOM_CLIENT_ID = os.environ["ZOOM_CLIENT_ID"] ZOOM_CLIENT_SECRET = os.environ["ZOOM_CLIENT_SECRET"] PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "300")) # API default 30; max may vary TIMEOUT = int(os.environ.get("TIMEOUT", "30")) TOKEN_URL = "https://zoom.us/oauth/token" REPORT_URL = "https://api.zoom.us/v2/report/operationlogs" s3 = boto3.client("s3") # ---- Helpers ---- def _http(req: urllib.request.Request): return urllib.request.urlopen(req, timeout=TIMEOUT) def get_token() -> str: params = urllib.parse.urlencode({ "grant_type": "account_credentials", "account_id": ZOOM_ACCOUNT_ID, }).encode() basic = base64.b64encode(f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}".encode()).decode() req = urllib.request.Request( TOKEN_URL, data=params, headers={ "Authorization": f"Basic {basic}", "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "Host": "zoom.us", }, method="POST", ) with _http(req) as r: body = json.loads(r.read()) return body["access_token"] def get_state() -> dict: try: obj = s3.get_object(Bucket=S3_BUCKET, Key=STATE_KEY) return json.loads(obj["Body"].read()) except Exception: # initial state: start today today = dt.date.today().isoformat() return {"cursor_date": today, "next_page_token": None} def put_state(state: dict): state["updated_at"] = dt.datetime.utcnow().isoformat() + "Z" s3.put_object(Bucket=S3_BUCKET, Key=STATE_KEY, Body=json.dumps(state).encode()) def write_chunk(items: list[dict], ts: dt.datetime) -> str: key = f"{S3_PREFIX}{ts:%Y/%m/%d}/zoom-operationlogs-{uuid.uuid4()}.json.gz" buf = io.BytesIO() with gzip.GzipFile(fileobj=buf, mode="w") as gz: for rec in items: gz.write((json.dumps(rec) + "n").encode()) buf.seek(0) s3.upload_fileobj(buf, S3_BUCKET, key) return key def fetch_page(token: str, from_date: str, to_date: str, next_page_token: str | None) -> dict: q = { "from": from_date, "to": to_date, "page_size": str(PAGE_SIZE), } if next_page_token: q["next_page_token"] = next_page_token url = REPORT_URL + "?" + urllib.parse.urlencode(q) req = urllib.request.Request(url, headers={ "Authorization": f"Bearer {token}", "Accept": "application/json", }) with _http(req) as r: return json.loads(r.read()) def lambda_handler(event=None, context=None): token = get_token() state = get_state() cursor_date = state.get("cursor_date") # YYYY-MM-DD # API requires from/to in yyyy-mm-dd, max one month per request from_date = cursor_date to_date = cursor_date total_written = 0 next_token = state.get("next_page_token") while True: page = fetch_page(token, from_date, to_date, next_token) items = page.get("operation_logs", []) or [] if items: write_chunk(items, dt.datetime.utcnow()) total_written += len(items) next_token = page.get("next_page_token") if not next_token: break # Advance to next day if we've finished this date today = dt.date.today().isoformat() if cursor_date < today: nxt = (dt.datetime.fromisoformat(cursor_date) + dt.timedelta(days=1)).date().isoformat() state["cursor_date"] = nxt state["next_page_token"] = None else: # stay on today; continue later with next_page_token=None state["next_page_token"] = None put_state(state) return {"ok": True, "written": total_written, "date": from_date} if __name__ == "__main__": print(lambda_handler())
Accédez à Configuration> Variables d'environnement> Modifier> Ajouter une variable d'environnement.
Saisissez les variables d'environnement suivantes en remplaçant les valeurs par les vôtres :
Clé Exemple de valeur S3_BUCKET
zoom-operation-logs
S3_PREFIX
zoom/operationlogs/
STATE_KEY
zoom/operationlogs/state.json
ZOOM_ACCOUNT_ID
<your-zoom-account-id>
ZOOM_CLIENT_ID
<your-zoom-client-id>
ZOOM_CLIENT_SECRET
<your-zoom-client-secret>
PAGE_SIZE
300
TIMEOUT
30
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.
Créer une programmation EventBridge
- Accédez à Amazon EventBridge> Scheduler.
- Cliquez sur Créer la programmation.
- Fournissez les informations de configuration suivantes :
- Planning récurrent : Tarif (
15 min
). - Cible : votre fonction Lambda
zoom_operationlogs_to_s3
. - Nom :
zoom-operationlogs-schedule-15min
.
- Planning récurrent : Tarif (
- Cliquez sur Créer la programmation.
Facultatif : Créez un utilisateur et des clés IAM en lecture seule pour Google SecOps
- Dans la console AWS, accédez à IAM > Utilisateurs > Ajouter des utilisateurs.
- Cliquez sur Add users (Ajouter des utilisateurs).
- Fournissez les informations de configuration suivantes :
- Utilisateur :
secops-reader
. - Type d'accès : Clé d'accès – Accès programmatique.
- Utilisateur :
- Cliquez sur Créer un utilisateur.
- Associez une stratégie de lecture minimale (personnalisée) : Utilisateurs > secops-reader > Autorisations > Ajouter des autorisations > Associer des stratégies directement > Créer une stratégie.
Dans l'éditeur JSON, saisissez la stratégie suivante :
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::zoom-operation-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::zoom-operation-logs" } ] }
Définissez le nom sur
secops-reader-policy
.Accédez à Créer une règle > recherchez/sélectionnez > Suivant > Ajouter des autorisations.
Accédez à Identifiants de sécurité > Clés d'accès > Créer une clé d'accès.
Téléchargez le CSV (ces valeurs sont saisies dans le flux).
Configurer un flux dans Google SecOps pour ingérer les journaux d'opération Zoom
- 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,
Zoom Operation Logs
). - Sélectionnez Amazon S3 V2 comme type de source.
- Sélectionnez Journaux des opérations Zoom comme Type de journal.
- Cliquez sur Suivant.
- Spécifiez les valeurs des paramètres d'entrée suivants :
- URI S3 :
s3://zoom-operation-logs/zoom/operationlogs/
- 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é appliqué aux événements de ce flux.
- URI S3 :
- 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 |
---|---|---|
action | metadata.product_event_type | Le champ de journal brut "action" est mappé sur ce champ UDM. |
category_type | additional.fields.key | Le champ de journal brut "category_type" est mappé à ce champ UDM. |
category_type | additional.fields.value.string_value | Le champ de journal brut "category_type" est mappé à ce champ UDM. |
Département | target.user.department | Le champ de journal brut "Department" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
Description | target.user.role_description | Le champ de journal brut "Description" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
Nom à afficher | target.user.user_display_name | Le champ de journal brut "Nom à afficher" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Adresse e-mail | target.user.email_addresses | Le champ de journal brut "Adresse e-mail" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Prénom | target.user.first_name | Le champ de journal brut "Prénom" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
Fonction | target.user.title | Le champ de journal brut "Job Title" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Nom | target.user.last_name | Le champ de journal brut "Nom" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
Emplacement | target.location.name | Le champ de journal brut "Location" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
operation_detail | metadata.description | Le champ de journal brut "operation_detail" est mappé à ce champ UDM. |
opérateur | principal.user.email_addresses | Le champ de journal brut "operator" est mappé sur ce champ UDM s'il correspond à une expression régulière d'adresse e-mail. |
opérateur | principal.user.userid | Le champ de journal brut "operator" est mappé sur ce champ UDM s'il ne correspond pas à une expression régulière d'adresse e-mail. |
Room Name | target.user.attribute.labels.value | Le champ de journal brut "Nom de la salle" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Nom du rôle | target.user.attribute.roles.name | Le champ de journal brut "Nom du rôle" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
temps | metadata.event_timestamp.seconds | Le champ de journal brut "time" est analysé et mappé sur ce champ UDM. |
Type | target.user.attribute.labels.value | Le champ de journal brut "Type" (extrait du champ "operation_detail") est mappé à ce champ UDM. |
Rôle de l'utilisateur | target.user.attribute.roles.name | Le champ de journal brut "Rôle utilisateur" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Type d'utilisateur | target.user.attribute.labels.value | Le champ de journal brut "Type d'utilisateur" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
metadata.log_type | La valeur "ZOOM_OPERATION_LOGS" est attribuée à ce champ UDM. | |
metadata.vendor_name | La valeur "ZOOM" est attribuée à ce champ UDM. | |
metadata.product_name | La valeur "ZOOM_OPERATION_LOGS" est attribuée à ce champ UDM. | |
metadata.event_type | La valeur est déterminée selon la logique suivante : 1. Si le champ "event_type" n'est pas vide, sa valeur est utilisée. 2. Si les champs "operator", "email" ou "email2" ne sont pas vides, la valeur est définie sur "USER_UNCATEGORIZED". 3. Sinon, la valeur est définie sur "GENERIC_EVENT". |
|
json_data | about.user.attribute.labels.value | Le champ de journal brut "json_data" (extrait du champ "operation_detail") est analysé au format JSON. Les champs "assistant" et "options" de chaque élément du tableau JSON analysé sont mappés au champ "value" du tableau "labels" dans l'UDM. |
json_data | about.user.userid | Le champ de journal brut "json_data" (extrait du champ "operation_detail") est analysé au format JSON. Le champ "userId" de chaque élément du tableau JSON analysé (sauf le premier) est mappé au champ "userid" de l'objet "about.user" dans l'UDM. |
json_data | target.user.attribute.labels.value | Le champ de journal brut "json_data" (extrait du champ "operation_detail") est analysé au format JSON. Les champs "assistant" et "options" du premier élément du tableau JSON analysé sont mappés au champ "value" du tableau "labels" dans l'UDM. |
json_data | target.user.userid | Le champ de journal brut "json_data" (extrait du champ "operation_detail") est analysé au format JSON. Le champ "userId" du premier élément du tableau JSON analysé est mappé au champ "userid" de l'objet "target.user" dans l'UDM. |
target.user.email_addresses | Le champ de journal brut "email" (extrait du champ "operation_detail") est mappé sur ce champ UDM. | |
email2 | target.user.email_addresses | Le champ de journal brut "email2" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
rôle | target.user.attribute.roles.name | Le champ de journal brut "role" (extrait du champ "operation_detail") est mappé sur ce champ UDM. |
Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.