> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# SCA: كيف تجد الثغرات المعروفة في مكتبات مشروعك
- URL: https://arabroot.io/articles/فحص-الاعتماديات-sca-مع-osv-scanner-و-dependency-track/
- Published: 2026-10-05T23:30:00.000Z
- Updated: 2026-10-09T09:43:09.000Z
- Description: كيف تجد أداة فحص الاعتماديات SCA المكتبات المصابة في مشروعك، ولماذا تختلف نتائج الأدوات، وكيف تبقي مكتباتك محدثة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية

في دليل [DevSecOps للمبتدئين](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/) شغلنا OSV-Scanner على ملف `requirements.txt` في تطبيق السلسلة، فوجد خلال ثانية عشرات الثغرات المنشورة في Flask وWerkzeug وJinja2، وفي دليل [أمن سلسلة التوريد البرمجية](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) رأيت لماذا تحتاج ملف القفل 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، مهما كان اسمها:

![أربع مراحل: Read inputs فيها requirements.txt كملف اعتماديات وrequirements.lock.txt كملف قفل وsite-packages للحزم المثبتة وDocker Image أو ملف تنفيذي، ثم Inventory فيها قائمة الاسم والإصدار ومعرف purl مثل pkg:pypi/urllib3@1.26.20، ثم Match مع قواعد OSV وGHSA وNVD حيث الإعلان PYSEC-2026-1999 يصيب urllib3 قبل 2.5.0، ثم Findings تعرض urllib3 1.26.20 وCVSS 5.3 والإصلاح في 2.5.0 بصيغ text وJSON وSARIF وCycloneDX](https://arabroot.io/content/images/2026/10/sca-01-how-sca-works.webp)

الأداة تبني قائمة بالمكتبات ثم تقارن كل إصدار بنطاقات الإصدارات المصابة

في الصورة أعلاه لاحظ التالي:

- **قراءة المدخلات Inputs:** الأداة لا تعرف مكتباتك إلا من الملف الذي تعطيه لها، وكل مدخل يرى جزءاً مختلفاً، كما في الجدول التالي.
- **بناء القائمة Inventory:** لكل مكتبة اسم وإصدار ونظام حزم Ecosystem، وتجمعها الأدوات الحديثة في معرف واحد هو [Package URL واختصاراً purl](https://github.com/package-url/purl-spec?ref=arabroot.io)، مثل `pkg:pypi/urllib3@1.26.20`، وهذا المعرف هو ما يطابق به أغلب قواعد البيانات.
- **المطابقة 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 قبل النشر لأنها ما يعمل فعلاً، وهذا الجزء الأخير هو موضوع مقال [فحص الحاويات والبنية ككود](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AD%D8%A7%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9-%D9%83%D9%83%D9%88%D8%AF-%D9%85%D8%B9-trivy/). وإذا لم يكن لمشروعك ملف قفل فاكتب واحداً، وأبسط طريقة في Python هي [pip freeze](https://pip.pypa.io/en/stable/cli/pip%5Ffreeze/?ref=arabroot.io) داخل بيئة نظيفة، والأفضل أداة تولد ملف القفل من ملف الاعتماديات مثل pip-compile أو uv، وسبب ذلك سوف يظهر في تجربة Renovate.

### الاعتماديات غير المباشرة Transitive Dependencies

الصورة التالية تبين شجرة الاعتماديات Dependency Tree لتطبيق السلسلة بعد أن أضفنا إليه مكتبة `requests`، وما يراه كل مدخل منها، والأرقام من التجربة العملية في هذا المقال:

![على اليسار شجرة: app.py يعتمد على Flask 2.2.2 وrequests 2.25.1 باللون البرتقالي لأنهما مكتوبان في requirements.txt، وتحت Flask مكتبات Werkzeug وJinja2 وitsdangerous وclick وMarkupSafe، وتحت requests مكتبات urllib3 1.26.20 وidna 2.10 باللون الأحمر وchardet 4.0.0 بترخيص LGPL باللون الأصفر وcertifi، بحدود متقطعة لأنها غير مكتوبة في أي ملف. وعلى اليمين ثلاث بطاقات: requirements.txt مع --no-resolve يرى 4 مكتبات و21 ثغرة، وrequirements.txt مع تخمين الأداة يرى urllib3 1.26.9 وidna 2.9.0 بدلاً من 1.26.20 و2.10 و33 ثغرة، وrequirements.lock.txt الناتج عن pip freeze يرى 11 مكتبة و30 ثغرة](https://arabroot.io/content/images/2026/10/sca-02-transitive.webp)

ثلاثة مدخلات للتطبيق نفسه، وثلاثة أرقام مختلفة

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

## قواعد بيانات الثغرات Vulnerability Databases وكيف تختلف

أداة SCA لا تعرف الثغرات من نفسها، وإنما تقرأ إعلانات تنشرها جهات مختلفة، والفرق بين هذه الجهات يفسر أغلب الاختلاف بين الأدوات. والصورة التالية تبين من أين تأتي البيانات:

![على اليسار مصادر متخصصة: GitHub Advisories بمعرفات GHSA، وقاعدة PyPA بمعرفات PYSEC، وGo vulndb بمعرفات GO، وRustSec، وتوزيعات Debian وAlpine وUbuntu، وكلها تصب في OSV.dev بصيغة واحدة مفتاحها pkg:pypi/flask ونطاق introduced 0 وfixed 2.2.5. وتحتها NVD من NIST تأتيه سجلات CVE ويصف المنتج بمعرف cpe:2.3:a:palletsprojects:flask. وعلى اليمين: المطابقة باسم الحزمة لأدوات OSV-Scanner وRenovate وDependency-Track وGrype وTrivy، والمطابقة بمعرف CPE للأجهزة وأنظمة التشغيل، وفي الأسفل: ثغرة واحدة بثلاثة معرفات CVE-2023-30861 وGHSA-m2qf-hxjv-5gpq وPYSEC-2023-62](https://arabroot.io/content/images/2026/10/sca-03-databases.webp)

المصادر المتخصصة تصب في OSV، وNVD يصف المنتجات بمعرف CPE

- [**NVD**](https://nvd.nist.gov/?ref=arabroot.io) **من NIST:** القاعدة الوطنية الأمريكية لسجلات CVE، وهي الأوسع لأنها تغطي أي منتج، من البرامج إلى أجهزة الشبكات، ولكنها تصف المنتج المصاب بمعرف CPE مثل `cpe:2.3:a:palletsprojects:flask` وليس باسم الحزمة في PyPI. ويوضح [توثيق Dependency-Track](https://dependencytrack.github.io/docs/next/concepts/about-vulnerability-data-sources/?ref=arabroot.io) النتيجة العملية لذلك: أغلب أدوات توليد SBOM تكتب معرف purl ولا تكتب CPE، فلا تجد بيانات NVD وحدها إلا القليل في مشروع مكتبات، حتى لو كانت لها سجلات CVE.
- [**GitHub Advisory Database**](https://docs.github.com/en/code-security/concepts/vulnerability-reporting-and-management/github-advisory-database?ref=arabroot.io) **واختصاراً GHSA:** إعلانات بأسماء الحزم في npm وPyPI وMaven وغيرها، وفيها نوعان: إعلانات راجعها فريق GitHub Reviewed، وإعلانات غير مراجعة Unreviewed تنشر آلياً من NVD، ويذكر التوثيق أن Dependabot لا ينبه على غير المراجعة لأنها لم تفحص من حيث الصحة والاكتمال. وبيانات المستودع [منشورة بصيغة OSV](https://github.com/github/advisory-database?ref=arabroot.io) برخصة CC-BY-4.0.
- [**OSV.dev**](https://osv.dev/?ref=arabroot.io)**:** قاعدة من Google تجمع إعلانات GitHub و[قاعدة PyPA](https://github.com/pypa/advisory-database?ref=arabroot.io) (معرفات PYSEC) وفريق Go وRustSec وفرق أمن التوزيعات في [صيغة OSV](https://ossf.github.io/osv-schema/?ref=arabroot.io) واحدة، مفتاحها اسم الحزمة ونظامها ونطاق الإصدارات بأحداث مثل `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 فقد شرحناه في [فرز النتائج في المقال الرئيسي](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#triage).

## تحليل قابلية الوصول Reachability

أداة SCA العادية تخبرك أن الإصدار مصاب، ولكنها لا تعرف هل يستدعي كودك الدالة المصابة أم لا، فمكتبة فيها مئة دالة وثغرة في واحدة منها تظهر في التقرير مهما كان استخدامك لها. وتحليل قابلية الوصول Reachability Analysis، ويسمى أيضاً تحليل الاستدعاءات Call Analysis، يبني رسماً للاستدعاءات Call Graph من كودك إلى المكتبات، ثم يسأل: هل يوجد مسار من الكود الذي كتبته إلى الدالة المذكورة في الإعلان؟ وهذا ممكن فقط عندما يذكر الإعلان أسماء الدوال المصابة Symbols، وهي معلومة توفرها قاعدة Go مثلاً في كل إعلان.

![ملف main.go والدالة langcheck.main على اليسار، وفي الوسط الوحدة golang.org/x/text v0.3.5 فيها ثلاث ثغرات: language.Parse مع GO-2021-0113 بالأحمر وسهم أحمر من main.go:13 إليها، وlanguage.ParseAcceptLanguage مع GO-2022-1059 بحدود رمادية متقطعة، وunicode/norm مع GO-2026-5970 بحدود رمادية متقطعة، وخط متقطع مكتوب عليه no call path. وعلى اليمين: المطابقة بالإصدار فقط مع --no-call-analysis=go تعطي 3، ومع تحليل الاستدعاءات في osv-scanner وgovulncheck تعطي 1 مستدعاة و2 غير مستدعاة](https://arabroot.io/content/images/2026/10/sca-04-reachability.webp)

الإصدار نفسه فيه ثلاث ثغرات، والكود يصل إلى واحدة منها فقط

والأدوات المفتوحة التي تقوم بذلك محدودة، وهذا ما وجدناه في توثيقها الرسمي:

- [**govulncheck**](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck?ref=arabroot.io) **من فريق Go:** برخصة BSD-3-Clause، يحلل الكود المصدري أو الملف التنفيذي، ويعرض فقط الثغرات التي يصل إليها الكود، مع مسار الاستدعاء.
- [**OSV-Scanner**](https://google.github.io/osv-scanner/usage/scan-source?ref=arabroot.io)**:** يستخدم مكتبة govulncheck نفسها، وتحليل Go فيه مفعل افتراضياً ويحتاج أن يكون مترجم Go موجوداً، وتحليل Rust فيه تجريبي Experimental، ويحذر التوثيق من أنه يشغل ملفات `build.rs` في المكتبات، أي أنه ينفذ كوداً من الاعتماديات.
- **Python وJavaScript وJava:** لا يوجد تحليل استدعاءات في الأدوات المفتوحة التي يغطيها هذا المقال، وتعلن عنه منتجات تجارية، فإذا كان مشروعك بهذه اللغات فالتقرير الذي تراه هو مطابقة إصدارات فقط.

وقد يتساءل البعض: إذا كانت الثغرة غير مستدعاة فهل نحذفها من التقرير؟ والإجابة لا، والسبب أن الكود يتغير، وسطر واحد في طلب دمج قادم قد يستدعي الدالة المصابة، والتحليل نفسه قد يخطئ مع الاستدعاء عبر الانعكاس Reflection أو الواجهات. لذلك استخدمه للترتيب، فتبدأ بالمستدعاة وتعطيها أقرب موعد، وتبقى غير المستدعاة في خطة التحديث العادية.

## فحص التراخيص License Compliance

كل مكتبة تأتي بترخيص، وبعض التراخيص تفرض عليك شروطاً إذا وزعت برنامجك، وقد شرحنا أنواعها من المتساهلة Permissive إلى الحقوق المتروكة Copyleft في [البرمجيات مفتوحة المصدر وتراخيصها](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/#copyleft). وأدوات SCA نفسها تقرأ الترخيص من بيانات الحزمة، فتستطيع أن تعطيك ملخصاً للتراخيص في مشروعك وأن تنبهك على ما هو خارج قائمة سماح Allowlist تحددها: OSV-Scanner بالخيار [\--licenses](https://google.github.io/osv-scanner/usage/license-scanning/?ref=arabroot.io) وبيانات الترخيص من deps.dev، وTrivy [بماسح التراخيص License Scanner](https://trivy.dev/latest/docs/scanner/license/?ref=arabroot.io) الذي يصنفها حسب تصنيف Google، وDependency-Track بسياسة ترخيص كما سوف ترى في التجربة.

ولاحظ أمرين قبل أن تجعل هذا الفحص بوابة تمنع الدمج، فالأول أن بيانات الترخيص في الحزم ناقصة كثيراً، فبعض حزم Python تذكر الترخيص بعبارة تصنيف قديمة مثل «BSD License» بدلاً من معرف SPDX دقيق، فتظهر عند الأداة كترخيص غير معروف Non-standard، والثاني أن الأداة تعطيك معلومة وليس حكماً قانونياً، فترخيص مثل LGPL أو MPL-2.0 مقبول في أغلب التطبيقات الداخلية ويحتاج مراجعة إذا كنت توزع البرنامج للعملاء. لذلك ابدأ بالتنبيه WARN، واجعل الفشل FAIL للتراخيص التي قررت الجهة منعها فقط.

## الأدوات: نقاط القوة ونقاط الضعف

الجدول التالي يجمع الأدوات المفتوحة التي استخدمناها في هذا المقال، مع الترخيص من مستودع كل أداة والإصدار الذي شغلناه في أكتوبر 2026، وكلها نشطة وصدرت لها إصدارات خلال الأسابيع الأخيرة:

| الأداة                                                                                  | الترخيص      | الإصدار  | نقاط القوة                                                                                                                                                                         | نقاط الضعف                                                                                                                                    |
| --------------------------------------------------------------------------------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [OSV-Scanner](https://github.com/google/osv-scanner?ref=arabroot.io)                    | Apache-2.0   | 2.6.0    | يقرأ ملفات القفل لأغلب اللغات، ويعتمد على OSV مباشرة، وفيه تحليل استدعاءات ل Go، وتجاهل بسبب وتاريخ انتهاء في ملف osv-scanner.toml، وملخص تراخيص                                   | جدوله النصي يكرر الحزم عند حل الاعتماديات فيضاعف المجموع، ويخمن الإصدارات غير المباشرة من ملف الاعتماديات تخميناً قد يخطئ                     |
| [Trivy](https://github.com/aquasecurity/trivy?ref=arabroot.io)                          | Apache-2.0   | 0.75.0   | أداة واحدة للمستودع وال Image وإعدادات IaC والتراخيص، وتجاهل بتاريخ انتهاء في .trivyignore                                                                                         | يقرأ ملف requirements.txt بالاسم فقط، وأي ملف قفل باسم آخر يحتاج \--file-patterns، وقاعدة بياناته حجمها حوالي 120 ميجابايت تنزل عند أول تشغيل |
| [Grype](https://github.com/anchore/grype?ref=arabroot.io)                               | Apache-2.0   | 0.120.1  | يقرأ المجلد أو ملف SBOM جاهزاً، ويرتب النتائج بدرجة خطر تجمع CVSS وEPSS وKEV                                                                                                       | لا يفحص التراخيص، ويحتاج Syft أو SBOM جاهزة ليرى ما داخل ال Image بالتفصيل                                                                    |
| [Dependency-Track](https://github.com/DependencyTrack/dependency-track?ref=arabroot.io) | Apache-2.0   | 5.1.2    | منصة تحتفظ بقائمة SBOM لكل مشروع وإصدار، وتعيد التحليل مع كل تحديث للبيانات، وسياسات أمن وترخيص، وتنبيهات إلى Webhook وSlack وTeams وMattermost وJira والبريد، وسجل تدقيق للقرارات | خدمة كاملة مع PostgreSQL تحتاج سيرفراً وصيانة، وأول نسخ لبيانات NVD استغرق في تجربتنا حوالي 22 دقيقة                                          |
| [Renovate](https://github.com/renovatebot/renovate?ref=arabroot.io)                     | AGPL-3.0     | 44.141.0 | يعمل مع Gitea وGitLab وGitHub وغيرها، وتجميع وجدولة ودمج تلقائي بقواعد، وminimumReleaseAge، ولوحة Dependency Dashboard                                                             | إعداداته كثيرة، والإعداد الخاطئ للدمج التلقائي يدمج دون اختبار كما سوف ترى                                                                    |
| [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck?ref=arabroot.io)     | BSD-3-Clause | 1.8.0    | أدق تحليل استدعاءات مفتوح، مع مسار الاستدعاء إلى السطر                                                                                                                             | ل Go فقط                                                                                                                                      |

وأداة [cyclonedx-py](https://github.com/CycloneDX/cyclonedx-python?ref=arabroot.io) (Apache-2.0، الإصدار 7.5.0) هي التي ولدنا بها قائمة SBOM من البيئة المثبتة، أما صيغ SBOM نفسها والفرق بين CycloneDX وSPDX فقد شرحناها في [أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#sbom). وDependabot لم يدخل الجدول لأنه خدمة مدمجة في GitHub وليس أداة تشغلها بنفسك، وسوف نتحدث عنه بعد التجربة.

## تجربة عملية Lab: من ثلاثة أرقام مختلفة إلى تقرير نظيف

الآن سوف نطبق كل ما سبق على تطبيق السلسلة نفسه. وكل شيء يعمل داخل Containers باسم يبدأ ب `lab-dso-sca-`، والمنافذ على العنوان 127.0.0.1 فقط بين 19895 و19899، وتحتاج Docker فقط كما في [دليل تثبيت Docker](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/).

### تجهيز نسخة التطبيق وثغرتين جديدتين

ننسخ التطبيق، وفيه أصلاً الثغرة VULN-06 أي إصدارات Flask وWerkzeug وJinja2 القديمة، ثم نضيف ثغرتين خاصتين بهذا المقال: الأولى VULN-20 وهي مسار جديد `/api/rates` يجلب أسعار الصرف بمكتبة `requests` بإصدار قديم دون تثبيت أي من مكتباتها، والثانية VULN-21 وهي أداة صغيرة بلغة Go تعتمد على إصدار قديم من `golang.org/x/text`، نستخدمها في قسم قابلية الوصول لأن Python لا تدعمه أداة مفتوحة:

```bash
cp -r content/labs/devsecops-app lab-sca/app
cd lab-sca/app
git init -q -b main
```

وملف الاعتماديات بعد الإضافة:

```text
# 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`:

```python
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`:

```bash
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'
```

والمخرج سوف يكون كما يلي:

```text
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 على ملف الاعتماديات وعلى ملف القفل

نبدأ بالأمر الذي يكتبه أغلب الناس، أي فحص ملف الاعتماديات:

```bash
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
```

والمخرج طويل، وهذا أوله ثم صفوف المكتبتين غير المباشرتين:

```text
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 حاول أن يحل شجرة الاعتماديات بنفسه من ملف الاعتماديات، مع أن صفحة [اللغات المدعومة](https://google.github.io/osv-scanner/supported-languages-and-lockfiles/?ref=arabroot.io) تذكر هذا الحل 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`:

```bash
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.txt
```

```text
Scanned /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` لأن اسمه مختلف:

```bash
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.txt
```

```text
Scanned /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 لقاعدة بياناته حتى لا تنزل في كل مرة:

```bash
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 /src
```

```text
WARN	[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`:

```bash
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
```

```text
│        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 على المجلد:

```bash
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 table
```

```text
NAME      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`:

```go
// 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، ونقارن الفحص دون التحليل ومعه:

```bash
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'
```

والمخرج سوف يكون كما يلي، الأول دون تحليل والثاني معه:

```text
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 تعطي السبب بالسطر:

```bash
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
```

```text
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/text@v0.3.5
    Fixed in: golang.org/x/text@v0.3.7
    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 أو أحدث تغلق الثلاث معاً.

⚠️

في المحاولة الأولى شغلنا govulncheck بالإصدار v1.1.4، وهو آخر إصدار يظهر في صفحة Releases على GitHub، فتوقف بخطأ داخلي طويل في مكتبة `golang.org/x/tools` مع Go 1.27.1، ثم عمل الأمر نفسه مع `@latest` الذي جلب v1.8.0\. فإذا ثبت إصدار govulncheck في خط CI فاختر إصداراً يدعم إصدار Go عندك.

### ملخص التراخيص مع OSV-Scanner

نعطي الأداة قائمة التراخيص المسموحة، وهي هنا التراخيص المتساهلة الشائعة:

```bash
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
```

```text
+--------------+-------------------------+
| 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 من البيئة المثبتة، وليس من ملف الاعتماديات، لأن البيئة فيها بيانات الترخيص من ملفات الحزم:

```bash
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 مبني على [ملف البداية السريعة الرسمي](https://dependencytrack.github.io/docs/next/tutorials/quickstart/?ref=arabroot.io) للإصدار 5.1.2، مع تغيير المنافذ إلى 127.0.0.1، وإضافة Container صغير يستقبل التنبيهات ويطبعها:

```yaml
# 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: {}
```

```bash
docker compose up -d
```

⚠️

في التشغيل الأول فشل الأمر برسالة `dependency failed to start: container lab-dso-sca-dt-db is unhealthy`، لأن PostgreSQL كانت ما زالت تنشئ قاعدة البيانات عندما انتهت محاولات فحص الصحة الثلاث، فأعدنا `docker compose up -d` فعمل. وكلمات المرور في هذا الملف للتجربة فقط، أما في الإنتاج فاتبع دليل النشر في التوثيق.

بعد ذلك عدة خطوات في الواجهة على `http://127.0.0.1:19895`، وقد نفذناها عبر ال API بالقيم نفسها حتى نكررها بسهولة:

1. **كلمة مرور المدير:** الدخول الأول بالمستخدم `admin` وكلمة المرور `admin`، ويطلب منك تغييرها فوراً.
2. **مصادر البيانات Vulnerability Sources:** في الإصدار 5.1.2 بدأ نسخ NVD تلقائياً بعد التشغيل، واستغرق 22 دقيقة لحوالي 410 آلاف سجل، ولكنه لا يكفي لمشروع Python كما شرحنا، فمن Administration ثم Vulnerability Sources ثم OSV فعلنا مصدر OSV واخترنا نظام PyPI وحده، فانتهى نسخه في أقل من ثلاث دقائق.
3. **المشروع Project:** باسم `devsecops-shop` والإصدار `1.0`.
4. **سياسة أمن Security Policy:** باسم «No High or Critical vulnerabilities» بحالة FAIL، وشرطان: الخطورة Severity تساوي CRITICAL، أو تساوي HIGH.
5. **سياسة ترخيص License Policy:** باسم «Permissive licences only» بحالة WARN، وشرط من نوع Expression بلغة CEL مأخوذ من [أمثلة التوثيق](https://dependencytrack.github.io/docs/next/reference/policies/condition-expressions/?ref=arabroot.io)، ويطابق كل مكتبة ليس لها ترخيص معروف أو ترخيصها خارج مجموعة التراخيص المتساهلة: `!component.is_internal && (!has(component.resolved_license) || !component.resolved_license.groups.exists(g, g.name == "Permissive"))`
6. **التنبيه Alert:** من Administration ثم Notifications ثم Alerts، تنبيه بالناشر Webhook إلى `http://lab-dso-sca-dt-hook:9000/`، لمجموعات NEW\_VULNERABILITY وPOLICY\_VIOLATION وBOM\_PROCESSED، كما في [درس التنبيهات](https://dependencytrack.github.io/docs/next/tutorials/setting-up-notifications/?ref=arabroot.io).
7. **مفتاح API:** من فريق Automation الذي يملك صلاحية رفع SBOM، وهو ما يستخدمه خط CI.

ثم نرفع قائمة SBOM كما يفعل خط CI بعد كل بناء:

```bash
curl -s -X POST -H "X-Api-Key: $DT_API_KEY" \
  -F "project=$PROJECT_UUID" -F "bom=@sbom.cdx.json" \
  http://127.0.0.1:19896/api/v1/bom
```

```json
{"token":"01a115d2-9146-798a-b133-601d9bed2978","projectUuid":"d70087eb-83e1-4a18-a667-2adee10414ad"}
```

وبعد ثوان كانت أرقام المشروع من `/api/v1/metrics/project/{uuid}/current` كما يلي، وقد اختصرنا الحقول:

```json
{"critical":0,"high":11,"medium":23,"low":2,"vulnerabilities":36,"vulnerableComponents":7,"components":12,
 "policyViolationsFail":4,"policyViolationsWarn":5,"policyViolationsSecurityTotal":4,"policyViolationsLicenseTotal":5}
```

الصورة التالية تبين صفحة المشروع في الواجهة بعد التحليل:

![صفحة مشروع devsecops-shop بالإصدار 1.0 في Dependency-Track بالوضع الداكن، وفي الأعلى دوائر الخطورة 0 و11 و23 و2 و0، وأزرار Upload BOM وApply VEX وExport وReanalyze، والتبويبات Components 12 وServices 0 وDependency Graph 0 وAudit Vulnerabilities 36 و71 وExploit Predictions 71، ثم رسم Project Vulnerabilities، وتحته Critical 0 وHigh 11 وMedium 23 وLow 2 وRisk Score 126](https://arabroot.io/content/images/2026/10/sca-07-dependency-track-overview.webp)

36 ثغرة في 7 مكتبات من 12، وتبويب التدقيق يعرض 36 و71

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

![تبويب Policy Violations في Dependency-Track فيه صفوف: FAIL من نوع Security لسياسة No High or Critical vulnerabilities على urllib3 1.26.20 وidna 2.10 وWerkzeug 2.2.2، وWARN من نوع License لسياسة Permissive licences only على requests 2.25.1 وidna 2.10 وitsdangerous 2.2.0 وchardet 4.0.0 وcertifi 2026.7.22، وكلها بتاريخ 7 Oct 2026](https://arabroot.io/content/images/2026/10/sca-08-dependency-track-violations.webp)

أربع مخالفات FAIL للأمن وخمس WARN للترخيص

في التجربة أعلاه لاحظ التالي:

- **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:

```json
{
  "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 أعطتنا المعلومة نفسها، وإنما فيما يحدث بعده، والصورة التالية تبين ذلك:

![خط زمني من release 1.0 في اليوم 0 إلى new advisory في اليوم 30، وخمس خطوات: 1 CI build، 2 رفع sbom.cdx.json بصيغة CycloneDX إلى PUT /api/v1/bom، 3 Dependency-Track يحفظ كل SBOM مع السياسات والنتائج وسجل التدقيق وحالات FAIL وWARN وINFO ويعيد التحليل بعد كل نسخ للبيانات، 4 إعلان جديد عن حزمة في SBOM يصل من OSV وNVD وGHSA التي تنسخ بجدول، 5 تنبيه Webhook إلى الفريق عبر Slack وTeams وMattermost والبريد وJira](https://arabroot.io/content/images/2026/10/sca-05-continuous.webp)

الإصدار الذي يعمل منذ شهر يفحص من جديد كلما وصلت إعلانات جديدة

وبهذا الشكل يصبح السؤال «هل نستخدم المكتبة التي أعلنت ثغرتها اليوم؟ وفي أي مشروع؟ وبأي إصدار؟» بحثاً في صفحة Components بدلاً من اجتماع طارئ، وهذا هو الضابط ARC-ASC-09-02 في [دليل ضوابط أمن التطبيقات المقترح](https://arabroot.io/articles/%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%AF%D9%84%D9%8A%D9%84-%D9%85%D9%82%D8%AA%D8%B1%D8%AD-%D9%85%D8%B9-%D8%B1%D8%A8%D8%B7-ecc-%D9%88-asvs/#d09).

### التحديث مع Renovate على Gitea في سيرفرك

الآن نريد أن نصلح، وأن تبقى المكتبات حديثة بعد الإصلاح. نشغل Gitea في Container، كما في [دليل تثبيت Gitea](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-gitea-%D9%85%D8%B9-docker-compose/) ولكن بأقل إعداد ممكن، ونصنع مستخدماً للمختبر ومستخدماً للبوت:

```bash
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 lab@example.com --must-change-password=false
docker exec lab-dso-sca-gitea gitea admin user create --username renovate \
  --password 'LabOnly-Reno-2026!' --email renovate@example.com --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 التالي:

```json
{
  "$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`:** كل التحديثات التي تشترك في الاسم توضع في فرع وطلب دمج واحد كما في [خيارات الإعداد](https://docs.renovatebot.com/configuration-options/?ref=arabroot.io)، فمكتبات Flask معاً ومكتبات HTTP معاً، والسبب أن هذه المكتبات تتحدث معاً عادة وتختبر معاً.
- **`schedule`:** يوم الاثنين بين منتصف الليل والخامسة صباحاً بتوقيت الرياض، والجدول لا يشغل Renovate وإنما يحدد متى يسمح له بإنشاء الفروع إذا عمل، ويوصي التوثيق بنافذة لا تقل عن ثلاث أو أربع ساعات.
- **`minimumReleaseAge` مع `internalChecksFilter: strict`:** لا يقترح أي إصدار عمره أقل من سبعة أيام، وقد شرحنا سببه الأمني في [أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#updates)، ويحتاج أن يعطي مصدر الحزم تاريخ النشر، وPyPI يعطيه.
- **`osvVulnerabilityAlerts`:** يجعل Renovate ينزل قاعدة OSV ويفتح طلبات دمج لإصلاح الثغرات، وتقول [خيارات الإعداد](https://docs.renovatebot.com/configuration-options/?ref=arabroot.io) إنه يغطي الاعتماديات المباشرة فقط، وإن طلبات الإصلاح الأمني تتجاهل الجدول وحدود عدد الطلبات.
- **الدمج التلقائي للتحديثات الصغيرة Patch:** وسوف ترى بعد قليل لماذا هذا السطر خطير دون شرط اختبار.

ونشغل Renovate مرة واحدة كما يشغله خط مجدول Cron:

```bash
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 <renovate@example.com>" -e LOG_LEVEL=info \
  renovate/renovate:44.141.0
```

والمخرج بعد حذف تحذيرات ملاحظات الإصدار المتكررة:

```text
 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 خلال ثوان من إنشائه دون أن يراه أحد:

```text
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 فوراً. ويحذر [توثيق الخيار](https://docs.renovatebot.com/configuration-options/?ref=arabroot.io) من هذا تحديداً: إذا لم تفرض المنصة نجاح الاختبارات قبل الدمج فقد تدمج طلبات Renovate قبل أن تبدأ الاختبارات أو حتى إذا فشلت. لذلك لا تقم بتفعيل الدمج التلقائي في مستودع لا يفرض فرعه الرئيسي فحوصاً ناجحة Required Status Checks، والسبب أن الدمج التلقائي يعني عندها «ادمج دون اختبار».

ثم فتحنا طلب الدمج الخامس، وهو يرفع `urllib3` وحدها في ملف القفل:

```diff
-urllib3==1.26.20
+urllib3==2.8.0
```

وجربنا تثبيت ملف القفل من فرع هذا الطلب:

```bash
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.txt
```

```text
ERROR: 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](https://docs.renovatebot.com/modules/manager/pip-compile/?ref=arabroot.io)، وفي كل الأحوال يجب أن يثبت خط CI ملف القفل ويشغل الاختبارات على كل طلب.

بعد ذلك أغلقنا الطلبات الأمنية الخمسة الباقية، وحذفنا الجدول وحد الساعة من الإعداد حتى نرى التجميع الآن، وأطفأنا `osvVulnerabilityAlerts`، ثم شغلنا Renovate مرة ثانية:

```text
 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 بعد التشغيل الثاني:

![صفحة Pull Requests في مستودع lab/shop على Gitea 28.1.0، فيها 4 Open و6 Closed: الطلب 11 Update http client (major) من الفرع renovate/major-http-client، والطلب 10 Update flask stack to v3 من renovate/major-flask-stack، والطلب 9 Update flask stack من renovate/flask-stack، والطلب 8 Update dependency requests to v2.34.2 من renovate/http-client، وكلها من المستخدم renovate](https://arabroot.io/content/images/2026/10/sca-09-renovate-prs.webp)

أربعة طلبات مجمعة بدلاً من طلب لكل مكتبة

في النتيجة أعلاه لاحظ التالي:

- **كل مجموعة صارت طلبين:** الطلب 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، أي أقل إصدار يصلح الثغرات.
- **إغلاق الطلب يعني تجاهل الإصدار:** تقول [صفحة طلبات الدمج](https://docs.renovatebot.com/key-concepts/pull-requests/?ref=arabroot.io) إنك إذا أغلقت طلب Renovate دون دمج فهو يعتبر ذلك تجاهلاً لهذا التحديث، فلا تغلق طلب تحديث لأنك مشغول، وإنما اتركه مفتوحاً أو استخدم لوحة Dependency Dashboard.

### الترقية وإعادة الفحص حتى يصبح التقرير نظيفاً

الحل الذي اخترناه هو أن نرفع المكتبات المباشرة الأربع معاً إلى أحدث إصداراتها، ونترك pip يحل الباقي، ونولد ملف القفل من جديد:

```text
# 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
```

وملف القفل الجديد:

```text
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 على الملفين:

```bash
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
```

```text
┌───────────────────────┬──────┬─────────────────┐
│        Target         │ Type │ Vulnerabilities │
├───────────────────────┼──────┼─────────────────┤
│ requirements.lock.txt │ pip  │        0        │
├───────────────────────┼──────┼─────────────────┤
│ requirements.txt      │ pip  │        0        │
└───────────────────────┴──────┴─────────────────┘
```

وكود الخروج صفر مع `--exit-code 1`، أي أن الخط سوف ينجح. ثم OSV-Scanner على الملفين معاً:

```bash
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.txt
```

```text
Scanned /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](https://google.github.io/osv-scanner/configuration/?ref=arabroot.io) بجانب الملف المفحوص، بمعرف الثغرة وسبب مكتوب وتاريخ انتهاء:

```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."
```

```text
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` كما في [توثيق التصفية](https://trivy.dev/latest/docs/configuration/filtering/?ref=arabroot.io).

وأخيراً رفعنا قائمة SBOM جديدة من البيئة المحدثة إلى المشروع نفسه في Dependency-Track:

```json
{"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 قد يحذف ما يخص مشاريع أخرى على الجهاز:

```bash
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

كل فحص من الفحوص السابقة له مكان في الخط، وقد شرحنا أماكن الفحوص كلها في [المقال الرئيسي](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#pipeline)، وما يخص الاعتماديات هو:

- **طلب الدمج Pull Request:** OSV-Scanner على ملف القفل، ويفشل الطلب إذا ظهرت ثغرة، مع ملف `osv-scanner.toml` للقبول المؤقت. وهذا سريع لأنه لا يحتاج بناء ال Image.
- **بعد الدمج في main وعند الإصدار:** توليد SBOM ورفعها إلى Dependency-Track باسم المشروع والإصدار، ومعها فحص ال Image نفسها.
- **كل يوم أو كل أسبوع:** لا تحتاج خطاً لإعادة الفحص، لأن Dependency-Track يعيد التحليل بنفسه، ولكن تحتاج تشغيل Renovate بجدول.

والخط التالي ل [Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/) يجمع الخطوتين الأوليين، وكل أمر فيه هو أمر شغلناه في التجربة أعلاه، أما الملف نفسه فكتبناه مثالاً، وتجميع الفحوص كلها في خط واحد مع بوابات حسب الخطورة هو موضوع مقال [خط CI/CD آمن من البداية للنهاية](https://arabroot.io/articles/%D8%AE%D8%B7-ci-cd-%D8%A7%D9%85%D9%86-%D9%85%D9%86-%D8%A7%D9%84%D8%A8%D8%AF%D8%A7%D9%8A%D8%A9-%D9%84%D9%84%D9%86%D9%87%D8%A7%D9%8A%D8%A9/):

```yaml
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 "bom=@sbom.cdx.json" \
            "${{ vars.DT_URL }}/api/v1/bom"
```

في الإعداد أعلاه لاحظ التالي:

- OSV-Scanner يخرج بالكود 1 إذا وجد ثغرة، وهذا وحده يفشل الخطوة، فلا تحتاج خياراً إضافياً.
- مفتاح Dependency-Track في الأسرار Secrets، وهو مفتاح فريق Automation الذي يملك صلاحية رفع SBOM فقط، وليس مفتاح المدير.
- ثبت إصدارات ال Docker Images في الخط، وثبت ال Actions على هاش Commit كامل كما في [أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#pin-actions)، والوسم هنا للتبسيط.

## Renovate أو Dependabot؟

Dependabot هو الأداة المدمجة في GitHub لنفس الغرض، ويعمل على خوادم GitHub، فلا تشغله ولا تصونه، وفيه نوعان: تحديثات أمنية Security Updates تفتح طلباً عندما يظهر تنبيه لمكتبة في مستودعك، وتحديثات إصدارات Version Updates بجدول تحدده في ملف `.github/dependabot.yml`. ويدعم التجميع بالخيار `groups`، ومهلة قبل اقتراح الإصدار الجديد بالخيار `cooldown`، ويذكر [مرجع الخيارات](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference?ref=arabroot.io) أن له مهلة افتراضية ثلاثة أيام إذا لم تحدد واحدة، ولا تطبق على التحديثات الأمنية. والملف التالي مثال مكتوب لنفس قواعد تجربتنا، ولم نشغله لأن مستودع التجربة على Gitea:

```yaml
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

التقرير لا يفيد شيئاً حتى يتحول إلى قرار، والصورة التالية تبين القرارات الأربعة الممكنة لكل نتيجة:

![من نتيجة الفحص سؤالان: هل الإصدار مثبت فعلاً وما خطورته واحتمال استغلاله ووجوده في KEV وهل هو مستدعى، ثم هل يوجد إصدار مصلح وهل يعمل التطبيق معه، ومسار متقطع للإنذار الخاطئ. ثم أربع بطاقات: 1 Upgrade الإجابة الافتراضية بطلب Renovate واختبارات وتحريك المجموعة كلها وإعادة فحص ملف القفل، 2 Patch عند عدم وجود إصدار أو قفزة كبيرة بنقل commit الإصلاح مع التنبيه أنك أصبحت تصون نسخة خاصة، 3 Mitigate بإغلاق الطريق إلى الثغرة بالإعداد أو قاعدة WAF أو إطفاء الميزة مؤقتاً مع تذكرة، 4 Accept until a date للثغرة غير المستدعاة أو الإنذار الخاطئ بسبب وصاحب وignoreUntil في osv-scanner.toml و.trivyignore وتحليل Dependency-Track](https://arabroot.io/content/images/2026/10/sca-06-remediation.webp)

كل قرار مكتوب، والقبول ينتهي بتاريخ

- **الترقية Upgrade:** هي الإجابة في أغلب الحالات، كما رأيت في التجربة، فهي أغلقت 30 ثغرة ومشكلة ترخيص في طلب دمج واحد. وارفع المكتبات المرتبطة معاً، وأعد فحص ملف القفل بعد الترقية، لأن الترقية نفسها قد تجلب مكتبة غير مباشرة جديدة.
- **الإصلاح الخاص Patch:** عندما لا يوجد إصدار مصلح بعد، أو يكون الإصدار المصلح قفزة كبيرة لا تستطيعها اليوم، فتنقل Commit الإصلاح إلى نسخة خاصة من المكتبة. وتذكر أنك أصبحت مسؤولاً عن صيانة نسخة Fork، لذلك احذفها عند أول ترقية ممكنة.
- **التخفيف Mitigation:** أن تغلق الطريق إلى الثغرة دون تغيير المكتبة، مثل إطفاء الميزة التي تستخدم الدالة المصابة، أو قاعدة في جدار حماية التطبيقات WAF، أو حد لحجم الطلب في حالة ثغرات حجب الخدمة. وهو حل مؤقت بتذكرة وموعد.
- **القبول بتاريخ انتهاء Risk Acceptance:** للإنذار الخاطئ، أو للثغرة غير المستدعاة منخفضة الخطورة، أو لثغرة في أداة بناء لا تصل إلى الإنتاج، ويكون بسبب مكتوب وصاحب وتاريخ انتهاء كما فعلنا في `osv-scanner.toml`، والسبب أن القبول دون تاريخ يتحول مع الوقت إلى قائمة تجاهل لا يقرؤها أحد.

أما ترتيب النتائج بالخطورة CVSS واحتمال الاستغلال EPSS وقائمة الثغرات المستغلة فعلاً KEV، والخط الأساسي Baseline للنتائج القديمة، فهي في [فرز النتائج في المقال الرئيسي](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#triage). ومدد الإصلاح حسب الخطورة يجب أن تكون مكتوبة ومقاسة، وهذا هو الضابط ARC-ASC-09-03 «فحص الاعتماديات وأطر المعالجة» في [دليل ضوابط أمن التطبيقات المقترح](https://arabroot.io/articles/%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%AF%D9%84%D9%8A%D9%84-%D9%85%D9%82%D8%AA%D8%B1%D8%AD-%D9%85%D8%B9-%D8%B1%D8%A8%D8%B7-ecc-%D9%88-asvs/#d09)، وهو مسودة من صاحب الموقع تربط هذا الضابط بمتطلبات ASVS 15.1.1 و15.2.1 وبالفئة A03 في [OWASP Top 10](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#a03). وإذا كانت المكتبة نفسها عميلاً ل API خارجية، كما في مسار `/api/rates`، فراجع أيضاً [الاستهلاك غير الآمن لل APIs](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-%D9%85%D8%B9-owasp-api-security-top-10/#api10)، فالترقية تصلح المكتبة ولا تصلح ثقتك العمياء في رد ال 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 ما بدأناه هنا مع مكتبات التطبيق.

[![](https://arabroot.io/content/images/2026/10/devsecops-cover-cover.webp)دليلDevSecOps للمبتدئين: الأمن داخل خط CI/CD ومتى يعمل كل فحصما الذي يجده كل فحص أمني في خط CI/CD وما الذي يفوته، وكيف ترتب نتائجه حتى تصل إلى قرار بدل قائمة طويلة.](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/)[![](https://arabroot.io/content/images/2026/10/supply-chain-cover-cover.webp)دليلأمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئينكل ما بين كود غيرك وسيرفرك قد يحمل كود المهاجم، وهنا تعرف التهديدات الحقيقية بحوادث موثقة والدفاع المناسب لكل حلقة.](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/sast-cover-cover.webp)دليلSAST: كيف يجد تحليل الكود الثابت الثغرات قبل التشغيلكيف تقرأ أداة SAST الكود بحثاً عن الثغرات، وما الذي لا تراه أبداً، وكيف تمنع الإنذارات الخاطئة من أن تفقد الفريق ثقته بها.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/)[![](https://arabroot.io/content/images/2026/10/container-scanning-cover-cover.webp)دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AD%D8%A7%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9-%D9%83%D9%83%D9%88%D8%AF-%D9%85%D8%B9-trivy/)[![](https://arabroot.io/content/images/2026/10/dast-cover-cover.webp)دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D8%AB%D9%86%D8%A7%D8%A1-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-dast-%D9%85%D8%B9-zap/)[![](https://arabroot.io/content/images/2026/10/secure-pipeline-cover-cover.webp)دليلخط CI/CD آمن من البداية للنهاية على Gitea Actionsكيف تجمع فحوص الأمن في خط واحد لا يستطيع طلب الدمج تجاوزه، ثم ترسل إلى الإنتاج نفس ال Docker Image الموقعة التي فحصتها.](https://arabroot.io/articles/%D8%AE%D8%B7-ci-cd-%D8%A7%D9%85%D9%86-%D9%85%D9%86-%D8%A7%D9%84%D8%A8%D8%AF%D8%A7%D9%8A%D8%A9-%D9%84%D9%84%D9%86%D9%87%D8%A7%D9%8A%D8%A9/)

## سجل التحديثات

- أكتوبر 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، والتحقق من التراخيص والسلوك من مستودعات الأدوات وتوثيقها الرسمي.