Coletar registros de eventos do Bitwarden Enterprise

Compatível com:

Neste documento, explicamos 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 da 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 (UE: 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 o 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 Fazer o download do arquivo CSV para salvar a chave de acesso e a chave de acesso secreta para uso posterior.
  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 e selecione a política AmazonS3FullAccess.
  18. Clique em Próxima.
  19. Clique em Adicionar permissões

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

  1. Acesse Console da AWS > IAM > Políticas > Criar política > guia JSON.
  2. Insira a seguinte política:

    {
      "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"
        }
      ]
    }
    
    
    • Substitua bitwarden-events se você tiver inserido um nome de bucket diferente.
  3. Clique em Próxima > Criar política.

  4. Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.

  5. Anexe a política recém-criada.

  6. 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
  4. Depois que a função for criada, abra a guia Código, exclua o stub e insira o seguinte código (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. Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.

  6. Insira as seguintes variáveis de ambiente, substituindo pelos seus valores:

    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
  7. Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua-função.

  8. Selecione a guia Configuração.

  9. No painel Configuração geral, clique em Editar.

  10. 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. No console da AWS, acesse IAM > Usuários e clique em Adicionar usuários.
  2. Informe os seguintes detalhes de configuração:
    • Usuário: insira um nome exclusivo (por exemplo, secops-reader)
    • Tipo de acesso: selecione Chave de acesso - Acesso programático.
    • Clique em Criar usuário.
  3. Anexe a política de leitura mínima (personalizada): Usuários > selecione secops-reader > Permissões > Adicionar permissões > Anexar políticas diretamente > Criar política
  4. No editor JSON, insira a seguinte política:

    {
      "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. Defina o nome como secops-reader-policy.

  6. Acesse Criar política > pesquise/selecione > Próxima > Adicionar permissões.

  7. Acesse Credenciais de segurança > Chaves de acesso > Criar chave de acesso.

  8. Faça o download do CSV (esses valores são inseridos 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 de fontes: 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 da UDM

Campo de registro Mapeamento do UDM Lógica
actingUserId target.user.userid Se enriched.actingUser.userId estiver vazio ou nulo, este 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 Extraiu o endereço IP do campo e mapeou 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.