Bir SOC analisti arama kutusuna UUID'nin tamamını yapıştırmaz. MITRE tekniğinin ilk sekiz karakterini, hostname parçasını, kullanıcı adının yarısını yazar — ve case'in gelmesini bekler. Bir cybersecurity analytics şirketinin bana getirdiği gereksinim tam olarak buydu: case dokümanlarında partial-match arama; üç seviye derinde, nested attack pattern ve stage'lerin içindeki alanlar dahil.
Zorluk şurada: dokümanlar hem geniş hem derin. Tek bir case; users, endpoints, techniques, pattern dizisi, her pattern'de yirmiden fazla stage, her stage'de ayrı bir MITRE ID taşıyor. Her alanı sorguda tek tek saymak ya da her alana elle autocomplete mapping'i yazmak ölçeklenmez. PoC'de kanıtladığımız pattern aşağıda — birebir sahadan.
Kısa cevap: Tek bir custom edge_ngram analyzer tanımlayın, match_mapping_type: "string" içeren bir dynamic_templates girdisiyle bütün string alanlara bağlayın, multi_match ile "fields": ["*"] üzerinde sorgulayın. Her derinlikteki her text alanı, alan başına sıfır mapping işiyle prefix aranabilir olur. Cevabı _source includes ile inceltin. Trade-off'ları bilin: index büyür ve production öncesi mutlaka ayrı bir search_analyzer tanımlayın.
Gereksinim: nested SOC case'inde partial arama
Alan listesi müşterinin kendi notundan: uid, users, endpoints, assignedTo, updatedAt, triggersNumber — patterns, stages ve steps ise iç onay bekliyordu. Doküman şekli klasik SOC: lateral_movement, privilege_escalation gibi techniques alanları, saldırı anlatısını taşıyan pattern'ler ve attack-pattern--dcaa092b-... biçiminde STIX ID taşıyan stage'ler.
Varsayılan mapping ile bu iş iki yerden kırılır. Birincisi, standart text analizi kelime sınırından tokenize eder — ngram olmadan dcaa yazınca hiçbir şey dönmez. İkincisi, ilginç alanlar sürekli değişiyor: şema hâlâ tartışılırken ("stages için yöneticime sormam lazım") alan bazlı mapping bakım tuzağıdır.
Index: tek analyzer, tek dynamic template
PoC'deki index tanımının tamamı; yalnızca isim genelleştirildi:
PUT soc-cases
{
"mappings": {
"dynamic_templates": [
{
"all_strings_autocomplete": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "auto_complete"
}
}
}
]
},
"settings": {
"analysis": {
"analyzer": {
"auto_complete": {
"tokenizer": "ngram_tokenizer",
"filter": [ "lowercase" ]
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 10,
"token_chars": [ "letter", "digit" ]
}
}
}
}
}
Buradaki üç bilinçli karar:
ID aramasını token_chars taşıyor
token_chars: ["letter", "digit"] ile tokenizer harf ve rakam dışındaki her şeyde böler — tire dahil. attack-pattern--dcaa092b-7de9-... değeri attack, pattern, dcaa092b, 7de9 parçalarına ayrılır ve her parça kendi edge ngram'lerini alır. Analistin UUID'nin ortasından case bulabilmesinin sebebi bu: parça, kendi token'ının başlangıcı.
min_gram: 2, max_gram: 10
Tek karakterlik arama gürültüdür; o yüzden min_gram: 2. max_gram: 10 ise term patlamasını sınırlar — uzun token'lar yalnızca ilk on karaktere kadar indexlenir. Kullanıcılar daha uzun parça yapıştırıyorsa ya max_gram'ı artırın ya da sorguyu ngram'lemeyen bir search-time analyzer'a yaslanın (aşağıda). Parametreler için Elastic'in edge_ngram tokenizer dokümantasyonuna bakın.
Dynamic template her derinlikteki her string'i yakalıyor
match_mapping_type: "string" sayesinde Elasticsearch'ün keşfettiği her string alan — top-level uid, nested patterns.stages.mitreId, gelecek sprintte eklenecek her alan — otomatik olarak auto_complete analyzer'lı text olur. Şema tartışması sonuçlandığında mapping güncellemesi gerekmez. Tarih ve sayılar tespit edilen tiplerinde kalır; updatedAt gerçek date olarak range sorgularına açık kalır.
Sorgu: * üzerinde multi_match, _source ile cerrahi kesim
Doğrulama sorgusu 8 karakterlik UUID parçasını her yerde aradı ve cevabı UI'ın ihtiyacına indirdi:
GET soc-cases/_search
{
"query": {
"multi_match": {
"query": "dcaa092b",
"fields": [ "*" ]
}
},
"_source": [
"uid", "users", "endpoints", "updatedAt",
"techniques", "patterns",
"steps.triggersNumber", "steps.mitreId"
]
}
_source include listesi kopyalanmaya değer: steps.triggersNumber ve steps.mitreId, steps dizisindeki her objeden yalnızca bu iki alanı döndürür — yirmi dolgun obje yerine. Konuşkan SOC dokümanlarında, search-as-you-type UI için gerçek payload tasarrufu.
Testin kanıtladıkları
PoC cevabındaki kanıt, fazlası değil:
- Nested bir stage'in MITRE UUID'sinin ilk 8 karakteri olan
dcaa092bparçası case'i döndürdü:hits.total.value: 1,max_score: 2.079955. took: 1ms — tek dokümanlık test index'inde; doğruluğu kanıtlar, performansı değil. Bunu benchmark diye pazarlamayacağım.- Cevaptaki
stepsobjeleri tam olaraktriggersNumbervemitreIdiçeriyordu;_sourcefiltresinin nested dizide çalıştığının teyidi.
Ölçekte index boyutu veya latency rakamı bu PoC'de yok — kaynak pakette bulunmuyor; uydurmak faydasızdan beter olur.
Production öncesi trade-off'lar
search_analyzer tanımlayın. PoC aynı ngram analyzer'ı hem index hem search tarafında kullandı; kısa girdilerde çalışır ama sürpriz eşleşmeler üretir: sorgu da ngram'lere bölündüğü için uzun sorgular gevşek eşleşir. Production'da auto_complete ile indexleyin, sorguyu düz lowercase tarzı bir analyzer ile olduğu gibi eşleyin.
edge_ngram index'i şişirir. 10 karaktere kadar her token 9'a kadar term üretir. Bunu her string alana uygulamak bilinçli bir depolama-latency takasıdır. Autocomplete yalnızca birkaç alana lazımsa, dynamic template'i her şeyi yakalamak yerine path_match ile daraltın.
fields: ["*"] pratik ama bedava değil. Yüzlerce alana sorgu anında genişleme CPU yer. PoC için doğruydu; sıcak bir arama kutusunda alan gruplarını açıkça listeleyin veya copy_to ile tek arama alanında toplayın. Cluster etkisinden endişeniz varsa izleyin — Elasticsearch monitoring çalışmalarında tam olarak bu sorgu yükü pattern'ini takip ediyoruz.
Öğrendiklerimiz
- Tek dynamic template, elli mapping güncellemesini döver.
match_mapping_type: "string"mevcut ve gelecek her text alana analyzer'ı otomatik bağlar. - ID araması
token_charsözelliğidir. Harf/rakam tokenizasyonu UUID'leri ve tireli ID'leri bağımsız aranabilir parçalara böler. - Gram sınırlarını bilin.
min_gram: 2tek karakter gürültüsünü keser;max_gram: 10term patlamasını sınırlar — "uzun sorgu eşleşmiyor" ticket'ı gelmeden önce ikisini de tanıyın. - Nested dizileri
_sourceincludes ile inceltin.steps.triggersNumberobje başına iki alan döndürür, objenin tamamını değil. - Index ve search analyzer'ı production öncesi ayırın. Aynı analyzer'la ngram araması klasik edge_ngram tuzağıdır.
- Sadece ölçtüğünüzü iddia edin. Tek dokümanda 1 ms
tookdoğruluk kontrolüdür, benchmark değil.
Sık Sorulan Sorular
Neden *dcaa092b* gibi wildcard sorgusu değil de edge_ngram?
Başında wildcard olan sorgular term dictionary'yi arama anında tarar ve index büyüdükçe yavaşlar. edge_ngram maliyeti bir kez, index anında öder; prefix araması exact term eşleşmesine dönüşür — interaktif arama kutusu için doğru takas.
Neden search_as_you_type field tipi değil?
search_as_you_type sağlam bir built-in ama seçilmiş alanlarda phrase-prefix tamamlamada parlar. Buradaki gereksinim, alan başına mapping olmadan bütün alanlarda — tireli UUID'lerin içi dahil — fragment eşleşmesiydi. Custom edge_ngram analyzer + dynamic template bu şekle daha iyi oturur ve token_chars kontrolünü sizde tutar.
Dynamic template tarihleri ve sayıları bozar mı?
Hayır. Yalnızca match_mapping_type: "string" olanları yakalar. Tespit edilen tarihler (updatedAt gibi) ve sayısal tipler normal tiplerinde kalır; sıralama ve range filtreleri çalışmaya devam eder.
max_gram'dan uzun parçaları nasıl ararım?
Ya max_gram'ı artırın (index büyümesini kabul ederek) ya da ngram'sız bir search_analyzer tanımlayın: uzun sorgu tek term olarak cap'e kadar indexlenmiş gram'lerle eşleşir; exact uzun aramalar için prefix sorgusuyla birleştirebilirsiniz.
Partial-match arama tasarımı — veya bunu kaldıracak bir Elasticsearch cluster'ı mı lazım? → searchali.com
