Mengumpulkan log Peristiwa Bitwarden Enterprise
Panduan 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. Alat 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 region):
- 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 file .csv untuk menyimpan Access Key dan Secret Access Key untuk referensi di masa mendatang.
- Klik Done.
- Pilih tab Permissions.
- Klik Tambahkan izin di bagian Kebijakan izin.
- Pilih Tambahkan izin.
- Pilih Lampirkan kebijakan secara langsung.
- Cari kebijakan AmazonS3FullAccess.
- Pilih kebijakan.
- Klik Berikutnya.
- Klik Add permissions.
Mengonfigurasi kebijakan dan peran IAM untuk upload S3
- Buka konsol AWS > IAM > Policies > Create policy > tab JSON.
- Salin dan tempel kebijakan di bawah.
- Policy JSON (ganti
bitwarden-eventsjika Anda memasukkan nama bucket yang berbeda):
{
"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"
}
]
}
- 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 AWS Console, 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, dan tempelkan kode di bawah (
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_events_jsonl(events: list, run_ts_s: int, page_index: int) -> str:
"""
Write events in JSONL format (one JSON object per line).
Only writes if there are events to write.
Returns the S3 key of the written file.
"""
if not events:
return None
# Build JSONL content: one event per line
lines = [json.dumps(event, separators=(",", ":")) for event in events]
jsonl_content = "\n".join(lines) + "\n" # JSONL format with trailing newline
# Generate unique filename with page number to avoid conflicts
key = f"{PREFIX}/{time.strftime('%Y/%m/%d/%H%M%S', time.gmtime(run_ts_s))}-page{page_index:05d}-bitwarden-events.jsonl"
s3.put_object(
Bucket=BUCKET,
Key=key,
Body=jsonl_content.encode("utf-8"),
ContentType="application/x-ndjson", # MIME type for JSONL
)
return key
def lambda_handler(event=None, context=None):
bearer, _ttl = _get_token()
cont = _read_state()
run_ts_s = int(time.time())
pages = 0
total_events = 0
written_files = []
while pages < MAX_PAGES:
data = _fetch_events(bearer, cont)
# Extract events array from API response
# API returns: {"object":"list", "data":[...], "continuationToken":"..."}
events = data.get("data", [])
# Only write file if there are events
if events:
s3_key = _write_events_jsonl(events, run_ts_s, pages)
if s3_key:
written_files.append(s3_key)
total_events += len(events)
pages += 1
# Check for next page token
next_cont = data.get("continuationToken")
if next_cont:
cont = next_cont
continue
else:
# No more pages
break
# Save state only if there are more pages to continue in next run
# If we hit MAX_PAGES and there's still a continuation token, save it
# Otherwise, clear the state (set to None)
_write_state(cont if pages >= MAX_PAGES and cont else None)
return {
"ok": True,
"pages": pages,
"total_events": total_events,
"files_written": len(written_files),
"nextContinuationToken": cont if pages >= MAX_PAGES else None
}
if __name__ == "__main__":
print(lambda_handler())
- Buka Configuration > Environment variables > Edit > Add new environment variable.
- Masukkan variabel lingkungan yang disediakan di bawah, lalu ganti dengan nilai Anda.
Variabel lingkungan
| 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
- Buka Konsol AWS > IAM > Pengguna > Tambahkan pengguna.
- Klik Add users.
- Berikan detail konfigurasi berikut:
- Pengguna: Masukkan
secops-reader. - Jenis akses: Pilih Kunci akses — Akses terprogram.
- Pengguna: Masukkan
- Klik Buat pengguna.
- Lampirkan kebijakan baca minimal (kustom): Pengguna > secops-reader > Izin > Tambahkan izin > Lampirkan kebijakan secara langsung > Buat kebijakan.
- JSON:
{
"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>"
}
]
}
- Nama =
secops-reader-policy. - Klik Buat kebijakan > cari/pilih > Berikutnya > Tambahkan izin.
- Buat kunci akses untuk
secops-reader: Kredensial keamanan > Kunci akses > Buat kunci akses > download.csv(Anda akan menempelkan nilai ini ke feed).
Mengonfigurasi feed di Google SecOps untuk menyerap 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 yang 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.