diff --git a/SovereignAI-Starter/LICENSE_REVIEW_2026-10.md b/SovereignAI-Starter/LICENSE_REVIEW_2026-10.md index e6d8dc4..9732ce5 100644 --- a/SovereignAI-Starter/LICENSE_REVIEW_2026-10.md +++ b/SovereignAI-Starter/LICENSE_REVIEW_2026-10.md @@ -1,6 +1,6 @@ # مراجعة أولية للتراخيص وتدفق البيانات -**تاريخ اللقطة:** 2026-10-04 +**تاريخ اللقطة:** 2026-10-07 **الغرض:** جرد هندسي للنماذج المثبتة ومزوّد التفريغ الصوتي. هذه ليست موافقة قانونية أو إقرارًا بأن المنتج جاهز للبيع. أُخذت أسماء الوسوم والبصمات من `ollama list`، وفُحص النص المضمّن لكل وسم عبر `ollama show --license`. ## النماذج المحلية المثبتة @@ -23,6 +23,16 @@ - [Groq: Your Data in GroqCloud](https://console.groq.com/docs/your-data) - [Groq Speech to Text](https://console.groq.com/docs/speech-to-text) +## ملفات PDFium وأوزان OCR المحلية — 2026-10-07 + +جُمعت بصمات ملفات التشغيل الفعلية من البيئة المحلية في `sbom/component-inventory.json`، بدل اعتبار اسم الحزمة وحده جردًا كافيًا: + +- `pypdfium2` 5.13.0 يحمّل `pdfium.dll` بحجم 7,260,672 بايت وبصمة SHA-256 `fb898a1f5ace57805834f390407500bdb6ef93eff326a252ad334a8aae809d8e`. الـwheel المثبت يضم 16 ملف `BUILD_LICENSES` لبناء Windows x64، وسُجلت بصمة كل ملف. توثيق [pypdfium2 الرسمي](https://github.com/pypdfium2-team/pypdfium2) يذكر أن الحزمة نفسها تحت Apache-2.0 أو BSD-3-Clause، وأن PDFium تحت رخصة BSD-style مع تراخيص تبعيات أخرى يجب إرفاقها مع الملفات الثنائية. هذا الجرد يحفظ أدلة الإشعار ولا يصنف كل إشعار تبعي أو يوافق على إعادة التوزيع. +- `arabic.pth` و`craft_mlt_25k.pth` المحليان يطابقان MD5 المنشور في إعداد EasyOCR 1.7.2: [arabic.zip](https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/arabic.zip) و[craft_mlt_25k.zip](https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/craft_mlt_25k.zip). بصمات SHA-256 والحجم ونتيجة المطابقة محفوظة في SBOM. رخصة [مستودع EasyOCR](https://github.com/JaidedAI/EasyOCR) لا تكفي وحدها لإثبات شروط إعادة توزيع أوزان التدريب؛ بقيت حالة ترخيص كل وزن «تحتاج مراجعة بشرية» حتى الحصول على دليل خاص بالأوزان ومصادر بيانات التدريب. +- `english_g2.pth` غير موجود في مجلد أوزان OCR المحلي وقت الجرد رغم أن `local_ocr.py` يهيئ EasyOCR للغتين العربية والإنجليزية. EasyOCR قد ينزله تلقائيًا عند أول تهيئة؛ يجب تنزيله والتحقق من بصمته ومصدره وإشعار ترخيصه قبل تثبيت حزمة تشغيل تجارية، أو توفير سياسة تنزيل منفصلة وموافقة واضحة. + +هذه اللقطة تصف ملفات الجهاز الحالي فقط. يجب إعادة الجرد من مجلد بناء الإصدار النهائي، لا نسخ هذه البصمات باعتبارها محتويات كل إصدار. + ## ما لم يُراجع بعد قبل التوزيع - جرد مكونات التطوير متاح الآن في [`sbom/component-inventory.json`](sbom/component-inventory.json)، مع طريقة التوليد والحدود في [`sbom/README.md`](sbom/README.md). هذا لا يغلق مراجعة التراخيص: 101 ملف رخصة Flutter تحتاج تصنيفًا يدويًا، كما أن الجرد ليس خاصًا بإصدار إنتاج نهائي. diff --git a/SovereignAI-Starter/ROADMAP.md b/SovereignAI-Starter/ROADMAP.md index 0ab70c0..ceb7128 100644 --- a/SovereignAI-Starter/ROADMAP.md +++ b/SovereignAI-Starter/ROADMAP.md @@ -4,6 +4,7 @@ ## الحالة الحالية — 2026-10-07 +- 2026-10-07 — استكمال جرد ملفات التشغيل لتقييم الإصدار التجاري: وسّع `scripts/generate_component_inventory.py` ليضيف binary PDFium وأدلة إشعاراته، وأوزان EasyOCR الموجودة كـCycloneDX components مع SHA-256 ومصدر/معرّف upstream وحالة الترخيص. ولّد SBOM الحالي 157 مكوّنًا: 50 Python و104 Flutter وPDFium واحد ووزنا OCR؛ تطابق ملفا OCR مع MD5 من إعداد EasyOCR 1.7.2، وسُجل غياب `english_g2.pth` المتوقع، مع تجزئة 16 إشعار بناء PDFium. اجتازت اختبارات المولّد 6/6 و`compileall` و`git diff --check`. أُحدّثت `sbom/README.md` و`LICENSE_REVIEW_2026-10.md`. بقي تصنيف 101 إشعار Flutter، تراخيص OCR/PDFium والنماذج، التحقق من مخطط CycloneDX، ومطابقة محتويات إصدار تجاري نهائي؛ هذا تقدم في الجرد وليس موافقة توزيع. - 2026-10-07 — إعادة فحص نافذة Windows: API على `127.0.0.1:8000` ما زالت سليمة (`/health` يعرض Gemma 4 وSQLite والوكيل)، لكن Flutter Web على 5301 متوقف. أداة Computer Use البديلة أعادت قائمة بلا نوافذ Mithqal؛ فحص العمليات وجد PID `55860` من بناء `ui-verify-20261004` مستجيبًا وله HWND، إلى جانب ست عمليات Debug أخرى. طلب فتح الملف التنفيذي الحالي أعاد `launched app did not expose a targetable window: Mithqal AI`؛ لذلك لم أرسل أي نقر أو كتابة إلى نافذة غير مستهدفة. محاولة `flutter --version` بقيت معلقة مع وجود Dart من SDK المعزول (PID 44892)، فأوقفت أمر الفحص وحده ولم أوقف عمليات التطبيق أو Dart. لا تزال تجربة Windows اليدوية غير منجزة؛ يلزم حل تعارض إتاحة النوافذ/قفل SDK أو تشغيل جلسة Debug واحدة معزولة ثم إعادة التحقق. - 2026-10-04 — تجربة حية لدورة عمل الوكيل مع مشروع مؤقت: سجلت مجلدًا تجريبيًا محليًا عبر `/v1/agent/projects`، ثم أرسل Flutter-equivalent request إلى `/v1/agent/run/stream` مع `README.md` محددًا. قرأ Gemma 4 الملف، ثم استدعى `propose_file_change` وأعاد diff لتحديثه خلال 77 ثانية مع heartbeat وHTTP 200؛ بقي الملف دون تطبيق التعديل. أُلغي تسجيل المشروع وحُذف المجلد المؤقت بعد التحقق. هذا يثبت مسار API من تسجيل المشروع حتى معاينة التغيير، ولا يغلق اختبار اختيار المجلد وعرض diff داخل نافذة Windows. - 2026-10-04 — متابعة انقطاع الوكيل بعد 4–5 دقائق: أُعيد تشغيل API التطبيق على `127.0.0.1:8000` مع قاعدة البيانات الحالية، وأكد `/health` أن Gemma 4 سليمة. طلب بحث حي عبر `/v1/agent/run/stream` اكتمل بـHTTP 200 وحدث `done` بعد 387 ثانية، مع 15 heartbeat؛ لم ينقطع SSE خلال أكثر من 6 دقائق. مهلة عميل Flutter للبث هي 10 دقائق، والـheartbeat يعيد ضبط مهلة خمول تدفق الأسطر، لذا لا تفسر مدة 4–5 دقائق وحدها الانقطاع. اجتازت مجموعة اختبارات الوكيل/البث/المهل 32/32، ونجح `compileall` و`git diff --check`. أُضيف fallback يعرض مقتطفًا لكل هدف بحث صريح إذا توقف Gemma قبل إكمال تنسيق الجواب، مع اختبار انحدار ناجح. بقي التحقق اليدوي في نافذة Windows غير ممكن هذه الجولة: أداة التحكم المرئي انتهت مهلتها بعد محاولات الاستعادة، والنوافذ المكتشفة كانت من مسار بناء قديم؛ لم يُرسل أي نقر إليها. نتيجة البث الحي تثبت صمود API في هذا الطلب، لكنها لا تعزل سبب كل انقطاع سابق قد يكون من العميل/النافذة أو من تغير تشغيل الخدمة. @@ -202,7 +203,7 @@ - تكرار التقييم مع إجابات مرجعية ومراجعين/درجات بشرية، وقياس استهلاك الذاكرة واعتماد مقارنة قابلة للتكرار. (2026-10-03: أُعيد التشغيل بمعايير أوسع: Gemma وQwen أكملتا 5/5 طلبات بلا أخطاء؛ Gemma مرّرت 9/9 فحوص شكلية، وQwen 8/9، ومتوسط الزمن 14.77 مقابل 6.25 ثانية. إجابة Qwen عن SQLite كانت غير دقيقة رغم اجتياز فحص وجود ثلاث نقاط، ما يثبت أن هذه الفحوص لا تقيس صحة المعنى. التقريرين النهائيين `evals/results/gemma4_e2b_2026-10-03_183058.json` و`evals/results/qwen2.5_1.5b-instruct-q4_K_M_2026-10-03_183140.json`. التقييمات السابقة المؤرخة `175827` و`180307` تستخدم فحوصًا أضيق. أُضيف `scripts/capture_ollama_runtime.ps1`: أظهر `/api/ps` حجم Gemma المحمّل 6,733,158,152 بايت وQwen 1,169,980,128 بايت، و`size_vram=0` لكليهما. اللقطتان `evals/results/ollama_runtime_2026-10-03_180846.json` و`...180934.json` لحظيتان وليستا peak؛ لا تربطان كل PID بالنموذج ولم تتوفر قراءة إجمالي/متاح RAM بسبب رفض CIM. لم تُسجل درجات بشرية بعد؛ تبقى مراجعة بشرية، قياس ذروة RAM/VRAM موثوق ومتكرر، وجولات إضافية للمقارنة.) الجولة السابقة كانت Gemma 8/10 بمتوسط 16.32 ثانية وQwen 5/10 بمتوسط 8.93 ثانية. أُصلحت منذها مشكلة الحساب المباشر مقابل الأداة وصيغة Qwen الرياضية؛ مشغّل التقييم يستخدم جلسة loopback مؤقتة ويلغيها. - [x] جرد أولي لترخيص وسوم Ollama المحلية في `LICENSE_REVIEW_2026-10.md` (2026-10-04): فُحصت `ollama show --license` للأوزان الخمسة المثبتة وبطاقات المصادر الرسمية. Gemma 4 E2B وMinistral 3:3b وQwen2.5 وGranite Embedding أظهرت Apache 2.0؛ `gemma3:1b` تستخدم شروط Gemma الإضافية. وثّق الجرد أيضًا تدفق Groq الصوتي الخارجي واحتفاظ البيانات المحتمل. - [x] إنشاء جرد مكونات آلي أولي `sbom/component-inventory.json` باستخدام `scripts/generate_component_inventory.py`: لقطة 2026-10-04 تضم 50 توزيع Python من `.venv` و104 حزم Flutter من `pubspec.lock`، مع بصمات 104 ملفات رخصة وبصمة أرشيف `NOTICES.Z` المضمّن في Windows Debug. اجتازت اختبارات محلّل الجرد 4/4 وفحوص JSON والمرجع الفريد. تُرك التصنيف اليدوي مطلوبًا لـ101 ملف رخصة عثرت عليها الأداة دون تصنيف تلقائي. الملف ليس جرد إصدار إنتاج محصورًا ولا مراجعة قانونية ولم يُتحقق من مخطط CycloneDX الرسمي؛ لا يتضمن علاقات الاعتماد أو كامل مكونات Windows الأصلية وPDFium وأوزان OCR/النماذج. -- [ ] اعتماد النماذج والاعتماديات لنسخة تجارية محددة: مطابقة مصدر وبصمة كل وزن سيُشحن، حزمة SBOM وإشعارات Python/Flutter/Windows/PDFium/OCR، معالجة قيود Gemma 3 أو استبعادها، وشروط Groq وإفصاح الصوت. لا يُعد الجرد الأولي أعلاه موافقة قانونية أو جاهزية إصدار. تقدم 2026-10-04: صُححت مراجعة Gemma 4 بعد مطابقة وسم Ollama المحلي وبصمته الكاملة مع `/api/tags` ونص Apache 2.0 من `/api/show`؛ بطاقة Google الرسمية تعلن Apache 2.0 أيضًا. بقيت مطابقة مصدر GGUF المحوّل وإشعارات نسخة الإصدار والاعتماديات وPDFium/OCR وشروط Groq والمراجعة القانونية. +- [ ] اعتماد النماذج والاعتماديات لنسخة تجارية محددة: مطابقة مصدر وبصمة كل وزن سيُشحن، حزمة SBOM وإشعارات Python/Flutter/Windows/PDFium/OCR، معالجة قيود Gemma 3 أو استبعادها، وشروط Groq وإفصاح الصوت. لا يُعد الجرد الأولي أعلاه موافقة قانونية أو جاهزية إصدار. تقدم 2026-10-04: صُححت مراجعة Gemma 4 بعد مطابقة وسم Ollama المحلي وبصمته الكاملة مع `/api/tags` ونص Apache 2.0 من `/api/show`؛ بطاقة Google الرسمية تعلن Apache 2.0 أيضًا. تقدم 2026-10-07: أُدرجت بصمة PDFium وإشعارات binary المطابقة، وملفات أوزان EasyOCR التي تطابق MD5 في إعداد 1.7.2، وسُجل غياب وزن الإنجليزية. بقيت مراجعة شروط توزيع أوزان OCR وتبعيات PDFium، وتصنيف إشعارات Flutter الـ101، ومطابقة المصدر/الأوزان لكل إصدار نهائي، وشروط Groq والمراجعة القانونية. - تحسين أولي عبر prompts وRAG والأدوات؛ هذه غالبًا تعالج نقص المعرفة أو القدرة على الفعل دون تغيير أوزان النموذج. - عند توفر GPU مناسب: تجربة LoRA/QLoRA على بيانات مرخصة ومنقحة، ومقارنة النتائج بالمجموعة المرجعية قبل اعتماد adapter. - تدريب نموذج أساسي من الصفر خارج نطاق العتاد الشخصي المعتاد؛ يحتاج بيانات وحوسبة وبنية تدريب كبيرة، ولا يكون خطتنا الأولى. diff --git a/SovereignAI-Starter/sbom/README.md b/SovereignAI-Starter/sbom/README.md index 0d30dff..dee57fc 100644 --- a/SovereignAI-Starter/sbom/README.md +++ b/SovereignAI-Starter/sbom/README.md @@ -1,17 +1,19 @@ # Preliminary component inventory -`component-inventory.json` is a point-in-time CycloneDX 1.5 inventory assembled from the active Python virtual environment, `flutter_app/pubspec.lock`, and (when supplied) the bundled Flutter `NOTICES.Z` file. It is an engineering aid, not a complete or legally approved commercial SBOM. +`component-inventory.json` is a point-in-time CycloneDX 1.5 inventory assembled from the active Python virtual environment, `flutter_app/pubspec.lock`, the installed PDFium native binary and its wheel-bundled build notices, observed local EasyOCR model weights, and (when supplied) the bundled Flutter `NOTICES.Z` file. Host-local runtime files are observations for the current machine, not a release artifact manifest. This is an engineering aid, not a complete or legally approved commercial SBOM. The generator performs local JSON and uniqueness checks; this snapshot has not yet been validated against the official [CycloneDX 1.5 JSON schema](https://github.com/CycloneDX/specification/blob/master/schema/bom-1.5.schema.json). The `pub` PURL type is listed by the [Package-URL type registry](https://github.com/package-url/purl-spec/blob/main/purl-types-index.json). -## Snapshot captured on 2026-10-04 +## Snapshot captured on 2026-10-07 - 50 Python distributions from `.venv`: 8 direct runtime requirements, 1 optional OCR requirement, and 41 installed transitive/development packages. Python versions are the versions installed in this local environment; `requirements.txt` still uses ranges and is not a fully pinned production lock. - 104 Flutter pub packages from the resolved lock: 15 direct main, 3 direct development, and 86 transitive/SDK packages. - 104 package license files have SHA-256 evidence. For 101 components the inventory can find a license file but deliberately does not classify its legal terms automatically. The remaining declarations come from distribution/package metadata or the Flutter SDK license notice. - The Windows Debug artifact bundled `NOTICES.Z` (SHA-256 `eb0096c70ca8a2b23d1a806f1fddb5ce379730712347b267b7c7de4599b8ba70`; 1,836,646 bytes after decompression). The application build already carries this Flutter notice archive. +- Three host-local runtime artifacts are now hashed as CycloneDX file/model components. `pypdfium2` 5.13.0's `pdfium.dll` is 7,260,672 bytes (SHA-256 `fb898a1f5ace57805834f390407500bdb6ef93eff326a252ad334a8aae809d8e`); the inventory records hashes for 16 Windows x64 `BUILD_LICENSES` files shipped in the wheel. +- EasyOCR 1.7.2's local `arabic.pth` (215,400,714 bytes; SHA-256 `2a9afd42c374deb98aed0b53c9b77d75e1d00d4e0501f3b0276c54190c89b1a8`) and `craft_mlt_25k.pth` (83,152,330 bytes; SHA-256 `4a5efbfb48b4081100544e75e1e2b57f8de3d84f213004b14b85fd4b3748db17`) match the MD5 identifiers in the pinned EasyOCR model configuration. This confirms artifact identity against that manifest, not redistribution rights; both model components remain marked for human license review. `english_g2.pth`, which the current Arabic/English reader expects, is absent from this host's OCR model directory and is recorded as missing. The OCR code may download it when first initializing because downloads are enabled by default. -The inventory does not include dependency edges, a release-only Python environment, all native Windows/C++ components, a complete PDFium notice review, OCR model-weight files, or model weights. It does not decide whether any license permits a specific commercial distribution. Continue the manual source, hash, notice, and terms review in `LICENSE_REVIEW_2026-10.md` for the exact release artifacts. +The inventory does not include dependency edges, a release-only Python environment, all native Windows/C++ components, or completed classification of PDFium/OCR notices. It does not decide whether any license permits a specific commercial distribution. Continue the manual source, hash, notice, and terms review in `LICENSE_REVIEW_2026-10.md` for the exact release artifacts. ## Regenerate @@ -21,4 +23,4 @@ From the repository root, after installing Python requirements in `.venv` and re & .\.venv\Scripts\python.exe .\scripts\generate_component_inventory.py ``` -To hash the notices archive from a Windows build, add `--flutter-notices `. The script reads installed Python distribution metadata, the Flutter lock and package cache, and the passed notice archive; it does not install packages or inspect model weights. +To hash the notices archive from a Windows build, add `--flutter-notices `. The script reads installed Python distribution metadata, the Flutter lock and package cache, the installed `pypdfium2_raw` binary and its license notices, the three EasyOCR model files from `LOCAL_OCR_MODEL_DIR` (or the app's default model directory), and the passed Flutter notice archive. It does not install packages, download models, or infer that a detected file is legally redistributable. diff --git a/SovereignAI-Starter/sbom/component-inventory.json b/SovereignAI-Starter/sbom/component-inventory.json index 1c1f22d..d7df26d 100644 --- a/SovereignAI-Starter/sbom/component-inventory.json +++ b/SovereignAI-Starter/sbom/component-inventory.json @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.5", - "serialNumber": "urn:uuid:4b7c89bc-351c-4aa3-821c-471543ead589", + "serialNumber": "urn:uuid:4882e02b-7508-44a4-aff7-8df48bd03f2a", "version": 1, "metadata": { - "timestamp": "2026-10-04T01:47:26.714482+00:00", + "timestamp": "2026-10-07T14:56:01.658964+00:00", "tools": [ { "name": "generate_component_inventory.py" @@ -32,6 +32,14 @@ "name": "inventory.flutter_lock_sha256", "value": "e29b3502666c804cf979f6d390e30ec04a668749ad86c13bb089d4b62d93bc8a" }, + { + "name": "inventory.runtime_artifact_scope", + "value": "Observed files from the current host only; not a release artifact manifest." + }, + { + "name": "inventory.missing_ocr_model", + "value": "english_g2.pth" + }, { "name": "inventory.flutter_notices_sha256", "value": "eb0096c70ca8a2b23d1a806f1fddb5ce379730712347b267b7c7de4599b8ba70" @@ -4570,6 +4578,141 @@ } } ] + }, + { + "type": "file", + "bom-ref": "file:PDFium native binary pdfium.dll:sha256:fb898a1f5ace57805834f390407500bdb6ef93eff326a252ad334a8aae809d8e", + "name": "PDFium native binary pdfium.dll", + "hashes": [ + { + "alg": "SHA-256", + "content": "fb898a1f5ace57805834f390407500bdb6ef93eff326a252ad334a8aae809d8e" + } + ], + "properties": [ + { + "name": "inventory.scope", + "value": "local-runtime-artifact" + }, + { + "name": "inventory.observed_filename", + "value": "pdfium.dll" + }, + { + "name": "inventory.size_bytes", + "value": "7260672" + }, + { + "name": "inventory.license_status", + "value": "upstream and bundled dependency notices recorded; release review required" + }, + { + "name": "inventory.evidence.python_distribution", + "value": "pypdfium2 5.13.0" + }, + { + "name": "inventory.evidence.bundled_build_license_files", + "value": "{\"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/abseil.txt\": \"f54fff0b905df5b3464527c652a30e903b172d6dcab4d89b5e6f105d5e4a4603\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/agg23.txt\": \"c110d3ea2ad77467ce0dcff7d3337e6c8be8049a5103f4b9bd5fd911a77972e5\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/fast_float.txt\": \"bf1b57355feca8fce77ee95f48002f8d4789fb71b30ec7599c06cda4901fbb2b\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/freetype.txt\": \"f4b133e25df1f86ad3ffea453aa0e613f0474f34778dbbb3e437e7b2724937d8\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/icu.txt\": \"93679f4389d53b6835d89843f251844fb9bc455b35bab036d3c8e7abe497a47a\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/lcms.txt\": \"7312b68c5b25e9bf2b828706fb4e29588f22705112f411fd42e1f7d84c3d139a\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/libjpeg_turbo.ijg\": \"db16a04128171879c60708d171b88d97345a2dd20f9bfc173680a4497c73f704\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/libjpeg_turbo.md\": \"be2b2b5ab168bce87bc3e31f2a5c5adba4b7f6e9e51d618e958d1d46972ebd95\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/libopenjpeg.txt\": \"c5ab0890a737c2dfa7ba675036554f6d17741d98629b0c2a145354d00617e6b2\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/libpng.txt\": \"452390433ba0f88aa3e2b122c647741b72a0c117cd6ed7a329b49785aecb5511\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/libtiff.txt\": \"92b72ba97e6c2749c2a94bc0ef646b47080217f1e772a482b33cf5a5f98a6506\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/llvm-libc.txt\": \"3b6226c32e168c83b891d8d6f0d3c29c2116dc3ef93dc93c307b54f279ecf383\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium-binaries.txt\": \"8854f4388f1ca13b3ad9baa42e95f5546b4c0b17109c159256d3eca7be39b09b\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/pdfium.txt\": \"961eacd9633fff6d051db7208b755e9210e30efac7adec3e6a6d52798f0ccf0e\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/simdutf.txt\": \"c172a0ba936ff31230febb5dad869e25cb7c1a07480c7a381be8cf011bb52719\", \"pypdfium2-5.13.0.dist-info/licenses/data/windows_x64/BUILD_LICENSES/zlib.txt\": \"33fd641c9f3b0e0be64bc78fea9e94807674cdd70c48477599226cb8956565fe\"}" + } + ] + }, + { + "type": "machine-learning-model", + "bom-ref": "file:EasyOCR model arabic.pth:sha256:2a9afd42c374deb98aed0b53c9b77d75e1d00d4e0501f3b0276c54190c89b1a8", + "name": "EasyOCR model arabic.pth", + "hashes": [ + { + "alg": "SHA-256", + "content": "2a9afd42c374deb98aed0b53c9b77d75e1d00d4e0501f3b0276c54190c89b1a8" + } + ], + "properties": [ + { + "name": "inventory.scope", + "value": "local-runtime-artifact" + }, + { + "name": "inventory.observed_filename", + "value": "arabic.pth" + }, + { + "name": "inventory.size_bytes", + "value": "215400714" + }, + { + "name": "inventory.license_status", + "value": "model redistribution terms not established; human review required" + }, + { + "name": "inventory.evidence.upstream_manifest", + "value": "easyocr==1.7.2 config.py" + }, + { + "name": "inventory.evidence.upstream_url", + "value": "https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/arabic.zip" + }, + { + "name": "inventory.evidence.upstream_manifest_md5", + "value": "993074555550e4e06a6077d55ff0449a" + }, + { + "name": "inventory.evidence.observed_md5", + "value": "993074555550e4e06a6077d55ff0449a" + }, + { + "name": "inventory.evidence.upstream_md5_match", + "value": "true" + } + ] + }, + { + "type": "machine-learning-model", + "bom-ref": "file:EasyOCR model craft_mlt_25k.pth:sha256:4a5efbfb48b4081100544e75e1e2b57f8de3d84f213004b14b85fd4b3748db17", + "name": "EasyOCR model craft_mlt_25k.pth", + "hashes": [ + { + "alg": "SHA-256", + "content": "4a5efbfb48b4081100544e75e1e2b57f8de3d84f213004b14b85fd4b3748db17" + } + ], + "properties": [ + { + "name": "inventory.scope", + "value": "local-runtime-artifact" + }, + { + "name": "inventory.observed_filename", + "value": "craft_mlt_25k.pth" + }, + { + "name": "inventory.size_bytes", + "value": "83152330" + }, + { + "name": "inventory.license_status", + "value": "model redistribution terms not established; human review required" + }, + { + "name": "inventory.evidence.upstream_manifest", + "value": "easyocr==1.7.2 config.py" + }, + { + "name": "inventory.evidence.upstream_url", + "value": "https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/craft_mlt_25k.zip" + }, + { + "name": "inventory.evidence.upstream_manifest_md5", + "value": "2f8227d2def4037cdb3b34389dcf9ec1" + }, + { + "name": "inventory.evidence.observed_md5", + "value": "2f8227d2def4037cdb3b34389dcf9ec1" + }, + { + "name": "inventory.evidence.upstream_md5_match", + "value": "true" + } + ] } ] } diff --git a/SovereignAI-Starter/scripts/generate_component_inventory.py b/SovereignAI-Starter/scripts/generate_component_inventory.py index 89598a1..85cf60e 100644 --- a/SovereignAI-Starter/scripts/generate_component_inventory.py +++ b/SovereignAI-Starter/scripts/generate_component_inventory.py @@ -6,16 +6,159 @@ import argparse import gzip import hashlib import importlib.metadata +import importlib.util import json +import os import re from datetime import UTC, datetime from pathlib import Path from urllib.parse import unquote, urlparse from uuid import uuid4 +EASYOCR_RUNTIME_MODELS = { + "arabic.pth": { + "url": "https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/arabic.zip", + "md5": "993074555550e4e06a6077d55ff0449a", + }, + "english_g2.pth": { + "url": "https://github.com/JaidedAI/EasyOCR/releases/download/v1.3/english_g2.zip", + "md5": "5864788e1821be9e454ec108d61b887d", + }, + "craft_mlt_25k.pth": { + "url": "https://github.com/JaidedAI/EasyOCR/releases/download/pre-v1.1.6/craft_mlt_25k.zip", + "md5": "2f8227d2def4037cdb3b34389dcf9ec1", + }, +} + + +def file_hash(path: Path, algorithm: str = "sha256") -> str: + hasher = ( + hashlib.md5(usedforsecurity=False) + if algorithm == "md5" + else hashlib.new(algorithm) + ) + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + hasher.update(chunk) + return hasher.hexdigest() + def digest(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() + return file_hash(path) + + +def file_component( + path: Path, + *, + name: str, + component_type: str = "file", + scope: str, + license_status: str, + evidence: dict[str, str] | None = None, +) -> dict[str, object]: + sha256 = digest(path) + properties = [ + {"name": "inventory.scope", "value": scope}, + {"name": "inventory.observed_filename", "value": path.name}, + {"name": "inventory.size_bytes", "value": str(path.stat().st_size)}, + {"name": "inventory.license_status", "value": license_status}, + ] + properties.extend( + {"name": f"inventory.evidence.{key}", "value": value} + for key, value in (evidence or {}).items() + ) + return { + "type": component_type, + "bom-ref": f"file:{name}:sha256:{sha256}", + "name": name, + "hashes": [{"alg": "SHA-256", "content": sha256}], + "properties": properties, + } + + +def _ocr_model_directory() -> Path: + configured = os.getenv("LOCAL_OCR_MODEL_DIR", "").strip() + if configured: + return Path(configured).expanduser() + local_app_data = os.getenv("LOCALAPPDATA") + base = Path(local_app_data) if local_app_data else Path.home() / ".local" / "share" + return base / "SovereignAI" / "models" / "easyocr" + + +def ocr_runtime_components(model_dir: Path) -> tuple[list[dict[str, object]], list[str]]: + """Record installed EasyOCR weights and report requested model files that are absent.""" + components: list[dict[str, object]] = [] + missing: list[str] = [] + for filename, source in EASYOCR_RUNTIME_MODELS.items(): + path = model_dir / filename + if not path.is_file(): + missing.append(filename) + continue + md5 = file_hash(path, "md5") # upstream manifest uses MD5 for artifact identity only + components.append( + file_component( + path, + name=f"EasyOCR model {filename}", + component_type="machine-learning-model", + scope="local-runtime-artifact", + license_status="model redistribution terms not established; human review required", + evidence={ + "upstream_manifest": "easyocr==1.7.2 config.py", + "upstream_url": source["url"], + "upstream_manifest_md5": source["md5"], + "observed_md5": md5, + "upstream_md5_match": str(md5 == source["md5"]).lower(), + }, + ) + ) + return components, missing + + +def pdfium_runtime_components() -> list[dict[str, object]]: + """Hash the installed PDFium native binary and its wheel-bundled notice evidence.""" + try: + spec = importlib.util.find_spec("pypdfium2_raw") + except (ImportError, ValueError): + return [] + if spec is None or not spec.submodule_search_locations: + return [] + package_root = Path(next(iter(spec.submodule_search_locations))) + binaries = [package_root / name for name in ("pdfium.dll", "libpdfium.so", "libpdfium.dylib")] + components: list[dict[str, object]] = [] + try: + distribution = importlib.metadata.distribution("pypdfium2") + except importlib.metadata.PackageNotFoundError: + distribution = None + notice_hashes: dict[str, str] = {} + if distribution is not None: + for item in distribution.files or (): + relative = str(item).replace("\\", "/") + if "/BUILD_LICENSES/" not in relative: + continue + notice = Path(distribution.locate_file(item)) + if notice.is_file(): + notice_hashes[relative] = digest(notice) + for binary in binaries: + if binary.is_file(): + components.append( + file_component( + binary, + name=f"PDFium native binary {binary.name}", + scope="local-runtime-artifact", + license_status="upstream and bundled dependency notices recorded; release review required", + evidence={ + "python_distribution": ( + f"pypdfium2 {distribution.version}" + if distribution is not None + else "pypdfium2 distribution metadata unavailable" + ), + "bundled_build_license_files": json.dumps( + notice_hashes, ensure_ascii=False, sort_keys=True + ), + }, + ) + ) + return components def canonical_name(value: str) -> str: @@ -238,7 +381,14 @@ def main() -> int: args = parser.parse_args() root = args.project_root.resolve() output = args.output or root / "sbom" / "component-inventory.json" - components = python_components(root) + flutter_components(root) + ocr_components, missing_ocr_models = ocr_runtime_components(_ocr_model_directory()) + pdfium_components = pdfium_runtime_components() + components = ( + python_components(root) + + flutter_components(root) + + pdfium_components + + ocr_components + ) metadata: dict[str, object] = { "timestamp": datetime.now(UTC).isoformat(), "tools": [{"name": "generate_component_inventory.py"}], @@ -264,8 +414,16 @@ def main() -> int: "name": "inventory.flutter_lock_sha256", "value": digest(root / "flutter_app" / "pubspec.lock"), }, + { + "name": "inventory.runtime_artifact_scope", + "value": "Observed files from the current host only; not a release artifact manifest.", + }, ], } + metadata["properties"].extend( + {"name": "inventory.missing_ocr_model", "value": filename} + for filename in missing_ocr_models + ) if args.flutter_notices: notice_bytes = args.flutter_notices.read_bytes() metadata["properties"].extend( @@ -292,9 +450,21 @@ def main() -> int: output.write_text( json.dumps(document, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) - python_count = sum(item["purl"].startswith("pkg:pypi/") for item in components) - pub_count = sum(item["purl"].startswith("pkg:pub/") for item in components) - missing = sum("licenses" not in item for item in components) + python_count = sum(str(item.get("purl", "")).startswith("pkg:pypi/") for item in components) + pub_count = sum(str(item.get("purl", "")).startswith("pkg:pub/") for item in components) + missing_library_license_metadata = sum( + item.get("type") == "library" and "licenses" not in item + for item in components + ) + runtime_license_review = sum( + item.get("type") in {"file", "machine-learning-model"} + and any( + prop["name"] == "inventory.license_status" + and "review required" in prop["value"] + for prop in item["properties"] + ) + for item in components + ) unclassified = sum( any( prop["name"] == "inventory.license_source" @@ -313,7 +483,11 @@ def main() -> int: "output": str(output), "python_components": python_count, "flutter_components": pub_count, - "components_missing_license_evidence": missing, + "pdfium_runtime_artifacts": len(pdfium_components), + "ocr_runtime_artifacts": len(ocr_components), + "missing_ocr_models": missing_ocr_models, + "library_components_missing_license_metadata": missing_library_license_metadata, + "runtime_artifacts_requiring_license_review": runtime_license_review, "license_files_hashed": hashed_licenses, "license_files_requiring_manual_classification": unclassified, "complete_commercial_audit": False, diff --git a/SovereignAI-Starter/tests/test_component_inventory.py b/SovereignAI-Starter/tests/test_component_inventory.py index 4b53b84..0c8106b 100644 --- a/SovereignAI-Starter/tests/test_component_inventory.py +++ b/SovereignAI-Starter/tests/test_component_inventory.py @@ -9,6 +9,8 @@ from uuid import uuid4 from scripts.generate_component_inventory import ( canonical_name, component, + file_component, + ocr_runtime_components, parse_lockfile, requirement_names, ) @@ -94,6 +96,53 @@ sdks: ) self.assertEqual(json.loads(json.dumps(result)), result) + def test_runtime_file_component_records_sha256_and_review_status(self) -> None: + artifact = self.fixture_root / "model.pth" + artifact.write_bytes(b"model fixture") + result = file_component( + artifact, + name="EasyOCR model model.pth", + component_type="machine-learning-model", + scope="local-runtime-artifact", + license_status="human review required", + ) + self.assertEqual(result["type"], "machine-learning-model") + self.assertEqual( + result["hashes"], + [{"alg": "SHA-256", "content": "21249a290a4255a0f3ee6685ff7933bffa241c3e35bb13a52dc0c7a679bed3b2"}], + ) + self.assertIn( + {"name": "inventory.license_status", "value": "human review required"}, + result["properties"], + ) + + def test_ocr_inventory_includes_present_weights_and_reports_missing_english(self) -> None: + model_dir = self.fixture_root / "models" + model_dir.mkdir() + (model_dir / "arabic.pth").write_bytes(b"arabic weights") + (model_dir / "craft_mlt_25k.pth").write_bytes(b"detection weights") + (model_dir / "arabic.pth.backup").write_bytes(b"backup") + (model_dir / "temp.zip").write_bytes(b"download fragment") + + artifacts, missing = ocr_runtime_components(model_dir) + + self.assertEqual( + {artifact["name"] for artifact in artifacts}, + {"EasyOCR model arabic.pth", "EasyOCR model craft_mlt_25k.pth"}, + ) + self.assertEqual(missing, ["english_g2.pth"]) + for artifact in artifacts: + properties = {item["name"]: item["value"] for item in artifact["properties"]} + self.assertEqual( + properties["inventory.license_status"], + "model redistribution terms not established; human review required", + ) + self.assertEqual( + properties["inventory.evidence.upstream_manifest"], + "easyocr==1.7.2 config.py", + ) + self.assertEqual(properties["inventory.evidence.upstream_md5_match"], "false") + if __name__ == "__main__": unittest.main()