Sinyal kanıt değildir
Bir SQL sorgusu şüphe üretir, hüküm vermez. Her sinyal için üç soru sor: Normal hâli ne? Masum açıklaması var mı? Başka hangi kayıt bunu doğrular?
ops_ tabloları hayali bir portal için tohumlu üreteçle kuruldu. Gerçek bir sistemden alınmadı; süre ve okuma değerleri ölçüm değildir. Motor SQLite olduğu için günlükteki SQL Server sorguları çalıştırılmaz, metin olarak incelenir. Tabloları sorgularsın, sistem yönetimi komutlarını yalnız okursun.Sinyal 1: Mesai dışı erişim
Burada mesai hafta içi 08:00-18:00. Düz sorgu servis hesaplarıyla dolar: gece ETL'i zaten mesai dışında çalışır. Bu yüzden hesap listesiyle birleştir: off_hours_ok = 0 olanlar kim?
SELECT a.login_name, a.event_at, a.client_ip
FROM ops_access_log a
JOIN ops_logins l ON l.login_name = a.login_name
WHERE a.event_type = 'login_ok' AND l.off_hours_ok = 0
AND (CAST(strftime('%H', a.event_at) AS INTEGER) NOT BETWEEN 8 AND 17 OR CAST(strftime('%w', a.event_at) AS INTEGER) IN (0, 6))
ORDER BY a.event_at;
Çıkan üç kaydı karşılaştır: biri akşam 18:22, biri cumartesi, biri salı sabaha karşı. Hepsi sorulur ama aciliyetleri aynı değildir. Aradaki fark adres sütunundadır.
Sinyal 2: Parola deneme
Başarısız girişi adrese göre say. İki desen vardır:
| Desen | Görünüşü | Hangi gruplama yakalar |
|---|---|---|
| Kaba kuvvet (brute force) | Tek hesaba çok deneme | Hesap başına |
| Parola püskürtme (password spray) | Çok hesaba az deneme, hesap kilidine takılmamak için | Adres başına farklı hesap sayısı |
SELECT client_ip, COUNT(*) AS deneme, COUNT(DISTINCT login_name) AS hesap,
MIN(event_at) AS ilk, MAX(event_at) AS son
FROM ops_access_log
WHERE event_type = 'login_fail'
GROUP BY client_ip
ORDER BY deneme DESC;
En çok denemeli adres saldırgan değil: svc_eski kapalı bir hesap, eski bir iş 30 dakikada bir bağlanmaya çalışıyor. İçeriden, tek hesaptan, düzenli aralıkla. Ama bu da bir bulgudur: işi kapatan yok ve gürültüsü gerçek saldırıyı gizler. En kritik soru sayı değil, başarısızlıklardan sonra başarı geldi mi?
Sinyal 3: Ardışık kayıt okuma
Gerçek kullanıcı müşterileri rastgele okur. Numarayı birer artırarak okuyan biri büyük olasılıkla listeyi çekiyordur (enumeration). LAG ile önceki kayıttan farkı al:
WITH o AS (
SELECT login_name, client_ip, event_at, record_id,
record_id - LAG(record_id) OVER (PARTITION BY login_name, client_ip ORDER BY event_at, event_id) AS fark
FROM ops_access_log
WHERE event_type = 'read'
)
SELECT login_name, client_ip, COUNT(*) AS ardisik, MIN(event_at) AS ilk, MAX(event_at) AS son
FROM o
WHERE fark = 1
GROUP BY login_name, client_ip
HAVING COUNT(*) >= 20
ORDER BY ardisik DESC;
İki sonuç çıkar. Biri haftalık ETL: planlı, içeriden, servis hesabı. Diğeri bir analist hesabı, dış adresten. Aynı sorgu, farklı anlam. Bağlam olmadan alarm kurarsan her pazar yanlış alarm gelir.
Sinyalleri zincirle
Tek başına zayıf sinyaller, aynı adres ve hesapta sırayla geliyorsa güçlü bir hikâye olur: çok deneme, sonra başarı, sonra toplu okuma. Bu sırayı tek bir zaman çizelgesine dök.
Kaynak nereden gelir?
Bu tablolar kurgu. Gerçek SQL Server'da karşılıkları (burada çalışmaz):
-- Sunucu denetimi: başarısız girişler (yalnız okumak için)
CREATE SERVER AUDIT denetim_giris TO FILE (FILEPATH = 'E:\Denetim\');
CREATE SERVER AUDIT SPECIFICATION giris_ozeti
FOR SERVER AUDIT denetim_giris ADD (FAILED_LOGIN_GROUP) WITH (STATE = ON);
ALTER SERVER AUDIT denetim_giris WITH (STATE = ON);
- SQL Server Audit: Belge, başarısız girişleri denetleyen bu örneği verir. Yeni denetim belirtimi kapalı başlar, açmayı unutma. Hassas sütunlu tabloları da denetlemeyi önerir.
- Giriş denetimi ayarı: Başarısız, başarılı ya da ikisini hata günlüğüne yazar. Değişiklik için sunucu yeniden başlatılmalıdır.
- SQL Trace: Eski
Audit Login Failedolayı kullanımdan kalkmakta olan SQL Trace'e aittir; yeni kurulumda denetim ya da Extended Events tercih edilir.