Dalla domanda investigativa alla query
Una sessione di advanced hunting non comincia dal KQL: comincia da una frase in italiano, tipo «quali macchine hanno eseguito questo binario nelle ultime 24 ore?». Tradurla richiede tre decisioni. La tabella: gli eventi di processo stanno in DeviceProcessEvents, le connessioni in DeviceNetworkEvents, le scritture di file in DeviceFileEvents, gli accessi alla macchina in DeviceLogonEvents, la posta in EmailEvents e EmailAttachmentInfo, i click sugli URL in UrlClickEvents, gli alert e le loro evidenze in AlertInfo e AlertEvidence. In Microsoft Sentinel il ragionamento è identico ma il vocabolario cambia: SecurityEvent, SigninLogs, AuditLogs, AzureActivity, CommonSecurityLog, SecurityAlert. Poi la finestra temporale e infine il fatto da estrarre — qui il nome della macchina.
Il modello mentale giusto è l’imbuto: si parte dalla tabella e ogni pipe successivo deve buttare via dati, non aggiungerne. Una query di caccia che restituisce diecimila righe non ha risposto a niente: ha solo spostato il lavoro sull’analista.
Gli operatori che servono davvero
where filtra le righe ed è l’operatore che decide le prestazioni. Va messo il prima possibile, e il primo filtro dovrebbe essere quello temporale, perché il tempo è la dimensione su cui i dati sono organizzati. Sul testo conta anche quale predicato si sceglie: has cerca termini interi ed è più efficiente di contains, che scorre la sottostringa; un confronto esatto su un nome file è più selettivo di entrambi.
project sceglie le colonne. Non è cosmesi: una riga di DeviceProcessEvents ha decine di campi, mentre una query leggibile ne mostra cinque — timestamp, device, account, processo, riga di comando. Proiettare presto riduce anche i dati trasportati nei passaggi successivi.
summarize è dove la caccia diventa analisi. count() conta le righe, dcount() conta i valori distinti (con una stima approssimata, non un conteggio esatto): la differenza è sostanziale, perché «quante esecuzioni» e «su quante macchine distinte» sono domande diverse e l’esame le contrappone volentieri. La clausola by definisce i gruppi, e dopo un summarize esistono solo le colonne prodotte dall’aggregazione.
order by (sinonimo sort by) ordina il risultato; top N by fa ordinamento e troncamento in un colpo solo ed è ciò che si usa per portare il segnale in cima. take (o limit) restituisce righe qualsiasi, non le prime: serve per sbirciare la forma dei dati, mai per costruire una classifica.
Tempo e correlazione
ago() esprime una finestra relativa a “adesso” e va confrontato con la colonna temporale della tabella (Timestamp in Microsoft Defender XDR, TimeGenerated nelle tabelle di Sentinel). bin() arrotonda i valori in intervalli regolari: combinato con summarize ... by bin(Timestamp, 1h) produce una serie temporale, cioè la forma con cui si riconosce un beaconing o un picco di logon falliti che una media giornaliera nasconderebbe.
join correla due tabelle su una chiave comune — per esempio processi ed eventi di rete sullo stesso device nella stessa finestra. È l’operatore più costoso: si filtrano entrambi i lati prima del join, si sceglie il tipo (inner, leftouter) in modo esplicito e si mette a sinistra la tabella più piccola. Quando i due insiemi vengono dalla stessa tabella, spesso un summarize con più aggregazioni evita del tutto il join.
Leggere una query scritta da altri
Si legge dall’alto verso il basso, un pipe alla volta, chiedendosi a ogni passaggio «quante righe restano e con quali colonne». Prima riga: quale telemetria. Primo where: quale perimetro. summarize: qual è l’unità di analisi. top/order: qual è il criterio di sospetto. Se una query non si spiega così, di solito è perché mescola filtro e aggregazione nell’ordine sbagliato.
Ultimo passo concettuale: una hunting query resta un’interrogazione manuale e non genera alcun alert. Per farla vivere da sola serve trasformarla in una custom detection rule in Defender XDR — che pretende dalla query colonne specifiche, fra cui Timestamp, ReportId e una colonna di entità — oppure in una analytics rule in Sentinel.
Trappole tipiche d’esame
- Serve rilevare automaticamente il pattern trovato in hunting → custom detection rule (o analytics rule), non la query salvata: la hunting query non produce alert né incident, per quanto sia precisa. Se la domanda dice “notify” o “trigger an incident”, la risposta non è “save the query”.
- La custom detection non si crea e la query sembra corretta → mancano le colonne obbligatorie: una query che proietta solo device e nome processo non basta; servono anche
Timestamp,ReportIde una colonna di entità utilizzabile per l’impatto. - “Su quanti dispositivi distinti” →
dcount(), noncount():count()gonfia il risultato contando ogni esecuzione. E ricorda chedcount()è una stima, quindi non è l’operatore da citare quando la domanda chiede un valore esatto. - “Le prime 10 macchine più colpite” →
top ... by, nontake 10:takerestituisce righe arbitrarie senza ordinare, quindi non risponde a nessuna domanda che contenga “most”, “highest” o “top”. - Query lenta o in timeout → filtro temporale e
whereprima deljoin, non un intervallo più corto a caso: filtrare tardi (o dopo il join) è la causa più comune; anchecontainsal posto dihassu volumi grandi si paga. - Andamento orario dell’attività →
summarize by bin(Timestamp, 1h), nonorder by Timestamp: ordinare mostra gli eventi, non la loro distribuzione; senzabin()non esiste serie temporale su cui vedere il picco.