Recolha registos de eventos do Bitwarden Enterprise
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
- 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(UE:https://identity.bitwarden.eu/connect/token) - API_BASE =
https://api.bitwarden.com(UE: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 Exemplo de utilização.
- Clicar em Seguinte.
- Opcional: adicione a 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 referência futura.
- 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 a política AmazonS3FullAccess.
- Selecione a política.
- 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.
- Copie e cole a política abaixo.
- JSON da política (substitua
bitwarden-eventsse 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"
}
]
}
- 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 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())
- Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
- 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 |
- 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.
- Forneça 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 horário.
(Opcional) Crie um utilizador e chaves da IAM só de leitura para o Google SecOps
- Aceda a AWS Console > IAM > Users > Add users.
- Clique em Adicionar utilizadores.
- Forneça os seguintes detalhes de configuração:
- Utilizador: introduza
secops-reader. - Tipo de acesso: selecione Chave de acesso – Acesso programático.
- Utilizador: introduza
- Clique em Criar utilizador.
- Anexe a política de leitura mínima (personalizada): Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy.
- 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>"
}
]
}
- Nome =
secops-reader-policy. - Clique em Criar política > procure/selecione > Seguinte > Adicionar autorizações.
- 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
- 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 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.