PyDA Course
متوسط نصف موجّه ⏱ 60 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

فاحص الثغرات الأمنية

securitycliautomation

تبدو ماسحات الأمان كالسحر حتى ترى أجزاءها: قائمة إصدارات معروفة بأنها سيئة (تلك هي قاعدة بيانات CVE)، وفحص “هل نسختي في نطاق سيئ؟” (ذلك حساب الإصدارات)، ومرور على أنماط يجب ألا تكون في الكود المُشحَن (ذلك SAST). يبني هذا المشروع الثلاثة في Python خالص ، حلّل requirements.txt إلى تبعيات منظمة، وطابقها مع قاعدة بيانات CVE صغيرة بدرجات خطورة، وافحص ملفات الكود بحثًا عن الأنماط المضادة الخطرة، وأصدر تقريرًا واحدًا مُرتَّبًا بدرجة الخطورة مع اقتراحات الترقية. لن يغطي سلسلة التوريد كلها؛ سيُزيل الغموض عن كيف يفكر هذا النوع من الماسحات بالضبط.

هذا يفترض Python 101 وقليلًا من regex ، لا شيء من تحليل البيانات مطلوب. هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للقائمة الكاملة والنامية.

🎯 ما ستفعله

  1. تحليل requirements.txt مُثبَّت في تبعيات منظمة.
  2. نمذجة قاعدة بيانات CVE صغيرة بنطاقات إصدارات متأثرة ودرجات خطورة.
  3. مطابقة الإصدارات المثبتة ضد النطاقات وجمع النتائج.
  4. تشغيل SAST قائم على regex على ملفات الكود بحثًا عن الأنماط الخطرة.
  5. دمج الاثنين في تقرير مُرتَّب بدرجة الخطورة مع اقتراحات إصلاح.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الأساسي. الماسح مكتبة قياسية خالصة، لكن قيمته الحقيقية في توجيهه إلى requirements.txt وsrc/ لمشروعك أنت ، ملفات تعيش على قرص تملكه.

Google Colab وKaggle Notebooks وBinder تشغّل كل خلية بشكل متطابق (المكتبة القياسية فقط)، ويشحن دفتر الملاحظات المثال requirements.txt وfragile.py حاملَين بداخله، فترى الفحص الكامل ضد مشروع عينة ثابت. التحفظ الصادق: دفتر ملاحظات يفحص تبعيات مستودع الدورة نفسه كان سيُريك المحرك نفسه ضد الشيء الحقيقي، لكن نظام ملفاته المؤقت يجعل “افحص مشروعي” حركة محلية فقط. استخدم الشارات لرؤية المحرك؛ وشغّل uv للتدقيق الحقيقي.

Open In Colab Open In Kaggle Binder

الإعداد

أنشئ المشروع. يستخدم الماسح المكتبة القياسية فقط ، json وre وpathlib، ومساعد تقسيم إصدارات ستكتبه لأن “أي إصدار أحدث” خوارزمية حقيقية.

uv init vulnerability-scanner
cd vulnerability-scanner
uv run python -c "import json, re; from pathlib import Path; print('ok')"

يخزّن json قاعدة بيانات CVE كبيانات، ويدعم re أنماط SAST، ويمشي pathlib في شجرة src/ الخاصة بك. ستكتب منطق مقارنة الإصدارات بنفسك في الخطوة 3 بدلًا من استيراد مكتبة إصدارات، لأن تلك المقارنة واحدة من فكرتين يعلّمهما هذا المشروع.

✅ قائمة التحقق

  • ✅ أنشأ uv init vulnerability-scanner مجلدًا مع pyproject.toml.
  • ✅ يطبع فحص الاستيراد ok ، صفر حزم مضافة.

الخطوة 1: حلّل التبعيات إلى مواصفات منظمة

يبدأ كل فحص بـ”ماذا لدينا مثبَّت فعلًا؟” requirements.txt حقيقة مُثبَّتة، لكن فقط إذا حوّلت كل سطر إلى مقارنة، لا إلى سلسلة.

1.1 اكتب parse_version وparse_requirements

👟 تلميح البداية : قسّم سلسلة إصدار مثل 2.28.1 إلى زوج عددي ، تقارن Python الأزواج بالترتيب الصحيح مجانًا ، ثم عالج كل سطر متطلبات بـ regex إلى name وoperator وversion.

# scanner.py
import json
import re
from dataclasses import dataclass
from pathlib import Path

@dataclass
class Dependency:
    name: str
    operator: str          # "==" | ">=" | ">" | "<" | "<=" | "any"
    version: tuple[int, ...]


def parse_version(v: str) -> tuple[int, ...]:
    return tuple(int(part) for part in v.split("."))

def parse_requirements(path: str = "requirements.txt") -> list[Dependency]:
    deps = []
    pattern = re.compile(r"([\w\-\.]+)\s*(==|>=|<=|>|<)\s*([0-9\.]+)")
    for line in Path(path).read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        match = pattern.match(line)
        if not match:
            continue
        deps.append(Dependency(name=match.group(1),
                               operator=match.group(2),
                               version=parse_version(match.group(3))))
    return deps

Path("requirements.txt").write_text(
    "requests==2.28.1\nflask==2.2.5\nurllib3==1.26.0\nmakedata==0.9.0\n"
)
for dep in parse_requirements():
    print(dep)

parse_version هي البطل الصامت: بتقسيم 2.28.1 إلى (2, 28, 1)، تقوم مقارنة الزوج الأصلية في Python بالمهمة الصعبة ، (2, 28, 1) < (2, 31, 0) هي true، وهذا بالضبط هو كيف يُجاب عن “هل هذا الإصدار عرضة للخطر” في الخطوة 3. يمنح @dataclass كل تبعية اسمًا وعقد مقارنة بدلًا من سلسلة تحلّلها يدويًا لاحقًا. يمنع regex المسافات والتعليقات من أن تصبح تبعيات وهمية: سطور # pinned تُتخطَّى، وفقط السطور التي تحمل اسمًا ومعاملًا وإصدارًا منقوطًا صالحًا تصبح كائنات Dependency.

🎯 الناتج المتوقع : أربعة أسطر Dependency(name=..., operator='==', version=(2, 28, 1)) ، واحد لكل متطلب حقيقي، والتعليقات والفراغات مُتجاهَلة.

🩹 إذا لم يعمل : إذا عاد إصدار تبعية كزوج فارغ، فـparse_version لم تخدم قط (مجموعة None من regex وصلت إلى dataclass). إذا تحطّمت سطور مثل flask --hash=...، ففشل match في regex وcontinue يبوّبها ، لكن match() صارم على ([\w\-\.]+) يُسقط أيضًا حزمًا مشروعة تحمل _ في الاسم؛ وسّع الصنف إلى [\w\.\-]. إذا حُلّلت المتطلبات من المجلد الخاطئ، فـPath("requirements.txt") نسبي لدليل العمل.

1.2 تحقق من التحليل

✅ قائمة التحقق

  • ✅ تتبلور أربع تبعيات من ملف العينة؛ تُتجاهل التعليقات.
  • ✅ ينتج سطر flask>=3.0.0 قيمة operator='>=' وversion=(3, 0, 0).
  • ✅ أزواج الإصدارات تقارن بشكل صحيح: (2, 28, 1) < (2, 31, 0) هي True.

🤔 سؤال (أسئلة) سقراطي(ة)

  • إصدار مثل 1.10.0 سيقارن كـأقل من 1.9.0 لو خزّنته كرقم واحد (int("1.10.0") ، مستحيل حرفيًا). أي جزء من هذا التصميم يجعل 1.10.0 > 1.9.0 صحيحة، وأين يكسرها إصدار حزمة مثل "2026.9.2rc1"؟
  • يتجاهل regex السطور التي لا يستطيع مطابقتها بصمت. متى يكون تخطّي متطلب مشوّه أسوأ من القاء خطأ، وما الذي ستسجّله لتجعل التخطي مرئيًا؟

الخطوة 2: نمذجة قاعدة بيانات CVE

الماسح بذكاء قاعدته فقط. ترمّز هذه الخطوة بضع CVEs كبيانات ، كلٌّ بنطاق متأثر ودرجة خطورة ، وتحمّلها من JSON ليعرفها الماسح بالطريقة نفسها التي يعرف بها أداة حقيقية خلاصتها.

2.1 حمّل قاعدة بيانات CVE

👟 تلميح البداية : أبقِ الـ CVEs كملف JSON صغير وحمّله مرة واحدة بـ json.load ، يعيد زوجا operator/version استخدام عقد المقارنة نفسه الذي بنته الخطوة 1.

# scanner.py (continued)
CVE_DB_PATH = Path("cve_db.json")
CVE_DB_PATH.write_text(json.dumps([
    {"id": "CVE-2026-0001", "package": "requests", "operator": "<",
     "version": "2.31.0", "severity": "high",
     "summary": "SSL verification bypass on redirect"},
    {"id": "CVE-2025-1234", "package": "flask", "operator": "<=",
     "version": "2.2.5", "severity": "critical",
     "summary": "RCE reachable in debug mode"},
    {"id": "CVE-2026-1000", "package": "makedata", "operator": "<",
     "version": "1.0.0", "severity": "medium",
     "summary": "Slowloris-style memory leak"},
], indent=2))

def load_cves(path: str | None = None) -> list[dict]:
    with open(path or CVE_DB_PATH) as f:
        return json.load(f)

print([c["id"] for c in load_cves()])

قرار النمذجة الحرج أن تحمل CVE معاملًا زائد إصدار ، ("<", "2.31.0") تعني “أي إصدار تحت 2.31.0 متأثر” ، فمطابقته مع تبعية في الخطوة 3 هو مجرد تطبيق نفس مقارنة الزوج التي كتبتها بالفعل لـ parse_version. إبقاء severity كبيانات (لا كود) يعني أن الفرز به لاحقًا (الخطوة 5) يصبح ترتيبًا، لا غابة if-else. وبما أن القاعدة تعيش في JSON بدلًا من ملف Python، فتحديثها تعديل بيانات، لا تعديل كود.

🎯 الناتج المتوقع : ['CVE-2026-0001', 'CVE-2025-1234', 'CVE-2026-1000'].

🩹 إذا لم يعمل : إذا كانت القائمة فارغة، ففتحت load_cves ملفًا فارغًا أو ابتلع json.load عدم تطابق مسار. إذا أظهر ترتيب أعدادًا صحيحة (1،2)، فخزّن الملف قيمًا رقمية لكن المخطط يتوقع درجة خطورة كسلسلة دقيقة مثل "high". إذا “لا تنطبق” CVE جديدة مهما كان الإصدار، فحقل operator/version بها فيه خطأ إملائي.

2.2 تحقق من القاعدة

✅ قائمة التحقق

  • ✅ تُعيد load_cves() الـ CVEs الثلاثة مع id وpackage وoperator وversion وseverity وsummary.
  • ✅ قيم severity هي بالضبط critical / high / medium (يعتمد عليها الترتيب في الخطوة 5).
  • ✅ تعديل cve_db.json يغيّر معرفة الماسح دون لمس الكود.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تستهدف كل CVE هنا حزمة واحدة. تستخدم CVEs حقيقية نطاقات إصدارات (>=1.0, <1.5). ماذا يحدث لنموذجك أحادي المعامل عندما يُشحَن إصلاح 1.5.0 يعيد إدخال الخلل، وما تغيير المخطط الذي يعبّر عن معاملين؟
  • تُحسب درجة CVSS التي تقرر severity في الحياة الواقعية من ناقل الهجوم وقابلية الاستغلال. إذا خزّنتها كرقم بدلًا من critical/high/medium، ماذا يمكن لتقريرك فعله لا تستطيع درجة خطورة نصية؟

الخطوة 3: طابق التبعيات ضد القاعدة

مع تحليل التبعيات وتحميل الـ CVEs، يكون الفحص نفسه دالة مقارنة واحدة: “هل هذا الإصدار المثبت في النطاق المتأثر لهذه CVE؟” تطبّق هذه الخطوة على كل زوج تبعية/CVE.

3.1 اكتب منطق المطابقة

👟 تلميح البداية : اكتب مساعد in_range(dep_version, cve) واحدًا يستخدم سلسلة المعامل كقاموس لامدا مقارنة ، ثم حلّق كل تبعية × كل CVE.

# scanner.py (continued)
COMPARE = {
    "<": lambda a, b: a < b,
    "<=": lambda a, b: a <= b,
    ">": lambda a, b: a > b,
    ">=": lambda a, b: a >= b,
    "==": lambda a, b: a == b,
}

def in_range(dep: Dependency, cve: dict) -> bool:
    if dep.name != cve["package"]:
        return False
    cve_version = parse_version(cve["version"])
    return COMPARE[cve["operator"]](dep.version, cve_version)

def scan_dependencies(deps: list[Dependency], cves: list[dict]) -> list[dict]:
    findings = []
    for dep in deps:
        for cve in cves:
            if in_range(dep, cve):
                findings.append({
                    "type": "dependency",
                    "package": dep.name,
                    "installed": ".".join(str(p) for p in dep.version),
                    "cve": cve["id"],
                    "severity": cve["severity"],
                    "summary": cve["summary"],
                })
    return findings

for f in scan_dependencies(parse_requirements(), load_cves()):
    print(f["severity"], f["package"], f["installed"], f["cve"])

COMPARE كقاموس لامدا هو جملة التبديل التي لا تملكها Python: سلسلة المعامل هي فرع الكود، فوصول CVE بمعامل "<=" يعمل دون تعديل المطابق. الحراسة dep.name != cve["package"] تقصّر دائرة عدم تطابق الحزم قبل أي حساب إصدار، وهو ما يبقي الحلقة المزدوجة (تبعيات × CVEs) رخيصة على نطاق حقيقي. قاموس النتيجة هو العقد الذي تستهلكه كل مرحلة لاحقة ، يحمل درجة الخطورة لترتيب الخطوة 5 والملخص للقراءة البشرية.

🎯 الناتج المتوقع : ثلاث نتائج مرتبة بالبيانات، لا بالحظ: requests 2.28.1 CVE-2026-0001 وflask 2.2.5 CVE-2025-1234 وmakedata 0.9.0 CVE-2026-1000 ، مع بلاغ flask بدرجة critical.

🩹 إذا لم يعمل : إذا لم يتطابق requests رغم كونه < 2.31.0، فقارن in_range قيمة dep.version بسلسلة لم تُعالَج بـ parse_version. إذا طابق كل تبعية كل CVE، فالحراسة على الحزمة مفقودة. إذا اشتعل KeyError عند COMPARE[...]، فلدى CVE معامل لا تغطيه الخريطة الخماسية ، أضفه إلى COMPARE أو تحقق من القاعدة عند التحميل.

3.2 تحقق من مسح التبعيات

✅ قائمة التحقق

  • ✅ ثلاث نتائج تطابق بالضبط مثبّتات العينة الضعيفة؛ لا تُنتج urllib3 أيًا منها.
  • CVE-2026-0001 (< 2.31.0) لا تُطلق لفرض requests==2.31.0 تخيلي.
  • ✅ بيانات ترتيب درجة الخطورة (critical/high/medium) حاضرة على كل نتيجة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • التبعيات المثبتة بـ >= بدلًا من == تُعلن حدًا أدنى، لا تثبيتًا دقيقًا. ماذا يمكن لماسح أن يزعم فعلًا عن سطر flask>=2.0.0 مقابل سطر flask==2.2.5، وأيهما موضوع صادق لفحص إصدارات؟
  • يفترض المطابق أن لديك الإصدار المثبت الدقيق. أين تنسجم ملفات القفل (uv.lock، وpackage-lock.json) في هذا ، ماذا يشتري لك مسح ملف قفل لن يشتريه مسح requirements.txt؟

الخطوة 4: امسح المصدر بحثًا عن أنماط مضادة مع SAST

إصدارات التبعيات نمط فشل واحد؛ الكود هو الآخر. يفحص اختبار أمان التطبيقات الثابتة (SAST) المصدر بحثًا عن أنماط يجب ألا تُشحَن ، eval، وسلاسل القشرة (shell)، والأسرار المكتوبة بجد ، دون تشغيل البرنامج. تشغّل هذه الخطوة مرور SAST مصغّرًا على كل ملف .py في مجلد.

4.1 اكتب قائمة الأنماط والماسح

👟 تلميح البداية : أبقِ الأنماط كأزواج (regex, label, severity)، وامشِ ملفات .py بـ Path.rglob، وابحث في كل سطر ، مع وضع علامات على أرقام الأسطر ليصبح التقرير قابلاً للتنفيذ.

# scanner.py (continued)
ANTI_PATTERNS = [
    (re.compile(r"\beval\s*\("), "eval() on untrusted data", "high"),
    (re.compile(r"\bshell\s*=\s*True"), "subprocess with shell=True", "high"),
    (re.compile(r"password\s*=\s*['\"][^'\"]+['\"]"), "Hardcoded password", "critical"),
    (re.compile(r"\bassert\s+"), "assert used for runtime checks", "low"),
    (re.compile(r"\bTODO\b|\bFIXME\b"), "Unresolved marker", "low"),
]

def scan_source(path: str = "src") -> list[dict]:
    findings = []
    for file in Path(path).rglob("*.py"):
        for lineno, line in enumerate(Path(file).read_text().splitlines(), 1):
            for pattern, label, severity in ANTI_PATTERNS:
                if pattern.search(line):
                    findings.append({
                        "type": "sast",
                        "file": str(file),
                        "line": lineno,
                        "severity": severity,
                        "summary": label,
                    })
    return findings

src = Path("src")
src.mkdir(exist_ok=True)
(src / "fragile.py").write_text(
    "import subprocess\n"
    "data = eval(input('code: '))\n"
    "password = 'hunter2'\n"
    "def run(cmd):\n"
    "    return subprocess.run(cmd, shell=True)\n"
    "assert password != ''\n"
    "# TODO: remove before ship\n"
)

for f in scan_source():
    print(f["line"], f["severity"], f["summary"])

تصميم النمط-كبيانات (regex, label, severity) يعني أن إضافة فحص هي إضافة زوج واحد، لا إعادة كتابة الماسح ، تمامًا كيف تتيح لك الأدوات الحقيقية إسقاط قواعد مخصصة. يجد rglob("*.py") الملفات في مجلدات متداخلة، وتكرار splitlines() مع enumerate(..., 1) يعطي أرقام أسطر بشرية. تحمل النتيجة الملف والخط، وهو ما يحوّل قائمة مشاكل إلى مراجعة قابلة للالتحاق بـ diff. assert وTODO بدرجة low عن قصد: إنها نظافة في معظمها، أُدرجت لترى أن لدرجة الخطورة مدى.

🎯 الناتج المتوقع : خمس نتائج بأرقام أسطر 2–7 ، eval (high) في السطر 2، وكلمة مرور مكتوبة بجد (critical) في السطر 3، وshell=True (high) في السطر 5، وassert وTODO (low) في سطريهما.

🩹 إذا لم يعمل : إذا لم يطبع شيء، فلم يجد rglob("*.py") أي ملفات ، تحقق من مسار مجلد src. إذا أُطلقت قاعدة كلمة المرور المكتوبة على متغير اسمه password = getenv(...)، فإن regex ['\"][^'\"]+ يطابق استدعاء دالة أيضًا ، اشترط حرفًا اقتباسًا حرفيًا. إذا عدّت النتائج سطرًا مرتين، فطابقت عدة أنماط السطر نفسه (شرعي) لكنك تريد نتيجة ممثِّلة واحدة لكل سطر ، أزل التكرار بـ (file, line).

4.2 تحقق من SAST

✅ قائمة التحقق

  • ✅ تُعيد scan_source("src") خمس نتائج مع الملف والخط ودرجة الخطورة والملخص.
  • ✅ تُبلِّغ قاعدة كلمة المرور المكتوبة عن critical.
  • ✅ إضافة زوج (regex, label, severity) جديد إلى ANTI_PATTERNS يُنتج فورًا نتائج على الأسطر المطابقة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يرى regex SAST shell=True في تعليق وفي docstring أيضًا، لأنه لا يشغّل الكود أبدًا. أي صنف من الإيجابيات الكاذبة يُنتج هذا، وماذا يجب أن تفعل أداة حقيقية قائمة على محلل (AST) بدلًا من ذلك لتمييز التعليق عن الكود؟
  • eval مُعلَّمة high، لا critical أبدًا ، لكن eval على مدخلات مهاجم critical بسهولة. ما المعلومات التي ينقصها فحص سطر لترفع تلك الدرجة بمسؤولية؟

الخطوة 5: ابنِ التقرير المُرتَّب بدرجة الخطورة مع الإصلاحات

الخطوة الأخيرة تجعل الماسح مفيدًا: ادمج نتائج التبعيات وSAST، ورتّبها بدرجة الخطورة، وأرفق اقتراح ترقية حيث يوجد، واطبع ملخصًا بشريًا، واكتب الكل إلى report.json.

5.1 اكتب build_report ونقطة الدخول الرئيسية

👟 تلميح البداية : رتّب بخريطة رتبseverity (critical ←1 → low)، وألحق recommendation من خريطة إصدارات مثبّتة، واطبع العدّادات وأعلى النتائج، وأفرغ القائمة المدمجة إلى JSON.

# scanner.py (continued)
import sys

SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
FIXED_VERSIONS = {"requests": "2.32.0", "flask": "3.0.0", "makedata": "1.0.1"}
FIX_HINT = "upgrade to >= {}"

def build_report(deps: list[Dependency], cves: list[dict], source_dir: str = "src") -> list[dict]:
    findings = scan_dependencies(deps, cves) + scan_source(source_dir)
    for f in findings:
        if f["type"] == "dependency" and f["package"] in FIXED_VERSIONS:
            f["recommendation"] = FIX_HINT.format(FIXED_VERSIONS[f["package"]])
    findings.sort(key=lambda f: SEVERITY_RANK.get(f["severity"], 9))
    return findings

def main() -> None:
    report = build_report(parse_requirements(), load_cves())
    Path("report.json").write_text(json.dumps(report, indent=2))
    print(f"report.json: {len(report)} findings")
    for f in report:
        rec = f.get("recommendation", "")
        print(f"  [{f['severity']:>8}] {f['summary']:<40} {f['package'] if f['type']=='dependency' else f['file']}  {rec}")

if __name__ == "__main__":
    main()

يدمج build_report الماسحين ويسلّم القائمة إلى ترتيب درجة خطورة واحد ، SEVERITY_RANK.get(severity, 9) يقع على رقم كبير فتُرتَّب درجة خطورة غير متوقعة أخيرًا بدلًا من تحطيم الكود. تُرفق recommendation كبيانات فقط حيث يوجد إصدار مثبّت معروف، فتبقى “كيف أصلح هذا؟” صادقة لا تخمينية. يظهر sys فقط ليغلق main خلف __name__، فلا يشغّل import scanner في اختبار الفحص أبدًا. JSON في النهاية هو العقد المقروء آليًا الذي سيقرؤه خط أنابيب CI (المستهلك الطبيعي لماسح).

🎯 الناتج المتوقع : report.json contains 8 findings؛ تبدأ القائمة المطبوعة بعنصري critical (RCE الخاص بـ flask وكلمة المرور المكتوبة)، ثم highs، ثم lows، مع توصيات ترقية على نتائج التبعيات الثلاث.

🩹 إذا لم يعمل : إذا بدأ التقرير بـ lows، فاستعلامات SEVERITY_RANK تفشل وفرزت كل درجة خطورة إلى مجموعة 9. إذا لم يظهر recommendation أبدًا، فلدى FIXED_VERSIONS مفتاح حزمة غير موجود في النتائج (تختلف حالة الأحرف ، تطبيع الأسماء عند التحليل يصلحها). إذا كتب report.json لكن محلل CI اختنق به، فنتيجة ينقصها أحد الحقول التي يتوقعها المحلل ، أبقِ عقد القاموس متطابقًا عبر نوعَي الماسح.

5.2 تحقق من الماسح النهائي

✅ قائمة التحقق

  • ✅ يكتب uv run python scanner.py ملف report.json بـ 8 نتائج، من الحرجة أولًا.
  • ✅ تحمل نتائج التبعيات الثلاث كلٌّ توصية ترقية ملموسة.
  • ✅ تحمل نتائج SAST file/line؛ تحمل نتائج التبعيات package/installed.
  • ✅ تشغيل الماسح على src الخاصة به يضيف بالضبط النتائج التي تتوقعها ، ماسح يعلّم على نفسه يعمل.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يُرتِّب التقرير بدرجة الخطورة لكنه يُبقي قابلية الوصول لثغرة خارج ترتيبها. أيهما أهم عند الفرز ، الدرجة وحدها، أم الدرجة × “هل هذا في المسار الساخن أصلًا؟” وما العمود الذي ستضيفه لترميز ذلك؟
  • ماسح يُبلِّغ عن كل شيء يدرّب الفرق على تجاهل كل شيء. ما (في بيانات هذا التقرير) ستعرضه بشكل مختلف لفريق يحصل على 200 نتيجة شهريًا مقابل فريق يحصل على 2 ، ولماذا يقرر واجهة الفرز إن كان الماسح يعيش أم يموت؟

⚠️ المآزق الشائعة

  • أزواج إصدارات لا تُحلَّل. إصدار نصي مثل "1.10.0rc1" يقتل int(part) ويسقط الفحص كله. الإصلاح: جرّد اللواحق (split("-")[0]، وأسقط rcN زائدة) قبل التحويل، ودع المثبّتات المشوّهة تُسجَّل بدلًا من إيقاف التنفيذ.
  • انحراف حالة الأحرف على أسماء الحزم. Requests مقابل requests مقابل requests[socks] كلها تفشل في حراسة التطابق الدقيق وتفقد الـ CVEs بصمت. الإصلاح: طبّع الأسماء (وأسقط الإضافات مثل [socks]) مرة واحدة عند التحليل.
  • الاعتماد على requirements.txt للحقيقة المثبتة. تعبّر المثبّتات عن نية، لا بالضرورة النسخة المشغّلة. الإصلاح: إذا كان للبيئة ملف قفل أو مخرجات pip freeze، فامسح ذاك بدلًا من ذلك ، إنه المخزون الحقيقي.
  • regex SAST يصرخ من تعليق. shell=True في تعليق ملف سياسة، لا ثقب. الإصلاح: بلّغ عنه لكن دع إنسانًا يزنّه، وفضّل فحوصات بأسلوب AST (أشجار الأسماء، وحرفية النصوص) لأي شيء ستعمل عليه تلقائيًا.
  • درجات خطورة غير مُرتَّبة تُرزق أخيرة بالصدفة. CVE بدرجة "critial" مكتوبة خطأ تغرق إلى القاع بدلًا من القمة. الإصلاح: SEVERITY_RANK.get(severity, 9) هو الاحتياط الآمن، زائد تحذير عند رؤية درجة خطورة غير معروفة.

ما بنيته للتو

ماسح ثغرات عامل بالمكتبة القياسية: تحليل تبعيات منظم، وقاعدة بيانات CVE من JSON مع مطابقة نطاقات، وSAST قائم على regex على المصدر، وتقرير واحد مُرتَّب بدرجة الخطورة مع توصيات ترقية مكتوبة إلى JSON. المهارة القابلة للنقل هي تحويل “الأمان” إلى عمليات بيانات: مقارنة نطاق الإصدارات، ومطابقة الأنماط، وترتيب درجة الخطورة هي الحركات نفسها خلف روبوتات التبعيات وقواعد الـ lint وكل أداة “افحص مشروعي” ، لقد بنيت الآن ثلاثة منها من الصفر.

شغّل نسخة أكمل دون أي إعداد محلي

examples/vulnerability-scanner/ في مستودع الدورة نسخة أكمل من الكود أعلاه، مع دعم ملفات القفل ومشروع عينة أغنى للمسح. استنسخه، أو افتح المستودع كاملًا في GitHub Codespace، وشغّله من هناك.

إلى أين تذهب من هنا

  • وسّع parse_requirements لقراءة جداول [project] من pyproject.toml، فيغطي الماسح مشاريع uv/pip، لا مجرد requirements.txt القديمة.
  • أضف وضع --ast يمشي في شجرة AST بدلًا من السطور ويعلّم eval/exec فقط عندما تصلان فعلًا إلى مدخلات غير موثوقة ، إيجابيات كاذبة أقل، نفس التغطية.
  • وصّل درجة خطورة CVE (عبر فكرة CVSS الرقمية من سؤال الخطوة 2) واطبع إجمالي مخاطر المشروع كعنوان الملخص.
  • اجعل main يُعيد رمز خروج غير صفري عندما توجد أي نتيجة critical/high، فيفشل عمل CI الذي يشغّل الماسح البناء فعلًا.

شارك مشروعك مع الصف

بَنيت شيئًا تفتخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وملف README الخاص به يحتوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request، حتى لو لم تستخدم git من قبل: عمل fork للمستودع، وإنشاء فرع، والالتزام بملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترَض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.