Bitwarden Enterprise-Ereignisprotokolle erfassen

Unterstützt in:

In diesem Leitfaden wird beschrieben, wie Sie Bitwarden Enterprise-Ereignislogs mit Amazon S3 in Google Security Operations aufnehmen. Der Parser wandelt Rohereignisprotokolle im JSON-Format in ein strukturiertes Format um, das dem Chronicle UDM entspricht. Es werden relevante Felder wie Nutzerdetails, IP-Adressen und Ereignistypen extrahiert und den entsprechenden UDM-Feldern zugeordnet, um eine konsistente Sicherheitsanalyse zu ermöglichen.

Hinweise

  • Google SecOps-Instanz.
  • Privilegierter Zugriff auf den Bitwarden-Mandanten.
  • Privilegierter Zugriff auf AWS (S3, IAM, Lambda, EventBridge).

Bitwarden-API-Schlüssel und ‑URL abrufen

  1. In der Bitwarden-Admin-Konsole.
  2. Rufen Sie die Einstellungen> Organisationsinformationen> API-Schlüssel ansehen auf.
  3. Kopieren Sie die folgenden Details und speichern Sie sie an einem sicheren Ort:
    • Client-ID
    • Client-Secret
  4. Ermitteln Sie Ihre Bitwarden-Endpunkte (basierend auf der Region):
    • IDENTITY_URL = https://identity.bitwarden.com/connect/token (EU: https://identity.bitwarden.eu/connect/token)
    • API_BASE = https://api.bitwarden.com (EU: https://api.bitwarden.eu)

AWS S3-Bucket und IAM für Google SecOps konfigurieren

  1. Erstellen Sie einen Amazon S3-Bucket. Folgen Sie dazu der Anleitung unter Bucket erstellen.
  2. Speichern Sie den Namen und die Region des Buckets zur späteren Verwendung (z. B. bitwarden-events).
  3. Erstellen Sie einen Nutzer gemäß dieser Anleitung: IAM-Nutzer erstellen.
  4. Wählen Sie den erstellten Nutzer aus.
  5. Wählen Sie den Tab Sicherheitsanmeldedaten aus.
  6. Klicken Sie im Abschnitt Zugriffsschlüssel auf Zugriffsschlüssel erstellen.
  7. Wählen Sie Drittanbieterdienst als Anwendungsfall aus.
  8. Klicken Sie auf Weiter.
  9. Optional: Fügen Sie ein Beschreibungstag hinzu.
  10. Klicken Sie auf Zugriffsschlüssel erstellen.
  11. Klicken Sie auf CSV-Datei herunterladen, um den Access Key (Zugriffsschlüssel) und den Secret Access Key (geheimer Zugriffsschlüssel) für die zukünftige Verwendung zu speichern.
  12. Klicken Sie auf Fertig.
  13. Wählen Sie den Tab Berechtigungen aus.
  14. Klicken Sie im Bereich Berechtigungsrichtlinien auf Berechtigungen hinzufügen.
  15. Wählen Sie Berechtigungen hinzufügen aus.
  16. Wählen Sie Richtlinien direkt anhängen aus.
  17. Suchen Sie nach der Richtlinie AmazonS3FullAccess.
  18. Wählen Sie die Richtlinie aus.
  19. Klicken Sie auf Weiter.
  20. Klicken Sie auf Berechtigungen hinzufügen.

IAM-Richtlinie und ‑Rolle für S3-Uploads konfigurieren

  1. Rufen Sie die AWS-Konsole > IAM > Richtlinien > Richtlinie erstellen > JSON-Tab auf.
  2. Kopieren Sie die Richtlinie unten und fügen Sie sie ein.
  3. Richtlinien-JSON (ersetzen Sie bitwarden-events, wenn Sie einen anderen Bucket-Namen eingegeben haben):
{
  "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. Klicken Sie auf Weiter > Richtlinie erstellen.
  2. Gehen Sie zu IAM > Rollen > Rolle erstellen > AWS-Service > Lambda.
  3. Hängen Sie die neu erstellte Richtlinie an.
  4. Geben Sie der Rolle den Namen WriteBitwardenToS3Role und klicken Sie auf Rolle erstellen.

Lambda-Funktion erstellen

  1. Rufen Sie in der AWS Console Lambda > Funktionen > Funktion erstellen auf.
  2. Klicken Sie auf Von Grund auf erstellen.
  3. Geben Sie die folgenden Konfigurationsdetails an:
Einstellung Wert
Name bitwarden_events_to_s3
Laufzeit Python 3.13
Architektur x86_64
Ausführungsrolle WriteBitwardenToS3Role
  1. Nachdem die Funktion erstellt wurde, öffnen Sie den Tab Code, löschen Sie den Stub und fügen Sie den Code unten (bitwarden_events_to_s3.py) ein.
#!/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. Klicken Sie auf Konfiguration> Umgebungsvariablen> Bearbeiten> Neue Umgebungsvariable hinzufügen.
  2. Geben Sie die unten angegebenen Umgebungsvariablen ein und ersetzen Sie die Platzhalter durch Ihre Werte.

Umgebungsvariablen

Schlüssel Beispiel
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 (EU: https://identity.bitwarden.eu/connect/token)
API_BASE https://api.bitwarden.com (EU: https://api.bitwarden.eu)
MAX_PAGES 10
  1. Bleiben Sie nach dem Erstellen der Funktion auf der entsprechenden Seite oder öffnen Sie Lambda > Funktionen > Ihre‑Funktion.
  2. Wählen Sie den Tab Konfiguration aus.
  3. Klicken Sie im Bereich Allgemeine Konfiguration auf Bearbeiten.
  4. Ändern Sie Zeitlimit in 5 Minuten (300 Sekunden) und klicken Sie auf Speichern.

EventBridge-Zeitplan erstellen

  1. Gehen Sie zu Amazon EventBridge > Scheduler > Create schedule (Amazon EventBridge > Scheduler > Zeitplan erstellen).
  2. Geben Sie die folgenden Konfigurationsdetails an:
    • Wiederkehrender Zeitplan: Preis (1 hour).
    • Ziel: Ihre Lambda-Funktion.
    • Name: bitwarden-events-1h.
  3. Klicken Sie auf Zeitplan erstellen.

(Optional) IAM-Nutzer mit Lesezugriff und Schlüssel für Google SecOps erstellen

  1. Rufen Sie die AWS-Konsole > IAM > Nutzer > Nutzer hinzufügen auf.
  2. Klicken Sie auf Nutzer hinzufügen.
  3. Geben Sie die folgenden Konfigurationsdetails an:
    • Nutzer: Geben Sie secops-reader ein.
    • Zugriffstyp: Wählen Sie Zugriffsschlüssel – programmatischer Zugriff aus.
  4. Klicken Sie auf Nutzer erstellen.
  5. Minimale Leseberechtigung (benutzerdefiniert) anhängen: Nutzer > secops-reader > Berechtigungen > Berechtigungen hinzufügen > Richtlinien direkt anhängen > Richtlinie erstellen.
  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. Name = secops-reader-policy.
  2. Klicken Sie auf Richtlinie erstellen> suchen/auswählen> Weiter> Berechtigungen hinzufügen.
  3. Erstellen Sie einen Zugriffsschlüssel für secops-reader: Sicherheitsanmeldedaten > Zugriffsschlüssel > Zugriffsschlüssel erstellen > .csv herunterladen (diese Werte fügen Sie in den Feed ein).

Feed in Google SecOps konfigurieren, um die Bitwarden Enterprise-Ereignisprotokolle aufzunehmen

  1. Rufen Sie die SIEM-Einstellungen > Feeds auf.
  2. Klicken Sie auf + Neuen Feed hinzufügen.
  3. Geben Sie im Feld Feedname einen Namen für den Feed ein, z. B. Bitwarden Events.
  4. Wählen Sie Amazon S3 V2 als Quelltyp aus.
  5. Wählen Sie Bitwarden-Ereignisse als Protokolltyp aus.
  6. Klicken Sie auf Weiter.
  7. Geben Sie Werte für die folgenden Eingabeparameter an:
    • S3-URI: s3://bitwarden-events/bitwarden/events/
    • Optionen zum Löschen der Quelle: Wählen Sie die gewünschte Option zum Löschen aus.
    • Maximales Dateialter: Standardmäßig 180 Tage.
    • Zugriffsschlüssel-ID: Nutzerzugriffsschlüssel mit Zugriff auf den S3-Bucket.
    • Geheimer Zugriffsschlüssel: Der geheime Schlüssel des Nutzers mit Zugriff auf den S3-Bucket.
    • Asset-Namespace: Der Asset-Namespace.
    • Aufnahmelabels: Das Label, das auf die Ereignisse aus diesem Feed angewendet wird.
  8. Klicken Sie auf Weiter.
  9. Prüfen Sie die neue Feedkonfiguration auf dem Bildschirm Abschließen und klicken Sie dann auf Senden.

UDM-Zuordnungstabelle

Logfeld UDM-Zuordnung Logik
actingUserId target.user.userid Wenn enriched.actingUser.userId leer oder null ist, wird dieses Feld verwendet, um das Feld target.user.userid auszufüllen.
collectionID security_result.detection_fields.key Füllt das Feld key in detection_fields in security_result aus.
collectionID security_result.detection_fields.value Füllt das Feld value in detection_fields in security_result aus.
Datum metadata.event_timestamp Geparsed und in ein Zeitstempelformat konvertiert und event_timestamp zugeordnet.
enriched.actingUser.accessAll security_result.rule_labels.key Legt den Wert auf „Access_All“ in rule_labels in security_result fest.
enriched.actingUser.accessAll security_result.rule_labels.value Füllt das Feld value in rule_labels in security_result mit dem Wert aus enriched.actingUser.accessAll aus, der in einen String konvertiert wurde.
enriched.actingUser.email target.user.email_addresses Füllt das Feld email_addresses in target.user aus.
enriched.actingUser.id metadata.product_log_id Füllt das Feld product_log_id in metadata aus.
enriched.actingUser.id target.labels.key Legt den Wert auf „ID“ in target.labels fest.
enriched.actingUser.id target.labels.value Füllt das Feld value in target.labels mit dem Wert aus enriched.actingUser.id aus.
enriched.actingUser.name target.user.user_display_name Füllt das Feld user_display_name in target.user aus.
enriched.actingUser.object target.labels.key Legt den Wert in target.labels auf „Object“ fest.
enriched.actingUser.object target.labels.value Füllt das Feld value in target.labels mit dem Wert aus enriched.actingUser.object aus.
enriched.actingUser.resetPasswordEnrolled target.labels.key Legt den Wert in target.labels auf „ResetPasswordEnrolled“ fest.
enriched.actingUser.resetPasswordEnrolled target.labels.value Füllt das Feld value in target.labels mit dem Wert aus enriched.actingUser.resetPasswordEnrolled aus, der in einen String konvertiert wurde.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.key Legt den Wert in rule_labels in security_result auf „Two Factor Enabled“ (Zwei-Faktor-Authentifizierung aktiviert) fest.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.value Füllt das Feld value in rule_labels in security_result mit dem Wert aus enriched.actingUser.twoFactorEnabled aus, der in einen String konvertiert wurde.
enriched.actingUser.userId target.user.userid Füllt das Feld userid in target.user aus.
enriched.collection.id additional.fields.key Legt den Wert auf „Sammlungs-ID“ in additional.fields fest.
enriched.collection.id additional.fields.value.string_value Füllt das Feld string_value in additional.fields mit dem Wert aus enriched.collection.id aus.
enriched.collection.object additional.fields.key Legt den Wert für additional.fields auf „Collection Object“ fest.
enriched.collection.object additional.fields.value.string_value Füllt das Feld string_value in additional.fields mit dem Wert aus enriched.collection.object aus.
enriched.type metadata.product_event_type Füllt das Feld product_event_type in metadata aus.
groupId target.user.group_identifiers Fügt den Wert dem group_identifiers-Array in target.user hinzu.
ipAddress principal.ip Die IP-Adresse wurde aus dem Feld extrahiert und principal.ip zugeordnet.
extensions.auth Ein leeres Objekt wird vom Parser erstellt.
metadata.event_type Wird anhand von enriched.type und dem Vorhandensein von principal- und target-Informationen bestimmt. Mögliche Werte: USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT.
security_result.action Wird anhand der enriched.type ermittelt. Mögliche Werte: ALLOW, BLOCK.
Objekt additional.fields.key Legt den Wert in additional.fields auf „Object“ fest.
Objekt additional.fields.value Füllt das Feld value in additional.fields mit dem Wert aus object aus.

Benötigen Sie weitere Hilfe? Antworten von Community-Mitgliedern und Google SecOps-Experten erhalten