Slack 監査ログを収集する
このドキュメントでは、Amazon S3 を使用して Slack 監査ログを Google Security Operations に取り込む方法について説明します。パーサーは、まずブール値を正規化し、事前定義されたフィールドをクリアします。次に、「message」フィールドを JSON として解析し、JSON 以外のメッセージは破棄して処理します。特定のフィールド(date_create
と user_id
)の有無に応じて、パーサーは異なるロジックを適用して、メタデータ、プリンシパル、ネットワーク、ターゲット、情報などの未加工ログフィールドを UDM にマッピングし、セキュリティ結果を構築します。
始める前に
次の前提条件を満たしていることを確認してください。
- Google SecOps インスタンス
- Slack Enterprise Grid テナントと管理コンソールへの特権アクセス
- AWS(S3、IAM、Lambda、EventBridge)への特権アクセス
Slack の前提条件(アプリ ID、OAuth トークン、組織 ID)を収集する
- Slack 管理コンソールにログインします。
- https://api.slack.com/apps にアクセスし、[Create New App > From scratch] をクリックします。
- 一意のアプリ名を入力し、Slack ワークスペースを選択します。
- [アプリを作成] をクリックします。
- 左側のサイドバーで [OAuth & Permissions] に移動します。
- [Scopes] セクションに移動し、次のユーザー トークン スコープ(auditlogs:read)を追加します。
- [Install to Workspace] > [Allow] をクリックします。
- インストールが完了したら、[組織レベルのアプリ] に移動します。
- [組織にインストール] をクリックします。
- 組織のオーナー/管理者アカウントでアプリを承認します。
xoxp-
で始まる [User OAuth Token] をコピーして安全に保存します(これが SLACK_AUDIT_TOKEN です)。- Slack 管理コンソールの [Settings & Permissions] > [Organization settings] で確認できる組織 ID をメモします。
Google SecOps 用に AWS S3 バケットと IAM を構成する
- バケットの作成のユーザーガイドに沿って、Amazon S3 バケットを作成します。
- 後で参照できるように、バケットの名前とリージョンを保存します(例:
slack-audit-logs
)。 - IAM ユーザーの作成のユーザーガイドに沿って、ユーザーを作成します。
- 作成したユーザーを選択します。
- [セキュリティ認証情報] タブを選択します。
- [アクセスキー] セクションで [アクセスキーを作成] をクリックします。
- [ユースケース] で [サードパーティ サービス] を選択します。
- [次へ] をクリックします。
- 省略可: 説明タグを追加します。
- [アクセスキーを作成] をクリックします。
- [CSV ファイルをダウンロード] をクリックして、[アクセスキー] と [シークレット アクセスキー] を保存し、後で使用できるようにします。
- [完了] をクリックします。
- [権限] タブを選択します。
- [権限ポリシー] セクションで、[権限を追加] をクリックします。
- [権限を追加] を選択します。
- [ポリシーを直接アタッチする] を選択します。
- AmazonS3FullAccess ポリシーを検索して選択します。
- [次へ] をクリックします。
- [権限を追加] をクリックします。
S3 アップロードの IAM ポリシーとロールを構成する
- AWS コンソールで、[IAM> ポリシー> ポリシーの作成> JSON タブ] に移動します。
次のポリシーを入力します。
{ "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" } ] }
- 別のバケット名を入力した場合は、
slack-audit-logs
を置き換えます。
- 別のバケット名を入力した場合は、
[次へ] > [ポリシーを作成] をクリックします。
[IAM] > [ロール] > [ロールの作成] > [AWS サービス] > [Lambda] に移動します。
新しく作成したポリシーを関連付けます。
ロールに「
SlackAuditToS3Role
」という名前を付けて、[ロールを作成] をクリックします。
Lambda 関数を作成する
- AWS コンソールで、[Lambda] > [Functions] > [Create function] に移動します。
- [Author from scratch] をクリックします。
- 次の構成情報を提供してください。
設定 | 値 |
---|---|
名前 | slack_audit_to_s3 |
ランタイム | Python 3.13 |
アーキテクチャ | x86_64 |
実行ロール | SlackAuditToS3Role |
関数を作成したら、[コード] タブを開き、スタブを削除して、次のコード(
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())
[構成> 環境変数 > 編集 > 新しい環境変数を追加] に移動します。
次の環境変数を入力し、実際の値に置き換えます。
キー 値の例 S3_BUCKET
slack-audit-logs
S3_PREFIX
slack/audit/
STATE_KEY
slack/audit/state.json
SLACK_AUDIT_TOKEN
xoxp-***
(auditlogs:read
を含む組織レベルのユーザー トークン)LIMIT
200
MAX_PAGES
20
LOOKBACK_SECONDS
3600
HTTP_TIMEOUT
60
HTTP_RETRIES
3
RETRY_AFTER_DEFAULT
2
ACTIONS
(省略可、CSV) user_login,app_installed
関数が作成されたら、そのページにとどまるか、[Lambda] > [関数] > [your-function] を開きます。
[CONFIGURATION] タブを選択します。
[全般設定] パネルで、[編集] をクリックします。
[Timeout] を [5 minutes (300 seconds)] に変更し、[Save] をクリックします。
EventBridge スケジュールを作成する
- Amazon EventBridge > Scheduler > スケジュールの作成に移動します。
- 次の構成の詳細を入力します。
- 定期的なスケジュール: レート(
1 hour
)。 - ターゲット: Lambda 関数
slack_audit_to_s3
。 - 名前:
slack-audit-1h
- 定期的なスケジュール: レート(
- [スケジュールを作成] をクリックします。
省略可: Google SecOps 用の読み取り専用の IAM ユーザーと鍵を作成する
- AWS コンソールで、[IAM] > [Users] > [Add users] に移動します。
- [ユーザーを追加] をクリックします。
- 次の構成の詳細を入力します。
- ユーザー:
secops-reader
。 - アクセスタイプ: アクセスキー - プログラマティック アクセス。
- ユーザー:
- [ユーザーを作成] をクリックします。
- 最小限の読み取りポリシー(カスタム)を関連付ける: [ユーザー] > [secops-reader] > [権限] > [権限を追加] > [ポリシーを直接関連付ける] > [ポリシーを作成]。
JSON エディタで、次のポリシーを入力します。
{ "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" } ] }
名前を
secops-reader-policy
に設定します。[ポリシーの作成> 検索/選択> 次へ> 権限を追加] に移動します。
[セキュリティ認証情報] > [アクセスキー] > [アクセスキーを作成] に移動します。
CSV をダウンロードします(これらの値はフィードに入力されます)。
Slack Audit Logs を取り込むように Google SecOps でフィードを構成する
- [SIEM 設定] > [フィード] に移動します。
- [+ 新しいフィードを追加] をクリックします。
- [フィード名] フィールドに、フィードの名前を入力します(例:
Slack Audit Logs
)。 - [ソースタイプ] として [Amazon S3 V2] を選択します。
- [ログタイプ] として [Slack Audit] を選択します。
- [次へ] をクリックします。
- 次の入力パラメータの値を指定します。
- S3 URI:
s3://slack-audit-logs/slack/audit/
- Source deletion options: 必要に応じて削除オプションを選択します。
- ファイルの最大経過日数: 指定した日数以内に変更されたファイルを含めます。デフォルトは 180 日です。
- アクセスキー ID: S3 バケットにアクセスできるユーザー アクセスキー。
- シークレット アクセスキー: S3 バケットにアクセスできるユーザーのシークレット キー。
- アセットの名前空間: アセットの名前空間。
- Ingestion labels: このフィードのイベントに適用されるラベル。
- S3 URI:
- [次へ] をクリックします。
- [Finalize] 画面で新しいフィードの設定を確認し、[送信] をクリックします。
UDM マッピング テーブル
ログフィールド | UDM マッピング | ロジック |
---|---|---|
action |
metadata.product_event_type |
未加工ログの action フィールドから直接マッピングされます。 |
actor.type |
principal.labels.value |
actor.type フィールドから直接マッピングされ、キー actor.type が追加されます。 |
actor.user.email |
principal.user.email_addresses |
actor.user.email フィールドから直接マッピングされます。 |
actor.user.id |
principal.user.product_object_id |
actor.user.id フィールドから直接マッピングされます。 |
actor.user.id |
principal.user.userid |
actor.user.id フィールドから直接マッピングされます。 |
actor.user.name |
principal.user.user_display_name |
actor.user.name フィールドから直接マッピングされます。 |
actor.user.team |
principal.user.group_identifiers |
actor.user.team フィールドから直接マッピングされます。 |
context.ip_address |
principal.ip |
context.ip_address フィールドから直接マッピングされます。 |
context.location.domain |
about.resource.attribute.labels.value |
context.location.domain フィールドから直接マッピングされ、キー context.location.domain が追加されます。 |
context.location.id |
about.resource.id |
context.location.id フィールドから直接マッピングされます。 |
context.location.name |
about.resource.name |
context.location.name フィールドから直接マッピングされます。 |
context.location.name |
about.resource.attribute.labels.value |
context.location.name フィールドから直接マッピングされ、キー context.location.name が追加されます。 |
context.location.type |
about.resource.resource_subtype |
context.location.type フィールドから直接マッピングされます。 |
context.session_id |
network.session_id |
context.session_id フィールドから直接マッピングされます。 |
context.ua |
network.http.user_agent |
context.ua フィールドから直接マッピングされます。 |
context.ua |
network.http.parsed_user_agent |
parseduseragent フィルタを使用して context.ua フィールドから取得した、解析済みのユーザー エージェント情報。 |
country |
principal.location.country_or_region |
country フィールドから直接マッピングされます。 |
date_create |
metadata.event_timestamp.seconds |
date_create フィールドのエポック タイムスタンプがタイムスタンプ オブジェクトに変換されます。 |
details.inviter.email |
target.user.email_addresses |
details.inviter.email フィールドから直接マッピングされます。 |
details.inviter.id |
target.user.product_object_id |
details.inviter.id フィールドから直接マッピングされます。 |
details.inviter.name |
target.user.user_display_name |
details.inviter.name フィールドから直接マッピングされます。 |
details.inviter.team |
target.user.group_identifiers |
details.inviter.team フィールドから直接マッピングされます。 |
details.reason |
security_result.description |
details.reason フィールドから直接マッピングされます。配列の場合は、カンマで連結されます。 |
details.type |
about.resource.attribute.labels.value |
details.type フィールドから直接マッピングされ、キー details.type が追加されます。 |
details.type |
security_result.summary |
details.type フィールドから直接マッピングされます。 |
entity.app.id |
target.resource.id |
entity.app.id フィールドから直接マッピングされます。 |
entity.app.name |
target.resource.name |
entity.app.name フィールドから直接マッピングされます。 |
entity.channel.id |
target.resource.id |
entity.channel.id フィールドから直接マッピングされます。 |
entity.channel.name |
target.resource.name |
entity.channel.name フィールドから直接マッピングされます。 |
entity.channel.privacy |
target.resource.attribute.labels.value |
entity.channel.privacy フィールドから直接マッピングされ、キー entity.channel.privacy が追加されます。 |
entity.file.filetype |
target.resource.attribute.labels.value |
entity.file.filetype フィールドから直接マッピングされ、キー entity.file.filetype が追加されます。 |
entity.file.id |
target.resource.id |
entity.file.id フィールドから直接マッピングされます。 |
entity.file.name |
target.resource.name |
entity.file.name フィールドから直接マッピングされます。 |
entity.file.title |
target.resource.attribute.labels.value |
entity.file.title フィールドから直接マッピングされ、キー entity.file.title が追加されます。 |
entity.huddle.date_end |
about.resource.attribute.labels.value |
entity.huddle.date_end フィールドから直接マッピングされ、キー entity.huddle.date_end が追加されます。 |
entity.huddle.date_start |
about.resource.attribute.labels.value |
entity.huddle.date_start フィールドから直接マッピングされ、キー entity.huddle.date_start が追加されます。 |
entity.huddle.id |
about.resource.attribute.labels.value |
entity.huddle.id フィールドから直接マッピングされ、キー entity.huddle.id が追加されます。 |
entity.huddle.participants.0 |
about.resource.attribute.labels.value |
entity.huddle.participants.0 フィールドから直接マッピングされ、キー entity.huddle.participants.0 が追加されます。 |
entity.huddle.participants.1 |
about.resource.attribute.labels.value |
entity.huddle.participants.1 フィールドから直接マッピングされ、キー entity.huddle.participants.1 が追加されます。 |
entity.type |
target.resource.resource_subtype |
entity.type フィールドから直接マッピングされます。 |
entity.user.email |
target.user.email_addresses |
entity.user.email フィールドから直接マッピングされます。 |
entity.user.id |
target.user.product_object_id |
entity.user.id フィールドから直接マッピングされます。 |
entity.user.name |
target.user.user_display_name |
entity.user.name フィールドから直接マッピングされます。 |
entity.user.team |
target.user.group_identifiers |
entity.user.team フィールドから直接マッピングされます。 |
entity.workflow.id |
target.resource.id |
entity.workflow.id フィールドから直接マッピングされます。 |
entity.workflow.name |
target.resource.name |
entity.workflow.name フィールドから直接マッピングされます。 |
id |
metadata.product_log_id |
id フィールドから直接マッピングされます。 |
ip |
principal.ip |
ip フィールドから直接マッピングされます。action フィールドに基づくロジックによって決定されます。デフォルトは USER_COMMUNICATION ですが、action の値に基づいて USER_CREATION 、USER_LOGIN 、USER_LOGOUT 、USER_RESOURCE_ACCESS 、USER_RESOURCE_UPDATE_PERMISSIONS 、USER_CHANGE_PERMISSIONS などの他の値に変更されます。「SLACK_AUDIT」にハードコードされています。date_create が存在する場合は「Enterprise Grid」に設定し、存在しない場合は user_id が存在すれば「監査ログ」に設定します。「Slack」にハードコードされます。「REMOTE」にハードコードされます。action に「user_login」または「user_logout」が含まれている場合は、「SSO」に設定されます。それ以外の場合は、「MACHINE」に設定されます。記載の例ではマッピングされていません。デフォルトは「ALLOW」ですが、action が「user_login_failed」の場合は「BLOCK」に設定されます。date_create が存在する場合は「Slack」に設定され、それ以外の場合は user_id が存在する場合は「SLACK」に設定されます。 |
user_agent |
network.http.user_agent |
user_agent フィールドから直接マッピングされます。 |
user_id |
principal.user.product_object_id |
user_id フィールドから直接マッピングされます。 |
username |
principal.user.product_object_id |
username フィールドから直接マッピングされます。 |
さらにサポートが必要な場合 コミュニティ メンバーや Google SecOps のプロフェッショナルから回答を得ることができます。