| # تقرير تقني: آلية عمل MurshidBackend_Colab.ipynb |
| |
| ## مشروع مُرشِد | From Alerts to Guidance |
| ### MITRE ATT&CK-Aligned Techniques Mapping for SOC Analysts |
| |
| --- |
| |
| ## 1. نظرة عامة |
| |
| `MurshidBackend_Colab.ipynb` هو دفتر Jupyter مُصمَّم لتشغيل الباكند الكامل لمشروع مُرشِد على بيئة **Google Colab** باستخدام **GPU (Tesla T4)**، مما يُتيح تشغيل نموذج **LLaMA 3 8B** بتكميم 4-bit لتوليد ملخصات دلالية غنية لقواعد Wazuh XML، وذلك على عكس البيئة المحلية التي تعمل بدون LLaMA (LOCAL mode). |
|
|
| ### الهدف الرئيسي |
| تشغيل **FULL mode** للـ pipeline: |
| ``` |
| قاعدة Wazuh XML |
| ↓ |
| LLaMA 3 8B ←── ملخص دلالي غني (GPU) |
| ↓ |
| SecureBERT+ ←── 768-dim embedding |
| ↓ |
| Logistic Regression ←── confidence scores لكل تقنية |
| ↓ |
| FastAPI + SQLite ←── تخزين وخدمة النتائج |
| ↓ |
| Cloudflare Tunnel ←── رابط عام للفرونت |
| ``` |
|
|
| --- |
|
|
| ## 2. المتطلبات قبل التشغيل |
|
|
| ### 2.1 إعداد Google Colab |
| | المتطلب | التفاصيل | |
| |---------|----------| |
| | **GPU** | Tesla T4 — يُفعَّل من: `Runtime → Change runtime type → T4 GPU` | |
| | **الذاكرة** | High RAM (machine_shape: "hm") | |
| | **الإنترنت** | مفعَّل لتنزيل النماذج من Hugging Face | |
| |
| ### 2.2 الملفات المطلوبة على Google Drive |
| ``` |
| MyDrive/ |
| ├── murshid_backend_for_drive.zip ← ملفات الباكند مضغوطة (44 KB) |
| │ أو |
| ├── murshid_backend/ ← المجلد مستخرج مسبقاً |
| │ ├── app/ |
| │ │ ├── main.py |
| │ │ ├── config.py |
| │ │ ├── api/routes/ |
| │ │ ├── ml/ |
| │ │ ├── models/ |
| │ │ ├── services/ |
| │ │ └── repositories/ |
| │ ├── alembic/ |
| │ ├── scripts/ |
| │ ├── alembic.ini |
| │ └── requirements.txt |
| │ |
| └── Needed/ |
| ├── murshid_logreg_pipeline_manual_oof_pcatuned.joblib ← نموذج LogReg |
| ├── murshid_logreg_thresholds_manual_oof_pcatuned.npy ← عتبات التنبؤ |
| ├── murshid_label_columns.json ← أسماء التقنيات الـ 20 |
| └── murshid_query_template_structure_clean_shared.xlsx ← 60 قالب WQL |
| ``` |
| |
| ### 2.3 Hugging Face Token |
| مطلوب للوصول إلى نموذج `meta-llama/Meta-Llama-3-8B-Instruct`: |
| - يُضاف في `Colab Secrets` باسم `HF_TOKEN` |
| - أو مباشرةً في خلية 5 من الدفتر |
|
|
| --- |
|
|
| ## 3. شرح الخلايا بالتفصيل |
|
|
| ### الخلية 1: التحقق من GPU |
|
|
| **الهدف:** التأكد من وجود GPU قبل البدء. |
|
|
| ```python |
| import torch |
| print('CUDA available:', torch.cuda.is_available()) |
| print('GPU:', torch.cuda.get_device_name(0)) |
| print('Memory:', round(torch.cuda.get_device_properties(0).total_memory / 1e9, 1), 'GB') |
| ``` |
|
|
| **المخرج المتوقع:** |
| ``` |
| CUDA available: True |
| GPU: Tesla T4 |
| Memory: 15.8 GB |
| ``` |
|
|
| **ماذا يحدث إذا لم يكن هناك GPU؟** |
| - LLaMA لن يُحمَّل (يحتاج CUDA) |
| - الخادم سيعمل بـ LOCAL mode فقط (بدون تلخيص) |
|
|
| --- |
|
|
| ### الخلية 2: تحميل Google Drive والتحقق من الملفات |
|
|
| **الهدف:** ربط Colab بـ Google Drive والتحقق من وجود جميع الملفات المطلوبة. |
|
|
| ```python |
| from google.colab import drive |
| drive.mount('/content/drive') |
| |
| NEEDED_PATH = '/content/drive/MyDrive/Needed' |
| BACKEND_PATH = '/content/drive/MyDrive/murshid_backend' |
| ZIP_PATH = '/content/drive/MyDrive/murshid_backend_for_drive.zip' |
| ``` |
|
|
| **ما يتحقق منه:** |
| | الملف | النوع | الحالة | |
| |-------|-------|--------| |
| | `murshid_logreg_pipeline_manual_oof_pcatuned.joblib` | إلزامي | ✅ / ❌ | |
| | `murshid_logreg_thresholds_manual_oof_pcatuned.npy` | إلزامي | ✅ / ❌ | |
| | `murshid_label_columns.json` | إلزامي | ✅ / ❌ | |
| | `murshid_query_template_structure_clean_shared.xlsx` | اختياري | ✅ / ⚠️ | |
| | `murshid_backend/` أو `.zip` | إلزامي | ✅ / ❌ | |
|
|
| --- |
|
|
| ### الخلية 3: تجهيز الباكند في /content |
|
|
| **الهدف:** نقل ملفات الباكند من Drive إلى `/content` لتسريع القراءة (Drive أبطأ في I/O). |
|
|
| **المنطق الذكي:** |
| ``` |
| هل murshid_backend/ موجود على Drive؟ |
| ↓ نعم → انسخ مباشرةً إلى /content |
| ↓ لا |
| هل murshid_backend_for_drive.zip موجود؟ |
| ↓ نعم → استخرجه إلى Drive أولاً ثم انسخ |
| ↓ لا |
| → ❌ خطأ: "ارفعي ZIP إلى Google Drive" |
| ``` |
|
|
| **الخطوات المنفَّذة:** |
| 1. **استخراج ZIP** (إذا لزم) إلى `MyDrive/` |
| 2. **نسخ** `murshid_backend/` إلى `/content/murshid_backend/` (بدون pycache وملفات مؤقتة) |
| 3. **إضافة** `/content/murshid_backend` إلى `sys.path` |
| 4. **تغيير** working directory إلى `/content/murshid_backend` |
|
|
| **لماذا النسخ إلى /content؟** |
| - Drive يعتمد على FUSE mount = بطيء للقراءة المتكررة |
| - `/content` على SSD محلي للـ VM = أسرع بـ 5-10x |
|
|
| --- |
|
|
| ### الخلية 4: تثبيت المتطلبات |
|
|
| **الهدف:** تثبيت جميع المكتبات اللازمة لتشغيل الباكند. |
|
|
| **المكتبات المثبَّتة:** |
|
|
| | المكتبة | الإصدار | الغرض | |
| |---------|---------|--------| |
| | `fastapi` | 0.115.0 | إطار API | |
| | `uvicorn` | 0.32.0 | خادم ASGI | |
| | `pydantic` | 2.9.0 | تحقق من البيانات | |
| | `sqlalchemy` | 2.0.0 | ORM | |
| | `alembic` | 1.13.0 | هجرة DB | |
| | `scikit-learn` | **1.6.1** | نموذج LogReg (يطابق بيئة التدريب) | |
| | `bitsandbytes` | ≥0.46.1 | تكميم LLaMA 4-bit | |
| | `accelerate` | آخر نسخة | `device_map="auto"` للـ GPU | |
| | `openpyxl` | آخر نسخة | قراءة ملف Excel | |
| | `lxml` | آخر نسخة | معالجة XML | |
| | `pyngrok` | آخر نسخة | (احتياطي — غير مستخدم) | |
|
|
| > **ملاحظة مهمة:** `scikit-learn==1.6.1` محدَّد بدقة لأن ملفات joblib دُرِّبت بهذه النسخة — استخدام نسخة مختلفة يُنتج تحذيرات `InconsistentVersionWarning`. |
|
|
| --- |
|
|
| ### الخلية 5: إعداد ملف .env |
|
|
| **الهدف:** إنشاء ملف الإعدادات لتشغيل FULL mode. |
|
|
| **محتوى الملف المُولَّد:** |
| ```env |
| MURSHID_DB_URL=sqlite:////content/murshid.db |
| MURSHID_MODELS_DIR=/content/drive/MyDrive/Needed |
| HF_TOKEN=**** |
| MURSHID_SKIP_LLM=false ← مفتاح FULL mode |
| SECRET_KEY=murshid_colab_2026 |
| LLAMA_MODEL_ID=meta-llama/Meta-Llama-3-8B-Instruct |
| EMBED_MODEL_ID=ehsanaghaei/SecureBERT_Plus |
| LOGREG_JOBLIB=murshid_logreg_pipeline_manual_oof_pcatuned.joblib |
| LOGREG_THRESHOLDS_NPY=murshid_logreg_thresholds_manual_oof_pcatuned.npy |
| LABEL_COLUMNS_JSON=murshid_label_columns.json |
| ``` |
|
|
| **الفرق بين FULL و LOCAL mode:** |
| | المتغير | FULL mode | LOCAL mode | |
| |---------|-----------|------------| |
| | `MURSHID_SKIP_LLM` | `false` | `true` | |
| | LLaMA يُحمَّل؟ | ✅ نعم | ❌ لا | |
| | جودة التلخيص | عالية | الوصف الخام فقط | |
| | T1484 confidence (مثال) | **94.76%** | 89.29% | |
|
|
| --- |
|
|
| ### الخلية 6: تهجير قاعدة البيانات (Alembic) |
|
|
| **الهدف:** إنشاء جداول قاعدة البيانات SQLite. |
|
|
| ```bash |
| python -m alembic upgrade head |
| ``` |
|
|
| **الجداول المُنشأة (من migration 0001):** |
|
|
| | الجدول | الغرض | مصدره في التقرير | |
| |--------|--------|-----------------| |
| | `users` | مستخدمو النظام (admin/analyst) | ER Diagram §3.2.6 | |
| | `mapping_jobs` | وظائف معالجة ملفات القواعد | ER Diagram §3.2.6 | |
| | `rules` | قواعد Wazuh المُحلَّلة | ER Diagram §3.2.6 | |
| | `techniques` | تقنيات MITRE ATT&CK | ER Diagram §3.2.6 | |
| | `rule_technique_mappings` | ربط القواعد بالتقنيات + confidence | ER Diagram §3.2.6 | |
| | `query_templates` | قوالب WQL للتحقيق | ER Diagram §3.2.6 | |
|
|
| > **ملاحظة:** قاعدة البيانات في `/content/murshid.db` — تُنشأ من جديد في كل جلسة Colab. |
|
|
| --- |
|
|
| ### الخلية 7: استيراد قوالب WQL من Excel |
|
|
| **الهدف:** تحميل 60 قالب WQL من ملف Excel إلى قاعدة البيانات. |
|
|
| **البيانات المستوردة:** |
|
|
| | الإحصائية | القيمة | |
| |-----------|--------| |
| | إجمالي التقنيات | 20 تقنية | |
| | إجمالي القوالب | 60 قالب (3 لكل تقنية) | |
| | التقنيات المشمولة | T1047, T1055, T1059.001, T1070.004, T1078, T1083, T1095, T1098, T1105, T1110, T1112, T1114, T1176, T1190, T1484, T1498, T1499, T1529, T1531, T1562.001 | |
|
|
| **مثال على قالب WQL (T1484):** |
| ``` |
| Template 1: Host pivot |
| agent.name:${HOST} AND win.system.eventID:(4728 OR 4729 ...) AND @timestamp:[now-24h TO now] |
| |
| Template 2: Actor pivot |
| win.eventdata.SubjectUserName:${USER} AND win.system.eventID:(...) AND @timestamp:[now-24h TO now] |
| |
| Template 3: High-impact target change |
| win.system.eventID:(...) AND win.eventdata.TargetUserName:("Domain Admins" OR ...) AND @timestamp:[now-24h TO now] |
| ``` |
|
|
| **منع التكرار:** |
| - يتحقق من وجود (`technique_id` + `purpose`) قبل الإضافة |
| - `replace=False` بشكل افتراضي (لا يُعيد الكتابة) |
|
|
| --- |
|
|
| ### الخلية 8: تشغيل FastAPI + Cloudflare Tunnel |
|
|
| **الهدف:** الخلية الرئيسية — تُشغّل الباكند وتُنشئ رابطاً عاماً. |
|
|
| #### 8.1 التحقق من bitsandbytes |
| ```python |
| import bitsandbytes as bnb |
| print(f'✅ bitsandbytes {bnb.__version__}') |
| ``` |
| > إذا فشل: يُوقف التشغيل فوراً مع رسالة واضحة. |
|
|
| #### 8.2 تشغيل uvicorn |
| ```bash |
| python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level info |
| ``` |
| - `--host 0.0.0.0`: يستمع على كل الواجهات (مطلوب للـ tunnel) |
| - اللوج يُحفظ في `/content/murshid_server.log` |
|
|
| #### 8.3 تحميل النماذج (lifespan) |
| عند بدء الخادم تُنفَّذ `load_models()` بهذا الترتيب: |
|
|
| ``` |
| 1. hf_login(token) ← 1-2 ثانية |
| 2. LLaMA 3 8B-Instruct (4-bit NF4) ← 5-8 دقائق (4.5 GB) |
| - BitsAndBytesConfig: load_in_4bit=True |
| - bnb_4bit_quant_type="nf4" |
| - bnb_4bit_compute_dtype=float16 |
| 3. SecureBERT+ (ehsanaghaei) ← 1-2 دقيقة |
| - AutoModel + AutoTokenizer |
| - mean pooling 768-dim |
| 4. LogisticRegressionModel ← < 1 ثانية |
| - joblib.load (Pipeline: PCA + OneVsRestClassifier) |
| - np.load thresholds |
| ``` |
|
|
| #### 8.4 الانتظار الذكي |
| ```python |
| for i in range(180): # 15 دقيقة كحد أقصى |
| time.sleep(5) |
| # فحص /health كل 5 ثوانٍ |
| # عرض اللوج كل 30 ثانية |
| # كشف مبكر للأخطاء (ERROR, ImportError) |
| ``` |
|
|
| #### 8.5 Cloudflare Tunnel |
| ```bash |
| wget cloudflared-linux-amd64 → /usr/local/bin/cloudflared |
| cloudflared tunnel --url http://localhost:8000 |
| ``` |
| - لا يحتاج حساباً أو توكناً |
| - يُنتج رابطاً مثل: `https://xxxx.trycloudflare.com` |
| - صالح طوال جلسة Colab |
|
|
| --- |
|
|
| ### الخلية 9: ربط الفرونت تلقائياً |
|
|
| **الهدف:** تحديث `index.html` بالرابط الجديد من Cloudflare تلقائياً. |
|
|
| ```python |
| # استخراج الرابط |
| match = re.search(r'https://[a-z0-9\-]+\.trycloudflare\.com', content) |
| public_url = match.group(0) |
| |
| # تحديث index.html على Drive |
| html = re.sub( |
| r"const BASE = '[^']*';", |
| f"const BASE = '{public_url}';", |
| html |
| ) |
| ``` |
|
|
| **النتيجة:** |
| ```javascript |
| // قبل |
| const BASE = 'http://127.0.0.1:8000'; |
| |
| // بعد |
| const BASE = 'https://xxxx.trycloudflare.com'; |
| ``` |
|
|
| --- |
|
|
|
|
|
|
| ### الخلية 10: اختبار الـ API |
|
|
| **الهدف:** التحقق من عمل كل مكون. |
|
|
| #### 10.1 Health Check |
| ```python |
| urllib.request.urlopen('http://localhost:8000/health') |
| ``` |
|
|
| **المخرج المتوقع (FULL mode):** |
| ```json |
| { |
| "pipeline_mode": "full", |
| "pipeline_description": "LLaMA + SecureBERT+ + LogReg", |
| "components": { |
| "llama_loaded": true, |
| "embedder_loaded": true, |
| "logreg_loaded": true, |
| "cuda_available": true |
| }, |
| "all_model_files_present": true |
| } |
| ``` |
|
|
| #### 10.2 تحليل قاعدة اختبار |
| ```python |
| rule_xml = '<rule id="18205" level="5">...' |
| POST http://localhost:8000/rules/analyze |
| ``` |
|
|
| **الـ pipeline خطوة بخطوة:** |
|
|
| ``` |
| XML Input (rule 18205) |
| ↓ |
| sanitize_rule_from_string() |
| - حذف: mitre, if_sid, group, if_group |
| ↓ |
| summarize_one_rule() [LLaMA] |
| - Input: sanitized XML |
| - Output: "Detects the deletion of a security-enabled global group on a Windows system." |
| ↓ |
| build_text_for_embedding() |
| - text = summary + ". " + description |
| - "Detects the deletion of a security-enabled global group on a Windows system. Windows: Security Enabled Global Group Deleted." |
| ↓ |
| SecureBERTEmbedder.embed_text() |
| - Chunks (256 tokens max) |
| - mean pooling per chunk |
| - average chunks → 768-dim vector |
| - L2 normalize |
| ↓ |
| LogisticRegressionModel.predict() |
| - predict_proba(X_user) |
| - pred = (proba >= logreg_thr) |
| - conf = proba * 100 |
| - gap = proba - logreg_thr |
| ↓ |
| save_technique_mappings() [DB] |
| - حفظ 20 تقنية مع confidence |
| ↓ |
| JSON Response |
| ``` |
|
|
| **المخرج للقاعدة 18205:** |
| ``` |
| Technique Pred Conf% Proba Thr Gap |
| T1484 ✅ 94.76 0.9476 0.74 +0.2076 ← Primary |
| T1531 ❌ 27.92 0.2792 ... ... |
| T1070.004 ❌ 21.03 0.2103 ... ... |
| T1098 ❌ 10.65 0.1065 ... ... |
| T1112 ❌ 9.27 0.0927 ... ... |
| ``` |
|
|
| --- |
| الخطوات القادمة للمود المحلي (lOCAL Mode) غير ضروريه |
|
|
| ### الخلية 11: تصدير النتائج (اختياري) |
|
|
| **الهدف:** تصدير نتائج القواعد المُحلَّلة إلى JSON لاستخدامها لاحقاً على الجهاز المحلي. |
|
|
| ```python |
| export_path = f'{NEEDED_PATH}/murshid_full_results.json' |
| json.dump(export_results, f, ensure_ascii=False, indent=2) |
| ``` |
|
|
| **الاستخدام:** يُمكِّن استيراد نتائج FULL mode في الباكند المحلي بدون GPU. |
|
|
| --- |
|
|
| ### الخلية 12: إيقاف الخادم |
|
|
| ```python |
| cf_proc.terminate() # إغلاق Cloudflare tunnel |
| server_proc.terminate() # إيقاف uvicorn |
| ``` |
|
|
| --- |
|
|
| ## 4. مقارنة أوضاع التشغيل |
|
|
| | | FULL mode (Colab) | LOCAL mode (الجهاز) | LITE mode | |
| |--|-------------------|---------------------|-----------| |
| | **LLaMA** | ✅ | ❌ | ❌ | |
| | **SecureBERT+** | ✅ | ✅ | ❌ | |
| | **LogReg** | ✅ | ✅ | ✅ | |
| | **GPU** | Tesla T4 | لا يلزم | لا يلزم | |
| | **Embedding** | نص مُثرى بـ LLaMA | وصف القاعدة فقط | عشوائي | |
| | **T1484 confidence** | **94.76%** | 89.29% | غير موثوق | |
| | **القرار النهائي** | T1484 ✅ | T1484 ✅ | غير موثوق | |
| | **وقت التحليل/قاعدة** | ~30-60 ثانية | ~2-5 ثوانٍ | < 1 ثانية | |
| | **الاستخدام** | إنتاج / عرض | تطوير محلي | اختبار فقط | |
|
|
| --- |
|
|
| ## 5. معمارية النظام الكاملة على Colab |
|
|
| ``` |
| ┌─────────────────────────────────────────────────────┐ |
| │ Google Colab VM │ |
| │ │ |
| │ ┌─────────────────────────────────┐ │ |
| │ │ /content/murshid_backend/ │ │ |
| │ │ │ │ |
| │ │ FastAPI (uvicorn :8000) │ │ |
| │ │ ├── /health │ │ |
| │ │ ├── POST /rules/analyze │ │ |
| │ │ ├── GET /results/{rule_id} │ │ |
| │ │ ├── GET /queries/{tech_id} │ │ |
| │ │ └── GET /api/db/... │ │ |
| │ └───────────────┬─────────────────┘ │ |
| │ │ │ |
| │ ┌───────────────┴───────────┐ │ |
| │ │ ML Models (GPU VRAM) │ │ |
| │ │ ├── LLaMA 3 8B (4-bit) │ │ |
| │ │ ├── SecureBERT+ │ │ |
| │ │ └── LogReg Pipeline │ │ |
| │ └───────────────────────────┘ │ |
| │ │ │ |
| │ ┌───────────────┴───────────┐ │ |
| │ │ /content/murshid.db │ │ |
| │ │ (SQLite — 6 جداول) │ │ |
| │ └───────────────────────────┘ │ |
| │ │ |
| │ ┌───────────────────────────┐ │ |
| │ │ cloudflared tunnel │ │ |
| │ │ localhost:8000 → HTTPS │ │ |
| │ └───────────────┬───────────┘ │ |
| └──────────────────┼──────────────────────────────────┘ |
| │ |
| ▼ |
| https://xxxx.trycloudflare.com |
| │ |
| ▼ |
| ┌─────────────────────────┐ |
| │ المتصفح / الفرونت │ |
| │ index.html (React) │ |
| └─────────────────────────┘ |
| ``` |
|
|
| --- |
|
|
| ## 6. الأخطاء الشائعة وحلولها |
|
|
| | الخطأ | السبب | الحل | |
| |-------|-------|------| |
| | `ImportError: bitsandbytes>=0.46.1` | نسخة قديمة | شغّلي `!pip install -U bitsandbytes>=0.46.1` | |
| | `FileNotFoundError: murshid_backend` | ZIP غير مرفوع | ارفعي `murshid_backend_for_drive.zip` إلى Drive | |
| | `ERR_NGROK_4018` | ngrok يحتاج حساباً | استخدمي Cloudflare Tunnel (خلية 9) | |
| | `Cannot connect to backend` | CORS مغلق | `allow_origins=["*"]` في `main.py` | |
| | Server يستغرق > 15 دقيقة | تنزيل LLaMA بطيء | في الجلسة الثانية التنزيل من Cache | |
| | `InconsistentVersionWarning` | sklearn إصدار مختلف | تأكدي من `scikit-learn==1.6.1` | |
|
|
| --- |
|
|
| ## 7. الـ Endpoints المتاحة بعد التشغيل |
|
|
| | Method | Endpoint | الوصف | |
| |--------|----------|-------| |
| | `GET` | `/health` | حالة الخادم والنماذج | |
| | `GET` | `/api/stats` | إحصائيات Dashboard | |
| | `GET` | `/api/db/summary` | عدد الصفوف في الجداول | |
| | `GET` | `/api/db/rules` | جميع القواعد في DB | |
| | `GET` | `/api/db/mappings` | جميع المطابقات | |
| | `GET` | `/api/db/techniques` | تقنيات MITRE المخزّنة | |
| | `GET` | `/api/db/templates` | قوالب WQL | |
| | `POST` | `/api/db/import-excel` | استيراد Excel | |
| | `POST` | `/rules/analyze` | تحليل قاعدة XML (FULL pipeline) | |
| | `GET` | `/results/{rule_id}` | نتائج تقنية قاعدة محددة | |
| | `GET` | `/queries/{technique_id}` | استعلامات WQL لتقنية | |
| | `POST` | `/admin/templates` | إضافة قالب WQL | |
| | `PATCH` | `/admin/templates/{id}` | تعديل قالب | |
| | `GET` | `/docs` | Swagger UI التفاعلي | |
|
|
| --- |
|
|
| ## 8. ملاحظات للعرض التقديمي |
|
|
| 1. **شغّلي الخلايا قبل العرض بـ 15 دقيقة** (وقت تحميل LLaMA) |
| 2. **انسخي رابط Cloudflare** وتحققي منه في المتصفح |
| 3. **الفرونت يُحدَّث تلقائياً** بالرابط الجديد في خلية 9 |
| 4. **كل جلسة Colab جديدة = رابط Cloudflare جديد** — كرّري الخطوات |
| 5. **DB فارغة في كل جلسة** — حلّلي القواعد عبر Admin Panel أو خلية اختبار |
|
|
| --- |
|
|
| *تاريخ الإنشاء: 8 أبريل 2026 | مشروع مُرشِد — CCIS, PNU* |
|
|