Leggere centinaia di righe di warning dopo un'anomalia è lento e ripetitivo. Un modello linguistico può aiutare in una parte precisa del lavoro: riassumere, raggruppare per causa probabile e dare una priorità indicativa. Non risolve i problemi e non sostituisce chi amministra il sistema. La guida descrive una pipeline minimale in cui i log di un host Linux vengono estratti, ripuliti dai dati sensibili e passati a un modello eseguito in locale.
Il caso d'uso e i suoi confini
Il compito è la sintesi: dato un estratto di journalctl, ottenere un elenco di gruppi di eventi con una gravità stimata e una verifica suggerita. Il risultato è un testo da leggere, non un'azione da eseguire. Questo confine decide tutto il resto: finché l'output è un suggerimento, un errore del modello costa qualche minuto di verifica; se diventasse un comando eseguito in automatico, costerebbe molto di più.
Perché in locale
I log contengono indirizzi IP, nomi utente, nomi host, percorsi e, per errore, a volte parametri di comando o credenziali stampate da un servizio. Inviarli a un servizio esterno significa trasferire a terzi informazioni sulla propria infrastruttura, con le implicazioni di riservatezza e conformità che ne derivano. Un modello eseguito in locale mantiene i dati nella rete e rende noti versione, configurazione e costo. Il prezzo è la qualità: un modello generico open-weight di piccole dimensioni è meno capace dei modelli ospitati più grandi, e conviene verificare che la sua licenza sia compatibile con l'uso previsto.
Architettura
journalctl -> filtro e pulizia -> mascheramento -> modello locale -> riassunto -> revisione umanaIl modello è servito da un runtime come Ollama, che per impostazione predefinita espone un'API HTTP sull'interfaccia locale (porta 11434). Runtime e modello si installano seguendo la documentazione del progetto; con Ollama il modello si scarica con ollama pull <NOME_MODELLO>. Altri runtime, come il server di llama.cpp, offrono interfacce simili. L'API non prevede autenticazione: deve restare raggiungibile solo dalla macchina che la usa, oppure da una rete di gestione ben delimitata, come descritto in segmentazione della rete con VLAN. Non va esposta a Internet.
Estrazione e mascheramento
L'estrazione si limita alle righe che contano: priorità warning o superiore, ultima ora.
journalctl -p warning --since "1 hour ago" -o short-iso --no-pagerPrima di arrivare al modello, il testo passa da uno script che sostituisce i dati sensibili con segnaposto. Lo stesso valore riceve sempre lo stesso segnaposto (<IP_1>, <UTENTE_2>), così il modello può ancora notare che un indirizzo compare molte volte senza conoscerlo.
import re
import sys
SEGRETI = re.compile(r"(?i)\b(pass(?:word|wd)?|token|secret|api[_-]?key)=\S+")
PATTERNS = {
"MAC": re.compile(r"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b"),
"IP": re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"),
"EMAIL": re.compile(r"\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b"),
"UTENTE": re.compile(
r"(?<=user )[\w.-]+|(?<=user=)[\w.-]+|(?<=for )[\w.-]+(?= from)", re.IGNORECASE
),
"TOKEN": re.compile(r"\b[A-Za-z0-9_-]{32,}\b"),
}
def maschera(testo: str) -> str:
mappa = {}
def sostituzione(tipo):
def _sub(m):
chiave = (tipo, m.group(0))
if chiave not in mappa:
numero = sum(1 for t, _ in mappa if t == tipo) + 1
mappa[chiave] = f"<{tipo}_{numero}>"
return mappa[chiave]
return _sub
testo = SEGRETI.sub(r"\1=<SEGRETO>", testo)
for tipo, regex in PATTERNS.items():
testo = regex.sub(sostituzione(tipo), testo)
return testo
if __name__ == "__main__":
sys.stdout.write(maschera(sys.stdin.read()))Importante
Il mascheramento è prudente, non perfetto: produce falsi positivi (per esempio numeri di versione a quattro cifre) e non copre IPv6, nomi host o percorsi che identificano un cliente. Le regole vanno completate sull'ambiente reale e provate su log di campione prima dell'uso. Il mascheramento resta utile anche con un modello locale, perché riduce l'impatto di cronologie, file temporanei e invii a un endpoint sbagliato.
Chiamata al modello locale
L'API di Ollama accetta una richiesta JSON; con stream: false la risposta arriva in un'unica volta nel campo response. L'esempio usa solo la libreria standard e legge il nome del modello da una variabile d'ambiente, senza fissarne la versione.
import json
import os
import sys
import urllib.request
URL = "http://127.0.0.1:11434/api/generate"
MODELLO = os.environ["MODELLO_LOG"]
def main() -> None:
righe = sys.stdin.read().splitlines()
log = "\n".join(f"{n}: {riga}" for n, riga in enumerate(righe, start=1))
with open("prompt.txt", encoding="utf-8") as f:
prompt = f.read().replace("{LOG}", log)
corpo = json.dumps(
{"model": MODELLO, "prompt": prompt, "stream": False, "options": {"temperature": 0}}
).encode("utf-8")
richiesta = urllib.request.Request(URL, data=corpo, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(richiesta, timeout=600) as risposta:
print(json.load(risposta)["response"])
if __name__ == "__main__":
main()export MODELLO_LOG="<NOME_MODELLO>"
journalctl -p warning --since "1 hour ago" -o short-iso --no-pager \
| tail -n 300 \
| python3 maschera.py \
| python3 riassumi.py > riepilogo.mdtemperature: 0 riduce la variabilità tra esecuzioni, senza garantire risposte identiche. Il limite di 300 righe va tarato sul contesto del modello.
Nota
La finestra di contesto predefinita di un runtime può essere molto più piccola di quella supportata dal modello, e alcuni runtime troncano senza errore il testo in eccesso. Il parametro num_ctx si imposta nelle opzioni della richiesta, ma resta un tetto: l'estrazione deve filtrare, deduplicare e limitare le righe prima dell'invio.
Il prompt
Un prompt efficace è esplicito su ruolo, vincoli e formato di output. Le righe numerate servono a chiedere al modello di citare le prove.
Sei un assistente per l'amministrazione di sistemi Linux. Ricevi righe di log
numerate e già mascherate: i segnaposto come <IP_1> o <UTENTE_1> sono voluti.
Compiti:
1. Raggruppa le righe per causa probabile.
2. Per ogni gruppo indica gravità (bassa, media, alta) e un riepilogo di una riga.
3. Segnala schemi sospetti, come tentativi ripetuti o servizi che si riavviano in ciclo.
Regole:
- Usa solo le informazioni presenti nel log. Se non bastano, scrivi "non determinabile dal log".
- Non inventare nomi di file, comandi, indirizzi o versioni.
- Non proporre comandi distruttivi. Descrivi le verifiche come suggerimenti.
- Per ogni gruppo cita i numeri delle righe a supporto.
Formato: elenco Markdown, un gruppo per voce, con i campi
Gravità, Riepilogo, Righe, Verifica suggerita.
Log:
{LOG}Suggerimento
Richiedere i numeri di riga trasforma ogni affermazione in qualcosa di controllabile: chi legge il riepilogo può aprire le righe citate e verificare in pochi secondi se il gruppo esiste davvero.
Limiti onesti
- Allucinazioni. Il modello può attribuire una causa plausibile ma non presente nei log. Il prompt riduce il rischio, non lo elimina.
- Contesto limitato. Un estratto troppo lungo viene ignorato o troncato: l'analisi vale solo per ciò che il modello vede.
- Nessun rimpiazzo degli alert. Il riassunto affianca le regole deterministiche, come quelle descritte in monitoraggio con Prometheus e Grafana, e non le sostituisce.
Attenzione
I log possono contenere testo scelto da chi attacca: un nome utente di login SSH, un'intestazione HTTP o un messaggio applicativo con finte istruzioni per il modello. Per questo l'output non va mai collegato a un'esecuzione automatica di comandi, tanto meno con privilegi: resta un suggerimento che una persona legge e verifica. Le fonti di log più esposte, come quelle di sshd trattate in hardening di SSH su Debian, sono proprio quelle in cui un attaccante può inserire testo arbitrario.
Valutare i risultati
Il confronto si fa sui propri log, non su numeri generici. Si raccoglie un piccolo insieme di estratti, già mascherati, e per ciascuno una persona scrive i gruppi attesi. Poi si confrontano le risposte del modello: gruppi corretti, gruppi inventati, eventi importanti omessi, riepiloghi non sostenuti dalle righe citate. Lo stesso insieme serve a confrontare modelli e prompt diversi e a ripetere la prova dopo ogni cambiamento. Il criterio finale è pratico: il riepilogo deve far risparmiare tempo senza introdurre errori che costringano a rileggere tutto il log.
Hardware indicativo
Un modello di piccole dimensioni, in versione quantizzata, può girare anche solo su CPU, con tempi di risposta più lunghi. Una GPU con memoria sufficiente accelera in modo sensibile, ma in una VM richiede il passthrough e quindi più lavoro di configurazione. La memoria necessaria cresce con le dimensioni del modello e con il contesto richiesto. Un'analisi oraria, eseguita da un timer, non richiede bassa latenza: spesso conviene partire dal modello più piccolo che dia risultati utili e crescere solo se serve.