في مقال إدارة الثغرات Vulnerability Management شرحنا لماذا يخدعك رقم الإصدار على التوزيعات، وكيف تمر الثغرة من نشرة المورد إلى سيرفرك، وما الدورة التي تحتاجها من الجرد Inventory حتى التحقق Verification. وفي هذا المقال سوف ننتقل إلى التطبيق، فنبني ثلاثة سيرفرات توقف تحديثها يوم 19 أكتوبر 2026، أي قبل يومين من نشرات Oracle وPHP وNode.js، وسيرفراً رابعاً محدثاً، ثم نسأل ثلاث أدوات مفتوحة المصدر السؤال نفسه: أي سيرفر متأثر بثغرات هذا الأسبوع؟ ثم نصلح ونعيد السؤال، ونقيس كم استغرقت كل أداة، وكم استهلكت من الذاكرة، وأين أخطأت.
وسوف نتناول في هذا المقال ما يلي:
- بناء سيرفرات قديمة بشكل يمكن تكراره Reproducible باستخدام أرشيف snapshot.debian.org، بدلاً من البحث عن Docker Images قديمة.
- الفحص بدون وكيل Agentless مع Trivy على نظام ملفات كل سيرفر Root File System.
- الفحص المستمر بالوكيل Agent مع Wazuh، وكم يحتاج من الذاكرة RAM والقرص والوقت قبل أن يعطيك أول نتيجة صحيحة.
- التنبيه Alert عند ظهور ثغرة جديدة في منتج تستخدمه، ولماذا اكتفينا بسكربت Python صغير بدلاً من تثبيت OpenCVE.
- الإصلاح Patch ثم إعادة الفحص، ومتى تغلق النتيجة ومتى تبقى مفتوحة رغم الإصلاح.
- الإنذارات الخاطئة False Positives والثغرات التي فاتت الأدوات False Negatives كما ظهرت في المختبر.
تصميم المختبر Lab: سيرفرات توقف تحديثها قبل النشرات
لنفرض أن لديك ثلاثة سيرفرات Debian 12 في الإنتاج، الأول يشغل تطبيق Java على OpenJDK 17 من مستودع التوزيعة، والثاني موقع PHP على Apache مع mod_php، والثالث خدمة Node.js ثبتها الفريق من مستودع NodeSource لأن نسخة Debian قديمة، وآخر مرة نفذ أحد apt upgrade عليها كانت يوم 19 أكتوبر. ثم صدرت في 20 و21 أكتوبر 2026 نشرة Oracle للثغرة CVE-2026-91403 في JSSE، ونشرتا PHP للثغرتين CVE-2026-109618 وCVE-2026-109619، وإصدارات Node.js الأمنية ومنها الثغرة CVE-2026-111497 في فك ضغط Brotli. وقد شرحنا تفاصيل هذه النشرات في المقال الأول من السلسلة، والسؤال هنا عملي: كيف تعرف بأداة وليس بالقراءة اليدوية أي سيرفر متأثر؟
الطريقة البسيطة: نبحث عن Docker Image قديمة لكل برنامج
أول ما يخطر على البال لبناء سيرفر قديم هو أن نسحب Docker Image بوسم Tag قديم مثل php:8.2.34-apache، لكن هذه ال Images تبني PHP من الكود المصدري Source Code مباشرة، فلا توجد حزمة Package باسم php في قاعدة بيانات الحزم، وبالتالي لا تشبه سيرفراً حقيقياً ثبتت عليه PHP من التوزيعة، والأدوات التي تقرأ قائمة الحزم سوف تتعامل معها بشكل مختلف تماماً. والمشكلة الثانية أن مستودع Debian الحي لا يحتفظ إلا بآخر نسخة من كل حزمة، فلا يمكنك أن تطلب منه php8.2 كما كانت قبل أسبوع.
الحل الأنسب: أرشيف snapshot.debian.org
يحتفظ مشروع Debian بأرشيف snapshot.debian.org لكل حالة مرت بها المستودعات، فإذا وجهت ملف المصادر Sources إلى لحظة معينة حصلت على السيرفر كما كان في تلك اللحظة بالضبط، مع المستودع الأمني bookworm-security أيضاً. والملف التالي Dockerfile واحد للسيرفرات الأربعة، والمتغير ROLE يحدد ما يثبت في كل سيرفر:
# One Dockerfile for the four lab servers; ROLE selects what gets installed.
# Old servers: Debian 12 frozen at 2026-10-19 (snapshot.debian.org), i.e. a server last updated before the October advisories.
# Patched server: the live Debian 12 repositories including bookworm-security.
FROM debian:bookworm-20261005-slim
ARG ROLE
ARG SNAPSHOT=20261019T000000Z
ARG NODE_VERSION=22.23.3-1nodesource1
ENV DEBIAN_FRONTEND=noninteractive
RUN if [ "$SNAPSHOT" != "live" ]; then \
printf 'Types: deb\nURIs: http://snapshot.debian.org/archive/debian/%s\nSuites: bookworm bookworm-updates\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\nCheck-Valid-Until: no\n\nTypes: deb\nURIs: http://snapshot.debian.org/archive/debian-security/%s\nSuites: bookworm-security\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\nCheck-Valid-Until: no\n' $SNAPSHOT $SNAPSHOT > /etc/apt/sources.list.d/debian.sources; fi \
&& apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends curl gnupg ca-certificates procps lsb-release
# Wazuh agent repository (the agent is pointed at the manager at runtime)
RUN curl -fsSL https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --dearmor -o /usr/share/keyrings/wazuh.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" > /etc/apt/sources.list.d/wazuh.list \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update && apt-get install -y wazuh-agent=4.14.9-1
RUN set -e; case "$ROLE" in \
java) apt-get install -y --no-install-recommends openjdk-17-jre-headless ;; \
php) apt-get install -y --no-install-recommends apache2 libapache2-mod-php8.2 ;; \
node) apt-get install -y nodejs=$NODE_VERSION ;; \
all) apt-get install -y --no-install-recommends openjdk-17-jre-headless apache2 libapache2-mod-php8.2 nodejs=$NODE_VERSION ;; \
esac
COPY start.sh /start.sh
CMD ["sh", "/start.sh"]والملف start.sh يوجه وكيل Wazuh إلى المدير Manager إذا أعطيناه عنوانه، ثم يشغل Apache إن وجد:
#!/bin/sh
# Point the agent at the manager (if given), start it, then keep the "service" running
if [ -n "$WAZUH_MANAGER" ]; then
sed -i "s|<address>.*</address>|<address>$WAZUH_MANAGER</address>|" /var/ossec/etc/ossec.conf
sed -i "s|<agent_name>.*</agent_name>||" /var/ossec/etc/ossec.conf
/var/ossec/bin/wazuh-control start
fi
if command -v apache2ctl >/dev/null 2>&1; then apache2ctl start; fi
exec tail -f /dev/nullفي الإعداد أعلاه لاحظ التالي:
- اخترنا ال Base Image بتاريخ 5 أكتوبر وليس 20 أكتوبر، والسبب أن ال Image الأحدث فيها حزم من bookworm-security أحدث من لحظة الأرشيف، فيرفض
aptالتثبيت برسالةperl : Depends: perl-base (= 5.36.0-7+deb12u3) but 5.36.0-7+deb12u4 is to be installed، وهذا ما ظهر لنا في المحاولة الأولى. - السطر
Check-Valid-Until: noضروري، لأن ملف Release في الأرشيف انتهت صلاحيته الزمنية، وaptيرفض المستودعات منتهية الصلاحية بشكل افتراضي. - السيرفر المحدث يبنى بالقيمة
SNAPSHOT=live، أي من المستودعات الحية معapt-get -y upgrade، ومعه NodeSource بالإصدار 22.23.4 المصلح.
والآن نبني السيرفرات الأربعة:
docker network create lab-net
for r in java php node; do docker build --build-arg ROLE=$r -t lab-srv-$r:old . ; done
docker build --build-arg ROLE=all --build-arg SNAPSHOT=live \
--build-arg NODE_VERSION=22.23.4-1nodesource1 -t lab-srv-patched:new .ثم نتأكد من النسخ المثبتة في كل سيرفر، لأن هذه النسخ هي ما سوف تقارنه الأدوات بقواعد بياناتها:
dpkg-query -W -f='${Package} ${Version}\n' openjdk-17-jre-headless libapache2-mod-php8.2 nodejs libbrotli1 ca-certificatesوالمخرج من السيرفرات الأربعة كان كما يلي:
| السيرفر | الحزمة الأساسية | النسخة | ca-certificates |
|---|---|---|---|
| srv-java | openjdk-17-jre-headless | 17.0.20.1+1-1~deb12u1 | 20250419~deb12u1 |
| srv-php | libapache2-mod-php8.2 | 8.2.34-1~deb12u1 | 20250419~deb12u1 |
| srv-node | nodejs (NodeSource) | 22.23.3-1nodesource1 | 20250419~deb12u1 |
| srv-patched | الثلاث معاً | 17.0.20.1 و8.2.35-1~deb12u1 و22.23.4-1nodesource1 | 20250419~deb12u2 |
لاحظ أن السيرفر المحدث ما زال على OpenJDK 17.0.20.1، والسبب أن Debian 12 لم تنشر حتى يوم التجربة حزمة openjdk-17 جديدة، فمتتبع Debian يعرض CVE-2026-91403 على أنها مصلحة في sid فقط. وسوف نعود لهذه النقطة لأنها أهم ما ظهر في المختبر.
والشكل التالي يبين المختبر كاملاً، والطرق الثلاث التي سوف نستخدمها:

الفحص بدون وكيل Agentless مع Trivy rootfs
نبدأ بالأداة الأخف، فأداة Trivy التي استخدمناها في فحص ال Docker Images تستطيع أيضاً أن تفحص نظام ملفات كامل لسيرفر بالأمر trivy rootfs، فتقرأ قاعدة بيانات الحزم /var/lib/dpkg/status وملفات اللغات مثل package-lock.json وملفات JAR، ثم تقارنها بقاعدتها. وعلى سيرفر حقيقي تشغلها على المسار / نفسه أو على نسخة من القرص، وفي المختبر نأخذ نسخة من نظام ملفات كل Container بالأمر docker export إلى Volume:
docker volume create lab-rootfs
for r in java php node patched; do
docker export srv-$r | docker run -i --rm -v lab-rootfs:/r alpine:3.22 \
sh -c "mkdir -p /r/$r && tar -x -C /r/$r"
doneثم نفحص كل سيرفر، ونحفظ قاعدة Trivy في Volume حتى لا تنزل في كل مرة:
docker run --rm -v lab-rootfs:/r:ro -v trivy-cache:/root/.cache aquasec/trivy:0.76.1 \
rootfs --scanners vuln --format json -o /dev/stdout /r/php > trivy-php.jsonواستغرق الفحص الأول دقيقتين و11 ثانية، أغلبها تنزيل قاعدة الثغرات التي كان تاريخ تحديثها 2026-10-22 13:15 UTC، أي بعد نشرة PHP بنحو 37 ساعة وبعد نشر سجلي CVE بنحو 23 ساعة. أما الفحوص التالية فاستغرقت بين 6 و10 ثوان لكل سيرفر.
ماذا يحدث إذا وجد Trivy ملفات JAR؟
عند فحص srv-java لأول مرة توقف Trivy بعد ست دقائق بهذا الخطأ:
INFO [javadb] Downloading Java DB...
INFO [javadb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-java-db:1"
WARN Provide a higher timeout value, see https://trivy.dev/docs/v0.76/guide/references/troubleshooting#timeout
FATAL Fatal error run error: rootfs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: walk dir error: context deadline exceededوالسبب أن Trivy وجد ملفات JAR فقرر تنزيل قاعدة Java الخاصة به وحجمها 941 MiB، واستهلك التنزيل المهلة الافتراضية وهي خمس دقائق. والحل أن ترفع المهلة بالخيار --timeout 15m في أول تشغيل، أو أن تنزل القاعدتين مسبقاً في خطوة منفصلة، وبعد ذلك استغرق فحص srv-java ست ثوان فقط مع الخيار --skip-java-db-update.
النتيجة: من المتأثر بثغرات Vulnerabilities هذا الأسبوع؟
عدد النتائج الكامل لا يفيدك في يوم نشرة حرجة، فقد أعطى Trivy بين 497 و751 نتيجة لكل سيرفر، أغلبها LOW وMEDIUM بلا إصلاح. لذلك نستخدم قالباً Template يطبع سطراً واحداً لكل نتيجة ثم نبحث عن ثغرات هذا الأسبوع فقط، والقالب يجب أن ينتهي اسمه بالامتداد .tpl وإلا رفضه Trivy برسالة template file must have .tpl extension:
{{- range . }}{{- range .Vulnerabilities }}{{ .PkgName }} {{ .InstalledVersion }} {{ .VulnerabilityID }} {{ .Severity }} {{ .Status }} {{ .FixedVersion }}
{{ end }}{{ end -}}for r in java php node patched; do
echo "== $r"
docker run --rm -v lab-rootfs:/r:ro -v lab-files:/f:ro -v trivy-cache:/root/.cache aquasec/trivy:0.76.1 \
rootfs --quiet --skip-db-update --skip-java-db-update --scanners vuln \
--format template --template @/f/short.tpl /r/$r \
| grep -E "CVE-2026-(91403|109618|109619|111497) " | sort
doneوالمخرج سوف يكون كما يلي:
== java
libbrotli1 1.0.9-2+b6 CVE-2026-111497 UNKNOWN affected
openjdk-17-jre-headless 17.0.20.1+1-1~deb12u1 CVE-2026-91403 UNKNOWN affected
== php
libapache2-mod-php8.2 8.2.34-1~deb12u1 CVE-2026-109618 CRITICAL fixed 8.2.35-1~deb12u1
libapache2-mod-php8.2 8.2.34-1~deb12u1 CVE-2026-109619 CRITICAL fixed 8.2.35-1~deb12u1
libbrotli1 1.0.9-2+b6 CVE-2026-111497 UNKNOWN affected
php8.2-cli 8.2.34-1~deb12u1 CVE-2026-109618 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-cli 8.2.34-1~deb12u1 CVE-2026-109619 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-common 8.2.34-1~deb12u1 CVE-2026-109618 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-common 8.2.34-1~deb12u1 CVE-2026-109619 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-opcache 8.2.34-1~deb12u1 CVE-2026-109618 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-opcache 8.2.34-1~deb12u1 CVE-2026-109619 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-readline 8.2.34-1~deb12u1 CVE-2026-109618 CRITICAL fixed 8.2.35-1~deb12u1
php8.2-readline 8.2.34-1~deb12u1 CVE-2026-109619 CRITICAL fixed 8.2.35-1~deb12u1
== node
libbrotli1 1.0.9-2+b6 CVE-2026-111497 UNKNOWN affected
== patched
libbrotli1 1.0.9-2+b6 CVE-2026-111497 UNKNOWN affected
openjdk-17-jre-headless 17.0.20.1+1-1~deb12u1 CVE-2026-91403 UNKNOWN affected في المخرج أعلاه لاحظ التالي:
- PHP كان دقيقاً تماماً: خمس حزم على srv-php بحالة
fixedومعها النسخة المصلحة 8.2.35-1~deb12u1 التي نشرتها Debian في DLA-4848-1، ولا شيء من PHP على srv-patched. وهذا هو السطر الذي تريده من أي أداة: الحزمة والنسخة المثبتة والنسخة التي تصلحها. - Java على السيرفر المحدث ما زال «affected»: لأن Debian لم تصدر openjdk-17 جديدة، فيبقى السطر نفسه على srv-java وsrv-patched، وسوف نرى في قسم الإصلاح أن هذا السطر إنذار خاطئ False Positive على سيرفر حدثت فيه حزمة الشهادات.
- Node.js غائب تماماً عن srv-node: الحزمة
nodejs 22.23.3-1nodesource1متأثرة بحسب نشرة Node.js، لكن Trivy لم يذكرها بأي ثغرة من ثغرات أكتوبر، وهذه ثغرة فاتت الأداة False Negative كاملة. - libbrotli1 ظهرت على السيرفرات الأربعة: وهي مكتبة النظام التي تستخدمها curl وغيرها، أما Node من NodeSource فلا يستخدمها، وهذا ما سوف نتحقق منه الآن.
لماذا لم ير Trivy ثغرة Node.js؟
قد يتساءل البعض: إذا كان Trivy يعرف CVE-2026-111497 ويعرضها على libbrotli1، فلماذا لا يعرضها على nodejs؟ والإجابة أن Trivy يطابق حزم Debian ببيانات Debian، ومتتبع Debian يضع nodejs لهذه الثغرة في حالة «unimportant» لأن نسخة Debian من Node تستخدم مكتبة النظام، أما الحزمة التي ثبتناها فليست من Debian أصلاً وإنما من NodeSource، وهي تحمل نسخة Brotli مدمجة داخل الملف التنفيذي، والأمر التالي يبين ذلك:
ldd /usr/bin/node | grep -i -E "brotli|ssl|z\."
node -v
node -p "process.versions.brotli + \" openssl \" + process.versions.openssl"والمخرج:
v22.23.3
1.1.0 openssl 3.5.8فلا يوجد ربط بأي مكتبة brotli أو ssl من النظام، وBrotli المستخدمة هي 1.1.0 المدمجة، وهي من النسخ المتأثرة بحسب سجل CVE (من 1.1.0 حتى ما قبل 1.1.1). وبالتالي فالأداة رأت المكتبة غير المستخدمة وفاتها البرنامج المتأثر فعلاً، وهذا يحدث مع كل برنامج تثبته من مستودع خارجي يحمل مكتباته معه، مثل NodeSource وحزم الموردين التجارية.
وماذا عن Java المثبت من ملف مضغوط Archive؟
ولو كان تطبيق Java يعمل على Eclipse Temurin المثبت في /opt/java/openjdk كما في ال Docker Image الرسمية eclipse-temurin:17.0.20.1_1-jre-noble، فإن النتيجة أسوأ، فقد فحصناها بالطريقة نفسها:
INFO Detected OS family="ubuntu" version="24.04"
INFO [ubuntu] Detecting vulnerabilities... os_version="24.04" pkg_num=136
INFO Number of language-specific files num=0فأعطى 31 نتيجة كلها لحزم Ubuntu، ولا شيء عن Java 17.0.20.1 مع أنها متأثرة بثغرات نشرة OpenJDK في 20 أكتوبر وبالثغرة CVE-2026-91403، والسبب أن Java هنا ملفات في مجلد وليست حزمة مسجلة في dpkg، فلا يراها أي فاحص يعتمد على قاعدة بيانات الحزم. ولذلك فالجرد Inventory الذي تبنيه بنفسك (ما الذي ثبتناه خارج مدير الحزم؟) جزء لا تغني عنه أي أداة.
الفحص المستمر بالوكيل Agent مع Wazuh
الفحص بأداة مثل Trivy صورة لحظية Snapshot، فإذا نسيت تشغيله أو تغيرت الحزم بعده فلن تعرف. أما كشف الثغرات في Wazuh فيعمل بشكل مختلف، فالوكيل Agent على كل سيرفر يجمع قائمة الحزم بوحدة Syscollector ويرسلها إلى المدير Manager، والمدير يقارنها بقاعدة Wazuh CTI ويحفظ النتيجة في الفهرس Indexer، وعندما تحدث حزمة على السيرفر تتغير القائمة فتغلق النتيجة وحدها. والسؤال الذي يهم من يدير عدداً قليلاً من السيرفرات هو كم يكلف هذا التشغيل المستمر.
التثبيت على Docker في عقدة واحدة Single Node
نستخدم المستودع الرسمي wazuh-docker بوسم الإصدار نفسه، ثم ننشئ الشهادات Certificates التي تتواصل بها المكونات الثلاث:
git clone -b v4.14.9 --depth 1 https://github.com/wazuh/wazuh-docker.git
cd wazuh-docker/single-node
docker compose -f generate-indexer-certs.yml run --rm generatorوقبل التشغيل غيرنا ثلاثة أشياء في docker-compose.yml، فحذفنا نشر المنافذ 1514 و1515 و514 و55000 و9200 على السيرفر المضيف لأن الوكلاء في الشبكة نفسها ولا حاجة لفتحها، وربطنا لوحة التحكم Dashboard بالعنوان المحلي فقط، وأضفنا الشبكة المشتركة مع السيرفرات:
wazuh.dashboard:
ports:
- "127.0.0.1:8443:5601"
networks:
default:
name: lab-net
external: trueSecretPassword للفهرس وMyS3cr37P450r.*- لواجهة API، وهي مناسبة لمختبر معزول فقط. قبل أي تشغيل حقيقي غيرها كما في دليل Wazuh على Docker، ولا تنشر المنافذ 9200 و55000 على الإنترنت.docker compose up -dثم نشغل السيرفرات الأربعة ونعطي كلاً منها عنوان المدير، فيسجل الوكيل نفسه عبر المنفذ 1515 ثم يرسل بياناته عبر 1514:
for r in java php node; do
docker run -d --name srv-$r --hostname srv-$r --network lab-net -e WAZUH_MANAGER=wazuh.manager lab-srv-$r:old
done
docker run -d --name srv-patched --hostname srv-patched --network lab-net -e WAZUH_MANAGER=wazuh.manager lab-srv-patched:new
docker exec single-node-wazuh.manager-1 /var/ossec/bin/agent_control -lWazuh agent_control. List of available agents:
ID: 000, Name: wazuh.manager (server), IP: 127.0.0.1, Active/Local
ID: 001, Name: srv-patched, IP: any, Active
ID: 002, Name: srv-java, IP: any, Active
ID: 003, Name: srv-php, IP: any, Active
ID: 004, Name: srv-node, IP: any, Activeوبعد التسجيل بدأت النتائج تظهر في لوحة التحكم، والصورة التالية من صفحة Vulnerability Detection بعد نحو عشر دقائق من تشغيل الوكلاء:

ماذا وجد Wazuh؟ تأخر قاعدة البيانات Feed Lag
للوهلة الأولى تبدو النتيجة مقنعة، فهي 374 نتيجة موزعة على السيرفرات، 128 منها على srv-patched و105 على srv-php و75 على srv-node و66 على srv-java، و30 منها Critical. لكن عندما سألنا الفهرس Indexer مباشرة عن الثغرات الأربع لم نجد سطراً واحداً، والاستعلام التالي يبين ذلك:
docker exec single-node-wazuh.indexer-1 curl -sk -u admin:SecretPassword \
-H 'Content-Type: application/json' \
"https://localhost:9200/wazuh-states-vulnerabilities-*/_search" \
-d '{"size":100,"query":{"bool":{"should":[{"terms":{"vulnerability.id":["CVE-2026-91403","CVE-2026-109618","CVE-2026-109619","CVE-2026-111497"]}}]}}}'فكان عدد النتائج صفراً، ثم سألنا عن أحدث تاريخ نشر vulnerability.published_at بين كل النتائج بتجميع max، فكان 2026-09-22T17:17:32.000Z، أي أن Wazuh كان يطابق السيرفرات بقاعدة عمرها شهر. والسبب يظهر في سجل المدير ossec.log:
2026/10/22 14:52:50 wazuh-modulesd:vulnerability-scanner: INFO: Starting database file decompression.
2026/10/22 15:02:58 wazuh-modulesd:vulnerability-scanner: INFO: Database decompression finished.
2026/10/22 15:02:59 wazuh-modulesd:vulnerability-scanner: INFO: Vulnerability scanner module started.
2026/10/22 15:13:00 wazuh-modulesd:vulnerability-scanner: INFO: Initiating update feed process.في السجل أعلاه لاحظ التالي:
- المدير يأتي بقاعدة ثغرات مضغوطة داخل ال Image، واستغرق فكها عشر دقائق قبل أن يبدأ أي فحص، وهذه القاعدة هي التي أعطت النتائج الأولى.
- بعد عشر دقائق أخرى بدأ تحديث القاعدة من خدمة Wazuh CTI، فنزل 104 ملفات تحديث Offsets، وبقي المدير يعالجها بين معالج Core واحد ومعالج ونصف حتى نحو 16:07، أي قرابة ساعة كاملة.
- حتى الساعة 16:17، أي بعد ثلاث ساعات ونصف من التشغيل، لم تظهر أي ثغرة من الأربع على أي سيرفر، مع أن صفحة خدمة CTI العامة كانت تعرض CVE-2026-109618 وCVE-2026-91403 في ذلك اليوم، ولم تكن تعرض CVE-2026-111497 بعد.
وقد يتساءل البعض: هل المشكلة في Wazuh نفسه؟ والإجابة أن هذا هو السلوك الطبيعي لأي أداة بقاعدة بيانات خاصة بها، فالثغرة تمر من نشرة المورد إلى سجل CVE ثم إلى مصادر الأداة ثم إلى قاعدتها المحلية ثم تنتظر دورة الفحص التالية، وتوثيق Wazuh يقول إن الجرد يرسل بشكل دوري دون أن يحدد مدة ثابتة للتحديث. والخطأ الحقيقي أن تفترض أن اللوحة الخضراء تعني أنك غير متأثر في يوم النشرة نفسه. وقد أوقفنا المختبر بعد ذلك لأن قرص السيرفر المضيف امتلأ، فلم نقس الوقت الذي ظهرت فيه الثغرات في النهاية، وهذا أيضاً درس عن الموارد كما سيأتي.
كم يكلف Wazuh من الموارد Resources؟
هذه الأرقام من الأمر docker stats ومن حجم المجلدات داخل ال Container خلال التجربة، مع أربعة وكلاء فقط:
| المكون | الذاكرة RAM | القرص Disk |
|---|---|---|
| المدير wazuh.manager | 638 MiB عند البدء، ثم 1.3 إلى 1.7 GiB أثناء تحديث القاعدة، ثم 2.7 GiB | قاعدة الثغرات /var/ossec/queue/vd بحجم 11 GB، وملفات التحديث 977 MB |
| الفهرس wazuh.indexer | بين 1.42 و1.51 GiB (الإعداد -Xms1g -Xmx1g) | ال Image بحجم 2.56 GB |
| لوحة التحكم wazuh.dashboard | بين 186 و230 MiB | ال Image بحجم 2.14 GB |
| كل وكيل Agent | بين 35 و85 MiB لل Container كاملاً | صغير |
أي أن العقدة الواحدة احتاجت نحو 4.4 GiB من الذاكرة وأكثر من 16 GB من القرص قبل أن تخزن أي تنبيه، وهذا يتفق مع متطلبات Wazuh الرسمية التي تطلب 4 vCPU و8 GiB من الذاكرة و50 GB من القرص لعدد يصل إلى 25 وكيلاً. لذلك لا تقم بتشغيل Wazuh على سيرفر صغير بجانب تطبيقاتك، والسبب أن تحديث القاعدة وحده يستهلك معالجاً كاملاً لساعة، وخصص له سيرفراً أو آلة افتراضية مستقلة.
التنبيه Alert عند ظهور ثغرة جديدة
الأداتان السابقتان تجيبان عن سؤال «أي سيرفر متأثر بما أعرفه؟»، أما السؤال الذي سأله صاحب السيرفر في بداية المقال فهو «كيف أعرف أن ثغرة جديدة ظهرت في شيء أستخدمه؟»، وهذا يحتاج اشتراكاً Subscription في المنتجات التي تستخدمها، وتنبيهاً يصلك عند ظهور ثغرة جديدة فيها.
لماذا لم نثبت OpenCVE في المختبر؟
الأداة المعروفة لهذا الغرض هي OpenCVE، وفكرتها كما في توثيقها أن تنشئ مشروعاً Project وتشترك فيه بالموردين Vendors والمنتجات Products، ثم تضيف أتمتة Automation تعمل كل ساعة وترسل تنبيهاً بالبريد أو Slack أو Webhook عندما تدخل ثغرة جديدة إلى المشروع أو يتغير تقييمها أو تضاف إلى قائمة KEV. وقد بدأنا تثبيت الإصدار 3.1.0 فعلاً، لكننا توقفنا لسببين:
- الحجم: النسخة المستضافة ذاتياً تحتاج خمسة مستودعات Git للبيانات، وحتى مع النسخ السطحي Shallow Clone كان حجمها نحو 15.6 GB (opencve-kb بحجم 5.1 GB وcvelistV5 بحجم 4.4 GB وopencve-nvd بحجم 3.9 GB وvulnrichment بحجم 1.8 GB وopencve-redhat بحجم 423 MB)، ثم قاعدة PostgreSQL التي يستورد إليها كل الثغرات، وتشغيل Airflow مع Redis وCelery، وقد استغرق بناء ال Images وحده 13 دقيقة. وفي يوم التجربة امتلأ قرص السيرفر المضيف مع Wazuh فتوقف Docker عن الاستجابة، فقررنا ألا نشغلها.
- الرخصة: OpenCVE منشورة برخصة Business Source License 1.1، وهي ليست رخصة مفتوحة المصدر، فهي تسمح بالاستخدام الداخلي وتمنع تقديم الأداة خدمة مراقبة وتنبيه تجارية لطرف ثالث، وتتحول إلى Apache 2.0 بعد تاريخ التحويل المذكور في الملف. وهذا مناسب لفريق يراقب سيرفراته، لكن عليك أن تعرفه قبل أن تبني عليها خدمة لعملائك.
وإذا كان عدد المنتجات التي تستخدمها قليلاً فإن الفكرة نفسها يمكن تنفيذها بسكربت صغير يسأل المصادر العامة مباشرة، وهذا ما سوف نفعله الآن.
سكربت تنبيه صغير بلغة Python
السكربت التالي لا يحتاج أي مكتبة خارجية، ويعمل على Windows أو Linux بلغة Python 3، وفيه ثلاثة أوامر:
"""cve_watch.py: a small CVE watcher (Python 3 standard library only).
1) match: ask OSV which known vulnerabilities affect the Debian packages in inventory.json
2) watch: ask NVD for CVEs published or modified in the last N hours that mention the
products we run, and print only the ones not seen before (state in seen.json)
3) feeds: read the vendors' own advisory feeds (RSS/Atom) in feeds.json and print the
entries of the last N hours
usage: python cve_watch.py match
python cve_watch.py watch --hours 48
python cve_watch.py feeds --hours 48
"""
import json, sys, re, time, email.utils, xml.etree.ElementTree as ET, urllib.request, urllib.parse, datetime, pathlib
HERE = pathlib.Path(__file__).parent
UA = {"User-Agent": "cve-watch-lab/1.0", "Content-Type": "application/json"}
def http_json(url, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=UA)
for attempt in range(3): # NVD is slow at times and allows 5 requests per 30 s without a key
try:
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)
except (TimeoutError, OSError):
time.sleep(10)
raise SystemExit(f"no answer from {url}")
def match():
inv = json.loads((HERE / "inventory.json").read_text(encoding="utf-8"))
queries, labels = [], []
for server, pkgs in inv.items():
for name, version in pkgs.items():
queries.append({"package": {"name": name, "ecosystem": "Debian:12"}, "version": version})
labels.append((server, name, version))
res = http_json("https://api.osv.dev/v1/querybatch", {"queries": queries})["results"]
for (server, name, version), r in zip(labels, res):
ids = sorted({v["id"] for v in r.get("vulns", [])})
week = [i for i in ids if any(i.endswith(x) for x in ("2026-91403", "2026-109618", "2026-109619", "2026-111497"))]
print(f"{server:12} {name:16} {version:24} known={len(ids):3} this-week={','.join(week) or '-'}")
def watch(hours):
products = json.loads((HERE / "products.json").read_text(encoding="utf-8"))
seen_file = HERE / "seen.json"
seen = set(json.loads(seen_file.read_text())) if seen_file.exists() else set()
end = datetime.datetime.now(datetime.timezone.utc)
start = end - datetime.timedelta(hours=hours)
fmt = "%Y-%m-%dT%H:%M:%S.000Z"
new = 0
for p in products: # {"keyword": text sent to NVD, "match": regex the description must match}
q = urllib.parse.urlencode({"keywordSearch": p["keyword"], "lastModStartDate": start.strftime(fmt),
"lastModEndDate": end.strftime(fmt)})
data = http_json("https://services.nvd.nist.gov/rest/json/cves/2.0?" + q)
time.sleep(6)
for item in data.get("vulnerabilities", []):
cve = item["cve"]
desc = " ".join(cve["descriptions"][0]["value"].split())
if cve["id"] in seen or not re.search(p["match"], desc):
continue
seen.add(cve["id"])
new += 1
metrics = cve.get("metrics", {})
score = "-"
for key in ("cvssMetricV40", "cvssMetricV31"):
if metrics.get(key):
score = metrics[key][0]["cvssData"]["baseScore"]
break
print(f"NEW [{p['keyword']}] {cve['id']} score={score} status={cve['vulnStatus']} published={cve['published'][:16]} {desc[:70]}")
seen_file.write_text(json.dumps(sorted(seen)))
print(f"{new} new CVE(s) since the last run, window {start:%Y-%m-%d %H:%M} to {end:%H:%M} UTC")
def feeds(hours):
since = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=hours)
for url in json.loads((HERE / "feeds.json").read_text(encoding="utf-8")):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 cve-watch-lab/1.0"})
root = ET.fromstring(urllib.request.urlopen(req, timeout=60).read())
atom = "{http://www.w3.org/2005/Atom}"
for e in root.iter("item"): # RSS 2.0
when = email.utils.parsedate_to_datetime(e.findtext("pubDate"))
if when >= since:
print(f"{when:%Y-%m-%d %H:%M} {url.split('/')[2]:16} {e.findtext('title')}")
for e in root.iter(atom + "entry"): # Atom
when = datetime.datetime.fromisoformat(e.findtext(atom + "updated"))
if when >= since:
print(f"{when:%Y-%m-%d %H:%M} {url.split('/')[2]:16} {e.findtext(atom + 'title')}")
if __name__ == "__main__":
if sys.argv[1:2] == ["match"]:
match()
elif sys.argv[1:2] == ["watch"]:
watch(int(sys.argv[3]) if len(sys.argv) > 3 else 24)
elif sys.argv[1:2] == ["feeds"]:
feeds(int(sys.argv[3]) if len(sys.argv) > 3 else 24)
else:
print(__doc__)وثلاثة ملفات صغيرة بجانبه، الأول جرد الحزم Inventory بأسماء الحزم المصدرية Source Packages في Debian ونسخها كما جمعناها من السيرفرات، ولاحظ أن الحزمة libbrotli1 بالنسخة 1.0.9-2+b6 تتبع الحزمة المصدرية brotli بالنسخة 1.0.9-2، وأن Node.js من NodeSource ليس له مكان هنا لأنه ليس حزمة Debian:
{
"srv-java": {"openjdk-17": "17.0.20.1+1-1~deb12u1", "ca-certificates": "20250419~deb12u1", "brotli": "1.0.9-2"},
"srv-php": {"php8.2": "8.2.34-1~deb12u1", "ca-certificates": "20250419~deb12u1", "brotli": "1.0.9-2"},
"srv-node": {"ca-certificates": "20250419~deb12u1", "brotli": "1.0.9-2"},
"srv-patched": {"openjdk-17": "17.0.20.1+1-1~deb12u1", "php8.2": "8.2.35-1~deb12u1", "ca-certificates": "20250419~deb12u2", "brotli": "1.0.9-2"}
}والثاني المنتجات التي نراقبها في NVD مع تعبير نمطي Regular Expression يجب أن يطابق الوصف، والثالث خلاصات RSS وAtom الرسمية للموردين:
[
{"keyword": "PHP", "match": "^PHP "},
{"keyword": "Java SE", "match": "Oracle Java SE"},
{"keyword": "Node.js", "match": "Node[.]js"},
{"keyword": "Brotli", "match": "Brotli"}
][
"https://nodejs.org/en/feed/vulnerability.xml",
"https://www.php.net/feed.atom",
"https://www.oracle.com/ocom/groups/public/@otn/documents/webcontent/rss-otn-sec.xml"
]المطابقة Matching مع OSV: أي سيرفر متأثر؟
python cve_watch.py matchوالمخرج في الساعة 18:11 UTC من يوم 22 أكتوبر كان كما يلي:
srv-java openjdk-17 17.0.20.1+1-1~deb12u1 known= 40 this-week=DEBIAN-CVE-2026-91403
srv-java ca-certificates 20250419~deb12u1 known= 0 this-week=-
srv-java brotli 1.0.9-2 known= 1 this-week=DEBIAN-CVE-2026-111497
srv-php php8.2 8.2.34-1~deb12u1 known= 2 this-week=DEBIAN-CVE-2026-109618,DEBIAN-CVE-2026-109619
srv-php ca-certificates 20250419~deb12u1 known= 0 this-week=-
srv-php brotli 1.0.9-2 known= 1 this-week=DEBIAN-CVE-2026-111497
srv-node ca-certificates 20250419~deb12u1 known= 0 this-week=-
srv-node brotli 1.0.9-2 known= 1 this-week=DEBIAN-CVE-2026-111497
srv-patched openjdk-17 17.0.20.1+1-1~deb12u1 known= 40 this-week=DEBIAN-CVE-2026-91403
srv-patched php8.2 8.2.35-1~deb12u1 known= 0 this-week=-
srv-patched ca-certificates 20250419~deb12u2 known= 0 this-week=-
srv-patched brotli 1.0.9-2 known= 1 this-week=DEBIAN-CVE-2026-111497والنتيجة مطابقة لنتيجة Trivy تماماً، وهذا منطقي لأن الاثنين يقرآن بيانات Debian نفسها، فتقول OSV إن PHP على srv-php متأثر وعلى srv-patched مصلح، وإن openjdk-17 متأثر على السيرفرين، ولا تقول شيئاً عن ca-certificates لأن إصلاح الشهادات نشر في Debian بنشرة DLA دون رقم CVE.
المراقبة Monitoring: ما الجديد في المنتجات التي نستخدمها؟
python cve_watch.py watch --hours 48في التشغيل الأول طبع السكربت 107 تنبيهات خلال 48 ساعة، وهذه أهم الأسطر منها كما ظهرت:
NEW [PHP] CVE-2026-109618 score=9.2 status=Modified published=2026-10-21T14:17 PHP uses longjmp() for out-of-memory (OOM) errors to exit the current
NEW [PHP] CVE-2026-109619 score=9.2 status=Modified published=2026-10-21T14:17 PHP has various global state that temporarily points into request memo
NEW [Java SE] CVE-2026-91403 score=9.1 status=Awaiting Analysis published=2026-10-21T22:16 Vulnerability in the Oracle Java SE, Oracle GraalVM for JDK, Oracle Gr
NEW [Node.js] CVE-2026-112951 score=5.4 status=Deferred published=2026-10-20T16:17 ApostropheCMS is an open-source Node.js content management system. Pri
NEW [Node.js] CVE-2026-111756 score=8.2 status=Awaiting Analysis published=2026-10-20T18:17 follow-redirects is a Node.js HTTP and HTTPS client wrapper that autom
NEW [Brotli] CVE-2026-111497 score=9.2 status=Awaiting Analysis published=2026-10-22T14:17 A buffer overflow vulnerability in the Brotli decoder (c/dec/decode.c)
107 new CVE(s) since the last run, window 2026-10-20 18:16 to 18:16 UTCوفي التشغيل الثاني بعد دقيقة واحدة كان المخرج:
0 new CVE(s) since the last run, window 2026-10-20 18:16 to 18:16 UTCفي المخرج أعلاه لاحظ التالي:
- الملف seen.json يمنع التكرار: فالتشغيل الثاني لم يطبع شيئاً، وهذا ما يجعل السكربت صالحاً للتشغيل كل ساعة من Task Scheduler في Windows أو cron في Linux، ثم ترسل مخرجه بالبريد أو إلى Webhook عندما لا يكون فارغاً.
- البحث بالكلمة Keyword مزعج: في المحاولة الأولى بحثنا عن PHP دون شرط على الوصف، فطبع السكربت عشرات الثغرات في إضافات WordPress وJoomla وتطبيقات مكتوبة بلغة PHP قبل أن يصل إلى ثغرتي PHP نفسها، ولذلك أضفنا الشرط
^PHP. وحتى مع الشرط فإن كلمة Node.js أعادت أكثر من 40 ثغرة في مكتبات npm وApostropheCMS، لأن وصفها يذكر Node.js. - البحث بالمعرف CPE دقيق لكنه متأخر: جربنا الاستعلام بالمعامل
virtualMatchStringعلىcpe:2.3:a:php:phpفأعاد ثغرتي PHP فقط، أماcpe:2.3:a:oracle:jdkوcpe:2.3:a:nodejs:node.jsوcpe:2.3:a:google:brotliفلم تعد شيئاً، لأن السجلات الثلاثة كانت في حالة Awaiting Analysis ولم تضف إليها NVD معرفات CPE بعد. - ثغرة Node.js وصلت باسم Brotli: سجل CVE-2026-111497 نشرته Google بصفتها CNA ووصفه لا يذكر Node.js، فلو اشتركت في Node.js وحده لفاتك، وهذا ما يحدث أيضاً في أي أداة تشترك فيها بالمورد والمنتج.
- بقية ثغرات Node.js لم تصل بعد: نشرة Node.js تذكر عشر ثغرات، لكن السجلات مثل CVE-2026-105482 لم تكن منشورة في CVE.org ولا في NVD حتى الساعة 18:17، أي بعد عشرين ساعة من النشرة.
لذلك راقب نشرات الموردين Vendor Advisories أيضاً
python cve_watch.py feeds --hours 482026-10-21 22:00 nodejs.org Wednesday, October 21, 2026 Security Releases
2026-10-21 00:35 www.php.net Security advisory: Critical remote code execution vulnerabilities, update now
2026-10-21 00:24 www.php.net PHP 8.5.12 Released!
2026-10-21 00:20 www.php.net PHP 8.4.27 Released!
2026-10-21 00:14 www.php.net PHP 8.3.36 Released!
2026-10-21 00:05 www.php.net PHP 8.2.35 Released!
2026-10-20 20:32 www.php.net Upcoming Critical PHP Security Release
2026-10-20 13:00 www.oracle.com Oracle Critical Security Patch Update Advisory - October 2026وهنا يظهر الفرق، فموقع PHP أعلن عن إصدار أمني حرج قبل صدوره بنحو ثلاث ساعات ونصف، ثم نشر الإصدارات بعد منتصف الليل، أي قبل سجلات CVE بأربع عشرة ساعة، ونشرة Node.js وصلت في خلاصتها قبل أن يصل أغلب سجلاتها إلى CVE.org. أما خلاصة Oracle فلم يظهر فيها في ذلك الوقت إلا التحديث الدوري CPU في 20 أكتوبر، دون التنبيه الخاص Security Alert بالثغرة CVE-2026-91403، فلا تعتمد على خلاصة واحدة لكل مورد. والشكل التالي يجمع توقيت كل مصدر وكل أداة:

الإصلاح Patch ثم إعادة الفحص
الإصلاح على سيرفر Debian يعني أن تعيد المستودع الأمني إلى ملف المصادر إذا كان معطلاً، ثم تحدث الحزم، ثم تعيد تشغيل الخدمة التي تحمل الكود القديم في الذاكرة:
apt-get update
apt-get install --only-upgrade libapache2-mod-php8.2 php8.2-cli php8.2-common php8.2-opcache php8.2-readline ca-certificates
apachectl -k gracefulوقد يتساءل البعض: لماذا إعادة تشغيل Apache إذا كانت الحزمة قد تحدثت؟ والإجابة أن mod_php يعمل داخل عمليات Apache نفسها، وقبل الإصلاح كانت هذه العمليات تعمل منذ بدء السيرفر كما يبين الأمر التالي على srv-php:
php -v | head -1
ps -o pid,lstart,cmd -C apache2 | head -3PHP 8.2.34 (cli) (built: Oct 4 2026 14:03:09) (NTS)
PID STARTED CMD
391 Thu Oct 22 14:52:26 2026 /usr/sbin/apache2 -k start
413 Thu Oct 22 14:52:27 2026 /usr/sbin/apache2 -k startفإذا حدثت الحزمة ولم تعد تشغيل Apache بقيت العمليات القديمة تنفذ PHP 8.2.34 المتأثر، والفاحص يقرأ قاعدة بيانات الحزم فيظن أن السيرفر مصلح. لذلك اجعل إعادة التشغيل جزءاً من خطوة الإصلاح نفسها، وتحقق من وقت بدء العمليات بعد الإصلاح.
وفي المختبر كان السيرفر srv-patched هو نتيجة هذا الإصلاح، فهو الملف Dockerfile نفسه مع المستودعات الحية وأمر apt-get -y upgrade، ولم نكرر التحديث داخل السيرفرات القديمة نفسها لأن Docker توقف قبل ذلك. ونتيجة Trivy عليه بعد الإصلاح ظهرت في قسم Trivy أعلاه، وهي كما يلي:
| الثغرة | قبل الإصلاح | بعد الإصلاح | هل النتيجة صحيحة؟ |
|---|---|---|---|
| CVE-2026-109618 و109619 (PHP) | 5 حزم CRITICAL بحالة fixed | لا شيء | نعم، أغلقت النتيجة |
| CVE-2026-91403 (Java) | openjdk-17 بحالة affected | openjdk-17 بحالة affected | لا، إنذار خاطئ False Positive بعد تحديث الشهادات |
| DLA-4845-1 (الشهادات) | ca-certificates قابلة للإصلاح | لا شيء | نعم، وهي الإصلاح الحقيقي لثغرة Java |
| CVE-2026-111497 (Node.js) | لا شيء على nodejs | لا شيء على nodejs | لا، ثغرة فاتت الأداة False Negative قبل الإصلاح |
| CVE-2026-111497 (libbrotli1) | affected | affected | صحيحة للمكتبة، ولا إصلاح لها في Debian 12 بعد |
لماذا بقيت ثغرة Java مفتوحة رغم الإصلاح؟ إنذار خاطئ False Positive
تقول نشرة OpenJDK في 21 أكتوبر إن CVE-2026-91403 تتعلق بثماني شهادات وسيطة Intermediate CA مخترقة، وإن Java المثبت من حزم التوزيعة في Debian وUbuntu وRed Hat يستخدم شهادات النظام، فيكفي تحديث شهادات النظام. وقد أصدرت Debian هذا الإصلاح في DLA-4845-1 يوم 20 أكتوبر، فحذفت الشهادتين «emSign ECC Root CA - G3» و«Comodo AAA Services root». ولنتحقق بأنفسنا، نقارن مخزن الشهادات Truststore الذي يستخدمه Java على السيرفرين:
keytool -list -cacerts -storepass changeit 2>/dev/null | grep -i -E "emsign|aaa"
keytool -list -cacerts -storepass changeit | grep -c trustedCertEntryوالمخرج على srv-java قبل الإصلاح:
debian:comodo_aaa_services_root.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_ecc_root_ca_-_c3.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_ecc_root_ca_-_g3.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_root_ca_-_c1.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_root_ca_-_g1.pem, Oct 22, 2026, trustedCertEntry,
150والمخرج على srv-patched بعد تحديث ca-certificates:
debian:emsign_ecc_root_ca_-_c3.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_root_ca_-_c1.pem, Oct 22, 2026, trustedCertEntry,
debian:emsign_root_ca_-_g1.pem, Oct 22, 2026, trustedCertEntry,
148فالشهادتان اختفتا من مخزن Java نفسه، أي أن الخطر الذي تصفه النشرة زال، ومع ذلك يبقى Trivy وOSV يعرضان الثغرة على openjdk-17 لأن متتبع Debian يربط الرقم بحزمة openjdk-17 وينتظر إصدارها الجديد، ويعرض إصلاح الشهادات تحت رقم DLA منفصل. والحل الأنسب هنا أن تسجل استثناء Exception لهذه النتيجة مع السبب ورابط النشرة وتاريخ انتهاء، كما شرحنا في فحص ال Docker Images، ثم تزيله عندما تصل حزمة openjdk-17 الجديدة، ولا تقم بإخفاء الرقم نهائياً، والسبب أن النشرة نفسها قد تتغير.
مقارنة النتائج Results: من رأى ماذا؟
وبعد التجربة نجمع ما رأته كل طريقة في جدول واحد، والحكم فيه على الثغرات الأربع فقط:
| السؤال | Trivy rootfs | Wazuh | سكربت OSV وNVD والخلاصات |
|---|---|---|---|
| PHP على srv-php | نعم، مع النسخة المصلحة | لا، حتى 16:17 | نعم في OSV، والخلاصة قبل CVE بأربع عشرة ساعة |
| Java على srv-java | نعم، بلا إصلاح | لا | نعم في OSV وNVD |
| Java بعد تحديث الشهادات | إنذار خاطئ | لم نقسه | إنذار خاطئ في OSV |
| Node.js من NodeSource | فاتته | لا | عبر Brotli في NVD ونشرة nodejs.org فقط |
| Java من Temurin في مجلد | فاته | لم نقسه | لا يعرف ما ثبت خارج الحزم |
| الموارد | ثوان، مع تنزيل قاعدة Java بحجم 941 MiB في أول فحص | 4.4 GiB RAM وأكثر من 16 GB قرص | لا شيء تقريباً |
مشكلات شائعة Common Problems وحلولها
- رسالة
context deadline exceededمن Trivy: السبب تنزيل قاعدة Java في أول تشغيل، فارفع المهلة بالخيار--timeout 15mأو نزل القواعد مسبقاً. - رسالة
template file must have .tpl extension: غير امتداد ملف القالب إلى.tpl. - رسالة
Depends: perl-base (= …) but … is to be installedعند بناء سيرفر قديم: ال Base Image أحدث من لحظة الأرشيف، فاختر Image أقدم من تاريخ snapshot. - لوحة Wazuh لا تعرض ثغرة نشرت اليوم: تحقق من تاريخ أحدث ثغرة في الفهرس ومن سطر
Initiating update feed processفي السجل، فقد تكون القاعدة لم تتحدث بعد، وافحص بأداة أخرى في يوم النشرة. - Docker يتوقف عن الاستجابة أثناء المختبر: راقب المساحة الحرة على قرص السيرفر المضيف قبل تشغيل Wazuh وOpenCVE معاً، فهما يحتاجان معاً أكثر من 30 GB.
- NVD ترد ببطء أو بخطأ: بدون مفتاح API يسمح بخمسة طلبات كل ثلاثين ثانية، والسكربت ينتظر ست ثوان بين الطلبات ويعيد المحاولة ثلاث مرات.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما ظهر في المختبر:
- الفحص بدون وكيل مع Trivy أعطى أدق نتيجة لثغرة PHP خلال ثوان، مع النسخة التي تصلحها، وهو الخيار الأنسب لمن يدير عدداً قليلاً من السيرفرات.
- Wazuh يعطيك صورة مستمرة لكل السيرفرات ويغلق النتيجة وحده بعد التحديث، لكنه احتاج 4.4 GiB من الذاكرة وأكثر من 16 GB من القرص، ولم يعرف ثغرات أكتوبر خلال ثلاث ساعات ونصف من التشغيل، فلا تعتمد عليه وحده في يوم النشرة.
- كل أداة تعتمد على قاعدة بيانات الحزم يفوتها ما ثبت خارجها، مثل Node.js من NodeSource وJava من ملف مضغوط، ولذلك فجردك لما ثبتته بيدك جزء أساسي من إدارة الثغرات.
- الإصلاح قد يكون في حزمة أخرى، كما في ثغرة Java التي أصلحتها حزمة الشهادات، فتبقى النتيجة مفتوحة وهي إنذار خاطئ، وتحتاج استثناء موثقاً بتاريخ انتهاء.
- التنبيه بالجديد لا يحتاج أداة كبيرة، فسكربت صغير يسأل OSV وNVD ونشرات الموردين يكفي لعدد قليل من المنتجات، ونشرات الموردين تسبق سجلات CVE بساعات.
- لا يكتمل الإصلاح قبل إعادة تشغيل الخدمة التي تحمل الكود القديم، ثم إعادة الفحص.
دليلإدارة الثغرات Vulnerability Management: هل سيرفرك متأثر؟كيف تعرف خلال ساعات أن ثغرة جديدة تمس سيرفراتك، ولماذا يخدعك رقم الإصدار، وكيف ترتب الإصلاح وتتأكد أنه تم فعلاً.
دليلأدوات إدارة الثغرات Vulnerability Management مفتوحة المصدرأي أداة مفتوحة المصدر تجرد سيرفراتك وتطابقها مع الثغرات وتنبهك بالجديد، وماذا رأى Trivy وGrype فعلاً في ثغرات أكتوبر.
دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.
دليلSCA: كيف تجد الثغرات المعروفة في مكتبات مشروعككيف تجد أداة فحص الاعتماديات SCA المكتبات المصابة في مشروعك، ولماذا تختلف نتائج الأدوات، وكيف تبقي مكتباتك محدثة.
دليلتأمين سيرفر VPS على Ubuntu من أول دخولقبل أن تثبت أي تطبيق على سيرفرك الجديد، هذه الخطوات تغلق أبرز الثغرات وأولها الدخول بمفتاح SSH بدل كلمة المرور.تدريب عملي Practice
- اكتب ملف inventory.json لسيرفراتك من مخرج
dpkg-query -W -f='${source:Package} ${source:Version}\n'، ثم شغلpython cve_watch.py matchوقارن النتيجة بما تعرفه عن كل سيرفر. - ابحث في سيرفراتك عن البرامج المثبتة خارج مدير الحزم، مثل المجلدات في
/optوالملفات التنفيذية في/usr/local/bin، واكتبها في جرد منفصل مع مصدر نشرات كل منها.
سجل التحديثات
- أكتوبر 2026: كتابة المقال وتنفيذ المختبر يوم 22 أكتوبر 2026 على Debian 12 مع Trivy 0.76.1 وWazuh 4.14.9 وسكربت Python للتنبيه، وقد توقف المختبر قبل أن تظهر ثغرات أكتوبر في Wazuh لأن قرص السيرفر المضيف امتلأ، فلم نقس وقت ظهورها فيه.