Raccogliere i log eventi di Bitwarden Enterprise

Supportato in:

Questa guida spiega come importare i log eventi di Bitwarden Enterprise in Google Security Operations utilizzando Amazon S3. Il parser trasforma i log eventi non elaborati in formato JSON in un formato strutturato conforme a Chronicle UDM. Estrae i campi pertinenti, come i dettagli utente, gli indirizzi IP e i tipi di eventi, mappandoli ai campi UDM corrispondenti per un'analisi della sicurezza coerente.

Prima di iniziare

  • Istanza Google SecOps.
  • Accesso con privilegi al tenant Bitwarden.
  • Accesso privilegiato ad AWS (S3, IAM, Lambda, EventBridge).

Ottenere la chiave API e l'URL di Bitwarden

  1. Nella console di amministrazione di Bitwarden.
  2. Vai a Impostazioni > Informazioni sull'organizzazione > Visualizza chiave API.
  3. Copia e salva i seguenti dettagli in una posizione sicura:
    • ID client
    • Client secret
  4. Determina gli endpoint Bitwarden (in base alla regione):
    • IDENTITY_URL = https://identity.bitwarden.com/connect/token (UE: https://identity.bitwarden.eu/connect/token)
    • API_BASE = https://api.bitwarden.com (UE: https://api.bitwarden.eu)

Configura il bucket AWS S3 e IAM per Google SecOps

  1. Crea un bucket Amazon S3 seguendo questa guida utente: Creazione di un bucket
  2. Salva il nome e la regione del bucket per riferimento futuro (ad esempio, bitwarden-events).
  3. Crea un utente seguendo questa guida utente: Creazione di un utente IAM.
  4. Seleziona l'utente creato.
  5. Seleziona la scheda Credenziali di sicurezza.
  6. Fai clic su Crea chiave di accesso nella sezione Chiavi di accesso.
  7. Seleziona Servizio di terze parti come Caso d'uso.
  8. Fai clic su Avanti.
  9. (Facoltativo) Aggiungi il tag della descrizione.
  10. Fai clic su Crea chiave di accesso.
  11. Fai clic su Scarica file .csv per salvare la chiave di accesso e la chiave di accesso segreta per riferimento futuro.
  12. Fai clic su Fine.
  13. Seleziona la scheda Autorizzazioni.
  14. Fai clic su Aggiungi autorizzazioni nella sezione Criteri per le autorizzazioni.
  15. Seleziona Aggiungi autorizzazioni.
  16. Seleziona Allega direttamente i criteri.
  17. Cerca il criterio AmazonS3FullAccess.
  18. Seleziona la policy.
  19. Fai clic su Avanti.
  20. Fai clic su Aggiungi autorizzazioni.

Configura il ruolo e il criterio IAM per i caricamenti S3

  1. Vai alla console AWS > IAM > Policy > Crea policy > scheda JSON.
  2. Copia e incolla il criterio riportato di seguito.
  3. JSON della policy (sostituisci bitwarden-events se hai inserito un nome del bucket diverso):
{
  "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. Fai clic su Avanti > Crea policy.
  2. Vai a IAM > Ruoli > Crea ruolo > Servizio AWS > Lambda.
  3. Allega la policy appena creata.
  4. Assegna al ruolo il nome WriteBitwardenToS3Role e fai clic su Crea ruolo.

Crea la funzione Lambda

  1. Nella console AWS, vai a Lambda > Funzioni > Crea funzione.
  2. Fai clic su Crea da zero.
  3. Fornisci i seguenti dettagli di configurazione:
Impostazione Valore
Nome bitwarden_events_to_s3
Tempo di esecuzione Python 3.13
Architettura x86_64
Ruolo di esecuzione WriteBitwardenToS3Role
  1. Dopo aver creato la funzione, apri la scheda Codice, elimina lo stub e incolla il codice riportato di seguito (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. Vai a Configurazione > Variabili di ambiente > Modifica > Aggiungi nuova variabile di ambiente.
  2. Inserisci le variabili di ambiente fornite di seguito, sostituendole con i tuoi valori.

Variabili di ambiente

Chiave Esempio
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 (UE: https://identity.bitwarden.eu/connect/token)
API_BASE https://api.bitwarden.com (UE: https://api.bitwarden.eu)
MAX_PAGES 10
  1. Dopo aver creato la funzione, rimani sulla relativa pagina (o apri Lambda > Funzioni > la tua funzione).
  2. Seleziona la scheda Configurazione.
  3. Nel riquadro Configurazione generale, fai clic su Modifica.
  4. Modifica Timeout impostando 5 minuti (300 secondi) e fai clic su Salva.

Creare una pianificazione EventBridge

  1. Vai a Amazon EventBridge > Scheduler > Crea pianificazione.
  2. Fornisci i seguenti dettagli di configurazione:
    • Programma ricorrente: Tariffa (1 hour).
    • Target: la tua funzione Lambda.
    • Nome: bitwarden-events-1h
  3. Fai clic su Crea pianificazione.

(Facoltativo) Crea chiavi e utenti IAM di sola lettura per Google SecOps

  1. Vai alla console AWS > IAM > Users (Utenti) > Add users (Aggiungi utenti).
  2. Fai clic su Add users (Aggiungi utenti).
  3. Fornisci i seguenti dettagli di configurazione:
    • Utente: inserisci secops-reader.
    • Tipo di accesso: seleziona Chiave di accesso - Accesso programmatico.
  4. Fai clic su Crea utente.
  5. Collega la criterio per la lettura minima (personalizzata): Utenti > secops-reader > Autorizzazioni > Aggiungi autorizzazioni > Collega le norme direttamente > Crea norma.
  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. Fai clic su Crea criterio > cerca/seleziona > Avanti > Aggiungi autorizzazioni.
  3. Crea la chiave di accesso per secops-reader: Credenziali di sicurezza > Chiavi di accesso > Crea chiave di accesso > scarica .csv (incollerai questi valori nel feed).

Configura un feed in Google SecOps per importare i log eventi di Bitwarden Enterprise

  1. Vai a Impostazioni SIEM > Feed.
  2. Fai clic su + Aggiungi nuovo feed.
  3. Nel campo Nome feed, inserisci un nome per il feed (ad esempio, Bitwarden Events).
  4. Seleziona Amazon S3 V2 come Tipo di origine.
  5. Seleziona Eventi Bitwarden come Tipo di log.
  6. Fai clic su Avanti.
  7. Specifica i valori per i seguenti parametri di input:
    • URI S3: s3://bitwarden-events/bitwarden/events/
    • Opzioni di eliminazione dell'origine: seleziona l'opzione di eliminazione in base alle tue preferenze.
    • Durata massima del file: 180 giorni per impostazione predefinita.
    • ID chiave di accesso: chiave di accesso utente con accesso al bucket S3.
    • Chiave di accesso segreta: chiave segreta dell'utente con accesso al bucket S3.
    • Spazio dei nomi dell'asset: lo spazio dei nomi dell'asset.
    • Etichette di importazione: l'etichetta applicata agli eventi di questo feed.
  8. Fai clic su Avanti.
  9. Controlla la nuova configurazione del feed nella schermata Finalizza e poi fai clic su Invia.

Tabella di mappatura UDM

Campo log Mappatura UDM Logic
actingUserId target.user.userid Se enriched.actingUser.userId è vuoto o nullo, questo campo viene utilizzato per compilare il campo target.user.userid.
collectionID security_result.detection_fields.key Compila il campo key all'interno di detection_fields in security_result.
collectionID security_result.detection_fields.value Compila il campo value all'interno di detection_fields in security_result.
data metadata.event_timestamp Analizzato e convertito in un formato timestamp e mappato a event_timestamp.
enriched.actingUser.accessAll security_result.rule_labels.key Imposta il valore su "Access_All" all'interno di rule_labels in security_result.
enriched.actingUser.accessAll security_result.rule_labels.value Compila il campo value all'interno di rule_labels in security_result con il valore di enriched.actingUser.accessAll convertito in stringa.
enriched.actingUser.email target.user.email_addresses Compila il campo email_addresses all'interno di target.user.
enriched.actingUser.id metadata.product_log_id Compila il campo product_log_id all'interno di metadata.
enriched.actingUser.id target.labels.key Imposta il valore su "ID" all'interno di target.labels.
enriched.actingUser.id target.labels.value Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.id.
enriched.actingUser.name target.user.user_display_name Compila il campo user_display_name all'interno di target.user.
enriched.actingUser.object target.labels.key Imposta il valore su "Object" all'interno di target.labels.
enriched.actingUser.object target.labels.value Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.object.
enriched.actingUser.resetPasswordEnrolled target.labels.key Imposta il valore su "ResetPasswordEnrolled" in target.labels.
enriched.actingUser.resetPasswordEnrolled target.labels.value Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.resetPasswordEnrolled convertito in stringa.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.key Imposta il valore su "Two Factor Enabled" (Autenticazione a due fattori attivata) all'interno di rule_labels in security_result.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.value Compila il campo value all'interno di rule_labels in security_result con il valore di enriched.actingUser.twoFactorEnabled convertito in stringa.
enriched.actingUser.userId target.user.userid Compila il campo userid all'interno di target.user.
enriched.collection.id additional.fields.key Imposta il valore su "ID raccolta" all'interno di additional.fields.
enriched.collection.id additional.fields.value.string_value Compila il campo string_value all'interno di additional.fields con il valore di enriched.collection.id.
enriched.collection.object additional.fields.key Imposta il valore su "Collection Object" all'interno di additional.fields.
enriched.collection.object additional.fields.value.string_value Compila il campo string_value all'interno di additional.fields con il valore di enriched.collection.object.
enriched.type metadata.product_event_type Compila il campo product_event_type all'interno di metadata.
groupId target.user.group_identifiers Aggiunge il valore all'array group_identifiers all'interno di target.user.
ipAddress principal.ip Indirizzo IP estratto dal campo e mappato a principal.ip.
N/D extensions.auth Il parser crea un oggetto vuoto.
N/D metadata.event_type Determinato in base a enriched.type e alla presenza di informazioni su principal e target. Valori possibili: USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT.
N/D security_result.action Determinato in base a enriched.type. Valori possibili: ALLOW, BLOCK.
oggetto additional.fields.key Imposta il valore su "Object" all'interno di additional.fields.
oggetto additional.fields.value Compila il campo value all'interno di additional.fields con il valore di object.

Hai bisogno di ulteriore assistenza? Ricevi risposte dai membri della community e dai professionisti di Google SecOps.