Microsoft KQL temelleri
- KQL ile Sentinel ve Defender XDR tablolarında başarısız girişleri, parola püskürtmeyi ve Office'ten başlayan şüpheli süreçleri ararsın.
- summarize, project, extend, parse ve ago() ile aynı kaba kuvvet sorusunu bu kez KQL ile yanıtlayacaksın.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Microsoft'un güvenlik ürünleri aynı sorgu dilini paylaşır: KQL (Kusto Query Language). Microsoft Sentinel, Defender XDR'ın gelişmiş av ekranı ve Log Analytics bu dili konuşur. Altay Kargo'da çalışanların kimlikleri Entra ID üzerindedir. Sunucular ve dizüstü bilgisayarlar Defender ile izlenir.
Gece vardiyasındaki analist bu yüzden sorularının yarısını KQL ile yanıtlar. Bu derste önce hangi verinin hangi tabloda durduğunu göreceksin. Sonra temel operatörleri, harf duyarlılığını ve zaman penceresini kuracaksın.
İki çözümlü avla parola püskürtmeyi ve Office belgesinden başlayan şüpheli süreçleri bulacaksın. En sonda sorguyu hızlandıran kuralları ve sık yapılan hataları öğreneceksin.
Hangi soru hangi tabloda
KQL sorgusu çoğu zaman bir tablo adıyla başlar. Yanlış tabloyu seçersen sorgu çoğu zaman hata vermez, yalnız boş döner. O yüzden tablo adlarını ezberlemek yerine mantığını kur. Sentinel ve Log Analytics'te veri, kaynağına göre tablolara ayrılır.
SecurityEvent, Windows Security günlüğünü taşır; 4624, 4625 ve 4688 gibi olay kodları buradadır. SigninLogs, Entra ID'ye yapılan etkileşimli girişleri tutar. Syslog, Linux sunuculardan gelen satırları alır. CommonSecurityLog ise güvenlik duvarı ve proxy gibi cihazların CEF biçimli kayıtlarını toplar.
Defender XDR'ın gelişmiş av ekranında tablolar uç noktadaki davranışa göre adlandırılır. DeviceProcessEvents süreç başlatmalarını, DeviceNetworkEvents ağ bağlantılarını gösterir. DeviceLogonEvents ise uç noktadaki oturum açma olaylarını tutar. Bu tablolarda zaman sütunu Timestamp adını taşır.
Aynı tabloların Sentinel'e aktarılan kopyalarında TimeGenerated sütunu da bulunur. Soru "kim giriş yaptı" ise kimlik tablosuna, "hangi program çalıştı" ise süreç tablosuna gidersin. Tablo adını unuttuğunda sorgu editörünün şema paneline bakabilirsin. Bir tablonun sütunlarını görmek için SigninLogs | getschema gibi bir sorgu yazman yeter.
Etkinlik 1 / 5
Bir analist, bir dizüstü bilgisayarda Word'ün PowerShell başlatıp başlatmadığını Defender XDR gelişmiş av ekranında arıyor. Sorguya hangi tabloyla başlar?
Temel operatörler ve harf duyarlılığı
Sorgu tablo adıyla başlar, ardından boru işaretiyle operatör eklenir. SecurityEvent | where EventID == 4625 yazdığında önce tabloyu seçer, sonra yalnız 4625 kodlu satırları bırakırsın. summarize, SPL'deki stats'ın karşılığıdır; gruplar ve sayar.
project yalnız istediğin sütunları bırakır ve SPL'deki table gibi çalışır. extend, eval gibi hesaplanmış yeni bir sütun ekler. Zaman penceresi ago() işleviyle yazılır.
TimeGenerated > ago(1h) son bir saati, ago(7d) son yedi günü kapsar. sort by sıralar; yön yazmazsan büyükten küçüğe dizer. take ya da limit ise ilk birkaç satırı alır.
Harf duyarlılığı KQL'de iki katmanda ele alınır. Tablo adları, sütun adları, operatör ve işlev adları her zaman harfe duyarlıdır. securityevent yazarsan tablo bulunamaz. Değer karşılaştırmasında ise kural operatöre göre değişir. == harfe duyarlı eşitliktir, =~ duyarsız eşitliktir.
contains ve has varsayılan olarak harfe duyarsızdır; duyarlı hâlleri contains_cs ve has_cs'dir. İkisinin farkı da önemlidir.
has bütün bir terimi arar ve tablonun terim dizinini kullanır. contains ise metnin herhangi bir yerindeki alt diziyi arar ve sütunu tarar. Aradığın şey tam bir kelimeyse has hem doğru hem hızlıdır.
Metin süzgeçlerinin ailesi geniştir. startswith ve endswith değerin başına ya da sonuna bakar. in, değeri bir listeyle karşılaştırır ve harfe duyarlıdır; in~ onun duyarsız hâlidir. has_any listedeki terimlerden herhangi birini arar.
matches regex düzenli ifade uygular, ama terim dizininden yararlanamaz ve çoğu zaman en pahalı seçenektir. Olumsuz hâller başa ünlem işareti alır: !has, !contains, !in. Bir süzgeç seçerken iki soru sor.
Aradığım şey tam bir terim mi? Harfini biliyor muyum? İkisine de evet diyorsan has_cs ya da == en ucuz yoldur.
SigninLogs | where TimeGenerated > ago(1h) | where ResultType != "0" | summarize FailCount = count() by IPAddress | sort by FailCount desc IPAddress FailCount 203.0.113.19 118 198.51.100.77 41 192.0.2.40 2
Sorgu SigninLogs tablosuyla başlar. İlk where zaman penceresini son bir saate indirir.
İkinci where yalnız başarısız girişleri bırakır. ResultType sütunu metin olarak tutulur ve başarılı giriş için "0" değerini taşır. Bu yüzden sayıyı tırnak içinde yazarsın. summarize FailCount = count() by IPAddress her adres için bir satır açar ve sayıma FailCount adını verir.
Ad vermezsen sütun count_ adını alır. sort by FailCount desc en çok deneyeni öne çıkarır. Adımların sırası SPL'dekiyle aynıdır: önce daralt, sonra grupla, sonra sırala.
Etkinlik 2 / 5
SPL komutunu KQL karşılığıyla eşleştir.
Çözümlü av: püskürtme ve şüpheli süreç
Temel operatörleri şimdi gerçek bir av sorusuna uygula. Parola püskürtmede saldırgan çok sayıda hesaba birkaç yaygın parola dener. Her hesaba düşen deneme az olduğu için hesap kilitlenmez. Hesap başına sayım yapan bir kural bu yüzden sessiz kalır.
Doğru soru kaynak adrese bakar: hangi adres kısa sürede çok sayıda farklı hesabı denedi? Bu soruyu dcount() ve bin() birlikte yanıtlar. Entra ID girişlerinde ResultType hatanın nedenini de söyler.
50126 geçersiz kullanıcı adı ya da parola demektir. 50053 çoğu zaman hesabın akıllı kilitlemeye takıldığını gösterir. Kötü niyetli olarak bilinen bir adresten gelen giriş engellendiğinde de aynı kod yazılır.
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType != "0"
| summarize Hesap = dcount(UserPrincipalName), Deneme = count()
by IPAddress, bin(TimeGenerated, 30m)
| where Hesap >= 20
| sort by Hesap desc
IPAddress TimeGenerated Hesap Deneme
203.0.113.19 2026-09-30 02:00:00 64 66
203.0.113.19 2026-09-30 02:30:00 52 53dcount(UserPrincipalName) her grupta kaç farklı hesabın denendiğini sayar, count() toplam denemeyi verir. bin(TimeGenerated, 30m) zamanı otuz dakikalık dilimlere yuvarlar. Böylece gruplama hem adrese hem zaman dilimine göre yapılır. where Hesap >= 20 eşiği uygular.
Çıktıda 203.0.113.19 otuz dakikada 64 farklı hesabı denemiş ve deneme sayısı hesap sayısına çok yakın. Yani her hesaba çoğunlukla bir kez dokunulmuş; bu, püskürtmenin tipik izidir. dcount() yaklaşık bir sayım yapar ve çok büyük kümelerde küçük sapma gösterebilir. Eşik kararı için bu sapma önemsizdir.
İkinci av uç noktada geçer. Word ya da Excel'in PowerShell başlatması çoğu kurumda olağan değildir ve makrolu belgeyle gelen ilk erişimin tipik izidir.
// Sentinel: Windows Security günlüğü
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4688
| where ParentProcessName has_any ("winword.exe", "excel.exe")
| project TimeGenerated, Computer, NewProcessName, CommandLine
// Defender XDR gelişmiş av
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~ ("winword.exe", "excel.exe")
| project Timestamp, DeviceName, FileName, ProcessCommandLineİlk sorgu yedi günlük pencerede 4688 olaylarını süzer. Windows bu olayı yalnız süreç oluşturma denetimi açıksa yazar. has_any listedeki terimlerden herhangi birini arar; tek tek or yazmana gerek kalmaz.
ParentProcessName başlatan sürecin tam yoludur, NewProcessName başlatılan süreçtir. CommandLine sütunu yalnız komut satırı kaydı ilkesi açıksa dolar. Bu ilke kapalıysa sütun boş gelir ve avın yarısı kör kalır.
İkinci sorgu aynı soruyu Defender XDR'a sorar. Orada başlatan süreç InitiatingProcessFileName, başlatılan süreç FileName adını taşır. in~ harfe duyarsız liste karşılaştırmasıdır; WINWORD.EXE ile winword.exe aynı sayılır.
Word'ün powershell.exe başlattığını görürsen önce komut satırını oku. Ardından aynı cihazın ağ bağlantılarını DeviceNetworkEvents tablosunda ara.
Etkinlik 3 / 5
Security olay günlüğünde 4625 kodlu olayları hesaba göre sayan KQL sorgusunu yaz.
Metni ayrıştırmak ve sorguyu parçalamak
Her kayıt hazır sütunlarla gelmez. Syslog tablosunda sshd satırının bütün bilgisi SyslogMessage adlı tek bir metin sütunundadır. Kaynak adresi bu metinden çıkarman gerekir.
parse operatörü bunu bir şablonla yapar. Şablonda sabit metinler tırnak içinde yazılır, yıldız işareti atlanacak kısmı gösterir. Aradaki adlar yeni sütun olur.
Satırların biçimi aynıysa parse hem okunur hem hızlıdır. OpenSSH 9.8 ve sonrasında bazı satırlar sshd-session süreç adıyla gelir. Bu yüzden süreç adını iki değerle süzmek daha güvenlidir.
Syslog
| where TimeGenerated > ago(1d)
| where ProcessName in ("sshd", "sshd-session")
| where SyslogMessage startswith "Failed password"
| parse SyslogMessage with * "from " SrcIP " port " *
| summarize Deneme = count() by SrcIP, Computer
SrcIP Computer Deneme
198.51.100.77 web01 96
203.0.113.19 web01 12parse satırını şöyle oku: baştan ne olursa olsun atla, "from " metnini bul, ardından gelen değeri " port " metnine kadar SrcIP sütununa yaz, kalanını atla. Şablon tutmayan satırda SrcIP boş kalır ve sorgu hata vermez. Bu yüzden boş değerleri isempty(SrcIP) ile ayrıca sayıp kontrol etmek iyi bir alışkanlıktır. Satır biçimleri değişkense extract() işlevini kullanırsın.
extract(@"from ([0-9.]+) port", 1, SyslogMessage) düzenli ifadeyle ilk yakalama grubunu döndürür. Biçim sabitse parse, değişkense extract seçilir. Microsoft'un önerisi de birden çok extract() yerine tek bir parse yazmaktır.
Bir değeri sorgu boyunca tekrar tekrar yazmak yerine let ile bir kere tanımlarsın. let esik = 5; yazdıktan sonra esik adını değişken gibi kullanırsın. Eşiği tek yerden değiştirirsin ve uzun sorgu okunur parçalara bölünür. Aynı adı iki let satırında tanımlarsan sorgu en son atanan değeri kullanır.
let bir alt sorguyu da tutabilir. Alt sorguyu iki kez kullanırsan KQL onu iki kez çalıştırabilir.
materialize() sonucu bir kez hesaplar ve sorgu boyunca saklar. Örneğin let basarisiz = materialize(SigninLogs | where ResultType != "0"); satırından sonra basarisiz tablosunu hem adres hem hesap sayımında kullanırsın. materialize'a vermeden önce sütunları project ile daraltırsan saklanan veri de küçülür.
Etkinlik 4 / 5
let esik = 5; SigninLogs | where ResultType != "0" | summarize FailCount = count() by IPAddress | where FailCount > esik sorgusu ne yapar?
Hızlı sorgu ve sık hatalar
Microsoft'un KQL en iyi uygulamaları tek bir ilkeye dayanır: işlenen veriyi azalt. İlk where her zaman zaman sütununa uygulanır. KQL'in datetime sütunlarında verimli bir dizini vardır ve ilgisiz veri parçaları hiç açılmaz. Ardından seçici metin süzgeçleri gelir.
Tam bir terim arıyorsan contains yerine has yaz. Harfi bildiğin değerde =~ yerine == kullan; harfe duyarlı karşılaştırma daha ucuzdur.
Bütün sütunlarda arama yapan * yerine belirli bir sütuna bak. Tanımadığın bir tabloda yeni bir sorgu denerken sonuna take 10 ekle. Gigabaytlarca sonuç hem seni hem ortamı yorar.
join'de de sıra önemlidir. Microsoft, satır sayısı az olan tabloyu sola, yani ilk sıraya koymayı önerir. Sağ taraf küçük bir başvuru tablosuysa join yerine lookup operatörünü kullanırsın. Tek bir sütuna göre süzmek için join'e hiç gerek yoktur; in operatörü aynı işi daha ucuza yapar.
Örneğin şüpheli adres listesini let ile tanımlar, sonra where IPAddress in (supheli) yazarsın. join kind=inner iki tarafta da eşleşen satırları getirir. leftouter ise sol tablodaki her satırı korur, karşılığı yoksa boş bırakır. Sütun adları farklıysa on $left.Alan == $right.Alan biçimini kullanırsın.
Avın bulduğu şey bir sonraki adımı belirler. Püskürtmeye karşı Entra ID'nin akıllı kilitleme özelliği aynı hesaba gelen tekrarlı denemeleri yavaşlatır. Çok faktörlü doğrulama varsa doğru tahmin edilen parola tek başına yetmez. Koşullu erişim ilkeleri eski kimlik doğrulama protokollerini kapatır; püskürtme denemeleri bu protokollere sık yönelir.
Office süreç avı için Defender'daki saldırı yüzeyi azaltma kuralları, Office uygulamalarının alt süreç başlatmasını engelleyebilir. Avda işe yarayan sorgu Sentinel'de zamanlanmış bir analitik kurala dönüştürülür. Böylece aynı soru her saat kendiliğinden sorulur ve analist yalnız sonuçla ilgilenir.
Derinleş:make-series ile saatlik anomali
Sabit bir eşik gün içindeki dalgalanmayı görmez. Gündüz yüz başarısız giriş olağan olabilir, gece üçte aynı sayı olağan değildir. KQL bu sorun için zaman serisi işlevleri sunar. make-series belirli aralıklarla sayım yapar ve her grup için bir dizi üretir.
SigninLogs | make-series Deneme = count() on TimeGenerated from ago(7d) to now() step 1h by IPAddress her adres için yedi günlük, saatlik bir dizi çıkarır. Boş saatler varsayılan olarak sıfırla doldurulur. series_decompose_anomalies() bu diziyi mevsimsellik, eğilim ve artık olarak ayırır. Artıkta beklenenden belirgin sapan noktaları +1 ya da -1 ile işaretler.
Varsayılan eşik 1,5'tir; büyüttükçe yalnız daha sert sapmalar işaretlenir. Bu yöntem gece yarısı yükselen bir deneme dalgasını gündüz yoğunluğuyla karıştırmadan ayırır. Sonuç yine de bir alarm değil, av için bir ipucudur.
Etkinlik 5 / 5
where TimeGenerated > ago(1h) satırı sorgunun başında değil sonunda yazılırsa ne olur?
Önce kodu oku, sonra çıktıyı seç.
SigninLogs | summarize count() by IPAddress | where TimeGenerated > ago(1h)
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.