Raccogliere gli audit log di Google Cloud Looker
Questo documento spiega come importare gli audit log di Google Cloud Looker in Google Security Operations utilizzando Google Cloud Storage o Amazon S3.
Looker è una piattaforma di business intelligence e analisi dei dati che consente alle organizzazioni di esplorare, analizzare e condividere approfondimenti aziendali in tempo reale. Il modello Attività di sistema di Looker monitora gli eventi di autenticazione degli utenti, la cronologia di esecuzione delle query, l'accesso a dashboard e Look, la creazione e la modifica dei contenuti, gli eventi di download, le chiamate API, gli eventi di distribuzione pianificata e le modifiche alle autorizzazioni.
Prima di iniziare
Assicurati di soddisfare i seguenti prerequisiti:
- Un'istanza Google SecOps
- Un'istanza di Looker con accesso amministrativo o l'autorizzazione
see_system_activity - Credenziali API Looker (ID client e client secret)
- Per il percorso GCS: un progetto GCP con le API Cloud Storage, Cloud Run, Pub/Sub e Cloud Scheduler abilitate
- Per il percorso S3: accesso privilegiato ad AWS (S3, IAM)
Configurare le credenziali dell'API Looker
Per consentire a Google SecOps di recuperare i dati di controllo dell'attività di sistema, devi creare le credenziali API in Looker e configurare un account di servizio con le autorizzazioni richieste.
Crea un account di servizio Looker per l'accesso API
- Accedi alla tua istanza di Looker come amministratore.
- Vai ad Amministrazione > Utenti.
- Fai clic su Aggiungi utenti.
- Nel campo Email, inserisci un'email del account di servizio (ad esempio,
chronicle-integration@yourcompany.com). Nella sezione Ruoli, seleziona un ruolo che includa l'autorizzazione
see_system_activity.Fai clic su Salva.
Genera credenziali API3
- Vai ad Amministrazione > Utenti.
- Trova l'utente del account di servizio che hai creato e fai clic su Modifica.
- Scorri verso il basso fino alla sezione Chiavi API.
- Fai clic su Nuova chiave API.
Copia e salva i seguenti valori in una posizione sicura:
- ID client: l'identificatore pubblico per l'autenticazione API
- Client secret: la chiave privata per l'autenticazione API
Identificare l'URL di base dell'API Looker
L'URL di base dell'API Looker segue questo formato:
https://<instance_name>.cloud.looker.com
Per le istanze di Looker ospitate su Google Cloud, Microsoft Azure e le istanze ospitate su AWS create il 07/07/2020 o in data successiva, l'API utilizza la porta 443 (HTTPS predefinito). Per le istanze ospitate su AWS meno recenti, l'API potrebbe utilizzare la porta 19999.
Puoi trovare l'URL dell'host dell'API andando ad Amministrazione > API nell'istanza di Looker.
Testare l'accesso API
Testa le tue credenziali prima di procedere con l'integrazione:
# Replace with your actual credentials LOOKER_BASE_URL="https://your-instance.cloud.looker.com" CLIENT_ID="your-client-id" CLIENT_SECRET="your-client-secret" # Obtain access token TOKEN=$(curl -s -X POST "${LOOKER_BASE_URL}/api/4.0/login" \ -d "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") # Test System Activity access curl -s -H "Authorization: token ${TOKEN}" \ "${LOOKER_BASE_URL}/api/4.0/queries/run/json" \ -X POST \ -H "Content-Type: application/json" \ -d '{"model":"system__activity","view":"event","fields":["event.name","event.created_time"],"limit":"5","sorts":["event.created_time desc"]}' \ | python3 -m json.tool
Una risposta positiva restituisce un array JSON degli eventi Looker recenti.
Opzione A: configura l'importazione utilizzando Google Cloud Storage
Questa opzione utilizza una funzione Cloud Run per eseguire il polling dell'API Looker per gli eventi di controllo dell'attività di sistema e scriverli in un bucket GCS per l'importazione da parte di Google SecOps.
Crea un bucket Google Cloud Storage
- Vai alla console Google Cloud.
- Seleziona il tuo progetto o creane uno nuovo.
- Nel menu di navigazione, vai a Cloud Storage > Bucket.
- Fai clic su Crea bucket.
Fornisci i seguenti dettagli di configurazione:
Impostazione Valore Assegna un nome al bucket Inserisci un nome univoco globale (ad esempio looker-audit-logs-gcs).Tipo di località Scegli in base alle tue esigenze (regione singola, a due regioni, multiregionale) Località Seleziona la posizione (ad esempio, us-central1).Classe di archiviazione Standard (consigliato per i log a cui si accede di frequente) Controllo degli accessi Uniforme (consigliato) Strumenti di protezione (Facoltativo) Abilita il controllo delle versioni degli oggetti o la policy di conservazione Fai clic su Crea.
Crea un account di servizio per la funzione Cloud Run
- Nella console Google Cloud, vai a IAM e amministrazione > Service account.
- Fai clic su Crea account di servizio.
- Fornisci i seguenti dettagli di configurazione:
- Nome del service account: inserisci
looker-audit-collector-sa - Descrizione service account: inserisci
Service account for Cloud Run function to collect Looker audit logs
- Nome del service account: inserisci
- Fai clic su Crea e continua.
- Nella sezione Concedi a questo account di servizio l'accesso al progetto, aggiungi i seguenti ruoli:
- Fai clic su Seleziona un ruolo.
- Cerca e seleziona Storage Object Admin.
- Fai clic su + Aggiungi un altro ruolo.
- Cerca e seleziona Cloud Run Invoker.
- Fai clic su + Aggiungi un altro ruolo.
- Cerca e seleziona Cloud Functions Invoker.
- Fai clic su Continua.
- Fai clic su Fine.
Concedi autorizzazioni IAM sul bucket GCS
- Vai a Cloud Storage > Bucket.
- Fai clic sul nome del bucket (
looker-audit-logs-gcs). - Vai alla scheda Autorizzazioni.
- Fai clic su Concedi l'accesso.
- Fornisci i seguenti dettagli di configurazione:
- Aggiungi entità: inserisci l'email del account di servizio (
looker-audit-collector-sa@PROJECT_ID.iam.gserviceaccount.com). - Assegna i ruoli: seleziona Storage Object Admin.
- Aggiungi entità: inserisci l'email del account di servizio (
- Fai clic su Salva.
Crea argomento Pub/Sub
- Nella console Google Cloud, vai a Pub/Sub > Argomenti.
- Fai clic su Crea argomento.
- Fornisci i seguenti dettagli di configurazione:
- ID argomento: inserisci
looker-audit-trigger - Lascia invariate le altre impostazioni predefinite.
- ID argomento: inserisci
- Fai clic su Crea.
Crea una funzione Cloud Run per raccogliere i log
- Nella console Google Cloud, vai a Cloud Run.
- Fai clic su Crea servizio.
- Seleziona Funzione (utilizza un editor in linea per creare una funzione).
Nella sezione Configura, fornisci i seguenti dettagli di configurazione:
Impostazione Valore Nome servizio looker-audit-collectorRegione Seleziona la regione corrispondente al tuo bucket GCS (ad esempio, us-central1)Tempo di esecuzione Seleziona Python 3.12 o versioni successive. Nella sezione Trigger (facoltativo):
- Fai clic su + Aggiungi trigger.
- Seleziona Cloud Pub/Sub.
- In Seleziona un argomento Cloud Pub/Sub, scegli
looker-audit-trigger. - Fai clic su Salva.
Nella sezione Autenticazione:
- Seleziona Richiedi autenticazione.
- Controlla Identity and Access Management (IAM).
Scorri verso il basso ed espandi Container, networking, sicurezza.
Vai alla scheda Sicurezza:
- Service account: seleziona
looker-audit-collector-sa.
- Service account: seleziona
Vai alla scheda Container:
- Fai clic su Variabili e secret.
- Fai clic su + Aggiungi variabile per ogni variabile di ambiente:
Nome variabile Valore di esempio Descrizione GCS_BUCKETlooker-audit-logs-gcsNome bucket GCS GCS_PREFIXlooker-auditPrefisso per i file di log STATE_KEYlooker-audit/state.jsonPercorso file di stato LOOKER_BASE_URLhttps://your-instance.cloud.looker.comURL di base dell'API Looker LOOKER_CLIENT_IDyour-client-idID client API Looker LOOKER_CLIENT_SECRETyour-client-secretClient secret dell'API Looker LOOKBACK_HOURS24Periodo di riferimento iniziale PAGE_SIZE5000Record per pagina API MAX_PAGES20Numero massimo di pagine per query Nella sezione Variabili e secret, scorri verso il basso fino a Richieste:
- Timeout richiesta: inserisci
600secondi (10 minuti)
- Timeout richiesta: inserisci
Vai alla scheda Impostazioni:
- Nella sezione Risorse:
- Memoria: seleziona 512 MiB o un valore superiore.
- CPU: seleziona 1
- Nella sezione Risorse:
Nella sezione Scalabilità della revisione:
- Numero minimo di istanze: inserisci
0 - Numero massimo di istanze: inserisci
100
- Numero minimo di istanze: inserisci
Fai clic su Crea.
Attendi la creazione del servizio (1-2 minuti).
Dopo aver creato il servizio, si aprirà automaticamente l'editor di codice incorporato.
Aggiungi codice per la funzione
- Inserisci main nel campo Entry point (Punto di ingresso).
- Nell'editor di codice incorporato, crea due file:
main.py:
import functions_framework from google.cloud import storage import json import os import urllib3 import urllib.parse from datetime import datetime, timezone, timedelta http = urllib3.PoolManager( timeout=urllib3.Timeout(connect=10.0, read=60.0), retries=False, ) storage_client = storage.Client() GCS_BUCKET = os.environ.get('GCS_BUCKET') GCS_PREFIX = os.environ.get('GCS_PREFIX', 'looker-audit') STATE_KEY = os.environ.get('STATE_KEY', 'looker-audit/state.json') LOOKER_BASE_URL = os.environ.get('LOOKER_BASE_URL', '').rstrip('/') CLIENT_ID = os.environ.get('LOOKER_CLIENT_ID') CLIENT_SECRET = os.environ.get('LOOKER_CLIENT_SECRET') LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24')) PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '5000')) MAX_PAGES = int(os.environ.get('MAX_PAGES', '20')) @functions_framework.cloud_event def main(cloud_event): if not all([GCS_BUCKET, LOOKER_BASE_URL, CLIENT_ID, CLIENT_SECRET]): print('Error: Missing required environment variables') return try: bucket = storage_client.bucket(GCS_BUCKET) state = load_state(bucket) now = datetime.now(timezone.utc) if isinstance(state, dict) and state.get('last_event_time'): try: last_val = state['last_event_time'] if last_val.endswith('Z'): last_val = last_val[:-1] + '+00:00' last_time = datetime.fromisoformat(last_val) last_time = last_time - timedelta(minutes=2) except Exception as e: print(f"Warning: Could not parse last_event_time: {e}") last_time = now - timedelta(hours=LOOKBACK_HOURS) else: last_time = now - timedelta(hours=LOOKBACK_HOURS) print(f"Fetching events from {last_time.isoformat()} to {now.isoformat()}") token = get_access_token() events = fetch_system_activity( token, 'event', [ 'event.id', 'event.name', 'event.category', 'event.created_time', 'event.is_api_call', 'event.is_admin', 'event.is_looker_employee', 'user.id', 'user.name', 'user.email' ], 'event.created_time', last_time, now ) history = fetch_system_activity( token, 'history', [ 'history.id', 'history.created_time', 'history.completed_time', 'history.status', 'history.source', 'history.issuer_source', 'history.runtime', 'history.message', 'query.id', 'query.model', 'query.view', 'user.id', 'user.name', 'user.email', 'dashboard.id', 'dashboard.title', 'look.id', 'look.title' ], 'history.created_time', last_time, now ) all_records = [] for e in events: e['_looker_record_type'] = 'event' all_records.append(e) for h in history: h['_looker_record_type'] = 'history' all_records.append(h) if not all_records: print("No new records found.") save_state(bucket, now.isoformat()) return timestamp = now.strftime('%Y%m%d_%H%M%S') object_key = f"{GCS_PREFIX}/looker_audit_{timestamp}.ndjson" blob = bucket.blob(object_key) ndjson = '\n'.join( [json.dumps(r, ensure_ascii=False, default=str) for r in all_records] ) + '\n' blob.upload_from_string(ndjson, content_type='application/x-ndjson') print(f"Wrote {len(all_records)} records to gs://{GCS_BUCKET}/{object_key}") newest = find_newest_time(events, history) save_state(bucket, newest if newest else now.isoformat()) print(f"Successfully processed {len(all_records)} records " f"(events: {len(events)}, history: {len(history)})") except Exception as e: print(f'Error processing logs: {str(e)}') raise def get_access_token(): url = f"{LOOKER_BASE_URL}/api/4.0/login" encoded_body = urllib.parse.urlencode({ 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET }).encode('utf-8') response = http.request( 'POST', url, body=encoded_body, headers={'Content-Type': 'application/x-www-form-urlencoded'} ) if response.status != 200: raise Exception( f"Looker login failed: {response.status} - " f"{response.data.decode('utf-8')}" ) data = json.loads(response.data.decode('utf-8')) token = data.get('access_token') if not token: raise Exception("No access_token in login response") print("Successfully obtained Looker API access token") return token def fetch_system_activity(token, view, fields, time_field, start_time, end_time): start_str = start_time.strftime('%Y-%m-%d %H:%M:%S') end_str = end_time.strftime('%Y-%m-%d %H:%M:%S') all_records = [] offset = 0 for page in range(MAX_PAGES): query_body = { "model": "system__activity", "view": view, "fields": fields, "filters": { time_field: f"{start_str} to {end_str}" }, "sorts": [f"{time_field} asc"], "limit": str(PAGE_SIZE), "offset": str(offset) } url = f"{LOOKER_BASE_URL}/api/4.0/queries/run/json" response = http.request( 'POST', url, body=json.dumps(query_body).encode('utf-8'), headers={ 'Authorization': f'token {token}', 'Content-Type': 'application/json' } ) if response.status == 429: print(f"Rate limited on {view} query. Stopping pagination.") break if response.status != 200: print(f"{view} query failed: {response.status} - " f"{response.data.decode('utf-8')}") break page_results = json.loads(response.data.decode('utf-8')) if not page_results: break all_records.extend(page_results) print(f"{view} page {page + 1}: {len(page_results)} records " f"(total: {len(all_records)})") if len(page_results) < PAGE_SIZE: break offset += PAGE_SIZE print(f"Total {view} records fetched: {len(all_records)}") return all_records def find_newest_time(events, history): newest = None for e in events: t = e.get('event.created_time') if t and (newest is None or t > newest): newest = t for h in history: t = h.get('history.created_time') if t and (newest is None or t > newest): newest = t return newest def load_state(bucket): try: blob = bucket.blob(STATE_KEY) if blob.exists(): return json.loads(blob.download_as_text()) except Exception as e: print(f"Warning: Could not load state: {e}") return {} def save_state(bucket, last_event_time_iso): try: state = { 'last_event_time': last_event_time_iso, 'last_run': datetime.now(timezone.utc).isoformat() } blob = bucket.blob(STATE_KEY) blob.upload_from_string( json.dumps(state, indent=2), content_type='application/json' ) print(f"Saved state: last_event_time={last_event_time_iso}") except Exception as e: print(f"Warning: Could not save state: {e}")requirements.txt:
functions-framework==3.* google-cloud-storage==2.* urllib3>=2.0.0
- Fai clic su Esegui il deployment per salvare la funzione ed eseguirne il deployment.
Attendi il completamento del deployment (2-3 minuti).
Crea job Cloud Scheduler
- Nella console Google Cloud, vai a Cloud Scheduler.
- Fai clic su Crea job.
Fornisci i seguenti dettagli di configurazione:
Impostazione Valore Nome looker-audit-collector-hourlyRegione Seleziona la stessa regione della funzione Cloud Run Frequenza 0 * * * *(ogni ora, allo scoccare dell'ora)Fuso orario Seleziona il fuso orario (UTC consigliato) Tipo di target Pub/Sub Argomento Seleziona looker-audit-triggerCorpo del messaggio {}(oggetto JSON vuoto)Fai clic su Crea.
Testare l'integrazione
- Nella console Cloud Scheduler, trova il tuo job (
looker-audit-collector-hourly). - Fai clic su Forza esecuzione per attivare il job manualmente.
- Attendi alcuni secondi.
- Vai a Cloud Run > Servizi.
- Fai clic su
looker-audit-collector. - Fai clic sulla scheda Log.
Verifica che la funzione sia stata eseguita correttamente. Cerca:
Fetching events from YYYY-MM-DDTHH:MM:SS+00:00 to YYYY-MM-DDTHH:MM:SS+00:00 Successfully obtained Looker API access token event page 1: X records (total: X) history page 1: X records (total: X) Wrote X records to gs://looker-audit-logs-gcs/looker-audit/looker_audit_YYYYMMDD_HHMMSS.ndjson Successfully processed X records (events: X, history: X)Vai a Cloud Storage > Bucket.
Fai clic su
looker-audit-logs-gcs.Vai alla cartella
looker-audit/.Verifica che sia stato creato un nuovo file
.ndjsoncon il timestamp corrente.
Se nei log vengono visualizzati errori:
- HTTP 401: verifica che le variabili di ambiente
LOOKER_CLIENT_IDeLOOKER_CLIENT_SECRETsiano corrette - HTTP 403: verifica che l'utente Looker disponga dell'autorizzazione
see_system_activity - HTTP 429: limitazione della frequenza: la funzione interromperà la paginazione e riprenderà alla successiva esecuzione pianificata
- Variabili di ambiente mancanti: verifica che tutte le variabili richieste siano impostate nella configurazione di Cloud Run Functions
Recuperare il account di servizio Google SecOps
- Vai a Impostazioni SIEM > Feed.
- Fai clic su Aggiungi nuovo feed.
- Fai clic su Configura un singolo feed.
- Nel campo Nome feed, inserisci un nome per il feed (ad esempio,
Looker Audit Logs GCS). - Seleziona Google Cloud Storage V2 come Tipo di origine.
- Seleziona Audit di Looker come Tipo di log.
Fai clic su Ottieni service account. Verrà visualizzato un indirizzo email del account di servizio univoco, ad esempio:
chronicle-12345678@chronicle-gcp-prod.iam.gserviceaccount.comCopia questo indirizzo email per utilizzarlo nel passaggio successivo.
Fai clic su Avanti.
Specifica i valori per i seguenti parametri di input:
URL del bucket di archiviazione: inserisci l'URI del bucket GCS con il percorso del prefisso:
gs://looker-audit-logs-gcs/looker-audit/
Opzione di eliminazione dell'origine: seleziona l'opzione di eliminazione in base alle tue preferenze:
- Mai: non elimina mai i file dopo i trasferimenti (opzione consigliata per i test).
- Elimina file trasferiti: elimina i file dopo il trasferimento riuscito.
Elimina file trasferiti e directory vuote: elimina i file e le directory vuote dopo il trasferimento riuscito.
Età massima del file: includi i file modificati nell'ultimo numero di giorni (il valore predefinito è 180 giorni)
Spazio dei nomi dell'asset: lo spazio dei nomi dell'asset
Etichette di importazione: l'etichetta da applicare agli eventi di questo feed
Fai clic su Avanti.
Controlla la nuova configurazione del feed nella schermata Finalizza e poi fai clic su Invia.
Concedi le autorizzazioni IAM al account di servizio Google SecOps
- Vai a Cloud Storage > Bucket.
- Fai clic su
looker-audit-logs-gcs. - Vai alla scheda Autorizzazioni.
- Fai clic su Concedi l'accesso.
- Fornisci i seguenti dettagli di configurazione:
- Aggiungi entità: incolla l'email del account di servizio Google SecOps
- Assegna i ruoli: seleziona Visualizzatore oggetti Storage.
Fai clic su Salva.
Opzione B: configura l'importazione utilizzando Amazon S3
Questa opzione utilizza una funzione AWS Lambda per eseguire il polling dell'API Looker per gli eventi di controllo dell'attività di sistema e scriverli in un bucket S3 per l'importazione da parte di Google SecOps.
Configura il bucket AWS S3 e IAM per Google SecOps
- Crea un bucket Amazon S3 seguendo questa guida dell'utente: Creazione di un bucket
- Salva il nome e la regione del bucket per riferimento futuro (ad esempio,
looker-audit-logs). - 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 di 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 le norme.
- Cerca la policy AmazonS3FullAccess.
- Seleziona la policy.
- Fai clic su Avanti.
- Fai clic su Aggiungi autorizzazioni.
Configura la policy e il ruolo IAM per i caricamenti S3
- Nella console AWS, vai a IAM > Policy > Crea policy > scheda JSON.
- Copia e incolla il criterio riportato di seguito.
JSON della policy (sostituisci
looker-audit-logsse hai inserito un nome del bucket diverso):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::looker-audit-logs/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::looker-audit-logs/looker-audit/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
LookerAuditCollectorRolee fai clic su Crea ruolo.
Crea la funzione Lambda
- Nella console AWS, vai a Lambda > Funzioni > Crea funzione.
- Fai clic su Crea autore da zero.
Fornisci i seguenti dettagli di configurazione:
Impostazione Valore Nome LookerAuditCollectorTempo di esecuzione Python 3.13 Architettura x86_64 Ruolo di esecuzione LookerAuditCollectorRoleDopo aver creato la funzione, apri la scheda Codice, elimina lo stub e incolla il codice riportato di seguito (LookerAuditCollector.py).
import urllib3 import json import boto3 import os from datetime import datetime, timezone, timedelta import logging import urllib.parse logger = logging.getLogger() logger.setLevel(logging.INFO) http = urllib3.PoolManager( timeout=urllib3.Timeout(connect=10.0, read=60.0), retries=False, ) s3 = boto3.client('s3') BUCKET = os.environ['S3_BUCKET'] PREFIX = os.environ['S3_PREFIX'] STATE_KEY = os.environ['STATE_KEY'] LOOKER_BASE_URL = os.environ['LOOKER_BASE_URL'].rstrip('/') CLIENT_ID = os.environ['LOOKER_CLIENT_ID'] CLIENT_SECRET = os.environ['LOOKER_CLIENT_SECRET'] LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24')) PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '5000')) MAX_PAGES = int(os.environ.get('MAX_PAGES', '20')) def lambda_handler(event, context): try: state = load_state() now = datetime.now(timezone.utc) if state and state.get('last_event_time'): try: last_time = datetime.fromisoformat( state['last_event_time'].replace('Z', '+00:00') ) last_time = last_time - timedelta(minutes=2) except Exception as e: logger.warning(f"Could not parse last_event_time: {e}") last_time = now - timedelta(hours=LOOKBACK_HOURS) else: last_time = now - timedelta(hours=LOOKBACK_HOURS) logger.info(f"Fetching events from {last_time.isoformat()} to {now.isoformat()}") token = get_access_token() events = fetch_events(token, last_time, now) history = fetch_history(token, last_time, now) all_records = [] for e in events: e['_looker_record_type'] = 'event' all_records.append(e) for h in history: h['_looker_record_type'] = 'history' all_records.append(h) if not all_records: logger.info("No new records found.") save_state(now.isoformat()) return {'statusCode': 200, 'body': json.dumps({'events': 0})} timestamp = now.strftime('%Y%m%d_%H%M%S') object_key = f"{PREFIX}/looker_audit_{timestamp}.ndjson" ndjson = '\n'.join( [json.dumps(r, ensure_ascii=False, default=str) for r in all_records] ) + '\n' s3.put_object( Bucket=BUCKET, Key=object_key, Body=ndjson.encode('utf-8'), ContentType='application/x-ndjson' ) logger.info(f"Wrote {len(all_records)} records to s3://{BUCKET}/{object_key}") newest_time = find_newest_time(events, history) save_state(newest_time if newest_time else now.isoformat()) return { 'statusCode': 200, 'body': json.dumps({ 'events': len(events), 'history': len(history), 'total': len(all_records) }) } except Exception as e: logger.error(f"Lambda execution failed: {str(e)}") raise def get_access_token(): url = f"{LOOKER_BASE_URL}/api/4.0/login" encoded_body = urllib.parse.urlencode({ 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET }).encode('utf-8') response = http.request( 'POST', url, body=encoded_body, headers={'Content-Type': 'application/x-www-form-urlencoded'} ) if response.status != 200: raise Exception( f"Login failed with status {response.status}: " f"{response.data.decode('utf-8')}" ) data = json.loads(response.data.decode('utf-8')) token = data.get('access_token') if not token: raise Exception("No access_token in login response") logger.info("Successfully obtained Looker API access token") return token def fetch_events(token, start_time, end_time): start_str = start_time.strftime('%Y-%m-%d %H:%M:%S') end_str = end_time.strftime('%Y-%m-%d %H:%M:%S') all_events = [] offset = 0 for page in range(MAX_PAGES): query_body = { "model": "system__activity", "view": "event", "fields": [ "event.id", "event.name", "event.category", "event.created_time", "event.is_api_call", "event.is_admin", "event.is_looker_employee", "user.id", "user.name", "user.email" ], "filters": { "event.created_time": f"{start_str} to {end_str}" }, "sorts": ["event.created_time asc"], "limit": str(PAGE_SIZE), "offset": str(offset) } url = f"{LOOKER_BASE_URL}/api/4.0/queries/run/json" response = http.request( 'POST', url, body=json.dumps(query_body).encode('utf-8'), headers={ 'Authorization': f'token {token}', 'Content-Type': 'application/json' } ) if response.status == 429: logger.warning("Rate limited on events query. Stopping pagination.") break if response.status != 200: logger.error( f"Events query failed: {response.status} - " f"{response.data.decode('utf-8')}" ) break page_results = json.loads(response.data.decode('utf-8')) if not page_results: logger.info(f"Events: No more results at offset {offset}") break all_events.extend(page_results) logger.info( f"Events page {page + 1}: Retrieved {len(page_results)} records " f"(total: {len(all_events)})" ) if len(page_results) < PAGE_SIZE: break offset += PAGE_SIZE logger.info(f"Total events fetched: {len(all_events)}") return all_events def fetch_history(token, start_time, end_time): start_str = start_time.strftime('%Y-%m-%d %H:%M:%S') end_str = end_time.strftime('%Y-%m-%d %H:%M:%S') all_history = [] offset = 0 for page in range(MAX_PAGES): query_body = { "model": "system__activity", "view": "history", "fields": [ "history.id", "history.created_time", "history.completed_time", "history.status", "history.source", "history.issuer_source", "history.runtime", "history.message", "query.id", "query.model", "query.view", "user.id", "user.name", "user.email", "dashboard.id", "dashboard.title", "look.id", "look.title" ], "filters": { "history.created_time": f"{start_str} to {end_str}" }, "sorts": ["history.created_time asc"], "limit": str(PAGE_SIZE), "offset": str(offset) } url = f"{LOOKER_BASE_URL}/api/4.0/queries/run/json" response = http.request( 'POST', url, body=json.dumps(query_body).encode('utf-8'), headers={ 'Authorization': f'token {token}', 'Content-Type': 'application/json' } ) if response.status == 429: logger.warning("Rate limited on history query. Stopping pagination.") break if response.status != 200: logger.error( f"History query failed: {response.status} - " f"{response.data.decode('utf-8')}" ) break page_results = json.loads(response.data.decode('utf-8')) if not page_results: logger.info(f"History: No more results at offset {offset}") break all_history.extend(page_results) logger.info( f"History page {page + 1}: Retrieved {len(page_results)} records " f"(total: {len(all_history)})" ) if len(page_results) < PAGE_SIZE: break offset += PAGE_SIZE logger.info(f"Total history records fetched: {len(all_history)}") return all_history def find_newest_time(events, history): newest = None for e in events: t = e.get('event.created_time') if t and (newest is None or t > newest): newest = t for h in history: t = h.get('history.created_time') if t and (newest is None or t > newest): newest = t return newest def load_state(): try: obj = s3.get_object(Bucket=BUCKET, Key=STATE_KEY) return json.loads(obj['Body'].read().decode('utf-8')) except s3.exceptions.NoSuchKey: logger.info("No previous state found, starting fresh") return None except Exception as e: logger.warning(f"Could not load state: {e}") return None def save_state(last_event_time): state = { 'last_event_time': last_event_time, 'last_run': datetime.now(timezone.utc).isoformat() } s3.put_object( Bucket=BUCKET, Key=STATE_KEY, Body=json.dumps(state, indent=2).encode('utf-8'), ContentType='application/json' ) logger.info(f"Saved state: last_event_time={last_event_time}")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 | Valore di esempio |
|---|---|
S3_BUCKET |
looker-audit-logs |
S3_PREFIX |
looker-audit/ |
STATE_KEY |
looker-audit/state.json |
LOOKER_BASE_URL |
https://your-instance.cloud.looker.com |
LOOKER_CLIENT_ID |
your-looker-client-id |
LOOKER_CLIENT_SECRET |
your-looker-client-secret |
LOOKBACK_HOURS |
24 |
PAGE_SIZE |
5000 |
MAX_PAGES |
20 |
- 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:
- Pianificazione ricorrente: Tariffa (
1 hour) - Target: la tua funzione Lambda
LookerAuditCollector - Nome:
LookerAuditCollector-1h
- Pianificazione ricorrente: Tariffa (
- Fai clic su Crea pianificazione.
Configura un feed in Google SecOps per importare i log di controllo di Looker
- Vai a Impostazioni SIEM > Feed.
- Fai clic su Aggiungi nuovo feed.
- Nella pagina successiva, fai clic su Configura un singolo feed.
- Inserisci un nome univoco per il nome feed.
- Seleziona Amazon S3 V2 come Tipo di origine.
- Seleziona Audit di Looker come Tipo di log.
- Fai clic su Avanti e poi su Invia.
Specifica i valori per i seguenti campi:
- URI S3:
s3://looker-audit-logs/looker-audit/ - Opzione di eliminazione dell'origine: seleziona l'opzione di eliminazione in base alle tue preferenze.
- Età massima del file: includi i file modificati nell'ultimo numero di giorni (il valore predefinito è 180 giorni)
- 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 da applicare agli eventi di questo feed
- URI S3:
Fai clic su Avanti e poi su Invia.
Riferimento ai dati dell'attività del sistema di Looker
La tabella seguente descrive i dati chiave disponibili nelle esplorazioni dell'attività di sistema di Looker raccolti da questa integrazione:
| Esplora | Dati raccolti | Conservazione |
|---|---|---|
| Evento | Eventi di autenticazione utente, creazione e modifica di contenuti, modifiche alle autorizzazioni, chiamate API, eventi di pubblicazione pianificata, eventi di download | 90 giorni (predefinito) |
| Cronologia | Cronologia di esecuzione delle query, dashboard e accesso a Look, runtime e stato delle query, origine della query (UI, API, pianificazione) | 90 giorni (predefinito) |
Tabella di mappatura UDM
| Campo log | Mappatura UDM | Logica |
|---|---|---|
| Link di modifica di gruppo | additional.fields.Group_Edit_Link_label.value.string_value | Valore copiato direttamente |
| ID gruppo | additional.fields.Group_ID_label.value.string_value | Valore copiato direttamente |
| Durata dell'esecuzione più recente della cronologia in secondi | additional.fields.History_Most_Recent_Run_Length_in_Seconds_label.value.string_value | Valore copiato direttamente |
| Slug della cronologia | additional.fields.History_Slug_label.value.string_value | Valore copiato direttamente |
| Origine della cronologia | additional.fields.History_Source_label.value.string_value | Valore copiato direttamente |
| Stato cronologia | additional.fields.History_Status_label.value.string_value | Valore copiato direttamente |
| Link Look | additional.fields.Look_Link_label.value.string_value | Valore copiato direttamente |
| Titolo del look | additional.fields.Look_Title_label.value.string_value | Valore copiato direttamente |
| Link di modifica utente | additional.fields.User_Edit_Link_label.value.string_value | Valore copiato direttamente |
| Cartella Home utente | additional.fields.User_Home_Folder_label.value.string_value | Valore copiato direttamente |
| dashboard.link | additional.fields.dashboard_link_label.value.string_value | Valore copiato direttamente |
| dashboard.title | additional.fields.dashboard_title_label.value.string_value | Valore copiato direttamente |
| history.source | additional.fields.history_source_label.value.string_value | Valore copiato direttamente |
| history.status | additional.fields.history_status_label.value.string_value | Valore copiato direttamente |
| history.id | additional.fields.id_label.value.string_value | Convertito in stringa |
| history.connection_name | additional.fields.name_label.value.string_value | Valore copiato direttamente |
| query.model | additional.fields.query_model_label.value.string_value | Valore copiato direttamente |
| query.view | additional.fields.query_view_label.value.string_value | Valore copiato direttamente |
| sql_text.text | additional.fields.sql_text_text_label.value.string_value | Valore copiato direttamente |
| Data/ora creazione cronologia | metadata.event_timestamp | Analizzato utilizzando il formato ISO8601, RFC 3339 o aaaa-MM-gg HH:mm:ss |
| has_principal_user | metadata.event_type | Impostato su "NETWORK_CONNECTION" se has_principal e has_target sono true; "USER_UNCATEGORIZED" se has_principal_user è true; "STATUS_UPDATE" se has_principal è true; altrimenti "GENERIC_EVENT" |
| has_principal | metadata.event_type | |
| has_target | metadata.event_type | |
| Indirizzo email dell'utente | principal.email | Valore di Email utente se non è vuoto, altrimenti user.email |
| user.email | principal.email | |
| Nome gruppo | principal.group.group_display_name | Valore copiato direttamente |
| ID utente | principal.user.product_object_id | Valore di User-ID se non è vuoto, altrimenti user.id |
| user.id | principal.user.product_object_id | |
| Nome utente | principal.user.userid | Valore di User Name se non è vuoto, altrimenti user.name |
| nome.utente | principal.user.userid | |
| Kevin Liu | security_result.category_details | Unito a "Kevin_Liu_label" se Kevin Liu non è vuoto, "History_ID_label" se History ID non è vuoto, "History_Created_Date_label" se History Created Date non è vuoto |
| ID cronologia | security_result.category_details | |
| Data di creazione della cronologia | security_result.category_details | |
| Descrizione del look | security_result.description | Valore copiato direttamente |
| Nome utente ordinato | target.hostname | Valore copiato se il nome del ramo di sviluppo utente non è vuoto |
user.name |
principal.user.user_display_name |
Mappato dal log delle modifiche |
Log delle modifiche
Visualizza il log delle modifiche per questo parser
Hai bisogno di ulteriore assistenza? Ricevi risposte dai membri della community e dai professionisti di Google SecOps.