Mengumpulkan log peristiwa Bitwarden Enterprise
Dokumen ini menjelaskan cara menyerap log peristiwa Bitwarden Enterprise ke Google Security Operations menggunakan Amazon S3. Parser mengubah log peristiwa berformat JSON mentah menjadi format terstruktur yang sesuai dengan UDM Chronicle. Fitur ini mengekstrak kolom yang relevan seperti detail pengguna, alamat IP, dan jenis peristiwa, lalu memetakannya ke kolom UDM yang sesuai untuk analisis keamanan yang konsisten.
Sebelum memulai
- Instance Google SecOps
- Akses istimewa ke tenant Bitwarden
- Akses istimewa ke AWS (S3, IAM, Lambda, EventBridge)
Mendapatkan kunci dan URL API Bitwarden
- Di konsol Admin Bitwarden.
- Buka Setelan > Info organisasi > Lihat kunci API.
- Salin dan simpan detail berikut ke lokasi yang aman:
- Client ID
- Rahasia Klien
- Tentukan endpoint Bitwarden Anda (berdasarkan wilayah):
- IDENTITY_URL:
https://identity.bitwarden.com/connect/token
(Uni Eropa:https://identity.bitwarden.eu/connect/token
) - API_BASE:
https://api.bitwarden.com
(Uni Eropa:https://api.bitwarden.eu
)
- IDENTITY_URL:
Mengonfigurasi bucket AWS S3 dan IAM untuk Google SecOps
- Buat bucket Amazon S3 dengan mengikuti panduan pengguna ini: Membuat bucket
- Simpan Name dan Region bucket untuk referensi di masa mendatang (misalnya,
bitwarden-events
). - Buat pengguna dengan mengikuti panduan pengguna ini: Membuat pengguna IAM.
- Pilih Pengguna yang dibuat.
- Pilih tab Kredensial keamanan.
- Klik Create Access Key di bagian Access Keys.
- Pilih Layanan pihak ketiga sebagai Kasus penggunaan.
- Klik Berikutnya.
- Opsional: tambahkan tag deskripsi.
- Klik Create access key.
- Klik Download CSV file untuk menyimpan Access Key dan Secret Access Key untuk digunakan nanti.
- Klik Selesai.
- Pilih tab Izin.
- Klik Tambahkan izin di bagian Kebijakan izin.
- Pilih Tambahkan izin.
- Pilih Lampirkan kebijakan secara langsung
- Telusuri dan pilih kebijakan AmazonS3FullAccess.
- Klik Berikutnya.
- Klik Add permissions.
Mengonfigurasi kebijakan dan peran IAM untuk upload S3
- Buka konsol AWS > IAM > Policies > Create policy > tab JSON.
Masukkan kebijakan berikut:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutBitwardenObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bitwarden-events/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::bitwarden-events/bitwarden/events/state.json" } ] }
- Ganti
bitwarden-events
jika Anda memasukkan nama bucket yang berbeda.
- Ganti
Klik Berikutnya > Buat kebijakan.
Buka IAM > Roles > Create role > AWS service > Lambda.
Lampirkan kebijakan yang baru dibuat.
Beri nama peran
WriteBitwardenToS3Role
, lalu klik Buat peran.
Buat fungsi Lambda
- Di Konsol AWS, buka Lambda > Functions > Create function.
- Klik Buat dari awal.
Berikan detail konfigurasi berikut:
Setelan Nilai Nama bitwarden_events_to_s3
Runtime Python 3.13 Arsitektur x86_64 Peran eksekusi WriteBitwardenToS3Role
Setelah fungsi dibuat, buka tab Code, hapus stub, lalu masukkan kode berikut (
bitwarden_events_to_s3.py
):#!/usr/bin/env python3 import os, json, time, urllib.parse from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError import boto3 IDENTITY_URL = os.environ.get("IDENTITY_URL", "https://identity.bitwarden.com/connect/token") API_BASE = os.environ.get("API_BASE", "https://api.bitwarden.com").rstrip("/") CID = os.environ["BW_CLIENT_ID"] # organization.ClientId CSECRET = os.environ["BW_CLIENT_SECRET"] # organization.ClientSecret BUCKET = os.environ["S3_BUCKET"] PREFIX = os.environ.get("S3_PREFIX", "bitwarden/events/").strip("/") STATE_KEY = os.environ.get("STATE_KEY", "bitwarden/events/state.json") MAX_PAGES = int(os.environ.get("MAX_PAGES", "10")) HEADERS_FORM = {"Content-Type": "application/x-www-form-urlencoded"} HEADERS_JSON = {"Accept": "application/json"} s3 = boto3.client("s3") def _read_state(): try: obj = s3.get_object(Bucket=BUCKET, Key=STATE_KEY) j = json.loads(obj["Body"].read()) return j.get("continuationToken") except Exception: return None def _write_state(token): body = json.dumps({"continuationToken": token}).encode("utf-8") s3.put_object(Bucket=BUCKET, Key=STATE_KEY, Body=body, ContentType="application/json") def _http(req: Request, timeout: int = 60, max_retries: int = 5): attempt, backoff = 0, 1.0 while True: try: with urlopen(req, timeout=timeout) as r: return json.loads(r.read().decode("utf-8")) except HTTPError as e: # Retry on 429 and 5xx if (e.code == 429 or 500 <= e.code <= 599) and attempt < max_retries: time.sleep(backoff); attempt += 1; backoff *= 2; continue raise except URLError: if attempt < max_retries: time.sleep(backoff); attempt += 1; backoff *= 2; continue raise def _get_token(): body = urllib.parse.urlencode({ "grant_type": "client_credentials", "scope": "api.organization", "client_id": CID, "client_secret": CSECRET, }).encode("utf-8") req = Request(IDENTITY_URL, data=body, method="POST", headers=HEADERS_FORM) data = _http(req, timeout=30) return data["access_token"], int(data.get("expires_in", 3600)) def _fetch_events(bearer: str, cont: str | None): params = {} if cont: params["continuationToken"] = cont qs = ("?" + urllib.parse.urlencode(params)) if params else "" url = f"{API_BASE}/public/events{qs}" req = Request(url, method="GET", headers={"Authorization": f"Bearer {bearer}", **HEADERS_JSON}) return _http(req, timeout=60) def _write_page(obj: dict, run_ts_s: int, page_index: int) -> str: # Make filename unique per page to avoid overwrites in the same second key = f"{PREFIX}/{time.strftime('%Y/%m/%d/%H%M%S', time.gmtime(run_ts_s))}-page{page_index:05d}-bitwarden-events.json" s3.put_object( Bucket=BUCKET, Key=key, Body=json.dumps(obj, separators=(",", ":")).encode("utf-8"), ContentType="application/json", ) return key def lambda_handler(event=None, context=None): bearer, _ttl = _get_token() cont = _read_state() run_ts_s = int(time.time()) pages = 0 written = 0 while pages < MAX_PAGES: data = _fetch_events(bearer, cont) # write page _write_page(data, run_ts_s, pages) pages += 1 # count entries (official shape: {"object":"list","data":[...], "continuationToken": "..."} ) entries = [] if isinstance(data.get("data"), list): entries = data["data"] elif isinstance(data.get("entries"), list): # fallback if shape differs entries = data["entries"] written += len(entries) # next page token (official: "continuationToken") next_cont = data.get("continuationToken") if next_cont: cont = next_cont continue break # Save state only if there are more pages to continue in next run _write_state(cont if pages >= MAX_PAGES and cont else None) return {"ok": True, "pages": pages, "events_estimate": written, "nextContinuationToken": cont} if __name__ == "__main__": print(lambda_handler())
Buka Configuration > Environment variables > Edit > Add new environment variable.
Masukkan variabel lingkungan berikut, ganti dengan nilai Anda:
Kunci Contoh S3_BUCKET
bitwarden-events
S3_PREFIX
bitwarden/events/
STATE_KEY
bitwarden/events/state.json
BW_CLIENT_ID
<organization client_id>
BW_CLIENT_SECRET
<organization client_secret>
IDENTITY_URL
https://identity.bitwarden.com/connect/token
(Uni Eropa:https://identity.bitwarden.eu/connect/token
)API_BASE
https://api.bitwarden.com
(Uni Eropa:https://api.bitwarden.eu
)MAX_PAGES
10
Setelah fungsi dibuat, tetap buka halamannya (atau buka Lambda > Functions > your-function).
Pilih tab Configuration
Di panel General configuration, klik Edit.
Ubah Waktu tunggu menjadi 5 menit (300 detik), lalu klik Simpan.
Membuat jadwal EventBridge
- Buka Amazon EventBridge > Scheduler > Create schedule.
- Berikan detail konfigurasi berikut:
- Jadwal berulang: Tarif (
1 hour
). - Target: fungsi Lambda Anda.
- Name:
bitwarden-events-1h
.
- Jadwal berulang: Tarif (
- Klik Buat jadwal.
Opsional: Buat pengguna & kunci IAM hanya baca untuk Google SecOps
- Di Konsol AWS, buka IAM > Users, lalu klik Add users.
- Berikan detail konfigurasi berikut:
- Pengguna: Masukkan nama unik (misalnya,
secops-reader
) - Jenis akses: Pilih Kunci akses - Akses terprogram
- Klik Buat pengguna.
- Pengguna: Masukkan nama unik (misalnya,
- Lampirkan kebijakan baca minimal (kustom): Pengguna > pilih
secops-reader
> Izin > Tambahkan izin > Lampirkan kebijakan secara langsung > Buat kebijakan Di editor JSON, masukkan kebijakan berikut:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::<your-bucket>/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::<your-bucket>" } ] }
Tetapkan nama ke
secops-reader-policy
.Buka Buat kebijakan > telusuri/pilih > Berikutnya > Tambahkan izin.
Buka Kredensial keamanan > Kunci akses > Buat kunci akses.
Download CSV (nilai ini dimasukkan ke dalam feed).
Mengonfigurasi feed di Google SecOps untuk memproses Log Peristiwa Bitwarden Enterprise
- Buka Setelan SIEM > Feed.
- Klik + Tambahkan Feed Baru.
- Di kolom Nama feed, masukkan nama untuk feed (misalnya,
Bitwarden Events
). - Pilih Amazon S3 V2 sebagai Jenis sumber.
- Pilih Peristiwa Bitwarden sebagai Jenis log.
- Klik Berikutnya.
- Tentukan nilai untuk parameter input berikut:
- URI S3:
s3://bitwarden-events/bitwarden/events/
- Opsi penghapusan sumber: Pilih opsi penghapusan sesuai preferensi Anda.
- Usia File Maksimum: Default 180 Hari.
- ID Kunci Akses: Kunci akses pengguna dengan akses ke bucket S3.
- Kunci Akses Rahasia: Kunci rahasia pengguna dengan akses ke bucket S3.
- Namespace aset: namespace aset.
- Label penyerapan: label yang diterapkan ke peristiwa dari feed ini.
- URI S3:
- Klik Berikutnya.
- Tinjau konfigurasi feed baru Anda di layar Selesaikan, lalu klik Kirim.
Tabel Pemetaan UDM
Kolom Log | Pemetaan UDM | Logika |
---|---|---|
actingUserId | target.user.userid | Jika enriched.actingUser.userId kosong atau null, kolom ini digunakan untuk mengisi kolom target.user.userid . |
collectionID | security_result.detection_fields.key | Mengisi kolom key dalam detection_fields di security_result . |
collectionID | security_result.detection_fields.value | Mengisi kolom value dalam detection_fields di security_result . |
tanggal | metadata.event_timestamp | Diuraikan dan dikonversi ke format stempel waktu serta dipetakan ke event_timestamp . |
enriched.actingUser.accessAll | security_result.rule_labels.key | Menetapkan nilai ke "Access_All" dalam rule_labels di security_result . |
enriched.actingUser.accessAll | security_result.rule_labels.value | Mengisi kolom value dalam rule_labels di security_result dengan nilai dari enriched.actingUser.accessAll yang dikonversi menjadi string. |
enriched.actingUser.email | target.user.email_addresses | Mengisi kolom email_addresses dalam target.user . |
enriched.actingUser.id | metadata.product_log_id | Mengisi kolom product_log_id dalam metadata . |
enriched.actingUser.id | target.labels.key | Menetapkan nilai ke "ID" dalam target.labels . |
enriched.actingUser.id | target.labels.value | Mengisi kolom value dalam target.labels dengan nilai dari enriched.actingUser.id . |
enriched.actingUser.name | target.user.user_display_name | Mengisi kolom user_display_name dalam target.user . |
enriched.actingUser.object | target.labels.key | Menetapkan nilai ke "Object" dalam target.labels . |
enriched.actingUser.object | target.labels.value | Mengisi kolom value dalam target.labels dengan nilai dari enriched.actingUser.object . |
enriched.actingUser.resetPasswordEnrolled | target.labels.key | Menetapkan nilai ke "ResetPasswordEnrolled" dalam target.labels . |
enriched.actingUser.resetPasswordEnrolled | target.labels.value | Mengisi kolom value dalam target.labels dengan nilai dari enriched.actingUser.resetPasswordEnrolled yang dikonversi menjadi string. |
enriched.actingUser.twoFactorEnabled | security_result.rule_labels.key | Menetapkan nilai ke "Two Factor Enabled" dalam rule_labels di security_result . |
enriched.actingUser.twoFactorEnabled | security_result.rule_labels.value | Mengisi kolom value dalam rule_labels di security_result dengan nilai dari enriched.actingUser.twoFactorEnabled yang dikonversi menjadi string. |
enriched.actingUser.userId | target.user.userid | Mengisi kolom userid dalam target.user . |
enriched.collection.id | additional.fields.key | Menetapkan nilai ke "ID Kumpulan" dalam additional.fields . |
enriched.collection.id | additional.fields.value.string_value | Mengisi kolom string_value dalam additional.fields dengan nilai dari enriched.collection.id . |
enriched.collection.object | additional.fields.key | Menetapkan nilai ke "Objek Kumpulan" dalam additional.fields . |
enriched.collection.object | additional.fields.value.string_value | Mengisi kolom string_value dalam additional.fields dengan nilai dari enriched.collection.object . |
enriched.type | metadata.product_event_type | Mengisi kolom product_event_type dalam metadata . |
groupId | target.user.group_identifiers | Menambahkan nilai ke array group_identifiers dalam target.user . |
ipAddress | principal.ip | Alamat IP diekstrak dari kolom dan dipetakan ke principal.ip . |
T/A | extensions.auth | Objek kosong dibuat oleh parser. |
T/A | metadata.event_type | Ditentukan berdasarkan enriched.type dan keberadaan informasi principal dan target . Nilai yang mungkin: USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT. |
T/A | security_result.action | Ditentukan berdasarkan enriched.type . Nilai yang mungkin: ALLOW, BLOCK. |
objek | additional.fields.key | Menetapkan nilai ke "Object" dalam additional.fields . |
objek | additional.fields.value | Mengisi kolom value dalam additional.fields dengan nilai dari object . |
Perlu bantuan lain? Dapatkan jawaban dari anggota Komunitas dan profesional Google SecOps.