في نهاية الربع الأول من التزامها بالضوابط الأساسية ECC-2:2024، كان على شركة منصة حجز مواعيد العيادات التي تابعناها في مقال CISO Assistant ومقال سجل المخاطر أن ترسل للمستشفى الحكومي تقييمها الذاتي مع الأدلة. وقضى مدير التقنية يومين كاملين يفتح لوحات التحكم ويأخذ لقطات شاشة: إعداد MFA في مزود الهوية، وإعداد SSH على كل سيرفر، وصفحة سياسة الاحتفاظ بالسجلات، وقائمة المديرين. وبعد أسبوعين عادت الملاحظات من فريق الأمن في المستشفى: «لقطة إعداد SSH لا تبين اسم السيرفر ولا تاريخها، ولقطتان من السيرفر نفسه فيما يبدو، وأين الدليل على السيرفر الثالث؟». ولم يكن المدير يكذب، لكنه لم يستطع أن يثبت أنه لا يكذب، وهذا بالضبط ما يفشل فيه الدليل اليدوي.
والسؤال الذي خرج به الفريق: لماذا نجمع بأيدينا شيئاً تعرفه الأنظمة نفسها؟ فالسيرفر يعرف إعداده، ومزود الهوية يعرف من لديه MFA، ومنصة السجلات تعرف مدة الاحتفاظ، والمطلوب أن نسأل كل نظام بأمر ثابت، ونحفظ الإجابة بتاريخها ومصدرها، ونثبت أنها لم تتغير بعد حفظها. وقد ذكرنا في مصطلحات GRC ما يقبله المدقق دليلاً، وبنينا في مقال أفكار المنتجات نموذجاً صغيراً يربط نتائج الفحص بأرقام الضوابط، أما هنا فنبني جامع أدلة كاملاً على سيرفرين، ونجرب عليه ما يحدث في الواقع: إعداد يتغير بين تشغيلين، وحساب مدير يضاف دون أن يعلم أحد، ودليل يعدله شخص بعد جمعه، وجامع يفقد صلاحيته بصمت.
دليلإدارة الامتثال Compliance مع CISO Assistant على سيرفركنشغل CISO Assistant ونحمل فيه ضوابط ECC-2:2024 ونقيم منصة حقيقية ونربطها بالضوابط والأدلة ونصدر التقرير، مع كل مشكلة واجهناها في الطريق.
دليلسجل المخاطر Risk Register مع SimpleRisk على سيرفركنبني سجل مخاطر حقيقياً بنموذج الهيئة ثم بأداة SimpleRisk، ونرى لماذا أعطت الأداة كل المخاطر مستوى واحداً، وكيف تكتب منهجية تعطي الأرقام معناها.
دليلتحقيق ضوابط ECC بأدوات مفتوحة المصدر Open Sourceخريطة تربط كل ضابط فرعي في الضوابط الأساسية ECC بالأداة مفتوحة المصدر التي تحققه وبالمقال الذي شرحها، مع إثبات ضابطين بمخرج حقيقي.وسوف نناقش في هذا المقال ما يلي:
- أنواع الأدلة، والأسئلة الأربعة التي يسألها المدقق عن كل دليل، ولماذا تفشل لقطة الشاشة فيها.
- ثلاث طرق للجمع الآلي: الوكيل Agent، والجمع دون وكيل Agentless عبر SSH، والـ API، ومتى تستخدم كلاً منها.
- تصميم حساب جمع للقراءة فقط بأقل صلاحية، ولماذا لا تستخدم root.
- تجربة كاملة: سيرفران وجامع في Docker، وملف فحوص مربوط بأرقام الضوابط، وسكربت Python بالمكتبة القياسية.
- المشكلة التي جعلت كل الفحوص «error» في التشغيل الأول، وكيف قرأنا السبب.
- كشف التغير Drift بين تشغيلين، والفرق بين الإعداد في الملف والإعداد الذي يعمل فعلاً.
- كشف تعديل الأدلة بالهاش والبيان المسلسل، ولماذا يحتاج البيان مرساة خارج السيرفر.
- الجدولة والاحتفاظ والخصوصية، وربط الأدلة بأداة GRC، وقائمة تحقق.
ما الذي يجعل الدليل دليلاً؟
عندما يقرأ المدقق دليلاً فهو يسأل أربعة أسئلة، حتى لو لم يقلها بصوت عال: متى أخذ؟ ومن أي نظام؟ وهل أستطيع أن أطلب إعادته فأحصل على النتيجة نفسها؟ وهل أعرف إن عدله أحد بعد أخذه؟ والشكل التالي يضع الأنواع الأربعة الشائعة أمام هذه الأسئلة:

ولقطة الشاشة ليست مرفوضة دائماً، فبعض الأنظمة لا تعطي غيرها، لكنها أضعف الأدلة لثلاثة أسباب: لا تحمل مصدرها، فصورة لوحة تحكم لا تقول على أي سيرفر أو حساب أخذت، ولا يعاد إنتاجها إلا بشخص يفتح الشاشة نفسها، ولا شيء يمنع تعديلها. ولهذا طلبت ملاحظات المستشفى اسم السيرفر والتاريخ. أما لقطة الإعداد التي يطبعها النظام نفسه، مثل مخرج sshd -T، فتحمل الأمر الذي أنتجها، ويستطيع أي شخص أن يعيد تشغيل الأمر ويقارن، وإذا حفظنا معها الهاش SHA-256 والتاريخ والسيرفر في ملف بيان Manifest، فقد أجبنا عن الأسئلة الأربعة دون أن نشرح شيئاً في الجلسة.
ولاحظ أن الضوابط نفسها تطلب نوعين مختلفين من الأدلة. فأغلب المكونات الفرعية في المكون الثاني من ECC تنتهي بضابط «يراجع تطبيق المتطلبات دورياً»، ودليله محضر مراجعة بتاريخ، وهو قرار بشري لا يجمع آلياً، لكن الضابط الذي يسبقه، أي الحد الأدنى التقني، دليله حالة الأنظمة، وهذه هي التي سوف نؤتمتها. وكما ذكرنا في مقال ضوابط ECC، الأداة تخدم «طبق» و«الحد الأدنى»، والناس يخدمون «وثق» و«راجع».
من التدقيق السنوي إلى المراقبة المستمرة
فكرة أن تجمع الأدلة آلياً ليست جديدة، لكنها انتقلت في السنوات الأخيرة من الجهات الكبيرة إلى الجميع. ففي 2011 نشر المعهد الوطني للمعايير والتقنية الأمريكي دليل NIST SP 800-137 عن المراقبة المستمرة لأمن المعلومات Information Security Continuous Monitoring، وفكرته الأساسية أن حالة الضوابط يجب أن تعرف بتكرار يناسب خطورتها، وليس مرة في السنة عند التدقيق، لأن الضابط الذي كان سليماً يوم التدقيق قد يتعطل في اليوم التالي ولا يعرف أحد حتى التدقيق القادم. والمثال الذي رأيناه في التجربة، مطور يفتح دخول root «مؤقتاً»، هو بالضبط ما تصمم المراقبة المستمرة لاكتشافه.
ثم جاءت موجة أدوات الامتثال التجارية التي تتصل بأنظمة العميل وتسحب الأدلة إلى لوحة واحدة، وقد وصفتها مجلة المحاسبة Journal of Accountancy التابعة للمعهد الأمريكي للمحاسبين القانونيين بأنها تسمح للمدققين بدل عشرات لقطات الشاشة وملفات Excel أن يتصلوا بأنظمة العميل ويسحبوا البيانات والأدلة آلياً إلى لوحة مركزية، لكن المقال نفسه حذر من أن الأداة لا تغني عن حكم المدقق، وأن التقرير الذي يعتمد على ما يقوله العميل دون فحص كاف قد يرفضه من يقرؤه. وهذا ينطبق علينا أيضاً: الجامع يعطيك أدلة أقوى وأكثر تكراراً، لكنه لا يقرر أن الضابط كاف، فهذا قرار من يراجع.
والفرق الذي يهم شركة صغيرة مثل منصة الحجز أن هذه الفكرة لم تعد تحتاج منتجاً تجارياً، فكل ما فعلناه في التجربة سكربت بالمكتبة القياسية وSSH، وما تدفع ثمنه في الأدوات التجارية هو الواجهة والتكاملات الجاهزة، وهي مفيدة عندما يكبر عدد الأنظمة، لكنها ليست شرطاً للبدء.
ثلاث طرق للجمع الآلي
قبل أن نكتب سطراً واحداً، قارن الفريق بين ثلاث طرق، ولكل منها مكان:
- الوكيل Agent على كل سيرفر: برنامج يعمل على الجهاز ويرسل حالته، مثل وكيل Wazuh الذي يقيس الإعدادات بمعايير CIS، أو osquery مع Fleet الذي شرحناه في إدارة أجهزة الموظفين. ميزته أنه يعمل حتى للأجهزة خلف شبكات لا تصل إليها، وأنه يرسل الحالة باستمرار، وثمنه أنه برنامج آخر على كل جهاز يجب تحديثه وتأمينه.
- دون وكيل Agentless عبر SSH: جامع مركزي يدخل إلى السيرفرات بحساب قراءة ويشغل أوامر ثابتة. لا يضيف شيئاً إلى السيرفر إلا مفتاحاً عاماً وسطراً في sudoers، وأوامره مفهومة لأي مدير أنظمة، وثمنه أن الجامع يحتاج طريقاً شبكياً إلى كل سيرفر، وأن مفتاحه يصبح أصلاً حساساً.
- الـ API: للخدمات التي لا تدخل إليها بـ SSH أصلاً، مثل مزود الهوية ومنصة السحابة وأداة النسخ الاحتياطي، فتسألها بحساب خدمة وتحفظ الرد JSON كما هو.
واختارت الشركة الطريقة الثانية للسيرفرات، لأن عندها ثلاثة سيرفرات فقط، ولأن الأوامر نفسها هي التي يكتبها المدير يدوياً اليوم، والثالثة لمزود الهوية في المرحلة التالية. والتصميم الذي بنيناه في التجربة يبينه الشكل التالي:

حساب الجمع: أقل صلاحية ممكنة
أسهل طريقة أن يدخل الجامع بحساب root، وهي أسوأ طريقة، والسبب أن مفتاح الجامع موجود على جهاز واحد يصل إلى كل السيرفرات، فإذا سرق فقد سرقت مفاتيح البنية كلها. والتصميم الصحيح حساب اسمه evidence يدخل بمفتاح فقط، ويقرأ ما يقرؤه أي مستخدم عادي، ويحتاج صلاحية root لأمرين فقط في تجربتنا: sshd -T لأنه يقرأ مفاتيح المضيف ولا يعمل دون root، وnginx -T لأنه يقرأ ملفات قد تكون محمية. فتسمح sudoers بهذين الأمرين بالضبط، بمساراتهما الكاملة وخياراتهما، ولا شيء غيرهما:
evidence ALL=(root) NOPASSWD: /usr/sbin/sshd -T, /usr/sbin/nginx -Tوتستطيع أن تضيق صلاحية المفتاح نفسه في ملف authorized_keys على كل سيرفر، فخيار from="10.0.5.20" قبل المفتاح يجعل OpenSSH يقبله من عنوان الجامع فقط، وخيار restrict يمنع تمرير المنافذ Port Forwarding والوكيل وحجز الطرفية، كما يشرح دليل sshd. ولم نستخدمهما في المختبر حتى نبقي الإعداد قصيراً، لكنهما في سيرفرات الشركة إلزاميان، والسبب أن مفتاحاً سرق من جهاز الجامع لا يفيد المهاجم من أي جهاز آخر، ولا يفتح له نفقاً إلى داخل الشبكة. وبهذا يصبح حساب الجمع ثلاث طبقات من التقييد: من أين يدخل، وماذا يستطيع أن يفعل في الجلسة، وأي أمرين فقط يرفعهما إلى صلاحية root.
وقد يتساءل البعض: أليس nginx -T نفسه خطراً لأنه يطبع كل ملفات الإعداد؟ والإجابة نعم إذا كانت ملفات الإعداد تحتوي أسراراً، مثل كلمات مرور في proxy_set_header Authorization، لذلك فإن قاعدة الفريق أن الأسرار لا تكتب في ملفات الإعداد أصلاً، وإذا كانت مكتوبة فالجامع لا يجمع هذا الملف، كما سوف نذكر في قسم الخصوصية.
التجربة: سيرفران وجامع
نحتاج في المختبر سيرفرين يشبهان سيرفرات الشركة، وجامعاً، وكلها في Docker على شبكة واحدة. وصورة السيرفر مبنية على Debian 13 وفيها SSH وNginx وchrony وsudo، وملف 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 من صفحات الخطأ والترويسات، وهو ما سوف يتحقق منه أحد الفحوص.
وملف التقوية نفسه خمسة أسطر:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 30وملف chrony يشير إلى المرجع الوطني للوقت كما شرحنا في مقال ضوابط ECC:
# 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 فقط:
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 يربط المفتاح العام فيهما للقراءة فقط:
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:
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:
{
"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:
# 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»
شغلنا الجامع في النسخة الأولى من السكربت، قبل الأسطر الثلاثة الخاصة بنسخة المفتاح، فكان المخرج كما يلي:
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 على السيرفر وجدنا:
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 فظهر السبب الحقيقي:
@ 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 لظن الفريق أن كل السيرفرات مخالفة، ولو تجاهل الأخطاء لظن أنها كلها سليمة.
التشغيل الناجح، وقراءة الأدلة
بعد التصحيح شغلنا الجامع على مجلد أدلة فارغ:
docker compose run --rm collector python collect.pyوالمخرج سوف يكون كما يلي:
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 كان:
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. وفي ملف إعداد SSH الفعلي 103 أسطر، والأسطر التي يتحقق منها الفحص ظهرت كما يلي، مع أرقام أسطرها في الملف:
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. ثم نشغل الجامع:
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:
{
"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.
والشكل التالي يجمع التشغيلات الثلاثة التي أجريناها في التجربة، ومنها الثالث الذي سوف نشرحه بعد قليل، في جدول واحد، وهو الشكل الذي يحتاجه من يقرأ نتائج الجامع كل صباح:

ما الذي يثبته الدليل بالضبط؟
هنا ظهرت ملاحظة دقيقة: نحن غيرنا ملف الإعداد على web2 ولم نعد تحميل sshd، ومع ذلك أظهر الفحص permitrootlogin yes. والسبب أن sshd -T لا يسأل العملية التي تعمل، وإنما يقرأ ملفات الإعداد من القرص ويطبع ما سوف يطبق. فهل العملية التي تعمل تسمح بدخول root فعلاً؟ جربنا ذلك بوضع المفتاح العام نفسه لحساب root على web2، ثم محاولة الدخول قبل إعادة التحميل وبعدها:
root@web2: Permission denied (publickey).
reloaded
root login acceptedقبل إعادة التحميل رفض sshd دخول root رغم أن الملف يقول yes، لأن العملية ما زالت تعمل بالإعداد الذي قرأته عند بدايتها، وبعد إرسال إشارة HUP إليها قرأت الملف من جديد وقبلت الدخول. والشكل التالي يلخص الفرق:

والنتيجة العملية أن دليل «الإعداد صحيح» ليس هو دليل «الضابط يعمل». فللضوابط المهمة اجمع النوعين: لقطة الإعداد، واختباراً سلوكياً من الخارج، مثل محاولة دخول root بمفتاح اختبار ورفضها، أو طلب إلى Nginx والتحقق من غياب رقم الإصدار في الترويسة. والاختبار السلوكي أقرب إلى ما يفعله المهاجم، لذلك يقبله المدقق بثقة أكبر، لكنه أصعب في الكتابة والصيانة، وتبدأ به الشركة في الضوابط التي يطلبها العميل بالاسم.
ماذا يحدث إذا فقد الجامع صلاحيته؟
الخطر الأكبر على أي جامع أدلة ليس أن يكتشف مخالفة، وإنما أن يتوقف عن الجمع دون أن يعلم أحد. فحذفنا ملف sudoers الخاص بحساب evidence على web1، كما قد يحدث عندما يعيد أحدهم بناء السيرفر من صورة قديمة، وشغلنا الجامع:
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:
# 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وعلى التشغيلين السليمين كان المخرج:
2026-10-26T055343Z items=10 chain=ok tampered=0
2026-10-26T075211Z items=10 chain=ok tampered=0 ثم نفترض أن أحداً أراد أن يخفي أن إعداد SSH على web2 كان سليماً في التشغيل الأول، أو أن يغير أي شيء فيه، فعدلنا ملف الدليل بمسافة واحدة في نهاية سطر:
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 الهاش كشف التعديل وحدد الملف. والخطوة التالية للمعدل الذكي أن يعيد حساب الهاش في بيان التشغيل الأول حتى يطابق الملف المعدل، ففعلنا ذلك أيضاً:
2026-10-26T055343Z items=10 chain=ok tampered=0
2026-10-26T075211Z items=10 chain=BROKEN tampered=0البيان الأول أصبح متسقاً مع ملفاته، لكن تعديله غير هاشه، والبيان الثاني يحمل الهاش القديم، فانقطعت السلسلة عند الثاني. والشكل التالي يبين الآلية:

لكن هذه الحماية لها حد واضح يجب أن تعرفه: من يملك المجلد كله يستطيع أن يعيد حساب كل البيانات من الأول إلى الأخير، فتعود السلسلة سليمة. والحل الذي شرحناه بالتفصيل في سجل التدقيق Audit Trail هو المرساة: أن تحفظ هاش آخر بيان في مكان لا يملكه الجامع ولا من يملك السيرفر، مثل تخزين كائنات بقفل لا يسمح بالتعديل ولا الحذف كما في S3 Object Lock، أو في أداة GRC كحقل في الدليل، أو حتى في رسالة بريد إلى صندوق يملكه المدقق الداخلي. وعندها يحتاج المعدل أن يعدل في مكانين يملكهما شخصان مختلفان.
كيف تكتب فحصاً جيداً؟
بعد أسبوعين من تشغيل الجامع يومياً اكتشف الفريق أن جودة الأدلة تعتمد على كتابة الفحوص أكثر مما تعتمد على الكود، ووضع لذلك قواعد خرجت من أخطاء حقيقية:
- حقيقة واحدة لكل فحص: فحص «إعداد SSH» يجمع الإعداد كاملاً ويتحقق من ثلاثة أنماط، وهذا مقبول لأنها كلها في مخرج واحد، أما خلط إعداد SSH وقائمة المستخدمين في أمر واحد فيجعل التغير في أحدهما يظهر كأنه تغير في الآخر.
- مخرج ثابت: الهاش يتغير مع أي حرف، فإذا كان الأمر يطبع وقتاً أو رقم عملية أو يرتب النتائج عشوائياً فسوف يظهر الفحص
changedفي كل تشغيل، ويتعلم الفريق أن يتجاهل التغير، وهذا أخطر من غياب الفحص. ولاحظ في التجربة أن الهاشات بقيت متطابقة بين السيرفرين وبين التشغيلين حين لم يتغير شيء، لأن الأوامر التي اخترناها تطبع نتيجة ثابتة. - الأمر نفسه الذي يكتبه الإنسان: المدقق يفهم
sshd -Tوdpkg-query، ولا يفهم سكربتاً من مئتي سطر يستخرج القيم بطرق خاصة، والدليل الذي يفهمه المدقق بنفسه لا يحتاج من يشرحه. - النمط يصف الضابط وليس الإعداد كله: النمط
^maxauthtries [1-3]$يقبل أي قيمة من 1 إلى 3، لأن المعيار الداخلي للشركة يقول «ثلاث محاولات كحد أقصى»، ولو كتبنا^maxauthtries 3$لفشل الفحص عند من يشدد الإعداد إلى محاولتين، وهذا ليس مخالفة. - ملف الفحوص في Git: كل إضافة أو تعديل لفحص يمر بمراجعة كما شرحنا للسياسات في مقال السياسات، لأن من يملك ملف الفحوص يستطيع أن يجعل أي مخالفة تنجح بتغيير نمط واحد.
من مجلد الأدلة إلى أداة الامتثال
الأدلة في مجلد على سيرفر الجامع لا يراها المدقق، والخطوة التالية في القصة أن يصل كل دليل إلى الضابط المطبق المقابل في CISO Assistant. والتصميم الذي اعتمدته الشركة بسيط: الجامع يرفع مجلد كل تشغيل إلى مخزن الأدلة كما هو، ثم يحدث في الأداة دليلاً واحداً لكل فحص، اسمه رقم الفحص واسم السيرفر، ورابطه إلى الملف في المخزن، وفي وصفه الهاش وتاريخ الجمع والحالة. وقد بينا في قسم الـ API في مقال CISO Assistant كيف تحصل على رمز الدخول وكيف تنشئ الأدلة وتربطها بالضوابط المطبقة. ولاحظ أن الحقل controls في ملف الفحوص هو الذي يربط الطرفين، فاكتب فيه أرقام الضوابط بالصيغة نفسها التي في مكتبة الأداة، مثل 2-3-3-4، حتى يكون الربط آلياً.
من يقرأ نتائج الجامع كل صباح؟
الجامع الذي لا يقرأ أحد مخرجه يجمع أدلة على أن الشركة رأت المشكلة وتجاهلتها، وهذا أسوأ من عدم الجمع. لذلك ربطت الشركة كل حالة بإجراء محدد ومالك واضح. فكل fail يفتح تذكرة لمالك السيرفر، وإذا كانت المخالفة مقصودة، مثل فتح دخول root لصيانة طارئة، فإنها تسجل استثناء بمدة وموافقة كما في السياسة، ويضاف رقم الاستثناء إلى التذكرة. وكل changed على قائمة المديرين يجب أن يقابله طلب صلاحية معتمد، وإذا لم يوجد طلب فهو حادث أمني يعامل بإجراء الحوادث. وكل error يذهب إلى مالك الجامع، لأنه يعني أن هناك سيرفراً لا نعرف حالته.
وفي آخر الشهر يجمع مسؤول الأمن ثلاثة أرقام للإدارة: نسبة الفحوص الناجحة، وعدد التغيرات التي لم يقابلها طلب، ومتوسط الأيام لإغلاق المخالفة. وعندما تتكرر مخالفة على الضابط نفسه تصبح مخاطرة في سجل المخاطر بقرار معالجة، لأن تكرارها يعني أن المشكلة في العملية وليست في سيرفر واحد. وبهذا الشكل يصبح الجامع جزءاً من دورة GRC كاملة: الضابط يطبق، والجامع يثبت، والمراجعة تقرر، والسجل يتذكر.
الجدولة والاحتفاظ
كم مرة تجمع؟ والإجابة تختلف بحسب الدليل وليس بحسب الجامع. فإعدادات السيرفرات تتغير مع أي تدخل، والشركة تجمعها يومياً، لأن التغير الذي يكتشف بعد يوم يمكن التحقيق فيه، والذي يكتشف بعد ربع سنة لا يعرف أحد سببه. أما قائمة المديرين فيومياً أيضاً، لأنها أهم مؤشر على تجاوز إجراءات الصلاحيات. وتقارير فحص الثغرات تجمع بعد كل فحص، ومحاضر اختبار الاستعادة بعد كل اختبار. والتشغيل الواحد في تجربتنا استغرق نحو 31 ثانية لعشرة فحوص على سيرفرين، ومعظمها وقت فتح اتصال SSH لكل أمر، فإذا كبر عدد السيرفرات فاستخدم خيار ControlMaster في SSH لإعادة استخدام الاتصال، أو شغل الفحوص على السيرفرات بالتوازي.
والحجم صغير جداً: ثلاثة تشغيلات على سيرفرين بلغت 120 كيلوبايت، أي أن تشغيلاً يومياً لسنة كاملة على عشرة سيرفرات يبقى في حدود عشرات الميجابايتات. فلا تقم بحذف الأدلة القديمة لتوفير المساحة، وإنما احتفظ بها المدة التي تحددها سياستك، والضابط 2-12-3-5 يطلب 12 شهراً للسجلات الأمنية، وكثير من الجهات تجعل مدة الأدلة مثلها على الأقل، لأن المدقق يسأل عادة عن الفترة منذ التدقيق السابق.
عندما يصبح عندك ثلاثون سيرفراً
الجامع في التجربة يقرأ قائمة السيرفرات من ملف الفحوص، وهذا يكفي لسيرفرين أو ثلاثة، ومع النمو تظهر أربع مشكلات، ولكل منها حل معروف لا يحتاج أداة جديدة:
- مصدر القائمة: القائمة المكتوبة يدوياً تنسى السيرفر الجديد، وهو بالضبط السيرفر الذي لم يقوه أحد بعد. لذلك اجعل الجامع يقرأ قائمة السيرفرات من سجل الأصول أو من أداة البنية ككود، وأي سيرفر في السجل لا يصل إليه الجامع يظهر
error، كما أن أي سيرفر يصل إليه وليس في السجل ملاحظة. - الزمن: عشرة فحوص على سيرفرين استغرقت 31 ثانية لأن كل أمر يفتح اتصال SSH جديداً بمصافحته وتشفيره، ومع ثلاثين سيرفراً وعشرين فحصاً يصبح ذلك دقائق طويلة. وخيار
ControlMasterفي SSH يعيد استخدام الاتصال الأول لبقية الأوامر على السيرفر نفسه، والتوازي بين السيرفرات يقسم الزمن على عددها. - الطريق الشبكي: السيرفرات الحساسة لا يصلها الجامع مباشرة، وإنما عبر بوابة وصول، كما شرحنا في سيرفر القفز Bastion، وحساب الجامع في البوابة له صلاحيات القراءة نفسها، وجلساته تسجل مثل جلسات المديرين.
- المفتاح: مفتاح واحد يصل إلى كل السيرفرات يجب أن يدار كسر حساس: يغير دورياً، ويحفظ في خزنة، ولا ينسخ إلى أجهزة الموظفين. والأفضل مع النمو استخدام شهادات SSH قصيرة العمر بدل المفاتيح الدائمة، فتنتهي صلاحيتها وحدها إذا سرقت.
وماذا عن Windows والأجهزة الشبكية؟
الفكرة نفسها تعمل على غير Linux، لأن السؤال دائماً واحد: ما الأمر الثابت الذي يطبع الحالة؟ ففي Windows Server يمكن تثبيت خادم OpenSSH، وعندها يرسل الجامع أوامر PowerShell بالطريقة نفسها، مثل Get-LocalGroupMember -Group Administrators لقائمة المديرين المحليين، وحساب الجمع يحتاج صلاحيات القراءة المناسبة فقط. وفي أجهزة الشبكة، مثل موجهات MikroTik التي شرحنا إعداد جدار الحماية فيها في جدار الحماية في MikroTik، يطبع الأمر /export الإعداد كاملاً نصاً يحفظ ويقارن كما نقارن sshd -T، وتغير قاعدة واحدة في جدار الحماية يظهر changed في اليوم التالي. لكن انتبه هنا إلى أن مخرج الإعداد في أجهزة الشبكة قد يتضمن معلومات حساسة مثل مفاتيح شبكة VPN أو كلمات مرور بعض الخدمات بحسب الإصدار والخيارات، فراجع المخرج قبل أن تجعله دليلاً يحفظ سنة، واحذف منه ما لا يلزم، كما في قواعد الخصوصية السابقة.
الخصوصية والأسرار في الأدلة
الجامع يرى الأنظمة من الداخل، والأدلة التي يجمعها قد تحتوي ما لا يجب أن يخرج من السيرفر. ولذلك وضعت الشركة ثلاث قواعد قبل أن تضيف أي فحص:
- لا أسرار في الأدلة: أي أمر قد يطبع كلمة مرور أو مفتاحاً أو رمز دخول لا يضاف، وإذا كان المطلوب إثبات وجود سر فالدليل هو بصمته أو تاريخ تغييره، وليس قيمته.
- أقل بيانات شخصية: قائمة المديرين بأسماء الحسابات تكفي، ولا حاجة لأسمائهم الكاملة ولا عناوين بريدهم، والسبب مبدأ الحد الأدنى في نظام حماية البيانات الشخصية، كما شرحنا في مقال PDPL.
- مخزن الأدلة أصل حساس: مجموع الأدلة خريطة لإعدادات البنية كلها، فصلاحية قراءته محدودة، ونسخه الاحتياطية مشفرة، ويظهر في سجل الأصول.
مصادر أخرى للأدلة الآلية
الجامع في التجربة يغطي السيرفرات، وبقية الأدلة تأتي من الأنظمة التي شرحناها في مقالات سابقة، وكل واحد منها يعطي دليلاً بالطريقة نفسها: أمر أو طلب ثابت، ومخرج يحفظ بتاريخه وهاشه:
| الضابط | المصدر | الدليل |
|---|---|---|
| 2-2-3-2 MFA للمديرين | مزود الهوية، مثل authentik، عبر الـ API | قائمة المستخدمين في مجموعة المديرين وحالة أجهزة التحقق لكل منهم |
| 2-10-3-1 فحص الثغرات | Trivy أو OpenVAS، كما في أدوات إدارة الثغرات | تقرير JSON بتاريخ الفحص ونسخة قاعدة الثغرات |
| 2-9-3-3 اختبار الاستعادة | سكربت استعادة مجدول، كما في قاعدة 3-2-1 | سجل الاستعادة وعدد الملفات أو الصفوف المستعادة والمقارنة بالأصل |
| 2-12-3-5 الاحتفاظ بالسجلات | منصة السجلات، مثل Loki | مخرج /config بمدة الاحتفاظ |
| 2-1 جرد الأصول | Snipe-IT عبر الـ 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:
سجل التحديثات
أكتوبر 2026: كتابة المقال وتجربة جامع الأدلة على سيرفرين Debian 13 بـ OpenSSH 10.0 وNginx 1.26.3، والجامع على Python 3.13.17.
