في دليل DevSecOps للمبتدئين شغلنا OSV-Scanner على ملف requirements.txt في تطبيق السلسلة، فوجد خلال ثانية عشرات الثغرات المنشورة في Flask وWerkzeug وJinja2، وفي دليل أمن سلسلة التوريد البرمجية رأيت لماذا تحتاج ملف القفل Lockfile وقائمة المكونات SBOM والتحديث الآلي. وفي هذا المقال سوف ننزل طبقة أعمق في فحص الاعتماديات نفسه، فالسؤال هنا ليس «هل نشغل أداة SCA؟» وإنما لماذا تعطيك ثلاث أدوات ثلاثة أرقام مختلفة للتطبيق نفسه، ومن أين تأتي بيانات الثغرات أصلاً، ولماذا يكون نصف ما تجده الأداة في مكتبات لم تكتب اسمها في أي ملف، وماذا يحدث بعد يوم الإصدار عندما تنشر ثغرة جديدة في مكتبة يعمل بها تطبيقك منذ شهر، وكيف تحدث المكتبات دون أن تكسر التطبيق.
وسوف نناقش في هذا المقال ما يلي:
- لماذا لا يكفي النظر في ملف الاعتماديات، والفرق بين ملف الاعتماديات Manifest وملف القفل Lockfile والحزم المثبتة Installed Packages وما يصل فعلاً إلى ال Docker Image.
- كيف تعمل أداة SCA من الداخل، والاعتماديات غير المباشرة Transitive Dependencies، ومعرف الحزمة purl ونطاقات الإصدارات.
- قواعد بيانات الثغرات OSV وGHSA وNVD، والفرق الحقيقي بينها، ولماذا تحمل الثغرة الواحدة ثلاثة معرفات.
- تحليل قابلية الوصول Reachability، وما الذي تدعمه الأدوات المفتوحة منه فعلاً.
- فحص التراخيص License Compliance باختصار.
- المراقبة المستمرة Continuous Monitoring مع Dependency-Track: رفع قائمة SBOM، والسياسات Policies، والتنبيهات Notifications.
- التحديث الآلي مع Renovate على Gitea في سيرفرك، والتجميع Grouping والجدولة Schedule والدمج التلقائي Automerge و
minimumReleaseAge، ومتى يكون Dependabot هو الخيار الأسهل. - تجربة عملية كاملة على تطبيق السلسلة: من ثلاث نتائج مختلفة لثلاثة مدخلات، إلى طلبات دمج Renovate، إلى الترقية وإعادة الفحص حتى يصبح التقرير نظيفاً.
- التعامل مع النتائج Findings: الترقية والإصلاح الخاص والتخفيف والقبول بتاريخ انتهاء.
لماذا لا يكفي أن تنظر في ملف الاعتماديات؟
المتابعة اليدوية للإصدارات Versions
الطريقة الأولى التي يبدأ بها أغلب المطورين هي أن يفتح ملف requirements.txt أو package.json من وقت لآخر، ويشغل أمراً مثل pip list --outdated، ويرفع ما يبدو قديماً. وهذه الطريقة تفشل لسببين واضحين، فالإصدار القديم ليس بالضرورة مصاباً والإصدار الجديد ليس بالضرورة آمناً، والأمر يخبرك أن هناك إصداراً أحدث ولا يخبرك أن الإصدار الذي عندك فيه ثغرة منشورة، ثم إن هذه المتابعة تعتمد على أن يتذكرها أحد، وفي فريق مشغول لن يتذكرها أحد إلا بعد حادثة.
فحص ملف الاعتماديات Manifest وحده
والحل الثاني أفضل، وهو أن تشغل أداة تقارن ملف الاعتماديات بقاعدة بيانات للثغرات، كما فعلنا في المقال الرئيسي. ولكن ملف الاعتماديات يذكر ما طلبته أنت فقط، أي المكتبات المباشرة Direct Dependencies، أما المكتبات التي تحتاجها هذه المكتبات، وهي الاعتماديات غير المباشرة Transitive Dependencies، فلا تظهر فيه أصلاً، ويختار مدير الحزم إصداراتها وقت التثبيت. وفي تجربتنا بعد قليل سوف ترى أن ملفاً فيه أربعة أسطر يتحول إلى إحدى عشرة مكتبة مثبتة، وأن أخطر الثغرات كانت في مكتبة لم يكتب أحد اسمها في المشروع.
الطريقة الصحيحة: فحص ما يعمل فعلاً، باستمرار
الحل الأنسب هو تحليل مكونات البرنامج Software Composition Analysis واختصاراً SCA بثلاثة أجزاء معاً: أداة تفحص القائمة الكاملة للمكتبات التي تعمل فعلاً، أي ملف القفل أو الحزم المثبتة أو ال Docker Image، في كل طلب دمج Pull Request، ومنصة تحتفظ بقائمة المكونات SBOM لكل إصدار وتعيد فحصها كلما نشرت ثغرة جديدة، وأداة تفتح طلبات دمج للتحديث فتمر بالاختبارات والمراجعة مثل أي تغيير آخر. وقد يتساءل البعض: أليس فحص خط CI كافياً؟ والإجابة أن خط CI يعرف الثغرات المنشورة يوم البناء فقط، والثغرة التي تنشر بعد شهر في مكتبة داخل إصدار يعمل الآن لن يراها أي خط، لأن أحداً لم يشغله.
كيف تعمل أداة فحص الاعتماديات من الداخل؟
الصورة التالية تبين المراحل الأربع التي تمر بها أي أداة SCA، مهما كان اسمها:

في الصورة أعلاه لاحظ التالي:
- قراءة المدخلات Inputs: الأداة لا تعرف مكتباتك إلا من الملف الذي تعطيه لها، وكل مدخل يرى جزءاً مختلفاً، كما في الجدول التالي.
- بناء القائمة Inventory: لكل مكتبة اسم وإصدار ونظام حزم Ecosystem، وتجمعها الأدوات الحديثة في معرف واحد هو Package URL واختصاراً purl، مثل
pkg:pypi/[email protected]، وهذا المعرف هو ما يطابق به أغلب قواعد البيانات. - المطابقة Matching: كل إعلان ثغرة Advisory يذكر الحزمة ونطاق الإصدارات المصابة، فالأداة تسأل سؤالاً واحداً: هل الإصدار المثبت داخل النطاق؟ ولهذا تسمى أحياناً «فحص الإصدارات»، فهي لا تقرأ كود المكتبة أبداً.
- النتائج Findings: معرف الثغرة وخطورتها وأقل إصدار فيه الإصلاح Fixed Version، وهذا العمود الأخير هو أهم ما في التقرير، لأنه يحول النتيجة إلى عمل محدد.
ملف الاعتماديات وملف القفل والحزم المثبتة وال Docker Image
والجدول التالي يجمع المدخلات الأربعة وما يراه كل منها، والأمثلة من Python لأنها لغة تطبيق السلسلة، والفكرة نفسها في كل لغة:
| المدخل | أمثلة | ما يراه | ما يفوته |
|---|---|---|---|
| ملف الاعتماديات Manifest | requirements.txt وpyproject.toml وpackage.json وpom.xml | المكتبات المباشرة كما كتبتها، وقد تكون نطاقات مثل >=2.0 | الاعتماديات غير المباشرة، والإصدار الذي اختاره مدير الحزم فعلاً |
| ملف القفل Lockfile | poetry.lock وuv.lock وpackage-lock.json، أو ملف ناتج عن pip freeze | كل مكتبة مباشرة وغير مباشرة بإصدارها الدقيق | ما يضاف خارج مدير الحزم، مثل مكتبة نسخت يدوياً |
| الحزم المثبتة Installed Packages | مجلد site-packages أو node_modules | ما ثبت فعلاً، ومعه بيانات الترخيص من ملفات الحزم | قد يرى أدوات البيئة نفسها مثل pip، وهي ليست من تطبيقك |
| ال Docker Image أو الملف التنفيذي Binary | Image التطبيق، أو ملف Go مبني | ما يصل إلى الإنتاج، ومعه حزم نظام التشغيل | لا يرى ما في المستودع ولم يدخل في البناء |
لذلك القاعدة هي: افحص ملف القفل في طلب الدمج لأنه الأسرع والأقرب إلى المطور، وافحص ال Image قبل النشر لأنها ما يعمل فعلاً، وهذا الجزء الأخير هو موضوع مقال فحص الحاويات والبنية ككود. وإذا لم يكن لمشروعك ملف قفل فاكتب واحداً، وأبسط طريقة في Python هي pip freeze داخل بيئة نظيفة، والأفضل أداة تولد ملف القفل من ملف الاعتماديات مثل pip-compile أو uv، وسبب ذلك سوف يظهر في تجربة Renovate.
الاعتماديات غير المباشرة Transitive Dependencies
الصورة التالية تبين شجرة الاعتماديات Dependency Tree لتطبيق السلسلة بعد أن أضفنا إليه مكتبة requests، وما يراه كل مدخل منها، والأرقام من التجربة العملية في هذا المقال:

لاحظ أن أخطر النتائج في urllib3 وidna، وهما مكتبتان لم يكتب اسمهما أحد في المشروع، وإنما جاءتا مع requests. ولاحظ أيضاً أن الأداة عندما ترى ملف اعتماديات فقط قد تحاول أن تخمن الشجرة بنفسها، فيكون التخمين مختلفاً عما ثبت فعلاً، وهذا ما سوف نراه بالأرقام.
قواعد بيانات الثغرات Vulnerability Databases وكيف تختلف
أداة SCA لا تعرف الثغرات من نفسها، وإنما تقرأ إعلانات تنشرها جهات مختلفة، والفرق بين هذه الجهات يفسر أغلب الاختلاف بين الأدوات. والصورة التالية تبين من أين تأتي البيانات:

- NVD من NIST: القاعدة الوطنية الأمريكية لسجلات CVE، وهي الأوسع لأنها تغطي أي منتج، من البرامج إلى أجهزة الشبكات، ولكنها تصف المنتج المصاب بمعرف CPE مثل
cpe:2.3:a:palletsprojects:flaskوليس باسم الحزمة في PyPI. ويوضح توثيق Dependency-Track النتيجة العملية لذلك: أغلب أدوات توليد SBOM تكتب معرف purl ولا تكتب CPE، فلا تجد بيانات NVD وحدها إلا القليل في مشروع مكتبات، حتى لو كانت لها سجلات CVE. - GitHub Advisory Database واختصاراً GHSA: إعلانات بأسماء الحزم في npm وPyPI وMaven وغيرها، وفيها نوعان: إعلانات راجعها فريق GitHub Reviewed، وإعلانات غير مراجعة Unreviewed تنشر آلياً من NVD، ويذكر التوثيق أن Dependabot لا ينبه على غير المراجعة لأنها لم تفحص من حيث الصحة والاكتمال. وبيانات المستودع منشورة بصيغة OSV برخصة CC-BY-4.0.
- OSV.dev: قاعدة من Google تجمع إعلانات GitHub وقاعدة PyPA (معرفات PYSEC) وفريق Go وRustSec وفرق أمن التوزيعات في صيغة OSV واحدة، مفتاحها اسم الحزمة ونظامها ونطاق الإصدارات بأحداث مثل
introducedوfixed. ولأن المطابقة باسم الحزمة دقيقة فهي الأنسب لمكتبات البرمجة، ولذلك يعتمد عليها OSV-Scanner وRenovate، ويوصي بها توثيق Dependency-Track كمصدر واحد للحزم المفتوحة إذا أردت مصدراً واحداً.
والنتيجة المهمة الأولى هي أن الثغرة الواحدة تحمل عدة معرفات تسمى الأسماء البديلة Aliases، فثغرة كوكي الجلسة في Flask هي CVE-2023-30861 في NVD، وهي GHSA-m2qf-hxjv-5gpq في GitHub، وهي PYSEC-2023-62 في قاعدة PyPA، فإذا رأيت عدداً مضاعفاً من النتائج في أداة فاسأل أولاً: هل تعد المعرفات أم تعد الثغرات؟ والنتيجة المهمة الثانية هي أن كل جهة قد تعطي الثغرة نفسها درجة خطورة مختلفة، ففي تجربتنا صنف Trivy ثلاث نتائج فقط على أنها HIGH في ملف requirements.txt، بينما صنف OSV-Scanner سبع نتائج على أنها High في الملف نفسه، والسبب أن كل أداة تأخذ الخطورة من مصدر مختلف، لذلك لا تقارن عدد «العالية» بين أداتين، وإنما قارن المعرفات نفسها. أما ترتيب النتائج حسب CVSS واحتمال الاستغلال EPSS وقائمة KEV فقد شرحناه في فرز النتائج في المقال الرئيسي.
تحليل قابلية الوصول Reachability
أداة SCA العادية تخبرك أن الإصدار مصاب، ولكنها لا تعرف هل يستدعي كودك الدالة المصابة أم لا، فمكتبة فيها مئة دالة وثغرة في واحدة منها تظهر في التقرير مهما كان استخدامك لها. وتحليل قابلية الوصول Reachability Analysis، ويسمى أيضاً تحليل الاستدعاءات Call Analysis، يبني رسماً للاستدعاءات Call Graph من كودك إلى المكتبات، ثم يسأل: هل يوجد مسار من الكود الذي كتبته إلى الدالة المذكورة في الإعلان؟ وهذا ممكن فقط عندما يذكر الإعلان أسماء الدوال المصابة Symbols، وهي معلومة توفرها قاعدة Go مثلاً في كل إعلان.

والأدوات المفتوحة التي تقوم بذلك محدودة، وهذا ما وجدناه في توثيقها الرسمي:
- govulncheck من فريق Go: برخصة BSD-3-Clause، يحلل الكود المصدري أو الملف التنفيذي، ويعرض فقط الثغرات التي يصل إليها الكود، مع مسار الاستدعاء.
- OSV-Scanner: يستخدم مكتبة govulncheck نفسها، وتحليل Go فيه مفعل افتراضياً ويحتاج أن يكون مترجم Go موجوداً، وتحليل Rust فيه تجريبي Experimental، ويحذر التوثيق من أنه يشغل ملفات
build.rsفي المكتبات، أي أنه ينفذ كوداً من الاعتماديات. - Python وJavaScript وJava: لا يوجد تحليل استدعاءات في الأدوات المفتوحة التي يغطيها هذا المقال، وتعلن عنه منتجات تجارية، فإذا كان مشروعك بهذه اللغات فالتقرير الذي تراه هو مطابقة إصدارات فقط.
وقد يتساءل البعض: إذا كانت الثغرة غير مستدعاة فهل نحذفها من التقرير؟ والإجابة لا، والسبب أن الكود يتغير، وسطر واحد في طلب دمج قادم قد يستدعي الدالة المصابة، والتحليل نفسه قد يخطئ مع الاستدعاء عبر الانعكاس Reflection أو الواجهات. لذلك استخدمه للترتيب، فتبدأ بالمستدعاة وتعطيها أقرب موعد، وتبقى غير المستدعاة في خطة التحديث العادية.
فحص التراخيص License Compliance
كل مكتبة تأتي بترخيص، وبعض التراخيص تفرض عليك شروطاً إذا وزعت برنامجك، وقد شرحنا أنواعها من المتساهلة Permissive إلى الحقوق المتروكة Copyleft في البرمجيات مفتوحة المصدر وتراخيصها. وأدوات SCA نفسها تقرأ الترخيص من بيانات الحزمة، فتستطيع أن تعطيك ملخصاً للتراخيص في مشروعك وأن تنبهك على ما هو خارج قائمة سماح Allowlist تحددها: OSV-Scanner بالخيار --licenses وبيانات الترخيص من deps.dev، وTrivy بماسح التراخيص License Scanner الذي يصنفها حسب تصنيف Google، وDependency-Track بسياسة ترخيص كما سوف ترى في التجربة.
ولاحظ أمرين قبل أن تجعل هذا الفحص بوابة تمنع الدمج، فالأول أن بيانات الترخيص في الحزم ناقصة كثيراً، فبعض حزم Python تذكر الترخيص بعبارة تصنيف قديمة مثل «BSD License» بدلاً من معرف SPDX دقيق، فتظهر عند الأداة كترخيص غير معروف Non-standard، والثاني أن الأداة تعطيك معلومة وليس حكماً قانونياً، فترخيص مثل LGPL أو MPL-2.0 مقبول في أغلب التطبيقات الداخلية ويحتاج مراجعة إذا كنت توزع البرنامج للعملاء. لذلك ابدأ بالتنبيه WARN، واجعل الفشل FAIL للتراخيص التي قررت الجهة منعها فقط.
الأدوات: نقاط القوة ونقاط الضعف
الجدول التالي يجمع الأدوات المفتوحة التي استخدمناها في هذا المقال، مع الترخيص من مستودع كل أداة والإصدار الذي شغلناه في أكتوبر 2026، وكلها نشطة وصدرت لها إصدارات خلال الأسابيع الأخيرة:
| الأداة | الترخيص | الإصدار | نقاط القوة | نقاط الضعف |
|---|---|---|---|---|
| OSV-Scanner | Apache-2.0 | 2.6.0 | يقرأ ملفات القفل لأغلب اللغات، ويعتمد على OSV مباشرة، وفيه تحليل استدعاءات ل Go، وتجاهل بسبب وتاريخ انتهاء في ملف osv-scanner.toml، وملخص تراخيص | جدوله النصي يكرر الحزم عند حل الاعتماديات فيضاعف المجموع، ويخمن الإصدارات غير المباشرة من ملف الاعتماديات تخميناً قد يخطئ |
| Trivy | Apache-2.0 | 0.75.0 | أداة واحدة للمستودع وال Image وإعدادات IaC والتراخيص، وتجاهل بتاريخ انتهاء في .trivyignore | يقرأ ملف requirements.txt بالاسم فقط، وأي ملف قفل باسم آخر يحتاج --file-patterns، وقاعدة بياناته حجمها حوالي 120 ميجابايت تنزل عند أول تشغيل |
| Grype | Apache-2.0 | 0.120.1 | يقرأ المجلد أو ملف SBOM جاهزاً، ويرتب النتائج بدرجة خطر تجمع CVSS وEPSS وKEV | لا يفحص التراخيص، ويحتاج Syft أو SBOM جاهزة ليرى ما داخل ال Image بالتفصيل |
| Dependency-Track | Apache-2.0 | 5.1.2 | منصة تحتفظ بقائمة SBOM لكل مشروع وإصدار، وتعيد التحليل مع كل تحديث للبيانات، وسياسات أمن وترخيص، وتنبيهات إلى Webhook وSlack وTeams وMattermost وJira والبريد، وسجل تدقيق للقرارات | خدمة كاملة مع PostgreSQL تحتاج سيرفراً وصيانة، وأول نسخ لبيانات NVD استغرق في تجربتنا حوالي 22 دقيقة |
| Renovate | AGPL-3.0 | 44.141.0 | يعمل مع Gitea وGitLab وGitHub وغيرها، وتجميع وجدولة ودمج تلقائي بقواعد، وminimumReleaseAge، ولوحة Dependency Dashboard | إعداداته كثيرة، والإعداد الخاطئ للدمج التلقائي يدمج دون اختبار كما سوف ترى |
| govulncheck | BSD-3-Clause | 1.8.0 | أدق تحليل استدعاءات مفتوح، مع مسار الاستدعاء إلى السطر | ل Go فقط |
وأداة cyclonedx-py (Apache-2.0، الإصدار 7.5.0) هي التي ولدنا بها قائمة SBOM من البيئة المثبتة، أما صيغ SBOM نفسها والفرق بين CycloneDX وSPDX فقد شرحناها في أمن سلسلة التوريد. وDependabot لم يدخل الجدول لأنه خدمة مدمجة في GitHub وليس أداة تشغلها بنفسك، وسوف نتحدث عنه بعد التجربة.
تجربة عملية Lab: من ثلاثة أرقام مختلفة إلى تقرير نظيف
الآن سوف نطبق كل ما سبق على تطبيق السلسلة نفسه. وكل شيء يعمل داخل Containers باسم يبدأ ب lab-dso-sca-، والمنافذ على العنوان 127.0.0.1 فقط بين 19895 و19899، وتحتاج Docker فقط كما في دليل تثبيت Docker.
تجهيز نسخة التطبيق وثغرتين جديدتين
ننسخ التطبيق، وفيه أصلاً الثغرة VULN-06 أي إصدارات Flask وWerkzeug وJinja2 القديمة، ثم نضيف ثغرتين خاصتين بهذا المقال: الأولى VULN-20 وهي مسار جديد /api/rates يجلب أسعار الصرف بمكتبة requests بإصدار قديم دون تثبيت أي من مكتباتها، والثانية VULN-21 وهي أداة صغيرة بلغة Go تعتمد على إصدار قديم من golang.org/x/text، نستخدمها في قسم قابلية الوصول لأن Python لا تدعمه أداة مفتوحة:
cp -r content/labs/devsecops-app lab-sca/app
cd lab-sca/app
git init -q -b mainوملف الاعتماديات بعد الإضافة:
# VULN-06: deliberately old versions with published vulnerabilities (Flask, Werkzeug, Jinja2 of 2022).
# Fix: update to current releases and let Renovate keep them current.
Flask==2.2.2
Werkzeug==2.2.2
Jinja2==3.1.2
# VULN-20 (SCA lab): an old requests for the new /api/rates route. Its own dependencies
# (urllib3, idna, chardet, certifi) are not listed here, so pip picks whatever fits at install time.
requests==2.25.1والمسار الجديد في app.py:
import requests
@app.route("/api/rates")
def rates():
# SCA lab addition: exchange rates for the price list, fetched with requests (VULN-20 in requirements.txt).
r = requests.get("https://rates.example/api/latest?base=SAR", timeout=5)
return r.json()ملف القفل من البيئة المثبتة
نثبت الاعتماديات في بيئة نظيفة داخل Container، ونحفظ ما ثبت فعلاً بالأمر pip freeze في ملف requirements.lock.txt:
docker run --rm --name lab-dso-sca-pip -v "$PWD:/src" -w /src python:3.12-slim sh -c '
python -m venv /venv && /venv/bin/pip install -q -r requirements.txt &&
/venv/bin/pip freeze > requirements.lock.txt && cat requirements.lock.txt'والمخرج سوف يكون كما يلي:
certifi==2026.7.22
chardet==4.0.0
click==8.5.0
Flask==2.2.2
idna==2.10
itsdangerous==2.2.0
Jinja2==3.1.2
MarkupSafe==3.0.4
requests==2.25.1
urllib3==1.26.20
Werkzeug==2.2.2أربعة أسطر في ملف الاعتماديات أصبحت إحدى عشرة مكتبة، وسبع منها لم يكتبها أحد. ولاحظ أن urllib3 ثبت بالإصدار 1.26.20 لأن requests 2.25.1 يطلب إصداراً أقل من 1.27، فاختار pip أحدث إصدار في هذا النطاق.
OSV-Scanner على ملف الاعتماديات وعلى ملف القفل
نبدأ بالأمر الذي يكتبه أغلب الناس، أي فحص ملف الاعتماديات:
docker run --rm --name lab-dso-sca-osv -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source -L /src/requirements.txtوالمخرج طويل، وهذا أوله ثم صفوف المكتبتين غير المباشرتين:
Scanned /src/requirements.txt file and found 4 packages
Total 9 packages affected by 50 known vulnerabilities (0 Critical, 22 High, 26 Medium, 2 Low, 0 Unknown) from 1 ecosystem.
50 vulnerabilities can be fixed.
| https://osv.dev/PYSEC-2024-60 | 7.5 | PyPI | idna | 2.9.0 | 3.7 | src/requirements.txt |
| https://osv.dev/PYSEC-2026-215 | 6.9 | PyPI | idna | 2.9.0 | 3.15 | src/requirements.txt |
| https://osv.dev/PYSEC-2023-192 | 8.1 | PyPI | urllib3 | 1.26.9 | 1.26.17 | src/requirements.txt |
| https://osv.dev/PYSEC-2023-212 | 5.7 | PyPI | urllib3 | 1.26.9 | 1.26.18 | src/requirements.txt |
| https://osv.dev/PYSEC-2026-141 | 8.2 | PyPI | urllib3 | 1.26.9 | 2.7.0 | src/requirements.txt |
| https://osv.dev/PYSEC-2026-1994 | 8.9 | PyPI | urllib3 | 1.26.9 | 2.6.0 | src/requirements.txt |
…في المخرج أعلاه لاحظ التالي:
- الملف فيه أربع مكتبات، ومع ذلك ظهرت
idnaوurllib3، والسبب أن OSV-Scanner 2.6.0 حاول أن يحل شجرة الاعتماديات بنفسه من ملف الاعتماديات، مع أن صفحة اللغات المدعومة تذكر هذا الحل Resolution لpom.xmlفي Maven، وتقول إن استخراج الإصدارات من ملفات الاعتماديات «بأفضل جهد Best-effort» وقد لا يطابق ما ثبت. - والإصدارات التي خمنها هي
urllib3 1.26.9وidna 2.9.0، أما pip فثبت1.26.20و2.10، أي أن جزءاً من النتائج يخص إصدارات لا تعمل عندك أصلاً. - الرقم 50 ليس عدد الثغرات، فالجدول عرض الملف مرتين، مرة كما هو ومرة بعد حل الشجرة، فتكررت Flask وJinja2 وWerkzeug، وعندما جمعنا المعرفات الفريدة من مخرج
--format jsonكانت 33 ثغرة.
ثم نطفئ هذا الحل بالخيار --no-resolve:
docker run --rm --name lab-dso-sca-osv -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source --no-resolve -L /src/requirements.txtScanned /src/requirements.txt file and found 4 packages
Total 4 packages affected by 21 known vulnerabilities (0 Critical, 7 High, 13 Medium, 1 Low, 0 Unknown) from 1 ecosystem.
21 vulnerabilities can be fixed.وهنا اختفت urllib3 وidna تماماً، وهذا هو التقرير الذي يراه من يفحص ملف الاعتماديات بأداة لا تحل الشجرة. وأخيراً نفحص ملف القفل، ونخبر الأداة أن صيغته صيغة requirements.txt لأن اسمه مختلف:
docker run --rm --name lab-dso-sca-osv -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source -L requirements.txt:/src/requirements.lock.txtScanned /src/requirements.lock.txt file and found 11 packages
Total 9 packages affected by 47 known vulnerabilities (0 Critical, 21 High, 24 Medium, 2 Low, 0 Unknown) from 1 ecosystem.
47 vulnerabilities can be fixed.
| https://osv.dev/PYSEC-2024-60 | 7.5 | PyPI | idna | 2.10.0 | 3.7 | src/requirements.lock.txt |
| https://osv.dev/PYSEC-2023-74 | 6.1 | PyPI | requests | 2.25.1 | 2.31.0 | src/requirements.lock.txt |
| https://osv.dev/PYSEC-2026-141 | 8.2 | PyPI | urllib3 | 1.26.20 | 2.7.0 | src/requirements.lock.txt |
| https://osv.dev/PYSEC-2026-1999 | 5.3 | PyPI | urllib3 | 1.26.20 | 2.5.0 | src/requirements.lock.txt |
…وهنا الإصدارات هي التي ثبتت فعلاً، والمعرفات الفريدة 30 ثغرة في ست مكتبات: Werkzeug بعشر، وurllib3 بسبع، وJinja2 بخمس، وrequests بأربع، وFlask وidna باثنتين لكل منهما. فالتطبيق نفسه أعطانا 21 و33 و30، والرقم الصحيح هو الأخير لأنه الوحيد المبني على ما يعمل.
المقارنة مع Trivy وGrype
نشغل Trivy على المجلد نفسه، مع Volume لقاعدة بياناته حتى لا تنزل في كل مرة:
docker volume create lab-dso-sca-trivy-cache
docker run --rm --name lab-dso-sca-trivy -v lab-dso-sca-trivy-cache:/root/.cache \
-v "$PWD:/src:ro" aquasec/trivy:0.75.0 fs --scanners vuln /srcWARN [pip] Unable to find python `site-packages` directory. License detection is skipped. err="unable to find path to Python executable"
│ Target │ Type │ Vulnerabilities │
│ requirements.txt │ pip │ 21 │
requirements.txt (pip)
Total: 21 (UNKNOWN: 0, LOW: 2, MEDIUM: 16, HIGH: 3, CRITICAL: 0)لاحظ أن Trivy لم ير ملف القفل أصلاً، لأنه يبحث عن ملفات pip باسم requirements.txt، فأعطى 21 مثل OSV-Scanner دون حل الشجرة. ونخبره بالاسم الآخر بالخيار --file-patterns:
docker run --rm --name lab-dso-sca-trivy -v lab-dso-sca-trivy-cache:/root/.cache \
-v "$PWD:/src:ro" aquasec/trivy:0.75.0 fs --scanners vuln --skip-db-update \
--file-patterns 'pip:requirements\.lock\.txt' /src│ Target │ Type │ Vulnerabilities │
│ requirements.lock.txt │ pip │ 30 │
│ requirements.txt │ pip │ 21 │
requirements.lock.txt (pip)
Total: 30 (UNKNOWN: 0, LOW: 2, MEDIUM: 19, HIGH: 9, CRITICAL: 0)ثلاثون مرة أخرى، وهذا الاتفاق بين أداتين بقاعدتي بيانات مختلفتين هو أفضل دليل على أن المدخل أهم من اسم الأداة. ثم Grype على المجلد:
docker volume create lab-dso-sca-grype-db
docker run --rm --name lab-dso-sca-grype -e GRYPE_DB_CACHE_DIR=/db -v lab-dso-sca-grype-db:/db \
-v "$PWD:/src:ro" anchore/grype:v0.120.1 dir:/src -o tableNAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
werkzeug 2.2.2 3.0.3 python GHSA-2g68-c3qc-8985 High 3.4% (88th) 2.5
urllib3 1.26.20 2.6.3 python GHSA-38jv-5279-wg99 High 2.9% (86th) 2.3
requests 2.25.1 2.31.0 python GHSA-j8r2-6x86-q33q Medium 3.0% (86th) 1.7
werkzeug 2.2.2 2.2.3 python GHSA-xg9f-g7g7-2323 High 1.4% (71st) 1.1
flask 2.2.2 2.2.5 python GHSA-m2qf-hxjv-5gpq High 1.3% (68th) 1.0
idna 2.10 3.7 python GHSA-jjg7-2v4v-x38h Medium 1.4% (71st) 0.8
…Grype قرأ الملفين، وأخذ إصدارات ملف القفل، وأخرج 30 سطراً أيضاً، ولكنه رتبها بعمود RISK الذي يجمع الخطورة واحتمال الاستغلال، فجاءت في أعلاها ثغرة متوسطة في requests قبل ثغرات عالية الخطورة، لأن احتمال استغلالها أعلى. وتعرض Grype معرفات GHSA بينما يعرض Trivy معرفات CVE ويعرض OSV-Scanner معرفات PYSEC، وكلها للثغرات نفسها.
قابلية الوصول على أداة Go الصغيرة
الثغرة VULN-21 أداة تتحقق من وسم اللغة Language Tag الذي يصل إلى المتجر، مثل ar-SA:
// VULN-21 (SCA lab): a small helper that checks the language tag sent to the shop.
// It depends on an old golang.org/x/text with published vulnerabilities.
package main
import (
"fmt"
"os"
"golang.org/x/text/language"
)
func main() {
tag, err := language.Parse(os.Args[1])
if err != nil {
fmt.Println("invalid tag:", err)
os.Exit(1)
}
fmt.Println("tag:", tag)
}وفي ملف go.mod السطر require golang.org/x/text v0.3.5. وتحليل الاستدعاءات يحتاج مترجم Go، لذلك نشغل OSV-Scanner داخل Container فيه Go، ونقارن الفحص دون التحليل ومعه:
docker volume create lab-dso-sca-gocache
curl -sL -o bin/osv-scanner \
https://github.com/google/osv-scanner/releases/download/v2.6.0/osv-scanner_linux_amd64
docker run --rm --name lab-dso-sca-go -v lab-dso-sca-gocache:/go -v "$PWD/bin:/opt/bin:ro" \
-v "$PWD/reach-go:/src" -w /src golang:1.27.1-bookworm sh -c '
install -m 755 /opt/bin/osv-scanner /usr/local/bin/ && go mod tidy &&
osv-scanner scan source --no-call-analysis=go -r /src;
osv-scanner scan source --all-vulns -r /src'والمخرج سوف يكون كما يلي، الأول دون تحليل والثاني معه:
Total 1 package affected by 3 known vulnerabilities (0 Critical, 2 High, 0 Medium, 0 Low, 1 Unknown) from 1 ecosystem.
| https://osv.dev/GO-2021-0113 | 7.5 | Go | golang.org/x/text | 0.3.5 | 0.3.7 | go.mod |
| https://osv.dev/GO-2022-1059 | 7.5 | Go | golang.org/x/text | 0.3.5 | 0.3.8 | go.mod |
| https://osv.dev/GO-2026-5970 | | Go | golang.org/x/text | 0.3.5 | 0.39.0 | go.mod |
Total 1 package affected by 1 known vulnerability (0 Critical, 1 High, 0 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
| https://osv.dev/GO-2021-0113 | 7.5 | Go | golang.org/x/text | 0.3.5 | 0.3.7 | go.mod |
| https://osv.dev/GHSA-ppp9-7jff-5vj2 | | | | | | |
+-------------------------------------+------+-----------+-------------------+---------+---------------+--------+
| Uncalled vulnerabilities | | | | | | |
+-------------------------------------+------+-----------+-------------------+---------+---------------+--------+
| https://osv.dev/GO-2022-1059 | 7.5 | Go | golang.org/x/text | 0.3.5 | 0.3.8 | go.mod |
| https://osv.dev/GO-2026-5970 | | Go | golang.org/x/text | 0.3.5 | 0.39.0 | go.mod |والأداة الأصلية govulncheck تعطي السبب بالسطر:
go run golang.org/x/vuln/cmd/govulncheck@latest ./...Vulnerability #1: GO-2021-0113
Out-of-bounds read in golang.org/x/text/language
More info: https://pkg.go.dev/vuln/GO-2021-0113
Module: golang.org/x/text
Found in: golang.org/x/[email protected]
Fixed in: golang.org/x/[email protected]
Example traces found:
#1: main.go:13:28: langcheck.main calls language.Parse
Your code is affected by 1 vulnerability from 1 module.
This scan also found 1 vulnerability in packages you import and 1 vulnerability
in modules you require, but your code doesn't appear to call these
vulnerabilities.في المخرج أعلاه لاحظ التالي:
- الإصدار نفسه فيه ثلاث ثغرات، والكود يستدعي
language.Parseفقط، فالثغرة GO-2021-0113 هي التي تصل إليها مدخلات المستخدم فعلاً. - GO-2022-1059 في الدالتين
ParseAcceptLanguageوMatchStringsمن الحزمة نفسها التي نستوردها، ولكننا لا نستدعيهما، وGO-2026-5970 في حزمةunicode/normالتي لا نستوردها أصلاً، ولهذا قال govulncheck «1 in packages you import» و«1 in modules you require». - ولاحظ أن الجدول الافتراضي لأداة OSV-Scanner يخفي غير المستدعاة، ولم تظهر إلا مع
--all-vulns، فإذا كان خط CI يعرض الجدول الافتراضي فلن يرى أحد الثغرتين، وهذا مقبول للبوابة Gate، ولكن الترقية تبقى مطلوبة. - والحل الصحيح هنا ترقية واحدة إلى v0.39.0 أو أحدث تغلق الثلاث معاً.
golang.org/x/tools مع Go 1.27.1، ثم عمل الأمر نفسه مع @latest الذي جلب v1.8.0. فإذا ثبت إصدار govulncheck في خط CI فاختر إصداراً يدعم إصدار Go عندك.ملخص التراخيص مع OSV-Scanner
نعطي الأداة قائمة التراخيص المسموحة، وهي هنا التراخيص المتساهلة الشائعة:
docker run --rm --name lab-dso-sca-osv -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source --licenses="MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC" \
-L requirements.txt:/src/requirements.lock.txt+--------------+-------------------------+
| LICENSE | NO. OF PACKAGE VERSIONS |
+--------------+-------------------------+
| BSD-3-Clause | 9 |
| non-standard | 3 |
| Apache-2.0 | 2 |
| MIT | 2 |
| MPL-2.0 | 1 |
+--------------+-------------------------+
+-------------------+-----------+--------------+-----------+---------------------------+
| LICENSE VIOLATION | ECOSYSTEM | PACKAGE | VERSION | SOURCE |
+-------------------+-----------+--------------+-----------+---------------------------+
| MPL-2.0 | PyPI | certifi | 2026.7.22 | src/requirements.lock.txt |
| non-standard | PyPI | chardet | 4.0.0 | src/requirements.lock.txt |
| non-standard | PyPI | idna | 2.10.0 | src/requirements.lock.txt |
| non-standard | PyPI | itsdangerous | 2.2.0 | src/requirements.lock.txt |
+-------------------+-----------+--------------+-----------+---------------------------+أربع مخالفات، ولا واحدة منها في مكتبة كتبناها بأنفسنا، فكلها جاءت مع Flask وrequests. وchardet 4.0.0 ترخيصها LGPL كما تقول بيانات الحزمة نفسها، ولكن الأداة لم تتعرف على صيغة ذكره فسمته non-standard، وcertifi بترخيص MPL-2.0 وهو من الحقوق المتروكة الضعيفة Weak Copyleft. ولاحظ أن جدول الملخص يعد الحزم المكررة أيضاً، فمجموعه 17 والملف فيه 11 مكتبة.
المراقبة المستمرة مع Dependency-Track
نولد أولاً قائمة SBOM بصيغة CycloneDX من البيئة المثبتة، وليس من ملف الاعتماديات، لأن البيئة فيها بيانات الترخيص من ملفات الحزم:
docker run --rm --name lab-dso-sca-pip -v "$PWD:/src" -w /src python:3.12-slim sh -c '
python -m venv /venv && /venv/bin/pip install -q -r requirements.txt &&
python -m venv /tools && /tools/bin/pip install -q cyclonedx-bom==7.5.0 &&
/tools/bin/cyclonedx-py environment --of JSON --output-reproducible -o /src/sbom.cdx.json /venv'ولاحظ أننا ثبتنا cyclonedx-py في بيئة منفصلة /tools، والسبب أنها لو ثبتت في بيئة التطبيق لدخلت هي ومكتباتها في قائمة مكونات التطبيق. ثم نشغل Dependency-Track بملف Compose مبني على ملف البداية السريعة الرسمي للإصدار 5.1.2، مع تغيير المنافذ إلى 127.0.0.1، وإضافة Container صغير يستقبل التنبيهات ويطبعها:
# Dependency-Track 5.1.2 for the SCA lab, based on the official quickstart compose file.
# Everything listens on 127.0.0.1 only.
name: lab-dso-sca-dt
services:
apiserver:
image: ghcr.io/dependencytrack/apiserver:5.1.2
container_name: lab-dso-sca-dt-api
depends_on:
postgres:
condition: service_healthy
deploy:
resources:
limits:
memory: 2g
environment:
DT_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/dtrack"
DT_DATASOURCE_USERNAME: "dtrack"
DT_DATASOURCE_PASSWORD: "dtrack"
ports:
- "127.0.0.1:19896:8080"
volumes:
- "apiserver-data:/data"
frontend:
image: ghcr.io/dependencytrack/frontend:5.1.2
container_name: lab-dso-sca-dt-ui
environment:
API_BASE_URL: "http://127.0.0.1:19896"
ports:
- "127.0.0.1:19895:8080"
postgres:
image: postgres:18-alpine
container_name: lab-dso-sca-dt-db
environment:
POSTGRES_DB: "dtrack"
POSTGRES_USER: "dtrack"
POSTGRES_PASSWORD: "dtrack"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 3
volumes:
- "postgres-data:/var/lib/postgresql"
hook:
# A tiny webhook receiver that prints every notification Dependency-Track sends.
image: python:3.12-slim
container_name: lab-dso-sca-dt-hook
command: ["python", "-u", "-c", "import http.server as h\nclass H(h.BaseHTTPRequestHandler):\n def do_POST(s):\n b=s.rfile.read(int(s.headers['Content-Length']));print(b.decode(),flush=True);s.send_response(200);s.end_headers()\nh.HTTPServer(('0.0.0.0',9000),H).serve_forever()"]
volumes:
apiserver-data: {}
postgres-data: {}docker compose up -ddependency failed to start: container lab-dso-sca-dt-db is unhealthy، لأن PostgreSQL كانت ما زالت تنشئ قاعدة البيانات عندما انتهت محاولات فحص الصحة الثلاث، فأعدنا docker compose up -d فعمل. وكلمات المرور في هذا الملف للتجربة فقط، أما في الإنتاج فاتبع دليل النشر في التوثيق.بعد ذلك عدة خطوات في الواجهة على http://127.0.0.1:19895، وقد نفذناها عبر ال API بالقيم نفسها حتى نكررها بسهولة:
- كلمة مرور المدير: الدخول الأول بالمستخدم
adminوكلمة المرورadmin، ويطلب منك تغييرها فوراً. - مصادر البيانات Vulnerability Sources: في الإصدار 5.1.2 بدأ نسخ NVD تلقائياً بعد التشغيل، واستغرق 22 دقيقة لحوالي 410 آلاف سجل، ولكنه لا يكفي لمشروع Python كما شرحنا، فمن Administration ثم Vulnerability Sources ثم OSV فعلنا مصدر OSV واخترنا نظام PyPI وحده، فانتهى نسخه في أقل من ثلاث دقائق.
- المشروع Project: باسم
devsecops-shopوالإصدار1.0. - سياسة أمن Security Policy: باسم «No High or Critical vulnerabilities» بحالة FAIL، وشرطان: الخطورة Severity تساوي CRITICAL، أو تساوي HIGH.
- سياسة ترخيص License Policy: باسم «Permissive licences only» بحالة WARN، وشرط من نوع Expression بلغة CEL مأخوذ من أمثلة التوثيق، ويطابق كل مكتبة ليس لها ترخيص معروف أو ترخيصها خارج مجموعة التراخيص المتساهلة:
!component.is_internal && (!has(component.resolved_license) || !component.resolved_license.groups.exists(g, g.name == "Permissive")) - التنبيه Alert: من Administration ثم Notifications ثم Alerts، تنبيه بالناشر Webhook إلى
http://lab-dso-sca-dt-hook:9000/، لمجموعات NEW_VULNERABILITY وPOLICY_VIOLATION وBOM_PROCESSED، كما في درس التنبيهات. - مفتاح API: من فريق Automation الذي يملك صلاحية رفع SBOM، وهو ما يستخدمه خط CI.
ثم نرفع قائمة SBOM كما يفعل خط CI بعد كل بناء:
curl -s -X POST -H "X-Api-Key: $DT_API_KEY" \
-F "project=$PROJECT_UUID" -F "[email protected]" \
http://127.0.0.1:19896/api/v1/bom{"token":"01a115d2-9146-798a-b133-601d9bed2978","projectUuid":"d70087eb-83e1-4a18-a667-2adee10414ad"}وبعد ثوان كانت أرقام المشروع من /api/v1/metrics/project/{uuid}/current كما يلي، وقد اختصرنا الحقول:
{"critical":0,"high":11,"medium":23,"low":2,"vulnerabilities":36,"vulnerableComponents":7,"components":12,
"policyViolationsFail":4,"policyViolationsWarn":5,"policyViolationsSecurityTotal":4,"policyViolationsLicenseTotal":5}الصورة التالية تبين صفحة المشروع في الواجهة بعد التحليل:

والصورة التالية تبين تبويب مخالفات السياسات Policy Violations:

في التجربة أعلاه لاحظ التالي:
- 36 وليس 30: قائمة SBOM مأخوذة من البيئة المثبتة، والبيئة فيها pip نفسه بالإصدار 25.0.1، وفيه ست ثغرات، وقد أعطتنا Grype الرقم نفسه 36 عندما فحصت الملف نفسه، فالفرق من المدخل وليس من الأداة.
- 36 و71 في تبويب التدقيق: مصدر OSV جاء بسجلات PYSEC وسجلات GHSA للثغرات نفسها، فالقائمة فيها 71 سجلاً، والأرقام في اللوحة تجمعها بالأسماء البديلة إلى 36 ثغرة، ووصل إلى ال Webhook 71 تنبيهاً من نوع NEW_VULNERABILITY، 36 منها بمعرفات GHSA و35 بمعرفات PYSEC، فإذا وصلت التنبيهات إلى قناة الفريق فتوقع الضعف.
- مخالفات الترخيص:
chardetكما توقعنا، ولكن معهاrequestsوidnaوitsdangerousلأن بيانات الترخيص فيها عبارة تصنيف قديمة لا يعرفها Dependency-Track كترخيص SPDX، وcertifiلأن MPL-2.0 ليست في مجموعة Permissive، وهذا ما قلناه عن جودة بيانات التراخيص. - التنبيهات: وصل إلى ال Webhook تنبيه BOM_PROCESSED واحد و71 تنبيه NEW_VULNERABILITY و9 تنبيهات POLICY_VIOLATION، وهذا أول التنبيه BOM_PROCESSED كما طبعه ال Container:
{
"notification": {
"level": "LEVEL_INFORMATIONAL",
"scope": "SCOPE_PORTFOLIO",
"group": "GROUP_BOM_PROCESSED",
"timestamp": "2026-10-07T10:04:46.707Z",
"title": "Bill of Materials Processed",
"content": "A CycloneDX BOM was processed",
"subject": {
"project": {
"uuid": "d70087eb-83e1-4a18-a667-2adee10414ad",
"name": "devsecops-shop",
"version": "1.0",
"isActive": true
},والقيمة الحقيقية لهذه المنصة ليست في هذا الرفع الأول، فالأداة في خط CI أعطتنا المعلومة نفسها، وإنما فيما يحدث بعده، والصورة التالية تبين ذلك:

وبهذا الشكل يصبح السؤال «هل نستخدم المكتبة التي أعلنت ثغرتها اليوم؟ وفي أي مشروع؟ وبأي إصدار؟» بحثاً في صفحة Components بدلاً من اجتماع طارئ، وهذا هو الضابط ARC-ASC-09-02 في دليل ضوابط أمن التطبيقات المقترح.
التحديث مع Renovate على Gitea في سيرفرك
الآن نريد أن نصلح، وأن تبقى المكتبات حديثة بعد الإصلاح. نشغل Gitea في Container، كما في دليل تثبيت Gitea ولكن بأقل إعداد ممكن، ونصنع مستخدماً للمختبر ومستخدماً للبوت:
docker network create lab-dso-sca-net
docker volume create lab-dso-sca-gitea-data
docker run -d --name lab-dso-sca-gitea --network lab-dso-sca-net -p 127.0.0.1:19898:3000 \
-v lab-dso-sca-gitea-data:/var/lib/gitea \
-e GITEA__security__INSTALL_LOCK=true -e GITEA__database__DB_TYPE=sqlite3 \
-e GITEA__server__ROOT_URL=http://lab-dso-sca-gitea:3000/ -e GITEA__server__DOMAIN=lab-dso-sca-gitea \
-e GITEA__service__DISABLE_REGISTRATION=true gitea/gitea:28.1.0-rootless
docker exec lab-dso-sca-gitea gitea admin user create --admin --username lab \
--password 'LabOnly-Gitea-2026!' --email [email protected] --must-change-password=false
docker exec lab-dso-sca-gitea gitea admin user create --username renovate \
--password 'LabOnly-Reno-2026!' --email [email protected] --must-change-password=false
docker exec lab-dso-sca-gitea gitea admin user generate-access-token --username lab --token-name admin --scopes all --raw
docker exec lab-dso-sca-gitea gitea admin user generate-access-token --username renovate --token-name bot --scopes all --rawثم ننشئ المستودع lab/shop عبر ال API، ونعطي البوت صلاحية الكتابة فيه، ونرفع التطبيق ومعه ملف القفل وملف إعداد Renovate التالي:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"timezone": "Asia/Riyadh",
"schedule": ["* 0-5 * * 1"],
"minimumReleaseAge": "7 days",
"internalChecksFilter": "strict",
"dependencyDashboard": true,
"osvVulnerabilityAlerts": true,
"packageRules": [
{
"matchPackageNames": ["/^(flask|werkzeug|jinja2|markupsafe|itsdangerous|click)$/i"],
"groupName": "flask stack"
},
{
"matchPackageNames": ["/^(requests|urllib3|idna|chardet|certifi|charset-normalizer)$/i"],
"groupName": "http client"
},
{
"matchUpdateTypes": ["patch"],
"automerge": true
}
]
}في الإعداد أعلاه لاحظ التالي:
groupName: كل التحديثات التي تشترك في الاسم توضع في فرع وطلب دمج واحد كما في خيارات الإعداد، فمكتبات Flask معاً ومكتبات HTTP معاً، والسبب أن هذه المكتبات تتحدث معاً عادة وتختبر معاً.schedule: يوم الاثنين بين منتصف الليل والخامسة صباحاً بتوقيت الرياض، والجدول لا يشغل Renovate وإنما يحدد متى يسمح له بإنشاء الفروع إذا عمل، ويوصي التوثيق بنافذة لا تقل عن ثلاث أو أربع ساعات.minimumReleaseAgeمعinternalChecksFilter: strict: لا يقترح أي إصدار عمره أقل من سبعة أيام، وقد شرحنا سببه الأمني في أمن سلسلة التوريد، ويحتاج أن يعطي مصدر الحزم تاريخ النشر، وPyPI يعطيه.osvVulnerabilityAlerts: يجعل Renovate ينزل قاعدة OSV ويفتح طلبات دمج لإصلاح الثغرات، وتقول خيارات الإعداد إنه يغطي الاعتماديات المباشرة فقط، وإن طلبات الإصلاح الأمني تتجاهل الجدول وحدود عدد الطلبات.- الدمج التلقائي للتحديثات الصغيرة Patch: وسوف ترى بعد قليل لماذا هذا السطر خطير دون شرط اختبار.
ونشغل Renovate مرة واحدة كما يشغله خط مجدول Cron:
docker run --rm --name lab-dso-sca-renovate --network lab-dso-sca-net \
-e RENOVATE_PLATFORM=gitea -e RENOVATE_ENDPOINT=http://lab-dso-sca-gitea:3000/api/v1 \
-e RENOVATE_TOKEN="$RENOVATE_TOKEN" -e RENOVATE_REPOSITORIES=lab/shop \
-e RENOVATE_GIT_AUTHOR="Renovate Bot <[email protected]>" -e LOG_LEVEL=info \
renovate/renovate:44.141.0والمخرج بعد حذف تحذيرات ملاحظات الإصدار المتكررة:
INFO: Dependency extraction complete (repository=lab/shop, baseBranch=main)
"stats": {
"managers": {
"dockerfile": {"fileCount": 1, "depCount": 1},
"pip_requirements": {"fileCount": 2, "depCount": 15}
},
"total": {"fileCount": 3, "depCount": 16}
}
INFO: PR created (repository=lab/shop, branch=renovate/pypi-jinja2-vulnerability)
"pr": 1,
"prTitle": "Update dependency Jinja2 to v3.1.6 [SECURITY]",
INFO: PR created (repository=lab/shop, branch=renovate/pypi-requests-vulnerability)
"pr": 2,
"prTitle": "Update dependency requests to v2.33.0 [SECURITY]",
INFO: PR created (repository=lab/shop, branch=renovate/pypi-flask-vulnerability)
"pr": 3,
"prTitle": "Update dependency Flask to v3 [SECURITY]",
INFO: PR created (repository=lab/shop, branch=renovate/pypi-idna-vulnerability)
"pr": 4,
"prTitle": "Update dependency idna to v3 [SECURITY]",
INFO: PR created (repository=lab/shop, branch=renovate/pypi-urllib3-vulnerability)
"pr": 5,
"prTitle": "Update dependency urllib3 to v2 [SECURITY]",
INFO: PR created (repository=lab/shop, branch=renovate/pypi-werkzeug-vulnerability)
"pr": 6,
"prTitle": "Update dependency Werkzeug to v3 [SECURITY]",
INFO: Repository finished (repository=lab/shop)في المخرج أعلاه لاحظ التالي:
- الجدول لم يمنع شيئاً: شغلنا Renovate يوم أربعاء، ومع ذلك فتح ستة طلبات دمج، لأنها كلها طلبات إصلاح أمني، وهذه تتجاهل الجدول كما يقول التوثيق، وهذا هو السلوك الذي تريده.
- التجميع لم يطبق: كل طلب أمني لمكتبة واحدة في فرع باسمها، فطلبات الإصلاح الأمني لا تدخل في
groupNameهنا. - الإصدار المقترح أقل إصدار يصلح: طلب
requestsيرفعها إلى 2.33.0 لأنه أقل إصدار يغلق كل ثغراتها، وليس إلى أحدث إصدار. - بقية التحديثات انتظرت: في لوحة Dependency Dashboard ظهر تحديث
chardetإلى v7 تحت Rate-Limited، والقيمة الافتراضية للخيارprHourlyLimitطلبان في الساعة، وظهر تحديثpythonفي ملف Dockerfile تحت Pending Status Checks، لأن Renovate لا يعتبر الإصدار مستقراً حتى يتحقق شرطminimumReleaseAge، ومع القيمة الافتراضيةtimestamp-requiredيجب أن يكون للإصدار تاريخ نشر معروف.
وعندما فتحنا المستودع وجدنا أن طلب الدمج الأول، أي Jinja2 من 3.1.2 إلى 3.1.6، دمج في main خلال ثوان من إنشائه دون أن يراه أحد:
db4b26e Merge pull request 'Update dependency Jinja2 to v3.1.6 [SECURITY]' (#1) from renovate/pypi-jinja2-vulnerability into main
bc3567b Update dependency Jinja2 to v3.1.6 [SECURITY]
588e5a9 pip freeze lockfile and Renovate configوالسبب هو قاعدة الدمج التلقائي للتحديثات الصغيرة، مع الخيار platformAutomerge الذي قيمته الافتراضية true، فيطلب Renovate من Gitea أن يدمج عند نجاح الفحوص، والمستودع لا توجد فيه أي فحوص مطلوبة، فدمجه Gitea فوراً. ويحذر توثيق الخيار من هذا تحديداً: إذا لم تفرض المنصة نجاح الاختبارات قبل الدمج فقد تدمج طلبات Renovate قبل أن تبدأ الاختبارات أو حتى إذا فشلت. لذلك لا تقم بتفعيل الدمج التلقائي في مستودع لا يفرض فرعه الرئيسي فحوصاً ناجحة Required Status Checks، والسبب أن الدمج التلقائي يعني عندها «ادمج دون اختبار».
ثم فتحنا طلب الدمج الخامس، وهو يرفع urllib3 وحدها في ملف القفل:
-urllib3==1.26.20
+urllib3==2.8.0وجربنا تثبيت ملف القفل من فرع هذا الطلب:
docker run --rm --name lab-dso-sca-pip -v "$PWD:/src:ro" -w /src python:3.12-slim \
pip install --dry-run -r requirements.lock.txtERROR: Cannot install -r requirements.lock.txt (line 9) and urllib3==2.8.0 because these package versions have conflicting dependencies.
The conflict is caused by:
The user requested urllib3==2.8.0
requests 2.25.1 depends on urllib3<1.27 and >=1.21.1
ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflictsوهذه أهم نتيجة في التجربة: طلب إصلاح أمني صحيح لمكتبة واحدة يكسر التثبيت، لأن ملف pip freeze قائمة مسطحة لا يعرف Renovate العلاقة بين سطورها، فيرى urllib3 مكتبة مستقلة يرفعها وحدها. لذلك إذا كان مشروعك Python فاكتب الاعتماديات المباشرة في ملف، وولد ملف القفل منه بأداة تحل الشجرة مثل pip-compile أو uv، ودع Renovate يحدث الملف الأول ويعيد توليد الثاني، كما في مدير pip-compile في Renovate، وفي كل الأحوال يجب أن يثبت خط CI ملف القفل ويشغل الاختبارات على كل طلب.
بعد ذلك أغلقنا الطلبات الأمنية الخمسة الباقية، وحذفنا الجدول وحد الساعة من الإعداد حتى نرى التجميع الآن، وأطفأنا osvVulnerabilityAlerts، ثم شغلنا Renovate مرة ثانية:
INFO: PR created (repository=lab/shop, branch=renovate/http-client)
"prTitle": "Update dependency requests to v2.34.2",
INFO: PR created (repository=lab/shop, branch=renovate/flask-stack)
"prTitle": "Update flask stack",
INFO: PR created (repository=lab/shop, branch=renovate/major-flask-stack)
"prTitle": "Update flask stack to v3",
INFO: PR created (repository=lab/shop, branch=renovate/major-http-client)
"prTitle": "Update http client (major)",الصورة التالية تبين طلبات الدمج في Gitea بعد التشغيل الثاني:

في النتيجة أعلاه لاحظ التالي:
- كل مجموعة صارت طلبين: الطلب 9 يرفع Flask إلى 2.3.3 وWerkzeug إلى 2.3.8، والطلب 10 يرفعهما إلى 3.1.3 و3.1.9، لأن Renovate يفصل التحديثات الكبيرة Major عن غيرها افتراضياً، وهذا جيد لأن الانتقال إلى Flask 3 قد يحتاج تعديل كود.
- ولكن الفصل كسر مجموعة HTTP: الطلب 11 يرفع
urllib3إلى 2.8.0 وidnaإلى 3.20 وchardetإلى 7.6.0، وrequestsباقية في فرعه على 2.25.1، فيتكرر خطأ pip نفسه، ورفعrequestsفي طلب منفصل هو الطلب 8. فإذا كانت مكتبات المجموعة مرتبطة بقيود إصدار فاجعل المجموعة لا تفصل الكبيرة عن الصغيرة بالخيارseparateMajorMinor، أو حدث ملف الاعتماديات المباشرة فقط وولد ملف القفل كما قلنا. - Jinja2 غير موجودة: لأنها دمجت تلقائياً في التشغيل الأول.
- أحدث إصدار وليس أقل إصلاح: الطلب 8 يرفع
requestsإلى 2.34.2، أي أحدث إصدار، بينما اقترح الطلب الأمني في التشغيل الأول 2.33.0، أي أقل إصدار يصلح الثغرات. - إغلاق الطلب يعني تجاهل الإصدار: تقول صفحة طلبات الدمج إنك إذا أغلقت طلب Renovate دون دمج فهو يعتبر ذلك تجاهلاً لهذا التحديث، فلا تغلق طلب تحديث لأنك مشغول، وإنما اتركه مفتوحاً أو استخدم لوحة Dependency Dashboard.
الترقية وإعادة الفحص حتى يصبح التقرير نظيفاً
الحل الذي اخترناه هو أن نرفع المكتبات المباشرة الأربع معاً إلى أحدث إصداراتها، ونترك pip يحل الباقي، ونولد ملف القفل من جديد:
# Direct dependencies only. The full set with transitive versions is in requirements.lock.txt (pip freeze).
Flask==3.1.3
Werkzeug==3.1.9
Jinja2==3.1.6
requests==2.34.2وملف القفل الجديد:
blinker==1.9.0
certifi==2026.7.22
charset-normalizer==3.5.2
click==8.5.0
Flask==3.1.3
idna==3.20
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.4
requests==2.34.2
urllib3==2.8.0
Werkzeug==3.1.9لاحظ أن chardet اختفت، لأن requests الحديثة تعتمد على charset-normalizer بترخيص MIT، فمشكلة الترخيص حلتها الترقية دون أي قرار قانوني. والآن نعيد Trivy على الملفين:
docker run --rm --name lab-dso-sca-trivy -v lab-dso-sca-trivy-cache:/root/.cache \
-v "$PWD:/src:ro" aquasec/trivy:0.75.0 fs --scanners vuln --skip-db-update \
--file-patterns 'pip:requirements\.lock\.txt' --exit-code 1 /src┌───────────────────────┬──────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────┼──────┼─────────────────┤
│ requirements.lock.txt │ pip │ 0 │
├───────────────────────┼──────┼─────────────────┤
│ requirements.txt │ pip │ 0 │
└───────────────────────┴──────┴─────────────────┘وكود الخروج صفر مع --exit-code 1، أي أن الخط سوف ينجح. ثم OSV-Scanner على الملفين معاً:
docker run --rm --name lab-dso-sca-osv -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source -L /src/requirements.txt -L requirements.txt:/src/requirements.lock.txtScanned /src/requirements.txt file and found 4 packages
Scanned /src/requirements.lock.txt file and found 12 packages
Total 1 package affected by 1 known vulnerability (0 Critical, 0 High, 1 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
1 vulnerability can be fixed.
| https://osv.dev/PYSEC-2026-215 | 6.9 | PyPI | idna | 3.9.0 | 3.15 | src/requirements.txt |
| https://osv.dev/GHSA-65pc-fj4g-8rjx | | | | | | |وهذا إنذار خاطئ False Positive حقيقي: ملف القفل نظيف، والنتيجة الوحيدة من ملف الاعتماديات، لأن الأداة خمنت مرة أخرى أن idna بالإصدار 3.9.0، وpip ثبت 3.20 كما في ملف القفل. والإصلاح الصحيح هنا ليس التجاهل، وإنما أن يفحص خط CI ملف القفل وحده، أو ملف الاعتماديات مع --no-resolve، لأن المشكلة في المدخل. ولكن لنفرض أنك لا تستطيع تغيير الخط اليوم، فالتجاهل الصحيح يكون في ملف osv-scanner.toml بجانب الملف المفحوص، بمعرف الثغرة وسبب مكتوب وتاريخ انتهاء:
# Accepted findings. Every entry needs an owner, a reason and an expiry date.
[[IgnoredVulns]]
id = "PYSEC-2026-215"
ignoreUntil = 2026-11-07
reason = "False positive: the manifest resolver guessed idna 3.9.0, pip installs idna 3.20 (see requirements.lock.txt). Owner: shop team."Loaded filter from: /src/osv-scanner.toml
PYSEC-2026-215 and 2 aliases have been filtered out because: False positive: the manifest resolver guessed idna 3.9.0, pip installs idna 3.20 (see requirements.lock.txt). Owner: shop team.
Filtered 2 vulnerabilities from output
No issues foundلاحظ أن التجاهل بمعرف واحد شمل أسماءه البديلة، ولاحظ ignoreUntil، فبعد شهر يعود الإنذار إلى التقرير إذا لم يصلح أحد الخط، وهذا هو الفرق بين قبول مؤقت ونسيان دائم. ويقابله في Trivy السطر CVE-… exp:2026-11-07 في ملف .trivyignore أو الحقل expired_at في ملف .trivyignore.yaml كما في توثيق التصفية.
وأخيراً رفعنا قائمة SBOM جديدة من البيئة المحدثة إلى المشروع نفسه في Dependency-Track:
{"components":13,"vulnerabilities":6,"vulnerableComponents":1,"policyViolationsFail":0,"policyViolationsWarn":3,"policyViolationsTotal":3}صفر مخالفات FAIL، والثغرات الست كلها في مكون واحد هو pip 25.0.1 الموجود في البيئة نفسها، أي ليس في تطبيقنا، وحلها أن ترفع pip في ال Image أو أن تولد SBOM للتطبيق دون أدوات البيئة. وبقيت ثلاث مخالفات WARN للترخيص: certifi بترخيص MPL-2.0، وitsdangerous وJinja2 3.1.6 لأن بيانات الترخيص فيهما عبارة تصنيف قديمة. وهذه تحتاج قراراً مكتوباً مرة واحدة في Dependency-Track بحالة تحليل Analysis وتعليق، وليس تعديلاً في الكود.
إزالة التجربة Cleanup
نحذف ما أنشأناه بالأسماء فقط، دون أي أمر Prune قد يحذف ما يخص مشاريع أخرى على الجهاز:
docker compose -p lab-dso-sca-dt down -v
docker rm -f lab-dso-sca-gitea
docker network rm lab-dso-sca-net
docker volume rm lab-dso-sca-gitea-data lab-dso-sca-trivy-cache lab-dso-sca-grype-db lab-dso-sca-gocache
docker image rm ghcr.io/google/osv-scanner:v2.6.0 ghcr.io/dependencytrack/apiserver:5.1.2 \
ghcr.io/dependencytrack/frontend:5.1.2 postgres:18-alpine gitea/gitea:28.1.0-rootless \
renovate/renovate:44.141.0 golang:1.27.1-bookwormتشغيل فحص الاعتماديات في خط CI
كل فحص من الفحوص السابقة له مكان في الخط، وقد شرحنا أماكن الفحوص كلها في المقال الرئيسي، وما يخص الاعتماديات هو:
- طلب الدمج Pull Request: OSV-Scanner على ملف القفل، ويفشل الطلب إذا ظهرت ثغرة، مع ملف
osv-scanner.tomlللقبول المؤقت. وهذا سريع لأنه لا يحتاج بناء ال Image. - بعد الدمج في main وعند الإصدار: توليد SBOM ورفعها إلى Dependency-Track باسم المشروع والإصدار، ومعها فحص ال Image نفسها.
- كل يوم أو كل أسبوع: لا تحتاج خطاً لإعادة الفحص، لأن Dependency-Track يعيد التحليل بنفسه، ولكن تحتاج تشغيل Renovate بجدول.
والخط التالي ل Gitea Actions يجمع الخطوتين الأوليين، وكل أمر فيه هو أمر شغلناه في التجربة أعلاه، أما الملف نفسه فكتبناه مثالاً، وتجميع الفحوص كلها في خط واحد مع بوابات حسب الخطورة هو موضوع مقال خط CI/CD آمن من البداية للنهاية:
name: dependencies
on:
pull_request:
push:
branches: [main]
jobs:
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: OSV-Scanner on the lockfile
run: |
docker run --rm -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
scan source -L requirements.txt:/src/requirements.lock.txt
sbom:
if: github.event_name == 'push'
needs: sca
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: CycloneDX SBOM from the installed environment
run: |
docker run --rm -v "$PWD:/src" -w /src python:3.12-slim sh -c '
python -m venv /venv && /venv/bin/pip install -q -r requirements.lock.txt &&
python -m venv /tools && /tools/bin/pip install -q cyclonedx-bom==7.5.0 &&
/tools/bin/cyclonedx-py environment --of JSON -o /src/sbom.cdx.json /venv'
- name: Upload to Dependency-Track
run: |
curl -sf -X POST -H "X-Api-Key: ${{ secrets.DT_API_KEY }}" \
-F "project=${{ vars.DT_PROJECT_UUID }}" -F "[email protected]" \
"${{ vars.DT_URL }}/api/v1/bom"في الإعداد أعلاه لاحظ التالي:
- OSV-Scanner يخرج بالكود 1 إذا وجد ثغرة، وهذا وحده يفشل الخطوة، فلا تحتاج خياراً إضافياً.
- مفتاح Dependency-Track في الأسرار Secrets، وهو مفتاح فريق Automation الذي يملك صلاحية رفع SBOM فقط، وليس مفتاح المدير.
- ثبت إصدارات ال Docker Images في الخط، وثبت ال Actions على هاش Commit كامل كما في أمن سلسلة التوريد، والوسم هنا للتبسيط.
Renovate أو Dependabot؟
Dependabot هو الأداة المدمجة في GitHub لنفس الغرض، ويعمل على خوادم GitHub، فلا تشغله ولا تصونه، وفيه نوعان: تحديثات أمنية Security Updates تفتح طلباً عندما يظهر تنبيه لمكتبة في مستودعك، وتحديثات إصدارات Version Updates بجدول تحدده في ملف .github/dependabot.yml. ويدعم التجميع بالخيار groups، ومهلة قبل اقتراح الإصدار الجديد بالخيار cooldown، ويذكر مرجع الخيارات أن له مهلة افتراضية ثلاثة أيام إذا لم تحدد واحدة، ولا تطبق على التحديثات الأمنية. والملف التالي مثال مكتوب لنفس قواعد تجربتنا، ولم نشغله لأن مستودع التجربة على Gitea:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
timezone: "Asia/Riyadh"
cooldown:
default-days: 7
groups:
flask-stack:
patterns: ["flask", "werkzeug", "jinja2", "markupsafe", "itsdangerous", "click"]
http-client:
patterns: ["requests", "urllib3", "idna", "certifi", "charset-normalizer"]والقرار بسيط: إذا كان مستودعك على GitHub وتريد أقل إعداد فاستخدم Dependabot، وإذا كان على Gitea أو GitLab في سيرفرك، أو تريد قواعد دقيقة مثل الدمج التلقائي حسب نوع التحديث ولوحة Dependency Dashboard ودعم مديري حزم أكثر، فاستخدم Renovate. وفي الحالتين القواعد نفسها: تجميع المكتبات المرتبطة، ومهلة قبل الإصدارات الجديدة، واختبارات مطلوبة قبل أي دمج.
التعامل مع النتائج Findings
التقرير لا يفيد شيئاً حتى يتحول إلى قرار، والصورة التالية تبين القرارات الأربعة الممكنة لكل نتيجة:

- الترقية Upgrade: هي الإجابة في أغلب الحالات، كما رأيت في التجربة، فهي أغلقت 30 ثغرة ومشكلة ترخيص في طلب دمج واحد. وارفع المكتبات المرتبطة معاً، وأعد فحص ملف القفل بعد الترقية، لأن الترقية نفسها قد تجلب مكتبة غير مباشرة جديدة.
- الإصلاح الخاص Patch: عندما لا يوجد إصدار مصلح بعد، أو يكون الإصدار المصلح قفزة كبيرة لا تستطيعها اليوم، فتنقل Commit الإصلاح إلى نسخة خاصة من المكتبة. وتذكر أنك أصبحت مسؤولاً عن صيانة نسخة Fork، لذلك احذفها عند أول ترقية ممكنة.
- التخفيف Mitigation: أن تغلق الطريق إلى الثغرة دون تغيير المكتبة، مثل إطفاء الميزة التي تستخدم الدالة المصابة، أو قاعدة في جدار حماية التطبيقات WAF، أو حد لحجم الطلب في حالة ثغرات حجب الخدمة. وهو حل مؤقت بتذكرة وموعد.
- القبول بتاريخ انتهاء Risk Acceptance: للإنذار الخاطئ، أو للثغرة غير المستدعاة منخفضة الخطورة، أو لثغرة في أداة بناء لا تصل إلى الإنتاج، ويكون بسبب مكتوب وصاحب وتاريخ انتهاء كما فعلنا في
osv-scanner.toml، والسبب أن القبول دون تاريخ يتحول مع الوقت إلى قائمة تجاهل لا يقرؤها أحد.
أما ترتيب النتائج بالخطورة CVSS واحتمال الاستغلال EPSS وقائمة الثغرات المستغلة فعلاً KEV، والخط الأساسي Baseline للنتائج القديمة، فهي في فرز النتائج في المقال الرئيسي. ومدد الإصلاح حسب الخطورة يجب أن تكون مكتوبة ومقاسة، وهذا هو الضابط ARC-ASC-09-03 «فحص الاعتماديات وأطر المعالجة» في دليل ضوابط أمن التطبيقات المقترح، وهو مسودة من صاحب الموقع تربط هذا الضابط بمتطلبات ASVS 15.1.1 و15.2.1 وبالفئة A03 في OWASP Top 10. وإذا كانت المكتبة نفسها عميلاً ل API خارجية، كما في مسار /api/rates، فراجع أيضاً الاستهلاك غير الآمن لل APIs، فالترقية تصلح المكتبة ولا تصلح ثقتك العمياء في رد ال API.
أخطاء شائعة Common Mistakes
- فحص ملف الاعتماديات وحده: رأيت كيف أعطى 21 ثغرة أو 33 حسب سلوك الأداة، والرقم الحقيقي 30، وأخطر النتائج كانت في مكتبات غير مكتوبة فيه.
- ملف قفل باسم لا تعرفه الأداة: Trivy تجاهل
requirements.lock.txtبصمت وأعطى تقريراً ناقصاً بكود خروج ناجح، فتأكد من سطر Scanned أو Target في المخرج أن الملف الذي تريده فحص فعلاً. - مقارنة الأرقام بين الأدوات: المجموع في جدول OSV-Scanner يعد الحزم المكررة، وتنبيهات Dependency-Track تعد المعرفات البديلة، والخطورة تختلف بين المصادر، فقارن المعرفات وليس الأرقام.
- الاعتماد على NVD وحدها لمشروع مكتبات: المطابقة فيها بمعرف CPE، وأغلب قوائم SBOM فيها purl فقط.
- دمج تلقائي دون اختبارات مطلوبة: دمج Gitea تحديث Jinja2 خلال ثوان لأن الفرع الرئيسي لا يطلب أي فحص.
- تحديث مكتبة غير مباشرة وحدها في ملف
pip freeze: طلب urllib3 الأمني كسر التثبيت، فولد ملف القفل بأداة تحل الشجرة واختبر التثبيت في كل طلب. - إغلاق طلبات Renovate للتخلص منها: الإغلاق يعني «تجاهل هذا الإصدار»، فلن يقترحه مرة أخرى.
- تجاهل دون تاريخ انتهاء: كل سطر في
osv-scanner.tomlأو.trivyignoreدونignoreUntilأوexp:قرار دائم لم يوافق عليه أحد. - اعتبار تحليل قابلية الوصول إذناً بالتجاهل: غير المستدعاة اليوم قد تستدعى غداً، فهي ترتيب للأولوية وليس إغلاقاً.
- SBOM فيها أدوات البناء: pip وأدوات توليد القائمة نفسها ظهرت كمكونات للتطبيق، فولد القائمة من بيئة التطبيق وحدها.
قائمة تحقق Checklist
- لكل مشروع ملف قفل في المستودع، مولد بأداة تحل الشجرة، ويثبت في خط CI.
- OSV-Scanner أو Trivy على ملف القفل في كل طلب دمج، ومخرج الأداة يذكر الملف الصحيح.
- فحص ال Docker Image قبل النشر، لأنه يرى حزم النظام وما يصل فعلاً.
- قائمة SBOM بصيغة CycloneDX لكل إصدار، مرفوعة إلى Dependency-Track باسم المشروع والإصدار.
- مصدر OSV مفعل في Dependency-Track للأنظمة التي تستخدمها، وNVD فقط إذا كانت لديك مكونات بمعرف CPE.
- سياسة أمن بحالة FAIL للعالية والحرجة، وسياسة ترخيص بحالة WARN، وتنبيه يصل إلى قناة يقرؤها الفريق.
- Renovate أو Dependabot بتجميع للمكتبات المرتبطة، ومهلة قبل الإصدارات الجديدة، وجدول، وطلبات أمنية فورية.
- دمج تلقائي فقط مع اختبارات مطلوبة على الفرع الرئيسي.
- تحليل قابلية الوصول لمشاريع Go مع govulncheck أو OSV-Scanner.
- كل قبول للخطر بسبب وصاحب وتاريخ انتهاء، ومدد إصلاح مكتوبة حسب الخطورة.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- أداة SCA تبني قائمة بالمكتبات من المدخل الذي تعطيه لها ثم تقارنها بنطاقات الإصدارات في الإعلانات، لذلك المدخل أهم من اسم الأداة: ملف الاعتماديات أعطانا 21 أو 33، وملف القفل أعطى 30 في OSV-Scanner وTrivy وGrype معاً.
- أغلب الخطر في الاعتماديات غير المباشرة، وفي تجربتنا كانت ثغرات urllib3 وidna في مكتبات لم يكتبها أحد.
- OSV يجمع المصادر المتخصصة بصيغة واحدة ويطابق باسم الحزمة، وGHSA فيها مراجع وغير مراجع، وNVD تطابق بمعرف CPE، والثغرة الواحدة لها عدة معرفات.
- تحليل قابلية الوصول متاح مفتوحاً ل Go، وقد خفض نتائج أداة Go الصغيرة من ثلاث إلى واحدة، وهو ترتيب للأولوية وليس تجاهلاً.
- Dependency-Track يحتفظ بقائمة SBOM لكل إصدار ويعيد تحليلها وينبه الفريق، فيرى الثغرة التي تنشر بعد يوم الإصدار.
- Renovate على Gitea فتح طلبات الإصلاح الأمني فوراً، ولكنه دمج تحديثاً دون اختبار، وكسر التثبيت بتحديث مكتبة غير مباشرة وحدها، فالإعداد والاختبارات المطلوبة أهم من الأداة.
- الترقية هي الأصل، والإصلاح الخاص والتخفيف مؤقتان، والقبول بسبب وصاحب وتاريخ انتهاء.
وفي المقال التالي من السلسلة بإذن الله سوف نتحدث عن فحص الحاويات والبنية ككود، حيث تكمل الثغرة VULN-07 في ال Base Image ما بدأناه هنا مع مكتبات التطبيق.
دليلDevSecOps للمبتدئين: الأمن داخل خط CI/CD ومتى يعمل كل فحصما الذي يجده كل فحص أمني في خط CI/CD وما الذي يفوته، وكيف ترتب نتائجه حتى تصل إلى قرار بدل قائمة طويلة.
دليلأمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئينكل ما بين كود غيرك وسيرفرك قد يحمل كود المهاجم، وهنا تعرف التهديدات الحقيقية بحوادث موثقة والدفاع المناسب لكل حلقة.
دليلSAST: كيف يجد تحليل الكود الثابت الثغرات قبل التشغيلكيف تقرأ أداة SAST الكود بحثاً عن الثغرات، وما الذي لا تراه أبداً، وكيف تمنع الإنذارات الخاطئة من أن تفقد الفريق ثقته بها.
دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.
دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.
دليلخط CI/CD آمن من البداية للنهاية على Gitea Actionsكيف تجمع فحوص الأمن في خط واحد لا يستطيع طلب الدمج تجاوزه، ثم ترسل إلى الإنتاج نفس ال Docker Image الموقعة التي فحصتها.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على نسخة من التطبيق التجريبي للسلسلة مع الثغرتين VULN-20 وVULN-21، بأدوات OSV-Scanner 2.6.0 وTrivy 0.75.0 وGrype 0.120.1 وDependency-Track 5.1.2 وcyclonedx-py 7.5.0 وRenovate 44.141.0 على Gitea 28.1.0 وgovulncheck 1.8.0 مع Go 1.27.1، والتحقق من التراخيص والسلوك من مستودعات الأدوات وتوثيقها الرسمي.