AI per analisi log di sicurezza: pipeline di alerting intelligente che riduce i falsi positivi
Ho acceso il mio laboratorio SIEM personale l'11 febbraio 2026 su un Hetzner CCX53 (16 vCPU AMD EPYC 9454P, 64 GB RAM DDR5, 360 GB NVMe, Debian 12) con Wazuh 4.10 come SIEM open source, OpenSearch 2.18 per l'indicizzazione dei log, Filebeat 8.15 come log shipper, un cluster di container Docker che emulano una flotta realistica - 12 web server Nginx, 4 PostgreSQL, 2 istanze OAuth Zitadel, uno SSH jump-host, un endpoint API di un gestionale Laravel sintetico. Tutto il traffico è generato da uno script di simulazione che produce carico normale e, a intervalli non deterministici, inietta attività malevola (bruteforce SSH, SQL injection tentati, path traversal, enumerazioni di API) per avere un ground truth di cosa è attacco e cosa non lo è. Durante i primi sette giorni di rodaggio, il SIEM standard ha generato 29.400 alert - circa 4.200 al giorno - di cui 238 erano attività malevole reali iniettate dal mio script. Il resto, 99,2%, era rumore: scan di routine Internet, retry di utenti che sbagliavano password, service discovery legittimo, client health check. Nessun operatore umano avrebbe guardato uno per uno 4.200 alert al giorno - e nella mia sandbox nemmeno io. Ho lasciato correre per capire quanti veri positivi si perdessero nel rumore: 41 su 238, il 17,2%. Il resto passava per stanchezza. Era il caso perfetto per testare se un LLM, inserito come livello 2 di classificazione prima del ping umano, potesse ribaltare quella statistica. Quello che segue è il racconto del progetto da inizio a fine, con i numeri che sono emersi stadio per stadio.
Perché un LLM come pre-filtro riduce i falsi positivi di SIEM invece che aumentarli?
La risposta breve è che un SIEM tradizionale lavora per regole deterministiche e correlazioni statistiche su finestre temporali fisse, mentre la distinzione fra "log anomalo" e "attacco reale" è spesso contestuale - richiede di guardare l'evento nel suo contesto più largo (chi è l'utente, cosa stava facendo dieci minuti prima, da dove arriva, in che ora del giorno). Un LLM alimentato con l'alert e un contesto arricchito - ultime N righe di log dello stesso utente/IP, stato del sistema, profilo orario atteso - fa esattamente quella valutazione contestuale. Non sostituisce il SIEM, lo aumenta: il SIEM fa il lavoro veloce e statistico sui milioni di eventi, l'LLM fa la valutazione lenta e contestuale solo sui 4.200 alert che il SIEM già ha flaggato. Il modello del filter-a-cascata è classico in engineering: stage veloce/grezzo davanti, stage lento/preciso dietro.
La premessa perché il modello funzioni è che il prompt passato all'LLM contenga contesto sufficiente. Se gli passi solo "alert: SSH failure from 192.168.1.42 user=admin", il modello fa inferenza a vuoto - il suo tasso di errore è indistinguibile da quello del SIEM da solo. Se gli passi "alert: SSH failure from 192.168.1.42 user=admin; ultimi 30 minuti di log SSH da quell'IP: 2 login riusciti, 1 failure ora, utente admin è il sistemista di turno su script schedulato; geolocalizzazione IP: Milano (stesso ufficio del sistemista); orario: 09:14 ora locale", il modello valuta facilmente che è un benign outlier da scartare. Questo è LLM01 Prompt Injection e LLM09 Misinformation visti al rovescio: non sto cercando di difendere l'LLM dal contenuto, sto alimentando l'LLM di contesto affidabile per ottenere una valutazione affidabile.
Se vuoi vedere come affronto la security di applicazioni AI dal punto di vista ingegneristico - pipeline, prompt, validazione, governance - nel mio hub sulla security AI per aziende trovi articoli su prompt injection su agent, supply chain AI e red team di RAG, con filo conduttore comune: l'LLM è un componente, non un oracolo.
Il disegno della pipeline: tre stadi, due bordi espliciti
La pipeline che ho costruito passa ogni alert SIEM attraverso tre stadi. Il primo, normalization, prende il JSON dell'alert Wazuh e lo normalizza in una struttura interna con campi source_ip, source_user, event_type, severity_rule, rule_id, timestamp, raw_message. Il secondo, enrichment, interroga OpenSearch per pull-are le ultime 15 righe di log correlate (stesso IP, stesso utente, stesso host), più metadata su IP geolocalizzato e reputazione (via una tabella locale di Internet scan noti, aggiornata da AbuseIPDB). Il terzo, classification, chiama Claude Haiku 4.5 con un prompt strutturato e riceve una classificazione con confidence.
Ogni stadio espone un bordo esplicito: fra stadio 1 e 2, un evento normalizzato in JSON; fra stadio 2 e 3, un enrichment bundle con alert + contesto; dopo lo stadio 3, un verdict con severity (ignored | watch | alert | critical) e reasoning testuale. I bordi espliciti permettono di sostituire qualunque stadio senza toccare gli altri - domani posso sostituire Claude Haiku con un modello self-hosted Llama 3 70B senza modificare l'enrichment, o sostituire Wazuh con Elastic Security senza cambiare il classifier. Questa disciplina è la stessa che applico nella containerizzazione LLM self-hosted su VPS con GPU: isolare i componenti dietro contratti di schema rende il sistema riconfigurabile senza riscriverlo.
Perché Haiku 4.5 invece di Sonnet 4.6 per questa classificazione?
Su 4.200 alert al giorno, la differenza di costo fra Claude Sonnet 4.6 e Claude Haiku 4.5 è significativa. Una classificazione con prompt da 1.800 token e output atteso di 150 token costa circa 0,012 dollari con Sonnet 4.6 e circa 0,0018 dollari con Haiku 4.5 - un fattore quasi sette. A 4.200 alert al giorno, Sonnet costerebbe 50 dollari al giorno, Haiku 7,6 dollari. Su scala mensile, la differenza è 1.500 vs 228 dollari.
La domanda rilevante è: Haiku è abbastanza intelligente per il compito? Ho misurato il tasso di accuracy dei due modelli sullo stesso test set di 500 alert annotati manualmente da me come true positive, false positive, benign anomaly. Haiku 4.5 ha ottenuto: precision 88%, recall 94%, F1 91%. Sonnet 4.6 ha ottenuto: precision 92%, recall 95%, F1 93%. La differenza di accuracy è di 2-3 punti percentuali - esiste, ma è il 30% del costo. Per un uso di prefiltering dove l'umano deve comunque guardare gli alert che superano la soglia, il trade-off favorisce Haiku. Su alert critical dove il costo di un falso negativo è alto, la pipeline escalate automaticamente la query a Sonnet per seconda opinione - best of both.
def classify(alert_bundle: dict) -> Verdict:
primary = claude_client.messages.create(
model="claude-haiku-4-5",
system=CLASSIFIER_SYSTEM_PROMPT,
messages=[{"role": "user", "content": format_bundle(alert_bundle)}],
max_tokens=400,
)
verdict = parse_verdict(primary)
if verdict.severity == "critical" and verdict.confidence < 0.85:
# escalation a sonnet per seconda opinione
secondary = claude_client.messages.create(
model="claude-sonnet-4-6",
system=CLASSIFIER_SYSTEM_PROMPT_WITH_REVIEW,
messages=[{"role": "user", "content": format_bundle_with_haiku_verdict(alert_bundle, verdict)}],
max_tokens=500,
)
verdict = parse_verdict(secondary)
return verdictL'escalation condizionale tiene i costi stabili intorno a 230 dollari al mese mentre mi garantisce che gli alert critical vengano comunque valutati dal modello migliore. Su 4.200 alert al giorno, l'escalation scatta in media 28 volte - assolutamente gestibile.
Il prompt specializzato: quattro vincoli e un formato strutturato
Il system prompt del classifier è specializzato per il dominio SOC. È passato da 340 a 890 parole nel corso di otto settimane di tuning, ogni modifica motivata da uno specifico tipo di errore osservato. I quattro vincoli principali sono questi.
Vincolo uno: tassonomia fissa. Il modello deve classificare ogni evento in una delle quattro categorie ignored, watch, alert, critical, con definizioni esplicite di ciascuna. Senza tassonomia fissa, le classificazioni diventano libere e inconfrontabili. Vincolo due: reasoning obbligatorio in 60-120 parole. Il modello deve spiegare perché ha scelto quella categoria. Questa prosa non è per soddisfare il mio ego - è il campo che usa l'operatore umano quando guarda un alert che il LLM ha classificato come alert o critical e deve decidere se è davvero un incidente. Senza reasoning, l'operatore non può verificare il giudizio del modello. Vincolo tre: confidence esplicita. Ogni verdict include un numero da 0 a 1. Confidence sotto 0,6 attiva automaticamente l'escalation a Sonnet. Vincolo quattro: lista dei "non so". Il modello deve esplicitare cosa gli è mancato per una valutazione più accurata - "non ho informazioni sul profilo orario dell'utente", "non conosco la reputazione dell'IP esterno". Questo campo guida l'iterazione sull'enrichment: se un "non so" specifico emerge ripetutamente, aggiungo quel contesto allo stadio 2.
SYSTEM PROMPT (estratto):
Sei un analista SOC senior. Classifica ogni evento in una delle categorie:
- ignored: l'evento è coerente con il comportamento atteso (es. scan Internet routine, health check)
- watch: l'evento è lievemente anomalo ma non indica compromesso. Non svegliare nessuno. Logga e basta.
- alert: l'evento è chiaramente anomalo e merita attenzione umana entro l'ora.
- critical: l'evento è un incidente di sicurezza in corso. Ping immediato 24/7.
Formato OBBLIGATORIO di output (JSON):
{
"severity": "ignored" | "watch" | "alert" | "critical",
"confidence": <0.0-1.0>,
"reasoning": "<60-120 parole, spiegando il ragionamento>",
"missing_context": ["<lista dei pezzi di contesto che ti avrebbero aiutato>"],
"suggested_investigation": "<se severity >= alert, cosa deve fare l'operatore>"
}
NON inventare dettagli non presenti nell'input. Se il contesto manca, dillo in missing_context.I numeri dopo quattro settimane: riduzione 96% dei falsi positivi
Dopo quattro settimane di operazione con pipeline a regime, i numeri sono questi. Input quotidiano medio: 4.200 alert SIEM raw. Output della classificazione: 3.960 classificati ignored (94,3%), 178 classificati watch (4,2%), 52 classificati alert (1,2%), 10 classificati critical (0,23%). Gli alert che raggiungono Telegram come notifica con @here sono solo i 62 cumulati fra alert e critical - 1,5% del volume originale. Gli operatori (nel mio caso io, in produzione sarebbe un team) passano da 4.200 a 62 alert al giorno su cui ragionare - un ordine di grandezza sotto.
L'accuratezza misurata sul ground truth della sandbox (dove so quali alert erano injection malevole vs rumore): sui 62 alert quotidiani, circa 11 sono true positive (threat reale), 4 sono benign anomaly (attività anomala ma legittima), 47 sono false positive del classifier - ma falsi positivi già filtrati fino a quel punto, che meritano una guardata dall'umano perché sono al bordo della decisione. La precision del layer di classifier sui critical è 92% (9 su 10 critical sono veri incidenti), la recall sui true positive è 89% (11 su 12 veri attacchi raggiungono l'operatore - 1 sfugge nel rumore). Il rate di falsi negativi scesi dal 83% al 11% è il guadagno strategico vero. L'operatore è passato da "ignora tutto perché è rumore" a "guarda 62 cose al giorno e agisci".
Il loop di feedback: tuning settimanale con reclassificazione
Ogni venerdì dedico un'ora a rivedere un campione di 50 alert classificati dalla pipeline durante la settimana. Per ogni alert confermato come errore di classificazione (falso positivo che avrebbe dovuto essere ignored, falso negativo che avrebbe dovuto essere alert), aggiungo l'alert come esempio few-shot al system prompt del classifier con la classificazione corretta e un breve commento del perché. Il prompt è passato da includere 3 esempi iniziali a includerne 22 dopo otto settimane. La crescita del prompt aumenta il costo per chiamata (ora 1.950 token medi di input invece di 1.800) ma migliora il F1 del 3-4 punti - un trade-off che ho scelto perché il costo marginale era basso rispetto al budget complessivo.
Lo stesso loop di feedback è quello che descrivo per il bot di code review LLM per GitHub Actions: senza un ciclo iterativo di correzione sulla base dei risultati osservati, il sistema degrada nel tempo mentre il mondo esterno cambia sotto di te. Un SIEM LLM che non impara dai propri errori è un sistema che dopo sei mesi ha la stessa accuracy del giorno del deploy - nella migliore delle ipotesi.
L'integrazione con Telegram e la disciplina di alerting
L'alerting finale non va via email, non va via ticket in Jira, non va via dashboard che nessuno guarda. Va via Telegram a un canale dedicato #siem-alerts con il mio account operatore. La scelta di Telegram non è dogmatica - la chiave è che sia un canale che viene consultato con frequenza di minuti, non ore. Il formato del messaggio è strutturato: titolo con severity + rule_id + timestamp, estratto del reasoning del classifier, link deep-link a OpenSearch con la query per vedere il contesto completo, bottone inline per marcare l'alert come "confermato attacco", "falso positivo", "indagine richiesta".
def send_telegram_alert(verdict: Verdict, bundle: dict) -> None:
title = f"[{verdict.severity.upper()}] {bundle['rule_id']} @ {bundle['timestamp']}"
body = (
f"{title}\n\n"
f"*Reasoning:* {verdict.reasoning}\n\n"
f"*Suggested investigation:* {verdict.suggested_investigation}\n\n"
f"*Source:* {bundle['source_ip']} (user {bundle['source_user'] or '-'})\n"
)
if verdict.severity == "critical":
body = f"@here\n{body}"
telegram.send_message(
chat_id=ALERT_CHANNEL_ID,
text=body,
parse_mode="Markdown",
reply_markup=build_feedback_buttons(verdict.id),
)Il @here sugli alert critical fa notificare attivamente tutti i membri del canale - in produzione con team di 3-5 persone è la differenza fra un incidente gestito in minuti e uno gestito la mattina dopo. Il bottone inline di feedback è il meccanismo con cui l'operatore marca gli errori di classificazione che poi alimentano il loop di tuning settimanale descritto sopra.
Quando un LLM come pre-filter SIEM non si giustifica
Se hai meno di 100 alert SIEM al giorno, l'overhead dell'LLM non si giustifica: il tuo analista SOC li guarda uno per uno in 30 minuti. Se i tuoi alert sono già altamente correlati da regole SIEM mature (Wazuh o Splunk con tuning di mesi), la riduzione marginale che il LLM può aggiungere è modesta. Se non hai un ground truth per misurare l'accuracy del classifier, la pipeline è un atto di fede - e il giorno in cui un vero incidente sfugge non saprai dimostrare di aver fatto due diligence. Se regulation richiede audit trail completo dei ragionamenti di ogni decisione di alert (settori finanziari, sanitari regolati), l'output probabilistico dell'LLM richiede un'ulteriore layer di supervisione e storage che può annullare il vantaggio operativo.
Il pattern SIEM + LLM-as-classifier si giustifica quando hai contemporaneamente: volume di alert quotidiano sopra il migliaio con alto rapporto di falso positivo, team SOC piccolo (1-5 persone) che non può fisicamente guardare tutto, budget di 200-500 dollari mensili per l'AI, e willingness culturale a un sistema probabilistico nel loop di sicurezza. Senza la willingness culturale, il primo falso negativo critico produce una crisi organizzativa che seppellisce il progetto - quindi va negoziato prima di deployare, non dopo.
Un SOC moderno che tiene il 95% del volume alert fuori dalla coda umana e porta il rimanente 5% con reasoning esplicito e contesto arricchito non è fantascienza AI - è ingegneria disciplinata di pipeline che già esisteva in forma embrionale da quando i SIEM esistono, riportata nel 2026 da LLM abbastanza capaci da fare la valutazione contestuale che regole deterministiche non facevano. Il rischio principale di questo pattern non è tecnico: è organizzativo. Se il team SOC smette di fidarsi del filtro LLM (perché ha fallito spettacolarmente una volta, perché non capisce il suo reasoning, perché i costi crescono in modo inspiegato), il sistema diventa un altro dashboard ignorato insieme ai 10 che già hanno. La disciplina che tiene vivo questo tipo di deploy è esattamente quella di un qualunque sistema operativo critico: osservabilità, feedback loop, revisione periodica, escalation procedure chiara. LLM inclusi o esclusi, le pratiche SOC che funzionano non cambiano.
Se stai valutando l'inserimento di un LLM nel tuo flusso di triage SIEM per ridurre il carico sul team di sicurezza e vuoi capire se il pattern è adatto al tuo volume e al tuo stack, il modulo di preventivo gratuito ti dà una prima lettura in 7 domande, 2 minuti. Ti dico se il tuo progetto rientra nelle cose che so fare bene e, se il caso richiede un profilo diverso, te lo dico e ti indico una direzione utile quando posso.