Recolha registos de eventos do Bitwarden Enterprise

Suportado em:

Este guia explica como pode carregar registos de eventos do Bitwarden Enterprise para o Google Security Operations através do Amazon S3. O analisador transforma os registos de eventos formatados em JSON não processados num formato estruturado em conformidade com o UDM do Chronicle. Extrai campos relevantes, como detalhes do utilizador, endereços IP e tipos de eventos, mapeando-os para campos da UDM correspondentes para uma análise de segurança consistente.

Antes de começar

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

Obtenha a chave da API e o URL do Bitwarden

  1. Na consola do administrador do Bitwarden.
  2. Aceda a Definições > Informações da organização > Ver chave da API.
  3. Copie e guarde os seguintes detalhes numa localização segura:
    • ID de cliente
    • Segredo do cliente
  4. Determine os seus pontos finais 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 (UE: https://api.bitwarden.eu)

Configure o contentor do AWS S3 e o IAM para o Google SecOps

  1. Crie um contentor do Amazon S3 seguindo este manual do utilizador: Criar um contentor
  2. Guarde o Nome e a Região do contentor para referência futura (por exemplo, bitwarden-events).
  3. Crie um utilizador seguindo este guia do utilizador: criar um utilizador do IAM.
  4. Selecione o utilizador criado.
  5. Selecione o separador Credenciais de segurança.
  6. Clique em Criar chave de acesso na secção Chaves de acesso.
  7. Selecione Serviço de terceiros como Exemplo de utilização.
  8. Clicar em Seguinte.
  9. Opcional: adicione a etiqueta de descrição.
  10. Clique em Criar chave de acesso.
  11. Clique em Transferir ficheiro .csv para guardar a chave de acesso e a chave de acesso secreta para referência futura.
  12. Clique em Concluído.
  13. Selecione o separador Autorizações.
  14. Clique em Adicionar autorizações na secção Políticas de autorizações.
  15. Selecione Adicionar autorizações.
  16. Selecione Anexar políticas diretamente.
  17. Pesquise a política AmazonS3FullAccess.
  18. Selecione a política.
  19. Clicar em Seguinte.
  20. Clique em Adicionar autorizações.

Configure a política e a função de IAM para carregamentos do S3

  1. Aceda a AWS console > IAM > Policies > Create policy > separador JSON.
  2. Copie e cole a política abaixo.
  3. JSON da política (substitua bitwarden-events se tiver introduzido um nome de contentor 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 Seguinte > Criar política.
  2. Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.
  3. Anexe a política criada recentemente.
  4. Dê o nome WriteBitwardenToS3Role à função e clique em Criar função.

Crie a função Lambda

  1. Na consola da AWS, aceda a Lambda > Functions > Create function.
  2. Clique em Criar do zero.
  3. Faculte os seguintes detalhes de configuração:
Definição Valor
Nome bitwarden_events_to_s3
Runtime Python 3.13
Arquitetura x86_64
Função de execução WriteBitwardenToS3Role
  1. Depois de criar a função, abra o separador Código, elimine o fragmento de código 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. Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
  2. Introduza as variáveis de ambiente indicadas abaixo, substituindo-as 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 de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your‑function).
  2. Selecione o separador Configuração.
  3. No painel Configuração geral, clique em Editar.
  4. Altere Tempo limite para 5 minutos (300 segundos) e clique em Guardar.

Crie um horário do EventBridge

  1. Aceda a Amazon EventBridge > Scheduler > Create schedule.
  2. Forneça os seguintes detalhes de configuração:
    • Agenda recorrente: Taxa (1 hour).
    • Alvo: a sua função Lambda.
    • Nome: bitwarden-events-1h.
  3. Clique em Criar horário.

(Opcional) Crie um utilizador e chaves da IAM só de leitura para o Google SecOps

  1. Aceda a AWS Console > IAM > Users > Add users.
  2. Clique em Adicionar utilizadores.
  3. Forneça os seguintes detalhes de configuração:
    • Utilizador: introduza secops-reader.
    • Tipo de acesso: selecione Chave de acesso – Acesso programático.
  4. Clique em Criar utilizador.
  5. Anexe a política de leitura mínima (personalizada): Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy.
  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. Nome = secops-reader-policy.
  2. Clique em Criar política > procure/selecione > Seguinte > Adicionar autorizações.
  3. Crie uma chave de acesso para secops-reader: Credenciais de segurança > Chaves de acesso > Criar chave de acesso > transfira o .csv (vai colar estes valores no feed).

Configure um feed no Google SecOps para carregar os registos de eventos do Bitwarden Enterprise

  1. Aceda a Definições do SIEM > Feeds.
  2. Clique em + Adicionar novo feed.
  3. No campo Nome do feed, introduza 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 registo.
  6. Clicar em Seguinte.
  7. Especifique valores para os seguintes parâmetros de entrada:
    • URI do S3: s3://bitwarden-events/bitwarden/events/
    • Opções de eliminação de origens: selecione a opção de eliminação de acordo com a sua preferência.
    • Idade máxima do ficheiro: predefinição de 180 dias.
    • ID da chave de acesso: chave de acesso do utilizador com acesso ao contentor do S3.
    • Chave de acesso secreta: chave secreta do utilizador com acesso ao contentor do S3.
    • Espaço de nomes do recurso: o espaço de nomes do recurso.
    • Etiquetas de carregamento: a etiqueta aplicada aos eventos deste feed.
  8. Clicar em Seguinte.
  9. Reveja a nova configuração do feed no ecrã Finalizar e, de seguida, clique em Enviar.

Tabela de mapeamento da UDM

Campo de registo Mapeamento da UDM Lógica
actingUserId target.user.userid Se enriched.actingUser.userId estiver vazio ou nulo, este campo é 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 num formato 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 "Two Factor Enabled" 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 recolha" 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 recolha" 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 Endereço IP extraído do campo e mapeado para principal.ip.
N/A extensions.auth O analisador cria um objeto vazio.
N/A metadata.event_type Determinado com base no enriched.type e na presença de informações 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 da Google SecOps.