AI per l'analisi di log di sicurezza: costruire un pipeline di alerting intelligente

AI per l'analisi di log di sicurezza: costruire un pipeline di alerting intelligente

A settembre 2025 ho passato due settimane a rispondere a un totale di 147 alert di sicurezza generati da un SIEM open source che un'azienda del settore servizi digitali con un centinaio di VPS Hetzner e OVH aveva installato l'anno prima. Di quei 147 alert, 143 erano falsi positivi: scansioni legittime del team di security, bot di indicizzazione dei motori di ricerca, richieste di uptime-check configurate male, endpoint di health check scambiati per path traversal, richieste Cloudflare di revalidation cache interpretate come anomalie. Tre erano veri ma già mitigati dalle regole WAF (scansioni Nikto bloccate, tentativi di SQL injection respinti da un filtro applicativo). Uno, l'ultimo della lista, era un attacco reale: un tentativo di brute force su un endpoint di login esposto di uno dei VPS di staging, che aveva raggiunto 1.200 richieste in sei minuti prima che un fail2ban correttamente configurato lo fermasse. Il costo operativo di quella pipeline di alert non era teorico: il tecnico di riferimento del cliente spendeva circa due ore al giorno a triagiare le notifiche, e nella pratica aveva sviluppato l'istinto opposto a quello che un SIEM dovrebbe costruire - ignorare le notifiche per default, perché statisticamente erano rumore. Il giorno in cui l'unico vero attacco è arrivato, nessuno lo stava guardando per primo: il blocco ce lo ha messo fail2ban, non il SIEM.

Il problema di quella configurazione non era il SIEM in sé - era il modello di alerting basato su regole statiche. Ogni "anomalia" corrispondeva a un pattern regex o a una soglia aritmetica su un campo del log; bastava che un utente legittimo si comportasse in modo leggermente diverso dalla norma, o che un crawler rispettabile iterasse cento URL consecutivi, per far scattare un alert. In quattro settimane di lavoro ho riprogettato l'intera pipeline di analisi dei log sostituendo il motore a regole con un orchestratore che invia finestre di log (Nginx, PHP-FPM, MySQL, auth.log) a un LLM - Claude 3.5 Sonnet via API, poi Claude 4.6 Sonnet quando è diventato disponibile - che valuta se la finestra contiene segnali reali di compromissione o è rumore legittimo. L'output del modello è strutturato come JSON con severity, descrizione tecnica, IoC estratti e raccomandazione operativa, e solo le finestre che il modello classifica come severity high o superiore generano notifica su Slack al canale #ops-security. Il risultato al sesto mese: 2.847 finestre di log analizzate, 23 alert generati, 21 dei 23 confermati come incidenti reali in fase di review (tasso di falsi positivi sotto il 10%, contro il 97% precedente), zero incidenti reali persi.

Questo articolo è il distillato tecnico di come ho progettato quella pipeline - l'architettura, i prompt che funzionano per l'analisi di log di sicurezza, i guardrail che impediscono al modello di fare allucinazioni e le metriche operative che uso per verificare che il sistema continui a comportarsi correttamente nel tempo. Non è un sostituto di un SIEM enterprise serio in contesti dove il SIEM è davvero giustificato (compliance stringente, forensics avanzate, correlazione su dataset molto grandi); è l'approccio pragmatico che funziona per le PMI, dove il trade-off fra copertura teorica e usabilità operativa va sempre calibrato dalla parte dell'usabilità.

Cosa rende un LLM più efficace di un motore a regole per l'analisi dei log di sicurezza?

La domanda che tutti i CIO di PMI mi fanno quando presento questa architettura è esattamente quella, ed è giusto darle una risposta diretta senza magia AI. La differenza non è che l'LLM sia "più intelligente" in senso astratto - la differenza è che un motore a regole classifica una singola riga di log contro un pattern predefinito, mentre un LLM può classificare una finestra di log contestualizzando le singole righe fra loro e contro una conoscenza generale di cosa significhi "traffico web normale" accumulata durante l'addestramento. La prima è un'operazione di pattern matching; la seconda è un'operazione di giudizio contestuale.

Il caso pratico più chiaro è quello del crawler benigno che scansiona un sito in modo aggressivo. Per un motore a regole, "200 richieste da un singolo IP in 10 minuti" è un pattern sospetto che fa scattare l'alert. Per un LLM che ha accesso alle 200 righe di log, è immediato vedere che lo user-agent è Googlebot/2.1, che le URL richieste seguono il pattern semantico della sitemap dell'applicazione, che i codici di risposta sono tutti 200 o 304 (Not Modified), e che il ritmo è regolare (una richiesta ogni 3 secondi) invece che a burst. Il giudizio conclusivo del modello è "crawler legittimo che rispetta il robots.txt", e non genera alert. La stessa finestra con 200 richieste da uno user-agent Mozilla/5.0 Zgrab, URL che iterano path di pannelli di amministrazione noti (/admin, /phpmyadmin, /wp-admin, /.git/config, /.env), codici di risposta 404 prevalenti, e ritmo irregolare con burst ogni 0,2 secondi, viene classificata come "scan di reconnaissance offensivo" con severity high. Nessuna delle due classificazioni richiede regole scritte a mano dall'amministratore: emergono dalla lettura contestuale della finestra.

Il secondo vantaggio, meno ovvio ma altrettanto importante, è la capacità dell'LLM di estrarre spiegazioni oltre alle classificazioni. Quando un alert arriva su Slack, non è "anomalia rilevata, regola #42" (che costringe l'ops a fare archeologia nei log per capire cosa è successo); è "tentativo di sfruttamento di CVE-2023-34362 (MOVEit Transfer SQL injection) dal host 45.XXX.XXX.XXX, 14 richieste in 3 minuti verso /moveitisapi/moveitisapi.dll, tutte respinte con 404 dal tuo Nginx perché l'applicazione non espone quell'endpoint, ma l'attaccante ha già iniziato a iterare su altri pattern di vulnerabilità note - probabile scansione automatica opportunistica, severity medium, azione suggerita: aggiungere il subnet all'abuse-list di fail2ban". L'operatore che riceve quella notifica ha già in una singola frase la diagnostica, la conferma che l'attacco è stato respinto, l'ipotesi di intento dell'attaccante e la remediation operativa. Il tempo di triage crolla.

Architettura della pipeline: come si incastrano i pezzi

La pipeline che ho costruito per il cliente dell'incipit, e poi replicato con varianti su altre quattro PMI, ha cinque componenti orchestrati da systemd timer e un pezzo di bash minimalista. Niente microservizi, niente Kubernetes, niente broker di messaggi. Un orchestratore deliberatamente semplice che qualunque ops con competenze bash standard può manutenere.

Il primo componente è il log collector: un processo che legge i log di Nginx, PHP-FPM, MySQL slow query log e auth.log dagli ultimi 15 minuti, li normalizza in JSONL, e li raggruppa per "finestra di analisi" (tipicamente gli ultimi 15 minuti di log di tutti i servizi su un singolo host). Il secondo è il filtro di rumore deterministico: un passaggio iniziale che scarta le righe chiaramente irrilevanti (richieste 200 verso asset statici con cache hit, health check interni da IP di monitoring noti, log di restart programmati dei servizi) prima di far girare l'LLM. Questo filtro riduce il volume di token inviati al modello di un fattore 10-20x e abbatte i costi di inferenza senza perdere segnale. Il terzo è il classificatore LLM: la chiamata all'API Claude con il prompt strutturato. Il quarto è il router delle risposte: legge l'output JSON del modello, lo valida contro uno schema, e in base alla severity smista l'alert su canali diversi (#ops-monitoring per info, #ops-security per medium/high, PagerDuty per critical). Il quinto è lo storage delle decisioni: ogni finestra analizzata, con relativo output del modello, viene archiviata in PostgreSQL per due scopi - audit trail di compliance e dataset per calibrazione futura del prompt.

Il flusso operativo reale, dal log raw all'alert su Slack, è questo:

#!/usr/bin/env bash
# /opt/logsec/run-window.sh - orchestratore della finestra di analisi
set -euo pipefail
HOST="$(hostname -s)"
NOW=$(date -u +%Y%m%dT%H%M%SZ)
WINDOW_DIR="/var/lib/logsec/windows/${HOST}-${NOW}"
mkdir -p "$WINDOW_DIR"

# 1. raccolta log ultimi 15 minuti
journalctl -u nginx --since '15 minutes ago' > "$WINDOW_DIR/nginx.log"
journalctl -u php8.2-fpm --since '15 minutes ago' > "$WINDOW_DIR/fpm.log"
tail -n 2000 /var/log/mysql/slow.log > "$WINDOW_DIR/slow.log" 2>/dev/null || true
tail -n 500 /var/log/auth.log > "$WINDOW_DIR/auth.log"

# 2. filtro rumore deterministico
python3 /opt/logsec/noise_filter.py "$WINDOW_DIR" > "$WINDOW_DIR/filtered.jsonl"

# se il filtro ha scartato tutto, esci senza chiamare il modello
if [ ! -s "$WINDOW_DIR/filtered.jsonl" ]; then
    echo "$(date): finestra pulita, skip LLM" >> /var/log/logsec.log
    exit 0
fi

# 3. chiamata al classificatore LLM
python3 /opt/logsec/classify.py \
    --window "$WINDOW_DIR/filtered.jsonl" \
    --output "$WINDOW_DIR/verdict.json"

# 4. routing alert in base a severity
python3 /opt/logsec/route.py "$WINDOW_DIR/verdict.json"

# 5. archivio decisione
psql -U logsec -d logsec -f /opt/logsec/archive.sql \
    -v window_dir="'$WINDOW_DIR'" \
    -v verdict_file="'$WINDOW_DIR/verdict.json'"

Questo script gira ogni 15 minuti come systemd timer, e su un host tipico del cliente produce 80-100 chiamate al giorno all'API, di cui l'80% si conclude al passo 2 (filtro rumore ha scartato tutto) e non raggiunge l'LLM. Il costo API risultante, calcolato su prezzo per token di Claude Sonnet, si attesta attorno ai 12-18 euro al mese per host - ordini di grandezza inferiori al costo di un SIEM enterprise e inferiore anche al costo operativo in ore-uomo del SIEM open source che avevano prima.

Il prompt che funziona: struttura, esempi e guardrail anti-allucinazione

Il cuore della pipeline è il prompt inviato all'API. Dopo una dozzina di iterazioni su dataset reali e su attacchi simulati tramite Atomic Red Team, il progetto MITRE per la simulazione di TTP offensive, la struttura che produce i risultati più solidi è questa (estratto):

Sei un analista SOC esperto di applicazioni web PHP.
Ricevi una finestra di log di 15 minuti da un host del cliente.
Il tuo compito è classificare la finestra in una delle categorie:
  - clean: traffico legittimo, nessun indicatore di compromissione
  - info: anomalie minori non pericolose (errori 5xx intermittenti,
    rate elevati ma benigni)
  - medium: tentativi di attacco respinti dalle difese esistenti
  - high: attacchi in corso che richiedono intervento manuale
  - critical: compromissione sospetta o confermata

REGOLE OPERATIVE:
1. Non inventare IoC (IP, user-agent, URL) che non compaiono nei log.
   Se non hai evidenza diretta, non riportarla.
2. Non classificare come attacco un pattern se non hai almeno due
   indicatori convergenti (es. user-agent sospetto + URL sensibile).
3. Gli uptime monitor, Googlebot, Bingbot, Facebookexternalhit,
   Cloudflare, StatusCake sono da considerare legittimi
   salvo evidenze contrarie.
4. Se trovi un attacco, cita il CVE o la tecnica MITRE ATT&CK pertinente
   SOLO se sei altamente confidente del match.

OUTPUT: restituisci JSON con schema {
  "severity": "clean|info|medium|high|critical",
  "summary": "descrizione in una frase",
  "detail": "analisi tecnica in 3-5 frasi",
  "iocs": [{"type": "ip|ua|url|hash", "value": "..."}],
  "mitre_tactic": "TA0001|TA0002|..." o null,
  "recommendation": "azione operativa suggerita"
}

Non aggiungere testo fuori dal JSON.

Tre scelte del prompt meritano una spiegazione. Prima: la regola 1 (non inventare IoC) è il guardrail più importante contro le allucinazioni. Gli LLM hanno una tendenza a riempire i campi "utili" anche quando l'evidenza nei log è insufficiente, e un IoC inventato - un IP attribuito a un attacco quando in realtà non compare nei log originali - è catastrofico per un processo di incident response che quell'IP lo bloccherà indiscriminatamente. La regola è rinforzata in fase di review: il router (componente 4 della pipeline) fa un controllo grep sui IoC riportati e verifica che compaiano effettivamente nei log inviati al modello. Se non compaiono, l'alert viene declassato di severity e marcato come llm_hallucination_suspected per revisione manuale.

Seconda: la regola 2 (almeno due indicatori convergenti) è quella che ha ridotto drasticamente i falsi positivi. Un singolo 404 verso /admin.php non è un attacco; 404 verso /admin.php, /wp-login.php, /.env e /phpmyadmin nello spazio di tre minuti . La regola istruisce il modello a non reagire al singolo segnale ma a cercare la convergenza, che è esattamente il tipo di giudizio umano che un analista SOC svilupperebbe con l'esperienza.

Terza: la regola 4 (citare CVE/MITRE solo se altamente confidenti) contrasta la tendenza a "over-classify" tipica degli LLM non calibrati. Un modello non calibrato a ogni 404 con pattern sospetto dirà "CVE-2021-41773 (Apache path traversal)" anche se il target non è Apache. La regola forza il modello a distinguere fra "pattern generico di scansione" e "exploit specifico di una CVE", e il risultato è che quando un CVE viene citato, l'analista sa che c'è stata un'analisi del fit - non una citazione pro-forma.

Stai cercando un Consulente Informatico esperto per costruire pipeline AI per la sicurezza delle tue infrastrutture PHP, oltre le configurazioni di default dei SIEM open source? Nel mio profilo professionale trovi l'esperienza concreta su Claude API, automazione offensiva/difensiva con LLM, e integrazione di modelli nelle pipeline di incident response.

Il filtro di rumore deterministico: il vero moltiplicatore economico

Il componente che rende la pipeline economicamente sostenibile non è il modello, è il filtro a monte. Senza di esso, inviare al modello le 50.000-200.000 righe di log che un singolo VPS attivo produce in 15 minuti costerebbe cifre assurde in token. Con esso, riduciamo il payload al modello al solo sottoinsieme interessante: tipicamente 200-800 righe, con occasionali picchi a 2.000 quando c'è movimento reale.

Il filtro è scritto in Python e applica tre classi di esclusione. La prima è asset statico cache-hit: richieste GET verso .css, .js, .jpg, .png, .woff2, .ico, .svg con codice 200 o 304. Queste sono la maggior parte del traffico di qualunque sito e sono di interesse nullo per l'analisi di sicurezza. La seconda è crawler legittimo: user-agent che matchano la lista curata di Googlebot, Bingbot, Facebookexternalhit, LinkedInBot, Twitterbot, e un piccolo set di crawler SEO come AhrefsBot e SemrushBot, con IP che risolvono effettivamente ai domini canonici dei rispettivi servizi (verifica reverse DNS). La terza è infrastructure noise: health check da IP di monitoring noti (cliente-side, Cloudflare, UptimeRobot, StatusCake), OCSP staple check, riavvii programmati di servizi.

Una riga concettuale del filtro:

# noise_filter.py - pseudo-codice con logica di esclusione
def is_noise(log_line: dict) -> bool:
    # 1. asset statico con cache hit
    if log_line['path'].endswith(STATIC_EXTENSIONS) and log_line['status'] in (200, 304):
        return True
    # 2. crawler verificato (reverse DNS match)
    if is_verified_crawler(log_line['ua'], log_line['ip']):
        return True
    # 3. health check da IP noto
    if log_line['ip'] in KNOWN_MONITORING_IPS and log_line['path'].endswith('/health'):
        return True
    return False

La verifica di reverse DNS per i crawler è il passaggio che molti omettono e che ho visto sfruttare in penetration test: un attaccante che spoofa lo user-agent Googlebot/2.1 passerebbe un filtro basato solo sullo user-agent. Il reverse DNS (gethostbyaddr(ip) + gethostbyname(hostname) per verificare roundtrip) costringe l'IP a risolvere effettivamente a *.googlebot.com per essere classificato come Googlebot legittimo. Senza questo check, un attaccante che si maschera da crawler può aggirare completamente l'analisi.

Metriche di calibrazione e quando il prompt smette di funzionare

Una pipeline di questo tipo non è "installa e dimentica" - richiede monitoraggio continuo della qualità delle decisioni del modello. Le tre metriche che tengo sotto osservazione sono: tasso di falsi positivi (alert medium+ che una review umana classifica come rumore), tasso di falsi negativi (incidenti reali emersi da altri canali che la pipeline aveva classificato come clean o info), e tempo medio di risposta del modello (la latenza ha un impatto diretto sul costo e sul throughput della pipeline).

Il tasso di falsi positivi l'ho misurato con una review settimanale sistematica: ogni venerdì, un operatore tecnico del cliente o io stesso rileggiamo tutti gli alert medium+ della settimana e votiamo real o noise. Il numero totale di noise diviso il totale degli alert è la metrica. Sul cliente dell'incipit, le prime quattro settimane abbiamo visto un tasso di FP del 35%, dopo cinque iterazioni sul prompt (aggiunta di regole 2 e 3, tightening dei guardrail, espansione della lista di crawler legittimi) è sceso al 9-12% stabile. Il tasso di falsi negativi è più difficile da misurare in assoluto perché presuppone un oracolo esterno che sappia davvero cosa è successo; lo stimo iniettando periodicamente attacchi di simulazione (pattern Nikto noti, tentativi di sqlmap, scan Dirbuster) contro un honeypot monitorato dalla pipeline, e verificando che vengano classificati correttamente. La copertura sulla simulazione è stata 94% fin dall'inizio, il che è coerente con quello che mi aspetto da un modello della classe di Claude Sonnet su questa tipologia di task.

Il tempo di risposta del modello è variabile ma tipicamente fra 1,8 e 4,5 secondi per finestra di analisi. Su una pipeline che processa 80 finestre al giorno per host, significa 3-6 minuti totali di latency cumulata giornaliera - completamente accettabile per un processo batch. Non è una pipeline di alerting real-time nel senso stretto del termine, è una pipeline a latenza bassa ma non zero, calibrata su minuti non secondi. Per il 99% degli incidenti in un contesto PMI questo va più che bene; nel caso raro in cui servisse sub-second alerting, la pipeline LLM andrebbe affiancata a un WAF applicativo in tempo reale - ma quella è un'altra architettura.

Un punto critico che emerge col tempo è la drift del comportamento del modello fra versioni. Quando Anthropic rilascia una nuova versione di Claude - ad esempio il passaggio dalla Sonnet 3.5 alla 4 - il comportamento sul prompt può cambiare leggermente, e il tasso di FP può oscillare. Il mio approccio è di pinnare la versione del modello nella chiamata API (non usare claude-sonnet-latest in produzione, usare claude-sonnet-4-6-20250910 o equivalente versionato), e testare i passaggi di versione su un ambiente di staging per una settimana prima di promuoverli in produzione. Questo allineamento con la disciplina del versioning è lo stesso che applico a qualsiasi dipendenza critica, e che ho discusso in un altro approfondimento sull'osservabilità minima di applicazioni PHP legacy con logging strutturato, metriche e alert quando si tratta di mantenere stabile nel tempo un sistema di rilevamento delle anomalie.

I limiti onesti della pipeline e quando serve un vero SIEM

Questa architettura non sostituisce un SIEM enterprise serio in contesti dove l'enterprise è giustificato, e vale la pena essere onesti sui casi in cui va integrata o superata. Primo, la correlazione cross-host: l'LLM analizza una finestra per volta su un singolo host, quindi un attacco che coinvolge dieci VPS simultaneamente non viene correlato come campagna unica. Un SIEM serio con regole di correlazione fa meglio questo. Secondo, la forensics post-incidente: gli alert LLM sono ottimi per il triage ma non sostituiscono un pipeline di SIEM con conservazione strutturata di log per mesi (o anni) con query indicizzate. Se il cliente ha obblighi normativi di conservazione log e ricerca forense (banche, sanità, alcuni contesti NIS2 essenziali), la pipeline LLM va affiancata a uno stack Elasticsearch/OpenSearch o a un SIEM gestito. Terzo, la copertura di segnali non testuali: traffico di rete a livello pacchetto, telemetria endpoint EDR, DNS monitoring - tutto roba che vive fuori dai log applicativi standard e dove l'LLM non arriva.

Nei progetti del mio perimetro - PMI italiane con 10-100 VPS, obblighi NIS2 da gestire ma non in fascia "operatore essenziale critico", budget per sicurezza nell'ordine di alcune decine di migliaia di euro all'anno - la pipeline LLM copre l'85-90% dei casi d'uso di alerting a una frazione del costo operativo di un SIEM tradizionale. Il rimanente 10-15% va integrato con strumenti specifici (Crowdsec per correlazione di rete e threat feed federati, fail2ban per la risposta immediata sui pattern ovvi, piano di incident response a 72 ore per applicazioni Laravel e Symfony NIS2-ready per la componente organizzativa e normativa). Se la tua azienda genera più log di quanti riesci a leggere, e il tuo SIEM attuale ti notifica alle 3 del mattino per scan Cloudflare che non ti dovrebbero nemmeno riguardare, oppure se non hai ancora un SIEM ma sai che stai vivendo in una fascia di rischio più alta di quella che il "ho fail2ban acceso" copre, contattami per un'analisi: in una giornata mappo lo stato della tua telemetria, identifico i tre gap più probabili di copertura, e ti propongo un'architettura che usa LLM dove conveniente, motori a regole dove serve real-time, e strumenti classici dove la maturità lo richiede - calibrato sul tuo budget e sulla complessità effettiva del tuo perimetro, non su slide di vendor.

Ultima modifica: