Coletar registros de eventos do Bitwarden Enterprise

Compatível com:

Este guia explica como ingerir registros de eventos do Bitwarden Enterprise no Google Security Operations usando o Amazon S3. O analisador transforma registros de eventos brutos formatados em JSON em um formato estruturado de acordo com o UDM do Chronicle. Ele extrai campos relevantes, como detalhes do usuário, endereços IP e tipos de eventos, mapeando-os para os campos correspondentes do UDM para uma análise de segurança consistente.

Antes de começar

  • Instância do Google SecOps.
  • Acesso privilegiado ao locatário do Bitwarden.
  • Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge).

Receber a chave de API e o URL do Bitwarden

  1. No Admin Console do Bitwarden.
  2. Acesse Configurações > Informações da organização > Ver chave de API.
  3. Copie e salve os seguintes detalhes em um local seguro:
    • ID do cliente
    • Client Secret
  4. Determine seus endpoints do Bitwarden (com base na região):
    • IDENTITY_URL = https://identity.bitwarden.com/connect/token (UE: https://identity.bitwarden.eu/connect/token)
    • API_BASE = https://api.bitwarden.com (EU: https://api.bitwarden.eu)

Configurar o bucket do AWS S3 e o IAM para o Google SecOps

  1. Crie um bucket do Amazon S3 seguindo este guia do usuário: Como criar um bucket
  2. Salve o Nome e a Região do bucket para referência futura (por exemplo, bitwarden-events).
  3. Crie um usuário seguindo este guia: Como criar um usuário do IAM.
  4. Selecione o usuário criado.
  5. Selecione a guia Credenciais de segurança.
  6. Clique em Criar chave de acesso na seção Chaves de acesso.
  7. Selecione Serviço de terceiros como Caso de uso.
  8. Clique em Próxima.
  9. Opcional: adicione uma tag de descrição.
  10. Clique em Criar chave de acesso.
  11. Clique em Baixar arquivo .csv para salvar a chave de acesso e a chave de acesso secreta para referência futura.
  12. Clique em Concluído.
  13. Selecione a guia Permissões.
  14. Clique em Adicionar permissões na seção Políticas de permissões.
  15. Selecione Adicionar permissões.
  16. Selecione Anexar políticas diretamente.
  17. Pesquise a política AmazonS3FullAccess.
  18. Selecione a política.
  19. Clique em Próxima.
  20. Clique em Adicionar permissões

Configurar a política e o papel do IAM para uploads do S3

  1. Acesse o console da AWS > IAM > Políticas > Criar política > guia JSON.
  2. Copie e cole a política abaixo.
  3. JSON da política (substitua bitwarden-events se você inseriu um nome de bucket diferente):
{
  "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. Clique em Próxima > Criar política.
  2. Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.
  3. Anexe a política recém-criada.
  4. Nomeie a função como WriteBitwardenToS3Role e clique em Criar função.

Criar a função Lambda

  1. No console da AWS, acesse Lambda > Functions > Create function.
  2. Clique em Criar do zero.
  3. Informe os seguintes detalhes de configuração:
Configuração Valor
Nome bitwarden_events_to_s3
Ambiente de execução Python 3.13
Arquitetura x86_64
Função de execução WriteBitwardenToS3Role
  1. Depois que a função for criada, abra a guia Código, exclua o stub e cole o código abaixo (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. Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
  2. Insira as variáveis de ambiente fornecidas abaixo, substituindo pelos seus valores.

Variáveis de ambiente

Chave Exemplo
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. Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua‑função.
  2. Selecione a guia Configuração.
  3. No painel Configuração geral, clique em Editar.
  4. Mude Tempo limite para 5 minutos (300 segundos) e clique em Salvar.

Criar uma programação do EventBridge

  1. Acesse Amazon EventBridge > Scheduler > Criar programação.
  2. Informe os seguintes detalhes de configuração:
    • Programação recorrente: Taxa (1 hour).
    • Destino: sua função Lambda.
    • Nome: bitwarden-events-1h.
  3. Clique em Criar programação.

(Opcional) Criar um usuário e chaves do IAM somente leitura para o Google SecOps

  1. Acesse Console da AWS > IAM > Usuários > Adicionar usuários.
  2. Clique em Add users.
  3. Informe os seguintes detalhes de configuração:
    • Usuário: insira secops-reader.
    • Tipo de acesso: selecione Chave de acesso — Acesso programático.
  4. Clique em Criar usuário.
  5. Anexe a política de leitura mínima (personalizada): Usuários > secops-reader > Permissões > Adicionar permissões > Anexar políticas diretamente > Criar política.
  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. Clique em Criar política > pesquisar/selecionar > Próxima > Adicionar permissões.
  3. Crie uma chave de acesso para secops-reader: Credenciais de segurança > Chaves de acesso > Criar chave de acesso > faça o download do .csv (cole esses valores no feed).

Configurar um feed no Google SecOps para ingerir os registros de eventos do Bitwarden Enterprise

  1. Acesse Configurações do SIEM > Feeds.
  2. Clique em + Adicionar novo feed.
  3. No campo Nome do feed, insira um nome para o feed (por exemplo, Bitwarden Events).
  4. Selecione Amazon S3 V2 como o Tipo de origem.
  5. Selecione Eventos do Bitwarden como o Tipo de registro.
  6. Clique em Próxima.
  7. Especifique valores para os seguintes parâmetros de entrada:
    • URI do S3: s3://bitwarden-events/bitwarden/events/
    • Opções de exclusão da fonte: selecione a opção de exclusão de acordo com sua preferência.
    • Idade máxima do arquivo: padrão de 180 dias.
    • ID da chave de acesso: chave de acesso do usuário com acesso ao bucket do S3.
    • Chave de acesso secreta: chave secreta do usuário com acesso ao bucket do S3.
    • Namespace do recurso: o namespace do recurso.
    • Rótulos de ingestão: o rótulo aplicado aos eventos deste feed.
  8. Clique em Próxima.
  9. Revise a nova configuração do feed na tela Finalizar e clique em Enviar.

Tabela de mapeamento do UDM

Campo de registro Mapeamento do UDM Lógica
actingUserId target.user.userid Se enriched.actingUser.userId estiver vazio ou nulo, esse campo será usado para preencher o campo target.user.userid.
collectionID security_result.detection_fields.key Preenche o campo key em detection_fields em security_result.
collectionID security_result.detection_fields.value Preenche o campo value em detection_fields em security_result.
data metadata.event_timestamp Analisado e convertido para um formato de carimbo de data/hora e mapeado para event_timestamp.
enriched.actingUser.accessAll security_result.rule_labels.key Define o valor como "Access_All" em rule_labels em security_result.
enriched.actingUser.accessAll security_result.rule_labels.value Preenche o campo value em rule_labels em security_result com o valor de enriched.actingUser.accessAll convertido em string.
enriched.actingUser.email target.user.email_addresses Preenche o campo email_addresses em target.user.
enriched.actingUser.id metadata.product_log_id Preenche o campo product_log_id em metadata.
enriched.actingUser.id target.labels.key Define o valor como "ID" em target.labels.
enriched.actingUser.id target.labels.value Preenche o campo value em target.labels com o valor de enriched.actingUser.id.
enriched.actingUser.name target.user.user_display_name Preenche o campo user_display_name em target.user.
enriched.actingUser.object target.labels.key Define o valor como "Object" em target.labels.
enriched.actingUser.object target.labels.value Preenche o campo value em target.labels com o valor de enriched.actingUser.object.
enriched.actingUser.resetPasswordEnrolled target.labels.key Define o valor como "ResetPasswordEnrolled" em target.labels.
enriched.actingUser.resetPasswordEnrolled target.labels.value Preenche o campo value em target.labels com o valor de enriched.actingUser.resetPasswordEnrolled convertido em string.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.key Define o valor como "Autenticação de dois fatores ativada" em rule_labels em security_result.
enriched.actingUser.twoFactorEnabled security_result.rule_labels.value Preenche o campo value em rule_labels em security_result com o valor de enriched.actingUser.twoFactorEnabled convertido em string.
enriched.actingUser.userId target.user.userid Preenche o campo userid em target.user.
enriched.collection.id additional.fields.key Define o valor como "ID da coleção" em additional.fields.
enriched.collection.id additional.fields.value.string_value Preenche o campo string_value em additional.fields com o valor de enriched.collection.id.
enriched.collection.object additional.fields.key Define o valor como "Objeto de coleta" em additional.fields.
enriched.collection.object additional.fields.value.string_value Preenche o campo string_value em additional.fields com o valor de enriched.collection.object.
enriched.type metadata.product_event_type Preenche o campo product_event_type em metadata.
groupId target.user.group_identifiers Adiciona o valor à matriz group_identifiers em target.user.
ipAddress principal.ip Extraímos o endereço IP do campo e o mapeamos para principal.ip.
N/A extensions.auth Um objeto vazio é criado pelo analisador.
N/A metadata.event_type Determinado com base no enriched.type e na presença de informações de principal e target. Valores possíveis: USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT.
N/A security_result.action Determinado com base no enriched.type. Valores possíveis: ALLOW, BLOCK.
objeto additional.fields.key Define o valor como "Object" em additional.fields.
objeto additional.fields.value Preenche o campo value em additional.fields com o valor de object.

Precisa de mais ajuda? Receba respostas de membros da comunidade e profissionais do Google SecOps.