Recolha registos de eventos do Bitwarden Enterprise
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
- Na consola do administrador do Bitwarden.
- Aceda a Definições > Informações da organização > Ver chave da API.
- Copie e guarde os seguintes detalhes numa localização segura:
- ID de cliente
- Segredo do cliente
- 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
)
- IDENTITY_URL:
Configure o contentor do AWS S3 e o IAM para o Google SecOps
- Crie um contentor do Amazon S3 seguindo este manual do utilizador: Criar um contentor
- Guarde o nome e a região do contentor para referência futura (por exemplo,
bitwarden-events
). - Crie um utilizador seguindo este guia do utilizador: Criar um utilizador do IAM.
- Selecione o utilizador criado.
- Selecione o separador Credenciais de segurança.
- Clique em Criar chave de acesso na secção Chaves de acesso.
- Selecione Serviço de terceiros como o Exemplo de utilização.
- Clicar em Seguinte.
- Opcional: adicione uma etiqueta de descrição.
- Clique em Criar chave de acesso.
- Clique em Transferir ficheiro CSV para guardar a chave de acesso e a chave de acesso secreta para utilização posterior.
- Clique em Concluído.
- Selecione o separador Autorizações.
- Clique em Adicionar autorizações na secção Políticas de autorizações.
- Selecione Adicionar autorizações.
- Selecione Anexar políticas diretamente
- Pesquise e selecione a política AmazonS3FullAccess.
- Clicar em Seguinte.
- Clique em Adicionar autorizações.
Configure a política e a função de IAM para carregamentos do S3
- Aceda a AWS console > IAM > Policies > Create policy > separador JSON.
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.
- Substitua
Clique em Seguinte > Criar política.
Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.
Anexe a política criada recentemente.
Dê o nome
WriteBitwardenToS3Role
à função e clique em Criar função.
Crie a função Lambda
- Na consola da AWS, aceda a Lambda > Functions > Create function.
- Clique em Criar do zero.
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
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())
Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
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
Depois de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your-function).
Selecione o separador Configuração.
No painel Configuração geral, clique em Editar.
Altere Tempo limite para 5 minutos (300 segundos) e clique em Guardar.
Crie um horário do EventBridge
- Aceda a Amazon EventBridge > Scheduler > Create schedule.
- Indique os seguintes detalhes de configuração:
- Agenda recorrente: Taxa (
1 hour
). - Alvo: a sua função Lambda.
- Nome:
bitwarden-events-1h
.
- Agenda recorrente: Taxa (
- Clique em Criar programação.
Opcional: crie um utilizador e chaves da IAM só de leitura para o Google SecOps
- Na consola da AWS, aceda a IAM > Users e, de seguida, clique em Add users.
- 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.
- Utilizador: introduza um nome único (por exemplo,
- 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 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>" } ] }
Defina o nome como
secops-reader-policy
.Aceda a Criar política > pesquise/selecione > Seguinte > Adicionar autorizações.
Aceda a Credenciais de segurança > Chaves de acesso > Criar chave de acesso.
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
- Aceda a Definições do SIEM > Feeds.
- Clique em + Adicionar novo feed.
- No campo Nome do feed, introduza um nome para o feed (por exemplo,
Bitwarden Events
). - Selecione Amazon S3 V2 como o Tipo de origem.
- Selecione Eventos do Bitwarden como o Tipo de registo.
- Clicar em Seguinte.
- 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.
- URI do S3:
- Clicar em Seguinte.
- 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.