TL;DR
يكتب الـ origin classifier لدينا كل محاولة هجوم إلى جدول
dbo.AttackProbes، ويُنتج عمودThreatScoreالمحسوب من نوع PERSISTED نقطةً بين 0 و25 بصيغة Severity × Confidence / 20. تُحفَظ 49 نمط path و13 توقيع User-Agent لأدوات الفحص و12 نمط query string في ثلاثة سجلّات static منفصلة. أيّ probe تتجاوز نقطته العتبة 11 يُحظَر الـ IP تلقائياً، وما دونها يدخل عدّاد burst مدته 15 دقيقة. المخطط الكامل وكود التصنيف أدناه، مأخوذان من النظام الحي.
ملاحظة الكاتب: المخطط والكود وقيم العتبة في هذه المقالة مأخوذة من نظام bilalkose.com.tr الحي. جدول AttackProbes وسجلّات الأنماط الثلاثة وتدفّق التصنيف RecordAttackAsync تعمل في الإنتاج منذ مايو 2026. تناول Pillar 2 جدول AttackProbes وThreatScore بإيجاز فقط، أمّا هذه المقالة فتتناول مخطط الجدول وسبب صيغة التسجيل ومطابقة الأنماط ثلاثية الطبقات بالتفصيل.
جدول المحتويات
- جدول
AttackProbes: 23 عموداً + ThreatScore محسوب PERSISTED - صيغة ThreatScore: لماذا Severity × Confidence / 20
- الكشف عبر الـ path: 49 نمطاً، 14 فئة
- الكشف عبر الـ User-Agent: 13 توقيع أداة فحص
- أنماط الـ query string: SQLi وXSS وPath Traversal وLog4Shell
- كود التصنيف:
RecordAttackAsync+ إزالة التكرار - الأسئلة الشائعة
- مقالات ذات صلة
جدول AttackProbes: 23 عموداً + ThreatScore محسوب PERSISTED
قال Pillar 2 إننا نجمع محاولات الهجوم في جدول منفصل. ذلك الجدول هو dbo.AttackProbes، أي صفّ واحد لكل probe هجوم يُخزَّن مع سياق الطلب ونقطة تهديد:
CREATE TABLE dbo.AttackProbes (
Id BIGINT IDENTITY(1,1) NOT NULL,
HitTime DATETIME2(0) NOT NULL CONSTRAINT DF_AttackProbes_HitTime DEFAULT SYSUTCDATETIME(),
DayIso CHAR(10) NOT NULL,
Host NVARCHAR(80) NULL,
Source VARCHAR(20) NOT NULL,
RawIp NVARCHAR(45) NOT NULL,
Country CHAR(2) NULL,
City NVARCHAR(80) NULL,
Asn NVARCHAR(120) NULL,
AsnNumber INT NULL,
Method VARCHAR(10) NOT NULL,
Path NVARCHAR(512) NOT NULL,
QueryString NVARCHAR(1024) NULL,
UserAgent NVARCHAR(512) NULL,
Referer NVARCHAR(512) NULL,
StatusCode SMALLINT NULL,
Category VARCHAR(40) NOT NULL,
Severity TINYINT NOT NULL,
Confidence TINYINT NOT NULL,
ThreatScore AS (CAST(Severity AS INT) * Confidence / 20) PERSISTED,
PatternMatched NVARCHAR(120) NULL,
CloudflareRayId VARCHAR(20) NULL,
CONSTRAINT PK_AttackProbes PRIMARY KEY CLUSTERED (Id),
CONSTRAINT CK_AttackProbes_Severity CHECK (Severity BETWEEN 1 AND 5),
CONSTRAINT CK_AttackProbes_Confidence CHECK (Confidence BETWEEN 1 AND 100)
);
ثلاثة قرارات تصميم تلفت النظر. الأول أنّ ThreatScore عمود محسوب من نوع PERSISTED، أي تُحسَب قيمة Severity × Confidence / 20 مرةً واحدةً عند كل INSERT وتُكتَب على القرص، ولا يُعاد حسابها وقت الاستعلام. يمكن فهرسته، وكود التطبيق لا يضبط هذا العمود يدوياً أبداً، ولا يستطيع.
الثاني قيود CHECK. يجب أن تقع Severity بين 1 و5، وConfidence بين 1 و100. إذا أفسد خطأ في كود التصنيف هذا المدى، يُرفَض الـ INSERT على مستوى قاعدة البيانات، إذ لا تدخل نقطة معطوبة الجدول إطلاقاً.
الثالث DayIso CHAR(10)، أي ختم يوم بصيغة "2026-05-12". يعمل التقرير اليومي على هذا العمود بـ WHERE DayIso = @today، صديقاً للفهرس، دون استدعاء دالة تاريخ على HitTime.
يحمل الجدول أربعة فهارس non-clustered: DayIso + Severity للتقرير اليومي، وRawIp + HitTime لتاريخ IP واحد، وCategory + DayIso لتوزيع الفئات، وThreatScore + HitTime لأخطر الـ probes. الأربعة جميعها شاملة بـ INCLUDE، فاستعلامات لوحة الإدارة لا تُجري key lookup إلى الفهرس الـ clustered.
صيغة ThreatScore: لماذا Severity × Confidence / 20
تُشتَقّ النقطة من مُدخلَين:
- Severity (1-5): الجدّية التقنية للهجوم. محاولة تسريب ملف
/.envتساوي 5، وفحص/api-docsيساوي 2. - Confidence (1-100): مدى اليقين بأن تطابق النمط هجومٌ فعلاً.
/.git/هجوم دائماً تقريباً (95)، و/consoleقد يكون صفحةً مشروعةً أيضاً (70).
فصل الاثنين مهم. لا نريد أن نرمي إشارةً عالية الجدّية لكن منخفضة اليقين، أي مساراً قد يكون مسار إدارة مشروعاً، في الدلو نفسه مع إشارة منخفضة الجدّية لكن مؤكَّدة. الضرب يدمج الاثنين، والقسمة على 20 تُجلِس النتيجة في مدى 0-25.
| Path | الفئة | Severity | Confidence | ThreatScore |
|---|---|---|---|---|
/.env |
secret_disclosure | 5 | 95 | 23 |
/xmlrpc.php |
wordpress_probe | 4 | 90 | 18 |
/wp-admin |
wordpress_probe | 3 | 85 | 12 |
/console |
admin_panel_probe | 3 | 70 | 10 |
/api-docs |
api_doc_probe | 2 | 65 | 6 |
عتبة الحظر التلقائي 11. تتجاوز /wp-admin العتبة بـ 12، ولا تتجاوزها /console بـ 10. هنا تظهر دقّة قسمة الأعداد الصحيحة، إذ يساوي 3 × 70 / 20 رياضياً 10.5، لكنه 10 في حساب الأعداد الصحيحة في SQL. اختيار 11 عتبةً يترك منطقة الحدود الكسرية هذه خارج الحظر عمداً، فطلب /console واحد لا يحظر IP. لو وضعنا العتبة 12 لاستطاعت أنماط هجوم حقيقية مثل 3 × 80 / 20 = 12 التسلّل عند الحدّ، ولذلك تبقى 11 نقطة القطع الصحيحة. أمّا الـ probes التي تعجز عن تجاوز العتبة فلا تُهمَل، بل تدخل عدّاد burst مدته 15 دقيقة (موضوع cluster C3).
الكشف عبر الـ path: 49 نمطاً، 14 فئة
تسعة وأربعون نمطاً موزّعةً على أربع عشرة فئة، هذا ما يحمله السجلّ الأوّل في أرشيف AttackPaths. تقوم الطبقة الأولى من الكشف على مطابقة الـ path المطلوب بهذه الأنماط المعروفة، والسجلّ مصفوفة static داخل الـ middleware، أي تخصيص واحد طوال عمر التطبيق لا يُعاد بناؤه عند كل طلب:
private static readonly (string Pattern, string Category, byte Severity, byte Confidence)[] AttackPaths =
[
// Secret / file disclosure — critical
("/.env", "secret_disclosure", 5, 95),
("/.aws", "cloud_creds_probe", 5, 95),
("/.ssh", "secret_disclosure", 5, 95),
("/.htpasswd", "secret_disclosure", 5, 90),
// VCS disclosure
("/.git/", "vcs_disclosure", 4, 95),
("/.svn/", "vcs_disclosure", 4, 90),
// WordPress probe
("/wp-admin", "wordpress_probe", 3, 85),
("/wp-login", "wordpress_probe", 3, 85),
("/xmlrpc.php", "wordpress_probe", 4, 90),
// PhpMyAdmin / Exchange
("/phpmyadmin", "phpmyadmin_probe", 4, 90),
("/owa/", "exchange_probe", 4, 85),
("/autodiscover","exchange_probe", 4, 85),
// RCE — highest severity
("/cmd.exe", "rce_pattern", 5, 95),
// ... 49 patterns, 14 categories in total
];
تنقسم الفئات إلى 14 عائلة: secret_disclosure وcloud_creds_probe وvcs_disclosure وwordpress_probe وphpmyadmin_probe وadmin_panel_probe وexchange_probe وbackup_probe وvendor_probe وrce_pattern وcgi_probe وoidc_probe وactuator_probe وapi_doc_probe. كل عائلة تستهدف النوع نفسه من الأصول. تجمع wordpress_probe طلبات /wp-admin و/wp-login و/xmlrpc.php التي تصل رغم أننا لم نثبّت WordPress أبداً، وتجمع exchange_probe فحوص /owa/ و/autodiscover التي تظنّ وجود خادم Exchange في الطريق.
سبب اختلاف قيم Confidence حاسم. تأخذ /.git/ الرقم 95 لأن متصفحاً مشروعاً لا يطلب هذا الـ path أبداً، وتأخذ /wp-admin الرقم 85 لأنه نظرياً يمكن الوصول من رابط خاطئ، أما /console فتأخذ 70 فقط لأنها قد تكون صفحة console فعلاً. تُضمِّن Confidence تكلفة الإيجابية الكاذبة داخل الصيغة.
الكشف عبر الـ User-Agent: 13 توقيع أداة فحص
أداة فحص حقن SQL تكتب اسمها في الـ User-Agent دون أن تخجل، وهذا تماماً ما يستغلّه السجلّ الثاني. حتى لو كان الـ path نظيفاً قد يفضح الـ User-Agent المهاجم، ولذلك تحمل المصفوفة الثانية تواقيع الـ UA لأدوات الفحص:
private static readonly (string UaPattern, string Category, byte Severity, byte Confidence)[] AttackTools =
[
("sqlmap", "tool_sqli_scan", 5, 98),
("nuclei", "tool_nuclei_scan", 4, 95),
("nmap", "tool_port_scan", 3, 90),
("masscan", "tool_port_scan", 3, 90),
("nikto", "tool_dir_brute", 4, 95),
("dirbuster", "tool_dir_brute", 4, 95),
("gobuster", "tool_dir_brute", 4, 92),
("ffuf", "tool_dir_brute", 4, 90),
("wpscan", "tool_wp_scan", 3, 88),
("censys", "tool_recon", 2, 75),
("shodan", "tool_recon", 2, 75),
// ... 13 tools in total
];
sqlmap أكثر مدخلات القائمة يقيناً بـ 98 confidence، إذ لا أحد يكتب اسم أداة حقن SQL في الـ User-Agent بالخطأ. وnikto وdirbuster وgobuster وffuf أدوات brute-force للأدلّة، جميعها فوق 90 confidence. وفي الطرف الآخر يجلس censys وshodan: 75 confidence فقط وseverity 2. هذه خدمات فحص جرد على نطاق الإنترنت، أي استطلاع لا هجوم مباشر، فتُعلَّم بنقطة منخفضة لكنها لا تُطلِق حظراً وحدها.
هنا دقّة يبدو أنها صغيرة لكنها حاسمة، إذ يُبحَث عن توقيع الـ UA بمنطق contains وتُخفَّض المقارنة إلى حروف صغيرة عمداً. شرح B1 فخّ عامل contains في لغة Cloudflare WAF كونه حسّاساً لحالة الأحرف، وحتى لا يفوت توقيع sqlmap على UA يكتب Sqlmap، لا نقع في الخطأ نفسه على جانب الـ origin.
أنماط الـ query string: SQLi وXSS وPath Traversal وLog4Shell
في 9 ديسمبر 2021 ظهر Log4Shell، ومنذ ذلك اليوم بقي توقيع ${jndi: أوضح بصمة هجوم تمرّ عبر الـ query string. السجلّ الثالث ينظر إلى الـ payload نفسه، أي تواقيع الهجوم داخل الـ query string:
private static readonly (string Pattern, string Category, byte Severity, byte Confidence)[] QueryPatterns =
[
("union select", "sqli_pattern", 5, 92),
("' or '1'='1", "sqli_pattern", 5, 95),
("'; drop ", "sqli_pattern", 5, 98),
("../../", "path_traversal", 5, 92),
("/etc/passwd", "path_traversal", 5, 98),
("<script", "xss_in_url", 4, 88),
("javascript:", "xss_in_url", 4, 80),
("${jndi:", "log4shell", 5, 99),
("system(", "rce_pattern", 5, 90),
("eval(", "rce_pattern", 5, 88),
// ... 12 patterns in total
];
${jndi: أعلى نقطة في الجدول كله بـ 99 confidence، إذ يبقى توقيع Log4Shell من 2021 نصاً لا تحمله أيّ حركة مرور مشروعة. أنماط SQLi محاولات حقن كلاسيكية، و../../ و/etc/passwd أنماط directory traversal. جميعها تأخذ severity 4-5 لأن نية الهجوم واضحة للعيان على مستوى الـ payload، وخلافاً لتخمين الـ path لا يوجد هامش شكّ هنا.
تبني السجلّات الثلاثة معاً مرشّحاً متعدّد الطبقات. إن كان الـ path نظيفاً يُفحَص الـ User-Agent، وإن كان الـ UA نظيفاً أيضاً يُفحَص الـ query string. الطلب الذي يمرّ من الطبقات الثلاث نظيفاً يُعَدّ مشروعاً، ويُكتَب إلى جدول PageHits (موضوع cluster C1) باعتباره زائراً لا هجوماً.
كود التصنيف: RecordAttackAsync + إزالة التكرار
كم صفّاً يكتب التصنيف عند هجوم واحد؟ في الواقع صفّ واحد فقط لكل خمس دقائق، وهذا ما يحرص عليه RecordAttackAsync منذ السطر الأوّل. حين تُنتج السجلّات الثلاثة تطابقاً يدخل RecordAttackAsync حيّز العمل، وأول مهامه إزالة التكرار:
private async Task RecordAttackAsync(HttpContext ctx, string ip, Classification c)
{
var path = ctx.Request.Path.Value ?? "/";
var category = c.Category ?? "unknown";
// The same IP+path+category is written once per 5 minutes
// (stops attack scanners from creating thousands of DB rows)
var dedupKey = $"{AttackDedupPrefix}{ip}:{path}:{category}";
if (_cache.TryGetValue(dedupKey, out _)) return;
_cache.Set(dedupKey, true, AttackDedupTtl);
// ... request context is gathered: GeoIP country/city/ASN, UA, referer, CF-Ray
// ... parameterised INSERT INTO dbo.AttackProbes (...)
تجرّب أداة فحص عشرات الـ paths في الثانية، ولولا إزالة التكرار لأنتج مهاجم واحد آلاف الصفوف في الدقيقة. يُخزَّن ثلاثي IP + path + category مؤقتاً 5 دقائق، أي يُكتَب صفّ واحد فقط لكل تركيبة ضمن تلك النافذة. بعد إزالة التكرار يُجمَع سياق الطلب (البلد والمدينة والـ ASN من GeoIP، والـ User-Agent، والـ referer، وترويسة CF-Ray من Cloudflare) ويُدرَج الصف باستعلام بمعاملات. تأتي Category وSeverity وConfidence من التصنيف، وتحسب قاعدة البيانات ThreatScore.
بعد الـ INSERT يُتَّخَذ قرار الحظر التلقائي:
// Auto-block — if the ThreatScore threshold is crossed, add to IpBlocklist
var threatScore = c.Severity * c.Confidence / 20;
if (threatScore >= AutoBlockThreshold)
{
await UpsertBlocklistAsync(ip, threatScore, category, c.PatternMatched);
return;
}
// Burst detector — even low-score probes auto-block at 3+ within 15 min
var burstCount = await IncrementBurstCounterAsync(ip, category);
if (burstCount >= AttackBurstThreshold)
{
await UpsertBlocklistAsync(ip, BurstFakeThreatScore, "attack_burst",
$"{burstCount} probes/{(int)BurstWindow.TotalMinutes}min, last={category}");
await ResetBurstCounterAsync(ip);
}
}
هناك مساران. إذا تجاوز ThreatScore وحده العتبة 11 يُحظَر الـ IP مباشرةً، وإن لم يتجاوزها يرتفع عدّاد الـ burst، فماسحٌ منخفض النقاط لكن مُصرّ يجري ثلاث probes خلال 15 دقيقة يُحظَر على السلوك الجماعي حتى حين لم تتجاوز أيّ probe منفردة العتبة. تكتب UpsertBlocklistAsync الـ IP إلى dbo.IpBlocklist وتُطلِق دفع Cloudflare بنمط fire-and-forget الذي شرحناه في cluster B2، حيث يبقى الكشف على جانب الـ origin بينما يجري الحظر فعلياً على الـ edge. يظهر حساب threatScore في الكود مرةً ثانية، وهذا فقط لاتخاذ قرار الحظر قبل اكتمال الـ INSERT، أمّا القيمة المكتوبة في الجدول فهي دائماً حساب PERSISTED في قاعدة البيانات.
الأسئلة الشائعة
لماذا يُحسَب ThreatScore في قاعدة البيانات لا في كود التطبيق؟
العمود المحسوب من نوع PERSISTED يثبّت الصيغة في مكان واحد، أي تعريف المخطط. سواء كان INSERT الحي من الـ middleware، أو backfill من نوع ETL، أو سجلّاً مُضافاً يدوياً، مهما كان المسار يُنتَج ThreatScore بالصيغة نفسها. ترى في الكود أيضاً حساب c.Severity * c.Confidence / 20، وهذا موجود فقط لاتخاذ قرار الحظر قبل اكتمال الـ INSERT.
Severity من 1 إلى 5، Confidence من 1 إلى 100: لماذا مديان مختلفان؟
Severity صنف خشن، فخمس درجات تكفي. أمّا Confidence فيحتاج ضبطاً دقيقاً، إذ /wp-admin و/wp-login كلاهما 85، لكن فرق /console بـ 70 مهم. مقياس من 100 يستطيع حمل هذا الفارق الدقيق بينما يعجز مقياس من 5 عن ذلك، والقسمة على 20 تجلب الاثنين إلى مقياس مشترك 0-25.
هل تلتقط الـ 49 نمط path كل الهجمات؟
لا، والتقاط كل الهجمات ليس الهدف أصلاً. يستهدف السجلّ أكثر أنماط الفحص الآلي شيوعاً، والغالبية العظمى من الـ probes التي تصل في الإنتاج الحقيقي على هذه القائمة. الهجوم المُوجَّه المصنوع يدوياً قد لا يطابق نمطاً، ولذلك توجد إلى جانب ThreatScore طبقة عدّاد الـ burst وطبقة IsSuspectNetwork المشروحة في Pillar 2، أي دفاع متعدّد الطبقات لا سجلّ واحد.
لماذا لا تُحظَر ماسحات مثل Censys وShodan؟
Severity 2، Confidence 75 → ThreatScore 7، أي تحت العتبة 11. هذه خدمات جرد على نطاق الإنترنت، استطلاع لا هجوم مباشر، فتُعلَّم ويُبلَّغ عنها لكنها لا تُطلِق حظراً وحدها. إن فحصت بإصرار يتدخّل عدّاد الـ burst البالغ 15 دقيقة رغم ذلك.
مقالات ذات صلة
- Pillar 2: قياسات أمن Kubernetes و.NET: كشف الزوّار الحقيقيين، أي الـ pillar الذي ينتمي إليه هذا الـ cluster؛ هذه تعمّقٌ في قسم AttackProbes + ThreatScore فيه.
- C1: PageHits Middleware: عدّ الطلبات بأقل تخصيص في ASP.NET Core، أي الجانب الذي تُعَدّ فيه حركة المرور غير الهجومية، فأنبوب الـ middleware نفسه.
- Cross-pillar: أمن حافة Cloudflare و.NET: WAF + قائمة حظر IP على K8s، أي كيف يُحظَر IP يتجاوز عتبة ThreatScore على الـ edge.
التعليقات (0)
لا توجد تعليقات بعد. كن أول من يعلق.