Salı 14:47'de notification patlaması
Sistem odasındaki monitörlere bakıyorsunuz, kahveniz elimizde. Slack'te ShamashAi entegrasyonu üst üste bildirim gönderiyor: "BRUTE_FORCE_DETECTED: dc01.firma.local". Bir tane değil. Beş dakika içinde 187 adet. Aynı cihaz, aynı kaynak IP, aynı event tipi. Klasik brute-force saldırısı — saldırgan 185.x.x.x adresinden Active Directory domain controller'ınıza karşı RDP şifre denemesi yapıyor.
Geleneksel yaklaşımda bu 187 event, 187 ayrı vaka demektir. Her biri ayrı ticket, ayrı triage, ayrı kapatma notu. Sysadmin ekibiniz bu event yığınını tek tek gözden geçirip tek tek kapatmaya çalışır. Yarısını kapatırken yenileri gelir. Bir süre sonra "hepsi aynı şey" deyip toplu silme veya ignore etme eğilimi başlar — ve işte o anda gerçek bir tehdit kaçırılır.
ShamashAi'nin
Incident Groups motoru bu senaryoda devreye girer. 187 event'i tek bir incident grubu altında toplar. Triage işlemini grup seviyesinde yaparsınız: bir kez "resolved" işaretlersiniz, tüm alt event'ler o grubun parçası olarak kapanır. Aynı kaynak yeniden saldırıya geçerse sistem yeni grup açar — eski grubu reopen etmez. Bu yazıda ShamashAi'nin group_key mantığını, upsertIncidentGroupFromEvent mekanizmasını ve triage iş akışını açıklıyoruz.
group_key: benzerliği tanımlayan hash
ShamashAi her event'i
dbo.events tablosuna kaydettiğinde, backend'deki
upsertIncidentGroupFromEvent fonksiyonu otomatik çağrılır. Bu fonksiyon event'in üç kritik özelliğini alır:
- event_type (ör. BRUTE_FORCE_DETECTED, AUTH_FAIL_USER, KNOWN_BAD_IP)
- device_id (hangi cihazda tetiklendi)
- src_ip (kaynak IP adresi — varsa)
Bu üçünün birleşiminden SHA-256 hash üretilir:
javascript
// Node.js Fastify – /src/services/incidentGroupService.js
const crypto = require('crypto');
function generateGroupKey(eventType, deviceId, srcIp) {
const raw =
${eventType}:${deviceId}:${srcIp || 'none'};
return crypto.createHash('sha256').update(raw).digest('hex');
}
async function upsertIncidentGroupFromEvent(eventId, eventType, deviceId, srcIp, severity) {
const groupKey = generateGroupKey(eventType, deviceId, srcIp);
// dbo.incident_groups'ta bu hash'e sahip OPEN grup var mı?
let group = await db.query(
`SELECT id, evidence_count, status FROM dbo.incident_groups
WHERE group_key = @groupKey AND status NOT IN ('resolved', 'false_positive')`,
{ groupKey }
);
if (group) {
// Var — evidence_count artır, son görülme zamanını güncelle
await db.query(
`UPDATE dbo.incident_groups
SET evidence_count = evidence_count + 1, last_seen_at = GETUTCDATE()
WHERE id = @groupId`,
{ groupId: group.id }
);
} else {
// Yok — yeni grup oluştur
const newGroup = await db.query(
`INSERT INTO dbo.incident_groups (group_key, event_type, device_id, src_ip, severity, evidence_count, status, first_seen_at, last_seen_at)
OUTPUT INSERTED.id
VALUES (@groupKey, @eventType, @deviceId, @srcIp, @severity, 1, 'open', GETUTCDATE(), GETUTCDATE())`,
{ groupKey, eventType, deviceId, srcIp, severity }
);
group = { id: newGroup.id };
}
// event'i gruba bağla
await db.query(
UPDATE dbo.events SET incident_group_id = @groupId WHERE id = @eventId,
{ groupId: group.id, eventId }
);
return group.id;
}
Bu kod parçası her event geldiğinde çalışır. Aynı
group_key hash'ine sahip
açık (open, acknowledged, investigating) bir grup varsa event o gruba eklenir ve
evidence_count bir artar. Eğer o hash'e sahip grup yoksa veya eski grup
resolved /
false_positive statüsündeyse
yeni grup açılır.
evidence_count: grubun ağırlığı
dbo.incident_groups tablosundaki
evidence_count kolonu, o grubun altında kaç adet
dbo.events kaydı olduğunu gösterir. Örneğin:
sql
SELECT
id,
event_type,
device_id,
src_ip,
severity,
evidence_count,
status,
first_seen_at,
last_seen_at
FROM dbo.incident_groups
WHERE status = 'open'
ORDER BY evidence_count DESC;
Bu sorgu size "en çok event toplayan" grupları gösterir. 187 event'li brute-force grubu listenin en üstünde çıkar. Triage yapan analist bu sayıya bakarak saldırının yoğunluğunu anlar: "3 deneme" ile "187 deneme" arasındaki risk farkı açıktır.
evidence_count artışı gerçek zamanlıdır. Saldırgan hâlâ deneme yapıyorsa, ShamashAi web arayüzünde "Incidents > Groups" sayfasındaki sayı her yeni event'te güncellenir. WebSocket push sayesinde sayfa yenilemeye gerek kalmaz.
Triage iş akışı: grup seviyesinde karar
ShamashAi'nin Incidents modülünde her grubun beş olası durumu vardır:
1.
open — yeni açılmış, henüz kimse bakmadı
2.
acknowledged — analist "gördüm" işareti koydu, inceleme sırasında
3.
investigating — aktif araştırma yapılıyor, ek veri toplanıyor
4.
resolved — tehdit giderildi veya meşru aktivite olduğu onaylandı
5.
false_positive — yanlış alarm, kural ayarı gerekiyor
Web arayüzünde grup detay sayfasına girdiğinizde sağ üst köşede durum dropdown'u vardır. Durumu değiştirdiğinizde backend şu endpoint'i çağırır:
javascript
// POST /incidents/groups/:groupId/status
fastify.post('/incidents/groups/:groupId/status', async (request, reply) => {
const { groupId } = request.params;
const { status, notes } = request.body; // status: 'resolved', 'false_positive' vs.
// İzin kontrolü
if (!['open', 'acknowledged', 'investigating', 'resolved', 'false_positive'].includes(status)) {
return reply.code(400).send({ error: 'Geçersiz status değeri' });
}
await db.query(
`UPDATE dbo.incident_groups
SET status = @status, resolved_at = CASE WHEN @status IN ('resolved', 'false_positive') THEN GETUTCDATE() ELSE NULL END, resolved_by = @userId, resolution_notes = @notes
WHERE id = @groupId`,
{ groupId, status, userId: request.user.id, notes }
);
// Audit log
await db.query(
`INSERT INTO dbo.audit_log (user_id, action, resource_type, resource_id, details)
VALUES (@userId, 'incident_group_status_change', 'incident_group', @groupId, @details)`,
{ userId: request.user.id, groupId, details: JSON.stringify({ status, notes }) }
);
return { success: true };
});
Bir grubu
resolved veya
false_positive yaptığınızda,
o grubun altındaki tüm event'ler de dolaylı olarak kapatılmış sayılır.
dbo.events tablosunda ayrı bir
status kolonu yoktur; event'in durumu
incident_group_id üzerinden belirlenir. Bu sayede 187 event'i tek tek kapatmak yerine bir kez işlem yapmış olursunuz.
Resolved grup yeniden açılmaz — yeni grup oluşur
Önemli tasarım kararı: bir grubu
resolved veya
false_positive yaptıktan sonra aynı
group_key hash'ine sahip yeni bir event gelirse, ShamashAi
eski grubu reopen etmez. Bunun yerine
yeni bir grup açar.
Neden? Çünkü çözülmüş bir vakayı yeniden açmak, zaman damgası ve triage geçmişini karıştırır. Eski grubun
resolved_at alanı doldurulmuştur ve raporlama/compliance için o anın kaydı önemlidir. Aynı saldırgan bir hafta sonra yeniden denerse, bu yeni bir olaydır ve yeni bir grup ID'si almalıdır.
Uygulama:
javascript
// upsertIncidentGroupFromEvent içinde
let group = await db.query(
`SELECT id FROM dbo.incident_groups
WHERE group_key = @groupKey AND status NOT IN ('resolved', 'false_positive')`,
{ groupKey }
);
if (!group) {
// Eski grup kapalı veya hiç grup yok — yeni oluştur
group = await db.query(
INSERT INTO dbo.incident_groups (...) OUTPUT INSERTED.id VALUES (...),
{ ... }
);
}
status NOT IN ('resolved', 'false_positive') filtresi sayesinde kapalı gruplar göz ardı edilir.
Grupları SOAR ile otomatikleştirme
ShamashAi'nin SOAR motoru incident gruplarıyla entegre çalışır. Örneğin "BRUTE_FORCE_DETECTED tipinde evidence_count > 50 olan gruplarda kaynak IP'yi otomatik engelle" kuralı oluşturabilirsiniz:
javascript
// SOAR playbook snippet (Node.js)
fastify.post('/soar/playbooks/run', async (request, reply) => {
const { playbookId, incidentGroupId } = request.body;
const group = await db.queryOne(
SELECT event_type, src_ip, evidence_count, device_id FROM dbo.incident_groups WHERE id = @groupId,
{ groupId: incidentGroupId }
);
if (group.event_type === 'BRUTE_FORCE_DETECTED' && group.evidence_count > 50) {
// Kaynak IP'yi firewall'da engelle
const blockResult = await fetch('https://firewall.firma.local/api/block', {
method: 'POST',
headers: { 'Authorization':
Bearer ${process.env.FW_TOKEN} },
body: JSON.stringify({ ip: group.src_ip, reason:
Incident group ${incidentGroupId} })
});
// SOAR aksiyonunu logla
await db.query(
`INSERT INTO dbo.soar_actions (incident_group_id, action_type, target, result, executed_at)
VALUES (@groupId, 'block_ip', @ip, @result, GETUTCDATE())`,
{ groupId: incidentGroupId, ip: group.src_ip, result: blockResult.status }
);
// Grubu investigating durumuna al
await db.query(
UPDATE dbo.incident_groups SET status = 'investigating' WHERE id = @groupId,
{ groupId: incidentGroupId }
);
}
return { success: true };
});
Bu şekilde grup eşiği aştığında otomatik müdahale başlar. SOAR aksiyonu
dbo.soar_actions tablosuna kaydedilir ve grubun detay sayfasında "Automated Actions" sekmesinde görünür.
Compliance ve kanıt paketi
ISO 27001:2022 Annex A.8.16 (monitoring activities) ve KVKK Madde 12 (veri güvenliği) gereklilikleri için, incident gruplarının tamamı ve altındaki event'ler kanıt niteliğindedir. ShamashAi
GET /compliance/evidence endpoint'i ile bir grubun tüm detaylarını JSON veya PDF formatında çıkarabilirsiniz:
http
GET /compliance/evidence?incident_group_id=12345&format=pdf
Authorization: Bearer <token>
Dönüş yapısı:
- Grup meta verisi (group_key, status, first_seen_at, last_seen_at, evidence_count)
- Alt event'lerin listesi (timestamp, severity, raw_log)
- Triage notları (resolution_notes, resolved_by, resolved_at)
- SOAR aksiyonları (dbo.soar_actions kayıtları)
- İlgili cihaz bilgisi (dbo.devices)
Bu paket denetim sırasında "olay kaydı" olarak sunulur. Gruplama sayesinde 187 event'in hepsi tek bir dokümanda toplanır, dağınıklık ortadan kalkar.
Web arayüzünde grup görünümü
ShamashAi Next.js web arayüzünde "Incidents > Groups" sayfası varsayılan olarak açık grupları
evidence_count DESC sıralamasıyla listeler. Her satırda:
- Event Type badge (renk kodlu: BRUTE_FORCE_DETECTED kırmızı, AUTH_FAIL_USER turuncu)
- Evidence Count (büyük font, dikkat çeker)
- Kaynak IP (tıklanabilir, IP detay sayfasına gider)
- First Seen / Last Seen (zaman aralığı)
- Status dropdown (open → acknowledged → investigating → resolved)
Grup satırına tıkladığınızda detay modal açılır:
jsx
// Next.js – /app/incidents/groups/[id]/page.tsx
import { useState, useEffect } from 'react';
export default function IncidentGroupDetail({ params }) {
const [group, setGroup] = useState(null);
const [events, setEvents] = useState([]);
useEffect(() => {
fetch(
/api/incidents/groups/${params.id})
.then(res => res.json())
.then(data => {
setGroup(data.group);
setEvents(data.events);
});
}, [params.id]);
if (!group) return <div>Yükleniyor...</div>;
return (
<div>
<h2>Incident Group #{group.id}</h2>
<p>Event Type: {group.event_type}</p>
<p>Evidence Count: <strong>{group.evidence_count}</strong></p>
<p>Source IP: {group.src_ip}</p>
<p>Status: {group.status}</p>
<h3>Alt Event'ler ({events.length})</h3>
<table>
<thead>
<tr><th>Timestamp</th><th>Severity</th><th>Message</th></tr>
</thead>
<tbody>
{events.map(evt => (
<tr key={evt.id}>
<td>{new Date(evt.timestamp).toLocaleString('tr-TR')}</td>
<td>{evt.severity}</td>
<td>{evt.message}</td>
</tr>
))}
</tbody>
</table>
<button onClick={() => updateStatus('resolved')}>Resolved olarak işaretle</button>
</div>
);
}
Bu sayfa altındaki event'lerin detaylarını gösterir, ama triage kararı grup seviyesinde verilir. "Resolved" butonuna bastığınızda tüm 187 event kapatılmış sayılır.
Performans: 10.000 event/saniye altında gruplama
ShamashAi'nin
upsertIncidentGroupFromEvent fonksiyonu her event insertion'da çağrıldığından performans kritiktir. SQL Server'da
dbo.incident_groups tablosunun
group_key kolonu
indexedtir:
sql
CREATE NONCLUSTERED INDEX IX_incident_groups_groupkey_status
ON dbo.incident_groups (group_key, status)
INCLUDE (id, evidence_count);
Bu index sayesinde "bu hash'e sahip açık grup var mı?" sorgusu <1ms'de döner. 10.000 event/saniye yük altında gruplama gecikmesi 50ms altında kalır (lab ortamında ölçülen ortalama). Gerçek üretim ortamında event akışı genelde 500-2000/saniye aralığında olduğundan sorun yaşanmaz.
Limitler ve dürüst notlar
- Grup birleştirme yok: İki ayrı grup (ör. farklı src_ip) sonradan birleştirilemiyor. Tasarım gereği her grup hash'e sıkı bağlı.
- Manuel grup açma yok: Kullanıcı elle "yeni incident group oluştur" yapamıyor. Gruplar sadece event akışından otomatik oluşur.
- Cross-device gruplama yok: Aynı src_ip farklı cihazlarda ayrı gruplar açar. Çünkü
device_id hash'in parçası. Multi-device korelasyon için AI investigation modülü gerekiyor.
- Zaman bazlı group kapatma yok: Bir grup 7 gün boyunca yeni event almasa bile otomatik kapanmaz. Analist manuel
resolved yapmalı. Roadmap'te "auto-expire" özelliği var.
- Group_key değişikliği zor: Saldırgan proxy değiştirip src_ip'si değişirse yeni grup açılır. Behavioral correlation motoru bu tür dağıtık saldırıları yakalar ama bu başka bir modül.
Sonuç
ShamashAi'nin Incident Groups motoru, SIEM'in en büyük operasyonel sorununu — alert fatigue ve manuel triage yükünü — otomatik kümeleme ile çözer.
group_key hash mantığı sayesinde aynı kaynak, aynı cihaz, aynı event tipindeki yüzlerce event tek bir vaka altında toplanır. Triage grup seviyesinde yapılır,
evidence_count saldırının ağırlığını gösterir, SOAR aksiyonları grup eşiklerine göre tetiklenir. Resolved gruplar reopen edilmez; aynı hash yeni event geldiğinde yeni grup açılır. Compliance raporlaması için tüm grup ve alt event'ler tek bir kanıt paketi olarak çıkarılabilir.
Pilot programı 30 gün ücretsiz: shamashai.com.tr/iletisim