Collecter les journaux d'audit Slack
Ce document explique comment ingérer des journaux d'audit Slack dans Google Security Operations à l'aide d'Amazon S3. Le parseur normalise d'abord les valeurs booléennes et efface les champs prédéfinis. Il analyse ensuite le champ "message" en tant que JSON, en supprimant les messages non JSON. En fonction de la présence de champs spécifiques (date_create
et user_id
), l'analyseur applique une logique différente pour mapper les champs de journaux bruts à l'UDM, y compris les métadonnées, les informations sur le principal, le réseau, la cible et les informations "À propos", et construit un résultat de sécurité.
Avant de commencer
Assurez-vous de remplir les conditions suivantes :
- Instance Google SecOps
- Accès privilégié au locataire Slack Enterprise Grid et à la console d'administration
- Accès privilégié à AWS (S3, IAM, Lambda, EventBridge)
Collecter les prérequis Slack (ID d'application, jeton OAuth, ID d'organisation)
- Connectez-vous à la console d'administration Slack.
- Accédez à https://api.slack.com/apps, puis cliquez sur Create New App > From scratch (Créer une application > À partir de zéro).
- Saisissez un nom d'application unique et sélectionnez votre espace de travail Slack.
- Cliquez sur Créer une application.
- Accédez à OAuth et autorisations dans la barre latérale de gauche.
- Accédez à la section Scopes (Champs d'application) et ajoutez le champ d'application de jeton utilisateur suivant : auditlogs:read.
- Cliquez sur Installer dans Workspace > Autoriser.
- Une fois l'application installée, accédez à Applications au niveau de l'organisation.
- Cliquez sur Installer dans l'organisation.
- Autorisez l'application avec un compte propriétaire/administrateur de l'organisation.
- Copiez et enregistrez de manière sécurisée le jeton OAuth de l'utilisateur qui commence par
xoxp-
(il s'agit de votre SLACK_AUDIT_TOKEN). - Notez l'ID de l'organisation, que vous trouverez dans la console d'administration Slack sous Paramètres et autorisations > Paramètres de l'organisation.
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,
slack-audit-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": "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" } ] }
- Remplacez
slack-audit-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
SlackAuditToS3Role
, 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 | slack_audit_to_s3 |
Durée d'exécution | Python 3.13 |
Architecture | x86_64 |
Rôle d'exécution | SlackAuditToS3Role |
Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et saisissez ce qui suit (
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())
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
slack-audit-logs
S3_PREFIX
slack/audit/
STATE_KEY
slack/audit/state.json
SLACK_AUDIT_TOKEN
xoxp-***
(jeton utilisateur au niveau de l'organisation avecauditlogs:read
)LIMIT
200
MAX_PAGES
20
LOOKBACK_SECONDS
3600
HTTP_TIMEOUT
60
HTTP_RETRIES
3
RETRY_AFTER_DEFAULT
2
ACTIONS
(facultatif, CSV) user_login,app_installed
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> Create schedule (Créer une programmation).
- Fournissez les informations de configuration suivantes :
- Planning récurrent : Tarif (
1 hour
). - Cible : votre fonction Lambda
slack_audit_to_s3
. - Nom :
slack-audit-1h
.
- 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:::slack-audit-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::slack-audit-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'audit Slack
- 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,
Slack Audit Logs
). - Sélectionnez Amazon S3 V2 comme type de source.
- Sélectionnez Audit Slack comme Type de journal.
- Cliquez sur Suivant.
- Spécifiez les valeurs des paramètres d'entrée suivants :
- URI S3 :
s3://slack-audit-logs/slack/audit/
- 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 |
Directement mappé à partir du champ action dans le journal brut. |
actor.type |
principal.labels.value |
Directement mappé à partir du champ actor.type , avec la clé actor.type ajoutée. |
actor.user.email |
principal.user.email_addresses |
Mappé directement à partir du champ actor.user.email . |
actor.user.id |
principal.user.product_object_id |
Mappé directement à partir du champ actor.user.id . |
actor.user.id |
principal.user.userid |
Mappé directement à partir du champ actor.user.id . |
actor.user.name |
principal.user.user_display_name |
Mappé directement à partir du champ actor.user.name . |
actor.user.team |
principal.user.group_identifiers |
Mappé directement à partir du champ actor.user.team . |
context.ip_address |
principal.ip |
Mappé directement à partir du champ context.ip_address . |
context.location.domain |
about.resource.attribute.labels.value |
Directement mappé à partir du champ context.location.domain , avec la clé context.location.domain ajoutée. |
context.location.id |
about.resource.id |
Mappé directement à partir du champ context.location.id . |
context.location.name |
about.resource.name |
Mappé directement à partir du champ context.location.name . |
context.location.name |
about.resource.attribute.labels.value |
Directement mappé à partir du champ context.location.name , avec la clé context.location.name ajoutée. |
context.location.type |
about.resource.resource_subtype |
Mappé directement à partir du champ context.location.type . |
context.session_id |
network.session_id |
Mappé directement à partir du champ context.session_id . |
context.ua |
network.http.user_agent |
Mappé directement à partir du champ context.ua . |
context.ua |
network.http.parsed_user_agent |
Informations analysées sur l'agent utilisateur, dérivées du champ context.ua à l'aide du filtre parseduseragent . |
country |
principal.location.country_or_region |
Mappé directement à partir du champ country . |
date_create |
metadata.event_timestamp.seconds |
L'horodatage epoch du champ date_create est converti en objet d'horodatage. |
details.inviter.email |
target.user.email_addresses |
Mappé directement à partir du champ details.inviter.email . |
details.inviter.id |
target.user.product_object_id |
Mappé directement à partir du champ details.inviter.id . |
details.inviter.name |
target.user.user_display_name |
Mappé directement à partir du champ details.inviter.name . |
details.inviter.team |
target.user.group_identifiers |
Mappé directement à partir du champ details.inviter.team . |
details.reason |
security_result.description |
Directement mappé à partir du champ details.reason ou, s'il s'agit d'un tableau, concaténé avec des virgules. |
details.type |
about.resource.attribute.labels.value |
Directement mappé à partir du champ details.type , avec la clé details.type ajoutée. |
details.type |
security_result.summary |
Mappé directement à partir du champ details.type . |
entity.app.id |
target.resource.id |
Mappé directement à partir du champ entity.app.id . |
entity.app.name |
target.resource.name |
Mappé directement à partir du champ entity.app.name . |
entity.channel.id |
target.resource.id |
Mappé directement à partir du champ entity.channel.id . |
entity.channel.name |
target.resource.name |
Mappé directement à partir du champ entity.channel.name . |
entity.channel.privacy |
target.resource.attribute.labels.value |
Directement mappé à partir du champ entity.channel.privacy , avec la clé entity.channel.privacy ajoutée. |
entity.file.filetype |
target.resource.attribute.labels.value |
Directement mappé à partir du champ entity.file.filetype , avec la clé entity.file.filetype ajoutée. |
entity.file.id |
target.resource.id |
Mappé directement à partir du champ entity.file.id . |
entity.file.name |
target.resource.name |
Mappé directement à partir du champ entity.file.name . |
entity.file.title |
target.resource.attribute.labels.value |
Directement mappé à partir du champ entity.file.title , avec la clé entity.file.title ajoutée. |
entity.huddle.date_end |
about.resource.attribute.labels.value |
Directement mappé à partir du champ entity.huddle.date_end , avec la clé entity.huddle.date_end ajoutée. |
entity.huddle.date_start |
about.resource.attribute.labels.value |
Directement mappé à partir du champ entity.huddle.date_start , avec la clé entity.huddle.date_start ajoutée. |
entity.huddle.id |
about.resource.attribute.labels.value |
Directement mappé à partir du champ entity.huddle.id , avec la clé entity.huddle.id ajoutée. |
entity.huddle.participants.0 |
about.resource.attribute.labels.value |
Directement mappé à partir du champ entity.huddle.participants.0 , avec la clé entity.huddle.participants.0 ajoutée. |
entity.huddle.participants.1 |
about.resource.attribute.labels.value |
Directement mappé à partir du champ entity.huddle.participants.1 , avec la clé entity.huddle.participants.1 ajoutée. |
entity.type |
target.resource.resource_subtype |
Mappé directement à partir du champ entity.type . |
entity.user.email |
target.user.email_addresses |
Mappé directement à partir du champ entity.user.email . |
entity.user.id |
target.user.product_object_id |
Mappé directement à partir du champ entity.user.id . |
entity.user.name |
target.user.user_display_name |
Mappé directement à partir du champ entity.user.name . |
entity.user.team |
target.user.group_identifiers |
Mappé directement à partir du champ entity.user.team . |
entity.workflow.id |
target.resource.id |
Mappé directement à partir du champ entity.workflow.id . |
entity.workflow.name |
target.resource.name |
Mappé directement à partir du champ entity.workflow.name . |
id |
metadata.product_log_id |
Mappé directement à partir du champ id . |
ip |
principal.ip |
Mappé directement à partir du champ ip . Déterminé par une logique basée sur le champ action . La valeur par défaut est USER_COMMUNICATION , mais elle peut être remplacée par d'autres valeurs telles que USER_CREATION , USER_LOGIN , USER_LOGOUT , USER_RESOURCE_ACCESS , USER_RESOURCE_UPDATE_PERMISSIONS ou USER_CHANGE_PERMISSIONS en fonction de la valeur de action . Codé en dur sur "SLACK_AUDIT". Défini sur "Enterprise Grid" si date_create existe, sinon défini sur "Journaux d'audit" si user_id existe. Codé en dur sur "Slack". Codé en dur sur "REMOTE". Définissez sur "SSO" si action contient "user_login" ou "user_logout". Sinon, définissez-le sur "MACHINE". Non mappé dans les exemples fournis. La valeur par défaut est "ALLOW", mais elle est définie sur "BLOCK" si action est "user_login_failed". Définissez la valeur sur "Slack" si date_create existe, sinon définissez-la sur "SLACK" si user_id existe. |
user_agent |
network.http.user_agent |
Mappé directement à partir du champ user_agent . |
user_id |
principal.user.product_object_id |
Mappé directement à partir du champ user_id . |
username |
principal.user.product_object_id |
Mappé directement à partir du champ username . |
Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.