Mengumpulkan log peristiwa Bitwarden Enterprise

Didukung di:

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

  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 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)

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 CSV file untuk menyimpan Access Key dan Secret Access Key untuk digunakan nanti.
  12. Klik Selesai.
  13. Pilih tab Izin.
  14. Klik Tambahkan izin di bagian Kebijakan izin.
  15. Pilih Tambahkan izin.
  16. Pilih Lampirkan kebijakan secara langsung
  17. Telusuri dan pilih kebijakan AmazonS3FullAccess.
  18. Klik Berikutnya.
  19. Klik Add permissions.

Mengonfigurasi kebijakan dan peran IAM untuk upload S3

  1. Buka konsol AWS > IAM > Policies > Create policy > tab JSON.
  2. 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.
  3. Klik Berikutnya > Buat kebijakan.

  4. Buka IAM > Roles > Create role > AWS service > Lambda.

  5. Lampirkan kebijakan yang baru dibuat.

  6. Beri nama peran WriteBitwardenToS3Role, lalu klik Buat peran.

Buat fungsi Lambda

  1. Di Konsol AWS, 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
  4. 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())
    
    
  5. Buka Configuration > Environment variables > Edit > Add new environment variable.

  6. 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
  7. Setelah fungsi dibuat, tetap buka halamannya (atau buka Lambda > Functions > your-function).

  8. Pilih tab Configuration

  9. Di panel General configuration, klik Edit.

  10. 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. Di Konsol AWS, buka IAM > Users, lalu klik Add users.
  2. Berikan detail konfigurasi berikut:
    • Pengguna: Masukkan nama unik (misalnya, secops-reader)
    • Jenis akses: Pilih Kunci akses - Akses terprogram
    • Klik Buat pengguna.
  3. Lampirkan kebijakan baca minimal (kustom): Pengguna > pilih secops-reader > Izin > Tambahkan izin > Lampirkan kebijakan secara langsung > Buat kebijakan
  4. 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>"
        }
      ]
    }
    
  5. Tetapkan nama ke secops-reader-policy.

  6. Buka Buat kebijakan > telusuri/pilih > Berikutnya > Tambahkan izin.

  7. Buka Kredensial keamanan > Kunci akses > Buat kunci akses.

  8. Download CSV (nilai ini dimasukkan ke dalam feed).

Mengonfigurasi feed di Google SecOps untuk memproses 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 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.