Recolha registos de eventos do Bitwarden Enterprise

Compatível com:

Este documento explica como 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, 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 (EU: https://identity.bitwarden.eu/connect/token)
    • API_BASE: https://api.bitwarden.com (EU: 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 o Exemplo de utilização.
  8. Clicar em Seguinte.
  9. Opcional: adicione uma 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 utilização posterior.
  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 e selecione a política AmazonS3FullAccess.
  18. Clicar em Seguinte.
  19. 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. Introduza 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 tiver introduzido um nome de contentor diferente.
  3. Clique em Seguinte > Criar política.

  4. Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.

  5. Anexe a política criada recentemente.

  6. 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
  4. Depois de criar a função, abra o separador Código, elimine o fragmento e introduza 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. Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.

  6. Introduza as seguintes variáveis de ambiente, substituindo-as 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 de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your-function).

  8. Selecione o separador Configuração.

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

  10. 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. Indique 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 programação.

Opcional: crie um utilizador e chaves da IAM só de leitura para o Google SecOps

  1. Na consola da AWS, aceda a IAM > Users e, de seguida, clique em Add users.
  2. Indique os seguintes detalhes de configuração:
    • Utilizador: introduza um nome único (por exemplo, secops-reader)
    • Tipo de acesso: selecione Chave de acesso – Acesso programático
    • Clique em Criar utilizador.
  3. Anexe a política de leitura mínima (personalizada): Utilizadores > selecione secops-reader > Autorizações > Adicionar autorizações > Anexar políticas diretamente > Criar política
  4. No editor JSON, introduza 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. Aceda a Criar política > pesquise/selecione > Seguinte > Adicionar autorizações.

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

  8. Transfira o CSV (estes valores são introduzidos 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 de 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.