Raccogliere i log eventi di Bitwarden Enterprise
Questa guida spiega come importare i log eventi di Bitwarden Enterprise in Google Security Operations utilizzando Amazon S3. Il parser trasforma i log eventi non elaborati in formato JSON in un formato strutturato conforme a Chronicle UDM. Estrae i campi pertinenti, come i dettagli utente, gli indirizzi IP e i tipi di eventi, mappandoli ai campi UDM corrispondenti per un'analisi della sicurezza coerente.
Prima di iniziare
- Istanza Google SecOps.
- Accesso con privilegi al tenant Bitwarden.
- Accesso privilegiato ad AWS (S3, IAM, Lambda, EventBridge).
Ottenere la chiave API e l'URL di Bitwarden
- Nella console di amministrazione di Bitwarden.
- Vai a Impostazioni > Informazioni sull'organizzazione > Visualizza chiave API.
- Copia e salva i seguenti dettagli in una posizione sicura:
- ID client
- Client secret
- Determina gli endpoint Bitwarden (in base alla regione):
- 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 =
Configura il bucket AWS S3 e IAM per Google SecOps
- Crea un bucket Amazon S3 seguendo questa guida utente: Creazione di un bucket
- Salva il nome e la regione del bucket per riferimento futuro (ad esempio,
bitwarden-events). - Crea un utente seguendo questa guida utente: Creazione di un utente IAM.
- Seleziona l'utente creato.
- Seleziona la scheda Credenziali di sicurezza.
- Fai clic su Crea chiave di accesso nella sezione Chiavi di accesso.
- Seleziona Servizio di terze parti come Caso d'uso.
- Fai clic su Avanti.
- (Facoltativo) Aggiungi il tag della descrizione.
- Fai clic su Crea chiave di accesso.
- Fai clic su Scarica file .csv per salvare la chiave di accesso e la chiave di accesso segreta per riferimento futuro.
- Fai clic su Fine.
- Seleziona la scheda Autorizzazioni.
- Fai clic su Aggiungi autorizzazioni nella sezione Criteri per le autorizzazioni.
- Seleziona Aggiungi autorizzazioni.
- Seleziona Allega direttamente i criteri.
- Cerca il criterio AmazonS3FullAccess.
- Seleziona la policy.
- Fai clic su Avanti.
- Fai clic su Aggiungi autorizzazioni.
Configura il ruolo e il criterio IAM per i caricamenti S3
- Vai alla console AWS > IAM > Policy > Crea policy > scheda JSON.
- Copia e incolla il criterio riportato di seguito.
- JSON della policy (sostituisci
bitwarden-eventsse hai inserito un nome del bucket diverso):
{
"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"
}
]
}
- Fai clic su Avanti > Crea policy.
- Vai a IAM > Ruoli > Crea ruolo > Servizio AWS > Lambda.
- Allega la policy appena creata.
- Assegna al ruolo il nome
WriteBitwardenToS3Rolee fai clic su Crea ruolo.
Crea la funzione Lambda
- Nella console AWS, vai a Lambda > Funzioni > Crea funzione.
- Fai clic su Crea da zero.
- Fornisci i seguenti dettagli di configurazione:
| Impostazione | Valore |
|---|---|
| Nome | bitwarden_events_to_s3 |
| Tempo di esecuzione | Python 3.13 |
| Architettura | x86_64 |
| Ruolo di esecuzione | WriteBitwardenToS3Role |
- Dopo aver creato la funzione, apri la scheda Codice, elimina lo stub e incolla il codice riportato di seguito (
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())
- Vai a Configurazione > Variabili di ambiente > Modifica > Aggiungi nuova variabile di ambiente.
- Inserisci le variabili di ambiente fornite di seguito, sostituendole con i tuoi valori.
Variabili di ambiente
| Chiave | Esempio |
|---|---|
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 |
- Dopo aver creato la funzione, rimani sulla relativa pagina (o apri Lambda > Funzioni > la tua funzione).
- Seleziona la scheda Configurazione.
- Nel riquadro Configurazione generale, fai clic su Modifica.
- Modifica Timeout impostando 5 minuti (300 secondi) e fai clic su Salva.
Creare una pianificazione EventBridge
- Vai a Amazon EventBridge > Scheduler > Crea pianificazione.
- Fornisci i seguenti dettagli di configurazione:
- Programma ricorrente: Tariffa (
1 hour). - Target: la tua funzione Lambda.
- Nome:
bitwarden-events-1h
- Programma ricorrente: Tariffa (
- Fai clic su Crea pianificazione.
(Facoltativo) Crea chiavi e utenti IAM di sola lettura per Google SecOps
- Vai alla console AWS > IAM > Users (Utenti) > Add users (Aggiungi utenti).
- Fai clic su Add users (Aggiungi utenti).
- Fornisci i seguenti dettagli di configurazione:
- Utente: inserisci
secops-reader. - Tipo di accesso: seleziona Chiave di accesso - Accesso programmatico.
- Utente: inserisci
- Fai clic su Crea utente.
- Collega la criterio per la lettura minima (personalizzata): Utenti > secops-reader > Autorizzazioni > Aggiungi autorizzazioni > Collega le norme direttamente > Crea norma.
- 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. - Fai clic su Crea criterio > cerca/seleziona > Avanti > Aggiungi autorizzazioni.
- Crea la chiave di accesso per
secops-reader: Credenziali di sicurezza > Chiavi di accesso > Crea chiave di accesso > scarica.csv(incollerai questi valori nel feed).
Configura un feed in Google SecOps per importare i log eventi di Bitwarden Enterprise
- Vai a Impostazioni SIEM > Feed.
- Fai clic su + Aggiungi nuovo feed.
- Nel campo Nome feed, inserisci un nome per il feed (ad esempio,
Bitwarden Events). - Seleziona Amazon S3 V2 come Tipo di origine.
- Seleziona Eventi Bitwarden come Tipo di log.
- Fai clic su Avanti.
- Specifica i valori per i seguenti parametri di input:
- URI S3:
s3://bitwarden-events/bitwarden/events/ - Opzioni di eliminazione dell'origine: seleziona l'opzione di eliminazione in base alle tue preferenze.
- Durata massima del file: 180 giorni per impostazione predefinita.
- ID chiave di accesso: chiave di accesso utente con accesso al bucket S3.
- Chiave di accesso segreta: chiave segreta dell'utente con accesso al bucket S3.
- Spazio dei nomi dell'asset: lo spazio dei nomi dell'asset.
- Etichette di importazione: l'etichetta applicata agli eventi di questo feed.
- URI S3:
- Fai clic su Avanti.
- Controlla la nuova configurazione del feed nella schermata Finalizza e poi fai clic su Invia.
Tabella di mappatura UDM
| Campo log | Mappatura UDM | Logic |
|---|---|---|
| actingUserId | target.user.userid | Se enriched.actingUser.userId è vuoto o nullo, questo campo viene utilizzato per compilare il campo target.user.userid. |
| collectionID | security_result.detection_fields.key | Compila il campo key all'interno di detection_fields in security_result. |
| collectionID | security_result.detection_fields.value | Compila il campo value all'interno di detection_fields in security_result. |
| data | metadata.event_timestamp | Analizzato e convertito in un formato timestamp e mappato a event_timestamp. |
| enriched.actingUser.accessAll | security_result.rule_labels.key | Imposta il valore su "Access_All" all'interno di rule_labels in security_result. |
| enriched.actingUser.accessAll | security_result.rule_labels.value | Compila il campo value all'interno di rule_labels in security_result con il valore di enriched.actingUser.accessAll convertito in stringa. |
| enriched.actingUser.email | target.user.email_addresses | Compila il campo email_addresses all'interno di target.user. |
| enriched.actingUser.id | metadata.product_log_id | Compila il campo product_log_id all'interno di metadata. |
| enriched.actingUser.id | target.labels.key | Imposta il valore su "ID" all'interno di target.labels. |
| enriched.actingUser.id | target.labels.value | Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.id. |
| enriched.actingUser.name | target.user.user_display_name | Compila il campo user_display_name all'interno di target.user. |
| enriched.actingUser.object | target.labels.key | Imposta il valore su "Object" all'interno di target.labels. |
| enriched.actingUser.object | target.labels.value | Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.object. |
| enriched.actingUser.resetPasswordEnrolled | target.labels.key | Imposta il valore su "ResetPasswordEnrolled" in target.labels. |
| enriched.actingUser.resetPasswordEnrolled | target.labels.value | Compila il campo value all'interno di target.labels con il valore di enriched.actingUser.resetPasswordEnrolled convertito in stringa. |
| enriched.actingUser.twoFactorEnabled | security_result.rule_labels.key | Imposta il valore su "Two Factor Enabled" (Autenticazione a due fattori attivata) all'interno di rule_labels in security_result. |
| enriched.actingUser.twoFactorEnabled | security_result.rule_labels.value | Compila il campo value all'interno di rule_labels in security_result con il valore di enriched.actingUser.twoFactorEnabled convertito in stringa. |
| enriched.actingUser.userId | target.user.userid | Compila il campo userid all'interno di target.user. |
| enriched.collection.id | additional.fields.key | Imposta il valore su "ID raccolta" all'interno di additional.fields. |
| enriched.collection.id | additional.fields.value.string_value | Compila il campo string_value all'interno di additional.fields con il valore di enriched.collection.id. |
| enriched.collection.object | additional.fields.key | Imposta il valore su "Collection Object" all'interno di additional.fields. |
| enriched.collection.object | additional.fields.value.string_value | Compila il campo string_value all'interno di additional.fields con il valore di enriched.collection.object. |
| enriched.type | metadata.product_event_type | Compila il campo product_event_type all'interno di metadata. |
| groupId | target.user.group_identifiers | Aggiunge il valore all'array group_identifiers all'interno di target.user. |
| ipAddress | principal.ip | Indirizzo IP estratto dal campo e mappato a principal.ip. |
| N/D | extensions.auth | Il parser crea un oggetto vuoto. |
| N/D | metadata.event_type | Determinato in base a enriched.type e alla presenza di informazioni su principal e target. Valori possibili: USER_LOGIN, STATUS_UPDATE, GENERIC_EVENT. |
| N/D | security_result.action | Determinato in base a enriched.type. Valori possibili: ALLOW, BLOCK. |
| oggetto | additional.fields.key | Imposta il valore su "Object" all'interno di additional.fields. |
| oggetto | additional.fields.value | Compila il campo value all'interno di additional.fields con il valore di object. |
Hai bisogno di ulteriore assistenza? Ricevi risposte dai membri della community e dai professionisti di Google SecOps.