ما الذي سنبنيه
المراجع مشروع Python صغير ببضعة أجزاء واضحة:| الجزء | ما يفعله |
|---|---|
| نماذج Pydantic | تعرّف Evidence وFinding وChain، وتمنحنا حدود تحقق صارمة بين LLM وبقية البرنامج |
| عميل Venice | يلفّ OpenAI Python SDK مع توجيهه إلى نقطة نهاية Venice المتوافقة مع OpenAI |
| خريطة المستودع بـ AST | يمشي شجرة الهدف بوحدة ast في Python ويبني خريطة محدّدة لكل رموز كل وحدة العامة وحواف الاستيراد |
| وكيل الفحص (Scanner) | يقرأ ملف Python واحدًا في كل مرة بالإضافة إلى شريحة الحيّ (neighborhood) من خريطة المستودع، ويُصدر نتائج ثغرات ذرّية مع أدلة file:line |
| وكيل التسلسل (Chainer) | يقرأ اتحاد النتائج بالإضافة إلى خريطة مستودع مكثّفة، ويُصدر سلاسل استغلال تجمع نتيجتين أو أكثر |
| محقّق المراجع | يُسقط أي سلسلة تُشير إلى معرّف نتيجة لم يُنتجه Scanner، أو تُسمي ملفًا لم يأتِ منه أي من نتائجها المُشار إليها |
| تقرير Markdown | يعرض النتائج والسلاسل في تقرير قابل للقراءة من البشر |
| CLI | يربط كل شيء معًا باستخدام Typer |
- امشِ في دليل الهدف لملفات
.py. - ابنِ خريطة مستودع محدّدة (الاستيرادات، الرموز العامة، التواقيع).
- لكل ملف، أرسل إلى Scanner مصدره بالإضافة إلى شريحة الحيّ من الخريطة، واجمع النتائج الذرّية.
- أرسل اتحاد النتائج بالإضافة إلى خريطة المستودع المكثّفة إلى Chainer واجمع سلاسل الاستغلال.
- أسقط أي سلسلة تُشير إلى معرّف نتيجة لم يُنتجه Scanner، أو تُسمي ملفًا لم يأتِ منه أي من نتائجها المُشار إليها.
- اكتب تقرير Markdown.
ast في Python ونبني خريطة بنيوية. يرى Scanner حيًّا لكل ملف (مَن يستورد من هذا الملف، ما يستورده هذا الملف، تواقيع تلك الرموز الخارجية). يرى Chainer خريطة كاملة مكثّفة (كل وحدة، كل رمز عام، كل حافة استيراد، بدون مصدر). هذا أقل قدر من هندسة السياق الذي وجدنا أنه يسمح لـ Chainer ببناء سلاسل يعبر تدفق بياناتها حدود الوحدات، دون دفع تكلفة الرموز لحشو قاعدة الشيفرة بالكامل في كل تعليمة.
المتطلبات المسبقة
- Python 3.12+
- مفتاح Venice API من venice.ai
- إلمام أساسي بـ Pydantic، ووحدة
astفي Python، و OpenAI Python SDK
uv لإدارة التبعيات، لكن بيئة افتراضية عادية تعمل بنفس الكفاءة.
إعداد المشروع
أنشئ مشروعًا جديدًا وثبّت التبعيات:pip، أنشئ بيئة افتراضية بدلًا من ذلك:
.env للتطوير المحلي:
src/venice_security_reviewer/ لإبقائه قابلًا للاستيراد كحزمة، مع التعليمات تحت prompts/ في جذر المستودع كي يمكن مراجعتها ومقارنتها كأي مصدر آخر:
إعداد عميل Venice
Venice متوافق مع OpenAI، لذا يمكننا استخدام OpenAI Python SDK الرسمي وتوجيهbase_url إلى Venice. تركيز إنشاء العميل في ملف واحد يعني أن باقي الشيفرة لا تحتاج إلى معرفة المزوّد الذي تخاطبه: تبديل الخلفية لا يلامس إلا هذه الوحدة الوحيدة.
أنشئ src/venice_security_reviewer/client.py:
- نلجأ افتراضيًا إلى
zai-org-glm-5لأنه نموذج Venice قوي عام الغرض، لكن يمكنك تجاوزه عبر متغير البيئةVENICE_MODEL. لقواعد شيفرة أكبر أو أكثر دقّة، تبديل نموذج أقوى يمكن أن يجعل Chainer أفضل بشكل ملحوظ في جودة السرد. - يُرجع
build_clientالعميل ومعرّف النموذج، لذا لا يضطر المتصلون لقراءة متغيرات البيئة بأنفسهم ويمكن للاختبارات حقن تكوين وهمي دون monkeypatching.
تعريف نماذج البيانات
الهدف الكامل من استخدام Pydantic هنا، بدلًا من تمرير قواميس خام، هو الحصول على حدود تحقق صارمة بين LLM وبقية البرنامج. إذا أعاد النموذج JSON مشوّهًا أو اخترع معرّف نتيجة غير موجود، يفشل التحليل بصخب ولا نُمرّر الهلوسة إلى التقرير أبدًا. أنشئsrc/venice_security_reviewer/models.py:
Finding.idوChain.idمقيّدتان بتعبير عادي مثلF-001،C-001. إذا حاول النموذج الإبداع في التنسيق، يفشل التحقق.Chain.findingsيتطلب مدخلَيْن على الأقل: «سلسلة» من نتيجة واحدة هي مجرد نتيجة.Chain.severityمقتصرة علىhighأوcritical. مزيج نتائج لا يرفع الأثر فوق أعلى شدّة فردية ليس سلسلة تستحق الإبلاغ عنها.Evidenceيفرض أنend_line >= start_lineبحيث لا يستطيع النموذج إرجاع نطاقات أسطر بلا معنى.
models.py:
بناء خريطة المستودع بـ AST
خريطة المستودع هي الهيكل البنيوي لقاعدة شيفرة Python: السطح العام لكل وحدة، كل حافة استيراد، وفهرس عكسي من «وحدة M» إلى «الوحدات التي تستورد من M». تُبنى مرة واحدة لكل تشغيل فحص باستخدامast في Python، أبدًا عبر التنفيذ، لذا فهي آمنة للتشغيل على شيفرة عدائية: المحلّل لا يستورد أو يستدعي شيئًا من الشجرة المفحوصة.
سنستهلك الخريطة بشكلين. يحصل Scanner على شريحة حيّ لكل ملف بحيث تبقى تعليماته محدودة الحجم. ويحصل Chainer على خريطة كاملة مكثّفة ليتمكن من بناء سلاسل عبر الملفات.
أنشئ src/venice_security_reviewer/repo_map.py وابدأ بنماذج Pydantic التي تصف الخريطة:
__all__ صريحة إن كانت موجودة. تواقيع الدوال وترويسات الفئات تُعرض كسلاسل مدمجة يستطيع LLM قراءتها مباشرة:
_SIGNATURE_CHAR_CAP البالغ 200 على التواقيع الواقعية النموذجية (بما فيها تلميحات الأنواع) مع منع الحالات المرضية كاتحاد مكتوب من 200 سطر يُفجّر التعليمة.
بعد ذلك، المُستخرج الذي يسحب البيانات البنيوية من وحدة محلّلة. نتعامل مع ast.FunctionDef وast.ClassDef وast.Assign وast.AnnAssign على المستوى الأعلى للثوابت، وكل من ast.Import وast.ImportFrom لحواف الاستيراد. الاستيرادات النسبية تُحلّ إلى صيغتها المطلقة بنقاط بحيث يستطيع Chainer مطابقتها بأسماء الوحدات لاحقًا:
tree.body ويُصدر مدخلات SymbolDef وImportEdge لكل عقدة من المستوى الأعلى. تغطي دالة _extract في repo_map.py للمستودع المرجعي التنفيذ الكامل. الشكل الذي يخرج هو قائمة من كائنات ModuleEntry، واحد لكل ملف.
الجزء المثير هو ما نفعله بتلك المدخلات. لفّها في RepoMap بطريقتين موجّهتين للمستهلك:
neighborhood(path) هو ما يستدعيه Scanner لكل ملف. يُرجع كائن ModuleNeighborhood يحتوي الوحدة نفسها، وكل وحدة أخرى تستورد منها، وكل رمز داخل المستودع تستورده من غيرها (مع تواقيعها المُحلّلة). يمنح ذلك Scanner سياقًا كافيًا لرصد نتائج لا تكون واضحة إلا في سياق متعدد الملفات، دون جرّ قاعدة الشيفرة كاملة إلى التعليمة.
condensed_dict() هو ما يحصل عليه Chainer. تُسقط المقاطع والتواقيع؛ ولا يبقى سوى المسارات وأسماء الوحدات والصادرات العامة وحواف الاستيراد. هذا أصغر تمثيل لا يزال يتيح لـ Chainer التفكير في تدفق البيانات بين الوحدات.
أخيرًا، نقطة الدخول التي تبني الشيء بأكمله:
كتابة وكيل Scanner
يمشي Scanner مسار الهدف، ويلتقط ملفات مصدر Python، ويطلب من Venice تحديد ثغرات ذرّية ملفًا واحدًا في كل مرة. الفحص لكل ملف يبقي التعليمة صغيرة ويجعل الإخفاقات معزولة: ملف سيئ واحد لا يقتل التشغيل كله. سنبقي التعليمة نفسها في ملف منفصل بحيث يمكن مراجعتها ومقارنتها كأي مصدر آخر. أنشئprompts/scanner.md:
- نُخبر النموذج بإصدار JSON فقط، بلا نثر أو أسوار. تدعم OpenAI SDK معامل
response_format={"type": "json_object"}يفرض هذا على جانب الواجهة، لكن تعزيزه في التعليمة يقلّل الحالات الحدّية. - نمنع Scanner صراحة من إنتاج سلاسل بين الملفات. السلاسل وظيفة Chainer، وطلب Scanner القيام بكليهما يُشوّش المسؤولية.
- نطلب نسخ المقطع حرفيًا. هذا يعني أن التقرير يستطيع اقتباس البايتات الدقيقة التي يدّعي النموذج رؤيتها، ويستطيع المراجع التحقق العشوائي من نتيجة بمقارنة المقطع بالمصدر.
src/venice_security_reviewer/scanner.py وابدأ بمتنقّل الملفات ومحمّل التعليمة:
MAX_FILE_BYTES حدّ سلامة. وراء ~200 كيلوبايت نتخطى بدلًا من إرسال تعليمة ضخمة من المرجح أنها مكلفة ومنخفضة الجودة.
القطعة التالية هي باني التعليمة. يستخدم القالب {filename} و{source} و{neighborhood} كنائبات؛ نستخدم str.replace بدلًا من .format() لأن القالب يحوي أمثلة JSON بأقواس مجعّدة حرفية:
F-001 لكل ملف، لكن Chainer يحتاج إلى الإشارة إلى نتائج عبر المستودع كله. نعيد إصدار المعرّفات مقابل عدّاد رتيب بحيث تكون فريدة عالميًا:
response_format={"type": "json_object"} ودرجة حرارة منخفضة، ونحلّل النتيجة:
- نُصلح مسار ملف الأدلة ليكون نسبيًا إلى
repo_rootبعد التحليل، لأن النموذج يردّد اسم الملف الذي أعطيناه له لكننا نريد شكلًا قانونيًا واحدًا عبر التقرير كله. temperature=0.1منخفضة عمدًا. نريد Scanner أن يكون محافظًا ومتسقًا عبر التشغيلات؛ الإبداع وظيفة Chainer.
كتابة وكيل Chainer
يأخذ Chainer اتحاد نتائج Scanner بالإضافة إلى خريطة المستودع المكثّفة، ويسأل Venice ما إذا كانت أي من النتائج تتجمع في سلسلة استغلال حقيقية. توجد حمايتان محدّدتان بين LLM والتقرير:- يجب أن تُشير كل سلسلة فقط إلى معرّفات نتائج أنتجها Scanner.
- يجب أن تدّعي كل سلسلة فقط ملفات تلامس أدلة نتيجة مشار إليها واحدة على الأقل.
prompts/chainer.md. جوهرها يبدو هكذا:
files_involved، وأهم شيء: متى لا نُسلسل. إخبار النموذج بأن «من الصحيح والمتوقع أن قواعد شيفرة كثيرة تحوي نتائج لا تتسلسل» هو ما يمنعه من اختراع سلاسل ليبدو منتجًا.
الآن شيفرة الوكيل. أنشئ src/venice_security_reviewer/chainer.py:
MAX_REPO_MAP_CHARS = 8000 سقف ناعم لكتلة خريطة المستودع المعروضة كـ JSON في تعليمة Chainer. عند نحو 4 أحرف لكل رمز، هذا ~2000 رمز، يجلس بشكل مريح داخل نافذة سياق أي نموذج Venice حتى مع وجود النتائج وميزانية السرد فوقها.
نُسلسل النتائج إلى كتلة JSON مدمجة. لاحظ أننا نُجرّد snippet من الأدلة هنا عمدًا: لا يحتاج Chainer إلى بايتات خام ليُقرّر ما إذا كانت نتيجتان تتجمعان، وتضمينها يضاعف تقريبًا تكلفة الرموز على قواعد الشيفرة الواقعية:
_pruned و_kept و_total، بحيث تستطيع تعليمة Chainer تحذير النموذج عندما تكون الخريطة قد قُصّت.
تحليل الاستجابة بنفس شكل Scanner: فكّ، حقّق كل سلسلة عبر Pydantic، أسقط المدخلات المشوّهة:
- نخرج قبل استدعاء النموذج عندما يكون هناك أقل من نتيجتين. لا يمكنك تسلسل نتيجة واحدة، وتخطّي الاستدعاء يعني أننا لا نُحرق رموزًا على نتيجة فارغة مضمونة.
temperature=0.2أعلى قليلًا من 0.1 لـ Scanner. يستفيد Chainer من لمسة إبداع إضافية لرصد التجمعات غير الواضحة، لكننا لا نزال نريده مرتكزًا على النتائج والخريطة التي أُعطي إياها.- بعد التحليل، يعمل
validate_chain_referencesالتحقق المتقاطع المحدّد الذي كتبناه سابقًا. كل ما ينجو آمن للعرض؛ وكل ما لا ينجو يُسجّل لذا يعرف المشغّل أن النموذج حاول اختراع شيء.
عرض تقرير Markdown
إبقاء العرض منفصلًا عن منطق الوكيل يعني أن كائناتFinding وChain نفسها يمكن تغذيتها لاحقًا إلى صيغ أخرى (JSON، SARIF، HTML) دون لمس Scanner أو Chainer.
سنستخدم Jinja2 مع ملف قالب صغير. أنشئ src/venice_security_reviewer/templates/report.md.j2:
src/venice_security_reviewer/report.py:
.html مستقبلية حسب الامتداد.
ربط واجهة CLI
CLI هو المنسّق: ابنِ خريطة المستودع، افحص، سلسل، اعرض. سنستخدم Typer لتولّي تحليل الوسائط و Rich لطباعة جدول ملخص جميل. أنشئsrc/venice_security_reviewer/cli.py:
pyproject.toml:
اختبار الحمايات
اعتمدنا بشدّة على فكرة واحدة طوال هذا البناء: الحمايات المحدّدة هي ما يفصل أداة أمان مفيدة عن أداة خاطئة بثقة. هذا الادعاء يستحق القول فقط إذا استطعنا إثبات أن الحمايات تصمد فعلًا، لذا فإن أكثر الاختبارات قيمة في هذا المشروع لا تستدعي Venice أبدًا. إنها تُحكم حدود Pydantic وأنابيب تجميع التعليمات، مما يعني أنها تعمل خارج الاتصال، في أجزاء من الألفية، بلا مفتاح API وبلا تكلفة رموز. أضف تبعيات التطوير أولًا:tests/test_models.py:
F-###، و«سلسلة» من نتيجة واحدة. إذا توقّف أي منها عن إطلاق استثناء، فإن فئة كاملة من الهلوسة قد أصبحت ممكنة مرة أخرى بهدوء.
أهم اختبار يغطي محقّق المرجع المتبادل، لأنها الدالة التي تُسقط فعلًا السلاسل المخترعة:
F-999 لم يُنتجها Scanner أبدًا، لذا فالسلسلة التي تُشير إليها تنتهي في dropped ولا تصل أبدًا إلى التقرير. الاختبار المرافق في المستودع المرجعي، test_validate_chain_references_drops_unknown_files، يفعل الشيء نفسه لسلسلة تدّعي ملفًا لم يأتِ منه أي من نتائجها.
الشيء الثاني الذي يستحق الاختبار هو الأنابيب التي تُغذّي Chainer. من السهل إعادة هيكلة تجميع التعليمة والتوقف بصمت عن تمرير السياق المتعدد الملفات، عند هذه النقطة سيستمر Chainer في العمل لكنه سيصبح أسوأ بهدوء. يبني هذا الاختبار ثبت وحدتين، يعرض التعليمة، ويؤكّد أن المعلومات بين الملفات موجودة فعلًا، مرة أخرى بلا جولة إلى Venice. أنشئ tests/test_cross_file_chain.py:
tests/test_scanner_parse.py و tests/test_chainer_parse.py و tests/test_repo_map.py، التي تغطي حالات حدّية لتحليل JSON (المدخلات المشوّهة تُسقط بدلًا من إعطاب التشغيل) وباني خريطة المستودع بـ AST.
تشغيل المشروع
لتجربته على قاعدة شيفرة حقيقية، وجّه CLI إلى دليل مصدر Python:pip install -e . ثم نفّذ venice-security-reviewer scan path/to/your/code.
يبدو الإخراج هكذا تقريبًا:
examples/vulnerable_app— تطبيق Flask متعدد الملفات بثلاث نتائج «منخفضة»، تتجمع اثنتان منها في سلسلة تصعيد امتيازات حرجة عبر الملفات. يختبر ما إذا كان Chainer انتقائيًا فيما يجمعه.examples/url_preview— جالب URL متعدد الملفات بقائمة سماح دفاعية لا تُطبّق لكل تكرار. يختبر التفكير بتدفق البيانات بين الملفات مدمجًا مع طوبولوجيا النشر (عناوين IP المحلية الرابط هي بوابات اعتماد سحابية).examples/csv_query— مصفاة CSV من ملف واحد بهروب صندوقevalعبر__class__.__base__.__subclasses__(). يختبر التفكير على مستوى اللغة بدلًا من تدفق HTTP.examples/webhook_handler— متحقّق HMAC من ملف واحد بثغرة تفاضلية في محلل JSON. يختبر التفكير عبر مواصفات متعددة.
chainer referenced N unknown finding id(s) or file(s); chains dropped، فهذا محقّق المرجع المتبادل يُمسك النموذج متلبسًا باختراع سلسلة. السلاسل المُسقطة لا تصل أبدًا إلى التقرير؛ تحصل فقط على تحذير يمكنك استخدامه لتعديل التعليمة أو أخذ عينات تشغيلات Chainer إضافية.
توسيع هذا المثال
شكل الوكيلين يعمّم بشكل جيد. بعض الاتجاهات الجديرة بالاستكشاف:- لغات أكثر. Scanner لا يتقيّد بلغة على مستوى التعليمة؛ باني AST هو ما يتقيّد بـ Python. بدّل إلى
tree-sitterويمكنك بناء نفس أشكال الحيّ/الخريطة المكثّفة لـ TypeScript أو Go أو Rust. - وكيل ثالث للإصلاحات. بمجرد أن تكون لديك سلسلة، طلب وكيل Patcher صياغة diff موحّد يُعطّل إحدى النتائج المكوّنة هو خطوة صغيرة. حقّق الـ diff عبر Pydantic مقابل نفس مجموعة ملفات الأدلة وستحصل على نفس حارس الهلوسة مجانًا.
- صيغ الإخراج.
render_reportهو المكان الوحيد الذي يعرف عن Markdown. أضف عارض SARIF ويمكن للنتائج نفسها أن تُسقط في فحص شيفرة GitHub. أضف عارض JSON ويمكنك توجيه النتائج إلى نظام مصبّ. - التخزين المؤقت بهاش الملف. استدعاءات Scanner لكل ملف مستقلة ومتعادية. التخزين المؤقت بـ
(file_hash, prompt_hash, model)يعني أن إعادة فحص مستودع تغيّر فيه ملف واحد تعيد تشغيل Scanner فقط على ذلك الملف. - أخذ عينات لـ Chainer. للتشغيلات عالية المخاطر، استدعِ Chainer N مرة بدرجة حرارة أعلى قليلًا واقطع النتائج. السلاسل التي يجدها النموذج باستمرار من المرجح أن تكون حقيقية؛ السلاسل التي يجدها مرة ولا يجدها مرة أخرى من المرجح أنها ضوضاء.
- نماذج أقوى.
zai-org-glm-5هو الافتراضي لأنه يحقّق توازنًا جيدًا بين التكلفة والجودة للتفكير التوافقي، لكن لقواعد الشيفرة الأصعب يمكن أن يجعل تبديل نموذج Venice أقوى (عبرVENICE_MODEL) سرديات Chainer أكثر إحكامًا بشكل ملحوظ.