Nejdůležitější logika KQL
Níže na stránce najdeš příklady dotazů a jejich podrobnější rozpis. Úplně dole jsou propracovanější dotazy s logickým rozpisem.
KQL dotaz se čte shora dolů. Začínáš tabulkou a postupně přidáváš filtry, výběr sloupců, počítání nebo řazení.
TableName | where ColumnName contains "keyword" | project TimeColumn, UserColumn, IPColumn | order by TimeColumn desc
Funkce where ColumnName contains "keyword" ponechá pouze řádky, ve kterých sloupec ColumnName obsahuje zadané klíčové slovo. Následně project TimeColumn, UserColumn, IPColumn odstraní všechny ostatní sloupce a ponechá pouze čas události, uživatele a IP adresu, které nás zajímají pro další analýzu. Na konci order by TimeColumn desc seřadí výsledky od nejnovější události po nejstarší. Logika uvažování je: nejprve vyfiltruji pouze relevantní události obsahující hledaný výraz, poté si zobrazím jen informace důležité pro vyšetřování a nakonec výsledky seřadím tak, aby byly nejnovější události vidět nahoře. Tento postup se často používá při vyhledávání konkrétních chybových hlášení, názvů procesů, domén, IP adres, uživatelů nebo jiných indikátorů kompromitace (IOC).
Časté názvy sloupců
| Timestamp / TimeGenerated | čas události |
| UserPrincipalName / AccountName | uživatel |
| IPAddress / RemoteIP / SourceIP | IP adresa |
| DeviceName / Computer / HostName | zařízení |
| FileName | název souboru |
| FolderPath | cesta k souboru |
| ProcessCommandLine | spuštěný příkaz |
| SHA1 / SHA256 | hash souboru |
| RemoteUrl | doména / URL |
Zjistit, jak tabulka vypadá
Vypíše pár řádků a ukáže dostupné sloupce.
TableName | take 10
Přepiš:
- TableName = název tabulky
Zobrazit jen vybrané sloupce
Skryje bordel a nechá jen sloupce, které potřebuješ.
TableName | project TimeColumn, UserColumn, IPColumn
Přepiš:
- TimeColumn = časový sloupec
- UserColumn = uživatel
- IPColumn = IP adresa
Hledání přesné hodnoty
Najde řádky, kde se sloupec přesně rovná hodnotě.
TableName | where UserColumn == "novak@firma.cz"
== znamená přesná shoda.
Hledání textu ve sloupci
Najde řádky, kde sloupec obsahuje dané slovo.
TableName | where ProcessCommandLine contains "powershell"
contains = obsahuje text.
Hledání více slov
Najde řádky, kde sloupec obsahuje alespoň jedno z uvedených slov.
TableName
| where ProcessCommandLine has_any ("powershell","cmd","wscript")has_any = obsahuje jedno nebo více slov ze seznamu.
Hodnota se nerovná
Vyfiltruje vše, co není daná hodnota. Ne vždy funguje.
TableName | where ResultType != 0
Funkce where ResultType != 0 ponechá pouze řádky, kde hodnota ve sloupci ResultType není rovna nule. Logika uvažování je: pokud hodnota 0 představuje úspěšný nebo očekávaný výsledek, chci se zaměřit pouze na chyby, selhání nebo jiné nestandardní stavy, proto všechny řádky s hodnotou 0 odfiltruji a ponechám pouze události vyžadující pozornost. Tento postup se často používá při vyhledávání neúspěšných přihlášení, chyb autentizace, zamítnutých požadavků nebo jiných událostí, kde je potřeba analyzovat pouze problémy a ignorovat běžný úspěšný provoz.
⚠️ Poznámka: význam hodnoty 0 závisí na konkrétní tabulce a zdroji logů. V mnoha Microsoft logách znamená 0 úspěch, ale vždy je vhodné ověřit dokumentaci daného datového zdroje.
Vyloučení známého šumu
Vyřadí hodnoty, které tě nezajímají.
TableName
| where AccountName !in ("SYSTEM","LOCAL SERVICE","NETWORK SERVICE")Funkce where AccountName !in ("SYSTEM","LOCAL SERVICE","NETWORK SERVICE") odstraní z výsledků všechny řádky, kde se ve sloupci AccountName nachází některý z uvedených systémových účtů. Logika uvažování je: při analýze mě obvykle zajímá aktivita skutečných uživatelů nebo servisních účtů vytvořených organizací, nikoliv běžná činnost operačního systému. Proto pomocí operátoru !in vyloučím předem známé systémové účty a zmenším množství šumu v datech. Tento postup se často používá při vyšetřování incidentů, analýze přihlášení, spuštěných procesů nebo změn v systému, kdy chceme soustředit pozornost na účty, které s větší pravděpodobností představují lidskou aktivitu nebo potenciální kompromitaci.
Začíná na text
Najde hodnoty, které začínají daným textem.
TableName | where IPAddress startswith "192.168."
startswith = začíná na.
Končí na text
Najde hodnoty, které končí daným textem.
TableName | where FileName endswith ".exe"
endswith = končí na.
Seřadit od nejnovějších
Nejnovější události budou nahoře.
TableName | order by TimeColumn desc
desc = sestupně. U času typicky nejnovější nahoře.
Seřadit od nejstarších
Nejstarší události budou nahoře.
TableName | order by TimeColumn asc
asc = vzestupně.
Počet výsledků
Spočítá, kolik řádků dotaz našel.
TableName | count
Hodí se pro rychlé ověření rozsahu.
Unikátní hodnoty
Vypíše každou hodnotu jen jednou.
TableName | distinct IPAddress
IPAddress je název sloupce. Může být třeba RemoteIP nebo SourceIP nebo src_ip….
Počet událostí podle IP
Spočítá, kolikrát se každá IP objevila.
TableName | summarize Count=count() by IPAddress | order by Count desc
summarize = seskupení. count() = počet řádků. Vytvoří sloupec Count, do kterého napíše výsledek.
Počet událostí podle uživatele
Ukáže, který uživatel má nejvíc událostí.
TableName | summarize Count=count() by UserColumn | order by Count desc
Přepiš UserColumn podle tabulky, např. UserPrincipalName.
Časový filtr – posledních 24 hodin
Ukáže jen novější události.
TableName | where TimeColumn > ago(24h)
ago(24h) = před 24 hodinami.
Časový filtr – konkrétní období
Ukáže události mezi dvěma daty.
TableName | where TimeColumn between (datetime(2026-06-01) .. datetime(2026-06-08))
between = mezi dvěma hodnotami.
KQL 201 – Analýza, statistiky a hunting
Počet unikátních hodnot
Co dělá:
Spočítá unikátní hodnoty místo všech výskytů.
TableName | summarize UniqueIPs=dcount(IPAddress)
Přepiš:
- TableName
- IPAddress
Příklad:
Kolik různých IP použil uživatel.
První a poslední výskyt
Co dělá:
Zjistí začátek a konec aktivity.
TableName
| summarize
FirstSeen=min(TimeColumn),
LastSeen=max(TimeColumn)
by UserColumnPříklad:
Od kdy do kdy útočník útočil.
Vytvoření seznamu IP
Co dělá:
Spojí více hodnot do jednoho seznamu.
TableName | summarize IPs=make_set(IPAddress) by UserColumn
Příklad:
Všechny IP adresy použité jedním uživatelem.
Počet podle více sloupců
Co dělá:
Seskupuje podle více hodnot současně.
TableName | summarize Count=count() by UserColumn, IPAddress
Příklad:
Který uživatel použil kterou IP.
Aktivita po hodinách
Co dělá:
Rozdělí události do hodinových bloků.
TableName | summarize Count=count() by bin(TimeColumn,1h)
bin() = zaokrouhlení času.
Aktivita po dnech
Co dělá:
Seskupí události podle dnů.
TableName | extend Day=startofday(TimeColumn) | summarize Count=count() by Day
startofday() = ořízne čas a nechá pouze datum.
Přidání nového sloupce
Co dělá:
Vytvoří nový sloupec z existujících dat.
TableName | extend Hour=hourofday(TimeColumn)
Příklad:
Funkce extend Hour = hourofday(TimeColumn) vytvoří nový sloupec s názvem Hour, do kterého uloží pouze hodinu z časového údaje obsaženého ve sloupci TimeColumn. Jinými slovy vezme datum a čas, například 2024-05-01 14:37:22, zahodí datum, minuty i sekundy a ponechá pouze hodinu 14. Výsledkem je nový sloupec obsahující hodnoty od 0 do 23, podle kterých lze následně filtrovat, seskupovat nebo analyzovat aktivitu v různých částech dne. Logika uvažování je: z původního časového údaje potřebuji získat pouze hodinu, proto vytvořím nový sloupec (extend) a pomocí funkce hourofday() z něj vytáhnu část reprezentující hodinu. Tento postup se často používá při hledání špiček provozu, neobvyklé aktivity v noci nebo při porovnávání chování uživatelů během pracovních a mimopracovních hodin.
Jednoduché IF
Co dělá:
Vyhodnotí podmínku.
TableName
| extend Risk=
iff(FailedLogins > 10,
"High",
"Low")Funkce extend Risk = iff(FailedLogins > 10, "High", "Low") vytvoří nový sloupec s názvem Risk a na základě podmínky do něj přiřadí hodnotu. Logika uvažování je: pokud počet neúspěšných přihlášení ve sloupci FailedLogins překročí hodnotu 10, považuji situaci za rizikovou a do sloupce Risk zapíšu hodnotu High. Pokud je počet neúspěšných přihlášení 10 nebo méně, zapíše se hodnota Low. Funkce iff() funguje podobně jako klasické if – nejprve vyhodnotí podmínku, pokud je pravdivá vrátí první hodnotu, pokud není pravdivá vrátí druhou hodnotu. Tento postup se často používá pro rychlou kategorizaci událostí podle závažnosti, například při vyhodnocování počtu failed loginů, množství přenesených dat, počtu spuštěných procesů nebo jiných indikátorů, které mohou signalizovat zvýšené riziko.
Více podmínek
Co dělá:
Náhrada za více IFů.
TableName | extend Risk= case( FailedLogins > 50,"High", FailedLogins > 10,"Medium", "Low" )
Funkce extend Risk = case(...) vytvoří nový sloupec s názvem Risk a podle zadaných podmínek do něj přiřadí úroveň rizika. Logika uvažování je: nejprve se zkontroluje, zda hodnota ve sloupci FailedLogins přesahuje 50. Pokud ano, do sloupce Riskse zapíše hodnota High. Pokud tato podmínka není splněna, pokračuje se na další podmínku a ověří se, zda počet neúspěšných přihlášení přesahuje 10. Pokud ano, zapíše se hodnota Medium. Pokud není splněna ani jedna z předchozích podmínek, použije se výchozí hodnota Low. Funkce case() funguje podobně jako řetězec několika podmínek if/else if/else a používá se v situacích, kdy potřebujeme rozdělit data do více kategorií podle různých prahových hodnot. Tento přístup je vhodný například pro klasifikaci rizika, závažnosti incidentů, objemu přenesených dat, počtu failed loginů nebo jiných metrik, které chceme rozdělit na nízkou, střední a vysokou úroveň.
Top 10 IP adres
Co dělá:
Najde nejaktivnější IP.
TableName | summarize Count=count() by IPAddress | order by Count desc | take 10
Password Spray – základ
Co dělá:
Hledá IP zkoušející více účtů.
SigninLogs | where ResultType != 0 | summarize Attempts=count(), Users=dcount(UserPrincipalName) by IPAddress | order by Attempts desc
Funkce where ResultType != 0 ponechá pouze události, které neskončily úspěchem. Následně summarize Attempts = count(), Users = dcount(UserPrincipalName) by IPAddress vytvoří hromádku pro každou IP adresu a spočítá dvě statistiky. Do sloupce Attemptsuloží celkový počet neúspěšných pokusů o přihlášení z dané IP adresy a do sloupce Users uloží počet unikátních uživatelských účtů, proti kterým byly tyto pokusy vedeny. Na konci order by Attempts desc seřadí výsledky od IP adresy s nejvyšším počtem neúspěšných přihlášení po nejnižší. Logika uvažování je: chci najít IP adresy, které generují nejvíce neúspěšných přihlášení, a zároveň zjistit, proti kolika různým účtům tato aktivita směřovala. Vysoký počet pokusů proti velkému množství uživatelů může indikovat password spraying, brute force útok nebo špatně nakonfigurovanou službu používající neplatné přihlašovací údaje. Samotný výsledek však incident nepotvrzuje a je potřeba zohlednit kontext, typ zařízení, časovou osu a běžné chování dané IP adresy.
Password Spray – časová osa
Co dělá:
Ukáže útok po hodinách.
SigninLogs | where ResultType != 0 | summarize Attempts=count() by bin(TimeGenerated,1h), IPAddress | order by TimeGenerated asc
Funkce where ResultType != 0 ponechá pouze neúspěšná nebo jinak nestandardní přihlášení. Následně summarize Attempts = count() by bin(TimeGenerated, 1h), IPAddress vytvoří hromádku pro každou kombinaci IP adresy a jedné hodiny času. Funkce count() poté spočítá, kolik neúspěšných přihlášení se v dané hodině z konkrétní IP adresy vyskytlo, a výsledek uloží do nového sloupce Attempts. Funkce bin(TimeGenerated, 1h) zaokrouhlí čas na celé hodiny, takže například události z 10:05, 10:23 a 10:57 budou zařazeny do stejné hodinové skupiny 10:00. Na konci order by TimeGenerated asc seřadí výsledky od nejstarší hodiny po nejnovější. Logika uvažování je: nechci vidět pouze celkový počet neúspěšných přihlášení z IP adresy, ale také kdy k nim docházelo. Díky tomu lze odhalit časové špičky, například situace, kdy jedna IP adresa během jedné hodiny vygeneruje desítky nebo stovky failed loginů, což může indikovat brute force útok, password spraying nebo špatně nakonfigurovanou službu používající neplatné přihlašovací údaje.
Podezřelé množství IP na jednom účtu
Co dělá:
Najde účty používající hodně IP adres.
SigninLogs
| summarize UniqueIPs=dcount(IPAddress)
by UserPrincipalName
| order by UniqueIPs desc
Podezřelé množství účtů na jedné IP
Co dělá:
Najde IP používající mnoho účtů.
SigninLogs | summarize Users=dcount(UserPrincipalName) by IPAddress | order by Users desc
První a poslední aktivita útočníka
Co dělá:
Určí dobu trvání útoku.
SigninLogs | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by IPAddress
Funkce summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by IPAddress vytvoří hromádku pro každou IP adresu a následně zjistí nejstarší a nejnovější časový záznam v této skupině. Funkce min(TimeGenerated) vyhledá první zaznamenaný výskyt aktivity dané IP adresy a uloží jej do nového sloupce FirstSeen. Funkce max(TimeGenerated) naopak vyhledá poslední zaznamenaný výskyt aktivity a uloží jej do sloupce LastSeen. Logika uvažování je: nechci vidět všechny jednotlivé události, ale potřebuji rychle zjistit, od kdy do kdy se konkrétní IP adresa v logách vyskytovala. Tento postup se často používá při vyšetřování incidentů, analýze podezřelých IP adres nebo ověřování dlouhodobé aktivity, kdy potřebujeme zjistit časové rozmezí výskytu určitého zařízení, uživatele nebo útočníka v prostředí.
Nejčastější procesy
Co dělá:
Ukáže nejčastěji spouštěné procesy.
DeviceProcessEvents | summarize Count=count() by FileName | order by Count desc
KQL CORE sumarize
TableName
= tabulka s logy
| take 10
= ukaž prvních 10 řádků
| where
= filtr
Příklad:
| where User == „novak“
| project
= zobraz jen vybrané sloupce
Příklad:
| project Timestamp, User, IPAddress
| summarize
= seskup
Příklad:
| summarize Count=count() by IPAddress
count()
= spočítej řádky
dcount()
= spočítej unikátní hodnoty
Příklad:
| summarize UniqueIPs=dcount(IPAddress)
| distinct
= vypiš unikátní hodnoty
Příklad:
| distinct IPAddress
| order by
= seřaď
asc
= od nejstaršího / nejmenšího
desc
= od nejnovějšího / největšího
Příklad:
| order by Timestamp desc
contains
= obsahuje text
Příklad:
| where ProcessCommandLine contains „powershell“
has_any
= obsahuje jedno z více slov
Příklad:
| where ProcessCommandLine has_any („powershell“,“cmd“,“wscript“)
== přesně se rovná
!=
= nerovná se
!in
= není v seznamu
Příklad:
| where AccountName !in („SYSTEM“,“ADMIN“)
ago(24h)
= posledních 24 hodin
Příklad:
| where Timestamp > ago(24h)
between
= mezi dvěma daty
Příklad:
| where Timestamp between (
datetime(2024-06-19) .. endofday(datetime(2024-06-19))
bin(timestamp,1h)
= seskup po hodinách
Příklad:
| summarize Count=count() by bin(timestamp,1h)
extend
= vytvoř nový dočasný sloupec
Příklad:
| extend Hour=hourofday(timestamp)
iff() = jednoduchý IF
Příklad:
| extend Risk=iff(FailedLogins>10,“High“,“Low“)
make_set()
= vytvoř seznam hodnot
Příklad:
| summarize IPs=make_set(IPAddress) by User
min()
= první výskyt
max()
= poslední výskyt
Příklad:
| summarize FirstSeen=min(timestamp),
LastSeen=max(timestamp)
by User
Praktické příklady rozšířených dotazů KQL
Kolik uníkátních IP bylo použito jednou v daném časovém okně?

logika je filtruj časové rozpětí – filtruj sloupec result který obsahuje přesnou shodu „Failed Login“ – vytvoř souhrn a spočítej pro každý „password_hash“ kolikrát se vyskytl (zobrazí každý hash + číslo) – filtruj sloupec count_ je menší nebo roven 1
Kolik unikátních hesel bylo použito na kolik účtů – password spaying filtr

Logika je: V tabulce AuthenticationEvents vyber události mezi dvěma daty, ponech pouze události = Failed Login. Seskup výsledky podle password_hash. Pro každý hash spočítej počet unikátních uživatelských účtů dcount(username). Výsledek ulož do sloupce unique_accounts a seřaď výsledky od nejvyššího poštu dolů. Výsledek v mém případě vrátil 1 hash 1 účet = není potvrzení password spraying.
V tabulce AuthenticationEvents ponech pouze události, kde je result = “Failed Login”. Následně seskup výsledky podle zdrojové IP adresy (src_ip). Pro každou IP spočítej celkový počet neuspěšných přihlášení do sloupce FailedCount a počet unikátních uživatelských účtů do sloupce UniqueUsers pomocí dcount(username). Výsledek seřaď sestupně podle počtu uživatelů.

Výsledek zobrazil pouze interní IP adresy. Bylo by potřeba prověřit časovou osu a detail aktivity zařízení. Je možné že se jedná o běžnou síťovou činnost nebo chybně nastavenou službu. V tento moment nelze jasně potvrdit nebo vyvráti password spraying.
Pokračování k password spraying detekci

Logika: V tabulce AuthenticationEvents ponech pouze neúspěšné přihlášení. Seskup výsledky podle zdrojové IP (src_ip). Do sloupce total_attempts spočítej celkový počet unikátních hesel (hashů hesel). Do sloupce first_seen ulož čas prvního zaznamenaného pokusu. Do sloupce last_seen ulož čas posledního zaznamenaného pokusu.
Tímto zjištuji IP které generují velký počet failed loginů. Zkouší velké množství různých hesel a po jakou dobu to provádějí. Například IP 10.10.0.75 byla detekována u 158 neuspěšných pokusu, použila 158 unikátních hesel a aktivita byla od 1.5 do 17.6. což vypadá jako systematické zkoušení hesel než běžná chyba uživatele.
Ovšem pro lepší potvrzení je potřeba zjistit kdo je 10.10.0.75 (dále jen .75), kolik unikátních uživatelů pracovalo s .75 a jak vypadaly pokus v čase.
Výsledky lze rozdělit do několika variant:
A) Normální provoz, velká firma, sdílená pracovní stanice: 45 dní, 158 failed loginů, 120 uživatelů. V podstatě legit situace, třeba z důvodu pravidelného, plošného vyžádání změny hesel (běžná situace ve větších firmách)
B) Podezřelý výsledek by byl kdyby došlo ke 158 Failed Logins během 10 minut a bylo použito 158 různých hesel a 1 uživatel. Mohlo by značit brute force útok.
C) Velmi podezřelý výsledek – například 158 failed loginů proti 158 uživatelům během 20 minut nebo jiného krátkého časového úseku. Takový výsledek může indikovat password spraying, ale sám o sobě jej nepotvrzuje. Pro potvrzení je potřeba analyzovat časovou osu, vztah mezi účty, úspěšná a neúspěšná přihlášení, historii aktivit účtů a identifikovat zařízení za danou IP adresou. Sofistikovanější password spraying může probíhat velmi nenápadně, například proti několika účtům denně nebo týdně, aby se vyhnul detekci a lockout politikám. Je také nutné ověřit, zda za aktivitou nestojí legitimní proces, služba, naplánovaná úloha (Scheduled Task), skript nebo špatně nakonfigurovaná aplikace používající neplatné přihlašovací údaje.
Vytvoř dotaz, který pro každého odesílatele zobrazí celkový počet odeslaných e-mailů a zároveň počet unikátních příjemců, kterým byly tyto e-maily doručeny. Seřadit od největších.

Funkce sumarize mail_count = count () prostě jen spočítá počet řádků ve skupině by sender – tedy udělej hromádku pro každého uživatele nebo taky pro každého sendera spočítej, kolik má řádků a zobrazí výsledek do vytvořeného sloupce mail_count. Abych snížil počet výsledků a splnil zadání, tak přidám unique_recipients = dcount(recipient) (je spojení distinct – unikátní a count – spočítej), tato funkce prostě spočítá počet unikátních řádků ve sloupci recipient. Na konec order by mail_count desc – seřad od největšího počtu mailů, který uživatel poslal.
Pozornost by vyžadoval například vysoký nepoměr počtu mailů vůči unikátním příjemcům, ideálně zkusit zjistit peak a typ uživatele. Newsletter systém by vykazoval jiné hodnoty než běžný user. Tzn teba novák během hodiny bin(timestamp, 1h) – poslal stovky mailů na jednu adresu = prověřit jakou adresu, zjistit zda-li došlo k exfiltrci mailů nebo ne. Záleží na situaci, útočník by mohl také například exfiltrovat po částech – menší porce.
Detekce e-mailů obsahujících odkazy na Google Docs nebo jinou doménu, kterou útočník využil pro exfiltraci dat

Funkce where link has "docs.google.com" ponechá pouze e-maily obsahující odkaz na Google Docs. Následně summarize first_seen = min(timestamp), last_seen = max(timestamp) by sender vytvoří hromádku pro každého odesílatele a u každého zjistí čas prvního výskytu (first_seen) a čas posledního výskytu (last_seen) e-mailu obsahujícího odkaz na Google Docs. Na konci sort by first_seen asc seřadí výsledky od nejstaršího zaznamenaného výskytu po nejnovější. Tento dotaz lze využít například při vyšetřování phishingu nebo sdílení souborů přes Google Docs, kdy potřebujeme zjistit, kteří uživatelé takové odkazy rozesílali a od kdy do kdy byla tato aktivita zaznamenána.
Detekce potenciálně zašifrovaných souborů na konkrétním zařízení – nebo jakýchkoliv jiných souborů dle potřeby

Funkce where filename contains ".encrypted" ponechá pouze události, kde název souboru obsahuje příponu .encrypted, která může indikovat zašifrované soubory například po ransomware útoku. Následně where hostname == "ENRQ-LAPTOP" omezí výsledky pouze na konkrétní zařízení s názvem ENRQ-LAPTOP. Poté summarize first_seen = min(timestamp), last_seen = max(timestamp) zjistí nejstarší a nejnovější zaznamenaný výskyt takového souboru. Logika uvažování je: nejprve vyfiltruji pouze soubory, které vypadají jako zašifrované, následně se zaměřím na konkrétní počítač a nakonec zjistím časové rozmezí, ve kterém se tyto soubory vyskytovaly. Díky tomu lze rychle určit, kdy potenciální šifrování začalo a kdy byla podobná aktivita zaznamenána naposledy, což může pomoci při vyšetřování ransomware incidentu nebo jiné podezřelé aktivity související se soubory.
Vyhledání spustitelných souborů stažených z podezřelé domény

Funkce where url has "nothing-to-see-here.net" ponechá pouze síťovou komunikaci směřující na zadanou doménu. Následně summarize make_set(url) by src_ip vytvoří hromádku pro každou zdrojovou IP adresu a do nového sloupce set_url uloží seznam unikátních URL adres, které byly z dané IP navštíveny. Poté where set_url contains ".exe" ponechá pouze ty výsledky, ve kterých se v seznamu URL nachází odkaz obsahující příponu .exe, což může indikovat stažení spustitelného souboru. Logika uvažování je: nejprve zúžím data pouze na komunikaci s podezřelou doménou, následně si pro každou IP adresu zobrazím všechny navštívené URL a nakonec vyfiltruji pouze případy, kde se objevuje potenciálně stažený spustitelný soubor. Tento postup lze využít při vyšetřování malware infekcí, phishing kampaní nebo stahování podezřelých nástrojů, kdy potřebujeme rychle zjistit, která zařízení komunikovala s rizikovou doménou a zda z ní byla stahována spustitelná aplikace.
Rozdíl make_set vs distinct
První výsledek je funkce distinct – co vrátí.

Druhý styl výsledku vrátí make_set.

distinct použij ve chvíli, kdy chceš vidět konkrétní unikátní hodnoty po jednotlivých řádcích a dále s nimi pracovat nebo je vyšetřovat. make_set() použij tehdy, když chceš získat rychlý přehled a zobrazit všechny unikátní hodnoty v jednom sloupci jako seznam. Zjednodušeně: distinct = ukaž mi jednotlivé unikátní výsledky, make_set() = ukaž mi všechny unikátní výsledky pohromadě v jedné buňce.
Kolik účtů je přihlášeno z více než 5 IP adres?
Vytvoř dotaz, který pro každého uživatele zobrazí seznam všech IP adres, ze kterých se úspěšně přihlásil, a zároveň počet unikátních IP adres, ze kterých se přihlásil. Následně vyfiltruj uživatele, kteří se přihlásili z více než 5 různých IP adres.

Funkce where result == "Successful Login" ponechá pouze úspěšná přihlášení. Následně summarize make_set(src_ip), dcount(src_ip) by username vytvoří hromádku pro každého uživatele a provede dvě operace. Funkce make_set(src_ip) vytvoří seznam všech unikátních IP adres, ze kterých se daný uživatel úspěšně přihlásil, zatímco dcount(src_ip) spočítá jejich celkový počet. KQL automaticky vytvoří sloupce set_src_ip a dcount_src_ip, pokud jim nepřiřadíme vlastní název. Na konci where dcount_src_ip > 5ponechá pouze uživatele, kteří se přihlásili z více než pěti různých IP adres. Logika uvažování je: nejprve vyfiltruji pouze úspěšná přihlášení, následně si pro každého uživatele zobrazím seznam IP adres a spočítám jejich počet, poté vyberu pouze účty s neobvykle vysokým počtem různých IP adres. Takový výsledek nemusí znamenat kompromitaci účtu, ale může indikovat sdílený účet, VPN, cestujícího uživatele nebo potenciální zneužití přihlašovacích údajů a je vhodným kandidátem pro další analýzu.
Zobrazení nejčastěji spouštěných procesů

Obrázek je ořízlý na 2 výsledky kvůli místu. Funkce summarize execution_count = count() by process_name vytvoří hromádku pro každý název procesu a spočítá, kolikrát se daný proces v logách vyskytl. Výsledek uloží do nového sloupce execution_count(exec._count není funkce, jen název sloupce), který představuje celkový počet spuštění daného procesu. Následně top 10 by execution_count vybere deset procesů s nejvyšším počtem spuštění a automaticky je seřadí od nejčastěji spouštěného po nejméně spouštěný z vybraných výsledků. Logika uvažování je: nechci vidět všechny jednotlivé události spuštění procesů, ale potřebuji rychle zjistit, které procesy se v prostředí spouštějí nejčastěji. Tento postup lze využít při tvorbě baseline běžného provozu, hledání neobvyklých procesů, analýze malware aktivity nebo při identifikaci aplikací, které generují nadměrné množství událostí v systému.
Analýza času událostí pomocí vytvoření nového sloupce

Funkce take 100 načte prvních 100 záznamů z tabulky AuthenticationEvents. Následně project timestamp, hostname, result ponechá pouze sloupce obsahující čas události, název zařízení a výsledek události, čímž zpřehlední výstup a odstraní nepotřebná data. Poté extend hour_of_day = hourofday(timestamp) vytvoří nový sloupec s názvem hour_of_day, do kterého uloží pouze hodinu z časového údaje ve sloupci timestamp. Logika uvažování je: nejprve si zobrazím pouze informace, které mě zajímají, a následně z časového údaje vytáhnu samotnou hodinu, abych mohl snadněji analyzovat, v jakou část dne k událostem docházelo. Tento postup se často používá při hledání noční aktivity, analýze přihlašování mimo pracovní dobu nebo při vytváření statistik podle jednotlivých hodin dne.
Zjišťoval jsem jaký je rozdíl při použití sumarize. Funkce sumarize agreguje výsledky. Viz obrázek níže.

Detekce autentizačních událostí například mimo běžnou pracovní dobu nebo jiné časové období

Funkce extend hour = hourofday(timestamp) vytvoří nový sloupec hour, do kterého uloží pouze hodinu z časového údaje ve sloupci timestamp. Následně where hour < 6 or hour >= 18 ponechá pouze události, které proběhly před 6. hodinou ranní nebo od 18. hodiny večerní včetně, tedy mimo běžnou pracovní dobu. Na konci count spočítá celkový počet takto vyfiltrovaných událostí. Logika uvažování je: nejprve potřebuji z časového údaje získat samotnou hodinu, následně odfiltruji běžnou pracovní dobu a ponechám pouze aktivitu, která proběhla v noci, brzy ráno nebo večer. Tento postup se často používá při hledání podezřelých přihlášení, kompromitovaných účtů nebo neobvyklé aktivity uživatelů, protože úspěšná autentizace mimo standardní pracovní dobu může představovat indikátor kompromitace, automatizované činnosti nebo administrativního zásahu vyžadujícího další ověření.
Vytvoření vlastního sloupce se stavem přihlášení

Funkce extend status = iff(result == "Successful Login", "OK", "FAILED") vytvoří nový sloupec s názvem status a na základě hodnoty ve sloupci result do něj přiřadí vlastní textové označení. Logika uvažování je: pokud je hodnota ve sloupci result rovna Successful Login, zapíše se do nového sloupce hodnota OK. Pokud podmínka není splněna, funkce iff() automaticky použije druhou možnost a zapíše hodnotu FAILED. Původní sloupec result zůstává zachován a pouze se vytvoří nový pomocný sloupec pro přehlednější práci s daty. Tento postup se často používá při kategorizaci událostí, vytváření reportů, dashboardů nebo při zjednodušování složitějších hodnot do snadno čitelných stavů, které lze následně filtrovat, seskupovat nebo vizualizovat.
Vyhodnocení úspěšných a neúspěšných přihlášení s automatickým určením rizika

Funkce summarize vytvoří hromádku pro každého uživatele (by username) a následně pomocí countif() spočítá zvlášť počet úspěšných přihlášení (SuccessfulLogins), počet neúspěšných přihlášení (FailedLogins) a celkový počet pokusů o přihlášení (TotalAttempts). Následně extend Risk = case(...) vytvoří nový sloupec Risk, který na základě počtu neúspěšných přihlášení automaticky přiřadí úroveň rizika. Pokud má uživatel více než 100 neúspěšných přihlášení, získá označení CRITICAL, při více než 50 HIGH, při více než 10 MEDIUM a ve všech ostatních případech LOW. Na konci order by FailedLogins desc seřadí výsledky od uživatelů s nejvyšším počtem neúspěšných přihlášení po nejnižší. Logika uvažování je: nejprve potřebuji zjistit, jak se jednotlivým uživatelům dařilo přihlašovat, následně oddělím úspěšné a neúspěšné pokusy a na základě množství failed loginů vytvořím jednoduché rizikové skóre. Tento přístup umožňuje rychle identifikovat účty s neobvykle vysokým počtem neúspěšných přihlášení, které mohou být cílem brute force útoku, password sprayingu, špatně nakonfigurované služby nebo jiného bezpečnostního problému vyžadujícího další analýzu.
Lze také přidat časové okno | where timestamp > ago(24h) nebo 7d .. dle situace. Nebo třeba by username, bin(timestamp, 1h) což jsou skupiny po hodině – detekce peaků.
Zjištění počtu unikátních domén odesílatelů po vyloučení známých domén

Funkce extend sender_domain = tostring(split(sender, "@")[-1]) vytvoří nový sloupec sender_domain, do kterého uloží pouze doménovou část e-mailové adresy. Nejprve rozdělí hodnotu ve sloupci sender podle znaku @ a následně pomocí [-1] vybere poslední část, tedy samotnou doménu. Poté where sender_domain !in ("jojoshospital.org", "kentuckypharmasupply.com") vyloučí všechny záznamy obsahující tyto dvě domény. Nakonec summarize UniqueDomains = dcount(sender_domain) spočítá počet unikátních domén, které ve výsledku zůstaly, a uloží jej do nového sloupce UniqueDomains. Logika uvažování je: nejprve potřebuji z e-mailové adresy oddělit doménu, následně odstranit známé nebo důvěryhodné domény, které mě v dané analýze nezajímají, a nakonec spočítat, kolik různých domén se ve zbývajících datech nachází. Tento postup se často používá při analýze phishingových kampaní, vyhledávání neznámých odesílatelů nebo při rychlém zjišťování, jak pestré je prostředí e-mailových domén v analyzovaném datasetu.
Rozdíl mezi operátory in a has_any
Operátor in slouží k ověření, zda hodnota ve sloupci přesně odpovídá některé z hodnot uvedených v seznamu. Logika uvažování je: vezmi hodnotu ze sloupce a ověř, zda je přesně stejná jako některá z povolených nebo hledaných hodnot. Naproti tomu has_any se používá především pro textové řetězce a kontroluje, zda text obsahuje alespoň jeden výraz ze zadaného seznamu. Logika uvažování je: nemusí se jednat o přesnou shodu, stačí aby se hledaný výraz nacházel kdekoliv uvnitř textu. Operátor in se často používá při filtrování konkrétních IP adres, uživatelů, procesů nebo domén, zatímco has_any je vhodný například pro analýzu příkazových řádků, URL adres, názvů souborů nebo jiných textových polí, kde hledáme výskyt více klíčových slov současně. Zjednodušeně lze říci, že in znamená „je přesně v seznamu hodnot“, zatímco has_any znamená „obsahuje alespoň jednu hodnotu ze seznamu“.
KQL 301
Zobrazení webové aktivity konkrétní skupiny zaměstnanců pomocí let

Příkaz let cat_care_specialist_ip = ... nejprve vytvoří pomocný seznam IP adres všech zaměstnanců, kteří mají v tabulce Employees roli Cat Care Specialist. Funkce distinct ip_addr zajistí, že každá IP adresa bude v seznamu uvedena pouze jednou. Následně se přejde do tabulky ProxyEvents, která obsahuje záznamy o webové aktivitě uživatelů. Pomocí where src_ip in (cat_care_specialist_ip) se zobrazí pouze záznamy, jejichž zdrojová IP adresa odpovídá některé IP adrese ze seznamu vytvořeného příkazem let. Na konci limit 100 omezí počet vrácených výsledků na prvních 100 řádků. Logika uvažování je: nejprve si vytvořím seznam zaměstnanců nebo zařízení, která mě zajímají, a následně tento seznam použiji jako filtr v jiné tabulce. Výhodou tohoto přístupu je jednoduchost a přehlednost, nevýhodou je, že ve výsledku vidím pouze data z tabulky ProxyEvents a nikoliv informace o samotných zaměstnancích. Proto se při složitějších analýzách často používá lookup nebo join, které dokáží data z více tabulek spojit do jednoho výsledku.