Raccogliere i log eventi di Bitwarden Enterprise

Supportato in:

Questo documento spiega come importare i log degli 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)

Ottieni la chiave API e l'URL di Bitwarden

  1. Nella Console di amministrazione 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: 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 un tag di 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 un utilizzo successivo.
  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 e seleziona il criterio AmazonS3FullAccess.
  18. Fai clic su Avanti.
  19. 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. Inserisci la seguente policy:

    {
      "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"
        }
      ]
    }
    
    
    • Sostituisci bitwarden-events se hai inserito un nome bucket diverso.
  3. Fai clic su Avanti > Crea policy.

  4. Vai a IAM > Ruoli > Crea ruolo > Servizio AWS > Lambda.

  5. Allega il criterio appena creato.

  6. 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 autore 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
  4. Dopo aver creato la funzione, apri la scheda Codice, elimina lo stub e inserisci il seguente codice (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. Vai a Configurazione > Variabili di ambiente > Modifica > Aggiungi nuova variabile di ambiente.

  6. Inserisci le seguenti variabili di ambiente, sostituendole con i tuoi valori:

    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
  7. Dopo aver creato la funzione, rimani sulla relativa pagina (o apri Lambda > Funzioni > la tua funzione).

  8. Seleziona la scheda Configurazione.

  9. Nel riquadro Configurazione generale, fai clic su Modifica.

  10. Modifica Timeout impostandolo su 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).
    • Destinazione: la tua funzione Lambda.
    • Nome: bitwarden-events-1h
  3. Fai clic su Crea pianificazione.

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

  1. Nella console AWS, vai a IAM > Utenti, poi fai clic su Aggiungi utenti.
  2. Fornisci i seguenti dettagli di configurazione:
    • Utente: inserisci un nome univoco (ad esempio secops-reader)
    • Tipo di accesso: seleziona Chiave di accesso - Accesso programmatico
    • Fai clic su Crea utente.
  3. Allega criterio per la lettura minimi (personalizzati): Utenti > seleziona secops-reader > Autorizzazioni > Aggiungi autorizzazioni > Allega criteri direttamente > Crea criteri
  4. Nell'editor JSON, inserisci la seguente policy:

    {
      "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. Imposta il nome su secops-reader-policy.

  6. Vai a Crea criterio > cerca/seleziona > Avanti > Aggiungi autorizzazioni.

  7. Vai a Credenziali di sicurezza > Chiavi di accesso > Crea chiave di accesso.

  8. Scarica il file CSV (questi valori vengono inseriti nel feed).

Configurare 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) in 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.