قياسات الأمان من جانب الأصل مع .NET و Kubernetes: كشف الزوار الحقيقيين

0 تعليق 520 مشاهدة

الأمن السيبراني kubernetes dotnet قياسات الأمان ThreatScore MaxMind GeoIP SQL Server Prometheus KVKK

26 دقيقة قراءة 5070 كلمة

ملخص قصير

إذا كانت لوحة التحليلات لديك تُظهر 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) تأتي مباشرة من تجربة الإنتاج.


جدول المحتويات

  1. ما هي قياسات الأمان ولماذا من جانب الأصل؟
  2. بنية PageHits Middleware
  3. AttackProbes + صيغة ThreatScore
  4. AttackBurstCounter: نافذة منزلقة MERGE+OUTPUT
  5. علم IsSuspectNetwork: مركز بيانات / VPN / ASN فارغ
  6. نمط MaxMind GeoIP Singleton Reader
  7. Log → AttackProbes Backfill ETL
  8. حلقة الحظر الأصلي: TrafficClassifier → IpBlocklist → CF List
  9. لوحة الإدارة للزوار الحقيقيين
  10. تصدير مقاييس Prometheus
  11. الخصوصية + KVKK: تصميم قياسات خالٍ من PII
  12. الأسئلة المتكررة
  13. مقالات ذات صلة

ما هي قياسات الأمان ولماذا من جانب الأصل؟

إذا كنت تستخدم حافة CDN مثل Cloudflare، فأنت تُوقِف معظم الهجمات على الحافة بالفعل، وقد تناولت ذلك بالتفصيل في Pillar 1. غير أنّ الحافة عديمة الحالة، إذ تقتصر على مُطابقة الأنماط وبحث IP واحتواء UA. أمّا القرارات ذات الحالة، كسلوك الانفجار وتاريخ الجلسة وتراكم ThreatScore، فتنتمي إلى الأصل، وهنا تعيش قياسات الأمان من جانب الأصل.

حيث تقصر الحافة

تصفية الحافة غير كافية في ثلاثة سيناريوهات:

  1. مهاجم بطيء-منخفض: يطلق طلبًا واحدًا في الساعة، لكن إلى مسار حساس مختلف كل ساعة، فترى الحافة «طلب واحد» ولا تُطابق النمط، بينما يرى الأصل «نفس IP، 8 مسارات admin في 8 ساعات».
  2. حشو بيانات اعتماد موزّع: brute force ضد نفس المستخدم من IPs مختلفة. لا تتجاوز الحافة حد معدل IP، فيما يطبّق الأصل قفلًا مرتبطًا بالمستخدم.
  3. شذوذ سلوكي: ينحرف مستخدم مُصادَق عن السلوك الطبيعي (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 آخر 7 أيام: 33% زوار حقيقيون مقابل 67% مشبوهة (DC/VPN/null ASN) — قياسات Sub-Faz A++


بنية PageHits Middleware

PageHits + AttackProbes + IpBlocklist + AttackBurstCounter: مخطط ER لـ Sub-Faz A++
PageHits + AttackProbes + IpBlocklist + AttackBurstCounter: مخطط ER لـ Sub-Faz A++

قلب النظام 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 أحادي الاتجاه.

dbo.PageHits آخر 7 أيام توزيع الدولة + ASN: أعلى 15 صفًا، عمود نسبة IsSuspectNetwork


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، وليس قاموس داخل الذاكرة؟

ثلاثة أسباب:

  1. تناسق متعدد النسخ: عداد pod-A غير مرئي لـ pod-B، فـ DB هي المصدر الوحيد للحقيقة.
  2. مرونة إعادة تشغيل pod: تُمحى حالة الذاكرة عند إعادة التشغيل، بينما تستمر DB.
  3. سجل تدقيق: «متى، وكم مرة، وأيّ نافذة» قابل للاستعلام.

التنازل المتبقّي هو أنّ كل طلب هجوم يضيف 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);

شرطان:

  1. !asnNumber.HasValue (ASN فارغ): ليس في قاعدة بيانات MaxMind ASN. على الإنترنت الحديث، تُحلّ جميع ISPs الشرعية (Turkcell، TTNet، Vodafone TR، إلخ)، فالفارغ يساوي نطاقًا مُخصّصًا حديثًا أو IP نفايات. وفي Sub-Faz A++ أصلح هذا القرار ~60 مصدر تسرّب.
  2. 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: بيانات أصلية، ومحتوى قائم على الحقائق.

/admin/dashboard بطاقة «Trafik özeti»: 4 أرقام (اليوم/7 أيام × حقيقي/مشبوه) + شريط تقدم 67.8% مشبوه


نمط 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 مجاني يتحدث أسبوعيًا، وثمّة نهجان:

  1. Cron + pod restart: wget mmdb الجديد، تحديث k8s ConfigMap، pod restart.
  2. 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؟

أضاف ثلاث قيم:

  1. تحليل الاتجاه: مخطط «تغيير نمط هجوم قبل مارس 2026 مقابل بعده».
  2. تدقيق: التأكد من أنّ IPs المهاجمة التاريخية وصلت إلى قائمة الحظر.
  3. تناسق البيانات: رقم 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.

صفحة Razor /admin/security/ip-blocklist: 15 IP، جميعها فعّالة على Cloudflare edge + زر «Push to CF»

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 { /* ... */ }

/admin/dashboard بطاقة «Trafik özeti» بتقريب — أرقام مُجمَّعة، بدون IP


تصدير مقاييس 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.

لوحة أمان Grafana — 30 يومًا: أحداث WAF حسب المضيف، معدل إضافة/إزالة قائمة حظر IP، قائمة الحظر النشطة عبر الزمن، وتوزيع فئات المصدر


الخصوصية + 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 مباشرة.

تمت ترقية 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:

  1. [email protected] email (الاتصال المُدرَج في Privacy.tr.resx).
  2. الطلب: «حذف»، «تصحيح»، «استفسار الغرض».
  3. الرد في غضون 30 يومًا (KVKK المادة 13).

بفضل التصميم الخالي من PII، فإنّ «ابحث عن سجلات المستخدم المحدد» غير ممكن، ولذا نُخطر هيئة KVKK بشكل استباقي بهذا («طلب حذف البيانات الشخصية: نظرًا لأنّنا لا نستطيع تحديد المستخدم بشكل فريد، لم يتم العثور على سجل محدد؛ يتم حذف جميع IPs لأمن النظام تلقائيًا في 90 يومًا.»)

/tr/Privacy قسم «Güvenlik kayıtları (zorunlu — onaysız)»: KVKK م.5/2-(f) + م.12 + GDPR 6(1)(f) + المادة 32 + 90 يومًا + Cloudflare WAF + قانون 5651


الأسئلة المتكررة

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:

  1. 🔗 ASN Registry CSV (gist): قائمة 42 ASN مركز بيانات مفتوحة المصدر، MIT (مشتركة مع Pillar 1)
  2. 🔗 24h Anonymized Telemetry Snapshot CSV (gist): تفصيل حقيقي مقابل مشبوه
  3. 🔗 PageHits Middleware Starter for ASP.NET Core 10 (GitHub repo): مرجع minimal allocation

الأسئلة الشائعة

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.

ألا يخلق علم 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 إلى تحديثات أسبوعية.

التعليقات (0)

اترك تعليقاً وتقييماً

لا توجد تعليقات بعد. كن أول من يعلق.