Coletar registros de eventos do Bitwarden Enterprise
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
- No Admin Console do Bitwarden.
- Acesse Configurações > Informações da organização > Ver chave de API.
- Copie e salve os seguintes detalhes em um local seguro:
- ID do cliente
- Client Secret
- 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)
- IDENTITY_URL =
Configurar o bucket do AWS S3 e o IAM para o Google SecOps
- Crie um bucket do Amazon S3 seguindo este guia do usuário: Como criar um bucket
- Salve o Nome e a Região do bucket para referência futura (por exemplo,
bitwarden-events). - Crie um usuário seguindo este guia: Como criar um usuário do IAM.
- Selecione o usuário criado.
- Selecione a guia Credenciais de segurança.
- Clique em Criar chave de acesso na seção Chaves de acesso.
- Selecione Serviço de terceiros como Caso de uso.
- Clique em Próxima.
- Opcional: adicione uma tag de descrição.
- Clique em Criar chave de acesso.
- Clique em Baixar arquivo .csv para salvar a chave de acesso e a chave de acesso secreta para referência futura.
- Clique em Concluído.
- Selecione a guia Permissões.
- Clique em Adicionar permissões na seção Políticas de permissões.
- Selecione Adicionar permissões.
- Selecione Anexar políticas diretamente.
- Pesquise a política AmazonS3FullAccess.
- Selecione a política.
- Clique em Próxima.
- Clique em Adicionar permissões
Configurar a política e o papel do IAM para uploads do S3
- Acesse o console da AWS > IAM > Políticas > Criar política > guia JSON.
- Copie e cole a política abaixo.
- JSON da política (substitua
bitwarden-eventsse 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"
}
]
}
- Clique em Próxima > Criar política.
- Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.
- Anexe a política recém-criada.
- Nomeie a função como
WriteBitwardenToS3Rolee clique em Criar função.
Criar a função Lambda
- No console da AWS, acesse Lambda > Functions > Create function.
- Clique em Criar do zero.
- 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 |
- 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())
- Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
- 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 |
- Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua‑função.
- Selecione a guia Configuração.
- No painel Configuração geral, clique em Editar.
- Mude Tempo limite para 5 minutos (300 segundos) e clique em Salvar.
Criar uma programação do EventBridge
- Acesse Amazon EventBridge > Scheduler > Criar programação.
- Informe os seguintes detalhes de configuração:
- Programação recorrente: Taxa (
1 hour). - Destino: sua função Lambda.
- Nome:
bitwarden-events-1h.
- Programação recorrente: Taxa (
- Clique em Criar programação.
(Opcional) Criar um usuário e chaves do IAM somente leitura para o Google SecOps
- Acesse Console da AWS > IAM > Usuários > Adicionar usuários.
- Clique em Add users.
- Informe os seguintes detalhes de configuração:
- Usuário: insira
secops-reader. - Tipo de acesso: selecione Chave de acesso — Acesso programático.
- Usuário: insira
- Clique em Criar usuário.
- 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.
- 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>"
}
]
}
- Name =
secops-reader-policy. - Clique em Criar política > pesquisar/selecionar > Próxima > Adicionar permissões.
- 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
- Acesse Configurações do SIEM > Feeds.
- Clique em + Adicionar novo feed.
- No campo Nome do feed, insira 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 registro.
- Clique em Próxima.
- 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.
- URI do S3:
- Clique em Próxima.
- 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.