تطوير الويب

كتابة قواعد Semgrep مخصصة: pattern و taint analysis و autofix

3 min de lecture

📍 الدليل الرئيسي: Pipeline SAST DAST SCA في 2026: معمارية، أدوات، وتكامل CI/CD
هذا الدرس يتعمّق في كتابة قواعد Semgrep خاصة بكود فريقك، كمكمّل للسجلّ العام.

لماذا كتابة قواعد Semgrep خاصة في 2026

تضمّن Semgrep CE 1.161.0 (إصدار 22 أبريل 2026) افتراضياً أكثر من 2800 قاعدة تصونها المجتمع، تغطّي الثغرات الكلاسيكية في OWASP Top 10 والأنماط المضادة الشائعة لـ 30 لغة. هذه القاعدة ممتازة كشبكة عامة، لكنها تبقى عامة: لا قاعدة مجتمعية تعرف أن دالتك الداخلية db_query() تتوقّع معاملاً مهرَّباً سلفاً، أو أن استخدام internal.crypto.legacy_hash() محظور منذ ثلاثة أشهر في ميثاق الأمن لديك. هذا الفراغ بالضبط ما تملأه القواعد المخصصة.

قاعدة Semgrep مخصصة مكتوبة جيداً لها ثلاث خصائص. تعبّر عن معرفة عملية لا يمتلكها سوى كودك، مما يجعلها مستحيلة التكرار عند ناشر خارجي. تعمل في ميلي ثوانٍ لكل ملف، فيمكن تشغيلها في pre-commit دون إبطاء المطور. وتحجب أو تنذر برسالة سياقية تشرح ليس فقط ماذا بل لماذا، مما يحوّل الأداة إلى آلة لنقل معايير الفريق. هذا الدرس يُظهر كيف ننتقل من البدء السريع إلى مستودع قواعد مُصدَّر، مُدمج في خط أنابيب CI و pre-commit hook.

المتطلبات المسبقة

  • Python 3.9+ أو Docker مثبّت على الجهاز.
  • Git ومستودع هدف — أي لغة من المدعومة (Python, JavaScript/TypeScript, Go, Java, C#, Ruby, PHP، إلخ).
  • فهم أساسي لـ AST (شجرة بنية مجرّدة) — مفيد لاستيعاب فلسفة pattern.
  • المستوى المتوقع: متوسط في التطوير، كتابة YAML سابقة، الراحة مع التعابير النمطية.
  • الوقت المقدّر: 75 دقيقة لتغطية المجموع ونشر 3 قواعد فريق.

الخطوة 1 — تثبيت Semgrep CE وتشغيل المسح المرجعي

يُثبَّت Semgrep بطريقتين. CLI Python يناسب الاستخدام المحلي ومحطة المطور؛ صورة Docker تُبسّط الدمج في CI دون فرض اعتمادية Python على الـ runner. الوضعان يقبلان نفس تركيب القاعدة بالضبط، مما يسمح بالنمذجة محلياً والتشغيل في CI دون احتكاك.

pip install semgrep
semgrep --version

cd /path/to/project
semgrep scan --config auto

الوضع --config auto يختار تلقائياً القواعد المناسبة حسب اللغات المكتشفة ويُرسل قياسات مجهولة إلى Semgrep؛ لبيئة offline تماماً، فضّل semgrep scan --config p/owasp-top-ten الذي يحمّل حزمة OWASP محلياً بلا اتصال شبكة. الخرج يُلخّص عدد الملفات المحلَّلة والوقت المنقضي وقائمة النتائج بخطورتها (LOW, MEDIUM, HIGH, CRITICAL منذ إعادة التصميم 2024). هذا المسح الأول يخدم كقاعدة قبل إضافة القواعد المخصصة.

الخطوة 2 — استيعاب فلسفة pattern

قاعدة Semgrep لا تصف نصاً للمطابقة بل بنية تركيبية. يحلّل المحرّك الكود المصدري إلى AST، ويُطبّع بعض التراكيب المتكافئة (مثلاً if (x == null) و if (null == x))، ثم يبحث عن الأشجار الفرعية التي تطابق pattern. هذا النهج البنيوي يلتقط حدوثاً يفوّته grep — متغيرات مُعاد تسميتها، مسافات مختلفة، ترتيب وسائط مقلوب — دون الانخداع بالتعليقات أو السلاسل التي تحتوي الشكل الحرفي.

الـ metavariables (المسبوقة بـ $) تلعب دور joker مُكتب. الـ pattern hashlib.md5($X) يطابق كل استدعاءات MD5 مهما كان المُمرَّر — سلسلة حرفية، متغير، أو تعبير معقد. الـ pattern os.system($CMD) مع metavariable $CMD يسمح لاحقاً بتفحص محتوى المعامل. ثلاث اتفاقيات تستحق الحفظ: $X يطابق أي تعبير، $STR مع metavariable-pattern يمكن تقييده بسلسلة حرفية، والـ ellipsis ... تمثّل سلسلة عشوائية من تعليمات أو وسائط.

الخطوة 3 — كتابة قاعدة أولى تستهدف نمطاً مضاداً داخلياً

لنفترض أن فريقك منع استخدام requests.get() دون timeout صريح: استدعاء بدون timeout يمكن أن يحجز worker لعدة دقائق مع هدف بطيء، حتى استنفاد pool الاتصالات. هذه القاعدة هي النموذج الأصيل للمعرفة العملية التي لا تظهر في أي registry. أنشئ ملف rules/python/no-requests-without-timeout.yml في جذر المستودع.

rules:
  - id: no-requests-without-timeout
    languages: [python]
    severity: HIGH
    message: |
      استدعاء requests.get() أو ما شابه بدون timeout يمكن أن يحجز
      worker بشكل غير محدود. أضف timeout=(3, 10) كحد أدنى.
    metadata:
      cwe: CWE-400
      owasp: A05:2021 - Security Misconfiguration
      author: equipe-secu
    pattern-either:
      - pattern: requests.get(...)
      - pattern: requests.post(...)
      - pattern: requests.put(...)
      - pattern: requests.delete(...)
    pattern-not: requests.$METHOD(..., timeout=$T, ...)

الفكرة تعتمد على تركيب pattern-either + pattern-not: نلتقط كل استدعاء لفعل HTTP في مكتبة requests، ثم نستثني صراحةً المتغيرات التي تحتوي معامل مُسمّى timeout. اختبر فوراً بـ semgrep scan --config rules/python/no-requests-without-timeout.yml. الخرج يسرد الأسطر المخالفة مع مقتطف كود، والرسالة تتضمّن المبرّر CWE-400 (Resource Exhaustion) لمساعدة المطور على فهم الخطورة.

الخطوة 4 — كتابة قاعدة taint لحقن SQL مخصص

وضع taint يتتبع انتشار البيانات من مصدر غير موثوق حتى مَصَب حساس. يتجاوز نطاق pattern الساكن بكثير: متغير يمكن أن يُسنَد، ويُمرَّر إلى عدة دوال، ويتحوّل عبر helpers داخلية؛ ما دام لم يُعقَّم صراحةً، يعتبره المحرّك tainted. هذا المثال يستهدف دالة مخصصة internal_db.query(sql) لا يشكّ أي أداة خارجية في وجودها.

rules:
  - id: internal-db-sql-injection
    languages: [python]
    severity: CRITICAL
    mode: taint
    message: |
      بيانات غير معقّمة قادمة من طلب HTTP تُحقن مباشرة في
      internal_db.query(). استخدم internal_db.query_safe(sql, params)
      الذي يمرّر المعاملات كـ bind variables.
    metadata:
      cwe: CWE-89
      owasp: A03:2021 - Injection
    pattern-sources:
      - pattern: request.args.get(...)
      - pattern: request.form.get(...)
      - pattern: request.json[...]
    pattern-sanitizers:
      - pattern: html.escape(...)
      - pattern: internal_security.escape_sql(...)
    pattern-sinks:
      - pattern: internal_db.query($SQL)

ثلاث قوائم تُنظّم القاعدة. المصادر تعدّ نقاط دخول HTTP لـ Flask. الـ sanitizers تعترف بـ helpers الداخلية التي تجعل البيانات آمنة — يستطيع الفريق إضافتها مع تطوّر النضج. الـ sinks تستهدف الدالة الداخلية الحساسة. شغّل المسح، ولاحظ أن القاعدة تنبعث فقط لمسارات بيانات حقيقية، مما يقلّص بشكل جذري الإيجابيات الكاذبة مقارنة بـ grep على internal_db.query.

الخطوة 5 — إضافة autofix للقواعد الحتمية

عندما يُستنتج التصحيح من pattern، يمكن لـ Semgrep اقتراح أو تطبيق الإصلاح تلقائياً عبر مفتاح fix:. هذه القدرة تحوّل قاعدة منع إلى قاعدة هجرة سلسة، مفيدة خصوصاً عند تغيير مكتبة أو إدخال دالة بديلة.

rules:
  - id: replace-deprecated-md5-hash
    languages: [python]
    severity: MEDIUM
    message: |
      hashlib.md5() مهجور لأي استخدام تشفيري.
      استخدم hashlib.sha256() أو bcrypt حسب حالة الاستخدام.
    pattern: hashlib.md5($X)
    fix: hashlib.sha256($X)
    metadata:
      cwe: CWE-327

التطبيق يدوي (وضع تفاعلي --autofix --dryrun الذي يعرض الـ diffs دون تعديل) أو تلقائي (semgrep scan --autofix الذي يُعيد كتابة الملفات). على مشروع حيّ، فضّل dryrun في CI لتوليد تعليق PR بالـ diffs المقترحة وترك المطور يقبلها فردياً. الـ autofix يصبح مدمراً إذا طابق pattern حالة شرعية لم يتنبأ بها المؤلف؛ راجع دائماً قبل المدمج.

الخطوة 6 — إصدار القواعد في مستودع مخصص

بعد كتابة ثلاث أو أربع قواعد، الانعكاس الصحيح هو استخراج القواعد إلى مستودع Git منفصل، مشترك بين كل مشاريع المؤسسة. يتجنّب ذلك التكرار، ويُمركز المراجعة، ويتيح إصداراً مستقلاً. البنية الموصى بها:

semgrep-rules-team/
├── README.md
├── python/
│   ├── no-requests-without-timeout.yml
│   ├── internal-db-sql-injection.yml
│   └── replace-deprecated-md5-hash.yml
├── javascript/
│   ├── no-eval.yml
│   └── react-no-dangerouslysetinnerhtml.yml
├── tests/
│   ├── python/
│   │   └── no-requests-without-timeout/
│   │       ├── test.py
│   │       └── test.expected.json
└── .pre-commit-hooks.yaml

كل قاعدة لها حالة اختبارها: ملف test.py يحتوي في الوقت نفسه حدوثاً متوقعاً (# ruleid: no-requests-without-timeout) وأمثلة مضادة (# ok: no-requests-without-timeout). الأمر semgrep --test يُشغّل الحزمة ويضمن ألا يكسر أي تعديل القواعد القائمة. هذه الانضباطية تحوّل مستودع القواعد إلى مشروع برمجي قائم بذاته، مع CI ومراجعة pull request إلزامية قبل المدمج.

الخطوة 7 — ربط Semgrep في pre-commit و CI

الفائدة القصوى تُحصَّل عندما يمرّ كل commit محلي وكل pull request بعيد عبر القواعد. لـ pre-commit، إطار pre-commit من Yelp ينظّم هذا التنفيذ. أضف في .pre-commit-config.yaml للمستودع الهدف:

repos:
  - repo: https://github.com/semgrep/semgrep
    rev: v1.161.0
    hooks:
      - id: semgrep
        args: ['--config', 'https://raw.githubusercontent.com/your-org/semgrep-rules-team/main', '--error']

الخيار --error يُرجع كود خروج غير صفري بمجرد ظهور نتيجة، مما يحجب الـ commit. لـ GitLab CI، أنشئ job مخصصاً يُعيد المسح على diff الـ merge request بـ semgrep scan --baseline-commit origin/main. هذا التركيب لا يبلّغ إلا عن المخالفات الجديدة المُدخَلة، مما يتجنّب إغراق PR في الدين التاريخي.

الخطوة 8 — قياس جودة حزمة قواعد

قاعدة تنتج كثيراً من الإيجابيات الكاذبة سيُعطّلها المطورون في أيام. قياس الجودة يمرّ عبر ثلاثة مؤشرات بسيطة. معدل القبول يحسب نسبة النتائج التي يصحّحها المطورون أو يبرّرونها صراحةً بـ nosemgrep، مقارنة بالباقية بلا رد — فوق 70%، القاعدة مفيدة. وقت المسح يجب أن يبقى تحت ثانيتين لـ 10 آلاف سطر، وإلا أصبح احتكاك pre-commit لا يُحتمل. أخيراً، عدد تذاكر المراجعة المفتوحة ضد القاعدة نفسها هو الإشارة الأوضح: إذا أنتجت قاعدة شكاوى أكثر من إصلاحات، أعِد تصميمها أو احذفها.

تصدير النتائج إلى DefectDojo يتم عبر خرج SARIF بـ semgrep scan --sarif --output report.sarif، ثم الاستيراد في DefectDojo عبر API الخاص به يسمح بتجميع Semgrep و Trivy و SonarQube في backlog إصلاح واحد.

الأخطاء الشائعة

العَرَض السبب الحل
القاعدة لا تطابق رغم أن الكود يبدو متطابقاً الـ pattern يستخدم مسافات أو معامل مُطبَّع بشكل مختلف اختبر pattern في playground semgrep.dev/playground قبل الإصدار
إيجابيات كاذبة كثيرة على مكتبة قديمة pattern عريض جداً، بلا pattern-not-inside قيّد بـ pattern-not-inside: def legacy_$F(...): ... لاستثناء السياقات المعروفة
وضع taint لا ينتشر عبر دالة داخلية الدالة غير مُعترَف بها كـ propagator أضف pattern-propagators بتوقيع الدالة و metavariable $X
الـ autofix يكسر التركيب الإصلاح لا يحفظ السياق (أقواس، فواصل) اختبر منهجياً بـ --dryrun قبل --autofix
أداء منخفض على monorepo مسح كامل عند كل commit استخدم --baseline-commit لمسح الـ diff فقط
نتائج مكرّرة بين Semgrep CE وحزمة OWASP قاعدتان تغطّيان نفس الـ pattern عطّل صراحةً بـ --exclude-rule في الأمر

الأسئلة الشائعة

كيف نُبرِّر إيجابية كاذبة بشكل نظيف؟ أضف تعليق nosemgrep: rule-id على السطر السابق للحدوث، مثالياً مع مبرّر: # nosemgrep: no-requests-without-timeout - timeout مُدار بواسطة decorator retry(). مراجعة الكود تُصادق أو ترفض المبرّر.

هل يدعم Semgrep CE تحليل cross-file (interprocedural global)؟ ليس في إصدار Community: تحليل cross-file (Pro Engine) مخصّص للعرض المدفوع منذ فصل 2024. للحالات الحرجة، اجمع CE مع CodeQL الذي يغطّي interprocedural بشكل أصيل.

هل ننشر قواعدنا علناً في registry؟ للقواعد العامة (أنماط مضادة للغة، ثغرات كلاسيكية)، نعم — المجتمع يستفيد ويحسّن القواعد بـ PR. أما القواعد العملية التي تكشف دالة داخلية حساسة، فاحتفظ بها خاصة.

ما الفرق مع ESLint أو Bandit لـ Python؟ ESLint يستهدف الأسلوب والجودة، تركيب plugin أثقل. Bandit يستهدف أمن Python فقط. Semgrep متعدد اللغات و DSL الخاص بـ pattern يبقى مقروءاً لمطور بلا خبرة compilation. الثلاثة يتعايشون بلا تعارض.

هل وضع taint مكلف في الأداء؟ نعم، حوالي 3 إلى 5 مرات أبطأ من وضع pattern. خصّصه للقواعد عالية القيمة (injections، deserialization)، لا لمنوعات الأسلوب.

دروس ذات صلة

  • الاستضافة الذاتية لـ SonarQube Community Build 26.4 على VPS Linux — تثبيت وضبط أداة SAST التاريخية، مكمّلة لـ Semgrep.
  • تخفيض الإيجابيات الكاذبة لـ SAST بانضباط — منهجية فرز قابلة للتطبيق على قواعد Semgrep المخصصة.

🔝 العودة إلى الدليل الرئيسي: Pipeline SAST DAST SCA في 2026: معمارية، أدوات، وتكامل CI/CD

قراءات موصى بها

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité