Recolha registos de auditoria do Slack
Este documento explica como carregar registos de auditoria do Slack para o Google Security Operations através do Amazon S3. O analisador normaliza primeiro os valores booleanos e limpa os campos predefinidos. Em seguida, analisa o campo "message" como JSON, processando as mensagens não JSON ao rejeitá-las. Consoante a presença de campos específicos (date_create
e user_id
), o analisador aplica uma lógica diferente para mapear os campos de registo não processados para o UDM, incluindo metadados, principal, rede, destino e informações sobre, e cria um resultado de segurança.
Antes de começar
Certifique-se de que tem os seguintes pré-requisitos:
- Instância do Google SecOps
- Acesso privilegiado ao inquilino do Slack Enterprise Grid e à consola do administrador
- Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge)
Recolha os pré-requisitos do Slack (ID da app, token OAuth e ID da organização)
- Inicie sessão na consola de administração do Slack.
- Aceda a https://api.slack.com/apps e clique em Create New App > From scratch.
- Introduza um Nome da app exclusivo e selecione o seu espaço de trabalho do Slack.
- Clique em Criar app.
- Navegue para OAuth e autorizações na barra lateral esquerda.
- Aceda à secção Âmbitos e adicione o seguinte âmbito do token de utilizador: auditlogs:read
- Clique em Instalar no Workspace > Permitir.
- Depois de instalada, aceda a Apps ao nível da organização.
- Clique em Instalar na organização.
- Autorize a app com uma conta de proprietário/administrador da organização.
- Copie e guarde em segurança a chave OAuth do utilizador que começa com
xoxp-
(esta é a sua SLACK_AUDIT_TOKEN). - Tenha em atenção o ID da organização, que pode encontrar na consola de administração do Slack em Definições e autorizações > Definições da organização.
Configure o contentor do AWS S3 e o IAM para o Google SecOps
- Crie um contentor do Amazon S3 seguindo este manual do utilizador: Criar um contentor
- Guarde o nome e a região do contentor para referência futura (por exemplo,
slack-audit-logs
). - Crie um utilizador seguindo este guia do utilizador: Criar um utilizador do IAM.
- Selecione o utilizador criado.
- Selecione o separador Credenciais de segurança.
- Clique em Criar chave de acesso na secção Chaves de acesso.
- Selecione Serviço de terceiros como o Exemplo de utilização.
- Clicar em Seguinte.
- Opcional: adicione uma etiqueta de descrição.
- Clique em Criar chave de acesso.
- Clique em Transferir ficheiro CSV para guardar a chave de acesso e a chave de acesso secreta para utilização posterior.
- Clique em Concluído.
- Selecione o separador Autorizações.
- Clique em Adicionar autorizações na secção Políticas de autorizações.
- Selecione Adicionar autorizações.
- Selecione Anexar políticas diretamente
- Pesquise e selecione a política AmazonS3FullAccess.
- Clicar em Seguinte.
- Clique em Adicionar autorizações.
Configure a política e a função de IAM para carregamentos do S3
- Na consola da AWS, aceda a IAM > Políticas > Criar política > separador JSON.
Introduza a seguinte política:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::slack-audit-logs/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::slack-audit-logs/slack/audit/state.json" } ] }
- Substitua
slack-audit-logs
se tiver introduzido um nome de contentor diferente.
- Substitua
Clique em Seguinte > Criar política.
Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.
Anexe a política criada recentemente.
Dê o nome
SlackAuditToS3Role
à função e clique em Criar função.
Crie a função Lambda
- Na consola da AWS, aceda a Lambda > Functions > Create function.
- Clique em Criar do zero.
- Faculte os seguintes detalhes de configuração:
Definição | Valor |
---|---|
Nome | slack_audit_to_s3 |
Runtime | Python 3.13 |
Arquitetura | x86_64 |
Função de execução | SlackAuditToS3Role |
Depois de criar a função, abra o separador Código, elimine o fragmento de código e introduza o seguinte (
slack_audit_to_s3.py
):#!/usr/bin/env python3 # Lambda: Pull Slack Audit Logs (Enterprise Grid) to S3 (no transform) import os, json, time, urllib.parse from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError import boto3 BASE_URL = "https://api.slack.com/audit/v1/logs" TOKEN = os.environ["SLACK_AUDIT_TOKEN"] # org-level user token with auditlogs:read BUCKET = os.environ["S3_BUCKET"] PREFIX = os.environ.get("S3_PREFIX", "slack/audit/") STATE_KEY = os.environ.get("STATE_KEY", "slack/audit/state.json") LIMIT = int(os.environ.get("LIMIT", "200")) # Slack recommends <= 200 MAX_PAGES = int(os.environ.get("MAX_PAGES", "20")) LOOKBACK_SEC = int(os.environ.get("LOOKBACK_SECONDS", "3600")) # First-run window HTTP_TIMEOUT = int(os.environ.get("HTTP_TIMEOUT", "60")) HTTP_RETRIES = int(os.environ.get("HTTP_RETRIES", "3")) RETRY_AFTER_DEFAULT = int(os.environ.get("RETRY_AFTER_DEFAULT", "2")) # Optional server-side filters (comma-separated "action" values), empty means no filter ACTIONS = os.environ.get("ACTIONS", "").strip() s3 = boto3.client("s3") def _get_state() -> dict: try: obj = s3.get_object(Bucket=BUCKET, Key=STATE_KEY) st = json.loads(obj["Body"].read() or b"{}") return {"cursor": st.get("cursor")} except Exception: return {"cursor": None} def _put_state(state: dict) -> None: body = json.dumps(state, separators=(",", ":")).encode("utf-8") s3.put_object(Bucket=BUCKET, Key=STATE_KEY, Body=body, ContentType="application/json") def _http_get(params: dict) -> dict: qs = urllib.parse.urlencode(params, doseq=True) url = f"{BASE_URL}?{qs}" if qs else BASE_URL req = Request(url, method="GET") req.add_header("Authorization", f"Bearer {TOKEN}") req.add_header("Accept", "application/json") attempt = 0 while True: try: with urlopen(req, timeout=HTTP_TIMEOUT) as r: return json.loads(r.read().decode("utf-8")) except HTTPError as e: # Respect Retry-After on 429/5xx if e.code in (429, 500, 502, 503, 504) and attempt < HTTP_RETRIES: retry_after = 0 try: retry_after = int(e.headers.get("Retry-After", RETRY_AFTER_DEFAULT)) except Exception: retry_after = RETRY_AFTER_DEFAULT time.sleep(max(1, retry_after)) attempt += 1 continue # Re-raise other HTTP errors raise except URLError: if attempt < HTTP_RETRIES: time.sleep(RETRY_AFTER_DEFAULT) attempt += 1 continue raise def _write_page(payload: dict, page_idx: int) -> str: ts = time.strftime("%Y/%m/%d/%H%M%S", time.gmtime()) key = f"{PREFIX}/{ts}-slack-audit-p{page_idx:05d}.json" body = json.dumps(payload, separators=(",", ":")).encode("utf-8") s3.put_object(Bucket=BUCKET, Key=key, Body=body, ContentType="application/json") return key def lambda_handler(event=None, context=None): state = _get_state() cursor = state.get("cursor") params = {"limit": LIMIT} if ACTIONS: params["action"] = [a.strip() for a in ACTIONS.split(",") if a.strip()] if cursor: params["cursor"] = cursor else: # First run (or reset): fetch a recent window by time params["oldest"] = int(time.time()) - LOOKBACK_SEC pages = 0 total = 0 last_cursor = None while pages < MAX_PAGES: data = _http_get(params) _write_page(data, pages) entries = data.get("entries") or [] total += len(entries) # Cursor for next page meta = data.get("response_metadata") or {} next_cursor = meta.get("next_cursor") or data.get("next_cursor") if next_cursor: params = {"limit": LIMIT, "cursor": next_cursor} if ACTIONS: params["action"] = [a.strip() for a in ACTIONS.split(",") if a.strip()] last_cursor = next_cursor pages += 1 continue break if last_cursor: _put_state({"cursor": last_cursor}) return {"ok": True, "pages": pages + (1 if total or last_cursor else 0), "entries": total, "cursor": last_cursor} if __name__ == "__main__": print(lambda_handler())
Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
Introduza as seguintes variáveis de ambiente, substituindo-as pelos seus valores:
Chave Valor de exemplo S3_BUCKET
slack-audit-logs
S3_PREFIX
slack/audit/
STATE_KEY
slack/audit/state.json
SLACK_AUDIT_TOKEN
xoxp-***
(token de utilizador ao nível da organização comauditlogs:read
)LIMIT
200
MAX_PAGES
20
LOOKBACK_SECONDS
3600
HTTP_TIMEOUT
60
HTTP_RETRIES
3
RETRY_AFTER_DEFAULT
2
ACTIONS
(opcional, CSV) user_login,app_installed
Depois de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your-function).
Selecione o separador Configuração.
No painel Configuração geral, clique em Editar.
Altere Tempo limite para 5 minutos (300 segundos) e clique em Guardar.
Crie um horário do EventBridge
- Aceda a Amazon EventBridge > Scheduler > Create schedule.
- Indique os seguintes detalhes de configuração:
- Agenda recorrente: Taxa (
1 hour
). - Destino: a sua função Lambda
slack_audit_to_s3
. - Nome:
slack-audit-1h
.
- Agenda recorrente: Taxa (
- Clique em Criar programação.
Opcional: crie um utilizador e chaves da IAM só de leitura para o Google SecOps
- Na consola da AWS, aceda a IAM > Utilizadores > Adicionar utilizadores.
- Clique em Adicionar utilizadores.
- Indique os seguintes detalhes de configuração:
- Utilizador:
secops-reader
. - Tipo de acesso: chave de acesso – acesso programático.
- Utilizador:
- Clique em Criar utilizador.
- Anexe a política de leitura mínima (personalizada): Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy.
No editor JSON, introduza a seguinte política:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::slack-audit-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::slack-audit-logs" } ] }
Defina o nome como
secops-reader-policy
.Aceda a Criar política > pesquise/selecione > Seguinte > Adicionar autorizações.
Aceda a Credenciais de segurança > Chaves de acesso > Criar chave de acesso.
Transfira o CSV (estes valores são introduzidos no feed).
Configure um feed no Google SecOps para carregar registos de auditoria do Slack
- Aceda a Definições do SIEM > Feeds.
- Clique em + Adicionar novo feed.
- No campo Nome do feed, introduza um nome para o feed (por exemplo,
Slack Audit Logs
). - Selecione Amazon S3 V2 como o Tipo de origem.
- Selecione Auditoria do Slack como o Tipo de registo.
- Clicar em Seguinte.
- Especifique valores para os seguintes parâmetros de entrada:
- URI do S3:
s3://slack-audit-logs/slack/audit/
- Opções de eliminação de origens: selecione a opção de eliminação de acordo com a sua preferência.
- Idade máxima do ficheiro: inclua ficheiros modificados no último número de dias. A predefinição é 180 dias.
- ID da chave de acesso: chave de acesso do utilizador com acesso ao contentor do S3.
- Chave de acesso secreta: chave secreta do utilizador com acesso ao contentor do S3.
- Espaço de nomes do recurso: o espaço de nomes do recurso.
- Etiquetas de carregamento: a etiqueta aplicada aos eventos deste feed.
- URI do S3:
- Clicar em Seguinte.
- Reveja a nova configuração do feed no ecrã Finalizar e, de seguida, clique em Enviar.
Tabela de mapeamento da UDM
Campo de registo | Mapeamento de UDM | Lógica |
---|---|---|
action |
metadata.product_event_type |
Mapeado diretamente a partir do campo action no registo não processado. |
actor.type |
principal.labels.value |
Mapeado diretamente a partir do campo actor.type , com a chave actor.type adicionada. |
actor.user.email |
principal.user.email_addresses |
Mapeado diretamente a partir do campo actor.user.email . |
actor.user.id |
principal.user.product_object_id |
Mapeado diretamente a partir do campo actor.user.id . |
actor.user.id |
principal.user.userid |
Mapeado diretamente a partir do campo actor.user.id . |
actor.user.name |
principal.user.user_display_name |
Mapeado diretamente a partir do campo actor.user.name . |
actor.user.team |
principal.user.group_identifiers |
Mapeado diretamente a partir do campo actor.user.team . |
context.ip_address |
principal.ip |
Mapeado diretamente a partir do campo context.ip_address . |
context.location.domain |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo context.location.domain , com a chave context.location.domain adicionada. |
context.location.id |
about.resource.id |
Mapeado diretamente a partir do campo context.location.id . |
context.location.name |
about.resource.name |
Mapeado diretamente a partir do campo context.location.name . |
context.location.name |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo context.location.name , com a chave context.location.name adicionada. |
context.location.type |
about.resource.resource_subtype |
Mapeado diretamente a partir do campo context.location.type . |
context.session_id |
network.session_id |
Mapeado diretamente a partir do campo context.session_id . |
context.ua |
network.http.user_agent |
Mapeado diretamente a partir do campo context.ua . |
context.ua |
network.http.parsed_user_agent |
Informações do agente do utilizador analisadas derivadas do campo context.ua através do filtro parseduseragent . |
country |
principal.location.country_or_region |
Mapeado diretamente a partir do campo country . |
date_create |
metadata.event_timestamp.seconds |
A indicação de tempo de época do campo date_create é convertida num objeto de indicação de tempo. |
details.inviter.email |
target.user.email_addresses |
Mapeado diretamente a partir do campo details.inviter.email . |
details.inviter.id |
target.user.product_object_id |
Mapeado diretamente a partir do campo details.inviter.id . |
details.inviter.name |
target.user.user_display_name |
Mapeado diretamente a partir do campo details.inviter.name . |
details.inviter.team |
target.user.group_identifiers |
Mapeado diretamente a partir do campo details.inviter.team . |
details.reason |
security_result.description |
Mapeado diretamente a partir do campo details.reason ou, se for uma matriz, concatenado com vírgulas. |
details.type |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo details.type , com a chave details.type adicionada. |
details.type |
security_result.summary |
Mapeado diretamente a partir do campo details.type . |
entity.app.id |
target.resource.id |
Mapeado diretamente a partir do campo entity.app.id . |
entity.app.name |
target.resource.name |
Mapeado diretamente a partir do campo entity.app.name . |
entity.channel.id |
target.resource.id |
Mapeado diretamente a partir do campo entity.channel.id . |
entity.channel.name |
target.resource.name |
Mapeado diretamente a partir do campo entity.channel.name . |
entity.channel.privacy |
target.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.channel.privacy , com a chave entity.channel.privacy adicionada. |
entity.file.filetype |
target.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.file.filetype , com a chave entity.file.filetype adicionada. |
entity.file.id |
target.resource.id |
Mapeado diretamente a partir do campo entity.file.id . |
entity.file.name |
target.resource.name |
Mapeado diretamente a partir do campo entity.file.name . |
entity.file.title |
target.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.file.title , com a chave entity.file.title adicionada. |
entity.huddle.date_end |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.huddle.date_end , com a chave entity.huddle.date_end adicionada. |
entity.huddle.date_start |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.huddle.date_start , com a chave entity.huddle.date_start adicionada. |
entity.huddle.id |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.huddle.id , com a chave entity.huddle.id adicionada. |
entity.huddle.participants.0 |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.huddle.participants.0 , com a chave entity.huddle.participants.0 adicionada. |
entity.huddle.participants.1 |
about.resource.attribute.labels.value |
Mapeado diretamente a partir do campo entity.huddle.participants.1 , com a chave entity.huddle.participants.1 adicionada. |
entity.type |
target.resource.resource_subtype |
Mapeado diretamente a partir do campo entity.type . |
entity.user.email |
target.user.email_addresses |
Mapeado diretamente a partir do campo entity.user.email . |
entity.user.id |
target.user.product_object_id |
Mapeado diretamente a partir do campo entity.user.id . |
entity.user.name |
target.user.user_display_name |
Mapeado diretamente a partir do campo entity.user.name . |
entity.user.team |
target.user.group_identifiers |
Mapeado diretamente a partir do campo entity.user.team . |
entity.workflow.id |
target.resource.id |
Mapeado diretamente a partir do campo entity.workflow.id . |
entity.workflow.name |
target.resource.name |
Mapeado diretamente a partir do campo entity.workflow.name . |
id |
metadata.product_log_id |
Mapeado diretamente a partir do campo id . |
ip |
principal.ip |
Mapeado diretamente a partir do campo ip . Determinado pela lógica com base no campo action . A predefinição é USER_COMMUNICATION , mas muda para outros valores, como USER_CREATION , USER_LOGIN , USER_LOGOUT , USER_RESOURCE_ACCESS , USER_RESOURCE_UPDATE_PERMISSIONS ou USER_CHANGE_PERMISSIONS , com base no valor de action . Codificado de forma rígida para "SLACK_AUDIT". Definido como "Enterprise Grid" se date_create existir. Caso contrário, é definido como "Registos de auditoria" se user_id existir. Codificado de forma rígida para "Slack". Codificado de forma rígida como "REMOTE". Definido como "SSO" se action contiver "user_login" ou "user_logout". Caso contrário, defina como "MACHINE". Não mapeado nos exemplos fornecidos. A predefinição é "ALLOW", mas é definida como "BLOCK" se action for "user_login_failed". Definido como "Slack" se date_create existir. Caso contrário, definido como "SLACK" se user_id existir. |
user_agent |
network.http.user_agent |
Mapeado diretamente a partir do campo user_agent . |
user_id |
principal.user.product_object_id |
Mapeado diretamente a partir do campo user_id . |
username |
principal.user.product_object_id |
Mapeado diretamente a partir do campo username . |
Precisa de mais ajuda? Receba respostas de membros da comunidade e profissionais da Google SecOps.