> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# جمع أدلة الامتثال Evidence تلقائياً بدل لقطات الشاشة
- URL: https://arabroot.io/articles/جمع-ادلة-الامتثال-تلقائيا/
- Published: 2026-10-26T07:14:15.000Z
- Updated: 2026-10-26T08:29:43.000Z
- Description: نبني جامع أدلة صغيراً يقرأ إعدادات السيرفرات عبر SSH ويحفظها بالهاش، ثم نجربه على تغيير مفاجئ ودليل معدل وجامع فقد صلاحيته بصمت.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, السيادة الرقمية والامتثال, لينكس والسيرفرات, الاستضافة الذاتية

في نهاية الربع الأول من التزامها بالضوابط الأساسية ECC-2:2024، كان على شركة منصة حجز مواعيد العيادات التي تابعناها في [مقال CISO Assistant](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D8%AA%D8%AB%D8%A7%D9%84-%D9%85%D8%B9-ciso-assistant/) و[مقال سجل المخاطر](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D9%85%D8%AE%D8%A7%D8%B7%D8%B1-risk-register-%D9%85%D8%B9-simplerisk/) أن ترسل للمستشفى الحكومي تقييمها الذاتي مع الأدلة. وقضى مدير التقنية يومين كاملين يفتح لوحات التحكم ويأخذ لقطات شاشة: إعداد MFA في مزود الهوية، وإعداد SSH على كل سيرفر، وصفحة سياسة الاحتفاظ بالسجلات، وقائمة المديرين. وبعد أسبوعين عادت الملاحظات من فريق الأمن في المستشفى: «لقطة إعداد SSH لا تبين اسم السيرفر ولا تاريخها، ولقطتان من السيرفر نفسه فيما يبدو، وأين الدليل على السيرفر الثالث؟». ولم يكن المدير يكذب، لكنه لم يستطع أن يثبت أنه لا يكذب، وهذا بالضبط ما يفشل فيه الدليل اليدوي.

والسؤال الذي خرج به الفريق: لماذا نجمع بأيدينا شيئاً تعرفه الأنظمة نفسها؟ فالسيرفر يعرف إعداده، ومزود الهوية يعرف من لديه MFA، ومنصة السجلات تعرف مدة الاحتفاظ، والمطلوب أن نسأل كل نظام بأمر ثابت، ونحفظ الإجابة بتاريخها ومصدرها، ونثبت أنها لم تتغير بعد حفظها. وقد ذكرنا في [مصطلحات GRC](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-grc-%D8%A7%D9%84%D8%AD%D9%88%D9%83%D9%85%D8%A9-%D9%88%D8%A7%D9%84%D9%85%D8%AE%D8%A7%D8%B7%D8%B1-%D9%88%D8%A7%D9%84%D8%A7%D9%85%D8%AA%D8%AB%D8%A7%D9%84/#evidence) ما يقبله المدقق دليلاً، وبنينا في [مقال أفكار المنتجات](https://arabroot.io/articles/%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D9%86-%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D9%87%D9%8A%D8%A6%D8%A9-%D8%A7%D9%84%D9%88%D8%B7%D9%86%D9%8A%D8%A9-%D9%84%D9%84%D8%A7%D9%85%D9%86-%D8%A7%D9%84%D8%B3%D9%8A%D8%A8%D8%B1%D8%A7%D9%86%D9%8A/#lab) نموذجاً صغيراً يربط نتائج الفحص بأرقام الضوابط، أما هنا فنبني جامع أدلة كاملاً على سيرفرين، ونجرب عليه ما يحدث في الواقع: إعداد يتغير بين تشغيلين، وحساب مدير يضاف دون أن يعلم أحد، ودليل يعدله شخص بعد جمعه، وجامع يفقد صلاحيته بصمت.

[![](https://arabroot.io/content/images/2026/10/ciso-assistant-cover-cover.webp)دليلإدارة الامتثال Compliance مع CISO Assistant على سيرفركنشغل CISO Assistant ونحمل فيه ضوابط ECC-2:2024 ونقيم منصة حقيقية ونربطها بالضوابط والأدلة ونصدر التقرير، مع كل مشكلة واجهناها في الطريق.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D8%AA%D8%AB%D8%A7%D9%84-%D9%85%D8%B9-ciso-assistant/)[![](https://arabroot.io/content/images/2026/10/risk-register-cover-cover.webp)دليلسجل المخاطر Risk Register مع SimpleRisk على سيرفركنبني سجل مخاطر حقيقياً بنموذج الهيئة ثم بأداة SimpleRisk، ونرى لماذا أعطت الأداة كل المخاطر مستوى واحداً، وكيف تكتب منهجية تعطي الأرقام معناها.](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D9%85%D8%AE%D8%A7%D8%B7%D8%B1-risk-register-%D9%85%D8%B9-simplerisk/)[![](https://arabroot.io/content/images/2026/10/ecc-open-source-cover-cover.webp)دليلتحقيق ضوابط ECC بأدوات مفتوحة المصدر Open Sourceخريطة تربط كل ضابط فرعي في الضوابط الأساسية ECC بالأداة مفتوحة المصدر التي تحققه وبالمقال الذي شرحها، مع إثبات ضابطين بمخرج حقيقي.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%82%D9%8A%D9%82-%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-ecc-%D8%A8%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/)

وسوف نناقش في هذا المقال ما يلي:

- أنواع الأدلة، والأسئلة الأربعة التي يسألها المدقق عن كل دليل، ولماذا تفشل لقطة الشاشة فيها.
- ثلاث طرق للجمع الآلي: الوكيل Agent، والجمع دون وكيل Agentless عبر SSH، والـ API، ومتى تستخدم كلاً منها.
- تصميم حساب جمع للقراءة فقط بأقل صلاحية، ولماذا لا تستخدم root.
- تجربة كاملة: سيرفران وجامع في Docker، وملف فحوص مربوط بأرقام الضوابط، وسكربت Python بالمكتبة القياسية.
- المشكلة التي جعلت كل الفحوص «error» في التشغيل الأول، وكيف قرأنا السبب.
- كشف التغير Drift بين تشغيلين، والفرق بين الإعداد في الملف والإعداد الذي يعمل فعلاً.
- كشف تعديل الأدلة بالهاش والبيان المسلسل، ولماذا يحتاج البيان مرساة خارج السيرفر.
- الجدولة والاحتفاظ والخصوصية، وربط الأدلة بأداة GRC، وقائمة تحقق.

## ما الذي يجعل الدليل دليلاً؟

عندما يقرأ المدقق دليلاً فهو يسأل أربعة أسئلة، حتى لو لم يقلها بصوت عال: متى أخذ؟ ومن أي نظام؟ وهل أستطيع أن أطلب إعادته فأحصل على النتيجة نفسها؟ وهل أعرف إن عدله أحد بعد أخذه؟ والشكل التالي يضع الأنواع الأربعة الشائعة أمام هذه الأسئلة:

![جدول: لقطة الشاشة اليدوية تفشل في معرفة المصدر وإعادة الإنتاج وكشف التعديل، ولقطة الإعداد من النظام تنجح في الأربعة، وتقارير الأدوات والسجلات تنجح جزئياً، والوثائق الموقعة تنجح في التاريخ والمصدر](https://arabroot.io/content/images/2026/10/evidence-collection-01-kinds.webp)

لقطة الإعداد من النظام نفسه تجيب عن الأسئلة الأربعة، ولقطة الشاشة لا تجيب إلا عن واحد أحياناً

ولقطة الشاشة ليست مرفوضة دائماً، فبعض الأنظمة لا تعطي غيرها، لكنها أضعف الأدلة لثلاثة أسباب: لا تحمل مصدرها، فصورة لوحة تحكم لا تقول على أي سيرفر أو حساب أخذت، ولا يعاد إنتاجها إلا بشخص يفتح الشاشة نفسها، ولا شيء يمنع تعديلها. ولهذا طلبت ملاحظات المستشفى اسم السيرفر والتاريخ. أما لقطة الإعداد التي يطبعها النظام نفسه، مثل مخرج `sshd -T`، فتحمل الأمر الذي أنتجها، ويستطيع أي شخص أن يعيد تشغيل الأمر ويقارن، وإذا حفظنا معها الهاش SHA-256 والتاريخ والسيرفر في ملف بيان Manifest، فقد أجبنا عن الأسئلة الأربعة دون أن نشرح شيئاً في الجلسة.

ولاحظ أن الضوابط نفسها تطلب نوعين مختلفين من الأدلة. فأغلب المكونات الفرعية في المكون الثاني من ECC تنتهي بضابط «يراجع تطبيق المتطلبات دورياً»، ودليله محضر مراجعة بتاريخ، وهو قرار بشري لا يجمع آلياً، لكن الضابط الذي يسبقه، أي الحد الأدنى التقني، دليله حالة الأنظمة، وهذه هي التي سوف نؤتمتها. وكما ذكرنا في [مقال ضوابط ECC](https://arabroot.io/articles/%D8%AA%D8%AD%D9%82%D9%8A%D9%82-%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-ecc-%D8%A8%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/#what-a-tool-covers)، الأداة تخدم «طبق» و«الحد الأدنى»، والناس يخدمون «وثق» و«راجع».

## من التدقيق السنوي إلى المراقبة المستمرة

فكرة أن تجمع الأدلة آلياً ليست جديدة، لكنها انتقلت في السنوات الأخيرة من الجهات الكبيرة إلى الجميع. ففي 2011 نشر المعهد الوطني للمعايير والتقنية الأمريكي دليل [NIST SP 800-137](https://csrc.nist.gov/pubs/sp/800/137/final?ref=arabroot.io) عن المراقبة المستمرة لأمن المعلومات Information Security Continuous Monitoring، وفكرته الأساسية أن حالة الضوابط يجب أن تعرف بتكرار يناسب خطورتها، وليس مرة في السنة عند التدقيق، لأن الضابط الذي كان سليماً يوم التدقيق قد يتعطل في اليوم التالي ولا يعرف أحد حتى التدقيق القادم. والمثال الذي رأيناه في التجربة، مطور يفتح دخول root «مؤقتاً»، هو بالضبط ما تصمم المراقبة المستمرة لاكتشافه.

ثم جاءت موجة أدوات الامتثال التجارية التي تتصل بأنظمة العميل وتسحب الأدلة إلى لوحة واحدة، وقد وصفتها [مجلة المحاسبة Journal of Accountancy](https://www.journalofaccountancy.com/issues/2026/feb/promises-of-fast-and-easy-threaten-soc-credibility/?ref=arabroot.io) التابعة للمعهد الأمريكي للمحاسبين القانونيين بأنها تسمح للمدققين بدل عشرات لقطات الشاشة وملفات Excel أن يتصلوا بأنظمة العميل ويسحبوا البيانات والأدلة آلياً إلى لوحة مركزية، لكن المقال نفسه حذر من أن الأداة لا تغني عن حكم المدقق، وأن التقرير الذي يعتمد على ما يقوله العميل دون فحص كاف قد يرفضه من يقرؤه. وهذا ينطبق علينا أيضاً: الجامع يعطيك أدلة أقوى وأكثر تكراراً، لكنه لا يقرر أن الضابط كاف، فهذا قرار من يراجع.

والفرق الذي يهم شركة صغيرة مثل منصة الحجز أن هذه الفكرة لم تعد تحتاج منتجاً تجارياً، فكل ما فعلناه في التجربة سكربت بالمكتبة القياسية وSSH، وما تدفع ثمنه في الأدوات التجارية هو الواجهة والتكاملات الجاهزة، وهي مفيدة عندما يكبر عدد الأنظمة، لكنها ليست شرطاً للبدء.

## ثلاث طرق للجمع الآلي

قبل أن نكتب سطراً واحداً، قارن الفريق بين ثلاث طرق، ولكل منها مكان:

- **الوكيل Agent على كل سيرفر:** برنامج يعمل على الجهاز ويرسل حالته، مثل وكيل Wazuh الذي يقيس الإعدادات بمعايير CIS، أو osquery مع Fleet الذي شرحناه في [إدارة أجهزة الموظفين](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D8%AC%D9%87%D8%B2%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B8%D9%81%D9%8A%D9%86-%D9%88%D8%AA%D8%A3%D9%85%D9%8A%D9%86%D9%87%D8%A7-mdm/). ميزته أنه يعمل حتى للأجهزة خلف شبكات لا تصل إليها، وأنه يرسل الحالة باستمرار، وثمنه أنه برنامج آخر على كل جهاز يجب تحديثه وتأمينه.
- **دون وكيل Agentless عبر SSH:** جامع مركزي يدخل إلى السيرفرات بحساب قراءة ويشغل أوامر ثابتة. لا يضيف شيئاً إلى السيرفر إلا مفتاحاً عاماً وسطراً في sudoers، وأوامره مفهومة لأي مدير أنظمة، وثمنه أن الجامع يحتاج طريقاً شبكياً إلى كل سيرفر، وأن مفتاحه يصبح أصلاً حساساً.
- **الـ API:** للخدمات التي لا تدخل إليها بـ SSH أصلاً، مثل مزود الهوية ومنصة السحابة وأداة النسخ الاحتياطي، فتسألها بحساب خدمة وتحفظ الرد JSON كما هو.

واختارت الشركة الطريقة الثانية للسيرفرات، لأن عندها ثلاثة سيرفرات فقط، ولأن الأوامر نفسها هي التي يكتبها المدير يدوياً اليوم، والثالثة لمزود الهوية في المرحلة التالية. والتصميم الذي بنيناه في التجربة يبينه الشكل التالي:

![ملف الفحوص checks.json، ثم الجامع collect.py في Container، ثم SSH بأوامر قراءة فقط إلى web1 وweb2 بحساب evidence وsudo محدود، ثم مجلد التشغيل بالملفات وmanifest.json، ثم مرساة خارجية في تخزين لا يعدل](https://arabroot.io/content/images/2026/10/evidence-collection-02-collector.webp)

الجامع يقرأ ولا يغير، والسيرفر لا يعرف عنه إلا مفتاحاً عاماً وسطراً في sudoers

## حساب الجمع: أقل صلاحية ممكنة

أسهل طريقة أن يدخل الجامع بحساب root، وهي أسوأ طريقة، والسبب أن مفتاح الجامع موجود على جهاز واحد يصل إلى كل السيرفرات، فإذا سرق فقد سرقت مفاتيح البنية كلها. والتصميم الصحيح حساب اسمه `evidence` يدخل بمفتاح فقط، ويقرأ ما يقرؤه أي مستخدم عادي، ويحتاج صلاحية root لأمرين فقط في تجربتنا: `sshd -T` لأنه يقرأ مفاتيح المضيف ولا يعمل دون root، و`nginx -T` لأنه يقرأ ملفات قد تكون محمية. فتسمح sudoers بهذين الأمرين بالضبط، بمساراتهما الكاملة وخياراتهما، ولا شيء غيرهما:

```text
evidence ALL=(root) NOPASSWD: /usr/sbin/sshd -T, /usr/sbin/nginx -T
```

وتستطيع أن تضيق صلاحية المفتاح نفسه في ملف `authorized_keys` على كل سيرفر، فخيار `from="10.0.5.20"` قبل المفتاح يجعل OpenSSH يقبله من عنوان الجامع فقط، وخيار `restrict` يمنع تمرير المنافذ Port Forwarding والوكيل وحجز الطرفية، كما يشرح [دليل sshd](https://man.openbsd.org/sshd?ref=arabroot.io#AUTHORIZED%5FKEYS%5FFILE%5FFORMAT). ولم نستخدمهما في المختبر حتى نبقي الإعداد قصيراً، لكنهما في سيرفرات الشركة إلزاميان، والسبب أن مفتاحاً سرق من جهاز الجامع لا يفيد المهاجم من أي جهاز آخر، ولا يفتح له نفقاً إلى داخل الشبكة. وبهذا يصبح حساب الجمع ثلاث طبقات من التقييد: من أين يدخل، وماذا يستطيع أن يفعل في الجلسة، وأي أمرين فقط يرفعهما إلى صلاحية root.

وقد يتساءل البعض: أليس `nginx -T` نفسه خطراً لأنه يطبع كل ملفات الإعداد؟ والإجابة نعم إذا كانت ملفات الإعداد تحتوي أسراراً، مثل كلمات مرور في `proxy_set_header Authorization`، لذلك فإن قاعدة الفريق أن الأسرار لا تكتب في ملفات الإعداد أصلاً، وإذا كانت مكتوبة فالجامع لا يجمع هذا الملف، كما سوف نذكر في قسم الخصوصية.

## التجربة: سيرفران وجامع

نحتاج في المختبر سيرفرين يشبهان سيرفرات الشركة، وجامعاً، وكلها في Docker على شبكة واحدة. وصورة السيرفر مبنية على Debian 13 وفيها SSH وNginx وchrony وsudo، وملف `Dockerfile` لها كما يلي:

```dockerfile
FROM debian:trixie-20261020-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssh-server nginx-light chrony sudo \
 && rm -rf /var/lib/apt/lists/* && mkdir -p /run/sshd
# read-only evidence account: key login only, sudo limited to two read commands
RUN useradd -m -s /bin/bash evidence && mkdir -p /home/evidence/.ssh && chmod 700 /home/evidence/.ssh \
 && echo 'evidence ALL=(root) NOPASSWD: /usr/sbin/sshd -T, /usr/sbin/nginx -T' > /etc/sudoers.d/evidence && chmod 440 /etc/sudoers.d/evidence
COPY 10-hardening.conf /etc/ssh/sshd_config.d/10-hardening.conf
COPY chrony.conf /etc/chrony/chrony.conf
RUN sed -i 's/# server_tokens off;/server_tokens off;/' /etc/nginx/nginx.conf
COPY start.sh /start.sh
CMD ["/bin/sh", "/start.sh"]
```

في الملف أعلاه لاحظ التالي:

- الحساب `evidence` ينشأ دون كلمة مرور، فلا يدخل إلا بالمفتاح، وملف sudoers له بصلاحيات 440 لأن sudo يرفض الملفات التي يكتب فيها غير root.
- ملف `10-hardening.conf` يوضع في مجلد `sshd_config.d` وليس في الملف الرئيسي، وهذه الطريقة المعتمدة في Debian لأن الملف الرئيسي يستبدل عند التحديث.
- السطر الذي يفعل `server_tokens off` يخفي رقم إصدار Nginx من صفحات الخطأ والترويسات، وهو ما سوف يتحقق منه أحد الفحوص.

وملف التقوية نفسه خمسة أسطر:

```text
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 30
```

وملف chrony يشير إلى المرجع الوطني للوقت كما شرحنا في [مقال ضوابط ECC](https://arabroot.io/articles/%D8%AA%D8%AD%D9%82%D9%8A%D9%82-%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-ecc-%D8%A8%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/#lab-time):

```ini
# ECC 2-3-3-4: national time reference (SASO)
server time.saso.gov.sa iburst
pool 2.debian.pool.ntp.org iburst
makestep 1.0 3
rtcsync
```

أما الجامع فصورة Python الرسمية مع عميل SSH فقط:

```dockerfile
FROM python:3.13.17-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client && rm -rf /var/lib/apt/lists/*
WORKDIR /work
```

ثم نولد مفتاح الجامع، ونبني الصورتين، ونشغل السيرفرين بملف Compose يربط المفتاح العام فيهما للقراءة فقط:

```bash
mkdir -p keys && ssh-keygen -q -t ed25519 -N "" -C evidence-collector -f keys/collector
docker build -t ev-target:1 target
docker build -t ev-collector:1 collector
docker compose up -d
```

وملف `docker-compose.yml` يشغل السيرفرين دائماً، والجامع عند الطلب فقط بفضل الـ Profile المسمى `run`:

```yaml
name: ev
services:
  web1:
    image: ev-target:1
    container_name: ev-web1
    hostname: web1
    volumes: ["./keys/collector.pub:/keys/collector.pub:ro"]
  web2:
    image: ev-target:1
    container_name: ev-web2
    hostname: web2
    volumes: ["./keys/collector.pub:/keys/collector.pub:ro"]
  collector:
    image: ev-collector:1
    container_name: ev-collector
    profiles: ["run"]
    volumes: ["./:/work"]
    working_dir: /work
networks:
  default:
    name: ev-net
```

واستغرق بناء الصورتين في تجربتنا نحو دقيقة ونصف، ومعظم الوقت في تنزيل الحزم من مستودع Debian.

### ملف الفحوص: كل فحص مربوط بضوابطه

قلب الجامع ليس الكود، وإنما ملف الفحوص، فهو الذي يقول ماذا نجمع، ولماذا، وما الذي يعد نجاحاً. ونحفظه باسم `checks.json`:

```json
{
  "hosts": ["web1", "web2"],
  "checks": [
    {"id": "ssh-effective-config", "controls": ["ECC 2-2-3-1", "ECC 2-3-2"],
     "command": "sudo -n /usr/sbin/sshd -T",
     "expect": ["^permitrootlogin no$", "^passwordauthentication no$", "^maxauthtries [1-3]$"]},
    {"id": "nginx-effective-config", "controls": ["ECC 2-15-2"],
     "command": "sudo -n /usr/sbin/nginx -T",
     "expect": ["server_tokens off;"]},
    {"id": "time-sources", "controls": ["ECC 2-3-3-4"],
     "command": "grep -E '^(server|pool) ' /etc/chrony/chrony.conf",
     "expect": ["^server time\\.saso\\.gov\\.sa"]},
    {"id": "admin-accounts", "controls": ["ECC 2-2-3-4", "ECC 2-2-3-5"],
     "command": "getent group sudo",
     "expect": []},
    {"id": "package-versions", "controls": ["ECC 2-3-3-3", "ECC 2-10-3-4"],
     "command": "dpkg-query -W -f='${Package} ${Version}\\n' openssh-server nginx-light chrony sudo",
     "expect": []}
  ]
}
```

في الملف أعلاه لاحظ التالي:

- كل فحص له حقل `controls` بأرقام الضوابط التي يثبتها، فالدليل يعرف لماذا جمع، وعندما يسأل المقيم عن 2-3-3-4 تبحث في البيانات عن هذا الرقم.
- حقل `expect` قائمة أنماط منتظمة يجب أن تظهر في المخرج حتى يعد الفحص ناجحاً، مثل `^permitrootlogin no$`. ولاحظ أن `sshd -T` يطبع الأسماء بحروف صغيرة، فالنمط يجب أن يطابق ذلك.
- بعض الفحوص بلا أنماط، مثل قائمة المديرين وإصدارات الحزم، لأن الهدف منها الأرشيف وكشف التغير، وليس النجاح والفشل.
- `sudo -n` يعني ألا يسأل sudo عن كلمة مرور أبداً، فإذا لم تكن الصلاحية موجودة يفشل فوراً بدل أن ينتظر إدخالاً لن يأتي.

### الجامع: سكربت Python بالمكتبة القياسية

السكربت التالي يقرأ ملف الفحوص، ويشغل كل أمر على كل سيرفر عبر SSH، ويحفظ المخرج في ملف، ويحسب هاشه، ويقارنه بالتشغيل السابق، ويكتب بياناً يحمل هاش البيان السابق، ونحفظه باسم `collect.py`:

```python
# Agentless evidence collector: runs read-only commands over SSH, stores each output with its SHA-256,
# checks expectations, detects drift against the previous run, and chains run manifests together.
import datetime, hashlib, json, os, pathlib, re, shutil, subprocess, sys, tempfile

ROOT = pathlib.Path("evidence")
cfg = json.load(open("checks.json"))
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
run_dir = ROOT / now.strftime("%Y-%m-%dT%H%M%SZ")
runs = sorted(p for p in ROOT.glob("*/manifest.json")) if ROOT.exists() else []
prev = json.load(open(runs[-1])) if runs else None
prev_hash = hashlib.sha256(runs[-1].read_bytes()).hexdigest() if runs else None
prev_items = {(i["host"], i["id"]): i["sha256"] for i in prev["items"]} if prev else {}

# ssh ignores a private key that others can read; a key on a Windows bind mount shows as 0777, so use a 0600 copy
KEY = os.path.join(tempfile.mkdtemp(), "collector")
shutil.copyfile("keys/collector", KEY); os.chmod(KEY, 0o600)

def sh(host, command):
    r = subprocess.run(["ssh", "-i", KEY, "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new",
                        "-o", "UserKnownHostsFile=keys/known_hosts", f"evidence@{host}", command],
                       capture_output=True, text=True, timeout=60)
    return r.returncode, r.stdout

items = []
for host in cfg["hosts"]:
    for c in cfg["checks"]:
        code, out = sh(host, c["command"])
        missing = [p for p in c["expect"] if not re.search(p, out, re.M)]
        status = "error" if code != 0 else ("fail" if missing else "pass")
        f = run_dir / host / f"{c['id']}.txt"
        f.parent.mkdir(parents=True, exist_ok=True)
        f.write_text(out, encoding="utf-8")
        digest = hashlib.sha256(out.encode()).hexdigest()
        old = prev_items.get((host, c["id"]))
        drift = "new" if old is None else ("changed" if old != digest else "same")
        items.append({"host": host, "id": c["id"], "controls": c["controls"], "command": c["command"],
                      "file": str(f), "bytes": len(out.encode()), "sha256": digest, "status": status,
                      "missing": missing, "drift": drift})
        print(f"{host:5} {c['id']:24} {status:5} {drift:7} {digest[:12]} {', '.join(missing)}")

manifest = {"run_at": now.isoformat(), "collector": "collect.py 1.0", "previous_manifest_sha256": prev_hash, "items": items}
(run_dir / "manifest.json").write_text(json.dumps(manifest, indent=1), encoding="utf-8")
print("manifest", run_dir / "manifest.json", hashlib.sha256((run_dir / "manifest.json").read_bytes()).hexdigest()[:16])
sys.exit(1 if any(i["status"] != "pass" for i in items) else 0)
```

في الكود أعلاه لاحظ التالي:

- كل تشغيل يكتب في مجلد اسمه وقت التشغيل بتوقيت UTC، مثل `2026-10-26T055343Z`، فالمجلدات ترتب زمنياً بالاسم، ولا يكتب تشغيل فوق آخر أبداً.
- الحالة ثلاث قيم وليست اثنتين: `pass` و`fail` و`error`. والفرق بين الأخيرتين جوهري: `fail` يعني أننا جمعنا الدليل ووجدنا الإعداد مخالفاً، و`error` يعني أننا لم نستطع الجمع أصلاً، وسوف ترى في التجربة لماذا يجب ألا تختلطا.
- عمود التغير Drift يقارن هاش كل دليل بهاش الدليل نفسه في التشغيل السابق، فيقول `new` أو `same` أو `changed`.
- البيان يحمل `previous_manifest_sha256`، أي هاش ملف البيان السابق كاملاً، وهذا ما يجعل التشغيلات سلسلة.
- السكربت يخرج برمز 1 إذا لم تنجح كل الفحوص، فإذا جدولته في نظام CI أو مؤقت systemd ظهر الفشل في التنبيهات.

### التشغيل الأول: كل شيء «error»

شغلنا الجامع في النسخة الأولى من السكربت، قبل الأسطر الثلاثة الخاصة بنسخة المفتاح، فكان المخرج كما يلي:

```text
web1  ssh-effective-config     error new     e3b0c44298fc ^permitrootlogin no$, ^passwordauthentication no$, ^maxauthtries [1-3]$
web1  nginx-effective-config   error new     e3b0c44298fc server_tokens off;
web1  time-sources             error new     e3b0c44298fc ^server time\.saso\.gov\.sa
web1  admin-accounts           error new     e3b0c44298fc
web1  package-versions         error new     e3b0c44298fc 
```

والدليل على السبب في العمود الخامس قبل أي سجل: كل الهاشات تبدأ بـ `e3b0c44298fc`، وهذه بداية هاش SHA-256 للنص الفارغ، وهي قيمة يحفظها كل من يعمل مع الهاش، أي أن كل الأوامر أعادت مخرجاً فارغاً. وفي سجل sshd على السيرفر وجدنا:

```text
Connection closed by authenticating user evidence 192.168.80.4 port 52568 [preauth]
drop connection #0 from [192.168.80.4]:52600 on [192.168.80.3]:22 penalty: failed authentication
```

السطر الأول يعني أن الاتصال انقطع قبل نجاح المصادقة، والثاني يبين ميزة في الإصدارات الحديثة من OpenSSH اسمها PerSourcePenalties، فبعد عدة محاولات فاشلة من العنوان نفسه يرفض sshd اتصالاته مؤقتاً، وقد ظهرت قيمتها في الإعداد الفعلي لاحقاً كما يلي: `persourcepenalties crash:90 authfail:5 noauth:1 ...`. وهذا يعني أن جامعاً معطلاً يعيد المحاولة بسرعة قد يحجب نفسه. ثم شغلنا SSH من الجامع بخيار `-v` فظهر السبب الحقيقي:

```text
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
Permissions 0777 for 'keys/collector' are too open.
This private key will be ignored.
Load key "keys/collector": bad permissions
evidence@web2: Permission denied (publickey).
```

المفتاح الخاص في مجلد مربوط من قرص Windows عبر Docker Desktop، وكل الملفات فيه تظهر داخل الـ Container بصلاحيات 0777، وSSH يرفض استخدام مفتاح خاص يستطيع غير صاحبه قراءته، وهذا سلوك صحيح منه. والحل الذي في السكربت أن ينسخ المفتاح إلى مجلد مؤقت داخل الـ Container بصلاحيات 0600 قبل الاستخدام. وعلى سيرفر Linux حقيقي لن تواجه هذه المشكلة إذا أنشأت المفتاح بصلاحياته الصحيحة، لكن الدرس الأهم أن الجامع يجب ألا يحول الفشل إلى صمت: لو كان السكربت يكتب `fail` بدل `error` لظن الفريق أن كل السيرفرات مخالفة، ولو تجاهل الأخطاء لظن أنها كلها سليمة.

### التشغيل الناجح، وقراءة الأدلة

بعد التصحيح شغلنا الجامع على مجلد أدلة فارغ:

```bash
docker compose run --rm collector python collect.py
```

والمخرج سوف يكون كما يلي:

```text
web1  ssh-effective-config     pass  new     41976bc6c1a0
web1  nginx-effective-config   pass  new     681702e3dbfc
web1  time-sources             pass  new     ad20b9bdf77f
web1  admin-accounts           pass  new     9ebda5b52ab2
web1  package-versions         pass  new     fb529970afdb
web2  ssh-effective-config     pass  new     41976bc6c1a0
web2  nginx-effective-config   pass  new     681702e3dbfc
web2  time-sources             pass  new     ad20b9bdf77f
web2  admin-accounts           pass  new     9ebda5b52ab2
web2  package-versions         pass  new     fb529970afdb
manifest evidence/2026-10-26T055343Z/manifest.json 29fc406068fd0f08
```

في المخرج أعلاه لاحظ التالي:

- كل الفحوص `new` لأنه أول تشغيل ولا يوجد ما نقارن به.
- الهاش متطابق بين السيرفرين في كل فحص، مثل `41976bc6c1a0` لإعداد SSH، وهذا دليل إضافي مفيد بحد ذاته: السيرفران مبنيان من الصورة نفسها، فإعدادهما متطابق حرفاً حرفاً. وفي الواقع إذا اختلف هاش سيرفر عن بقية مجموعته فهذا أول ما تسأل عنه.
- السطر الأخير يطبع أول 16 حرفاً من هاش البيان نفسه، وهو الرقم الذي سوف نحفظه خارج السيرفر.

وملفات الأدلة نصوص عادية يقرؤها المدقق دون أداة. فملف إصدارات الحزم على web1 كان:

```text
chrony 4.6.1-3+deb13u2
nginx-light 1.26.3-3+deb13u10
openssh-server 1:10.0p1-7+deb13u4
sudo 1.9.16p2-3+deb13u2
```

وهذا دليل على 2-3-3-3 الخاص بإدارة حزم التحديثات، لأن اللاحقة `+deb13u` ورقمها تبين آخر تحديث أمني من Debian لكل حزمة، ويقارنه المدقق بنشرات [متتبع الأمان في Debian](https://security-tracker.debian.org/tracker/?ref=arabroot.io). وفي ملف إعداد SSH الفعلي 103 أسطر، والأسطر التي يتحقق منها الفحص ظهرت كما يلي، مع أرقام أسطرها في الملف:

```text
7:logingracetime 30
9:maxauthtries 3
16:permitrootlogin no
31:passwordauthentication no
32:kbdinteractiveauthentication no
```

ولاحظ أن الإعداد الفعلي فيه 103 أسطر بينما ملف التقوية خمسة أسطر فقط، فالباقي قيم افتراضية يطبعها `sshd -T` كما يطبقها، وهذا بالضبط ما يجعله دليلاً أقوى من ملف الإعداد نفسه: ملف الإعداد يقول ما كتبته، والمخرج يقول ما سوف يطبقه البرنامج بعد دمج كل الملفات والقيم الافتراضية.

### ملاحظة غير متوقعة: من هم المديرون فعلاً؟

فحص `admin-accounts` يطبع أعضاء مجموعة sudo، وكانت النتيجة على السيرفرين `sudo:x:27:`، أي لا أحد. وهذا صحيح من ناحية المجموعة، لكنه غير كامل: فالحساب `evidence` نفسه له صلاحية sudo على أمرين، وهي معرفة في ملف داخل `/etc/sudoers.d` وليست في المجموعة. والدرس أن دليل «من لديه صلاحيات مميزة» الذي يطلبه 2-2-3-4 لا يكفيه سؤال واحد، فالصلاحية قد تأتي من مجموعة sudo، أو من ملف في sudoers.d، أو من مفتاح في `authorized_keys` لحساب root. وفي الجامع الحقيقي أضافت الشركة فحصاً يعد ملفات sudoers.d ويطبع محتواها، وفحصاً آخر يطبع بصمات المفاتيح في ملفات authorized\_keys لكل الحسابات، وكلاهما يحتاج أمراً إضافياً في سطر sudoers، وهذا ثمن مقبول.

## التشغيل الثاني: كشف التغير Drift

لنجرب ما يحدث في الحياة الحقيقية بين تشغيلين: مطور يحتاج دخولاً سريعاً إلى web2 فيغير `PermitRootLogin` إلى `yes` «مؤقتاً»، وموظف دعم يضيف حساب متعاقد إلى مجموعة sudo على web1\. ثم نشغل الجامع:

```text
web1  ssh-effective-config     pass  same    41976bc6c1a0
web1  nginx-effective-config   pass  same    681702e3dbfc
web1  time-sources             pass  same    ad20b9bdf77f
web1  admin-accounts           pass  changed e5236fc6916b
web1  package-versions         pass  same    fb529970afdb
web2  ssh-effective-config     fail  changed cbf10f02aeff ^permitrootlogin no$
web2  nginx-effective-config   pass  same    681702e3dbfc
web2  time-sources             pass  same    ad20b9bdf77f
web2  admin-accounts           pass  same    9ebda5b52ab2
web2  package-versions         pass  same    fb529970afdb
manifest evidence/2026-10-26T075211Z/manifest.json c6e3061d7d267cac
```

في المخرج أعلاه لاحظ التالي:

- إعداد SSH على web2 `fail` و`changed`، والعمود الأخير يقول أي نمط مفقود بالضبط: `^permitrootlogin no$`. فلا يحتاج من يقرأ التقرير أن يفتح الملف ليعرف المشكلة.
- قائمة المديرين على web1 `pass` لأنها بلا نمط متوقع، لكنها `changed`، وهذا هو الإنذار: لا أحد يقول إن إضافة مدير خطأ، لكن كل إضافة يجب أن يقابلها طلب معتمد، والتغير هو الذي يدفعك للبحث عن الطلب. والملف الجديد يقول `sudo:x:27:contractor`.
- بقية الفحوص `same`، أي أن الدليل لم يتغير، وهذا أيضاً معلومة: خلال الفترة بين التشغيلين لم تتغير الحزم ولا مصادر الوقت ولا إعداد Nginx.

والبيان يحفظ كل ذلك بصيغة يقرؤها البرنامج والإنسان، وهذا العنصر الخاص بإعداد SSH على web2 كما هو في `manifest.json`:

```json
{
 "host": "web2",
 "id": "ssh-effective-config",
 "controls": [
  "ECC 2-2-3-1",
  "ECC 2-3-2"
 ],
 "command": "sudo -n /usr/sbin/sshd -T",
 "file": "evidence/2026-10-26T075211Z/web2/ssh-effective-config.txt",
 "bytes": 4685,
 "sha256": "cbf10f02aeffebcf60d8da6c9b5b4437bbbe0e6aa12f992535c8b20e9ffd14d9",
 "status": "fail",
 "missing": [
  "^permitrootlogin no$"
 ],
 "drift": "changed"
}
```

ولاحظ حجم الملف: 4685 بايتاً مقابل 4684 في التشغيل الأول، والفرق بايت واحد هو الفرق بين `no` و`yes`.

والشكل التالي يجمع التشغيلات الثلاثة التي أجريناها في التجربة، ومنها الثالث الذي سوف نشرحه بعد قليل، في جدول واحد، وهو الشكل الذي يحتاجه من يقرأ نتائج الجامع كل صباح:

![جدول لخمسة فحوص على سيرفرين في ثلاثة تشغيلات بألوان الحالة: الأول كله pass وnew، والثاني إعداد SSH على web2 fail وchanged وحسابات المديرين على web1 changed، والثالث إعداد SSH وNginx على web1 error بعد حذف ملف sudoers](https://arabroot.io/content/images/2026/10/evidence-collection-05-runs.webp)

كل خانة حالة وتغير، والخانات غير الخضراء هي عمل اليوم

## ما الذي يثبته الدليل بالضبط؟

هنا ظهرت ملاحظة دقيقة: نحن غيرنا ملف الإعداد على web2 ولم نعد تحميل sshd، ومع ذلك أظهر الفحص `permitrootlogin yes`. والسبب أن `sshd -T` لا يسأل العملية التي تعمل، وإنما يقرأ ملفات الإعداد من القرص ويطبع ما سوف يطبق. فهل العملية التي تعمل تسمح بدخول root فعلاً؟ جربنا ذلك بوضع المفتاح العام نفسه لحساب root على web2، ثم محاولة الدخول قبل إعادة التحميل وبعدها:

```text
root@web2: Permission denied (publickey).
reloaded
root login accepted
```

قبل إعادة التحميل رفض sshd دخول root رغم أن الملف يقول `yes`، لأن العملية ما زالت تعمل بالإعداد الذي قرأته عند بدايتها، وبعد إرسال إشارة HUP إليها قرأت الملف من جديد وقبلت الدخول. والشكل التالي يلخص الفرق:

![مقارنة بين ملفات الإعداد على القرص التي يقرؤها sshd -T ويثبت ما سوف يعمل بعد إعادة التشغيل، والبرنامج الذي يعمل الآن ويثبته اختبار سلوكي من الخارج](https://arabroot.io/content/images/2026/10/evidence-collection-04-file-vs-running.webp)

لقطة الإعداد تقول ما ينبغي أن يكون، والاختبار السلوكي يقول ما هو كائن

والنتيجة العملية أن دليل «الإعداد صحيح» ليس هو دليل «الضابط يعمل». فللضوابط المهمة اجمع النوعين: لقطة الإعداد، واختباراً سلوكياً من الخارج، مثل محاولة دخول root بمفتاح اختبار ورفضها، أو طلب إلى Nginx والتحقق من غياب رقم الإصدار في الترويسة. والاختبار السلوكي أقرب إلى ما يفعله المهاجم، لذلك يقبله المدقق بثقة أكبر، لكنه أصعب في الكتابة والصيانة، وتبدأ به الشركة في الضوابط التي يطلبها العميل بالاسم.

## ماذا يحدث إذا فقد الجامع صلاحيته؟

الخطر الأكبر على أي جامع أدلة ليس أن يكتشف مخالفة، وإنما أن يتوقف عن الجمع دون أن يعلم أحد. فحذفنا ملف sudoers الخاص بحساب `evidence` على web1، كما قد يحدث عندما يعيد أحدهم بناء السيرفر من صورة قديمة، وشغلنا الجامع:

```text
web1  ssh-effective-config     error changed e3b0c44298fc ^permitrootlogin no$, ^passwordauthentication no$, ^maxauthtries [1-3]$
web1  nginx-effective-config   error changed e3b0c44298fc server_tokens off;
web1  time-sources             pass  same    ad20b9bdf77f
web1  admin-accounts           pass  same    e5236fc6916b
web1  package-versions         pass  same    fb529970afdb
web2  ssh-effective-config     fail  same    cbf10f02aeff ^permitrootlogin no$
```

الفحصان اللذان يحتاجان sudo أصبحا `error` بهاش النص الفارغ، وبقية الفحوص تعمل لأنها لا تحتاج صلاحية. وعندما شغلنا الأمر نفسه يدوياً بالحساب نفسه كان رد sudo: `sudo: a password is required`، وهذا هو أثر الخيار `-n`: يفشل فوراً بدل أن ينتظر. ولولا التفريق بين `error` و`fail` لاختلطت هذه الحالة بالمخالفة الحقيقية على web2، فاجعل تنبيهك على `error` مستقلاً، ويذهب إلى من يملك الجامع، بينما يذهب `fail` إلى من يملك السيرفر.

## هل يكشف الجامع تعديل الأدلة بعد جمعها؟

الدليل الذي يستطيع أي شخص أن يعدله دون أثر ليس دليلاً، والجامع يحفظ هاش كل ملف في البيان، ويحفظ في كل بيان هاش البيان الذي قبله. وللتحقق كتبنا سكربتاً صغيراً يعيد حساب كل شيء، ونحفظه باسم `verify.py`:

```python
# Re-hashes every evidence file and checks that each manifest points to the one before it
import hashlib, json, pathlib
runs = sorted(pathlib.Path("evidence").glob("*/manifest.json"))
prev = None
for m in runs:
    data = json.loads(m.read_text(encoding="utf-8"))
    link = "ok" if data["previous_manifest_sha256"] == (hashlib.sha256(prev.read_bytes()).hexdigest() if prev else None) else "BROKEN"
    bad = [i["file"] for i in data["items"] if hashlib.sha256(pathlib.Path(i["file"]).read_bytes()).hexdigest() != i["sha256"]]
    print(f"{m.parent.name}  items={len(data['items'])}  chain={link}  tampered={len(bad)} {' '.join(bad)}")
    prev = m
```

وعلى التشغيلين السليمين كان المخرج:

```text
2026-10-26T055343Z  items=10  chain=ok  tampered=0
2026-10-26T075211Z  items=10  chain=ok  tampered=0 
```

ثم نفترض أن أحداً أراد أن يخفي أن إعداد SSH على web2 كان سليماً في التشغيل الأول، أو أن يغير أي شيء فيه، فعدلنا ملف الدليل بمسافة واحدة في نهاية سطر:

```text
2026-10-26T055343Z  items=10  chain=ok  tampered=1 evidence/2026-10-26T055343Z/web2/ssh-effective-config.txt
2026-10-26T075211Z  items=10  chain=ok  tampered=0 
```

الهاش كشف التعديل وحدد الملف. والخطوة التالية للمعدل الذكي أن يعيد حساب الهاش في بيان التشغيل الأول حتى يطابق الملف المعدل، ففعلنا ذلك أيضاً:

```text
2026-10-26T055343Z  items=10  chain=ok  tampered=0
2026-10-26T075211Z  items=10  chain=BROKEN  tampered=0
```

البيان الأول أصبح متسقاً مع ملفاته، لكن تعديله غير هاشه، والبيان الثاني يحمل الهاش القديم، فانقطعت السلسلة عند الثاني. والشكل التالي يبين الآلية:

![بيانان: الأول بتاريخ 2026-10-26T055343Z وفيه هاش كل ملف وprevious_manifest_sha256 بقيمة null، والثاني بتاريخ 2026-10-26T075211Z ويحمل هاش البيان الأول، وتحتهما حالتان: تعديل ملف يكشفه الهاش، وإعادة حساب البيان يكشفها انقطاع السلسلة](https://arabroot.io/content/images/2026/10/evidence-collection-03-chain.webp)

تعديل الملف يكشفه هاشه، وإخفاء التعديل في البيان يكشفه البيان الذي بعده

لكن هذه الحماية لها حد واضح يجب أن تعرفه: من يملك المجلد كله يستطيع أن يعيد حساب كل البيانات من الأول إلى الأخير، فتعود السلسلة سليمة. والحل الذي شرحناه بالتفصيل في [سجل التدقيق Audit Trail](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/#tamper-evidence) هو المرساة: أن تحفظ هاش آخر بيان في مكان لا يملكه الجامع ولا من يملك السيرفر، مثل تخزين كائنات بقفل لا يسمح بالتعديل ولا الحذف كما في [S3 Object Lock](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A7%D9%84%D9%89-s3-%D9%85%D8%B9-object-lock-%D8%B6%D8%AF-%D8%A7%D9%84%D9%81%D8%AF%D9%8A%D8%A9/)، أو في أداة GRC كحقل في الدليل، أو حتى في رسالة بريد إلى صندوق يملكه المدقق الداخلي. وعندها يحتاج المعدل أن يعدل في مكانين يملكهما شخصان مختلفان.

## كيف تكتب فحصاً جيداً؟

بعد أسبوعين من تشغيل الجامع يومياً اكتشف الفريق أن جودة الأدلة تعتمد على كتابة الفحوص أكثر مما تعتمد على الكود، ووضع لذلك قواعد خرجت من أخطاء حقيقية:

- **حقيقة واحدة لكل فحص:** فحص «إعداد SSH» يجمع الإعداد كاملاً ويتحقق من ثلاثة أنماط، وهذا مقبول لأنها كلها في مخرج واحد، أما خلط إعداد SSH وقائمة المستخدمين في أمر واحد فيجعل التغير في أحدهما يظهر كأنه تغير في الآخر.
- **مخرج ثابت:** الهاش يتغير مع أي حرف، فإذا كان الأمر يطبع وقتاً أو رقم عملية أو يرتب النتائج عشوائياً فسوف يظهر الفحص `changed` في كل تشغيل، ويتعلم الفريق أن يتجاهل التغير، وهذا أخطر من غياب الفحص. ولاحظ في التجربة أن الهاشات بقيت متطابقة بين السيرفرين وبين التشغيلين حين لم يتغير شيء، لأن الأوامر التي اخترناها تطبع نتيجة ثابتة.
- **الأمر نفسه الذي يكتبه الإنسان:** المدقق يفهم `sshd -T` و`dpkg-query`، ولا يفهم سكربتاً من مئتي سطر يستخرج القيم بطرق خاصة، والدليل الذي يفهمه المدقق بنفسه لا يحتاج من يشرحه.
- **النمط يصف الضابط وليس الإعداد كله:** النمط `^maxauthtries [1-3]$` يقبل أي قيمة من 1 إلى 3، لأن المعيار الداخلي للشركة يقول «ثلاث محاولات كحد أقصى»، ولو كتبنا `^maxauthtries 3$` لفشل الفحص عند من يشدد الإعداد إلى محاولتين، وهذا ليس مخالفة.
- **ملف الفحوص في Git:** كل إضافة أو تعديل لفحص يمر بمراجعة كما شرحنا للسياسات في [مقال السياسات](https://arabroot.io/articles/%D8%B3%D9%8A%D8%A7%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D9%85%D9%86-%D8%A7%D9%84%D8%B3%D9%8A%D8%A8%D8%B1%D8%A7%D9%86%D9%8A-%D9%82%D9%88%D8%A7%D9%84%D8%A8-%D9%88%D9%83%D9%8A%D9%81-%D8%AA%D9%83%D8%AA%D8%A8-%D8%B3%D9%8A%D8%A7%D8%B3%D8%A9-%D8%AA%D8%B7%D8%A8%D9%82/#lab)، لأن من يملك ملف الفحوص يستطيع أن يجعل أي مخالفة تنجح بتغيير نمط واحد.

## من مجلد الأدلة إلى أداة الامتثال

الأدلة في مجلد على سيرفر الجامع لا يراها المدقق، والخطوة التالية في القصة أن يصل كل دليل إلى الضابط المطبق المقابل في CISO Assistant. والتصميم الذي اعتمدته الشركة بسيط: الجامع يرفع مجلد كل تشغيل إلى مخزن الأدلة كما هو، ثم يحدث في الأداة دليلاً واحداً لكل فحص، اسمه رقم الفحص واسم السيرفر، ورابطه إلى الملف في المخزن، وفي وصفه الهاش وتاريخ الجمع والحالة. وقد بينا في [قسم الـ API في مقال CISO Assistant](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D8%AA%D8%AB%D8%A7%D9%84-%D9%85%D8%B9-ciso-assistant/#api) كيف تحصل على رمز الدخول وكيف تنشئ الأدلة وتربطها بالضوابط المطبقة. ولاحظ أن الحقل `controls` في ملف الفحوص هو الذي يربط الطرفين، فاكتب فيه أرقام الضوابط بالصيغة نفسها التي في مكتبة الأداة، مثل `2-3-3-4`، حتى يكون الربط آلياً.

## من يقرأ نتائج الجامع كل صباح؟

الجامع الذي لا يقرأ أحد مخرجه يجمع أدلة على أن الشركة رأت المشكلة وتجاهلتها، وهذا أسوأ من عدم الجمع. لذلك ربطت الشركة كل حالة بإجراء محدد ومالك واضح. فكل `fail` يفتح تذكرة لمالك السيرفر، وإذا كانت المخالفة مقصودة، مثل فتح دخول root لصيانة طارئة، فإنها تسجل استثناء بمدة وموافقة كما في السياسة، ويضاف رقم الاستثناء إلى التذكرة. وكل `changed` على قائمة المديرين يجب أن يقابله طلب صلاحية معتمد، وإذا لم يوجد طلب فهو حادث أمني يعامل بإجراء الحوادث. وكل `error` يذهب إلى مالك الجامع، لأنه يعني أن هناك سيرفراً لا نعرف حالته.

وفي آخر الشهر يجمع مسؤول الأمن ثلاثة أرقام للإدارة: نسبة الفحوص الناجحة، وعدد التغيرات التي لم يقابلها طلب، ومتوسط الأيام لإغلاق المخالفة. وعندما تتكرر مخالفة على الضابط نفسه تصبح مخاطرة في [سجل المخاطر](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D9%85%D8%AE%D8%A7%D8%B7%D8%B1-risk-register-%D9%85%D8%B9-simplerisk/) بقرار معالجة، لأن تكرارها يعني أن المشكلة في العملية وليست في سيرفر واحد. وبهذا الشكل يصبح الجامع جزءاً من دورة GRC كاملة: الضابط يطبق، والجامع يثبت، والمراجعة تقرر، والسجل يتذكر.

## الجدولة والاحتفاظ

كم مرة تجمع؟ والإجابة تختلف بحسب الدليل وليس بحسب الجامع. فإعدادات السيرفرات تتغير مع أي تدخل، والشركة تجمعها يومياً، لأن التغير الذي يكتشف بعد يوم يمكن التحقيق فيه، والذي يكتشف بعد ربع سنة لا يعرف أحد سببه. أما قائمة المديرين فيومياً أيضاً، لأنها أهم مؤشر على تجاوز إجراءات الصلاحيات. وتقارير فحص الثغرات تجمع بعد كل فحص، ومحاضر اختبار الاستعادة بعد كل اختبار. والتشغيل الواحد في تجربتنا استغرق نحو 31 ثانية لعشرة فحوص على سيرفرين، ومعظمها وقت فتح اتصال SSH لكل أمر، فإذا كبر عدد السيرفرات فاستخدم خيار `ControlMaster` في SSH لإعادة استخدام الاتصال، أو شغل الفحوص على السيرفرات بالتوازي.

والحجم صغير جداً: ثلاثة تشغيلات على سيرفرين بلغت 120 كيلوبايت، أي أن تشغيلاً يومياً لسنة كاملة على عشرة سيرفرات يبقى في حدود عشرات الميجابايتات. فلا تقم بحذف الأدلة القديمة لتوفير المساحة، وإنما احتفظ بها المدة التي تحددها سياستك، والضابط 2-12-3-5 يطلب 12 شهراً للسجلات الأمنية، وكثير من الجهات تجعل مدة الأدلة مثلها على الأقل، لأن المدقق يسأل عادة عن الفترة منذ التدقيق السابق.

## عندما يصبح عندك ثلاثون سيرفراً

الجامع في التجربة يقرأ قائمة السيرفرات من ملف الفحوص، وهذا يكفي لسيرفرين أو ثلاثة، ومع النمو تظهر أربع مشكلات، ولكل منها حل معروف لا يحتاج أداة جديدة:

- **مصدر القائمة:** القائمة المكتوبة يدوياً تنسى السيرفر الجديد، وهو بالضبط السيرفر الذي لم يقوه أحد بعد. لذلك اجعل الجامع يقرأ قائمة السيرفرات من سجل الأصول أو من أداة البنية ككود، وأي سيرفر في السجل لا يصل إليه الجامع يظهر `error`، كما أن أي سيرفر يصل إليه وليس في السجل ملاحظة.
- **الزمن:** عشرة فحوص على سيرفرين استغرقت 31 ثانية لأن كل أمر يفتح اتصال SSH جديداً بمصافحته وتشفيره، ومع ثلاثين سيرفراً وعشرين فحصاً يصبح ذلك دقائق طويلة. وخيار `ControlMaster` في SSH يعيد استخدام الاتصال الأول لبقية الأوامر على السيرفر نفسه، والتوازي بين السيرفرات يقسم الزمن على عددها.
- **الطريق الشبكي:** السيرفرات الحساسة لا يصلها الجامع مباشرة، وإنما عبر بوابة وصول، كما شرحنا في [سيرفر القفز Bastion](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-bastion-host-%D9%88-jump-server/)، وحساب الجامع في البوابة له صلاحيات القراءة نفسها، وجلساته تسجل مثل جلسات المديرين.
- **المفتاح:** مفتاح واحد يصل إلى كل السيرفرات يجب أن يدار كسر حساس: يغير دورياً، ويحفظ في خزنة، ولا ينسخ إلى أجهزة الموظفين. والأفضل مع النمو استخدام شهادات SSH قصيرة العمر بدل المفاتيح الدائمة، فتنتهي صلاحيتها وحدها إذا سرقت.

## وماذا عن Windows والأجهزة الشبكية؟

الفكرة نفسها تعمل على غير Linux، لأن السؤال دائماً واحد: ما الأمر الثابت الذي يطبع الحالة؟ ففي Windows Server يمكن تثبيت خادم OpenSSH، وعندها يرسل الجامع أوامر PowerShell بالطريقة نفسها، مثل `Get-LocalGroupMember -Group Administrators` لقائمة المديرين المحليين، وحساب الجمع يحتاج صلاحيات القراءة المناسبة فقط. وفي أجهزة الشبكة، مثل موجهات MikroTik التي شرحنا إعداد جدار الحماية فيها في [جدار الحماية في MikroTik](https://arabroot.io/articles/%D8%AC%D8%AF%D8%A7%D8%B1-%D8%A7%D9%84%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D9%81%D9%8A-mikrotik-routeros-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/)، يطبع الأمر `/export` الإعداد كاملاً نصاً يحفظ ويقارن كما نقارن `sshd -T`، وتغير قاعدة واحدة في جدار الحماية يظهر `changed` في اليوم التالي. لكن انتبه هنا إلى أن مخرج الإعداد في أجهزة الشبكة قد يتضمن معلومات حساسة مثل مفاتيح شبكة VPN أو كلمات مرور بعض الخدمات بحسب الإصدار والخيارات، فراجع المخرج قبل أن تجعله دليلاً يحفظ سنة، واحذف منه ما لا يلزم، كما في قواعد الخصوصية السابقة.

## الخصوصية والأسرار في الأدلة

الجامع يرى الأنظمة من الداخل، والأدلة التي يجمعها قد تحتوي ما لا يجب أن يخرج من السيرفر. ولذلك وضعت الشركة ثلاث قواعد قبل أن تضيف أي فحص:

- **لا أسرار في الأدلة:** أي أمر قد يطبع كلمة مرور أو مفتاحاً أو رمز دخول لا يضاف، وإذا كان المطلوب إثبات وجود سر فالدليل هو بصمته أو تاريخ تغييره، وليس قيمته.
- **أقل بيانات شخصية:** قائمة المديرين بأسماء الحسابات تكفي، ولا حاجة لأسمائهم الكاملة ولا عناوين بريدهم، والسبب مبدأ الحد الأدنى في نظام حماية البيانات الشخصية، كما شرحنا في [مقال PDPL](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-pdpl-%D9%84%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9/).
- **مخزن الأدلة أصل حساس:** مجموع الأدلة خريطة لإعدادات البنية كلها، فصلاحية قراءته محدودة، ونسخه الاحتياطية مشفرة، ويظهر في سجل الأصول.

## مصادر أخرى للأدلة الآلية

الجامع في التجربة يغطي السيرفرات، وبقية الأدلة تأتي من الأنظمة التي شرحناها في مقالات سابقة، وكل واحد منها يعطي دليلاً بالطريقة نفسها: أمر أو طلب ثابت، ومخرج يحفظ بتاريخه وهاشه:

| الضابط                     | المصدر                                                                                                                                                                                                                                                                                                      | الدليل                                                          |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| 2-2-3-2 MFA للمديرين       | مزود الهوية، مثل [authentik](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-authentik-%D9%84%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF/)، عبر الـ API                                                                                                                 | قائمة المستخدمين في مجموعة المديرين وحالة أجهزة التحقق لكل منهم |
| 2-10-3-1 فحص الثغرات       | Trivy أو OpenVAS، كما في [أدوات إدارة الثغرات](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AB%D8%BA%D8%B1%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/)                                            | تقرير JSON بتاريخ الفحص ونسخة قاعدة الثغرات                     |
| 2-9-3-3 اختبار الاستعادة   | سكربت استعادة مجدول، كما في [قاعدة 3-2-1](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/) | سجل الاستعادة وعدد الملفات أو الصفوف المستعادة والمقارنة بالأصل |
| 2-12-3-5 الاحتفاظ بالسجلات | منصة السجلات، مثل [Loki](https://arabroot.io/articles/%D8%AC%D9%85%D8%B9-%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logs-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A7-%D9%85%D8%B9-grafana-loki/)                                                                                                                   | مخرج /config بمدة الاحتفاظ                                      |
| 2-1 جرد الأصول             | [Snipe-IT](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-snipe-it-%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%A7%D8%AC%D9%87%D8%B2%D8%A9-%D9%88%D8%A7%D9%84%D8%B1%D8%AE%D8%B5-%D9%84%D9%84%D8%A7%D9%86%D8%AA%D8%A7%D8%AC/) عبر الـ API                                                                 | تصدير الأصول بتاريخه، ومقارنته بالسيرفرات التي يصل إليها الجامع |

والسطر الأخير يستحق وقفة: إذا كان الجامع يصل إلى سيرفر غير موجود في سجل الأصول، أو كان في السجل سيرفر لا يصل إليه الجامع، فكلاهما ملاحظة، والمقارنة بين القائمتين من أرخص الفحوص وأكثرها فائدة.

## ماذا نسلم للمدقق من كل هذا؟

المدقق لا يريد مجلداً فيه آلاف الملفات، وإنما يريد أن يختار هو، وهذا ما يغيره الجمع اليومي جذرياً. ففي التدقيق التقليدي يطلب المدقق «دليلاً على أن SSH لا يسمح بدخول root»، فتعطيه لقطة واحدة من يوم التدقيق، وهي تثبت أن الإعداد كان سليماً في ذلك اليوم فقط. أما المدقق الذي يريد أن يعرف هل عمل الضابط طوال الفترة، وهو ما تسأل عنه تقارير مثل SOC 2 من النوع الذي يغطي فترة، فيختار عينة من تواريخ داخل الفترة ويطلب الدليل لكل تاريخ. ومع جامع يعمل يومياً تجيب عن أي تاريخ يختاره في دقائق: مجلد ذلك اليوم، وملف السيرفر المطلوب، والبيان بهاشه، وسلسلة البيانات حتى المرساة تثبت أن الملف لم يكتب لاحقاً.

والأهم أن العينة قد تقع على يوم فيه مخالفة، مثل يوم فتح دخول root على web2 في تجربتنا، وهنا يظهر الفرق بين برنامج امتثال حقيقي وبرنامج للعرض: الدليل نفسه يبين المخالفة، والتذكرة تبين أنها اكتشفت في اليوم التالي، والاستثناء يبين من وافق ولماذا، والتشغيل الذي بعده يبين أنها أغلقت. وهذه السلسلة من الأدلة تقنع المدقق أكثر من سنة كاملة بلا أي مخالفة، لأنها تثبت أن الضابط الرقابي نفسه يعمل، أي أن المؤسسة ترى ما يحدث فيها وتتصرف. لذلك لا تقم بحذف التشغيلات التي فيها مخالفات، ولا تحاول أن تجعل التقرير «أخضر» قبل التدقيق، فالمدقق الذي يرى سجلاً خالياً من أي مخالفة لسنة كاملة يسأل عادة هل يعمل الجامع أصلاً.

## قائمة تحقق لجامع الأدلة

| السؤال                                                | في التجربة                                 |
| ----------------------------------------------------- | ------------------------------------------ |
| هل يدخل الجامع بحساب قراءة لا بحساب root؟             | حساب evidence بمفتاح فقط                   |
| هل sudo محدود بأوامر كاملة المسار؟                    | أمران فقط، مع sudo -n                      |
| هل كل فحص مربوط بأرقام الضوابط؟                       | حقل controls                               |
| هل يفرق الجامع بين المخالفة والعجز عن الجمع؟          | fail وerror بتنبيهين منفصلين               |
| هل يكشف التغير بين تشغيلين؟                           | عمود drift                                 |
| هل يكشف تعديل الأدلة بعد جمعها؟                       | هاش لكل ملف وبيان مسلسل                    |
| هل للسلسلة مرساة خارج السيرفر؟                        | الخطوة التالية: S3 Object Lock أو أداة GRC |
| هل يجمع النوعين للضوابط المهمة؟                       | لقطة الإعداد، ثم اختبار سلوكي لدخول root   |
| هل الأدلة خالية من الأسرار والبيانات الشخصية الزائدة؟ | لا أوامر تطبع أسراراً، وأسماء حسابات فقط   |
| هل يقارن الجامع بسجل الأصول؟                          | في المرحلة التالية مع Snipe-IT             |

## الخلاصة

- الدليل الجيد يجيب عن أربعة أسئلة دون شرح: متى، ومن أي نظام، وهل يعاد إنتاجه، وهل يكشف تعديله، ولقطة الشاشة تفشل في أغلبها.
- الجمع دون وكيل عبر SSH بحساب قراءة وsudo محدود يكفي لعدد صغير من السيرفرات، ولا يضيف إلى السيرفر إلا مفتاحاً وسطراً.
- كل الفحوص كانت `error` في التشغيل الأول لأن المفتاح الخاص ظهر بصلاحيات 0777 من مجلد Windows، والهاش `e3b0c44298fc` كشف أن المخرج فارغ.
- التشغيل الثاني كشف تغيير `PermitRootLogin` ومديراً جديداً، و`sshd -T` يثبت ما في الملف، والاختبار السلوكي يثبت ما يعمل.
- الهاش والبيان المسلسل كشفا تعديل دليل ثم إخفاءه، وتبقى المرساة خارج السيرفر شرطاً لحماية حقيقية.

وفي المقالين التاليين من القصة نضع كل ذلك أمام المدقق، ونرى كيف يبدو المسار نفسه بلغة ISO/IEC 27001:

[![](https://arabroot.io/content/images/2026/10/iso27001-open-source-cover-cover.webp)دليلISO 27001 بأدوات مفتوحة المصدر من المخاطر إلى التدقيقكيف تبني شركة صغيرة نظام ISO/IEC 27001 على ما بنته لضوابط ECC، من النطاق وسجل المخاطر إلى بيان قابلية التطبيق والتدقيق الداخلي.](https://arabroot.io/articles/iso-27001-%D8%A8%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AE%D8%A7%D8%B7%D8%B1-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82/)

## سجل التحديثات

أكتوبر 2026: كتابة المقال وتجربة جامع الأدلة على سيرفرين Debian 13 بـ OpenSSH 10.0 وNginx 1.26.3، والجامع على Python 3.13.17.