المقال ده اتكتب من جديد في أكتوبر 2026. نسخة أقدم منه كانت بتوصف بوابة correlation أشد، وكانت بتذكر رقم لتقليل الـ false positives. ولا الوصف ولا الرقم كانوا دقيقين: أول إصدار كانت البوابة فيه معتمدة على الخطورة بس، وتأثير الـ correlation على الـ false positives عمره ما اتقاس. النسخة دي بتشرح البوابة بتشتغل إزاي فعلًا.
أغلب بوابات الأمان في الـ CI بتاخد قرار واحد: هل فيه أي finding خطورته (severity) عند threshold معيّن أو أعلى؟ لو أيوه، الـ build بيفشل. القاعدة دي سهل تتشرح، وسهل تجاوزها. الـ findings اللي مش مُثبتة بتوقّف الإصدارات، والفِرق بتتعلم تعمل override للبوابة، وقيمة البوابة بتقل مع الوقت.
وفيه كمان فشل أصعب في الملاحظة. لو scanner مشتغلش — الأداة مكانتش متسطّبة، أو الـ target مكانش ينفع يتوصل له، أو الـ configuration كانت غلط — مبيطلّعش أي findings، وغياب الـ findings شكله بالضبط زي فحص ناجح من غير مشاكل.
درجة الخطورة لوحدها مش كفاية
- الخطورة (severity) بتقول الـ finding هيبقى خطير قد إيه لو طلع حقيقي. ومبتقولش أي حاجة عن احتمال إنه يكون حقيقي.
- الـ DAST بيشوف السلوك، بس مش بيشوف الكود: ممكن يبيّن إن endpoint بيرد بشكل غلط من غير ما يعرف أنهي handler اللي طلّع الرد ده.
- الـ SAST بيشوف الكود، بس مش بيشوف السلوك: ممكن يعلّم على pattern خطير من غير ما يعرف الـ path ده ممكن يتوصل له ولا لأ، أو عليه حماية بالفعل ولا لأ.
عشان كده Fendix بيحتفظ بالخطورة — هي لسه بتوصف التأثير — بس قرار إن الـ finding يوقّف الـ build ولا لأ بياخده من الدليل اللي وراه.
الـ correlation إشارة، مش البوابة
لما static finding و runtime finding يوصفوا نفس المشكلة في نفس المكان، Fendix بيدمجهم في correlated finding واحد. الـ matching بيمشي على cascade ثابت، مش على weighted score: لازم الـ categories تكون مرتبطة ببعض، وبعدين لازم الأماكن تتطابق بالـ route pattern، أو بالـ endpoint بعد الـ normalisation، أو بالـ path suffix، أو بـ path segment مشترك، بالترتيب ده.
// Simplified from the correlator: is this static (whitebox) finding
// the same issue as this runtime (blackbox) finding?
func sameIssue(w, b Finding) bool {
if !relatedCategories(w.Category, b.Category) {
return false
}
switch {
case w.Route != "" && routeMatches(w.Route, b.Method, b.URL):
return true // route pattern confirmed against the live request
case normalize(w.Endpoint) == normalize(b.Endpoint):
return true
case suffixMatches(w.Endpoint, b.Endpoint):
return true
default:
return sharesSegment(w.Endpoint, b.Endpoint)
}
}اتفاق الـ engines مع بعض دليل قوي، بس هو إشارة واحدة من سبع إشارات مستقلة. route اتعمله confirm، أو taint path ممكن يتوصل له، أو payload اتعمله validation، أو أداة مستقلة بتبلّغ عن نفس الـ CWE في نفس المكان — دول كمان بيتحسبوا. وفيه أدلة كفاية لوحدها: رد فعلي من الـ API بيبيّن المشكلة، أو credential حقيقي اتعمله commit في كود الـ production، ممكن يوقّف الـ build من غير engine تاني. والـ correlation عمره ما بيشيل finding؛ كل حاجة بتفضل في التقرير.
درجة ثقة نقدر نراجع أسبابها
كل finding بياخد درجة ثقة (confidence score) deterministic من 0 لـ 100. بتبدأ من قيمة أساسية (base)، وكل قاعدة بتنطبق بتزوّد أو بتنقّص مقدار ثابت، ومعاها سبب مكتوب بكلام بسيط. وفيه اختبارات بتضمن إن مجموع النقاط المرتبطة بالأسباب يساوي الـ score، عشان محدش يبقى مضطر يثق في black box.
confidence_score: 90 (HIGH)
base 35
+ static evidence 10
+ runtime evidence 10
+ cross-engine agreement 25
+ route confirmed 10
bands: HIGH >= 70, MEDIUM >= 40, LOW < 40إيه اللي بيوقّف الـ build فعلًا
- تحت الـ threshold بتاع --fail-on: يبقى WARN (لـ medium وأعلى) أو INFO. عمره ما يبقى block.
- عند الـ threshold أو أعلى منه، والثقة LOW: يبقى WARN.
- عند الـ threshold أو أعلى منه، والثقة MEDIUM ومفيش إشارة مستقلة: يبقى WARN.
- غير كده: BLOCK — مع كام حالة القرار بيتخفّف فيها، زي الـ test fixtures اللي مفيهاش دليل مستقل، أو dependency فيها ثغرة بس عمرها ما بتتعمل import.
بوابة الثقة دي نزلت في v2.0 كـ breaking change: فيه builds كانت بتفشل وبقت دلوقتي بتعدّي. عشان كده --enforce-confidence=false بيرجّع بالضبط السلوك القديم اللي كان معتمد على الخطورة بس، وفيه اختبار بيغطي 54 تركيبة من الخطورة والـ threshold بيضمن إن الخيار ده يفضل محافظ على السلوك القديم. والقرارات اللي بتتاخد بالطريقة دي بتظهر في التقرير بعلامة بتوضّح إنها policy overrides.
الـ scanner اللي مشتغلش مش pass
كل analyzer بيسجّل هو اشتغل، ولا اتعمله skip، ولا فشل، ومعاه reason code. في الـ CLI، فيه flags اختيارية (opt-in) بتحوّل الـ analyzers الناقصة أو الفجوات في التغطية (coverage) لـ exit code بيدل على فشل. وفي الـ hosted workspace، التغطية المطلوبة بتعتمد على الـ scan mode، ولو فيه دليل مطلوب ناقص، قرار الإصدار بيطلع INCOMPLETE بدل PASS.
اللي كنت هقوله لنفسي أيام أول نسخة
- اكتب الـ semantics بتاعة البوابة كاختبارات قبل ما تكتب الـ README. أول README كان بيوصف قاعدة الكود مكانش بينفّذها.
- انشر الـ benchmarks ومعاها حدودها. الـ corpus المختار بعناية بيقيس القواعد على حالات إنت اللي كاتبها، مش معدل اكتشاف (detection rate) في الواقع.
- أي تغيير يخلّي البوابة أشد أو أخف اعتبره breaking change، ونزّل معاه escape hatch بيرجّع السلوك القديم بالضبط.
الـ engine بتاع Fendix مفتوح المصدر (MIT) على github.com/Fendix-app/Fendix؛ والـ hosted workspace على fendix.dev. دراسة الحالة كاملة على /ar/case-studies/fendix