Collecter les journaux d'événements Bitwarden Enterprise

Compatible avec :

Ce guide explique comment ingérer les journaux d'événements Bitwarden Enterprise dans Google Security Operations à l'aide d'Amazon S3. L'analyseur transforme les journaux d'événements bruts au format JSON en un format structuré conforme à l'UDM Chronicle. Il extrait les champs pertinents tels que les informations sur les utilisateurs, les adresses IP et les types d'événements, et les mappe aux champs UDM correspondants pour une analyse de sécurité cohérente.

Avant de commencer

  • Instance Google SecOps.
  • Accès privilégié au locataire Bitwarden.
  • Accès privilégié à AWS (S3, IAM, Lambda, EventBridge).

Obtenir la clé API et l'URL Bitwarden

  1. Dans la console d'administration Bitwarden.
  2. Accédez à Paramètres> Informations sur l'organisation> Afficher la clé API.
  3. Copiez et enregistrez les informations suivantes dans un emplacement sécurisé :
    • ID client
    • Code secret du client
  4. Déterminez vos points de terminaison Bitwarden (en fonction de la région) :
    • 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)

Configurer un bucket AWS S3 et IAM pour Google SecOps

  1. Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
  2. Enregistrez le Nom et la Région du bucket pour référence ultérieure (par exemple, bitwarden-events).
  3. Créez un utilisateur en suivant ce guide de l'utilisateur : Créer un utilisateur IAM.
  4. Sélectionnez l'utilisateur créé.
  5. Sélectionnez l'onglet Informations d'identification de sécurité.
  6. Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
  7. Sélectionnez Service tiers comme Cas d'utilisation.
  8. Cliquez sur Suivant.
  9. Facultatif : Ajoutez une balise de description.
  10. Cliquez sur Créer une clé d'accès.
  11. Cliquez sur Download .csv file (Télécharger le fichier .csv) pour enregistrer la clé d'accès et la clé d'accès secrète pour référence ultérieure.
  12. Cliquez sur OK.
  13. Sélectionnez l'onglet Autorisations.
  14. Cliquez sur Ajouter des autorisations dans la section Règles relatives aux autorisations.
  15. Sélectionnez Ajouter des autorisations.
  16. Sélectionnez Joindre directement des règles.
  17. Recherchez la règle AmazonS3FullAccess.
  18. Sélectionnez la règle.
  19. Cliquez sur Suivant.
  20. Cliquez sur Ajouter des autorisations.

Configurer la stratégie et le rôle IAM pour les importations S3

  1. Accédez à la console AWS> IAM> Policies> Create policy> onglet JSON.
  2. Copiez et collez le règlement ci-dessous.
  3. JSON de la règle (remplacez bitwarden-events si vous avez saisi un autre nom de bucket) :
{
  "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. Cliquez sur Suivant > Créer une règle.
  2. Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.
  3. Associez la règle que vous venez de créer.
  4. Nommez le rôle WriteBitwardenToS3Role, puis cliquez sur Créer un rôle.

Créer la fonction Lambda

  1. Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
  2. Cliquez sur Créer à partir de zéro.
  3. Fournissez les informations de configuration suivantes :
Paramètre Valeur
Nom bitwarden_events_to_s3
Durée d'exécution Python 3.13
Architecture x86_64
Rôle d'exécution WriteBitwardenToS3Role
  1. Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et collez le code ci-dessous (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. Accédez à Configuration> Variables d'environnement> Modifier> Ajouter une variable d'environnement.
  2. Saisissez les variables d'environnement fournies ci-dessous, en les remplaçant par vos valeurs.

Variables d'environnement

Clé Exemple
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. Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre‑fonction).
  2. Accédez à l'onglet Configuration.
  3. Dans le panneau Configuration générale, cliquez sur Modifier.
  4. Définissez Délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.

Créer une programmation EventBridge

  1. Accédez à Amazon EventBridge> Scheduler> Create schedule.
  2. Fournissez les informations de configuration suivantes :
    • Planning récurrent : Tarif (1 hour).
    • Cible : votre fonction Lambda.
    • Nom : bitwarden-events-1h.
  3. Cliquez sur Créer la programmation.

(Facultatif) Créez un utilisateur et des clés IAM en lecture seule pour Google SecOps

  1. Accédez à la console AWS> IAM> Utilisateurs> Ajouter des utilisateurs.
  2. Cliquez sur Add users (Ajouter des utilisateurs).
  3. Fournissez les informations de configuration suivantes :
    • Utilisateur : saisissez secops-reader.
    • Type d'accès : sélectionnez Clé d'accès – Accès programmatique.
  4. Cliquez sur Créer un utilisateur.
  5. Associez une stratégie de lecture minimale (personnalisée) : Utilisateurs > secops-reader > Autorisations > Ajouter des autorisations > Associer des stratégies directement > Créer une stratégie.
  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. Nom = secops-reader-policy.
  2. Cliquez sur Créer une règle> recherchez/sélectionnez > Suivant> Ajouter des autorisations.
  3. Créez une clé d'accès pour secops-reader : Identifiants de sécurité> Clés d'accès> Créer une clé d'accès> téléchargez le fichier .csv (vous collerez ces valeurs dans le flux).

Configurer un flux dans Google SecOps pour ingérer les journaux d'événements Bitwarden Enterprise

  1. Accédez à Paramètres SIEM> Flux.
  2. Cliquez sur + Ajouter un flux.
  3. Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple, Bitwarden Events).
  4. Sélectionnez Amazon S3 V2 comme type de source.
  5. Sélectionnez Événements Bitwarden comme Type de journal.
  6. Cliquez sur Suivant.
  7. Spécifiez les valeurs des paramètres d'entrée suivants :
    • URI S3 : s3://bitwarden-events/bitwarden/events/
    • Options de suppression de la source : sélectionnez l'option de suppression de votre choix.
    • Âge maximal des fichiers : 180 jours par défaut.
    • ID de clé d'accès : clé d'accès utilisateur ayant accès au bucket S3.
    • Clé d'accès secrète : clé secrète de l'utilisateur ayant accès au bucket S3.
    • Espace de noms de l'élément : espace de noms de l'élément.
    • Libellés d'ingestion : libellé appliqué aux événements de ce flux.
  8. Cliquez sur Suivant.
  9. Vérifiez la configuration de votre nouveau flux sur l'écran Finaliser, puis cliquez sur Envoyer.

Table de mappage UDM

Champ de journal Mappage UDM Logique
actingUserId target.user.userid Si enriched.actingUser.userId est vide ou nul, ce champ est utilisé pour renseigner le champ target.user.userid.
collectionID security_result.detection_fields.key Renseigne le champ key dans detection_fields dans security_result.
collectionID security_result.detection_fields.value Renseigne le champ value dans detection_fields dans security_result.
date metadata.event_timestamp Analysé et converti au format d'horodatage, puis mappé à event_timestamp.
enriched.actingUser.accessAll security_result.rule_labels.key Définit la valeur sur "Access_All" dans rule_labels de security_result.
enriched.actingUser.accessAll security_result.rule_labels.value Renseigne le champ value dans rule_labels de security_result avec la valeur de enriched.actingUser.accessAll convertie en chaîne.
enriched.actingUser.email target.user.email_addresses Renseigne le champ email_addresses dans target.user.
enriched.actingUser.id metadata.product_log_id Renseigne le champ product_log_id dans metadata.
enriched.actingUser.id target.labels.key Définit la valeur sur "ID" dans target.labels.
enriched.actingUser.id target.labels.value Renseigne le champ value dans target.labels avec la valeur de enriched.actingUser.id.
enriched.actingUser.name target.user.user_display_name Renseigne le champ user_display_name dans target.user.
enriched.actingUser.object target.labels.key Définit la valeur sur "Object" dans target.labels.
enriched.actingUser.object target.labels.value Renseigne le champ value dans target.labels avec la valeur de enriched.actingUser.object.
enriched.actingUser.resetPasswordEnrolled target.labels.key Définit la valeur sur "ResetPasswordEnrolled" dans target.labels.
enriched.actingUser.resetPasswordEnrolled target.labels.value Renseigne le champ value dans target.labels avec la valeur de enriched.actingUser.resetPasswordEnrolled convertie en chaîne.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.key Définit la valeur sur "Two Factor Enabled" (Authentification à deux facteurs activée) dans rule_labels de security_result.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.value Renseigne le champ value dans rule_labels de security_result avec la valeur de enriched.actingUser.twoFactorEnabled convertie en chaîne.
enriched.actingUser.userId target.user.userid Renseigne le champ userid dans target.user.
enriched.collection.id additional.fields.key Définit la valeur sur "ID de collection" dans additional.fields.
enriched.collection.id additional.fields.value.string_value Renseigne le champ string_value dans additional.fields avec la valeur de enriched.collection.id.
enriched.collection.object additional.fields.key Définit la valeur sur "Collection Object" dans additional.fields.
enriched.collection.object additional.fields.value.string_value Renseigne le champ string_value dans additional.fields avec la valeur de enriched.collection.object.
enriched.type metadata.product_event_type Renseigne le champ product_event_type dans metadata.
groupId target.user.group_identifiers Ajoute la valeur au tableau group_identifiers dans target.user.
ipAddress principal.ip Adresse IP extraite du champ et mappée à principal.ip.
N/A extensions.auth Un objet vide est créé par l'analyseur.
N/A metadata.event_type Déterminé en fonction de enriched.type et de la présence des informations principal et target. Valeurs possibles : USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT.
N/A security_result.action Déterminé en fonction de enriched.type. Valeurs possibles : ALLOW, BLOCK.
objet additional.fields.key Définit la valeur sur "Object" dans additional.fields.
objet additional.fields.value Renseigne le champ value dans additional.fields avec la valeur de object.

Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.