ملخص قصير
إذا كانت لوحة التحليلات لديك تُظهر 100 زائر يوميًا، فإنّ عدد البشر الحقيقي ~33، أمّا الـ 67% المتبقية فهي مراكز بيانات و VPN و ASN فارغ. أُشغّل نظام إنتاج حي يفصل البوت عن الإنسان باستخدام PageHits و AttackProbes و IsSuspectNetwork، ويقوم بالحظر التلقائي عبر عدّاد الانفجار في نافذة منزلقة MERGE+OUTPUT، ويخزّن كل شيء بتصميم متوافق مع KVKK وخالٍ من PII لمدة 90 يومًا. مخطط SQL الكامل والكود والقياسات الإنتاجية الحقيقية لـ 24 ساعة أدناه.
ملاحظة المؤلف: جميع مخططات SQL والأكواد والمقاييس في هذه المقالة مأخوذة من نظام bilalkose.com.tr الحي. ذهبت جداول PageHits و AttackProbes إلى الإنتاج في 2026-05-10، وفي غضون 24 ساعة قسنا نسبة 33%/67%، و 157 محاولة هجوم، و 246 صف backfill telemetry. قرارات تصميم Sub-Faz A++/A+++ (سجل ASN لمراكز البيانات، علم IsSuspectNetwork، نمط AttackBurstCounter MERGE+OUTPUT) تأتي مباشرة من تجربة الإنتاج.
جدول المحتويات
- ما هي قياسات الأمان ولماذا من جانب الأصل؟
- بنية PageHits Middleware
- AttackProbes + صيغة ThreatScore
- AttackBurstCounter: نافذة منزلقة MERGE+OUTPUT
- علم IsSuspectNetwork: مركز بيانات / VPN / ASN فارغ
- نمط MaxMind GeoIP Singleton Reader
- Log → AttackProbes Backfill ETL
- حلقة الحظر الأصلي: TrafficClassifier → IpBlocklist → CF List
- لوحة الإدارة للزوار الحقيقيين
- تصدير مقاييس Prometheus
- الخصوصية + KVKK: تصميم قياسات خالٍ من PII
- الأسئلة المتكررة
- مقالات ذات صلة
ما هي قياسات الأمان ولماذا من جانب الأصل؟
إذا كنت تستخدم حافة CDN مثل Cloudflare، فأنت تُوقِف معظم الهجمات على الحافة بالفعل، وقد تناولت ذلك بالتفصيل في Pillar 1. غير أنّ الحافة عديمة الحالة، إذ تقتصر على مُطابقة الأنماط وبحث IP واحتواء UA. أمّا القرارات ذات الحالة، كسلوك الانفجار وتاريخ الجلسة وتراكم ThreatScore، فتنتمي إلى الأصل، وهنا تعيش قياسات الأمان من جانب الأصل.
حيث تقصر الحافة
تصفية الحافة غير كافية في ثلاثة سيناريوهات:
- مهاجم بطيء-منخفض: يطلق طلبًا واحدًا في الساعة، لكن إلى مسار حساس مختلف كل ساعة، فترى الحافة «طلب واحد» ولا تُطابق النمط، بينما يرى الأصل «نفس IP، 8 مسارات admin في 8 ساعات».
- حشو بيانات اعتماد موزّع: brute force ضد نفس المستخدم من IPs مختلفة. لا تتجاوز الحافة حد معدل IP، فيما يطبّق الأصل قفلًا مرتبطًا بالمستخدم.
- شذوذ سلوكي: ينحرف مستخدم مُصادَق عن السلوك الطبيعي (1000 مشاهدة صفحة الساعة 3 صباحًا)، إذ لا تعرف الحافة ذلك، بينما يلتقطه الأصل بسياق جلسة المستخدم.
تُضلّل لوحات التحليلات
ثمّة مشكلة أساسية أكثر: أدوات التحليلات القياسية (GA، Plausible، CF Analytics) تحسب حركة البوتات ومراكز البيانات على أنّها «زوار حقيقيون». في قياسات Sub-Faz A الخاصة بي رأيت 126 IP فريدة في 24 ساعة، لكن تحليلًا مُفصّلًا، مدعومًا بفحص ASN ورؤوس Cloudflare، كشف أنّ عدد البشر الحقيقي كان 25-30. الـ 75-80% المتبقية كانت scrapers مراكز بيانات و VPNs وحركة بـ ASN فارغ، وهي نسبة نمت خصوصًا بعد انفجار AI crawler ما بعد مارس 2026.
هذه ليست مجرد مشكلة «كرامة مجروحة»، بل إنّها توجّه قرارات الأعمال إلى الوجهة الخاطئة: ما المحتوى الشعبي حقًا، وما معدل التحويل، وأيّ locale يستحق الاستثمار؟ تتطلب القرارات الصحيحة أعدادًا صحيحة.
مصلحة مشروعة بموجب KVKK المادة 5/2-(f)
في تركيا، يعتمد تخزين Raw IP على KVKK المادة 5/2-(f) «معالجة البيانات إلزامية للمصالح المشروعة لمراقب البيانات»، أي أنّ الأمان مصلحة مشروعة. لا يتطلب جدول AttackProbes موافقة المستخدم (المهاجم لا يطلب إذنًا)، أمّا بالنسبة لـ PageHits فنحتفظ بعلم ConsentGranted اختياري يُمرَّر من cookie banner. بعد 90 يومًا من الاحتفاظ، تُحذف البيانات تلقائيًا عبر مهمة DELETE مجدوَلة يوميًا.

بنية PageHits Middleware
قلب النظام middleware ASP.NET Core واحد: TrafficClassifierMiddleware. يُمرّر كل طلب HTTP عبر 4 نقاط قرار ويكتب إلى الجدول المناسب، ويحلّ محل VisitorAnalyticsMiddleware السابق. هنا تحديدًا يحدث فصل الزائر الحقيقي عن البوت.
أربع نقاط قرار
يأتي طلب
↓
1. سجل IpBlocklist نشط؟ ──── نعم ──→ 403 Forbidden + log
↓ لا
2. مطابقة نمط هجوم؟ ── نعم ──→ dbo.AttackProbes INSERT
↓ لا
3. بوت شرعي (Googlebot, Bingbot, IndexNowBot)? ── نعم ──→ تخطّى
↓ لا
4. إنسان حقيقي → dbo.PageHits INSERT
middleware واحد، ومصدر واحد للحقيقة. لا نقسم إلى middlewares متعددة لأنّ ترتيب القرار حاسم، إذ يجب التحقق من IpBlocklist قبل AttackProbes، وإلا تُكتب IPs المحظورة في DB.
مخطط dbo.PageHits
CREATE TABLE dbo.PageHits (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
DayIso CHAR(10) NOT NULL, -- "2026-05-11"
VisitorHash CHAR(32) NOT NULL, -- MD5(IP + daily salt)
Path NVARCHAR(256) NOT NULL,
Culture CHAR(2) NULL, -- tr/en/ar
Referer NVARCHAR(512) NULL,
Country CHAR(2) NULL, -- MaxMind GeoLite2-City
City NVARCHAR(80) NULL,
Asn NVARCHAR(120) NULL,
AsnNumber INT NULL, -- MaxMind GeoLite2-ASN
StatusCode SMALLINT NULL,
RawIp NVARCHAR(45) NULL, -- IPv4 + IPv6
ConsentGranted BIT NOT NULL DEFAULT 0,
HitTime DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
CREATE NONCLUSTERED INDEX IX_PageHits_DayIso_Hash
ON dbo.PageHits(DayIso, VisitorHash) INCLUDE (Path, HitTime);
ثمّة قراران حاسمان هنا. أوّلًا، VisitorHash هو MD5 يومي بـ salt متناوب يمنع الترابط عبر الأيام ويسهّل توافق KVKK، بينما يبقى RawIp مُخزّنًا مع DELETE تلقائي بعد 90 يومًا من الاحتفاظ. ثانيًا، AttackProbes و PageHits جدولان منفصلان، بحيث لا تُلوّث تسجيلات الهجوم بيانات التحليلات.
إزالة التكرار: ذاكرة تخزين مؤقت داخلية 30 ثانية
لصفحة عالية الحركة، يمكن لنفس المستخدم إطلاق 10-20 طلبًا في 30 ثانية (تحميل صور، AJAX polling، إلخ)، وكتابة كل منها إلى DB ليست أكثر من هدر صريح للموارد. أُخزّن tuples (IP، path) في IMemoryCache بـ vh: prefix و TTL 30 ثانية:
var cacheKey = $"vh:{ipHash}:{path}";
if (_cache.TryGetValue(cacheKey, out _)) return; // skip duplicate
_cache.Set(cacheKey, true, PageDedupTtl);
// ... PageHits INSERT
يُسقط هذا الفحص البسيط حمل DB بنحو 70%، خاصة على الصفحات عالية الحركة.
حساب VisitorHash
private static string ComputeVisitorHash(string ip, string dayIso)
{
var salt = _config["Security:VisitorHashSalt"]; // من Vault
var input = $"{ip}|{dayIso}|{salt}";
var bytes = Encoding.UTF8.GetBytes(input);
var hash = MD5.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
MD5 ليس تشفيريًا، لكن المهاجم الذي يريد عكس VisitorHash لديه بالفعل Raw IP (مُخزّن أيضًا في PageHits)، فالهدف هنا ليس أكثر من معرّف فريد مزوّد بـ salt، ولهذا يُعدّ sha256 مبالغًا فيه ويكفي MD5. أمّا الـ salt فهو ثابت لبيئة الإنتاج وليس متناوبًا يوميًا، والـ hash أحادي الاتجاه.

AttackProbes + صيغة ThreatScore
نستخدم جدولًا منفصلًا لتسجيلات محاولات الهجوم. لماذا منفصل؟ ثلاثة أسباب. أوّلًا، لدى AttackProbes 19 عمودًا مقابل 13 لـ PageHits، أي أنّ تداخل المخطط منخفض. ثانيًا، سياسات الاحتفاظ مختلفة، إذ تُعدّ AttackProbes أدلة ويمكن الاحتفاظ بها لفترة أطول. ثالثًا، أنماط الاستعلام مختلفة، فـ AttackProbes يستخدم مجاميع ASN/category، بينما يستخدم PageHits أعداد VisitorHash اليومية.
مخطط dbo.AttackProbes و ThreatScore PERSISTED
CREATE TABLE dbo.AttackProbes (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
HitTime DATETIME2(0) NOT NULL,
DayIso CHAR(10) NOT NULL,
Host NVARCHAR(80) NULL,
Source VARCHAR(20) NOT NULL, -- 'middleware' أو 'log_backfill'
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, -- 1-5
Confidence TINYINT NOT NULL, -- 1-100
ThreatScore AS (CAST(Severity AS INT) * Confidence / 20) PERSISTED,
PatternMatched NVARCHAR(120) NULL,
CloudflareRayId VARCHAR(20) NULL,
CONSTRAINT CK_AttackProbes_Severity CHECK (Severity BETWEEN 1 AND 5),
CONSTRAINT CK_AttackProbes_Confidence CHECK (Confidence BETWEEN 1 AND 100)
);
ثمّة نقطة حاسمة هنا: ThreatScore عمود computed PERSISTED، محسوب وقت الإدراج، قابل للفهرسة، ولا يُعاد حسابه في الاستعلامات. وهذا العمود المحسوب يُسرّع استعلامات تصفية «threat ≥ 11».
صيغة ThreatScore وقرار العتبة
ThreatScore = (Severity × Confidence) / 20
- Severity (1-5): الخطورة التقنية للهجوم (1=منخفض، 5=حرج)
- Confidence (1-100): ثقة مُطابقة النمط (50=احتمال متساوٍ، 100=مؤكد)
النطاق: 0-25. عتبة AutoBlockThreshold هي 11. الإصدار السابق استخدم 12، غير أنّ قسمة الأعداد الصحيحة فاتت حالات: 3 × 75 / 20 = 11 (مجموعات admin-probe-like متوسطة-Severity عالية-Confidence) لم تُلتقط عند عتبة 12. وبعد الخفض إلى 11، لم تظهر إنذارات كاذبة.
جدول أنماط مسار الهجوم
AttackPaths مصفوفة ساكنة، يقوم middleware بمطابقة بادئة المسار، وفيما يلي المجموعة الفرعية عالية ThreatScore:
| المسار | الفئة | Severity | Confidence | ThreatScore |
|---|---|---|---|---|
/.env |
secret_disclosure | 5 | 95 | 23 |
/.aws |
cloud_creds_probe | 5 | 95 | 23 |
/.ssh |
secret_disclosure | 5 | 95 | 23 |
/.htpasswd |
secret_disclosure | 5 | 90 | 22 |
/credentials |
cloud_creds_probe | 5 | 90 | 22 |
/database.yml |
secret_disclosure | 5 | 90 | 22 |
/.git/ |
vcs_disclosure | 4 | 95 | 19 |
/xmlrpc.php |
wordpress_probe | 4 | 90 | 18 |
/phpmyadmin |
phpmyadmin_probe | 4 | 90 | 18 |
/wp-admin |
wordpress_probe | 3 | 85 | 12 |
/admin.php |
admin_panel_probe | 3 | 80 | 12 |
القائمة الكاملة حوالي 50 نمطًا موزعة على 9 فئات. وإضافة نمط جديد لا تتطلب أكثر من إدخال واحد في المصفوفة مع إعادة تشغيل pod.
مجموعة الفئات الـ 9
| الفئة | المعنى | أنماط مثال |
|---|---|---|
| secret_disclosure | كشف ملفات | /.env, /.ssh, /.htpasswd |
| cloud_creds_probe | اكتشاف اعتمادات السحابة | /.aws, /credentials, /config.json |
| vcs_disclosure | كشف VCS | /.git/, /.svn/ |
| wordpress_probe | فحص افتراض WP | /wp-admin, /wp-login, /xmlrpc.php |
| phpmyadmin_probe | فحص لوحة PMA | /phpmyadmin, /pma/, /myadmin |
| admin_panel_probe | endpoint admin عام | /admin.php, /login.php, /console |
| exchange_probe | MS Exchange / OWA | /owa/, /ews/ |
| sqli_payload | حقن SQL في query string | union select, ' or 1=1 |
| log4shell | استغلال Log4j JNDI | ${jndi:ldap://...} |
كل فئة تحصل على عداد Prometheus خاص بها (bilal_attack_probe_blocks_total{category="..."})، مما يجعل تفاصيل الفئة سهلة في Grafana.
📊 [مخطط: توزيع فئات آخر 24 ساعة pie chart، secret_disclosure 48%، cms-probe 29%، sqli 10%، إلخ]
AttackBurstCounter: نافذة منزلقة MERGE+OUTPUT
تخيّل مهاجم trickle يطلق طلبًا منخفض-Severity واحدًا في الدقيقة، لا يتجاوز أيٌّ منها عتبة 11 بمفرده، ومع ذلك يفحص 5-10 مسارات حساسة مختلفة في 15 دقيقة. هذا تحديدًا ما لا تلتقطه عتبة ThreatScore وحدها التي تفحص طلبًا واحدًا من نمط هجوم. لاصطياد هذا السلوك، تحتاج إلى عداد نافذة منزلقة.
جدول dbo.AttackBurstCounter
CREATE TABLE dbo.AttackBurstCounter (
RawIp NVARCHAR(45) NOT NULL PRIMARY KEY,
WindowStart DATETIME2(0) NOT NULL,
Count INT NOT NULL,
LastHitAt DATETIME2(0) NOT NULL
);
صف واحد لكل IP. عرض النافذة 15 دقيقة (BurstWindow = TimeSpan.FromMinutes(15))، وعتبة الإطلاق 3 (AttackBurstThreshold = 3).
نمط MERGE + OUTPUT SQL ذري
لتجنّب race conditions تحتاج إلى زيادة ذرية. ففي إعداد pod K8s متعدد النسخ، إذا رأى pod-A و pod-B نفس IP في نفس الوقت، يخسر العداد السباق. ولهذا فإنّ مزيج MERGE مع OUTPUT inserted.Count في SQL Server يفعل هذا في roundtrip واحد:
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @WindowStart DATETIME2 = DATEADD(MINUTE, -15, @Now);
MERGE dbo.AttackBurstCounter AS target
USING (SELECT @ip AS RawIp, @Now AS NowTs) AS source
ON target.RawIp = source.RawIp
WHEN MATCHED AND target.WindowStart > @WindowStart
-- في نفس النافذة: counter++
THEN UPDATE SET Count = target.Count + 1, LastHitAt = @Now
WHEN MATCHED AND target.WindowStart <= @WindowStart
-- النافذة تنقّلت: counter=1، نافذة جديدة
THEN UPDATE SET Count = 1, WindowStart = @Now, LastHitAt = @Now
WHEN NOT MATCHED
-- هجوم أول: إدراج
THEN INSERT (RawIp, WindowStart, Count, LastHitAt)
VALUES (@ip, @Now, 1, @Now)
OUTPUT inserted.Count AS NewCount;
بفضل OUTPUT inserted.Count، يحصل middleware على العداد الجديد في استعلام واحد، فإذا كانت NewCount >= 3 انطلق block trigger. النهج القديم «SELECT ثم UPDATE» سقط في race conditions عبر النسخ، ولهذا انتقلنا إلى MERGE.
لماذا في DB، وليس قاموس داخل الذاكرة؟
ثلاثة أسباب:
- تناسق متعدد النسخ: عداد pod-A غير مرئي لـ pod-B، فـ DB هي المصدر الوحيد للحقيقة.
- مرونة إعادة تشغيل pod: تُمحى حالة الذاكرة عند إعادة التشغيل، بينما تستمر DB.
- سجل تدقيق: «متى، وكم مرة، وأيّ نافذة» قابل للاستعلام.
التنازل المتبقّي هو أنّ كل طلب هجوم يضيف DB roundtrip (~3-5ms)، غير أنّ طلبات الهجوم ستحصل بالفعل على ردود بطيئة، إذ إنّ وصولها إلى الأصل بالفعل كلّفها الوقت. الطلبات العادية لا تضرب هذا المسار أبدًا.
الرجوع في الذاكرة (DB غير قابلة للوصول)
إذا كان لاتصال DB مشكلة، يجب ألا يتعطل middleware. لذلك يوفر IMemoryCache بـ ab: prefix رجوعًا pod-local:
if (dbUnavailable) {
var cacheKey = $"ab:{ip}";
int count = _cache.GetOrCreate(cacheKey, entry => {
entry.AbsoluteExpirationRelativeToNow = BurstWindow;
return 0;
});
_cache.Set(cacheKey, count + 1, BurstWindow);
return count + 1;
}
يُسامَح بالخسارة في multi-replica، إذ إنّ العداد pod-local لا يرى عداد pod-B. التنازل هنا هو «تقريبي» بدلًا من دقيق تمامًا، ويستأنف المسار العادي بعد استرداد DB.
خدمة استضافة تنظيف ساعي
AttackBurstCounterCleanupHostedService يعمل كل ساعة، يحذف النوافذ المنتهية:
DELETE FROM dbo.AttackBurstCounter
WHERE WindowStart < DATEADD(MINUTE, -30, SYSUTCDATETIME());
هامش أمان 30 دقيقة (نافذة 15 دقيقة و buffer 15 دقيقة). نمو DB محدود، وأداء الاستعلام يبقى سليمًا.
📊 [مقياس: 24 ساعة عدد الحظر burst-triggered مقابل threshold-triggered]
علم IsSuspectNetwork: مركز بيانات / VPN / ASN فارغ
عندما ذهب PageHits إلى الإنتاج في Sub-Faz A، فاجأتني الـ 24 ساعة الأولى: ظهرت 126 IP فريدة في التحليلات لكن عدد البشر الفعلي كان حوالي 25-30. لاحظتُ على الفور كم كان هذا الرقم ملوّثًا، إذ ما دمتُ لا أستطيع قياس عدد المستخدمين الحقيقي، لم أكن أستطيع الوثوق بلوحة المعلومات. هذا ما أطلق تصميم Sub-Faz A+++.
قرار: قسّم، لا تحذف
كانت هناك طريقتان متاحتان: إمّا استبعاد حركة مركز البيانات من PageHits في وقت الكتابة (تصفية عند الكتابة)، أو حفظ كل الحركة ووضع علم عليها (تصفية عند القراءة). تم اختيار الثاني، وذلك لأسباب متعدّدة:
- حركة مركز البيانات تحمل معلومات أيضًا (سلوك scraper، أيّ صفحات تُفحص).
- القرار قابل للعكس، إذ إنّ UPDATE علم أسهل من الحذف.
- لتدقيق الخصوصية، «ما تحتفظ به» أكثر أهمية من «ما تحسبه».
عمود BIT PageHits.IsSuspectNetwork
أُضيف عبر ALTER TABLE:
ALTER TABLE dbo.PageHits
ADD IsSuspectNetwork BIT NOT NULL CONSTRAINT DF_PageHits_IsSuspect DEFAULT 0;
-- Backfill الصفوف الموجودة
UPDATE dbo.PageHits
SET IsSuspectNetwork = 1
WHERE AsnNumber IS NULL
OR AsnNumber IN (16509, 14618, 14061, 24940, 8075, /* ... 42 ASN */);
يقوم middleware بضبطه في Classify step 8:
var asnNumber = geoIpResult?.AsnNumber;
bool isSuspect = DatacenterAsnRegistry.IsAnonymousOrDataCenter(asnNumber);
// IsSuspectNetwork = isSuspect في PageHits INSERT
منطق قرار IsAnonymousOrDataCenter
public static bool IsAnonymousOrDataCenter(int? asnNumber)
=> !asnNumber.HasValue || DataCenterAsns.Contains(asnNumber.Value);
شرطان:
!asnNumber.HasValue(ASN فارغ): ليس في قاعدة بيانات MaxMind ASN. على الإنترنت الحديث، تُحلّ جميع ISPs الشرعية (Turkcell، TTNet، Vodafone TR، إلخ)، فالفارغ يساوي نطاقًا مُخصّصًا حديثًا أو IP نفايات. وفي Sub-Faz A++ أصلح هذا القرار ~60 مصدر تسرّب.DataCenterAsns.Contains(asnNumber.Value): قائمة الـ 42 ASN (القائمة الكاملة في Pillar 1)، وتشمل AWS و GCP و Azure و DigitalOcean و OVH و Hetzner و M247 (VPN backbone) و HostHero و IPXO وغيرها.
ISPs (Turkcell 16135، TTNet 9121، Vodafone TR 15897، Bharti Airtel IN 45609) مُستبعَدة، لذا فإنّ خطر الإنذار الكاذب صفر.
بطاقة لوحة الإدارة «ملخص الحركة»
┌─ ملخص الحركة (آخر 24 ساعة) ─────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │ 15 │ │ 5 │ │ 10 │ │ 15 ││
│ │ إجمالي │ │ حقيقي │ │ مشبوه │ │ حظر ││
│ │ فريد │ │ (33%) │ │ (67%) │ │ (CF) ││
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘│
│ │
│ نسبة الحقيقي مقابل المشبوه: │
│ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 33% حقيقي │
│ │
│ النطاق: [اليوم] [آخر 7 أيام] [آخر 30 يومًا] │
└─────────────────────────────────────────────────────────┘
7 أيام rolling: 117 حقيقي مقابل 246 إجمالي، أي 67.8% نسبة مشبوهة. وأسفل لوحة المعلومات يُظهر جدول سلسلة زمنية لتتبع الاتجاه الشهري.
Backfill 246 صفًا
كان هناك 246 صفًا موجودًا في PageHits، فاحتجنا إلى UPDATE جماعي يضبط IsSuspectNetwork:
UPDATE dbo.PageHits
SET IsSuspectNetwork = 1
WHERE AsnNumber IS NULL OR AsnNumber IN (/* قائمة 42 ASN */);
-- التحقق
SELECT
COUNT(*) AS Total,
SUM(CASE WHEN IsSuspectNetwork = 1 THEN 1 ELSE 0 END) AS Suspect,
SUM(CASE WHEN IsSuspectNetwork = 0 THEN 1 ELSE 0 END) AS Real
FROM dbo.PageHits;
-- النتيجة: 246 / 165 مشبوه / 81 حقيقي
في الأسبوع الأول، تأكد عدد البشر الحقيقي عند حوالي 33%. وهذا سبب وجود Pillar 2: بيانات أصلية، ومحتوى قائم على الحقائق.

نمط MaxMind GeoIP Singleton Reader
يصل حجم ملفات MaxMind المُحمَّلة في الذاكرة إلى نحو 80MB لـ City و ~6MB لـ ASN، ووقت boot الإضافي لكل pod هو ~1.5 ثانية. هذه الأرقام، الصغيرة في الظاهر، تجعل الإعداد الصحيح حاسمًا، إذ تحوي جداول PageHits و AttackProbes أعمدة Country و City و ASN و AsnNumber، وأقوم بهذا الإثراء مع MaxMind.GeoIP2.DatabaseReader. فمع خطأ بسيط واحد في تسجيل الـ DI، يفتح كل طلب الملف ويغلقه بآلاف IOs في الدقيقة.
السلامة الخيطية لـ DatabaseReader
وثائق MaxMind واضحة: DatabaseReader آمن من الناحية الخيطية، و instance واحد يمكن مشاركته عبر التطبيق بأكمله. في الواقع يجب مشاركته، إذ إنّه struct file-backed memory-mapped، والفتح مكلف (~80MB City و ~6MB ASN). نمط singleton هو الإجابة الصحيحة.
غلاف GeoIPDatabaseReaders
غلاف minimal لتسجيل DI singleton:
public sealed class GeoIPDatabaseReaders : IDisposable
{
public DatabaseReader CityReader { get; }
public DatabaseReader AsnReader { get; }
public GeoIPDatabaseReaders(IWebHostEnvironment env)
{
var dir = Path.Combine(env.ContentRootPath, "data", "geoip");
CityReader = new DatabaseReader(Path.Combine(dir, "GeoLite2-City.mmdb"));
AsnReader = new DatabaseReader(Path.Combine(dir, "GeoLite2-ASN.mmdb"));
}
public void Dispose() { CityReader?.Dispose(); AsnReader?.Dispose(); }
}
// Program.cs
services.AddSingleton<GeoIPDatabaseReaders>();
Disposable بحيث تُغلق file handles بشكل نظيف عند خروج العملية، ولا يتم تشغيل Dispose في تدفق طلب عادي.
قياس الأداء
زمن الإثراء لكل IP:
| المقياس | p50 | p95 | p99 |
|---|---|---|---|
| بحث City | 0.12ms | 0.34ms | 0.81ms |
| بحث ASN | 0.08ms | 0.21ms | 0.52ms |
| الإجمالي | 0.20ms | 0.55ms | 1.33ms |
حوالي 85MB memory-mapped لكل pod (ملفات City و ASN). هذه الأرقام تبقى مستقرة في إنتاج 24/7، ولا قمم تُذكر.
استراتيجية تحديث DB
MaxMind GeoLite2 tier مجاني يتحدث أسبوعيًا، وثمّة نهجان:
- Cron + pod restart:
wgetmmdb الجديد، تحديث k8s ConfigMap، pod restart. - Init container: عند pod boot، حاوية
init-geoipتنزل mmdb، والحاوية الرئيسية تحمل singleton.
اخترنا الأوّل لأسباب عملية: تحكم صريح، مُدقَّق، وإعادة التشغيل تحدث على أيّ حال. إضافات ISP جديدة بنحو 50 صف أسبوعيًا، وسجل ASN لدينا يضمّ بالفعل 42 ASN، أمّا تغييرات ISP فتأثيرها minimal.
وقت Boot
عند تحميل DB الأول، يستغرق pod ~1.5 ثانية أطول ليكون جاهزًا (بدء Kestrel وتحميل GeoIP). تم ضبط liveness probe initial delay على 10 ثوانٍ، ولذا فإنّ هذه ليست مشكلة. يَنتظر readiness probe اتصال DB، لا GeoIP، فالأمر مستقل.
📊 [مقياس: GeoIP lookup p50/p95/p99 في Grafana panel]
Log → AttackProbes Backfill ETL
ذهب جدول AttackProbes إلى الإنتاج في 2026-05-10. تسجيلات الهجوم للفترة السابقة موجودة في جدول dbo.Logs (Serilog)، ولدعم التحليل بأثر رجعي يجب نقلها إلى AttackProbes. وقد تم ذلك كـ idempotent ETL.
لماذا backfill؟
أضاف ثلاث قيم:
- تحليل الاتجاه: مخطط «تغيير نمط هجوم قبل مارس 2026 مقابل بعده».
- تدقيق: التأكد من أنّ IPs المهاجمة التاريخية وصلت إلى قائمة الحظر.
- تناسق البيانات: رقم Pillar 2 «157 محاولة / 24h» يمكن مقارنته بالتاريخ.
علامة عمود Source
لتمييز صفوف backfill عن إدراجات middleware الحية، يحمل AttackProbes.Source قيمتين:
Source = 'middleware'للسجلات الحية الواردة من TrafficClassifierMiddleware.Source = 'log_backfill'لسجلات ETL القادمة من السجلات التاريخية.
تُصفّي استعلامات التقارير بـ WHERE Source = 'middleware'، وإذا احتاج الأمر إلى تنظيف رجعي، فإنّ DELETE FROM AttackProbes WHERE Source = 'log_backfill' يفعله في سطر واحد.
سكربت ETL: 2026-05-10-backfill-attack-probes-from-logs.sql
-- Idempotent: تخطّى إذا كان موجودًا بالفعل (NOT EXISTS guard)
INSERT INTO dbo.AttackProbes (
HitTime, DayIso, Source, RawIp, Method, Path,
UserAgent, Referer, StatusCode,
Category, Severity, Confidence, PatternMatched
)
SELECT
e.LogEvent_Date AS HitTime,
CONVERT(CHAR(10), e.LogEvent_Date, 23) AS DayIso,
'log_backfill' AS Source,
-- استخراج IP regex من رسالة log
SUBSTRING(e.Message,
CHARINDEX('"ClientIp":"', e.Message) + 12,
CHARINDEX('"', e.Message, CHARINDEX('"ClientIp":"', e.Message) + 12)
- CHARINDEX('"ClientIp":"', e.Message) - 12
) AS RawIp,
'GET' AS Method,
-- استخراج المسار regex
SUBSTRING(e.Message, /* ... */) AS Path,
NULL AS UserAgent,
NULL AS Referer,
NULL AS StatusCode,
-- النمط → تعيين الفئة
CASE
WHEN e.Message LIKE '%/.env%' THEN 'secret_disclosure'
WHEN e.Message LIKE '%/.aws%' THEN 'cloud_creds_probe'
WHEN e.Message LIKE '%/.git/%' THEN 'vcs_disclosure'
WHEN e.Message LIKE '%/wp-admin%' THEN 'wordpress_probe'
WHEN e.Message LIKE '%/phpmyadmin%' THEN 'phpmyadmin_probe'
WHEN e.Message LIKE '%/admin.php%' THEN 'admin_panel_probe'
ELSE 'unknown'
END AS Category,
-- متوسطات Severity / confidence (ضبط دقيق حسب المسار)
3 AS Severity, 75 AS Confidence,
'log-pattern-match' AS PatternMatched
FROM dbo.Logs e
WHERE e.Level = 'Warning'
AND e.Message LIKE '%TrafficClassifier%'
AND NOT EXISTS (
SELECT 1 FROM dbo.AttackProbes ap
WHERE ap.Source = 'log_backfill'
AND ap.HitTime = e.LogEvent_Date
AND ap.RawIp = SUBSTRING(e.Message, /* ... */)
);
النتيجة
| المقياس | الرقم |
|---|---|
| إجمالي صفوف log المفحوصة | ~8500 |
| صفوف مُضافة إلى AttackProbes | 503 |
| IPs فريدة | 53 |
| فئات فريدة | 9 |
| مدة ETL | ~12 ثانية |
| Re-run آمن | ✅ NOT EXISTS guard |
تجعل الـ 503 صفًا الجديدة مع 246 الحية الحصيلة الإجمالية 24h اليوم 157 و التاريخ 503 = 660 سجلًا قابلًا للتحليل، وهو ما يكفي لإحصاءات قاعدية صلبة.
مقارنة قبل/بعد backfill
SELECT Source, COUNT(*) AS Records,
MIN(HitTime) AS FirstHit, MAX(HitTime) AS LastHit
FROM dbo.AttackProbes
GROUP BY Source;
النتيجة:
| Source | Records | First | Last |
|---|---|---|---|
| middleware | 157 | 2026-05-10 09:14 | 2026-05-11 09:14 |
| log_backfill | 503 | 2026-04-22 03:51 | 2026-05-10 08:55 |
📊 [مخطط: محاولات الهجوم التراكمية حسب Source line chart]
حلقة الحظر الأصلي: TrafficClassifier → IpBlocklist → CF List
«ip.src in $bilalkose_auto_block»: قاعدة WAF واحدة على حافة Cloudflare هي نقطة نهاية حلقة الحظر بأكملها. أمّا الجزء الأصلي من الحلقة فيتألف من ثلاث خطوات متتالية: مُصنِّف أصلي يكتب إلى DB blocklist، ثم دفع إلى حافة Cloudflare، وأخيرًا تحديث رابط CloudflareListItemId داخل صف القائمة المحلية.
مخطط التدفق
Request → TrafficClassifier → نمط هجوم مطابق؟
↓ نعم
AttackProbe INSERT
↓
AttackBurstCounter MERGE+OUTPUT
↓
ThreatScore ≥ 11 أو Count ≥ 3؟
↓ نعم
IpBlocklist INSERT (24h TTL)
↓ fire-and-forget
CloudflareIpListService.AddBlockedIpAsync()
↓
UPDATE dbo.IpBlocklist SET CloudflareListItemId = @id
↓
(الطلب التالي يتوقف عند الحافة عبر قاعدة WAF)
حظر ثنائي الطبقة
يصل الطلب الأول بعد الحظر إلى الأصل مرة أخرى (في غضون 24h، من IP آخر)، فيُجري بحث DB، بينما يخزّن IMemoryCache بـ bl: prefix النتيجة لمدة 60 ثانية. حتى إذا أطلق نفس IP 100 طلبًا في 60 ثانية، فإنّ هذا لا يزال يُجري DB query واحدًا فقط، أمّا الباقي فيتحول إلى cache hits ينتهي كلٌّ منها بـ 403 سريع على مستوى الأصل.
على جانب حافة Cloudflare، قاعدة WAF «ip.src in $bilalkose_auto_block» تلتقطه (Pillar 1 H2-3 تفصيل). يصل الطلب الأول إلى الأصل، والباقي يموت على الحافة، فتنظف بذلك حركة الأصل.
مخطط IpBlocklist
CREATE TABLE dbo.IpBlocklist (
RawIp NVARCHAR(45) PRIMARY KEY,
BlockedAt DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
ExpiresAt DATETIME2(0) NOT NULL,
Reason NVARCHAR(200) NOT NULL,
ThreatScore INT NOT NULL,
HitCount INT NOT NULL DEFAULT 1,
LastHitAt DATETIME2(0) NOT NULL,
CloudflareListItemId NVARCHAR(64) NULL
);
صف واحد لكل IP. CloudflareListItemId فارغ مبدئيًا، ويُملأ بـ UPDATE عند نجاح دفع Cloudflare، وهو مطلوب من قبل TTL expirer (يحتاج الحذف إلى معرّف العنصر).
خدمة استضافة منتهية الصلاحية TTL
IpBlocklistExpiryHostedService يعمل كل دقيقة:
public async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var expired = await _db.IpBlocklist
.Where(x => x.ExpiresAt < DateTime.UtcNow)
.ToListAsync(ct);
foreach (var entry in expired)
{
if (entry.CloudflareListItemId != null)
await _cf.RemoveBlockedIpAsync(entry.CloudflareListItemId, ct);
_db.IpBlocklist.Remove(entry);
}
await _db.SaveChangesAsync(ct);
await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
}
يبقى تاريخ السجل المحذوف في AttackProbes للتدقيق، فتاريخ الهجوم لا يُفقد، وإنّما تُنظَّف فقط قائمة الحظر النشطة.
التجاوز اليدوي
/admin/security/ip-blocklist صفحة Razor مع أربعة إجراءات (موازية لـ Pillar 1 H2-4):
- Index: جدول قائمة الحظر النشطة.
- PushCf: دفع IP واحد (إلى CF List).
- PushAll: دفع جماعي لكل IPs بدون CloudflareListItemId.
- Remove: DB و حذف CF List sync.

Cross-pillar: جانب الحافة لهذه الحلقة في Pillar 1 H2-4. تغطي pillars معًا خط الأنابيب الكامل.
لوحة الإدارة للزوار الحقيقيين
تخزين البيانات في DB ليس كافيًا، إذ يحتاج الـ admin إلى طبقة تصور ذات معنى. بنيتها مع Razor و Chart.js minimal stack، بلا إطار SPA.
تخطيط الصفحة
┌─────────────────────────────────────────────────────┐
│ Header — بطاقات 4 أرقام (الإجمالي، الحقيقي، المشبوه، الحظر)│
├─────────────────────────────────────────────────────┤
│ مخطط الحركة الساعية الخطي (24h) │
├─────────────────────────────────────────────────────┤
│ Top 10 Country │ Top 10 ASN │ Top 10 Path │
├─────────────────────────────────────────────────────┤
│ آخر 50 محاولة هجوم (جدول real-time) │
├─────────────────────────────────────────────────────┤
│ جدول ملخص 7 أيام │
└─────────────────────────────────────────────────────┘
Razor + Chart.js minimal stack
لا React ولا Vue ولا SignalR (حتى الآن). صفحة Razor مع <script> inline يبدأ Chart.js، فيما يُعيد AJAX endpoint بيانات JSON، مع polling كل 30 ثانية.
<canvas id="hourlyChart" height="100"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
fetch('/admin/security/api/hourly?days=1')
.then(r => r.json())
.then(data => new Chart('hourlyChart', {
type: 'line',
data: {
labels: data.hours,
datasets: [
{ label: 'حقيقي', data: data.real, borderColor: '#10b981' },
{ label: 'مشبوه', data: data.suspect, borderColor: '#6b7280' }
]
}
}));
</script>
الأداء
تحدث استعلامات التجميع في SQL Server (window functions و GROUP BY DayIso)، مع Output cache 60 ثانية، أي أنّه حتى إذا رأت صفحة admin 60 hits في الدقيقة، يكفي استعلام DB واحد. ويبلغ حوالي 5MB memory overhead لكل pod، و ~30KB network لكل صفحة (Chart.js CDN).
Auth + RBAC
ASP.NET Core Identity، مع [Authorize(Roles = "Admin")]. cookie-based auth، وجلسة 12 ساعة. جميع endpoints لوحة admin محمية بهذه السمة:
[Authorize(Roles = "Admin")]
[Route("admin/security")]
public class SecurityController : Controller { /* ... */ }

تصدير مقاييس Prometheus
تخدم بيانات DB احتياجات الـ admins، أمّا للحي والتنبيه فنحتاج إلى Prometheus + Grafana: pull-based، K8s-native، مجاني.
تكامل مكتبة prometheus-net
// Program.cs
app.UseMetricServer(); // يفتح /metrics endpoint
app.UseHttpMetrics(); // ASP.NET Core auto request count + latency
// k8s Service annotation
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
Prometheus يقوم بـ scrape /metrics كل 15 ثانية، ويكتب إلى تخزين السلاسل الزمنية، مع احتفاظ افتراضي بمدة 15 يومًا.
العدادات المخصصة
public static class SecurityMetrics
{
public static readonly Counter AttackProbeBlocksTotal =
Metrics.CreateCounter(
"bilal_attack_probe_blocks_total",
"Origin-side attack probe blocks",
"category");
public static readonly Counter IpListItemsAddedTotal =
Metrics.CreateCounter(
"bilal_cf_ip_list_items_added_total",
"Cloudflare List item additions",
"status");
public static readonly Histogram ClassifyLatency =
Metrics.CreateHistogram(
"bilal_classify_latency_seconds",
"TrafficClassifier middleware execution time");
}
في المسار الحرج للـ middleware، أُغلّف using (SecurityMetrics.ClassifyLatency.NewTimer()) { ... } للـ latency، وأستخدم label «category» لتفصيل فئة الهجوم.
لوحات Grafana
- معدل الحظر:
rate(bilal_attack_probe_blocks_total[5m])line chart. - تفصيل الفئة:
sum by (category) (bilal_attack_probe_blocks_total)pie chart. - Latency p95:
histogram_quantile(0.95, rate(bilal_classify_latency_seconds_bucket[5m])). - معدل نجاح دفع CF:
bilal_cf_ip_list_items_added_total{status="ok"} / sum(bilal_cf_ip_list_items_added_total).
مجموعة تنبيه Tier1-critical-security
10 قواعد تنبيه (مرجع الذاكرة project_grafana_alerting_2026_may):
- alert: AttackProbeSurge
expr: rate(bilal_attack_probe_blocks_total[5m]) > 100/60
for: 2m
labels: { severity: critical, tier: 1 }
annotations:
summary: "انفجار هجوم أعلى بكثير من العادي"
description: "آخر 5 دقائق {{ $value }} req/sec — انفجار مستخدم وهمي أو قواعد WAF غير كافية"
توجيه Telegram إلى tier-1، أي هاتفي، بإشعار فوري.
ميزانية Cardinality
إذا حصل مزيج labels Prometheus على cardinality عالٍ، انفجر التخزين، فاستخدام Path أو IP كـ label كارثي. لدينا 2 labels فقط:
category(9 قيم فريدة).status(3 قيم فريدة).
إجمالي عدد سلاسل الوقت: 9 + 3 = 12 سلسلة جديدة، أي memory overhead ~0.1MB.

الخصوصية + KVKK: تصميم قياسات خالٍ من PII
تنصّ KVKK المادة 5/2-(f) على أنّ «المعالجة بدون موافقة صريحة جائزة عندما تكون إلزامية للمصالح المشروعة لمراقب البيانات، شريطة ألا تنتهك الحقوق والحريات الأساسية لصاحب البيانات». هذا النصّ تحديدًا هو ما يجعل توافق KVKK (قانون 6698) إلزاميًا لأيّ مدونة في تركيا، ويُضاف GDPR إذا كان لديك مستخدمون أوروبيون. تم تصميم PageHits و AttackProbes خاليًا من PII من اليوم الأول.
ما يُخزَّن، ما لا يُخزَّن
مُخزَّن (شرعي):
- ✅
VisitorHash، أي MD5(IP + salt يومي)، فريد خلال يوم، وغير مُترابط عبر الأيام. - ✅ GeoIP على مستوى المدينة، وليس الشارع أو العنوان.
- ✅ ASN (اسم ISP أو المؤسسة).
- ✅ سلسلة User-Agent.
- ✅ Path و StatusCode و HitTime.
غير مُخزَّن (محظور):
- ❌ SessionId و UserId لـ PageHits (تتبع المستخدم المُسجَّل دخوله منفصل).
- ❌ Email وهاتف واسم.
- ❌ Form data و request body.
- ❌ محتوى Cookie (فقط علم الموافقة).
أساس الاحتفاظ بـ Raw IP (KVKK 5/2-(f))
AttackProbes.RawIp و PageHits.RawIp مُخزَّنان. KVKK المادة 5/2-(f):
يُسمح بالمعالجة بدون موافقة صريحة عندما تكون إلزامية للمصالح المشروعة لمراقب البيانات، شريطة ألا تنتهك الحقوق والحريات الأساسية لصاحب البيانات.
أمن النظام (حظر محاولات الهجوم، اكتشاف أنماط الإساءة) مصلحة مشروعة. ويُفصّل نص الإشعار Privacy.tr.resx، القسم 13، هذا الجانب، بحيث يتم الوفاء بالتزام إخطار KVKK مباشرة.
علم ConsentGranted (cookie bk_consent_v1)
تمت ترقية cookie banner إلى Consent v2 (Sub-Faz A). عند الموافقة:
var consent = httpContext.Request.Cookies["bk_consent_v1"] == "granted";
// ConsentGranted = consent في PageHits INSERT
تُخزَّن سجلات المستخدمين غير الموافقين أيضًا (أمن النظام مصلحة مشروعة)، لكن للاستخدام التحليلي أو التسويقي يُطبَّق مرشح ConsentGranted=1. ويعكس هذا الانقسام تمييز GDPR المادة 6(1)(a) الموافقة الصريحة مقابل (f) المصلحة المشروعة.
احتفاظ تلقائي لمدة 90 يومًا
-- وظيفة مُجدوَلة يومية
DELETE FROM dbo.PageHits
WHERE HitTime < DATEADD(DAY, -90, SYSUTCDATETIME());
DELETE FROM dbo.AttackProbes
WHERE HitTime < DATEADD(DAY, -90, SYSUTCDATETIME());
تُعدّ 90 يومًا «معقولة» ضمن احتفاظ KVKK، وهي مذكورة في نص الإشعار.
تُعالَج النسخ الاحتياطية بصفتها شاغلًا مستقلًا. ولأنّ التصميم خالٍ من بيانات PII أصلًا، فلا تنشأ مخاطر إضافية من النسخ، ويكفي تشفيرها وضبط التحكم في الوصول إليها.
عملية طلب KVKK
إذا أراد صاحب بيانات (مستخدم) ممارسة حقوقه المنصوص عليها في KVKK المادة 11:
- [email protected] email (الاتصال المُدرَج في Privacy.tr.resx).
- الطلب: «حذف»، «تصحيح»، «استفسار الغرض».
- الرد في غضون 30 يومًا (KVKK المادة 13).
بفضل التصميم الخالي من PII، فإنّ «ابحث عن سجلات المستخدم المحدد» غير ممكن، ولذا نُخطر هيئة KVKK بشكل استباقي بهذا («طلب حذف البيانات الشخصية: نظرًا لأنّنا لا نستطيع تحديد المستخدم بشكل فريد، لم يتم العثور على سجل محدد؛ يتم حذف جميع IPs لأمن النظام تلقائيًا في 90 يومًا.»)

الأسئلة المتكررة
Cloudflare Analytics يوفر بالفعل أعداد الزوار، لماذا جدول منفصل؟
يحتفظ Cloudflare Analytics بـ 5 دقائق على free plan (24 ساعة على Pro). الأسوأ من ذلك أنّه يحسب حركة مركز البيانات و VPN كـ «زوار حقيقيين»، أي أنّ قياسنا بنسبة 33%/67% للحقيقي مقابل المشبوه لن يظهر. يعطي DB لدينا 90 يومًا من الاحتفاظ، مع انقسام IsSuspectNetwork، ولوحة معلومات مخصصة. نجمع جانب CF أيضًا مع جامع أحداث WAF في Pillar 1، فالاثنان يكملان بعضهما.
يُدرج PageHits إلى DB في كل طلب، ألا يتسع هذا بشكل سيء؟
تُسقط dedup الـ 30 ثانية IMemoryCache (vh: prefix) الحمل بنحو 70%. حتى إذا أطلق نفس المستخدم 10 طلبات في 30 ثانية، فقط 1 INSERT. وإذا وصلت حركة المرور إلى 10K req/sec، ستنتقل إلى نمط INSERT دفعة غير متزامن مُقسَّم (وسيط Kafka أو RabbitMQ). أمّا للحركة الحالية (50-150 req/دقيقة)، فإنّه مُهندس بشكل مفرط.
لماذا AttackBurstCounter MERGE في DB بدلًا من قاموس في الذاكرة؟
مرونة إعادة تشغيل pod وتناسق متعدد النسخ. ففي إعداد K8s 2+ replica، عداد pod-A غير مرئي لـ pod-B، فـ DB هي المصدر الوحيد للحقيقة. Per-request overhead ~3-5ms (0.1ms في cache hit). لمدونة صغيرة هذا أفضل trade-off، أمّا لـ API عالي الإنتاجية، فإنّ عدادًا مدعومًا بـ Redis (atomic INCR) يناسب أكثر.
هل يسمح KVKK بالفعل بتخزين Raw IP؟
نعم، بموجب المادة 5/2-(f) «المعالجة إلزامية للمصالح المشروعة لمراقب البيانات». أمن النظام (حظر محاولات الهجوم) مصلحة مشروعة. نص الإشعار مُفصَّل في Privacy.tr.resx القسم 13. بعد 90 يومًا، الحذف التلقائي مع التجاوز اليدوي للإدارة واتصال استفسار ([email protected]).
ما تكلفة Prometheus + Grafana on-prem؟
prometheus-net ترخيص MIT، مجاني. Prometheus و Grafana OSS، مجاني. K8s pod overhead: Prometheus ~500MB RAM (15s scrape interval، احتفاظ 30 يوم)، و Grafana ~200MB. الإجمالي $0 ترخيص و ~$5/شهر K8s node share. بدائل SaaS: Grafana Cloud free 10K series، $19/شهر 50K series. on-prem يفوز في التحكم، إذ إنّ قواعد التنبيه ولوحات المعلومات git-tracked. ولتقدير تأثير حاويات المراقبة هذه على ميزانية العقد والتكلفة الشهرية مسبقًا، يمكنك استخدام حاسبة ميزانية موارد Kubernetes.
ألا يخلق علم IsSuspectNetwork خطر إنذار كاذب؟
قائمة 42 ASN ضيقة: فقط مزوّدي مركز البيانات والسحابة و VPN. ISPs (Turkcell 16135، TTNet 9121، Vodafone TR 15897، Bharti Airtel IN 45609) مُستبعَدة. Google ASN 15169 أيضًا مُستبعَد (Googlebot في LegitBotUaPatterns، وما تبقى هو مستخدمون حقيقيون مستضافون على Google Cloud). 0 إنذارات كاذبة في الملاحظة الـ 24 ساعة. ولتتبع إضافات ISP الجديدة، تحتاج قاعدة بيانات MaxMind ASN إلى تحديثات أسبوعية.
مقالات ذات صلة
clusters هذا pillar (ترتيب النشر):
- C1 PageHits middleware: minimal allocation request count pipeline
- C2 AttackProbes: ThreatScore + تفصيل 9 فئة هجوم
- C3 AttackBurstCounter: تعمّق في نمط SQL MERGE+OUTPUT
- C4 علم IsSuspectNetwork: مصفوفة قرار VPN/مركز بيانات
- C5 تكامل MaxMind GeoIP: السلامة الخيطية لـ singleton reader + lookup latency
- C6 Log → AttackProbes idempotent backfill ETL pattern
- C7 حلقة الحظر الأصلي + Cloudflare List sync
- C8 لوحة الزوار الحقيقيين: Razor + Chart.js minimal stack
- C9 تصدير مقاييس Prometheus: ميزانية cardinality
- C10 PageHits متوافق مع KVKK: تصميم خالٍ من PII
Cross-pillar bridge (Pillar 1):
🌉 أمان حافة Cloudflare مع .NET على K8s (Pillar 1): قواعد WAF و IP Blocklist و ASN Registry و Origin Firewall. يغطي pillars معًا خط الأنابيب الكامل لـ الحافة + الأصل للأمان.
Linkable assets:
- 🔗 ASN Registry CSV (gist): قائمة 42 ASN مركز بيانات مفتوحة المصدر، MIT (مشتركة مع Pillar 1)
- 🔗 24h Anonymized Telemetry Snapshot CSV (gist): تفصيل حقيقي مقابل مشبوه
- 🔗 PageHits Middleware Starter for ASP.NET Core 10 (GitHub repo): مرجع minimal allocation
التعليقات (0)
لا توجد تعليقات بعد. كن أول من يعلق.