فاحص الثغرات الأمنية
تبدو ماسحات الأمان كالسحر حتى ترى أجزاءها: قائمة إصدارات معروفة بأنها سيئة (تلك هي قاعدة بيانات CVE)، وفحص “هل نسختي في نطاق سيئ؟” (ذلك حساب الإصدارات)، ومرور على أنماط يجب ألا تكون في الكود المُشحَن (ذلك SAST). يبني هذا المشروع الثلاثة في Python خالص ، حلّل requirements.txt إلى تبعيات منظمة، وطابقها مع قاعدة بيانات CVE صغيرة بدرجات خطورة، وافحص ملفات الكود بحثًا عن الأنماط المضادة الخطرة، وأصدر تقريرًا واحدًا مُرتَّبًا بدرجة الخطورة مع اقتراحات الترقية. لن يغطي سلسلة التوريد كلها؛ سيُزيل الغموض عن كيف يفكر هذا النوع من الماسحات بالضبط.
هذا يفترض Python 101 وقليلًا من regex ، لا شيء من تحليل البيانات مطلوب. هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للقائمة الكاملة والنامية.
🎯 ما ستفعله
- تحليل
requirements.txtمُثبَّت في تبعيات منظمة. - نمذجة قاعدة بيانات CVE صغيرة بنطاقات إصدارات متأثرة ودرجات خطورة.
- مطابقة الإصدارات المثبتة ضد النطاقات وجمع النتائج.
- تشغيل SAST قائم على regex على ملفات الكود بحثًا عن الأنماط الخطرة.
- دمج الاثنين في تقرير مُرتَّب بدرجة الخطورة مع اقتراحات إصلاح.
أين تُشغّل هذا
محليًا باستخدام uv هو المسار الأساسي. الماسح مكتبة قياسية خالصة، لكن قيمته الحقيقية في توجيهه إلى requirements.txt وsrc/ لمشروعك أنت ، ملفات تعيش على قرص تملكه.
Google Colab وKaggle Notebooks وBinder تشغّل كل خلية بشكل متطابق (المكتبة القياسية فقط)، ويشحن دفتر الملاحظات المثال requirements.txt وfragile.py حاملَين بداخله، فترى الفحص الكامل ضد مشروع عينة ثابت. التحفظ الصادق: دفتر ملاحظات يفحص تبعيات مستودع الدورة نفسه كان سيُريك المحرك نفسه ضد الشيء الحقيقي، لكن نظام ملفاته المؤقت يجعل “افحص مشروعي” حركة محلية فقط. استخدم الشارات لرؤية المحرك؛ وشغّل uv للتدقيق الحقيقي.
الإعداد
أنشئ المشروع. يستخدم الماسح المكتبة القياسية فقط ، 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 خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.