Coletar registros de operações do Zoom

Compatível com:

Este documento explica como ingerir registros de operações do Zoom no Google Security Operations usando o Amazon S3. O analisador transforma os registros brutos em um modelo de dados unificado (UDM). Ele extrai campos da mensagem de registro bruta, realiza limpeza e normalização de dados e mapeia as informações extraídas para os campos correspondentes do UDM, enriquecendo os dados para análise e correlação em um sistema SIEM.

Antes de começar

Verifique se você tem os pré-requisitos a seguir:

  • Instância do Google SecOps
  • Acesso privilegiado ao Zoom
  • Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge)

Coletar pré-requisitos dos registros de operações do Zoom (IDs, chaves de API, IDs da organização, tokens)

  1. Faça login no Zoom App Marketplace.
  2. Acesse Desenvolver > Criar app > OAuth de servidor para servidor.
  3. Crie o app e adicione o seguinte escopo: report:read:operation_logs:admin (ou report:read:admin).
  4. Em Credenciais do app, copie e salve os seguintes detalhes em um local seguro:
    • ID da conta.
    • ID do cliente.
    • Chave secreta do cliente.

Configurar o bucket do AWS S3 e o IAM para o Google SecOps

  1. Crie um bucket do Amazon S3 seguindo este guia do usuário: Como criar um bucket
  2. Salve o Nome e a Região do bucket para referência futura (por exemplo, zoom-operation-logs).
  3. Crie um usuário seguindo este guia: Como criar um usuário do IAM.
  4. Selecione o usuário criado.
  5. Selecione a guia Credenciais de segurança.
  6. Clique em Criar chave de acesso na seção Chaves de acesso.
  7. Selecione Serviço de terceiros como o Caso de uso.
  8. Clique em Próxima.
  9. Opcional: adicione uma tag de descrição.
  10. Clique em Criar chave de acesso.
  11. Clique em Fazer o download do arquivo CSV para salvar a chave de acesso e a chave de acesso secreta para uso posterior.
  12. Clique em Concluído.
  13. Selecione a guia Permissões.
  14. Clique em Adicionar permissões na seção Políticas de permissões.
  15. Selecione Adicionar permissões.
  16. Selecione Anexar políticas diretamente.
  17. Pesquise e selecione a política AmazonS3FullAccess.
  18. Clique em Próxima.
  19. Clique em Adicionar permissões

Configurar a política e o papel do IAM para uploads do S3

  1. No console da AWS, acesse IAM > Políticas > Criar política > guia JSON.
  2. Insira a seguinte política:

    {
      "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"
        }
      ]
    }
    
    • Substitua zoom-operation-logs se você tiver inserido um nome de bucket diferente.
  3. Clique em Próxima > Criar política.

  4. Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.

  5. Anexe a política recém-criada.

  6. Nomeie a função como WriteZoomOperationLogsToS3Role e clique em Criar função.

Criar a função Lambda

  1. No console da AWS, acesse Lambda > Functions > Create function.
  2. Clique em Criar do zero.
  3. Informe os seguintes detalhes de configuração:
Configuração Valor
Nome zoom_operationlogs_to_s3
Ambiente de execução Python 3.13
Arquitetura x86_64
Função de execução WriteZoomOperationLogsToS3Role
  1. Depois que a função for criada, abra a guia Código, exclua o stub e insira o seguinte código(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. Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.

  3. Insira as seguintes variáveis de ambiente, substituindo pelos seus valores:

    Chave Valor de exemplo
    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. Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua-função.

  5. Selecione a guia Configuração.

  6. No painel Configuração geral, clique em Editar.

  7. Mude Tempo limite para 5 minutos (300 segundos) e clique em Salvar.

Criar uma programação do EventBridge

  1. Acesse Amazon EventBridge > Scheduler.
  2. Clique em Criar programação.
  3. Informe os seguintes detalhes de configuração:
    • Programação recorrente: Taxa (15 min).
    • Destino: sua função Lambda zoom_operationlogs_to_s3.
    • Nome: zoom-operationlogs-schedule-15min.
  4. Clique em Criar programação.

Opcional: criar um usuário e chaves do IAM somente leitura para o Google SecOps

  1. No console da AWS, acesse IAM > Usuários > Adicionar usuários.
  2. Clique em Add users.
  3. Informe os seguintes detalhes de configuração:
    • Usuário: secops-reader.
    • Tipo de acesso: Chave de acesso — Acesso programático.
  4. Clique em Criar usuário.
  5. Anexe a política de leitura mínima (personalizada): Usuários > secops-reader > Permissões > Adicionar permissões > Anexar políticas diretamente > Criar política.
  6. No editor JSON, insira a seguinte política:

    {
      "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. Defina o nome como secops-reader-policy.

  8. Acesse Criar política > pesquise/selecione > Próxima > Adicionar permissões.

  9. Acesse Credenciais de segurança > Chaves de acesso > Criar chave de acesso.

  10. Faça o download do CSV (esses valores são inseridos no feed).

Configurar um feed no Google SecOps para ingerir registros de operações do Zoom

  1. Acesse Configurações do SIEM > Feeds.
  2. Clique em + Adicionar novo feed.
  3. No campo Nome do feed, insira um nome para o feed (por exemplo, Zoom Operation Logs).
  4. Selecione Amazon S3 V2 como o Tipo de origem.
  5. Selecione Registros de operações do Zoom como o Tipo de registro.
  6. Clique em Próxima.
  7. Especifique valores para os seguintes parâmetros de entrada:
    • URI do S3: s3://zoom-operation-logs/zoom/operationlogs/
    • Opções de exclusão de fontes: selecione a opção de exclusão de acordo com sua preferência.
    • Idade máxima do arquivo: inclui arquivos modificados no último número de dias. O padrão é de 180 dias.
    • ID da chave de acesso: chave de acesso do usuário com acesso ao bucket do S3.
    • Chave de acesso secreta: chave secreta do usuário com acesso ao bucket do S3.
    • Namespace do recurso: o namespace do recurso.
    • Rótulos de ingestão: o rótulo aplicado aos eventos deste feed.
  8. Clique em Próxima.
  9. Revise a nova configuração do feed na tela Finalizar e clique em Enviar.

Tabela de mapeamento do UDM

Campo de registro Mapeamento do UDM Lógica
ação metadata.product_event_type O campo de registro bruto "action" é mapeado para esse campo do UDM.
category_type additional.fields.key O campo de registro bruto "category_type" é mapeado para esse campo do UDM.
category_type additional.fields.value.string_value O campo de registro bruto "category_type" é mapeado para esse campo do UDM.
Departamento target.user.department O campo de registro bruto "Department" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Descrição target.user.role_description O campo de registro bruto "Description" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Nome de exibição target.user.user_display_name O campo de registro bruto "Nome de exibição" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Endereço de e-mail target.user.email_addresses O campo de registro bruto "Email Address" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Nome target.user.first_name O campo de registro bruto "Nome" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Cargo target.user.title O campo "Cargo" do registro bruto (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Sobrenome target.user.last_name O campo de registro bruto "Sobrenome" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Local target.location.name O campo de registro bruto "Location" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
operation_detail metadata.description O campo de registro bruto "operation_detail" é mapeado para esse campo do UDM.
operador principal.user.email_addresses O campo de registro bruto "operator" é mapeado para esse campo do UDM se corresponder a uma expressão regular de e-mail.
operador principal.user.userid O campo de registro bruto "operator" é mapeado para esse campo da UDM se não corresponder a uma expressão regular de e-mail.
Room Name target.user.attribute.labels.value O campo de registro bruto "Nome da sala" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Nome do papel target.user.attribute.roles.name O campo de registro bruto "Nome da função" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
tempo metadata.event_timestamp.seconds O campo de registro bruto "time" é analisado e mapeado para esse campo do UDM.
Tipo target.user.attribute.labels.value O campo de registro bruto "Type" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Função do usuário target.user.attribute.roles.name O campo de registro bruto "Função do usuário" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
Tipo de usuário target.user.attribute.labels.value O campo de registro bruto "Tipo de usuário" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
metadata.log_type O valor "ZOOM_OPERATION_LOGS" é atribuído a esse campo da UDM.
metadata.vendor_name O valor "ZOOM" é atribuído a esse campo da UDM.
metadata.product_name O valor "ZOOM_OPERATION_LOGS" é atribuído a esse campo da UDM.
metadata.event_type O valor é determinado com base na seguinte lógica:
1. Se o campo "event_type" não estiver vazio, o valor dele será usado.
2. Se os campos "operator", "email" ou "email2" não estiverem vazios, o valor será definido como "USER_UNCATEGORIZED".
3. Caso contrário, o valor será definido como "GENERIC_EVENT".
json_data about.user.attribute.labels.value O campo de registro bruto "json_data" (extraído do campo "operation_detail") é analisado como JSON. Os campos "assistant" e "options" de cada elemento da matriz JSON analisada são mapeados para o campo "value" da matriz "labels" na UDM.
json_data about.user.userid O campo de registro bruto "json_data" (extraído do campo "operation_detail") é analisado como JSON. O campo "userId" de cada elemento da matriz JSON analisada (exceto o primeiro) é mapeado para o campo "userid" do objeto "about.user" na UDM.
json_data target.user.attribute.labels.value O campo de registro bruto "json_data" (extraído do campo "operation_detail") é analisado como JSON. Os campos "assistant" e "options" do primeiro elemento da matriz JSON analisada são mapeados para o campo "value" da matriz "labels" na UDM.
json_data target.user.userid O campo de registro bruto "json_data" (extraído do campo "operation_detail") é analisado como JSON. O campo "userId" do primeiro elemento da matriz JSON analisada é mapeado para o campo "userid" do objeto "target.user" na UDM.
e-mail target.user.email_addresses O campo de registro bruto "email" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
email2 target.user.email_addresses O campo de registro bruto "email2" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.
papel target.user.attribute.roles.name O campo de registro bruto "role" (extraído do campo "operation_detail") é mapeado para esse campo do UDM.

Precisa de mais ajuda? Receba respostas de membros da comunidade e profissionais do Google SecOps.