تطوير الويب

حجب PR على الثغرات الجديدة دون عرقلة الفريق

4 min de lecture

📍 الدليل الرئيسي: Pipeline SAST DAST SCA في 2026: معمارية، أدوات، وتكامل CI/CD
هذا الدرس يفصّل الإستراتيجية التي تجعل خط أنابيب DevSecOps مقبولاً لدى المطورين: حجب الثغرات الجديدة فقط، لا الدين التاريخي أبداً.

لماذا تبنّي سياسة fail-on-new-issue بدلاً من fail-on-any

عندما يُفعِّل فريقٌ SAST و SCA و DAST في آنٍ معاً على مستودع نشِط منذ خمس سنوات، يكشف المسح الأول عادةً مئات النتائج الموروثة. إذا حجبت سياسة CI أي خط أنابيب يعرض ثغرة، فلن يمرّ أي merge بعد ذلك ولن يبقى للفريق سوى خياران: تعطيل المسح (خسارة صافية) أو تجميد كل التطوير حتى تصحيح الدين (مستحيل). سياسة fail-on-new-issue تحلّ هذه المعضلة: تقارن نتائج pull request بحالة مرجعية (baseline)، ولا يفشل الـ job إلا عند ظهور نتيجة لم تكن موجودة سابقاً.

تنتج هذه الإستراتيجية تأثير الزنبرك الأحادي: يبقى الدين التاريخي مرئياً في التقارير دون أن يمنع أحداً من التقدم، بينما لا يستطيع أي مشكل جديد التسلّل بصمت. مع معالجة الفريق للدين، تنكمش baseline طبيعياً. يُنفَّذ المفهوم بشكل مختلف حسب الأدوات — --baseline-commit في Semgrep، New Code Period في SonarQube، Vulnerability State في GitLab Security Policies — لكن المبدأ يبقى واحداً. يستعرض هذا الدرس كل تنفيذ ويعطي هيكل خط أنابيب GitLab CI و GitHub Actions المقابل.

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

  • مستودع Git قائم بخط أنابيب CI يعمل.
  • أداة SAST أو SCA واحدة على الأقل مرتبطة (Semgrep، SonarQube، Trivy، أو الماسحات الأصيلة لـ GitLab/GitHub).
  • فهم نموذج pull request / merge request في forge لديك.
  • المستوى المتوقع: متوسط DevOps، الراحة مع متغيرات CI وقراءة YAML خط أنابيب.
  • الوقت المقدّر: 60 دقيقة لإعداد السياسة على أداة، 90 دقيقة لتعميمها على ثلاث أدوات.

الخطوة 1 — رسم خريطة الدين القائم

قبل تحديد عتبة، يجب قياس ما هو قائم. شغّل كل أداة في الوضع الإعلامي على الفرع الرئيسي وصدِّر التقرير. على Semgrep، مثلاً، من جذر المستودع:

semgrep scan --config auto --json --output baseline.json
jq '.results | group_by(.extra.severity) | map({severity: .[0].extra.severity, count: length})' baseline.json

الخرج يُلخّص عدد النتائج لكل خطورة. على مشروع نموذجي بـ 100 ألف سطر Python نشِط منذ ثلاث سنوات، ستجد عموماً 5 إلى 30 نتيجة HIGH/CRITICAL ومئات الـ MEDIUM/LOW. هذه الصورة ثمينة كـ baseline تقنية وكمؤشر اتصال داخلي معاً — الإعلان عن «لدينا 12 نتيجة حرجة لمعالجتها» أكثر قابلية للعمل من «لدينا مشاكل أمنية».

الخطوة 2 — تفعيل الوضع التفاضلي على Semgrep

يدعم Semgrep بشكل أصيل المقارنة بـ commit مرجعي عبر خيار --baseline-commit. يمسح المحرّك مرتين (الحالة قبل والحالة بعد)، ثم لا يبلّغ إلا عن النتائج الغائبة في الحالة قبل. هذا المنطق موثوق حتى عند تغيير ملف: نتيجة على سطر لم يكن موجوداً تصبح نتيجة جديدة، نتيجة على سطر مُزاح تبقى تاريخية.

semgrep_diff:
  image: returntocorp/semgrep:1.161.0
  stage: scan
  script:
    - git fetch origin "$CI_DEFAULT_BRANCH"
    - semgrep scan
      --config auto
      --baseline-commit "origin/$CI_DEFAULT_BRANCH"
      --error
      --severity ERROR
      --severity WARNING
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

ثلاث خيارات تستحق الانتباه. --error يُرجع كود خروج غير صفري بمجرد ظهور نتيجة جديدة، مما يُفشل job CI. مكرر --severity يقصر الحجب على ERROR و WARNING (يقابل CRITICAL و HIGH في التصنيف الجديد)، تاركاً INFO في الوضع الإعلامي. القاعدة rules: - if: ... merge_request_event تحصر الـ job في MR فقط، متجنبةً الحجب الكاذب على push مباشر على main حيث لا توجد baseline ذات معنى.

الخطوة 3 — إعداد New Code Period في SonarQube

SonarQube لا يُفكر بـ commit بل بـ New Code Period، نافذة زمنية أو إصدارية تُعرّف ما هو «جديد». ثلاثة أوضاع موجودة: منذ تاريخ محدد، منذ نسخة (رقم release)، أو منذ الفرع المرجعي. الأخير هو الأنسب لفريق يمارس trunk-based development.

اضبط في Project Settings → New Code → Specific setting for this project واختر Reference branch: main. منذ تلك اللحظة، لا يأخذ Quality Gate إلا بالكود المُعدَّل منذ آخر تباعد عن main. شارة Quality Gate Passed على الـ PR تعكس حالة الكود الجديد، لا الحالة الإجمالية للمشروع.

sonar-scanner \
  -Dsonar.projectKey=my-project \
  -Dsonar.host.url=https://sonar.example.com \
  -Dsonar.login=$SONAR_TOKEN \
  -Dsonar.pullrequest.key=$CI_MERGE_REQUEST_IID \
  -Dsonar.pullrequest.branch=$CI_COMMIT_REF_NAME \
  -Dsonar.pullrequest.base=$CI_DEFAULT_BRANCH \
  -Dsonar.qualitygate.wait=true \
  -Dsonar.qualitygate.timeout=300

الخيار sonar.qualitygate.wait=true يحجز خروج الماسح حتى صدور حكم Quality Gate. بدون هذا الخيار، ينتهي الماسح فوراً ويصبح job CI أخضر قبل أن ينتهي SonarQube من التحليل. الـ timeout بـ 300 ثانية يحمي من قائمة انتظار مشبعة. إذا فشل الـ gate، يُرجع الماسح 1 ويفشل job CI؛ يتلقى المطورون تعليقاً تلقائياً على الـ MR بتفاصيل الشروط غير المُحقَّقة.

الخطوة 4 — تعريف Quality Gate صارم للكود الجديد

الـ Quality Gate الافتراضي Sonar way متساهل عمداً. لسياسة fail-on-new-issue صارمة، أنشئ gate مخصصاً في Quality Gates → Create بالشروط التالية المطبَّقة على new code فقط:

الشرط العامل العتبة
New Vulnerabilities أكبر من 0
New Security Hotspots Reviewed أقل من 100%
New Bugs (خطورة CRITICAL) أكبر من 0
New Coverage أقل من 80%
New Duplicated Lines (%) أكبر من 3

عدم التماثل ضروري: صفر تسامح على الثغرات الجديدة، مطلب قوي على تغطية الكود الجديد (80%)، لكن بلا قيد على الكود القائم. هذا الإعداد متطلب بما يكفي لإبقاء الفريق تحت ضغط إيجابي دون حجب متواصل. اربط الـ gate بالمشروع عبر Project Settings → Quality Gate.

الخطوة 5 — سياسات GitLab Security وعتبات حسب الخطورة

على GitLab Premium، تعرض Security Policies نهجاً أكثر تصريحية من متغيرات البيئة لكل job. سياسة YAML تنطبق على عدة مشاريع عبر Compliance Frameworks وتبقى مستقلة عن ملفات .gitlab-ci.yml لكل مستودع. أنشئ في Security & Compliance → Policies → Scan result policy:

type: scan_result_policy
name: Fail on new high SAST vulnerabilities
description: Block MR with newly detected HIGH or CRITICAL SAST findings
enabled: true
rules:
  - type: scan_finding
    branches: [main, develop]
    scanners: [sast, secret_detection, container_scanning, dependency_scanning]
    vulnerabilities_allowed: 0
    severity_levels: [critical, high]
    vulnerability_states: [newly_detected]
actions:
  - type: require_approval
    approvals_required: 1
    role_approvers: [security_engineer]

الثنائي vulnerabilities_allowed: 0 + vulnerability_states: [newly_detected] يُنفِّذ تماماً دلالة fail-on-new: صفر نتيجة جديدة HIGH أو CRITICAL متسامَح معها دون موافقة صريحة من security engineer. لسياسة محجبة بحتة (بلا منفذ هروب عبر الموافقة)، غيّر الإجراء إلى require_approval بدور غير موجود — لن يستطيع أحد الموافقة على الـ MR وبالتالي لن تُدمج ما دامت النتيجة قائمة.

الخطوة 6 — Trivy مع ignorefile وعتبات خطورة

Trivy لا يمتلك خيار --baseline-commit مباشراً، لكنه يعرض .trivyignore الذي يخدم كـ baseline يدوية للـ CVE المقبولة. النمط الموصى به يجمع عتبة خطورة دنيا و ignorefile مُصدَّر في المستودع.

trivy image \
  --severity HIGH,CRITICAL \
  --exit-code 1 \
  --ignore-unfixed \
  --ignorefile .trivyignore \
  --format json \
  --output trivy-report.json \
  $IMAGE_TAG

الخيار --exit-code 1 يُفشل الـ job بمجرد ظهور CVE غير مُتجاهَلة بخطورة HIGH أو CRITICAL. --ignore-unfixed يستثني CVE التي لم يصدر لها patch بعد، متجنباً الحجب على حالات لا يستطيع المطور فعل شيء حيالها. ملف .trivyignore يحتوي CVE واحدة لكل سطر مع تعليق ومثالياً تاريخ انتهاء:

# CVE-2024-12345 - إيجابية كاذبة على مكتبة X، لا تنطبق على الوضع المستخدم. أعد المراجعة 2026-09-01.
CVE-2024-12345
# CVE-2024-67890 - قابلة للاستغلال فقط مع وصول admin محلي. قبلها CISO 2026-04-15.
CVE-2024-67890

المراجعة الفصلية لـ ignorefile جزء من الطقس الأمني: بدون تاريخ انتهاء، ملف يبدأ بـ 5 أسطر ينتهي بـ 200 في سنتين، مُفرِغاً السياسة من معناها.

الخطوة 7 — التنفيذ في GitHub Actions مع Code Scanning

GitHub Code Scanning يحسب تلقائياً الفرق بين حالة الفرع الهدف وحالة الـ PR. تُضبط سياسة fail-on-new على مستوى المستودع عبر Settings → Code security → Code scanning → Default branch، باختيار عتبة الخطورة التي يجب أن تُفشل الفحص.

name: Code scanning
on:
  pull_request:
    branches: [main]
permissions:
  security-events: write
  contents: read
jobs:
  analyze:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v4
        with:
          languages: javascript-typescript, python
      - uses: github/codeql-action/analyze@v4

خطوة analyze تنشر تلقائياً SARIF في GitHub Code Scanning؛ تُحدَّد خطورة الحجب بعد ذلك على مستوى المستودع في Settings → Code security → Code scanning → Protection rules، باختيار المستوى (مثلاً «Block PRs with errors of severity High and above»). مع branch protection rule تشترط مرور فحص CodeQL للدمج (مُعدَّة في Settings → Rules → Rulesets → Require status checks to pass)، تحجب هذه السياسة فعلياً الـ PR المخالفة دون الاعتماد على action خارجية.

الخطوة 8 — آلية استثناء مُتتبَّعة

سياسة صارمة بلا منفذ هروب لا تُحتمل. عندما يكون للمطور سبب شرعي للدمج رغم نتيجة (إيجابية كاذبة مُؤكَّدة، سياق معروف، دين مقبول مؤقتاً)، يجب أن يستطيع ذلك — لكن بأثر. ثلاث آليات متكاملة تُنظِّم الاستثناء المُتحكَّم به.

الأولى الإشارة inline: # nosemgrep: rule-id - مبرّر في Semgrep، // trivy:ignore:CVE-... - سبب في Trivy، أو // codeql[ql/...] = "سبب" في CodeQL. المبرّر إلزامي في code review. الثانية الموافقة الإضافية في Security Policies الخاصة بـ GitLab: يجب أن يوافق security engineer صراحةً، اسمه يظهر في السجل. الثالثة flag dismissed في GitHub Code Scanning أو DefectDojo، مع اختيار إلزامي لسبب بين false positive، used in tests، won t fix.

سياسة جيدة تجمع الثلاث: إشارة للحالات الواضحة المحلية، موافقة للحالات التي تستحق نظرة خبير، dismiss للتوثيق المركزي. المراجعة الفصلية للـ dismissals لحظة سليمة لاكتشاف الإسراف وضبط القواعد المخطئة.

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

العَرَض السبب الحل
كل النتائج موسومة «جديدة» في كل مسح لا baseline مُعدَّة أو commit baseline غير موجود تأكد من git fetch origin main قبل المسح، وإلا لن يستطيع Semgrep المقارنة
SonarQube Quality Gate Passed لكن نتائج حرجة ظاهرة الـ gate يحتوي شروطاً على new code فقط، الدين التاريخي يمرّ هذا السلوك المرغوب: أنشئ dashboard منفصل لمتابعة الدين الإجمالي
Trivy يحجب على CVE لا يوجد لها fix غياب --ignore-unfixed أضف الـ flag، أو اعتمد CVE صراحةً مع تاريخ انتهاء قصير
سياسة Approval في GitLab لا تنطلق السياسة محصورة بـ main بينما MR تستهدف release/* وسّع branches: بـ wildcard أو أضف الفروع الهدف صراحةً
عدد النتائج «new» يتذبذب في كل MR اختلاف بين المسح المحلي للمطور ومسح CI (إصدارات مختلفة) ثبّت إصدار الأداة بشكل متطابق محلياً وفي CI
إشارة nosemgrep مُتجاهَلة الإشارة على السطر الخطأ ضعها مباشرة فوق السطر المعني، بلا سطر فارغ بينهما

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

هل نُفشل على MEDIUM أم فقط على HIGH/CRITICAL؟ التسوية العملية هي الحجب على HIGH/CRITICAL والإشارة إعلامياً على MEDIUM. الحجب على MEDIUM في البداية يولّد احتكاكاً كثيراً ويدفع الفريق لتعطيل الأداة. مع انخفاض نسبة الإيجابيات الكاذبة تحت 10%، يمكن التشديد.

كيف نُبرِّر السياسة لمطورين متحفظين؟ اعرض إحصاءَين محسوسَين: (1) عدد دقائق CI المُوفَّرة لكل MR (المسح التفاضلي عادةً 5 إلى 10 مرات أسرع من المسح الكامل)، و (2) عدد النتائج المُصحَّحة فعلاً في ستة أشهر بفضل الضغط على الكود الجديد. هذان الرقمان يُظهران أن السياسة تخدم الفريق، لا الامتثال المجرّد.

ما العمل بقاعدة legacy تتجاوز 1000 نتيجة؟ ارفض إغراء التنظيف الكبير. فعّل fail-on-new فوراً لإيقاف النزيف، ثم خصّص security debt budget من 10 إلى 20% من الـ sprint لمعالجة الدين بترتيب خطورة تنازلي. في 12 إلى 18 شهراً، ينخفض الدين تحت العتبة المقبولة دون تجييش أحد في وضع أزمة.

هل يكتشف الوضع التفاضلي تراجعاً أمنياً صُحِّح ثم أُعيد إدخاله؟ نعم في معظم الحالات: بما أن baseline هي الفرع الهدف، إذا صُحِّحت CVE على main ثم حذفت MR الإصلاح، يكتشف المسح التفاضلي التراجع كنتيجة جديدة. هذه الخاصية ثمينة لتجنّب التراجعات الصامتة عند merges معقدة.

كيف نقيس نجاح السياسة؟ ثلاثة مؤشرات: (1) نسبة MR التي تفشل في فحص الأمن (الهدف 5 إلى 15%)، (2) المدة الوسيطة بين الاكتشاف وتصحيح نتيجة HIGH (الهدف < 14 يوماً)، (3) عدد dismissals المُبرَّرة في الفصل (للمقارنة بـ baseline لكشف التطبيع).

دروس ذات صلة

  • تخفيض الإيجابيات الكاذبة لـ SAST بأسلوب منضبط — منهجية فرز تكمّل سياسة fail-on-new.
  • دمج SAST + SCA + DAST في خط أنابيب GitLab CI 18 خطوة بخطوة — خط أنابيب كامل لتطبيق هذه السياسة عليه.

🔝 العودة إلى الدليل الرئيسي: 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é