Collecter les journaux d'opération Zoom

Compatible avec :

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)

  1. Connectez-vous à la Zoom App Marketplace.
  2. Accédez à Développer > Créer une application > OAuth serveur à serveur.
  3. Créez l'application et ajoutez le champ d'application suivant : report:read:operation_logs:admin (ou report:read:admin).
  4. 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

  1. Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
  2. Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple, zoom-operation-logs).
  3. Créez un utilisateur en suivant ce guide : Créer un utilisateur IAM.
  4. Sélectionnez l'utilisateur créé.
  5. Sélectionnez l'onglet Informations d'identification de sécurité.
  6. Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
  7. Sélectionnez Service tiers comme Cas d'utilisation.
  8. Cliquez sur Suivant.
  9. Facultatif : ajoutez un tag de description.
  10. Cliquez sur Créer une clé d'accès.
  11. 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.
  12. Cliquez sur OK.
  13. Sélectionnez l'onglet Autorisations.
  14. Cliquez sur Ajouter des autorisations dans la section Règles d'autorisation.
  15. Sélectionnez Ajouter des autorisations.
  16. Sélectionnez Joindre directement des règles.
  17. Recherchez et sélectionnez la règle AmazonS3FullAccess.
  18. Cliquez sur Suivant.
  19. Cliquez sur Ajouter des autorisations.

Configurer la stratégie et le rôle IAM pour les importations S3

  1. Dans la console AWS, accédez à IAM > Stratégies > Créer une stratégie > Onglet JSON.
  2. 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.
  3. Cliquez sur Suivant > Créer une règle.

  4. Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.

  5. Associez la règle que vous venez de créer.

  6. Nommez le rôle WriteZoomOperationLogsToS3Role, puis cliquez sur Créer un rôle.

Créer la fonction Lambda

  1. Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
  2. Cliquez sur Créer à partir de zéro.
  3. 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
  1. 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())
    
  2. Accédez à Configuration> Variables d'environnement> Modifier> Ajouter une variable d'environnement.

  3. 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
  4. Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre-fonction).

  5. Accédez à l'onglet Configuration.

  6. Dans le panneau Configuration générale, cliquez sur Modifier.

  7. Définissez le délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.

Créer une programmation EventBridge

  1. Accédez à Amazon EventBridge> Scheduler.
  2. Cliquez sur Créer la programmation.
  3. 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.
  4. Cliquez sur Créer la programmation.

Facultatif : Créez un utilisateur et des clés IAM en lecture seule pour Google SecOps

  1. Dans la console AWS, accédez à IAM > Utilisateurs > Ajouter des utilisateurs.
  2. Cliquez sur Add users (Ajouter des utilisateurs).
  3. Fournissez les informations de configuration suivantes :
    • Utilisateur : secops-reader.
    • Type d'accès : Clé d'accès – Accès programmatique.
  4. Cliquez sur Créer un utilisateur.
  5. 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.
  6. 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"
        }
      ]
    }
    
  7. Définissez le nom sur secops-reader-policy.

  8. Accédez à Créer une règle > recherchez/sélectionnez > Suivant > Ajouter des autorisations.

  9. Accédez à Identifiants de sécurité > Clés d'accès > Créer une clé d'accès.

  10. 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

  1. Accédez à Paramètres SIEM> Flux.
  2. Cliquez sur + Ajouter un flux.
  3. Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple, Zoom Operation Logs).
  4. Sélectionnez Amazon S3 V2 comme type de source.
  5. Sélectionnez Journaux des opérations Zoom comme Type de journal.
  6. Cliquez sur Suivant.
  7. 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.
  8. Cliquez sur Suivant.
  9. 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.
e-mail 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.