Mengumpulkan log Peristiwa Bitwarden Enterprise

Didukung di:

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

  1. Di Konsol Admin Bitwarden.
  2. Buka Setelan > Info organisasi > Lihat kunci API.
  3. Salin dan simpan detail berikut ke lokasi yang aman:
    • Client ID
    • Rahasia Klien
  4. 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)

Mengonfigurasi bucket AWS S3 dan IAM untuk Google SecOps

  1. Buat bucket Amazon S3 dengan mengikuti panduan pengguna ini: Membuat bucket
  2. Simpan Name dan Region bucket untuk referensi di masa mendatang (misalnya, bitwarden-events).
  3. Buat Pengguna dengan mengikuti panduan pengguna ini: Membuat pengguna IAM.
  4. Pilih Pengguna yang dibuat.
  5. Pilih tab Kredensial keamanan.
  6. Klik Create Access Key di bagian Access Keys.
  7. Pilih Layanan pihak ketiga sebagai Kasus penggunaan.
  8. Klik Berikutnya.
  9. Opsional: Tambahkan tag deskripsi.
  10. Klik Create access key.
  11. Klik Download file .csv untuk menyimpan Access Key dan Secret Access Key untuk referensi di masa mendatang.
  12. Klik Done.
  13. Pilih tab Permissions.
  14. Klik Tambahkan izin di bagian Kebijakan izin.
  15. Pilih Tambahkan izin.
  16. Pilih Lampirkan kebijakan secara langsung.
  17. Cari kebijakan AmazonS3FullAccess.
  18. Pilih kebijakan.
  19. Klik Berikutnya.
  20. Klik Add permissions.

Mengonfigurasi kebijakan dan peran IAM untuk upload S3

  1. Buka konsol AWS > IAM > Policies > Create policy > tab JSON.
  2. Salin dan tempel kebijakan di bawah.
  3. Policy JSON (ganti bitwarden-events jika 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"
    }
  ]
}

  1. Klik Berikutnya > Buat kebijakan.
  2. Buka IAM > Roles > Create role > AWS service > Lambda.
  3. Lampirkan kebijakan yang baru dibuat.
  4. Beri nama peran WriteBitwardenToS3Role, lalu klik Buat peran.

Buat fungsi Lambda

  1. Di AWS Console, buka Lambda > Functions > Create function.
  2. Klik Buat dari awal.
  3. Berikan detail konfigurasi berikut:
Setelan Nilai
Nama bitwarden_events_to_s3
Runtime Python 3.13
Arsitektur x86_64
Peran eksekusi WriteBitwardenToS3Role
  1. 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())
  1. Buka Configuration > Environment variables > Edit > Add new environment variable.
  2. 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
  1. Setelah fungsi dibuat, tetap buka halamannya (atau buka Lambda > Functions > your‑function).
  2. Pilih tab Configuration
  3. Di panel General configuration, klik Edit.
  4. Ubah Waktu Tunggu menjadi 5 menit (300 detik), lalu klik Simpan.

Membuat jadwal EventBridge

  1. Buka Amazon EventBridge > Scheduler > Create schedule.
  2. Berikan detail konfigurasi berikut:
    • Jadwal berulang: Tarif (1 hour).
    • Target: Fungsi Lambda Anda.
    • Name: bitwarden-events-1h.
  3. Klik Buat jadwal.

(Opsional) Buat pengguna & kunci IAM hanya baca untuk Google SecOps

  1. Buka Konsol AWS > IAM > Pengguna > Tambahkan pengguna.
  2. Klik Add users.
  3. Berikan detail konfigurasi berikut:
    • Pengguna: Masukkan secops-reader.
    • Jenis akses: Pilih Kunci akses — Akses terprogram.
  4. Klik Buat pengguna.
  5. Lampirkan kebijakan baca minimal (kustom): Pengguna > secops-reader > Izin > Tambahkan izin > Lampirkan kebijakan secara langsung > Buat kebijakan.
  6. 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>"
    }
  ]
}
  1. Nama = secops-reader-policy.
  2. Klik Buat kebijakan > cari/pilih > Berikutnya > Tambahkan izin.
  3. 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

  1. Buka Setelan SIEM > Feed.
  2. Klik + Tambahkan Feed Baru.
  3. Di kolom Nama feed, masukkan nama untuk feed (misalnya, Bitwarden Events).
  4. Pilih Amazon S3 V2 sebagai Jenis sumber.
  5. Pilih Peristiwa Bitwarden sebagai Jenis log.
  6. Klik Berikutnya.
  7. 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.
  8. Klik Berikutnya.
  9. 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.