> ## 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.

# SAST: كيف يجد تحليل الكود الثابت الثغرات قبل التشغيل
- URL: https://arabroot.io/articles/تحليل-الكود-الثابت-sast-مع-semgrep-و-sonarqube/
- Published: 2026-10-05T22:45:00.000Z
- Updated: 2026-10-09T09:43:05.000Z
- Description: كيف تقرأ أداة SAST الكود بحثاً عن الثغرات، وما الذي لا تراه أبداً، وكيف تمنع الإنذارات الخاطئة من أن تفقد الفريق ثقته بها.
- 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/) رأيت أن الفحص الثابت للكود SAST وجد حقن SQL وXSS في تطبيقنا التجريبي خلال ثوان، ورأيت معه أول إنذار خاطئ False Positive، وأن الأداة لم تر مفتاح API مكتوباً في الكود نفسه. وفي هذا المقال سوف ننزل طبقة أعمق، فالسؤال هنا ليس «هل نشغل أداة SAST؟» وإنما كيف تقرأ هذه الأداة الكود أصلاً، ولماذا تجد ثغرة في سطر وتفوتها في السطر الذي يليه، ولماذا يتوقف الفريق عن قراءة نتائجها بعد شهر إذا لم يضبطها أحد، وما الأداة المناسبة لمشروعك إذا كان بلغة Python أو Java أو C#.

وسوف نناقش في هذا المقال ما يلي:

- لماذا لا تكفي مراجعة الكود Code Review والبحث النصي grep لاكتشاف الثغرات، وما الذي يضيفه الفحص الثابت.
- كيف تعمل أداة SAST من الداخل: تحويل الكود إلى شجرة AST، ومطابقة الأنماط Pattern Matching، وتتبع البيانات الملوثة Taint Analysis من المصدر Source مروراً بالمنظف Sanitizer إلى المكان الخطر Sink، وحدود التحليل بين الدوال Inter-procedural.
- ما الذي لا تراه أداة SAST أبداً، والدقة Precision والشمول Recall، ولماذا تقتل الإنذارات الخاطئة الأداة في الفريق.
- Semgrep CE وSonarQube Community Build وCodeQL بتراخيصها وشروطها الحقيقية، وجدول أدوات لكل لغة، مع اهتمام خاص بأدوات Java وC#.
- تجربة عملية على التطبيق التجريبي: نتائج حقيقية، وإنذار خاطئ نتجاهله بالطريقة الصحيحة، وقاعدة Semgrep نكتبها لنمط خاص بتطبيقنا، ومقارنة مع Bandit، وإصلاح ثغرة ورؤية النتيجة تختفي، والخط الأساسي Baseline، ومخرج SARIF، وSonarQube مع بوابة الجودة Quality Gate، ثم SpotBugs مع FindSecBugs على Java، ومحللات Roslyn على C#.
- تشغيل الفحص في خط CI: الفحص التزايدي على طلب الدمج مقابل الفحص الكامل، والكود القديم، وكيف تعرض منصات الكود نتائج SARIF.

## لماذا نحتاج أداة تقرأ الكود بحثاً عن الثغرات Vulnerabilities؟

### مراجعة الكود Code Review وحدها

الطريقة الأولى التي تعتمد عليها أغلب الفرق هي مراجعة الكود في طلب الدمج Pull Request، فيقرأ زميل التغيير ويعلق عليه قبل الدمج. وهذه المراجعة مهمة جداً لكنها تفشل في اكتشاف الثغرات لأسباب واضحة، فالمراجع يقرأ الفرق Diff وليس الملف كاملاً، فإذا كانت القيمة تأتي من المستخدم في دالة لم تتغير وتصل إلى استعلام SQL في الدالة التي تغيرت فلن يرى إلا نصف القصة، وهو يركز عادة على المنطق والتسمية والتصميم، ومع طلب دمج فيه ستمائة سطر في نهاية يوم طويل فإن سطراً واحداً يبني استعلاماً بـ f-string يمر بسهولة.

### البحث النصي grep عن الدوال الخطرة

والحل الثاني الذي يخطر على البال هو أن تبحث في الكود عن الكلمات الخطرة، مثل `execute(` و`shell=True` و`eval(`، وهذا أفضل لأنه آلي ويمر على كل الملفات، لكنه يفشل من الجهتين، فهو يجد كل استدعاء ل `execute` بما فيها الاستعلامات الآمنة التي تستخدم المعاملات Parameters، فتغرق في النتائج، ولا يعرف شيئاً عن الكود نفسه، فإذا كتب أحدهم `run = db().execute` ثم `run(sql)` لم يجده، ولا يعرف إن كانت القيمة داخل الاستعلام ثابتة أم قادمة من المستخدم.

### الفحص الثابت SAST: أداة تفهم بنية الكود Code Structure

الحل الأنسب هو أداة تقرأ الكود كما يقرأه المترجم Compiler، أي أنها تفهم أن هذا استدعاء دالة وأن هذه القيمة جاءت من `request.args` ثم مرت في هذا المتغير ثم وصلت إلى ذلك الاستعلام، وهذا هو الفحص الثابت لأمان التطبيقات Static Application Security Testing واختصاراً SAST. والكلمة المهمة هنا «الثابت Static»، أي أن الأداة لا تشغل التطبيق أبداً، فلا تحتاج سيرفراً ولا قاعدة بيانات ولا متصفحاً، وإنما تقرأ الملفات فقط، لذلك تعمل في ثوان على جهاز المطور وفي كل طلب دمج، وتشير إلى الملف والسطر قبل أن يصل الكود إلى أي بيئة. وعكسها الفحص أثناء التشغيل DAST الذي شرحناه في [فحص التطبيق أثناء التشغيل DAST مع ZAP](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/)، والذي يهاجم التطبيق وهو يعمل من الخارج دون أن يرى الكود.

## كيف تعمل أداة SAST من الداخل؟

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

![مخطط من خمس خطوات: ملف app.py فيه request.args وexecute، ثم Parser، ثم شجرة AST فيها Call execute وتحتها Attribute وf-string والمتغير q باللون الأحمر، ثم محركان: Pattern matching يطابق الشكل وData flow يتتبع المصدر إلى المكان الخطر، ثم Findings برقم السطر وصيغ text وJSON وSARIF](https://arabroot.io/content/images/2026/10/sast-01-how-sast-works.webp)

الأداة لا تشغل الكود، وإنما تحوله إلى شجرة ثم تبحث فيها بالقواعد

### تحويل الكود إلى شجرة AST بالتحليل النحوي Parsing

الخطوة الأولى هي التحليل النحوي Parsing، فالأداة تملك محللاً Parser لكل لغة تدعمها، يحول النص إلى شجرة البنية المجردة Abstract Syntax Tree واختصاراً AST، وهي الشجرة نفسها التي يبنيها المترجم قبل أن يحول الكود إلى برنامج. ففي الشجرة لا يوجد شيء اسمه «سطر فيه كلمة execute»، وإنما يوجد عقدة استدعاء Call، دالتها `db().execute`، ومعاملها الأول نص منسق f-string فيه جزء ثابت هو `SELECT ...` وجزء متغير هو `q`. وهذا ما يجعل الأداة لا تهتم بالمسافات والتعليقات وأسماء المتغيرات، فالكود `db().execute(f"...{q}")` والكود نفسه مقسماً على خمسة أسطر لهما الشجرة نفسها.

ويجدر الانتباه هنا إلى أن بعض الأدوات تحتاج أكثر من الشجرة، فأدوات Java وC# الكبيرة تحتاج أن يبنى المشروع Build حتى تعرف نوع كل متغير والمكتبة التي جاء منها كل كلاس، فـ SpotBugs مثلاً يقرأ ملفات ال bytecode بعد الترجمة وليس ملفات `.java`، ومحللات Roslyn تعمل داخل المترجم نفسه أثناء `dotnet build`، بينما Semgrep يكتفي بالملفات كما هي ولا يحتاج إلى بناء المشروع، وهذا يجعله أسرع وأسهل في الإعداد، وأقل معرفة بالأنواع Types في الوقت نفسه.

### مطابقة الأنماط Pattern Matching

أبسط نوع من القواعد هو مطابقة الأنماط، أي أن القاعدة تصف شكل الكود الخطر، مثل «أي استدعاء ل `$DB.execute(...)` معامله الأول نص منسق»، والأداة تبحث في الشجرة عن كل عقدة بهذا الشكل. وهذه القواعد سريعة وسهلة الكتابة، وتجد الثغرة حتى لو كانت القيمة قادمة من دالة أخرى، لكنها لا تعرف من أين جاءت القيمة داخل النص، فتبلغ عن الاستعلام الذي يبنى من قائمة ثابتة كما تبلغ عن الاستعلام الذي يبنى من مدخلات المستخدم، وهنا يبدأ الإنذار الخاطئ كما سوف ترى في التجربة.

### تتبع البيانات الملوثة Taint Analysis

النوع الأذكى هو تحليل تدفق البيانات Data Flow Analysis، وأشهر صوره تتبع البيانات الملوثة Taint Analysis. والفكرة أن القاعدة تعرف ثلاثة أشياء:

- **المصدر Source:** المكان الذي تدخل منه بيانات لا تثق بها، مثل `request.args` و`request.form` في Flask، أو معامل دالة Controller في ASP.NET Core، أو `HttpServletRequest.getParameter` في Java.
- **المكان الخطر Sink:** الدالة التي يصبح فيها هذا المدخل خطراً، مثل `execute` لقاعدة البيانات، و`subprocess.run` مع `shell=True`، و`redirect`، ودالة تكتب HTML.
- **المنظف Sanitizer:** الدالة التي تجعل القيمة آمنة لهذا المكان بالتحديد، مثل `markupsafe.escape` قبل HTML، أو المعامل `?` بدل دمج النص في SQL.

ثم تعلم الأداة كل قيمة تخرج من المصدر بأنها «ملوثة Tainted»، وتتبعها عبر الإسناد إلى متغير آخر وبناء النصوص واستدعاء الدوال، فإذا وصلت إلى المكان الخطر دون أن تمر بمنظف أبلغت عنها. والصورة التالية تبين المسارين:

![مسارين: في الأول يخرج request.args من مستخدم أحمر ويمر عبر q وf-string ويصل إلى db().execute دون شيء في الطريق فتظهر علامة finding حمراء، وفي الثاني يمر عبر درع أخضر مكتوب تحته escape(q) أو ? param فيصل إلى db().execute وتظهر علامة quiet خضراء، وتحتهما أمثلة لأماكن خطرة أخرى: PAGE.format للـ XSS وredirect وsubprocess.run](https://arabroot.io/content/images/2026/10/sast-02-taint.webp)

المصدر Source والمنظف Sanitizer والمكان الخطر Sink، والأداة تعرفها من القواعد فقط

لاحظ الجملة الأخيرة في الصورة، فالأداة لا تعرف أن الدالة `clean_input()` التي كتبها زميلك قبل سنتين منظف، ولا تعرف أن `PAGE` في تطبيقنا قالب HTML خام، إلا إذا أخبرتها القاعدة بذلك، وهذا هو سبب أهمية القواعد المخصصة Custom Rules التي سوف نكتب واحدة منها في التجربة.

### حدود التحليل بين الدوال Inter-procedural

وقد يتساءل البعض: إذا كانت الأداة تتبع القيمة، فلماذا لا تتبعها دائماً حتى النهاية؟ والإجابة أن تتبع القيمة داخل دالة واحدة Intra-procedural رخيص، أما تتبعها عبر كل استدعاء لكل دالة في كل ملف Inter-procedural فيكلف وقتاً وذاكرة تزيد بسرعة مع حجم المشروع، لذلك تتوقف كل أداة في مكان ما. ويذكر جدول [مقارنة Semgrep CE مع Semgrep Code](https://docs.semgrep.dev/semgrep-pro-vs-oss?ref=arabroot.io) أن النسخة المجتمعية تقوم بتحليل التلوث داخل الدالة الواحدة Single function taint analysis، أما التحليل عبر الدوال والملفات فهو في النسخة التجارية، وصفحة القاعدة [CA3001 من Microsoft](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca3001?ref=arabroot.io) تقول إن القاعدة لا تتتبع البيانات بين ال Assemblies، وإن لعمق التتبع بين الدوال حداً يمكن ضبطه.

ولنأخذ مثالاً صغيراً نقسم فيه ثغرة SQL على دالتين، فالمصدر في دالة المسار والمكان الخطر في دالة مساعدة:

```python
import sqlite3

from flask import Flask, request

app = Flask(__name__)

def find_products(term):
    conn = sqlite3.connect("/tmp/shop.db")
    return conn.execute(f"SELECT name FROM products WHERE name LIKE '%{term}%'").fetchall()

@app.route("/search2")
def search2():
    return {"rows": find_products(request.args.get("q", ""))}

```

الآن نفحص الملف بقواعد Flask فقط، وهي قواعد تتبع التلوث:

```bash
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.178.0 \
  semgrep scan --config p/flask --metrics=off .
```

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

```text
✅ Scan completed successfully.
 • Findings: 0 (0 blocking)
 • Rules run: 18
 • Targets scanned: 1
 • Parsed lines: ~100.0%
 • No ignore information available
Ran 18 rules on 1 file: 0 findings.
```

صفر نتائج، والثغرة موجودة. ثم نفحص الملف نفسه بالمجموعة الافتراضية `p/default` التي تضم قواعد أنماط أيضاً:

```text
┌─────────────────┐
│ 2 Code Findings │
└─────────────────┘

    helper.py
    ❯❱ python.lang.security.audit.formatted-sql-query.formatted-sql-query
          ❰❰ Blocking ❱❱
          Detected possible formatted SQL query. Use parameterized queries instead.
          Details: https://sg.run/EkWw

           10┆ return conn.execute(f"SELECT name FROM products WHERE name LIKE '%{term}%'").fetchall()

   ❯❯❱ python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
          ❰❰ Blocking ❱❱
          Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL
          Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy
          provides TextualSQL to easily used prepared statement with named parameters. For complex SQL
          composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy
          ORM will be a better option.
          Details: https://sg.run/2b1L

           10┆ return conn.execute(f"SELECT name FROM products WHERE name LIKE '%{term}%'").fetchall()
```

في المخرج أعلاه لاحظ التالي:

- قاعدة Flask التي تتبع التلوث `tainted-sql-string` لم تجد شيئاً، لأن المصدر في `search2` والمكان الخطر في `find_products`، والتحليل في النسخة المجتمعية يتوقف عند حدود الدالة، وهذا إنذار مفقود False Negative.
- القاعدتان اللتان وجدتا الثغرة قاعدتا أنماط، فهما تريان «نص منسق داخل execute» ولا تعرفان أن `term` قادم من المستخدم، وهما نفسهما سوف تبلغان عن استعلام آمن في التجربة بعد قليل.
- Bandit أيضاً وجد هذا السطر بالقاعدة `B608`، وهي قاعدة أنماط كذلك، كما سوف ترى في المقارنة.

والصورة التالية تلخص ما حدث، ومن يستطيع تتبع القيمة عبر الاستدعاء:

![كود من دالتين: search2 فيها request.args.get ومكتوب تحتها Source here، وfind_products فيها conn.execute مع f-string ومكتوب تحتها Sink here، وسهم call بينهما، وعلى اليمين ثلاث نتائج: Taint inside one function بعلامة حمراء لأن Semgrep CE وجد صفر نتائج، وPattern on the sink line بعلامة صفراء لأن formatted-sql-query وBandit B608 وجدا السطر، وTaint across functions بعلامة خضراء لـ CodeQL وRoslyn CA3001 والمحركات التجارية](https://arabroot.io/content/images/2026/10/sast-03-functions.webp)

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

والدرس هنا عملي، فلا تحكم على أداة من عدد النتائج في مثال صغير، وإنما اسأل: هل تتبع القيمة بين الدوال؟ وبين الملفات؟ وإلى أي عمق؟ فالمشروع الحقيقي يقرأ المدخل في Controller ويمرره إلى Service ثم إلى Repository، أي ثلاث دوال في ثلاثة ملفات، وهذا هو المكان الذي تختلف فيه الأدوات فعلاً.

## ما الذي لا تراه أداة SAST أبداً؟

هناك أشياء لا تراها أي أداة SAST مهما كانت قوية، والسبب أنها ليست في الكود أصلاً، أو أنها في الكود لكن الخطأ فيها يعتمد على معنى لا تعرفه الأداة. والصورة التالية تبين ما تراه الأداة وما يقع خارج نظرها، والفحص الذي يغطي كل جزء:

![على اليسار لوحة خضراء Inside the repository فيها ملفات وعدسة وقائمة بما تراه الأداة: SQL وأوامر من نصوص، ومدخلات تصل إلى HTML وredirect، ومفاتيح مكتوبة في الكود، وتشفير ضعيف، وshell=True، والملف والسطر، وعلى اليمين خمس بطاقات خارج نظرها: Runtime configuration مع DAST، وAuthorization logic مع الاختبارات والمراجعة، وBusiness logic مع نمذجة التهديدات، وLibraries مع SCA، وDocker Image والسيرفر مع فحص الحاويات](https://arabroot.io/content/images/2026/10/sast-04-blind-spots.webp)

أداة SAST ترى المستودع فقط، ولكل منطقة خارجه فحص آخر

- **الإعداد وقت التشغيل Runtime Configuration:** متغير البيئة `DEBUG=1` على سيرفر الإنتاج Production، وترويسات الأمان التي يضيفها ال Reverse Proxy، وإعداد TLS، فكلها خارج المستودع أو تتغير بعد النشر، لذلك يكشفها الفحص أثناء التشغيل وفحص الإعدادات، وقد ناقشنا الترويسات في [ترويسات الأمان HSTS وCSP](https://arabroot.io/articles/%D8%AA%D8%B1%D9%88%D9%8A%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%85%D8%A7%D9%86-hsts-%D9%88-csp/).
- **منطق الصلاحيات Authorization Logic:** إذا فتح المستخدم B الرابط `/orders/17` الخاص بالمستخدم A وظهر له الطلب، فهذه ثغرة خطيرة، لكن الكود «صحيح» من وجهة نظر الأداة، فهو يقرأ رقماً ويجلب سطراً من قاعدة البيانات بمعامل آمن، والأداة لا تعرف من يملك الطلب رقم 17، لذلك يكشف هذا النوع اختبار يكتبه الفريق «مستخدم لا يرى بيانات غيره» ومراجعة تصميم الصلاحيات، وقد شرحنا نماذجها في [الصلاحيات المركزية Authorization](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/).
- **منطق العمل Business Logic:** كوبون خصم يستخدم مرتين، أو كمية سالبة تجعل السعر سالباً، أو خطوة دفع يمكن تخطيها، وكلها أخطاء في المعنى وليس في الشكل، ومكانها [نمذجة التهديدات Threat Modeling](https://arabroot.io/articles/%D9%86%D9%85%D8%B0%D8%AC%D8%A9-%D8%A7%D9%84%D8%AA%D9%87%D8%AF%D9%8A%D8%AF%D8%A7%D8%AA-threat-modeling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) قبل الكتابة والاختبارات بعدها.
- **المكتبات التي لم تكتبها:** Flask 2.2.2 في تطبيقنا فيه ثغرات منشورة، لكن الكود الضعيف في مجلد المكتبات وليس في مستودعك، ومكانه فحص الاعتماديات SCA في [فحص الاعتماديات مع OSV-Scanner وDependency-Track](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA-sca-%D9%85%D8%B9-osv-scanner-%D9%88-dependency-track/).
- **ال Docker Image والسيرفر:** حزم نظام التشغيل القديمة والمنافذ المفتوحة، ومكانها [فحص الحاويات والبنية ككود مع Trivy](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/).

لذلك لا تقم بقراءة تقرير SAST نظيف على أنه «التطبيق آمن»، والسبب أن التقرير يقول شيئاً واحداً فقط، وهو أن الأنماط التي تعرفها القواعد لم تظهر في الكود الذي قرأته الأداة.

## الدقة Precision والشمول Recall، ولماذا تقتل الإنذارات الخاطئة الأداة

كل أداة فحص تقع بين هدفين متعارضين. الأول هو الشمول Recall، أي نسبة الثغرات الحقيقية التي وجدتها الأداة من كل الثغرات الموجودة، والثاني هو الدقة Precision، أي نسبة التنبيهات الصحيحة من كل التنبيهات التي أطلقتها. والصورة التالية مثال توضيحي لأداتين على الكود نفسه، وفيه عشر ثغرات حقيقية بين ستين موضعاً:

![لوحتان من النقاط: في Tool A الصاخبة 40 حلقة تنبيه تحيط بكل الثغرات العشر الحمراء و30 نقطة رمادية آمنة، فالدقة 25% والشمول 100%، وفي Tool B الهادئة 7 حلقات تحيط بست ثغرات ونقطة آمنة واحدة، فالدقة 86% والشمول 60%](https://arabroot.io/content/images/2026/10/sast-05-precision-recall.webp)

مثال توضيحي: الأداة الصاخبة تجد كل شيء ولا يقرؤها أحد، والهادئة يقرأ الفريق كل تنبيه منها

والمعادلتان بسيطتان، فالدقة هي عدد التنبيهات الصحيحة True Positives مقسوماً على كل التنبيهات، والشمول هو عدد التنبيهات الصحيحة مقسوماً على كل الثغرات الحقيقية، أي الصحيحة مع المفقودة False Negatives. وقد تبدو الأداة A أفضل لأنها لم تفوت شيئاً، لكن في الواقع فهي غير كذلك، فالمطور الذي يفتح طلب دمج ويجد أربعين تنبيهاً، ويكتشف أن أول عشرة منها خاطئة، سوف يتوقف عن القراءة ويضغط «تجاهل الكل»، وبهذا تصبح الثغرات العشر الحقيقية مدفونة بين التنبيهات، وتصبح الدقة الفعلية للأداة صفراً لأن أحداً لا يقرؤها. أما الأداة B فقد فوتت أربع ثغرات، لكن كل تنبيه منها يقرأ ويصلح، والثغرات الأربع يمكن أن تكشفها طبقة أخرى من الفحص أو قاعدة إضافية في المستقبل.

لذلك فالقاعدة العملية هي: ابدأ بقواعد عالية الدقة تمنع الدمج، واجعل القواعد الصاخبة في وضع التنبيه فقط أو في الفحص الليلي، ولا تقم بتفعيل كل القواعد المتاحة في اليوم الأول، والسبب أن ثقة الفريق في الأداة تبنى ببطء وتضيع بسرعة. وقد ذكرنا في [قسم الإنذارات الخاطئة في دليل 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/#false-positives) كيف يتعامل الفريق مع النتيجة الخاطئة، وفي التجربة سوف نطبق ذلك على نتيجة حقيقية.

## الأدوات Tools بين المحركات العامة وأدوات كل لغة

تنقسم أدوات SAST مفتوحة المصدر إلى محركات عامة تدعم لغات كثيرة بقواعد قابلة للكتابة، وأدوات خاصة بلغة واحدة تعرفها بعمق. وقبل الجدول نتوقف عند الثلاثة الكبار، لأن تراخيصها وشروطها فيها تفاصيل يجب أن تعرفها قبل أن تبني عليها خط CI.

### Semgrep CE: المحرك Engine مفتوح والقواعد Rules لها ترخيص آخر

[Semgrep](https://github.com/semgrep/semgrep?ref=arabroot.io) محرك يكتب قواعده بلغة قريبة من الكود نفسه، فالقاعدة `$DB.execute(f"...")` تشبه الكود الذي تبحث عنه، وهذا ما يجعل كتابة القواعد المخصصة سهلة لأي مطور. وتنص [صفحة التراخيص الرسمية](https://docs.semgrep.dev/licensing?ref=arabroot.io) على أن المحرك المجتمعي Semgrep CE مفتوح المصدر بترخيص LGPL-2.1، أما القواعد التي تكتبها الشركة وتنشرها في السجل Registry فلها ترخيص منفصل اسمه Semgrep Rules License v1.0، يسمح للأفراد والشركات باستخدامها داخلياً، ويمنع استخدامها في منتج أو خدمة SaaS منافسة. وفي النسخة المجتمعية التحليل داخل الملف الواحد والدالة الواحدة كما رأيت، والتحليل عبر الملفات في المنتج التجاري Semgrep Code.

ويجدر الانتباه إلى أمرين عمليين. الأول أن Semgrep يرسل افتراضياً إحصاءات استخدام Metrics عند تحميل القواعد من السجل، وتؤكد [صفحة الإحصاءات](https://docs.semgrep.dev/metrics?ref=arabroot.io) أن الكود المصدري لا يرسل أبداً، لكن مع مشروع خاص في خط CI فالأنسب أن تضيف `--metrics=off`، وهنا تظهر المشكلة الثانية، فالخيار `--config auto` الذي يختار القواعد حسب لغات مشروعك لا يعمل مع إيقاف الإحصاءات، وهذا ما رأيناه في التجربة:

```text
[00.30][ERROR]: Cannot create auto config when metrics are off. Please allow metrics or run with a specific config.
```

لذلك استخدم `--config auto` للاستكشاف الأول على جهازك، وفي خط CI اختر مجموعة قواعد محددة مثل `p/default` أو `p/python` مع `--metrics=off`، وهذا أفضل أيضاً لأن النتائج تصبح قابلة للتكرار. وهناك أيضاً [Opengrep](https://github.com/opengrep/opengrep?ref=arabroot.io)، وهو نسخة منفصلة Fork من Semgrep بترخيص LGPL-2.1، أنشأها تحالف من شركات أمن بعد أن نقلت Semgrep بعض الميزات إلى الترخيص التجاري، ويقول ملف README الخاص به إنه متوافق مع قواعد Semgrep ويضيف تتبعاً للتلوث بين الدوال داخل الملف الواحد بالخيار `--taint-intrafile`، ولم نختبره في هذا المقال.

### SonarQube Community Build: الجودة Quality والأمان Security في لوحة واحدة

[SonarQube](https://github.com/SonarSource/sonarqube?ref=arabroot.io) سيرفر يجمع نتائج تحليل الكود في لوحة واحدة، الأخطاء Bugs ورائحة الكود Code Smells والتكرار والتغطية والأمان، ونسخته المجانية اسمها اليوم Community Build، وكانت تسمى سابقاً Community Edition، وترخيصها LGPL-3.0 كما في ملف LICENSE في المستودع. وأهم فكرة فيه هي بوابة الجودة Quality Gate، وهي شروط يجب أن ينجح فيها التحليل، والبوابة الافتراضية اسمها Sonar way، وتذكر [صفحة بوابات الجودة](https://docs.sonarsource.com/sonarqube-community-build/quality-standards-administration/managing-quality-gates/introduction-to-quality-gates?ref=arabroot.io) أنها تطبق شروطها على الكود الجديد New Code فقط: لا توجد مشاكل جديدة، وكل نقاط الأمان الحساسة الجديدة تمت مراجعتها، وتغطية الاختبارات للكود الجديد 80% على الأقل، والتكرار فيه 3% على الأكثر. وهذه الفكرة، أي أن تحكم على الكود الجديد فقط، هي نفسها فكرة الخط الأساسي Baseline التي سوف نعود إليها.

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

- تذكر [مقارنة النسخ في مدونة Sonar](https://www.sonarsource.com/blog/sonarqube-compare-editions/?ref=arabroot.io) أن Community Build لا تحتوي على تحليل الفروع Branches وطلبات الدمج Pull Requests، أي أنها تحلل فرعاً رئيسياً واحداً، ولا تحتوي على تحليل التلوث Taint Analysis، وهو المسؤول عن قواعد الحقن مثل SQL Injection وCommand Injection في النسخ التجارية، وتعالج تحليلاً واحداً في الوقت نفسه.
- نقاط الأمان الحساسة Security Hotspots كانت نوعاً خاصاً من النتائج في SonarQube، وهي كود حساس يحتاج مراجعة إنسان ليقرر إن كان آمناً، بحالات «للمراجعة To review» و«تم الإقرار Acknowledged» و«تم الإصلاح Fixed» و«آمن Safe»، بينما الثغرة Vulnerability مشكلة يجب إصلاحها. لكن Sonar أعلنت في [مارس 2026](https://community.sonarsource.com/t/moving-security-hotspots-to-security-issues/180583?ref=arabroot.io) أنها تعيد كتابة كل قواعد ال Hotspots كمشاكل أمان عادية Security Issues، وأن واجهاتها البرمجية سوف تحذف لاحقاً، وفي النسخة التي شغلناها 26.9 لم نجد أي قاعدة من نوع Hotspot، وظهرت قاعدة مثل «SECRET\_KEY مكتوب في الكود» كثغرة Vulnerability، وبعضها يحمل الوسم `former-hotspot`، وصفحة Security Hotspots في الواجهة عليها كلمة Deprecated كما سوف ترى في الصورة.
- قاعدة البيانات المدمجة في ال Docker Image للتجربة فقط، والواجهة نفسها تحذر من ذلك، وللاستخدام الحقيقي تحتاج PostgreSQL.

### CodeQL: الكود كقاعدة بيانات Database، وشروط استخدام Terms of Use يجب قراءتها

CodeQL من GitHub يعامل الكود كبيانات، فيبني من المشروع قاعدة بيانات CodeQL Database فيها الشجرة وتدفق البيانات والأنواع، ثم تشغل عليها استعلامات بلغة QL تشبه SQL، كما تشرح [صفحة Code Scanning](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning?ref=arabroot.io). وهو من أقوى الأدوات في التحليل بين الدوال والملفات، ويدعم C وC++ وC# وGo وJava وKotlin وJavaScript وTypeScript وPython وRuby وRust وSwift. لكن هنا تفصيل في الترخيص يقع فيه كثيرون:

- الاستعلامات والمكتبات في مستودع [github/codeql](https://github.com/github/codeql?ref=arabroot.io) بترخيص MIT، أي أنها مفتوحة المصدر.
- أما أداة CodeQL CLI نفسها، أي البرنامج الذي يبني قاعدة البيانات ويشغل الاستعلامات، فلها [شروط GitHub CodeQL](https://github.com/github/codeql-cli-binaries/blob/main/LICENSE.md?ref=arabroot.io)، وهي تسمح باستخدامها للبحث الأكاديمي وللعرض التوضيحي، وعلى الكود المنشور بترخيص مفتوح المصدر معتمد من OSI فقط، وتسمح ببناء قواعد البيانات في خط CI آلي فقط للمشاريع مفتوحة المصدر المستضافة على GitHub.com، وتمنع صراحة استخدامها مع كود ليس مفتوح المصدر.
- ولاستخدامها على مستودع خاص في GitHub تحتاج ترخيص GitHub Code Security، كما تذكر صفحة Code Scanning نفسها.

لذلك إذا كان كود شركتك خاصاً ومستضافاً على Gitea أو GitLab على سيرفرك، فلا تقم بتشغيل CodeQL CLI عليه في خط CI، والسبب أن الشروط تمنع ذلك صراحة حتى لو كانت الأداة متاحة للتحميل، والخيار المفتوح لهذه الحالة هو Semgrep CE أو Opengrep مع SonarQube Community Build وأدوات اللغة.

### جدول الأدوات لكل لغة Per-language Tools

بالإضافة إلى المحركات العامة، لكل لغة أدوات تعرفها بعمق، وهي غالباً أسهل في الإضافة لأنها تعمل كإضافة لأداة البناء نفسها. والجدول التالي يجمع أشهرها مع الترخيص من ملف LICENSE في المستودع وآخر إصدار وقت كتابة المقال:

| الأداة                                                                                                                                            | اللغة                                                              | الترخيص                                                                                                                                     | آخر إصدار                                                      | نقاط القوة                                                                                                                                       | نقاط الضعف                                                                                                                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [Semgrep CE](https://github.com/semgrep/semgrep?ref=arabroot.io)                                                                                  | أكثر من 30 لغة                                                     | المحرك LGPL-2.1، والقواعد Semgrep Rules License                                                                                             | 1.179.0 (أكتوبر 2026)                                          | قواعد قريبة من الكود وسهلة الكتابة، ولا يحتاج بناء المشروع، وسريع                                                                                | التلوث داخل الدالة الواحدة، وتكرار النتائج من قواعد كثيرة على السطر نفسه                                                                      |
| [SonarQube Community Build](https://github.com/SonarSource/sonarqube?ref=arabroot.io)                                                             | Java وC# وPython وJavaScript وTypeScript وأكثر من 15 لغة أخرى      | LGPL-3.0                                                                                                                                    | 26.9.0                                                         | لوحة وتاريخ وبوابة جودة على الكود الجديد، والجودة مع الأمان                                                                                      | لا يوجد تحليل تلوث ولا تحليل للفروع وطلبات الدمج في النسخة المجانية، ويحتاج سيرفراً وقاعدة بيانات                                             |
| [CodeQL](https://github.com/github/codeql?ref=arabroot.io)                                                                                        | C/C++ وC# وGo وJava وKotlin وJavaScript وPython وRuby وRust وSwift | الاستعلامات MIT، والأداة بشروط GitHub                                                                                                       | مع GitHub                                                      | أعمق تحليل بين الدوال والملفات، واستعلامات قابلة للكتابة                                                                                         | الأداة نفسها للكود مفتوح المصدر فقط إلا بترخيص تجاري، وبناء قاعدة البيانات بطيء                                                               |
| [Bandit](https://github.com/PyCQA/bandit?ref=arabroot.io)                                                                                         | Python                                                             | Apache-2.0                                                                                                                                  | 1.9.4 (فبراير 2026)                                            | بسيط وسريع، ويجد الأسرار المكتوبة في الكود ودوال خطرة كثيرة                                                                                      | قواعد أنماط فقط، فلا يرى XSS ولا Open Redirect في تجربتنا                                                                                     |
| [gosec](https://github.com/securego/gosec?ref=arabroot.io)                                                                                        | Go                                                                 | Apache-2.0                                                                                                                                  | 2.29.0 (أغسطس 2026)                                            | يفهم أنواع Go ويخرج SARIF، ونشط التطوير                                                                                                          | خاص بلغة Go فقط، فتحتاج معه أداة أخرى للقواعد المخصصة أو للغات الأخرى في المشروع                                                              |
| [Brakeman](https://github.com/presidentbeef/brakeman?ref=arabroot.io)                                                                             | Ruby on Rails                                                      | [Brakeman Public Use License](https://github.com/presidentbeef/brakeman/blob/main/LICENSE.md?ref=arabroot.io) (ليس مفتوح المصدر بتعريف OSI) | 8.1.0 (أكتوبر 2026)                                            | يفهم بنية Rails من النماذج إلى ال Views                                                                                                          | الترخيص يسمح بفحص برامجك، ويمنع الاستخدام التجاري مثل خدمة SaaS أو منتج دون ترخيص مدفوع                                                       |
| [SpotBugs](https://github.com/spotbugs/spotbugs?ref=arabroot.io) مع [FindSecBugs](https://github.com/find-sec-bugs/find-sec-bugs?ref=arabroot.io) | Java وKotlin وScala                                                | SpotBugs بترخيص LGPL-2.1، وFindSecBugs بترخيص LGPL-3.0                                                                                      | SpotBugs 4.10.4 (أغسطس 2026)، وFindSecBugs 1.14.0 (أبريل 2025) | يعمل في Maven وGradle، ويقرأ ال bytecode فيعرف الأنواع، و[144 نوعاً من الثغرات](https://find-sec-bugs.github.io/?ref=arabroot.io) في FindSecBugs | يحتاج بناء المشروع، وإصدارات FindSecBugs متباعدة                                                                                              |
| [Roslyn .NET Analyzers](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings?ref=arabroot.io)      | C# وVisual Basic                                                   | MIT                                                                                                                                         | تأتي مع .NET SDK                                               | داخل المترجم نفسه، ولا تحتاج أي تثبيت، وقواعد CA3001 إلى CA3012 تتتبع التلوث بين الدوال                                                          | قواعد التلوث معطلة افتراضياً، ولا تتتبع البيانات بين ال Assemblies                                                                            |
| [Security Code Scan](https://github.com/security-code-scan/security-code-scan?ref=arabroot.io)                                                    | C# وVisual Basic                                                   | LGPL-3.0                                                                                                                                    | 5.6.7 (سبتمبر 2022)                                            | كان أشهر محلل أمان مفتوح ل .NET                                                                                                                  | لا يوجد إصدار منذ سبتمبر 2022، فلا تبدأ به مشروعاً جديداً                                                                                     |
| [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security?ref=arabroot.io)                                              | JavaScript وNode.js                                                | Apache-2.0                                                                                                                                  | 4.2.0 (أكتوبر 2026)                                            | يعمل داخل ESLint الموجود في المشروع غالباً                                                                                                       | يقول ملف README نفسه إنه يجد الكثير من الإنذارات الخاطئة التي تحتاج فرزاً، مثل قاعدة detect-object-injection التي تنبه على كل variable\[key\] |

### مطورو Java وC#: ماذا تستخدم بالتحديد؟

إذا كنت تكتب Java فالبداية الأسهل هي [إضافة SpotBugs ل Maven](https://spotbugs.github.io/spotbugs-maven-plugin/?ref=arabroot.io) مع FindSecBugs كإضافة داخلها، فهي تعمل بعد الترجمة على ال bytecode، لذلك تعرف أن `Statement.executeQuery` هي دالة JDBC بالتحديد، وأن المعامل نص غير ثابت، وقائمة [أنواع الثغرات في FindSecBugs](https://find-sec-bugs.github.io/bugs.htm?ref=arabroot.io) تغطي Spring وJAX-RS وServlets وJPA وأخطاء التشفير، ثم أضف SonarQube Community Build للوحة والتاريخ وبوابة الجودة، وSemgrep للقواعد المخصصة لأن كتابة قاعدة فيه أسهل بكثير من كتابة كاشف Detector في SpotBugs.

وإذا كنت تكتب C# فلديك محلل أمان داخل الأداة التي تستخدمها كل يوم، فمحللات [Roslyn .NET Analyzers](https://github.com/dotnet/roslyn-analyzers?ref=arabroot.io) انتقلت إلى مستودع dotnet/sdk وتأتي مع كل .NET SDK، وفيها مجموعة قواعد الأمان التي تبدأ ب CA2xxx وCA3xxx وCA5xxx. وأهمها قواعد التلوث من CA3001 لحقن SQL إلى CA3012، والتي تتتبع القيمة من مدخلات طلب HTTP إلى المكان الخطر عبر الدوال، لكنها كما في صفحة CA3001 معطلة افتراضياً في .NET 10، فإذا لم تقم بتفعيلها فأنت لا تفحص شيئاً من هذا النوع وأنت تظن العكس. أما Security Code Scan الذي تجده في مقالات كثيرة فلا يوجد له إصدار منذ سبتمبر 2022 كما في [صفحته على NuGet](https://www.nuget.org/packages/SecurityCodeScan.VS2019/?ref=arabroot.io)، لذلك الأنسب لمشروع جديد هو قواعد Roslyn المدمجة مع SonarQube Community Build، وكلاهما سوف تراه يعمل في التجربة.

## تجربة عملية Lab: التطبيق نفسه تحت أربع أدوات

سوف نستخدم [التطبيق التجريبي في دليل 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/#lab-app)، وهو متجر صغير ب Flask فيه ثغرات مقصودة مرقمة من VULN-01 إلى VULN-07، ونأخذ منه نسخة إلى مجلد التجربة ونضيف إليها ما نحتاجه هنا، وكل ثغرة نضيفها نرقمها من VULN-10\. ولا تقم بتشغيل هذا التطبيق على سيرفر متصل بالإنترنت، والسبب أنه مكتوب ليخترق. وكل ال Containers في التجربة تبدأ أسماؤها ب `lab-dso-sast-`، والمنفذ الوحيد المفتوح هو SonarQube على `127.0.0.1:19890`.

### تجهيز نسخة التطبيق ومستودع Git Repository

نضيف إلى النسخة مساراً جديداً `/products` يرتب المنتجات بعمود يختاره المستخدم، وال SQL لا يسمح بتمرير اسم عمود كمعامل `?`، لذلك نختار اسم العمود من قائمة سماح Allowlist ثابتة، وهذا كود آمن، لكننا نتوقع أن تعترض عليه الأدوات:

```python
@app.route("/products")
def products():
    # Sort column: SQL cannot take a column name as a ? parameter, so it is chosen from a fixed allowlist.
    sort = request.args.get("sort", "name")
    column = {"name": "name", "price": "price"}.get(sort, "name")
    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()
    return {"products": [dict(r) for r in rows]}
```

ثم نجعل المجلد مستودع Git، لأن Semgrep يقرأ الملفات التي يتتبعها Git، ولأننا سوف نحتاج تاريخ ال Commits في الخط الأساسي بعد قليل:

```bash
cp -r content/labs/devsecops-app lab-sast/app
cd lab-sast/app
git init -q -b main
git add . && git commit -qm "demo app as shipped"
# after adding /products:
git commit -qam "lab: /products sorted by an allowlisted column"
```

### الفحص الأول First Scan مع Semgrep

نبدأ بالأمر الذي يكتبه أي شخص يجرب Semgrep لأول مرة، أي اختيار القواعد آلياً حسب لغات المشروع:

```bash
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.178.0 semgrep scan --config auto
```

والمخرج طويل، حوالي مئتي سطر، وهذا أوله وآخره كما ظهر:

```text
┌─────────────┐
│ Scan Status │
└─────────────┘
  Scanning 3 files tracked by git with 1074 Code rules:

  Language      Rules   Files          Origin      Rules
 ─────────────────────────────        ───────────────────
  <multilang>      47       3          Community    1074
  python          243       1
  dockerfile        6       1

┌──────────────────┐
│ 18 Code Findings │
└──────────────────┘

    Dockerfile
   ❯❯❱ dockerfile.security.missing-user.missing-user
          ❰❰ Blocking ❱❱
          By not specifying a USER, a program in the container may run as 'root'. This is a security hazard.
          If an attacker can control a process running as root, they may have control over the container.
          Ensure that the last USER in a Dockerfile is a USER other than 'root'.
          Details: https://sg.run/Gbvn

           ▶▶┆ Autofix ▶ USER non-root CMD ["python", "app.py"]
           12┆ CMD ["python", "app.py"]

    app.py
   ❯❯❱ python.flask.security.audit.hardcoded-config.avoid_hardcoded_config_SECRET_KEY
          ❰❰ Blocking ❱❱
          Hardcoded variable `SECRET_KEY` detected. Use environment variables or config files instead
          Details: https://sg.run/Ekde

           16┆ app.config["SECRET_KEY"] = "flask-dev-secret-2024"
...
    ❯❱ python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host
          ❰❰ Blocking ❱❱
          Running flask app with host 0.0.0.0 could expose the server publicly.
          Details: https://sg.run/eLby

          133┆ app.run(host="0.0.0.0", port=8000)

┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
 • Findings: 18 (18 blocking)
 • Rules run: 296
 • Targets scanned: 3
 • Parsed lines: ~100.0%
 • Scan was limited to files tracked by git
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 296 rules on 3 files: 18 findings.
```

ولأن هذا الشكل يصعب قراءته في تقرير، نشغل المجموعة `p/default` مع إيقاف الإحصاءات، وبصيغة `--emacs` التي تضع كل نتيجة في سطر واحد، وقد أعطت النتائج الثماني عشرة نفسها في هذا المشروع:

```bash
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.178.0 \
  semgrep scan --config p/default --metrics=off --emacs --quiet
```

والمخرج سوف يكون كما يلي، بعد قطع الأسطر الطويلة عند نهاية الكود لأن الرسالة نفسها ظهرت كاملة أعلاه:

```text
Dockerfile:12:1:error(missing-user):CMD ["python", "app.py"]:By not specifying a USER, a p
app.py:16:1:error(avoid_hardcoded_config_SECRET_KEY):app.config["SECRET_KEY"] = "flask-dev-secret-2024"
app.py:86:5:warning(sql-injection-db-cursor-execute):    q = request.args.get("q", "")
app.py:90:12:warning(formatted-sql-query):    rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
app.py:90:12:error(sqlalchemy-execute-raw-query):    rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
app.py:90:25:error(tainted-sql-string):    rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
app.py:90:25:error(tainted-sql-string):    rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
app.py:91:21:warning(raw-html-format):    items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows)
app.py:91:21:warning(raw-html-format):    items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows)
app.py:94:5:warning(directly-returned-format-string):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>")
app.py:94:12:warning(raw-html-format):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>")
app.py:94:12:warning(raw-html-format):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>")
app.py:101:12:error(open-redirect):    return redirect(request.args.get("next", "/"))
app.py:109:12:warning(formatted-sql-query):    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()
app.py:109:12:error(sqlalchemy-execute-raw-query):    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()
app.py:109:25:error(tainted-sql-string):    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()
app.py:109:25:error(tainted-sql-string):    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()
app.py:133:5:warning(avoid_app_run_with_bad_host):    app.run(host="0.0.0.0", port=8000)
```

### من النتائج Findings إلى الثغرات Vulnerabilities، ماذا وجدت الأداة وماذا فاتها؟

ثماني عشرة نتيجة لا تعني ثماني عشرة ثغرة، فأول عمل بعد أي فحص هو أن تجمع النتائج حسب المشكلة الحقيقية. والجدول التالي يربط النتائج بثغرات التطبيق:

| الثغرة                                            | السطر         | عدد النتائج | الحكم                                                              |
| ------------------------------------------------- | ------------- | ----------- | ------------------------------------------------------------------ |
| VULN-01 مفتاح الجلسة SECRET\_KEY في الكود         | 16            | 1           | صحيحة True Positive                                                |
| VULN-01 التوكن ADMIN\_API\_TOKEN في الكود         | 17            | 0           | مفقودة False Negative                                              |
| VULN-02 حقن SQL في البحث                          | 86 و90        | 5           | صحيحة، لكن من خمس قواعد                                            |
| VULN-03 XSS في صفحة النتائج                       | 91 و94        | 5           | صحيحة، والسطر 91 فيه بيانات من قاعدة البيانات وليس من الطلب مباشرة |
| VULN-05 إعادة التوجيه المفتوحة Open Redirect      | 101           | 1           | صحيحة                                                              |
| VULN-07 ال Container يعمل ك root                  | Dockerfile 12 | 1           | صحيحة                                                              |
| الترتيب بعمود من قائمة سماح في /products          | 109           | 4           | خاطئة False Positive                                               |
| الاستماع على 0.0.0.0 داخل ال Container            | 133           | 1           | خاطئة في سياقنا                                                    |
| VULN-04 ترويسات الأمان، وVULN-06 المكتبات القديمة |               | 0           | خارج نظر SAST كما ذكرنا                                            |

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

- ثماني نتائج من الثماني عشرة جاءت من قواعد Django وSQLAlchemy على تطبيق Flask لا يستخدم أياً منهما، لأن مجموعة القواعد العامة تضم قواعد لأطر عمل كثيرة تتشابه أنماطها، وهذا مثال حقيقي على الضجيج Noise، فالثغرة الواحدة ظهرت خمس مرات.
- التوكن `ADMIN_API_TOKEN` لم يظهر، لأن القاعدة الموجودة تعرف المفتاح `SECRET_KEY` في إعداد Flask بالتحديد، ولا تعرف أن هذا النص الطويل سر، وهذا عمل أداة أخرى تفحص النصوص نفسها، وقد شرحناها في [اكتشاف الأسرار المسربة مع Gitleaks وTruffleHog](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/).
- الإنذاران الخاطئان سوف نتعامل معهما الآن بالطريقة الصحيحة.

### الإنذار الخاطئ False Positive وتجاهله بالطريقة الصحيحة

السطر 109 آمن لأن `column` لا يمكن أن يكون إلا `name` أو `price`، لكن قواعد التلوث رأت أن قيمة من `request.args` دخلت إلى `dict.get` ثم خرجت إلى الاستعلام، وهي لا تعرف أن نتيجة البحث في القاموس قيمة من القاموس نفسه وليست قيمة المستخدم. والطريقة الخاطئة هنا هي حذف القاعدة `tainted-sql-string` من الإعداد، والسبب أن القاعدة نفسها هي التي وجدت VULN-02 الحقيقية قبل سطرين، والطريقة الخاطئة الثانية هي استثناء الملف كله. والطريقة الصحيحة هي أن نتجاهل هذه النتيجة في هذا السطر فقط، وبأسماء القواعد بالتحديد، ومع السبب. وتشرح [صفحة التجاهل في Semgrep](https://docs.semgrep.dev/ignoring-files-folders-code?ref=arabroot.io) أن التعليق `# nosemgrep: rule-id` يوضع على السطر نفسه أو على السطر الذي قبله، وأن اسم القاعدة يكتب كاملاً بمساره، ولا توجد في الصيغة خانة رسمية للسبب، لذلك نكتب السبب في تعليق مستقل قبله:

```python
    # Reviewed 2026-10: column is one of two fixed strings from the allowlist above, never the raw request value.
    # nosemgrep: python.flask.security.injection.tainted-sql-string.tainted-sql-string, python.django.security.injection.tainted-sql-string.tainted-sql-string, python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
    rows = db().execute(f"SELECT name, price FROM products ORDER BY {column}").fetchall()  # nosec B608
```

وبالطريقة نفسها نتجاهل نتيجة `0.0.0.0`، فالتطبيق داخل Container يجب أن يستمع على كل الواجهات حتى يصل إليه Docker، والحماية الحقيقية هي نشر المنفذ على `127.0.0.1` فقط في السيرفر:

```python
    # Inside the Container the app must listen on all interfaces; the host publishes it on 127.0.0.1 only.
    # nosemgrep: python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host
    app.run(host="0.0.0.0", port=8000)  # nosec B104
```

ثم نعيد الفحص بالأمر نفسه، وهذا ملخصه:

```text
✅ Scan completed successfully.
 • Findings: 13 (13 blocking)
 • Rules run: 296
 • Targets scanned: 3
 • Parsed lines: ~100.0%
 • No ignore information available
Ran 296 rules on 3 files: 13 findings.
```

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

- اختفت النتائج الخمس، أربع على السطر 109 وواحدة على السطر 133، وبقيت النتائج الحقيقية كلها، لأننا ذكرنا القواعد بأسمائها ولم نكتب `# nosemgrep` وحدها، فالتعليق دون أسماء يخفي أي قاعدة جديدة تظهر على هذا السطر في المستقبل.
- أضفنا `# nosec B608` و`# nosec B104` ل Bandit، لأن لكل أداة صيغة تجاهل خاصة بها، فالتعليق الذي يفهمه Semgrep لا يفهمه Bandit، وSonarQube يستخدم حالة «مقبول Accepted» أو «إنذار خاطئ False Positive» في الواجهة.
- السطر الأول بتاريخ وسبب، حتى يعرف من يقرأ الكود بعد سنة لماذا تم التجاهل، ومتى يجب أن يراجعه، فإذا تغير الكود وأصبح `column` يأتي من مكان آخر فالتعليق يقول بوضوح إن الافتراض انكسر.
- التجاهل يمر بمراجعة الكود في طلب الدمج مثل أي سطر آخر، وهذا أهم من الصيغة نفسها، فمطور يتجاهل نتيجة دون أن يراه أحد هو ثغرة في العملية وليس في الكود.

📌

كتبنا أيضاً السبب في السطر نفسه بعد أسماء القواعد في ملف اختبار منفصل، مثل `# nosemgrep: rule-id (allowlisted column)`، ونجح التجاهل في Semgrep 1.178.0، لكن الصيغة غير موثقة، لذلك فضلنا التعليق المستقل للسبب حتى لا يتغير سلوكها في إصدار قادم.

### كتابة قاعدة مخصصة Custom Rule لنمط خاص بتطبيقنا

تطبيقنا يبني صفحاته بقالب HTML خام اسمه `PAGE` مع `PAGE.format(...)`، وهذا نمط خاص بنا لا تعرفه القواعد العامة، فهي وجدت XSS بقواعد عامة عن «نص HTML منسق»، وبخمس نتائج على سطرين. والقاعدة المخصصة تقول ما نعرفه نحن عن الكود بالضبط: المصدر مدخلات الطلب، والمكان الخطر `PAGE.format`، والمنظف `escape`. وننشئ ملف القاعدة `rules/page-format-xss.yaml` بوضع التلوث `mode: taint` الذي تشرحه [صفحة Taint Mode](https://docs.semgrep.dev/writing-rules/data-flow/taint-mode?ref=arabroot.io):

```yaml
rules:
  - id: page-format-xss
    mode: taint
    languages: [python]
    severity: ERROR
    message: >-
      Request data reaches PAGE.format() without escaping. PAGE is our raw HTML
      template, so this is reflected XSS. Escape the value with markupsafe.escape()
      first, or render a Jinja2 template.
    metadata:
      cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation"
      owner: shop-team
    pattern-sources:
      - pattern: flask.request.args.get(...)
      - pattern: flask.request.form.get(...)
      - pattern: flask.request.args[...]
      - pattern: flask.request.form[...]
    pattern-sanitizers:
      - pattern: markupsafe.escape(...)
      - pattern: html.escape(...)
    pattern-sinks:
      - pattern: PAGE.format(...)
```

والقاعدة كود، والكود يحتاج اختباراً، لذلك نكتب بجانبها ملفاً بالاسم نفسه `rules/page-format-xss.py` فيه حالة يجب أن تجدها، وحالتان يجب ألا تجدهما، بالتعليقين `# ruleid:` و`# ok:` كما في [صفحة اختبار القواعد](https://docs.semgrep.dev/writing-rules/testing-rules?ref=arabroot.io):

```python
from flask import request
from markupsafe import escape

PAGE = "<html><body>{}</body></html>"

def search():
    q = request.args.get("q", "")
    # ruleid: page-format-xss
    return PAGE.format(f"<h1>Results for {q}</h1>")

def search_escaped():
    q = request.args.get("q", "")
    # ok: page-format-xss
    return PAGE.format(f"<h1>Results for {escape(q)}</h1>")

def welcome():
    name = "guest"
    # ok: page-format-xss
    return PAGE.format(f"<p>Hello {name}</p>")
```

الآن نشغل الاختبار من مجلد التجربة الذي يحتوي المجلدين `app` و`rules`:

```bash
docker run --rm -v "$PWD:/src" -w /src semgrep/semgrep:1.178.0 \
  semgrep scan --test --metrics=off rules/
```

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

```text
┌─────────────┐
│ Scan Status │
└─────────────┘
  Scanning 1 file with 1 Code rule:
  Scanning 1 file.
1/1: ✓ All tests passed
No tests for fixes found.
```

ثم نشغل القاعدة وحدها على التطبيق:

```bash
docker run --rm -v "$PWD:/src" -w /src semgrep/semgrep:1.178.0 \
  semgrep scan --config rules/page-format-xss.yaml --metrics=off app/
```

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

```text
│ 1 Code Finding │
└────────────────┘

    app/app.py
   ❯❯❱ rules.page-format-xss
          ❰❰ Blocking ❱❱
          Request data reaches PAGE.format() without escaping. PAGE is our raw HTML template, so this is
          reflected XSS. Escape the value with markupsafe.escape() first, or render a Jinja2 template.

           94┆ return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>")

┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
 • Findings: 1 (1 blocking)
 • Rules run: 1
 • Targets scanned: 1
 • Parsed lines: ~100.0%
 • Scan was limited to files tracked by git
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 1 rule on 1 file: 1 finding.
```

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

- نتيجة واحدة على السطر الصحيح، برسالة تذكر قالبنا باسمه وطريقة الإصلاح المناسبة لتطبيقنا، بدل خمس نتائج عامة تقترح `django.shortcuts.render` على تطبيق Flask.
- الصفحة الرئيسية `index()` تمرر `session.get("user")` إلى `PAGE.format` ولم تظهر، لأن الجلسة ليست مصدراً في قاعدتنا، وهذا قرار يجب أن تتخذه بوعي، فإذا كان اسم المستخدم يمكن أن يحتوي HTML عند التسجيل فأضف الجلسة إلى المصادر.
- اسم القاعدة في المخرج `rules.page-format-xss`، أي أن Semgrep يضيف اسم المجلد قبل اسم القاعدة، وهذا هو الاسم الذي تكتبه في `# nosemgrep:` إذا احتجت إليه.
- ملف الاختبار يجعل القاعدة آمنة للتعديل، فإذا أضاف أحد مصدراً جديداً أو منظفاً جديداً فأمر `--test` في خط CI يخبره فوراً إن كسر حالة قديمة.

### المقارنة Comparison مع Bandit

Bandit أداة Python بقواعد أنماط مكتوبة ككود Python، ونشغله على النسخة نفسها قبل التجاهل:

```bash
docker run --rm -v "$PWD:/src" -w /src python:3.13-slim sh -c '
  pip install -q --root-user-action=ignore bandit==1.9.4
  bandit -q -r app/ -f custom --msg-template "{relpath}:{line} {test_id} {severity}/{confidence} {msg}"'
```

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

```text
app/app.py:16 B105 LOW/MEDIUM Possible hardcoded password: 'flask-dev-secret-2024'
app/app.py:17 B105 LOW/MEDIUM Possible hardcoded password: 'dso_live_7Hq2Lx9Vt4Rz8Wn3Bk6Mh1Sd5Gj0CyPa'
app/app.py:19 B108 MEDIUM/MEDIUM Probable insecure usage of temp file/directory.
app/app.py:90 B608 MEDIUM/MEDIUM Possible SQL injection vector through string-based query construction.
app/app.py:109 B608 MEDIUM/MEDIUM Possible SQL injection vector through string-based query construction.
app/app.py:133 B104 MEDIUM/MEDIUM Possible binding to all interfaces.
```

والجدول التالي يضع الأداتين جنباً إلى جنب على الثغرات نفسها:

| الثغرة                        | Semgrep p/default      | Bandit 1.9.4                               |
| ----------------------------- | ---------------------- | ------------------------------------------ |
| VULN-01 SECRET\_KEY           | وجدها                  | وجدها B105                                 |
| VULN-01 ADMIN\_API\_TOKEN     | فاتته                  | وجدها B105 لأن اسم المتغير فيه TOKEN       |
| VULN-02 حقن SQL               | وجدها بالتلوث والأنماط | وجدها B608 بالنمط                          |
| VULN-03 XSS                   | وجدها                  | فاتته                                      |
| VULN-05 Open Redirect         | وجدها                  | فاتته                                      |
| VULN-07 root في Dockerfile    | وجدها                  | لا يقرأ Dockerfile                         |
| العمود من قائمة السماح        | إنذار خاطئ من 4 قواعد  | إنذار خاطئ B608                            |
| المسار /tmp/shop.db الافتراضي | لا شيء                 | B108، وهي ملاحظة صحيحة لأن /tmp مكان مشترك |

والخلاصة من المقارنة أن الأداتين تكمل إحداهما الأخرى، فـ Bandit يعرف Python بقواعد قليلة ودقيقة على الأسرار والدوال الخطرة، ولا يعرف شيئاً عن Flask، وSemgrep يعرف أطر العمل ويتتبع البيانات، لكنه فوت التوكن. وتشغيل الاثنين في خط CI يأخذ ثواني معدودة، لذلك لا يوجد سبب للاختيار بينهما في مشروع Python.

### إصلاح الثغرة Fix ورؤية النتيجة Finding تختفي

نصلح VULN-02 بالطريقة الموجودة في التطبيق نفسه في دالة `login()`، أي معامل `?` بدل دمج النص:

```diff
-    rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
+    rows = db().execute("SELECT name, price FROM products WHERE name LIKE ?", (f"%{q}%",)).fetchall()
```

ثم نعيد الفحص بصيغة السطر الواحد:

```text
Dockerfile:12:1:error(missing-user):CMD ["python", "app.py"]:By not specifying a USER, a program in the contai
app.py:16:1:error(avoid_hardcoded_config_SECRET_KEY):app.config["SECRET_KEY"] = "flask-dev-secret-2024":Hardco
app.py:91:21:warning(raw-html-format):    items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows):Detected u
app.py:91:21:warning(raw-html-format):    items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows):Detected u
app.py:94:5:warning(directly-returned-format-string):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{ite
app.py:94:12:warning(raw-html-format):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>"):Dete
app.py:94:12:warning(raw-html-format):    return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>"):Dete
app.py:101:12:error(open-redirect):    return redirect(request.args.get("next", "/")):Data from request is pas
```

اختفت النتائج الخمس على السطرين 86 و90، وبقيت ثماني نتائج كلها لثغرات ما زالت موجودة. لكن لاحظ السطر 91، فهو ما زال يظهر بعد الإصلاح، والسبب أن قواعد التلوث تعتبر نتيجة الاستعلام `rows` ملوثة لأن `q` دخل في الاستعلام، حتى بعد أن أصبح معاملاً آمناً، وأسماء المنتجات في الحقيقة قادمة من قاعدة البيانات. وهل هذا إنذار خاطئ؟ ليس تماماً، فلو كان المستخدم يستطيع إضافة منتج باسم فيه `<script>` لكانت هذه ثغرة XSS مخزنة Stored XSS، والإصلاح الصحيح للسطرين 91 و94 واحد، وهو استخدام قالب Jinja2 الذي يهرب كل القيم آلياً.

### الخط الأساسي Baseline: نتائج طلب الدمج فقط

الآن نحاكي ما يحدث في طلب دمج، فنضيف مساراً جديداً لعمل نسخة احتياطية، وفيه ثغرة حقن أوامر Command Injection نرقمها VULN-10:

```python
@app.route("/api/admin/backup")
def backup():
    # VULN-10: command injection. The file name from the URL goes into a shell command,
    # so name=x;id runs a second command on the server. Fix: no shell, a fixed argument list, a validated name.
    name = request.args.get("name", "shop")
    subprocess.run(f"sqlite3 {DB_PATH} .dump > /tmp/{name}.sql", shell=True, check=False)
    return {"backup": name}
```

ونحفظ التغيير في Commit، ثم نطلب من Semgrep أن يقارن مع ال Commit السابق ويعرض ما أضافه التغيير فقط:

```bash
git commit -qam "add admin backup endpoint"
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.178.0 \
  semgrep scan --config p/default --metrics=off --baseline-commit HEAD~1
```

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

```text
┌─────────────┐
│ Scan Status │
└─────────────┘
  Scanning 1 file tracked by git with 1074 Code rules:

  Language      Rules   Files          Origin      Rules
 ─────────────────────────────        ───────────────────
  python          243       1          Community    1074
  <multilang>      47       1

  Current version has 15 findings.

Creating git worktree from 'HEAD~1' to scan baseline.
  Will report findings introduced by these commits (may be incomplete for shallow checkouts):
    * 1daf81d add admin backup endpoint

┌─────────────┐
│ Scan Status │
└─────────────┘
  Scanning 1 file tracked by git with 13 Code rules:
  Scanning 1 file with 13 python rules.

┌─────────────────┐
│ 3 Code Findings │
└─────────────────┘

    app.py
   ❯❯❱ python.flask.security.injection.subprocess-injection.subprocess-injection
          ❰❰ Blocking ❱❱
          Detected user input entering a `subprocess` call unsafely. This could result in a command injection
          vulnerability. An attacker could use this vulnerability to execute arbitrary commands on the host,
          which allows them to download malware, scan sensitive data, or run any command they wish on the
          server. Do not let users choose the command to run. In general, prefer to use Python API versions of
          system commands. If you must use subprocess, use a dictionary to allowlist a set of commands.
          Details: https://sg.run/5gW3

          121┆ subprocess.run(f"sqlite3 {DB_PATH} .dump > /tmp/{name}.sql", shell=True, check=False)

   ❯❯❱ python.lang.security.dangerous-subprocess-use.dangerous-subprocess-use
          ❰❰ Blocking ❱❱
          Detected subprocess function 'run' with user controlled data. A malicious actor could leverage this
          to perform command injection. You may consider using 'shlex.escape()'.
          Details: https://sg.run/NWxp

          121┆ subprocess.run(f"sqlite3 {DB_PATH} .dump > /tmp/{name}.sql", shell=True, check=False)

   ❯❯❱ python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
          ❰❰ Blocking ❱❱
          Found 'subprocess' function 'run' with 'shell=True'. This is dangerous because this call will spawn
          the command using a shell process. Doing so propagates current shell settings and variables, which
          makes it much easier for a malicious actor to execute commands. Use 'shell=False' instead.
          Details: https://sg.run/J92w

           ▶▶┆ Autofix ▶ False
          121┆ subprocess.run(f"sqlite3 {DB_PATH} .dump > /tmp/{name}.sql", shell=True, check=False)

┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
 • Findings: 3 (3 blocking)
 • Rules run: 290
 • Targets scanned: 1
 • Parsed lines: ~100.0%
 • Scan was limited to files changed since baseline commit.
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 290 rules on 1 file: 3 findings.
```

في المخرج أعلاه لاحظ التالي:

- الملف `app.py` فيه الآن خمس عشرة نتيجة، منها الثماني القديمة والخمس التي تجاهلناها، لكن التقرير عرض ثلاثاً فقط، وكلها على السطر 121 الجديد، لأن Semgrep فحص ال Commit السابق في Worktree منفصل ثم حذف النتائج الموجودة فيه.
- هذا بالضبط ما تحتاجه على مشروع قديم فيه مئات النتائج، فطلب الدمج يفشل بسبب ما أضافه هو فقط، والنتائج القديمة لها خطة إصلاح منفصلة كما ذكرنا في [قسم الخط الأساسي في دليل 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/#baseline).
- الرسالة `may be incomplete for shallow checkouts` مهمة في خط CI، فأغلب أنظمة CI تجلب آخر Commit فقط افتراضياً، وعندها لا يجد Semgrep ال Commit الذي تقارن به، لذلك تحتاج جلب التاريخ كاملاً.

⚠️

في المحاولة الأولى شغلنا الأمر مع `--baseline-commit HEAD` والتغيير لم يحفظ بعد في Commit، فكان المخرج `Scanning 0 files tracked by git` و`Nothing to scan` مع صفر نتائج، أي أن التغيير غير المحفوظ لم يفحص أصلاً، وهذا خطير لأن المخرج أخضر. لذلك في خط CI قارن دائماً بين Commits حقيقية، أي رأس طلب الدمج مع نقطة التفرع من main.

### مخرج SARIF: صيغة واحدة لكل الأدوات

كل أداة تخرج نتائجها بشكل مختلف، ولو أردت جمع نتائج Semgrep وSpotBugs وRoslyn في مكان واحد فسوف تكتب محولاً لكل أداة، لذلك اتفقت الأدوات على صيغة اسمها Static Analysis Results Interchange Format واختصاراً SARIF، وهي [معيار من OASIS](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html?ref=arabroot.io) بصيغة JSON، والإصدار المستخدم اليوم 2.1.0\. ونطلب من Semgrep ملف SARIF:

```bash
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.178.0 \
  semgrep scan --config p/default --metrics=off --quiet --sarif --output /src/semgrep.sarif
```

وهذه نتيجة VULN-10 كما ظهرت في الملف، مع اختصار نص الرسالة الطويل، وبعدها تعريف القاعدة من قسم `tool.driver.rules`:

```json
{
  "fingerprints": {
    "matchBasedId/v1": "requires login"
  },
  "locations": [
    {
      "physicalLocation": {
        "artifactLocation": {
          "uri": "app.py",
          "uriBaseId": "%SRCROOT%"
        },
        "region": {
          "endColumn": 90,
          "endLine": 121,
          "snippet": {
            "text": "    subprocess.run(f\"sqlite3 {DB_PATH} .dump > /tmp/{name}.sql\", shell=True, check=False)"
          },
          "startColumn": 5,
          "startLine": 121
        }
      }
    }
  ],
  "message": {
    "text": "Detected user input entering a `subprocess` call unsafely. This could result in a command injection vulnerability. ..."
  },
  "properties": {},
  "ruleId": "python.flask.security.injection.subprocess-injection.subprocess-injection"
}
{
  "id": "python.flask.security.injection.subprocess-injection.subprocess-injection",
  "defaultConfiguration": {
    "level": "error"
  },
  "properties": {
    "precision": "very-high",
    "tags": [
      "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')",
      "HIGH CONFIDENCE",
      "OWASP-A01:2017 - Injection",
      "OWASP-A03:2021 - Injection",
      "OWASP-A05:2025 - Injection",
      "security"
    ]
  }
}
```

في المخرج أعلاه لاحظ التالي:

- كل نتيجة فيها `ruleId` والملف والسطر والعمود، وكل قاعدة فيها المستوى `level` ورقم CWE، وهذه الحقول هي ما تقرؤه منصات الكود لترسم التنبيه على السطر في طلب الدمج.
- الحقل `fingerprints` فيه `requires login`، أي أن البصمة الثابتة للنتيجة في Semgrep CE تحتاج تسجيل الدخول إلى منصتهم، وتذكر [صفحة دعم SARIF في GitHub](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning?ref=arabroot.io) أنها تستخدم الحقل `partialFingerprints` لتعرف أن نتيجتين في تحليلين مختلفين هما المشكلة نفسها، وأنها تحاول حسابه من الملفات إذا كان ناقصاً، وإلا قد يرى المستخدم التنبيه نفسه مكرراً.
- الملف فيه أيضاً النتائج الخمس التي تجاهلناها، لكن كل واحدة منها تحمل `"suppressions": [{"kind": "inSource"}]`، أي أنها متجاهلة بتعليق في الكود، فالأداة التي تقرأ الملف تعرف أنها موجودة وأن أحداً قرر تجاهلها، وهذا أفضل من أن تختفي تماماً.

### SonarQube Community Build وبوابة الجودة Quality Gate

الآن نشغل SonarQube في Container على المنفذ `127.0.0.1:19890` فقط، ونغير كلمة مرور المدير الافتراضية وننشئ توكن للتحليل عبر ال API:

```bash
docker run -d --name lab-dso-sast-sonarqube -p 127.0.0.1:19890:9000 \
  -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true sonarqube:community

curl -s http://127.0.0.1:19890/api/system/status
# wait until "status":"UP"

curl -s -u admin:admin -X POST http://127.0.0.1:19890/api/users/change_password \
  -d "login=admin&previousPassword=admin&password=$SONAR_ADMIN_PASSWORD"
curl -s -u "admin:$SONAR_ADMIN_PASSWORD" -X POST http://127.0.0.1:19890/api/user_tokens/generate \
  -d "name=lab&type=GLOBAL_ANALYSIS_TOKEN"
```

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

```text
{"id":"147B411E-AaESFGsAnTQjhbMx5lLX","version":"26.9.0.129388","status":"UP"}
```

ثم نشغل الماسح SonarScanner في Container على الشبكة نفسها، والمتغير `sonar.qualitygate.wait=true` يجعل الماسح ينتظر حكم بوابة الجودة ويعيد رمز خروج غير صفري إذا فشلت، وهذا ما يحتاجه خط CI:

```bash
docker network create lab-dso-sast-net
docker network connect --alias sonarqube lab-dso-sast-net lab-dso-sast-sonarqube

docker run --rm --name lab-dso-sast-scanner --network lab-dso-sast-net -v "$PWD:/usr/src" \
  -e SONAR_HOST_URL=http://sonarqube:9000 -e SONAR_TOKEN="$SONAR_TOKEN" \
  sonarsource/sonar-scanner-cli:latest \
  -Dsonar.projectKey=devsecops-shop -Dsonar.projectName="DevSecOps demo shop" \
  -Dsonar.projectVersion=1.0 -Dsonar.sources=. -Dsonar.exclusions=semgrep.sarif \
  -Dsonar.python.version=3.13 -Dsonar.qualitygate.wait=true
```

والتحليل الأول بالإصدار 1.0 انتهى هكذا:

```text
16:42:48.591 INFO  QUALITY GATE STATUS: PASSED - View details on http://sonarqube:9000/dashboard?id=devsecops-shop
```

نجحت البوابة والكود فيه ثغرات، وقد يتساءل البعض: كيف؟ والإجابة أن البوابة تحكم على الكود الجديد فقط، وفي التحليل الأول لا يوجد كود جديد لأنه لا يوجد شيء نقارن به، فكل الكود يعتبر قديماً. ولنر البوابة تعمل، نضيف مساراً لإعادة تعيين كلمة المرور فيه ثغرة نرقمها VULN-13، فالرمز الذي يرسل للمستخدم ينتج من `random` وهو مولد يمكن توقع قيمه:

```python
@app.route("/reset", methods=["POST"])
def reset_password():
    # VULN-13: a password reset code from random, which is predictable. Fix: secrets.token_urlsafe().
    code = str(random.randint(100000, 999999))
    session["reset_code"] = code
    return {"sent": True}
```

ثم نحفظ التغيير ونحلل بالإصدار 1.1، فيصبح الكود الجديد هو ما تغير منذ الإصدار 1.0:

```text
16:43:00.370 INFO  SonarScanner CLI 8.1.0.6389
16:43:02.190 INFO  Communicating with SonarQube Community Build 26.9.0.129388
...
16:43:23.741 ERROR QUALITY GATE STATUS: FAILED - View details on http://sonarqube:9000/dashboard?id=devsecops-shop
16:43:23.763 INFO  EXECUTION FAILURE
16:43:23.764 INFO  Total time: 23.425s
```

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

![لوحة مشروع DevSecOps demo shop في SonarQube Community Build بالإصدار 1.1، وبوابة الجودة Failed، وفي تبويب New Code منذ 1.0 شرط واحد فشل هو New issues بقيمة 1 والمطلوب 0، وملاحظة أن بعض الشروط تم تجاهلها لقلة الأسطر الجديدة، والتغطية 0.0% على 5 أسطر، والتكرار 0.0%، وSecurity Hotspots عليها كلمة Deprecated](https://arabroot.io/content/images/2026/10/sast-07-sonarqube-gate.webp)

بوابة Sonar way فشلت بسبب مشكلة واحدة جديدة في الكود الذي تغير منذ الإصدار 1.0

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

- الشرط الذي فشل هو «مشاكل جديدة New issues» بقيمة 1 والمطلوب 0، والمشكلة هي `python:S2245` «Make sure that using this pseudorandom number generator is safe here» على سطر `random.randint`.
- التغطية 0% ومع ذلك لم يفشل شرطها، والسبب الملاحظة على اليسار: بعض الشروط تم تجاهلها لأن عدد الأسطر الجديدة قليل، وهذا سلوك مقصود في SonarQube حتى لا يفشل تعديل من خمسة أسطر بسبب التغطية.
- بطاقة Security Hotspots وصفحتها في القائمة عليهما كلمة Deprecated، كما ذكرنا في قسم SonarQube.

والصورة التالية تبين صفحة المشاكل بعد اختيار جودة الأمان Security فقط:

![صفحة Issues في SonarQube مع فلتر Security وفيها 6 مشاكل: Avoid binding the application to all network interfaces بخطورة Blocker، وMake sure disabling CSRF protection is safe here بخطورة High، وMake sure publicly writable directories are used safely here بخطورة High مع وسم former-hotspot، وSECRET_KEY detected here بخطورة Medium، وبداية مشكلة pseudorandom number generator، وعلى اليسار عدد المشاكل حسب الخطورة: Blocker 1 وHigh 2 وMedium 2 وLow 1](https://arabroot.io/content/images/2026/10/sast-08-sonarqube-issues.webp)

ست مشاكل أمان في Community Build، وبعضها يحمل الوسم former-hotspot

وعند مقارنة هذه القائمة بثغرات التطبيق تظهر حدود النسخة المجانية بوضوح، فقد وجد SonarQube مفتاح الجلسة و`random` و`/tmp` و`0.0.0.0` والمستخدم root في Dockerfile، ووجد أيضاً غياب الحماية من CSRF في Flask، وهي ملاحظة لم تجدها الأدوات الأخرى، لكنه لم يجد XSS ولا Open Redirect ولا حقن الأوامر في VULN-10، لأن هذه كلها تحتاج تحليل التلوث غير الموجود في Community Build. ولاحظ أيضاً أن `0.0.0.0` ظهر رغم تعليق `nosemgrep` و`nosec`، لأن SonarQube لا يفهم صيغ الأدوات الأخرى. لذلك استخدم SonarQube Community Build للوحة والتاريخ وبوابة الجودة وقواعد الجودة، ولا تعتمد عليه وحده في ثغرات الحقن.

### Java: SpotBugs مع FindSecBugs

لمطوري Java نكتب كلاساً صغيراً فيه ثغرة حقن SQL عبر JDBC نرقمها VULN-11، وبجانبها الطريقة الصحيحة:

```java
package io.arabroot.shop;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ProductSearch {

    // VULN-11: SQL injection. The search text is concatenated into the SQL string.
    public ResultSet search(Connection conn, String q) throws SQLException {
        Statement st = conn.createStatement();
        return st.executeQuery("SELECT name, price FROM products WHERE name LIKE '%" + q + "%'");
    }

    // The fix: a PreparedStatement with a ? parameter.
    public ResultSet searchSafe(Connection conn, String q) throws SQLException {
        PreparedStatement ps = conn.prepareStatement("SELECT name, price FROM products WHERE name LIKE ?");
        ps.setString(1, "%" + q + "%");
        return ps.executeQuery();
    }
}
```

وفي ملف `pom.xml` نضيف إضافة SpotBugs ل Maven، وFindSecBugs كإضافة داخلها، ونطلب مخرج SARIF أيضاً:

```xml
<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.10.4.1</version>
  <configuration>
    <effort>Max</effort>
    <threshold>Low</threshold>
    <sarifOutput>true</sarifOutput>
    <plugins>
      <plugin>
        <groupId>com.h3xstream.findsecbugs</groupId>
        <artifactId>findsecbugs-plugin</artifactId>
        <version>1.14.0</version>
      </plugin>
    </plugins>
  </configuration>
</plugin>
```

ثم نبني المشروع ونشغل الفحص في Container ل Maven مع Java 21:

```bash
docker volume create lab-dso-sast-m2
docker run --rm --name lab-dso-sast-maven -v "$PWD:/src" -v lab-dso-sast-m2:/root/.m2 -w /src \
  maven:3-eclipse-temurin-21 mvn -q -B compile com.github.spotbugs:spotbugs-maven-plugin:4.10.4.1:check
```

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

```text
[ERROR] Medium: io.arabroot.shop.ProductSearch.search(Connection, String) may fail to clean up java.sql.Statement on checked exception [io.arabroot.shop.ProductSearch, io.arabroot.shop.ProductSearch] Obligation to clean up resource created at ProductSearch.java:[line 13] is not dischargedPath continues at ProductSearch.java:[line 14] OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE
[ERROR] Medium: io.arabroot.shop.ProductSearch.searchSafe(Connection, String) may fail to clean up java.sql.Statement on checked exception [io.arabroot.shop.ProductSearch, io.arabroot.shop.ProductSearch] Obligation to clean up resource created at ProductSearch.java:[line 19] is not dischargedPath continues at ProductSearch.java:[line 20] OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE
[ERROR] Medium: This use of java/sql/Statement.executeQuery(Ljava/lang/String;)Ljava/sql/ResultSet; can be vulnerable to SQL injection (with JDBC) [io.arabroot.shop.ProductSearch] At ProductSearch.java:[line 14] SQL_INJECTION_JDBC
[ERROR] Low: io.arabroot.shop.ProductSearch.search(Connection, String) passes a nonconstant String to an execute or addBatch method on an SQL statement [io.arabroot.shop.ProductSearch] At ProductSearch.java:[line 14] SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE
[ERROR] Failed to execute goal com.github.spotbugs:spotbugs-maven-plugin:4.10.4.1:check (default-cli) on project shop: failed with 4 bugs and 0 errors -> [Help 1]
```

في المخرج أعلاه لاحظ التالي:

- `SQL_INJECTION_JDBC` من FindSecBugs على السطر 14 بالضبط، و`SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE` من قواعد SpotBugs الأصلية على السطر نفسه، أي أن الثغرة ظهرت من الأداتين.
- الدالة `searchSafe` لم تظهر في نتائج الحقن، لأن `PreparedStatement` مع `?` هو الطريقة الصحيحة، والأداة تعرف ذلك لأنها تقرأ الأنواع الحقيقية من ال bytecode.
- النتيجتان `OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE` ليستا ثغرتي أمان، وإنما تحذير من أن ال Statement قد لا يغلق إذا حدث استثناء، والحل `try-with-resources`، وهذا مثال على أن SpotBugs أداة جودة في الأصل، والأمان فيه يأتي من FindSecBugs.
- الهدف `check` يفشل البناء عند وجود نتائج، لذلك هو المناسب لخط CI، بينما الهدف `spotbugs` يكتب التقرير فقط، والملف `target/spotbugsSarif.json` فيه النتائج نفسها بصيغة SARIF 2.1.0.

### C#: محللات Roslyn Analyzers وقاعدة CA3001

ولمطوري C# نكتب Controller صغيراً في ASP.NET Core على .NET 10، فيه ثغرة حقن SQL نرقمها VULN-12، وبجانبها الطريقة الصحيحة بمعامل:

```csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;

namespace Shop;

[ApiController]
[Route("products")]
public class ProductsController : ControllerBase
{
    // VULN-12: SQL injection. The query string value is concatenated into CommandText.
    [HttpGet("search")]
    public IActionResult Search([FromQuery] string q)
    {
        using var conn = new SqliteConnection("Data Source=shop.db");
        conn.Open();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT name FROM products WHERE name LIKE '%" + q + "%'";
        using var reader = cmd.ExecuteReader();
        var names = new List<string>();
        while (reader.Read()) names.Add(reader.GetString(0));
        return Ok(names);
    }

    // The fix: a parameter instead of concatenation.
    [HttpGet("search-safe")]
    public IActionResult SearchSafe([FromQuery] string q)
    {
        using var conn = new SqliteConnection("Data Source=shop.db");
        conn.Open();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT name FROM products WHERE name LIKE $q";
        cmd.Parameters.AddWithValue("$q", "%" + q + "%");
        using var reader = cmd.ExecuteReader();
        var names = new List<string>();
        while (reader.Read()) names.Add(reader.GetString(0));
        return Ok(names);
    }
}
```

ثم نبني المشروع بالأمر `dotnet build` دون أي إعداد إضافي، ولا تظهر أي نتيجة أمان، لأن قواعد التلوث معطلة افتراضياً كما ذكرنا. والآن نقوم بتفعيلها في ملف `.editorconfig`، ونطلب في ملف المشروع مخرج SARIF عبر الخاصية `ErrorLog`:

```ini
root = true

[*.cs]
# The taint rules for injection (CA3001 to CA3012) are off by default; turn on the ones you need.
dotnet_diagnostic.CA3001.severity = warning
dotnet_diagnostic.CA2100.severity = warning
```

```xml
<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
  <AnalysisMode>Recommended</AnalysisMode>
  <ErrorLog>bin/roslyn.sarif,version=2.1</ErrorLog>
</PropertyGroup>
```

والمخرج بعد `dotnet build` على .NET SDK 10.0.400 سوف يكون كما يلي، بعد حذف المسار الكامل للمجلد من أول كل سطر:

```text
ProductsController.cs(17,9): warning CA2100: Review if the query string passed to 'string SqliteCommand.CommandText' in 'Search', accepts any user input (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100)
ProductsController.cs(17,9): warning CA3001: Potential SQL injection vulnerability was found where 'string SqliteCommand.CommandText' in method 'IActionResult ProductsController.Search(string q)' may be tainted by user-controlled data from 'string q' in method 'IActionResult ProductsController.Search(string q)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3001)
```

في المخرج أعلاه لاحظ التالي:

- القاعدة CA3001 تتبعت القيمة من المعامل `string q` في Action عليها `[FromQuery]`، أي أنها تعرف أن معاملات ال Controller في ASP.NET Core مدخلات من المستخدم، حتى وصلت إلى `CommandText`، وهذا تحليل تلوث حقيقي داخل المترجم.
- القاعدة [CA2100](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2100?ref=arabroot.io) قاعدة نمط أقدم، فهي تنبه على أي نص غير ثابت في `CommandText` وتطلب منك المراجعة دون أن تعرف مصدره.
- الدالة `SearchSafe` لم تظهر في أي من القاعدتين.
- عند تغيير الخطورة إلى `dotnet_diagnostic.CA3001.severity = error` فشل البناء نفسه بالرسالة `error CA3001` و`Build FAILED`، وهذا يكفي ليمنع خط CI الدمج دون أي أداة إضافية.

ويجدر الإشارة إلى أن البناء نفسه أظهر تحذيراً آخر لا علاقة له بالكود: `warning NU1903: Package 'SQLitePCLRaw.lib.e_sqlite3' 2.1.11 has a known high severity vulnerability`، وهذا ليس فحصاً ثابتاً، وإنما فحص الاعتماديات المدمج في NuGet، وهو موضوع [مقال SCA](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA-sca-%D9%85%D8%B9-osv-scanner-%D9%88-dependency-track/).

### إزالة التجربة Cleanup

نحذف ما أنشأناه بالأسماء فقط، دون أي أمر Prune، لأن السيرفر قد يحتوي Containers وVolumes لمشاريع أخرى:

```bash
docker rm -f lab-dso-sast-sonarqube
docker network rm lab-dso-sast-net
docker volume rm lab-dso-sast-m2
docker rmi sonarqube:community sonarsource/sonar-scanner-cli:latest
```

ولاحظ أن ال Docker Image الخاصة ب SonarQube تعرف Volumes دون اسم لمجلدات البيانات، فإذا شغلت ال Container دون `-v` فسوف تبقى هذه ال Volumes بعد حذفه، فإما أن تعطيها أسماء عند التشغيل مثل `-v lab-dso-sast-sq-data:/opt/sonarqube/data`، أو تحذف ال Container ب `docker rm -f -v` حتى تحذف معه.

## تشغيل الفحص الثابت SAST في خط CI

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

![أربع مراحل مرقمة: Editor أو pre-commit مع قواعد CA وdotnet build، ثم Pull request مع --baseline-commit main ونتائج الطلب فقط وblocks merge، ثم Merge to main مع فحص كامل وSonarQube وبوابة جودة Passed أو Failed، ثم Nightly مع قواعد بطيئة وفتح مهام دون منع، وتحتها ملف results.sarif يذهب إلى code host annotations على الفرق في طلب الدمج وإلى findings dashboard مثل DefectDojo](https://arabroot.io/content/images/2026/10/sast-06-ci-flow.webp)

الفحص السريع يمنع الجديد في طلب الدمج، والفحص الكامل يعمل على main وفي الليل

### الفحص التزايدي Incremental مقابل الفحص الكامل Full Scan

الفحص التزايدي يفحص ما تغير في طلب الدمج ويعرض النتائج الجديدة فقط، كما فعلنا مع `--baseline-commit`، وهو سريع ودقيق في رسالته للمطور: «هذا ما أضفته أنت». لكنه لا يكفي وحده، والسبب أن قاعدة جديدة تضاف إلى مجموعة القواعد غداً لن تفحص الكود القديم إلا في فحص كامل، وأن التحليل بين الملفات قد يجد مشكلة في ملف لم يتغير بسبب تغيير في ملف آخر. لذلك الترتيب الصحيح هو:

- **على كل طلب دمج:** فحص تزايدي بقواعد عالية الدقة، يمنع الدمج إذا ظهرت نتيجة جديدة بخطورة عالية، ويعرض الباقي كتعليقات.
- **بعد الدمج في main:** فحص كامل، وتحليل SonarQube مع بوابة الجودة، وحفظ ملف SARIF.
- **كل ليلة أو كل أسبوع:** القواعد البطيئة والصاخبة، ومجموعات القواعد الجديدة، وتفتح النتائج كمهام لها صاحب ولا تمنع أي شيء.

### الكود القديم Legacy Code والخط الأساسي Baseline

عندما تضيف أداة SAST إلى مشروع عمره سنوات فسوف تجد مئات النتائج، والطريقة الخاطئة هي أن تجعل الخط يفشل بسببها كلها، فيتوقف الفريق ويعطل الأداة بعد يومين. والطريقة الصحيحة هي أن تبدأ بالخط الأساسي، والأدوات تدعمه بطرق مختلفة: Semgrep بالمقارنة مع Commit كما رأيت، وSonarQube بتعريف الكود الجديد New Code منذ إصدار أو تاريخ، وBandit بملف نتائج قديم تمرره بالخيار `-b`، ومنصات الكود بحالة التنبيه على الفرع الرئيسي. ثم تضع خطة للدين القديم حسب الخطورة، فثغرات الحقن في مسارات عامة أولاً، ثم الباقي على مراحل، ولا تقم بتجاهل النتائج القديمة كلها بتعليقات `nosemgrep`، والسبب أن التعليق يقول «راجعها أحد وقرر أنها آمنة»، وهذا غير صحيح.

### كيف تعرض منصات الكود Code Hosts نتائج SARIF

ميزة SARIF أن أي منصة تفهمه تعرض النتائج في مكانها، فـ GitHub يحول ملف SARIF المرفوع إلى تنبيهات Code Scanning تظهر على أسطر الفرق في طلب الدمج وفي تبويب الأمان، وتذكر [صفحة Code Scanning](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning?ref=arabroot.io) أن استخدامه على المستودعات الخاصة يحتاج ترخيص GitHub Code Security. وعلى سيرفرك مع Gitea Actions احفظ الملف كمرفق Artifact للتشغيل، وافتحه محلياً بإضافة [SARIF Viewer في VS Code](https://marketplace.visualstudio.com/items?itemName=MS-SarifVSCode.sarif-viewer&ref=arabroot.io)، أو أرسل ملفات كل الأدوات إلى منصة تجمع النتائج وترتبها مثل DefectDojo، وهذا ما سوف نبنيه في [خط 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/).

### مثال خط Gitea Actions

الملف التالي يشغل الفحوص التي نفذناها في التجربة على كل طلب دمج، بالأوامر نفسها التي نفذناها في التجربة، وقد شرحنا تثبيت المشغل في [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/):

```yaml
name: sast
on:
  pull_request:
  push:
    branches: [main]

jobs:
  semgrep:
    runs-on: ubuntu-latest
    container: semgrep/semgrep:1.178.0
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0              # the baseline needs the history, not a shallow clone
      - name: Custom rules have passing tests
        run: semgrep scan --test --metrics=off rules/
      - name: New findings in this pull request
        if: github.event_name == 'pull_request'
        run: |
          git config --global --add safe.directory "$PWD"
          BASE=$(git merge-base origin/${{ github.base_ref }} HEAD)
          semgrep scan --config p/default --config rules/ --metrics=off \
            --baseline-commit "$BASE" --error
      - name: Full scan on main
        if: github.event_name == 'push'
        run: |
          semgrep scan --config p/default --config rules/ --metrics=off \
            --sarif --output semgrep.sarif
      - uses: actions/upload-artifact@v4
        if: github.event_name == 'push'
        with:
          name: semgrep-sarif
          path: semgrep.sarif

  bandit:
    runs-on: ubuntu-latest
    container: python:3.13-slim
    steps:
      - uses: actions/checkout@v4
      - run: pip install bandit==1.9.4 && bandit -q -r app/
```

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

- `fetch-depth: 0` يجلب التاريخ كاملاً، وبدونه يظهر التحذير `may be incomplete for shallow checkouts` الذي رأيته في التجربة، وقد لا يجد Semgrep نقطة المقارنة أصلاً.
- نقطة المقارنة هي `git merge-base` بين الفرع الهدف ورأس الطلب، وليست آخر Commit في main، حتى لا تظهر في الطلب نتائج أضافها غيره في main بعد التفرع.
- `--error` يجعل الأمر يعيد رمز خروج 1 عند وجود نتائج، فيفشل ال Job ويمنع الدمج إذا كان الفرع محمياً.
- اختبار القواعد المخصصة يعمل قبل الفحص، حتى لا يدمج أحد قاعدة مكسورة تصمت عن كل شيء.
- إصدار ال Docker Image محدد `1.178.0` وليس `latest`، والسبب أن إصداراً جديداً قد يضيف قواعد فيفشل طلب دمج لم يتغير فيه شيء، والأنسب أن تحدث الإصدار بطلب دمج خاص به، وقد شرحنا تثبيت الإصدارات وأمان ال Actions نفسها في [أمان سلسلة التوريد البرمجية](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/).
- هذا الملف لم نشغله على مشغل Gitea في هذا المقال، وإنما شغلنا كل أمر فيه في Docker كما رأيت في التجربة، أما الخط الكامل الذي يعمل على مشغل حقيقي فهو موضوع مقال الخط الآمن من البداية للنهاية.

ولمشاريع Java وC# فالخطوة أبسط، فالفحص جزء من البناء نفسه: `mvn compile spotbugs:check` في Maven، و`dotnet build` مع خطورة `error` لقواعد CA3001 وما بعدها في ملف `.editorconfig`، ثم الماسح SonarScanner مع `-Dsonar.qualitygate.wait=true` بعد الدمج.

## أخطاء شائعة Common Mistakes

- **تفعيل كل القواعد في اليوم الأول:** مئات النتائج من قواعد صاخبة تجعل الفريق يتجاهل الأداة قبل أن تجد أول ثغرة حقيقية، فابدأ بقواعد عالية الدقة تمنع الدمج، والباقي تنبيه.
- **حذف القاعدة بسبب إنذار خاطئ واحد:** القاعدة التي أخطأت في السطر 109 هي نفسها التي وجدت VULN-02، فتجاهل النتيجة في مكانها، باسم القاعدة وسبب مكتوب.
- **`# nosemgrep` دون أسماء القواعد:** يخفي أي قاعدة تظهر على السطر في المستقبل، ومنها ثغرات لم تكن موجودة يوم كتبت التعليق.
- **اعتبار التقرير النظيف دليل أمان:** الأداة لا ترى الصلاحيات ولا منطق العمل ولا الإعداد ولا المكتبات، والتقرير النظيف يعني فقط أن أنماط القواعد لم تظهر.
- **قواعد Roslyn للأمان معطلة دون أن تعرف:** CA3001 وأخواتها معطلة افتراضياً في .NET 10، فالبناء الأخضر لا يعني أن الحقن تم فحصه.
- **الاعتماد على SonarQube Community Build لثغرات الحقن:** النسخة المجانية لا تتتبع التلوث، فلم تجد XSS ولا Open Redirect ولا حقن الأوامر في تجربتنا.
- **تشغيل CodeQL CLI على كود خاص خارج GitHub:** شروط الأداة تمنع ذلك صراحة، حتى لو كان التحميل متاحاً للجميع.
- **فحص طلب الدمج بنسخة سطحية Shallow Clone:** الخط الأساسي يحتاج التاريخ، وبدونه قد يفحص الأمر لا شيء ويظهر أخضر.
- **قواعد مخصصة دون اختبارات:** قاعدة تعدلها لتقليل الضجيج قد تصمت عن الثغرة الحقيقية، وملف `# ruleid:` و`# ok:` يكشف ذلك فوراً.
- **Docker Image الأداة بالوسم latest في خط CI:** قواعد جديدة تظهر فجأة وتفشل طلبات دمج لم تتغير، فثبت الإصدار وحدثه بطلب دمج.

## قائمة تحقق Checklist

- أداة SAST عامة (Semgrep CE أو Opengrep) وأداة خاصة بلغتك (Bandit أو SpotBugs مع FindSecBugs أو قواعد Roslyn) تعمل في خط CI.
- في مشاريع C#: قواعد CA3001 إلى CA3012 مفعلة في `.editorconfig` بخطورة تكفي لإفشال البناء.
- طلب الدمج يفحص بالخط الأساسي ويمنع النتائج الجديدة عالية الخطورة فقط، مع `fetch-depth: 0`.
- فحص كامل بعد الدمج في main، وفحص ليلي للقواعد البطيئة والجديدة.
- `--metrics=off` ومجموعة قواعد محددة بدل `--config auto` في خط CI.
- كل تجاهل باسم القاعدة وسبب مكتوب وتاريخ، ويمر بمراجعة الكود.
- قاعدة مخصصة لكل نمط خطير خاص بمشروعك، ومعها ملف اختبار يعمل في خط CI.
- النتائج تحفظ بصيغة SARIF وتصل إلى مكان يراه الفريق.
- إصدارات الأدوات وال Docker Images مثبتة، وتحدث بطلب دمج.
- خطة مكتوبة للنتائج القديمة حسب الخطورة، بصاحب وموعد.
- الفحوص الأخرى تغطي ما لا يراه SAST: الأسرار والاعتماديات وال Docker Image والفحص أثناء التشغيل واختبارات الصلاحيات.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- أداة SAST تقرأ الكود دون تشغيله، فتحوله إلى شجرة AST ثم تبحث فيها بقواعد أنماط أو تتبع القيمة من المصدر Source إلى المكان الخطر Sink مروراً بالمنظف Sanitizer.
- كل أداة تتوقف في مكان ما عند تتبع القيمة بين الدوال والملفات، ورأيت كيف فقدت قواعد Flask في Semgrep CE ثغرة مقسومة على دالتين، بينما وجدتها قواعد الأنماط ومعها إنذار خاطئ في مكان آخر.
- الدقة Precision أهم في البداية من الشمول Recall، لأن الأداة التي لا يقرأ أحد نتائجها لا تجد شيئاً.
- Semgrep CE محركه LGPL-2.1 وقواعده بترخيص منفصل للاستخدام الداخلي، وSonarQube Community Build بترخيص LGPL-3.0 دون تحليل تلوث ودون فروع وطلبات دمج، وال Hotspots فيه تتحول إلى مشاكل أمان عادية، وCodeQL CLI للكود مفتوح المصدر فقط إلا بترخيص GitHub التجاري.
- لمطوري Java: SpotBugs مع FindSecBugs في Maven، ولمطوري C#: قواعد Roslyn المدمجة بعد تفعيل CA3001 وأخواتها، وكلاهما وجد الحقن في تجربتنا ولم يعترض على الكود الآمن.
- تجاهل الإنذار الخاطئ في مكانه باسم القاعدة وسبب مكتوب، واكتب قاعدة مخصصة مع اختبارها لكل نمط خاص بمشروعك، وافحص طلب الدمج بالخط الأساسي، وأخرج النتائج بصيغة SARIF.

وفي المقالات التالية من السلسلة بإذن الله سوف نتحدث عن فحص الاعتماديات SCA وفحص الحاويات، ثم نجمع كل الفحوص في خط واحد.

[![](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/secrets-scanning-cover-cover.webp)دليلSecrets Scanning: كيف تكتشف الأسرار المسربة في الكودكيف يخرج المفتاح من جهازك إلى تاريخ Git وخط CI، وكيف تكتشفه قبل غيرك، وماذا تفعل في الساعة الأولى بعد تسربه.](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/)[![](https://arabroot.io/content/images/2026/10/sca-cover-cover.webp)دليلSCA: كيف تجد الثغرات المعروفة في مكتبات مشروعككيف تجد أداة فحص الاعتماديات SCA المكتبات المصابة في مشروعك، ولماذا تختلف نتائج الأدوات، وكيف تبقي مكتباتك محدثة.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA-sca-%D9%85%D8%B9-osv-scanner-%D9%88-dependency-track/)[![](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: كتابة المقال، وتنفيذ التجربة على نسخة من التطبيق التجريبي للسلسلة بأدوات Semgrep CE 1.178.0 وBandit 1.9.4 وSonarQube Community Build 26.9 مع SonarScanner CLI 8.1 وSpotBugs 4.10.4 مع FindSecBugs 1.14.0 على Java 21 ومحللات Roslyn في .NET SDK 10.0.400، والتحقق من تراخيص الأدوات وشروط CodeQL من مستودعاتها وصفحاتها الرسمية.