
تحقق بشكل متقاطع من عروض سطح الهجوم الخاص بك واعثر على نقاط النهاية التي لا يمكنها تأكيد بعضها البعض.
تحقق من تطابق وجهات نظر سطح الهجوم الخاص بك واعثر على نقاط النهاية التي لا يمكنها تأكيد بعضها البعض.
يجب أن تكون نقطة النهاية قادرة على تبرير وجودها. إنها في الكود، لذا يجب أن يصفها عقد. إنها في العقد، لذا يجب أن ينفذها شيء. إنها تستقبل حركة مرور حقيقية، لذا من الأفضل أن تكون موجودة في مكان ما. عندما تعرف إحدى وجهات النظر عن نقطة نهاية ولا تعرف الأخريات عنها، فإن تلك الفجوة هي النتيجة.
يشغّل alibi أداة OWASP noir، ويقرأ مخرجات JSON الخاصة بها، ويقارن وجهات النظر مع بعضها البعض.
يقرأ Noir بالفعل خمس وجهات نظر مستقلة لنفس السطح:
| وجهة النظر | تُقرأ من |
|---|---|
| code | أكثر من 200 محلل عبر 33 لغة |
| doc | OpenAPI، RAML، WSDL، GraphQL SDL، AsyncAPI، gRPC، Smithy، TypeSpec، OData، OpenRPC |
| traffic | HAR، mitmproxy، Burp، Caido، ZAP، Postman، Insomnia، Bruno، .http |
| gateway | nginx، Apache، Envoy، Kong، Traefik، APISIX، Caddy، Istio، Kubernetes Ingress و Gateway API |
| infra | Terraform، CloudFormation، CDK، Serverless، Vercel، Netlify، Wrangler، Azure Functions، Kamal |
ما لا تفعله هو مقارنتها. هذه هي المهمة الكاملة هنا، وهي لا تحتاج إلى أي تغيير في noir — يشغّله alibi مرة واحدة لكل وجهة نظر ويجمع النتائج.
جزء كل وجهة نظر مهم. يقوم Noir بإزالة التكرار حسب (method, url) عبر كل محلل، لذا فإن مسار Flask ومسار OpenAPI المكتوبين بنفس الطريقة يندمجان في نقطة نهاية واحدة تحمل تقنية واحدة. هذا صحيح بالنسبة لأداة اكتشاف — إنها نقطة نهاية واحدة — لكنه يمحو التأكيد المتبادل الذي بُنيت هذه الأداة لقياسه، ويمحوه في أسوأ اتجاه ممكن: كلما اتفقت وجهتا نظر بشكل أفضل، اختفى المزيد منهما. يُفحص Casdoor بـ 372 نقطة نهاية في الكود و9 موثقة؛ افحص دليل swagger/ الخاص به وحده وستجد أن المواصفة تحتوي على 235.
يقيّد --only-techs مجموعة الكواشف، لذا فإن فحصًا واحدًا لكل وجهة نظر يحافظ على كل واحدة منها كاملة. أي تقنية تتحدث عن أي وجهة نظر هو views.yml؛ وأي التقنيات موجودة هو ما يبلغ عنه noir list techs.
لا يحلل alibi أي تنسيقات API بنفسه. مدخله الوحيد هو JSON الخاص بـ noir.
يتطلب noir الإصدار 1.0.0 أو أحدث على PATH -- هذا هو الإصدار الذي أصبح فيه noir list techs أمرًا فرعيًا، وهذا الكتالوج هو ما يعيّن كل تقنية إلى وجهة نظر. تتبع التطوير إصدار noir الحالي. يتم رفض أي ملف تنفيذي أقدم بالاسم بدلًا من تركه يفشل عند أول قراءة للكتالوج.
$ uv tool install noir-alibi # or: pipx install noir-alibi
$ alibi scan ./my-service
$ alibi scan # the working directory
$ alibi scan ./service ./contracts ./prod.har # or wherever the views live
كل مسار هو مصدر، يُفحص مرة واحدة لكل وجهة نظر. وجّهه إلى أي شيء لديك — شجرة مصدر، دليل مواصفات، ملف التقاط واحد — ووجهات النظر المفقودة تُعطّل قواعدها بدلًا من إغراق التقرير.
alibi · 1 source · 377 endpoints
code 372 doc 235
230 corroborated -- vouched for by more than one view
19 endpoints nearly matched another view -- these may be matching failures, not real gaps
SHADOW Shadow API -- Implemented, but no contract describes it
134 findings · 4 critical, 57 high, 62 medium, 11 low
critical POST /api/upload-groups router.go:87
upload paths carry more consequence than reads
critical POST /api/upload-permissions router.go:208
...
... and 122 more (SHADOW in full: -f json)
TWO SURFACES?
The doc view is 97% under /api, and 37 of these findings are outside it.
If that is a separate surface the contract never covered, narrow the scan:
alibi scan <paths> --ignore '^/(?!api(/|$))'
If it is the same surface left undocumented, they are the findings that matter most.
تتوقف المجموعات عند اثنتي عشرة — الترتيب من الأسوأ أولًا، لذا فإن الذيل هو الجزء الأقل إفادة، و-f json يحتوي على كل شيء.
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
يقول التقرير إن وجهات النظر تختلف؛ ويقول --endpoints ما احتوته كل واحدة منها.
$ alibi scan ./repo -f json --endpoints
تحصل كل وجهة نظر على قائمة: المفتاح، وأي وجهات نظر أكدته، والتقنيات التي تقف خلفه، والملفات، والكتابة قبل التطبيع — وهنا يكون الفرق دائمًا عندما يجب أن يتطابق صفان ولم يتطابقا. إنها ثلاثة إلى أربعة أضعاف بقية الحمولة، لذا فهي علامة وليست الافتراضي.
أو بوّب مباشرة: alibi scan . ./contracts --fail-on high يخرج بقيمة غير صفرية عندما يصل اكتشاف إلى تلك الخطورة. الفحص الذي لم يتمكن noir من قراءته بالكامل يبلّغ عن executionSuccessful: false، لذا فإن تشغيلًا متدهورًا لا يمر كتشغيل نظيف.
يحتفظ Noir بصيغة المسار الخاصة بكل إطار عمل بدلًا من اختراع صيغة مشتركة، لذا تصل نفس نقطة النهاية مكتوبة بعدة طرق:
python_flask /api/users/<int:user_id>
aiohttp /users/{id}
java_spring /api/catalog/{id}
oas3 /v1/pets/{petId}
rails /posts/:id
nginx /admin/.*
القاعدة التي تجعل هذه قابلة للمقارنة: اسم معامل المسار ليس جزءًا من هويته. {petId} و<int:user_id> يصفان نفس الفتحة؛ فقط موضعها وما إذا كانت تمتد عبر / هو ما يهم. تُحفظ الأسماء كدليل ويُبلّغ عنها، لكنها لا تصل أبدًا إلى المفتاح.
توضح الاكتشافات كيف تمت المطابقة:
| الدرجة | المعنى |
|---|---|
G1 | الكتابات اتفقت بالفعل |
G2 | تتفق بمجرد تطبيع صيغة المعامل |
G0 | وجهة نظر واحدة فقط تملكها — لم تتم مطابقة أي شيء |
أداة كهذه تموت بالإبلاغ عن مئات الاكتشافات في أول تشغيل لها، أو بالإبلاغ عن تقدم لم يحققه أحد. ستة أشياء تدفع في الاتجاه المعاكس:
القواعد لا تُفعّل بدون وجهتي النظر معًا. افحص قاعدة كود بدون أي عقود في أي مكان وستكون كل نقطة نهاية مؤهلة تقنيًا كواجهة برمجية ظل غير موثقة. تلك الاكتشافات لا تقول شيئًا سوى أنك لم تقدم أي توثيق، لذا فإن القاعدة لا تعمل إلا عندما تكون كل وجهة نظر تستنتج منها موجودة فعليًا في الفحص. يسمي التقرير القواعد التي لم تشارك.
الاقترابات القريبة تُبلّغ عنها كشك، لا كاكتشافات. "في الكود، ليس في الوثائق" لا يمكن تمييزه عن "في كليهما، لكن alibi فشل في محاذاتهما." لذا تُفحص نقطة النهاية التي تقع في وجهة نظر واحدة مقابل الأخريات بحثًا عن اقتراب قريب — نفس المسار بفعل مختلف، أو على بعد مقطع واحد حيث يكون لأحد الجانبين معامل والآخر قيمة حرفية. الاكتشافات التي تحمل اقترابًا قريبًا تُخفَّض وتُعلَّم للمراجعة. هذا العدد يجلس بجانب الإجماليات، لأن كل اكتشاف موثوق بقدر ما هو صغير.
وجهات النظر التي لم تلتقِ قط هي تشخيص واحد، لا مئات الاكتشافات. يسجّل Argo CD المسار /api في Go ويوثّق 198 مسارًا تحته، لذا لا تشترك شيفرته ومواصفته في نقطة نهاية واحدة. قراءة حرفية لذلك تعني 58 واجهة برمجية ظل و198 عقدًا وهميًا، لا شيء منها حقيقي. صفر تأكيد متبادل بين وجهتي نظر مأهولتين يعني أن المقارنة لم تنجح — نقطة تركيب تمثل المسارات تحتها، أو حزمة لم يتمكن noir من قراءتها — لذا تُحجب القواعد ويُطبع السبب بدلًا من ذلك. المسارات التي يتبين أن لها العديد من نقاط النهاية من وجهات نظر أخرى تحتها تُصنّف كنقاط تركيب محتملة.
عندما تتراصف وجهتا النظر بمجرد إزالة بادئة ثابتة من إحداهما، يقول التشخيص ذلك ويسمي البادئة. تصرّح مواصفة Gitea المولّدة بـ basePath: /GITEA-API-APP-SUBURL/api/v1 بينما يركّب موجّه Go الخاص بها /api/v1؛ لا تشترك وجهتا النظر في شيء، لكن 154 من 535 مسارًا موثقًا تطابق مسار كود بمجرد إزالة تلك المقاطع الثلاثة. هذا basePath في مواصفة، أو servers[].url، أو نقطة تركيب أسقطها قارئ الكود — ويُبلّغ عنه، ولا يُطبّق أبدًا، لأن إعادة محاذاة المسارات ستخفي الخطأ الذي وجدته.
الفيضان الذي هو في الحقيقة شجرة فرعية مفقودة واحدة يُسمّى كواحد: 207 من 354 عقدًا وهميًا في NodeBB تقع تحت /api/v3، حيث لا تحتوي وجهة نظر الكود على أي شيء على الإطلاق.
وجهة نظر مفقودة ووجهة نظر فارغة تعنيان شيئين متعاكسين. يبلّغ Noir عما لم يتمكن من قراءته، ويطبعه alibi فوق الاكتشافات. يشحن NetBox مستند OpenAPI بحجم 12.35MB مع 308 مسارات؛ يتخطاه noir لتجاوزه حد حجم الملف، وبدون ذلك التقرير يصرّح alibi بأن المشروع لا يوثّق شيئًا — ليس مجرد ناقص، بل الإجابة الخاطئة مُصرّح بها بثقة.
قاعدة توقفت عن العمل لم تحل أي شيء. تسجيل الفحوصات ومقارنتها يعيد إدخال نفس الخطأ من بعيد: انسَ دليل العقود في تشغيل واحد وSHADOW لا يقيّم شيئًا، وهو ما يبدو لفرق ساذج تمامًا كأن كل واجهة برمجية ظل قد أُغلقت. في عيّنة وجهات النظر الخمس، أسقط وسيطًا واحدًا فتحولت سبعة اكتشافات قائمة إلى "محلولة". تسجّل اللقطات أي القواعد قيّمت، والاختلافات تنظر فقط في القواعد التي عملت في كلا الفحصين، والباقي يُسمّى تحت NOT COMPARED.
الغياب دليل فقط عندما توجد الإشارة. تغطي واسمات المصادقة في Noir أطر العمل التي تعرفها. في حزمة لا تغطيها، لا شيء يحمل وسم مصادقة، ومعاملة ذلك كـ"غير مصادق" سترفع كل اكتشاف وتستنزف عمود الخطورة من معناه. التعديلات التي تُفعّل عند وسم مفقود تتطلب ظهور ذلك الوسم في مكان ما في الفحص أولًا.
ثم تتغير الخطورة بناءً على ما وجدته واسمات noir: بيانات شخصية، رفع ملفات، لا أثر للمصادقة، أو فعل يغيّر الحالة.
كل من خريطة وجهات النظر (views.yml) والقواعد (rules.yml) هي بيانات، وليست كودًا.
location /api/ واحدة تمثل كل شيء تحتها، لذا تجيب قواعد البوابة والبنية التحتية على سؤال هل يصل هذا إلى تلك النقطة بدلًا من هل يحتوي هذا عليها. عند المقارنة كمجموعات، تبدو كل قاعدة بادئة كمسار لم ينفذه أحد وكل مسار منفذ يبدو غير قابل للوصول.
التغطية سخية عن قصد. يبلّغ Noir عن المسار الذي تطابقه القاعدة لكن ليس ما إذا كانت تطابقه كبادئة أو بالضبط (location = /x، أو Ingress بـ pathType: Exact)، لذا لا يمكن استعادة الدقة — ومعاملة كل قاعدة كبادئة تكبت الاكتشافات بدلًا من اختراعها.
يقول التقرير مقدار الكود الذي تصل إليه كل وجهة نظر توجيه، لأن ما إذا كانت "34 نقطة نهاية لا تصل إليها أي بوابة" حقيقية يعتمد على ما إذا كان ذلك التكوين هو الذي يواجه الخدمة. لا عتبة تفصل بين تلك بأمانة: عيّنة اختبار e2e في Argo CD تصل إلى 39% من كوده وتكوين NetBox الحقيقي يصل إلى 100%.
قاعدة شاملة — location /، أو Ingress عند /، أو RewriteRule ^(.*)$ — ليست دليلًا في أي من الاتجاهين. إنها توجّه كل شيء أو لا شيء، نفس الشيء لكل نقطة نهاية، لذا تُحتسب كأنها لا تصل إلى أي منها. وجهة نظر بوابة لا تحتوي على أي شيء آخر ليس لديها إشارة تقدمها، وUNEXPOSED تتنحى وتقول ذلك بدلًا من الإبلاغ عن كل نقطة نهاية كغير قابلة للوصول. مخطط Helm الخاص بـ Casdoor هو بالضبط ذلك: قاعدة Ingress واحدة عند /، والتي عند قراءتها كدليل أنتجت 365 اكتشافًا.
التقاط HAR يسجّل الطلبات التي حدثت. مجموعة Postman تسجّل الطلبات التي قصد شخص ما إرسالها. ORPHAN وLIVE_UNDOC وCOLD كلها تستنتج حول ما جرى، لذا تتطلب وجهة نظر رصدها شخص فعليًا وتقول ذلك عندما تتنحى.
يبلّغ Noir عن وسائط CLI ومواضيع Kafka وروابط التطبيقات العميقة للجوال في نفس القائمة. cli://gitops-engine/agent عند تسطيحه إلى HTTP يصبح /agent — يتصادم مع أي مسار ويب بذلك الاسم ويُسأل عما إذا كانت بوابة توجّه إليه. البروتوكول ينتمي إلى هوية نقطة النهاية؛ http وhttps فضاء واحد وكل شيء آخر يحتفظ بفضائه الخاص.
بعض الفجوات هي الحالة المقصودة. ضع ملف .alibi.yml بجانب المصدر:
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
أو مرّر --ignore REGEX لحالة عابرة. تُحتسب الاكتشافات المكتومة ويُطبع العدد — أداة تُسقط الاكتشافات بهدوء أسوأ من واحدة تطبع الكثير، لأنه لم تعد هناك أي طريقة لمعرفة ما حجبته.
مبكرة، لكن وجهات النظر الخمس كلها تُقارن.
مقاسة مقابل خمسة مستودعات:
Casdoor هي الحالة الأنظف: 230 من 235 نقطة نهاية موثقة لديها طابقت الكود، بدون أي إخفاقات في تطبيع المسار على الإطلاق. كل الـ19 اقترابًا قريبًا كانت نفس المسار بفعل مختلف — noir يسجّل كل فعل على معالج Go الشامل، وليست مشكلة مطابقة.
NetBox هي المعلّمة. تحتوي على سطحين في مستودع واحد: واجهة ويب مُصيَّرة من الخادم وواجهة REST API بموجّه DRF لا يُوثّق سوى الثاني. عند فحصها كاملة تبلّغ عن 746 اكتشافًا، معظمها الملاحظة الصحيحة لكن غير المفيدة بأن واجهة الويب ليست في مواصفة API. عند تحديد نطاقها للسطح الذي يصفه العقد، تنهار إلى ما كان يستحق قوله فعلًا:
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3 واجهات برمجية ظل: /api/plugins، /api/schema/redoc، /api/schema/swagger-ui، الثلاثة تُخدَم فعليًا وتغيب فعليًا عن المخطط. الـ397 وهمًا المتبقية هي عمليات الكتلة التي تضيفها فئة الموجّه الفرعية الخاصة بـ NetBox إلى كل نقطة نهاية قائمة، والتي لا يمكن لأي تجوال urlconf رؤيتها.
الثلاثة الأخرى محجوبة، كل واحدة لسبب يستحق المعرفة:
/api في Go ويوثّق 198 مسارًا تحته — نفس السطح على مستويين من التفصيل.urls لكل تطبيق مثبّت، وهو ما لا يمكن لأي قارئ ثابت تتبعه..proto، ولهذا يتحدث grpc عن وجهة نظر الكود: عند تصنيفه هناك، 36 من 36 مسارًا موثقًا لديه تؤكد الكود.وهذا هو سقف هذه الأداة، مُصرّح به بوضوح: إنها تقارن ما يمكن لـ noir قراءته، ووجهة نظر تُقرأ على مستوى تفصيل خاطئ أسوأ من واحدة لم تُقرأ على الإطلاق. معظم الآلية أعلاه موجودة للتمييز بين تلك بدلًا من الإبلاغ عنها كعيوب.
MIT
| القاعدة | الشرط | الخطورة |
|---|
ORPHAN | تستقبل طلبات حقيقية، غائبة عن الكود | عالية |
LIVE_UNDOC | تستقبل طلبات حقيقية، لا يصفها أي عقد | عالية |
SHADOW | في الكود، ليست في أي عقد | متوسطة |
DANGLING | قاعدة بوابة لا تصل إلى أي شيء منفذ | متوسطة |
DRIFT | مصرّح بها للنشر، مفقودة من الكود | متوسطة |
PHANTOM | في عقد، ليست في الكود | منخفضة |
UNEXPOSED | منفذة، لكن لا تصل إليها أي قاعدة بوابة | منخفضة |
COLD | منفذة، لم تُرَ تستقبل طلبًا قط | معلومات |
| المستودع | code | doc | مؤكدة متبادلًا | اكتشافات | code↔doc |
|---|
| casdoor | 372 | 235 | 230 (98%) | 139 | مقارنة |
| netbox | 1146 | 1193 | 796 (67%) | 746 | مقارنة |
| argo-cd | 59 | 198 | 1 | 31 | محجوبة |
| authentik | 231 | 1193 | 1 | 192 | محجوبة |
| flipt | 2 | 42 | 0 | 0 | محجوبة |