Recopila registros de auditoría de Slack
En este documento, se explica cómo transferir registros de auditoría de Slack a Google Security Operations con Amazon S3. Primero, el analizador normaliza los valores booleanos y borra los campos predefinidos. Luego, analiza el campo "message" como JSON y descarta los mensajes que no son JSON. Según la presencia de campos específicos (date_create
y user_id
), el analizador aplica una lógica diferente para asignar los campos de registro sin procesar al UDM, incluida la información de metadatos, principal, red, destino y acerca de, y construye un resultado de seguridad.
Antes de comenzar
Asegúrate de cumplir con los siguientes requisitos previos:
- Instancia de Google SecOps
- Acceso con privilegios al arrendatario de Slack Enterprise Grid y a la Consola del administrador
- Acceso con privilegios a AWS (S3, IAM, Lambda, EventBridge)
Recopila los requisitos previos de Slack (ID de la app, token de OAuth y ID de la organización)
- Accede a la Consola del administrador de Slack.
- Ve a https://api.slack.com/apps y haz clic en Crear una app nueva > Desde cero.
- Ingresa un nombre de aplicación único y selecciona tu espacio de trabajo de Slack.
- Haz clic en Crear app.
- Navega a OAuth y permisos en la barra lateral izquierda.
- Ve a la sección Alcances y agrega el siguiente alcance de token de usuario: auditlogs:read
- Haz clic en Instalar en Workspace > Permitir.
- Una vez instalada, ve a Apps a nivel de la organización.
- Haz clic en Install to Organization.
- Autoriza la app con una cuenta de administrador o propietario de la organización.
- Copia y guarda de forma segura el token de OAuth del usuario que comienza con
xoxp-
(este es tu SLACK_AUDIT_TOKEN). - Ten en cuenta el ID de organización, que se encuentra en la Consola del administrador de Slack en Configuración y permisos > Configuración de la organización.
Configura el bucket de AWS S3 y el IAM para Google SecOps
- Crea un bucket de Amazon S3 siguiendo esta guía del usuario: Crea un bucket
- Guarda el Nombre y la Región del bucket para futuras referencias (por ejemplo,
slack-audit-logs
). - Crea un usuario siguiendo esta guía del usuario: Cómo crear un usuario de IAM.
- Selecciona el usuario creado.
- Selecciona la pestaña Credenciales de seguridad.
- Haz clic en Crear clave de acceso en la sección Claves de acceso.
- Selecciona Servicio de terceros como el Caso de uso.
- Haz clic en Siguiente.
- Opcional: Agrega una etiqueta de descripción.
- Haz clic en Crear clave de acceso.
- Haz clic en Descargar archivo CSV para guardar la clave de acceso y la clave de acceso secreta para usarlas más adelante.
- Haz clic en Listo.
- Selecciona la pestaña Permisos.
- Haz clic en Agregar permisos en la sección Políticas de permisos.
- Selecciona Agregar permisos.
- Selecciona Adjuntar políticas directamente.
- Busca y selecciona la política AmazonS3FullAccess.
- Haz clic en Siguiente.
- Haz clic en Agregar permisos.
Configura la política y el rol de IAM para las cargas de S3
- En la consola de AWS, ve a IAM > Políticas > Crear política > pestaña JSON.
Ingresa la siguiente 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" } ] }
- Reemplaza
slack-audit-logs
si ingresaste un nombre de bucket diferente.
- Reemplaza
Haz clic en Siguiente > Crear política.
Ve a IAM > Roles > Crear rol > Servicio de AWS > Lambda.
Adjunta la política recién creada.
Asigna el nombre
SlackAuditToS3Role
al rol y haz clic en Crear rol.
Crea la función Lambda
- En la consola de AWS, ve a Lambda > Functions > Create function.
- Haz clic en Crear desde cero.
- Proporciona los siguientes detalles de configuración:
Configuración | Valor |
---|---|
Nombre | slack_audit_to_s3 |
Tiempo de ejecución | Python 3.13 |
Arquitectura | x86_64 |
Rol de ejecución | SlackAuditToS3Role |
Después de crear la función, abre la pestaña Code, borra el código auxiliar y, luego, ingresa lo siguiente (
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())
Ve a Configuración > Variables de entorno > Editar > Agregar nueva variable de entorno.
Ingresa las siguientes variables de entorno y reemplázalas por tus valores:
Clave Valor de ejemplo S3_BUCKET
slack-audit-logs
S3_PREFIX
slack/audit/
STATE_KEY
slack/audit/state.json
SLACK_AUDIT_TOKEN
xoxp-***
(token de usuario a nivel de la organización conauditlogs: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
Después de crear la función, permanece en su página (o abre Lambda > Funciones > tu-función).
Selecciona la pestaña Configuración.
En el panel Configuración general, haz clic en Editar.
Cambia Tiempo de espera a 5 minutos (300 segundos) y haz clic en Guardar.
Crea una programación de EventBridge
- Ve a Amazon EventBridge > Scheduler > Create schedule.
- Proporciona los siguientes detalles de configuración:
- Programación recurrente: Frecuencia (
1 hour
). - Destino: Tu función Lambda
slack_audit_to_s3
. - Nombre:
slack-audit-1h
.
- Programación recurrente: Frecuencia (
- Haz clic en Crear programación.
Opcional: Crea un usuario y claves de IAM de solo lectura para Google SecOps
- En la consola de AWS, ve a IAM > Usuarios > Agregar usuarios.
- Haz clic en Agregar usuarios.
- Proporciona los siguientes detalles de configuración:
- Usuario:
secops-reader
. - Tipo de acceso: Clave de acceso: Acceso programático
- Usuario:
- Haz clic en Crear usuario.
- Adjunta la política de lectura mínima (personalizada): Usuarios > secops-reader > Permisos > Agregar permisos > Adjuntar políticas directamente > Crear política.
En el editor de JSON, ingresa la siguiente 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" } ] }
Configura el nombre como
secops-reader-policy
.Ve a Crear política > busca o selecciona > Siguiente > Agregar permisos.
Ve a Credenciales de seguridad > Claves de acceso > Crear clave de acceso.
Descarga el archivo CSV (estos valores se ingresan en el feed).
Configura un feed en Google SecOps para transferir Registros de auditoría de Slack
- Ve a Configuración de SIEM > Feeds.
- Haz clic en + Agregar feed nuevo.
- En el campo Nombre del feed, ingresa un nombre para el feed (por ejemplo,
Slack Audit Logs
). - Selecciona Amazon S3 V2 como el Tipo de fuente.
- Selecciona Auditoría de Slack como el Tipo de registro.
- Haz clic en Siguiente.
- Especifica valores para los siguientes parámetros de entrada:
- URI de S3:
s3://slack-audit-logs/slack/audit/
- Opciones de borrado de la fuente: Selecciona la opción de borrado según tu preferencia.
- Antigüedad máxima del archivo: Incluye los archivos modificados en la cantidad de días especificada. El valor predeterminado es de 180 días.
- ID de clave de acceso: Clave de acceso del usuario con acceso al bucket de S3.
- Clave de acceso secreta: Clave secreta del usuario con acceso al bucket de S3.
- Espacio de nombres del recurso: Es el espacio de nombres del recurso.
- Etiquetas de transmisión: Es la etiqueta que se aplica a los eventos de este feed.
- URI de S3:
- Haz clic en Siguiente.
- Revisa la nueva configuración del feed en la pantalla Finalizar y, luego, haz clic en Enviar.
Tabla de asignación de UDM
Campo de registro | Asignación de UDM | Lógica |
---|---|---|
action |
metadata.product_event_type |
Se asigna directamente desde el campo action en el registro sin procesar. |
actor.type |
principal.labels.value |
Se asigna directamente desde el campo actor.type , con la clave actor.type agregada. |
actor.user.email |
principal.user.email_addresses |
Se asigna directamente desde el campo actor.user.email . |
actor.user.id |
principal.user.product_object_id |
Se asigna directamente desde el campo actor.user.id . |
actor.user.id |
principal.user.userid |
Se asigna directamente desde el campo actor.user.id . |
actor.user.name |
principal.user.user_display_name |
Se asigna directamente desde el campo actor.user.name . |
actor.user.team |
principal.user.group_identifiers |
Se asigna directamente desde el campo actor.user.team . |
context.ip_address |
principal.ip |
Se asigna directamente desde el campo context.ip_address . |
context.location.domain |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo context.location.domain , con la clave context.location.domain agregada. |
context.location.id |
about.resource.id |
Se asigna directamente desde el campo context.location.id . |
context.location.name |
about.resource.name |
Se asigna directamente desde el campo context.location.name . |
context.location.name |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo context.location.name , con la clave context.location.name agregada. |
context.location.type |
about.resource.resource_subtype |
Se asigna directamente desde el campo context.location.type . |
context.session_id |
network.session_id |
Se asigna directamente desde el campo context.session_id . |
context.ua |
network.http.user_agent |
Se asigna directamente desde el campo context.ua . |
context.ua |
network.http.parsed_user_agent |
Es la información analizada del usuario-agente que se deriva del campo context.ua con el filtro parseduseragent . |
country |
principal.location.country_or_region |
Se asigna directamente desde el campo country . |
date_create |
metadata.event_timestamp.seconds |
La marca de tiempo de época del campo date_create se convierte en un objeto de marca de tiempo. |
details.inviter.email |
target.user.email_addresses |
Se asigna directamente desde el campo details.inviter.email . |
details.inviter.id |
target.user.product_object_id |
Se asigna directamente desde el campo details.inviter.id . |
details.inviter.name |
target.user.user_display_name |
Se asigna directamente desde el campo details.inviter.name . |
details.inviter.team |
target.user.group_identifiers |
Se asigna directamente desde el campo details.inviter.team . |
details.reason |
security_result.description |
Se asigna directamente desde el campo details.reason o, si es un array, se concatena con comas. |
details.type |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo details.type , con la clave details.type agregada. |
details.type |
security_result.summary |
Se asigna directamente desde el campo details.type . |
entity.app.id |
target.resource.id |
Se asigna directamente desde el campo entity.app.id . |
entity.app.name |
target.resource.name |
Se asigna directamente desde el campo entity.app.name . |
entity.channel.id |
target.resource.id |
Se asigna directamente desde el campo entity.channel.id . |
entity.channel.name |
target.resource.name |
Se asigna directamente desde el campo entity.channel.name . |
entity.channel.privacy |
target.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.channel.privacy , con la clave entity.channel.privacy agregada. |
entity.file.filetype |
target.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.file.filetype , con la clave entity.file.filetype agregada. |
entity.file.id |
target.resource.id |
Se asigna directamente desde el campo entity.file.id . |
entity.file.name |
target.resource.name |
Se asigna directamente desde el campo entity.file.name . |
entity.file.title |
target.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.file.title , con la clave entity.file.title agregada. |
entity.huddle.date_end |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.huddle.date_end , con la clave entity.huddle.date_end agregada. |
entity.huddle.date_start |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.huddle.date_start , con la clave entity.huddle.date_start agregada. |
entity.huddle.id |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.huddle.id , con la clave entity.huddle.id agregada. |
entity.huddle.participants.0 |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.huddle.participants.0 , con la clave entity.huddle.participants.0 agregada. |
entity.huddle.participants.1 |
about.resource.attribute.labels.value |
Se asigna directamente desde el campo entity.huddle.participants.1 , con la clave entity.huddle.participants.1 agregada. |
entity.type |
target.resource.resource_subtype |
Se asigna directamente desde el campo entity.type . |
entity.user.email |
target.user.email_addresses |
Se asigna directamente desde el campo entity.user.email . |
entity.user.id |
target.user.product_object_id |
Se asigna directamente desde el campo entity.user.id . |
entity.user.name |
target.user.user_display_name |
Se asigna directamente desde el campo entity.user.name . |
entity.user.team |
target.user.group_identifiers |
Se asigna directamente desde el campo entity.user.team . |
entity.workflow.id |
target.resource.id |
Se asigna directamente desde el campo entity.workflow.id . |
entity.workflow.name |
target.resource.name |
Se asigna directamente desde el campo entity.workflow.name . |
id |
metadata.product_log_id |
Se asigna directamente desde el campo id . |
ip |
principal.ip |
Se asigna directamente desde el campo ip . Se determina según la lógica basada en el campo action . El valor predeterminado es USER_COMMUNICATION , pero cambia a otros valores, como USER_CREATION , USER_LOGIN , USER_LOGOUT , USER_RESOURCE_ACCESS , USER_RESOURCE_UPDATE_PERMISSIONS o USER_CHANGE_PERMISSIONS , según el valor de action . Está codificado como "SLACK_AUDIT". Se establece en "Enterprise Grid" si existe date_create ; de lo contrario, se establece en "Registros de auditoría" si existe user_id . Se codificó de forma rígida como "Slack". Se codificó de forma rígida como "REMOTE". Se establece en "SSO" si action contiene "user_login" o "user_logout". De lo contrario, configúralo como "MACHINE". No se asigna en los ejemplos proporcionados. El valor predeterminado es "ALLOW", pero se establece en "BLOCK" si action es "user_login_failed". Se establece en "Slack" si existe date_create ; de lo contrario, se establece en "SLACK" si existe user_id . |
user_agent |
network.http.user_agent |
Se asigna directamente desde el campo user_agent . |
user_id |
principal.user.product_object_id |
Se asigna directamente desde el campo user_id . |
username |
principal.user.product_object_id |
Se asigna directamente desde el campo username . |
¿Necesitas más ayuda? Obtén respuestas de miembros de la comunidad y profesionales de Google SecOps.