الأدلة التقنية

Snipe-IT: تثبيت نظام سجل الأجهزة والرخص وتشغيله

كيف تثبت Snipe-IT للإنتاج خلف Reverse Proxy، وتربطه بالدليل والبريد، وتأخذ منه نسخة احتياطية تستعيدها فعلاً.

Snipe-IT: تثبيت نظام سجل الأجهزة والرخص وتشغيله

في مقال دورة حياة أجهزة المؤسسة استخدمنا Snipe-IT لنطبق الإجراءات على نظام حقيقي، فأنشأنا الأصول عبر ال API وطبعنا ملصقات QR وسلمنا وجردنا وأتلفنا، ولكن ذلك التثبيت كان تثبيت تجربة على منفذ محلي دون شهادة ودون دليل مستخدمين ودون بريد. وعندما تنقل النظام نفسه إلى الإنتاج تظهر أسئلة مختلفة تماماً: ما الطريقة المدعومة للتثبيت؟ كيف يعرف التطبيق أنه خلف Reverse Proxy؟ من أين يأتي الموظفون، من الدليل أم يدوياً؟ وهل النسخة الاحتياطية التي يأخذها النظام وحده كل أسبوع تكفي لتعيده على سيرفر جديد؟ وهذا الدليل يجيب عن هذه الأسئلة بتثبيت كامل طبقناه خطوة خطوة، مع أخطاء صنعناها عمداً لنرى كيف تظهر.

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

  • طريقة التثبيت المدعومة، وتوليد مفتاح التطبيق APP_KEY ومفاتيح OAuth.
  • التشغيل خلف Reverse Proxy مع HTTPS، وماذا يحدث عندما يكون APP_URL خاطئاً.
  • البريد، ومزامنة المستخدمين من LDAP، والدخول الموحد SSO عبر authentik.
  • الأدوار والصلاحيات، ودعم الشركات المتعددة Full Company Support.
  • الموديلات ومجموعات الحقول Fieldsets والإهلاك، واستيراد ملف CSV.
  • التراخيص والملحقات والمستهلكات والقطع، وإقرار الاستلام EULA، والتنبيهات والتقارير وال API.
  • نسخة احتياطية تعمل فعلاً مع اختبار استعادة، ثم التحديث والتأمين.

أما الإجراءات نفسها، أي ترقيم الأصول والملصقات والتسليم والجرد والمغادرة والإتلاف، فلن نكررها هنا، وسوف نشير إلى أقسامها في ذلك المقال كلما احتجنا إليها:

دليلدورة حياة أجهزة المؤسسة IT Asset Managementكيف تتابع كل جهاز في مؤسستك من الطلب والشراء إلى التسليم والجرد والإرجاع، ثم المسح والإتلاف بسجل كامل.دليلأي نظام تذاكر Helpdesk يناسب قسم تقنية المعلومات؟مقارنة أنظمة التذاكر المفتوحة بتراخيصها وأجزائها المدفوعة ودعمها للعربية، مع تجربة البريد وال SLA على ثلاثة منها.دليلكيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxyإذا كان تطبيقك يرى عنوان ال Proxy بدلاً من الزائر أو يولد روابط http، فهذا هو السبب والإعداد الصحيح لإطار عملك.

شكل التثبيت الذي سوف نبنيه

الصورة التالية تبين التثبيت كاملاً، والفكرة الأساسية فيها أن Snipe-IT نفسه لا يستقبل أي طلب من الخارج مباشرة، وإنما يمر كل طلب عبر ال Reverse Proxy الذي يحمل الشهادة، وحول التطبيق ثلاث خدمات لا يعمل في الإنتاج دونها: الدليل والبريد والنسخ الاحتياطي:

سبعة أجزاء مرقمة: 1 متصفح الموظف أو الفني، 2 Reverse Proxy يحمل شهادة TLS ويرسل X-Forwarded-Proto وX-Forwarded-For، 3 Snipe-IT v8.8.0 مع المتغيرات APP_URL وAPP_TRUSTED_PROXIES وAPP_TRUSTED_HEADERS وAPP_KEY وملفات oauth-*.key، 4 قاعدة بيانات MariaDB 11.4.7، وفي الأسفل 5 LDAP وSAML لمزامنة الحسابات وتسجيل الدخول من Active Directory أو authentik، 6 SMTP لإقرارات الاستلام والتنبيهات، 7 النسخة الاحتياطية مع APP_KEY لأن النسخة وحدها لا تكفي للاستعادة
كل طلب يمر عبر ال Reverse Proxy، وحول التطبيق الدليل والبريد والنسخ الاحتياطي

والإصدار الذي استخدمناه هو Snipe-IT v8.8.0 الصادر في 30 سبتمبر 2026، وهو إصدار يتطلب PHP 8.2 على الأقل كما في ملاحظاته، وفيه ميزات جديدة تهمنا في هذا الدليل سوف نذكرها في مكانها، مثل محولات المزامنة Sync Adapters مع أنظمة إدارة الأجهزة، وأداة دمج الموديلات المكررة.

أي طريقة تثبيت هي المدعومة؟

لـ Snipe-IT طريقتان للتثبيت، والسؤال الأول هو أيهما تختار:

  • التثبيت اليدوي على السيرفر: تنزل الكود وتثبت PHP وApache أو IIS وقاعدة البيانات بنفسك، وتقول صفحة المتطلبات إن الإصدار الحالي يحتاج PHP من 8.2 إلى ما قبل 8.6، وMySQL 5.7 أو أحدث أو MariaDB، وفيها قسم خاص بـ Linux وآخر بـ Windows Server.
  • التثبيت عبر Docker: تقول صفحة Docker في التوثيق صراحة إن ال Image الوحيدة التي يدعمها المشروع هي snipe/snipe-it، وإن ما سواها من Images غير مدعوم، والمشروع نفسه ينشر ملف Docker Compose للإنتاج في مستودعه.

وقد اخترنا Docker بال Image الرسمية، والسبب أن نسخة PHP وإضافاتها وإعداد Apache تأتي مختبرة مع الإصدار نفسه، فلا تكسر ترقية PHP على السيرفر نظام الأصول دون أن تنتبه. وتحذر صفحة Docker أيضاً من استخدام الوسم latest، لأنه يسبب مشكلات عند الاستعادة، فثبت رقم الإصدار دائماً كما سوف نفعل.

ملف Docker Compose

الملف التالي مبني على ملف المشروع، مع ثلاثة فروق: لا يوجد منفذ منشور للتطبيق نفسه، وأمامه Caddy كـ Reverse Proxy، ومعه في التجربة خادم بريد للاختبار Mailpit وخادم OpenLDAP بدل دليل المؤسسة. وكل المنافذ مربوطة على 127.0.0.1 فقط:

# Snipe-IT production-style lab: app behind a TLS reverse proxy, MariaDB, SMTP sink, LDAP directory (test data only)
#   docker compose -p lab-sn up -d
name: lab-sn

services:
  app:
    image: snipe/snipe-it:v8.8.0
    container_name: lab-sn-snipe
    restart: unless-stopped
    volumes:
      - storage:/var/lib/snipeit
    env_file:
      - snipe.env
    depends_on:
      db:
        condition: service_healthy
    networks: [net]

  db:
    image: mariadb:11.4.7
    container_name: lab-sn-db
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: snipeit
      MYSQL_USER: snipeit
      MYSQL_PASSWORD: lab-db-pass-2026
      MYSQL_ROOT_PASSWORD: lab-root-pass-2026
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 10s
      retries: 60
      start_period: 600s
    networks: [net]

  proxy:
    image: caddy:2.11.7-alpine
    container_name: lab-sn-proxy
    restart: unless-stopped
    ports:
      - "127.0.0.1:18443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    networks: [net]

  mail:
    image: axllent/mailpit:v1.31.5
    container_name: lab-sn-mail
    ports:
      - "127.0.0.1:18325:8025"
    networks: [net]

  ldap:
    image: osixia/openldap:1.5.0
    container_name: lab-sn-ldap
    environment:
      LDAP_ORGANISATION: "ArabRoot Lab"
      LDAP_DOMAIN: lab.test
      LDAP_ADMIN_PASSWORD: lab-ldap-admin-2026
    networks: [net]

networks:
  net:
    name: lab-sn-net
    ipam:
      config:
        - subnet: 10.242.17.0/24

volumes:
  db_data:
    name: lab-sn-db-data
  storage:
    name: lab-sn-storage
  caddy_data:
    name: lab-sn-caddy-data

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

  • ال Volume المسمى storage يحمل كل ما يكتبه التطبيق: الملفات المرفوعة وإقرارات الاستلام بصيغة PDF ومفاتيح OAuth والنسخ الاحتياطية، فلا تحذفه، وتذكر أن docker compose down -v يحذفه دون أن يسألك كما تنبه صفحة Docker.
  • أعطينا الشبكة نطاق عناوين ثابتاً 10.242.17.0/24، والسبب أننا سوف نحتاجه بعد قليل لنقول للتطبيق من هو ال Proxy الذي يثق به.
  • زدنا مهلة فحص الصحة لقاعدة البيانات، لأن أول تشغيل يطبق مئات ملفات الترحيل Migrations، كما رأينا في تثبيت التجربة السابقة.
  • خادما Mailpit وOpenLDAP للتجربة فقط، وفي الإنتاج تضع مكانهما خادم البريد ودليل المؤسسة.

مفتاح التطبيق APP_KEY

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

docker run --rm --entrypoint php snipe/snipe-it:v8.8.0 /var/www/html/artisan key:generate --show
base64:yuiB0WriVm34yZWYqMir8mUK/BNwMpQD83sIPnrmQuQ=

وهذا المفتاح هو مفتاح التجربة، فلا تستخدمه، واحفظ مفتاحك في مدير الأسرار في نفس اليوم، والسبب سوف تراه بوضوح في اختبار الاستعادة: النسخة الاحتياطية دون هذا المفتاح لا تعيد كل شيء. وإذا تسرب المفتاح يوماً ففي الإصدار نفسه أمر اسمه snipeit:rotate-key يغير المفتاح ويعيد تشفير البيانات بالمفتاح الجديد كما في وصفه.

ملف المتغيرات

ملف snipe.env التالي هو الملف النهائي بعد كل الإصلاحات التي سوف نمر بها في هذا الدليل، وبدأنا التجربة بقيمة APP_URL من ملف المثال، أي http://localhost:8000، ودون السطرين الأخيرين، حتى نرى ما يحدث:

# Lab values only. Generate your own APP_KEY, never reuse the one in .env.docker:
#   docker run --rm --entrypoint php snipe/snipe-it:v8.8.0 /var/www/html/artisan key:generate --show
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:yuiB0WriVm34yZWYqMir8mUK/BNwMpQD83sIPnrmQuQ=
APP_URL=https://assets.lab.test:18443
APP_TIMEZONE=Asia/Riyadh
APP_LOCALE=en-US
# mysql, not mariadb: the image ships mysqldump only, and snipeit:backup with mariadb looks for mariadb-dump
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=lab-db-pass-2026
DB_DUMP_PATH=/usr/bin
MAIL_MAILER=smtp
MAIL_HOST=mail
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
[email protected]
MAIL_FROM_NAME=IT Assets
[email protected]
MAIL_REPLYTO_NAME=IT Assets
SESSION_LIFETIME=480
SECURE_COOKIES=true
BACKUP_ENV=true
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# The proxy network, and the headers to believe from it (without APP_TRUSTED_HEADERS nothing is trusted)
APP_TRUSTED_PROXIES=10.242.17.0/24
APP_TRUSTED_HEADERS=HEADER_X_FORWARDED_FOR,HEADER_X_FORWARDED_HOST,HEADER_X_FORWARDED_PORT,HEADER_X_FORWARDED_PROTO

مفاتيح OAuth لل API

بعد أول تشغيل وجدنا مجلد المفاتيح في ال Volume فارغاً، وهي المشكلة نفسها التي ظهرت في التجربة السابقة: أول محاولة لإنشاء مفتاح API تفشل بخطأ Invalid key supplied، ثم بخطأ Personal access client not found. والحل نفسه بثلاثة أوامر تشغلها مرة واحدة بعد التثبيت:

docker exec -u docker lab-sn-snipe php /var/www/html/artisan passport:keys
docker exec -u docker lab-sn-snipe php /var/www/html/artisan passport:client --personal \
  --name="Snipe-IT Personal Access Client" --no-interaction
docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:make-api-key \
  --user_id=2 --name=lab --key-only
total 16
drwxr-xr-x 2 docker root  4096 Oct 15 15:01 .
drwxr-xr-x 6 root   root  4096 Oct 15 14:57 ..
-rw------- 1 docker staff 3322 Oct 15 15:01 oauth-private.key
-rw-rw---- 1 docker staff  812 Oct 15 15:01 oauth-public.key

المخرج أعلاه هو محتوى المجلد /var/lib/snipeit/keys بعد الأمر الأول، ولاحظ أن المفاتيح تكتب في ال Volume وليس داخل ال Container، فتبقى بعد إعادة إنشائه، وأن المفتاح الخاص بصلاحية 600، وسوف نعود إلى هذه الصلاحية في الاستعادة.

خلف ال Reverse Proxy مع HTTPS

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

# TLS from Caddy's internal CA (lab only); in production use your real domain and a public certificate
assets.lab.test {
	tls internal
	reverse_proxy app:80
}

اكسرها: عنوان APP_URL من ملف المثال

شغلنا التثبيت أول مرة بقيمة APP_URL=http://localhost:8000 كما في ملف المثال، ثم طلبنا الموقع عبر ال Proxy بعنوانه الصحيح:

C="curl -sk --resolve assets.lab.test:18443:127.0.0.1"
$C -o /dev/null -w "GET / -> %{http_code} Location: %{redirect_url}\n" https://assets.lab.test:18443/
$C https://assets.lab.test:18443/setup | grep -o -E '(href|src|action)="[^"]*(css|js|setup)[^"]*"' | head -5
GET / -> 302 Location: http://localhost:8000/setup
href="http://localhost:8000/css/dist/all.css?id=3f797bdf9cac1af9e94708e6e839527b"
href="http://localhost:8000/setup"
href="http://localhost:8000/setup/user"
action="http://localhost:8000/setup/migrate"
src="http://localhost:8000/js/dist/all.js"

الطلب وصل إلى التطبيق عبر HTTPS، ولكن التطبيق حول الزائر إلى http://localhost:8000، وكتب كل روابط الصفحة وملفاتها بهذا العنوان، فيرى الموظف صفحة بلا تنسيق أو خطأ اتصال. والسبب في الكود نفسه: ملف AppServiceProvider في الإصدار v8.8.0 يجبر كل الروابط على العنوان المكتوب في APP_URL، ويجبر HTTPS إذا بدأ العنوان بـ https://، وذلك حتى لا يستطيع أحد أن يغير الروابط بترويسة Host مزورة. أي أن APP_URL هو مصدر الحقيقة الوحيد للروابط، وهو أيضاً الرابط الذي يدخل في رسائل البريد وفي رموز QR على الملصقات كما رأينا في الملصق ورمز QR، فاكتبه بالعنوان النهائي كاملاً مع المنفذ إن وجد، قبل أول ملصق وأول بريد:

GET / -> 302 Location: https://assets.lab.test:18443/setup
href="https://assets.lab.test:18443/css/dist/all.css?id=3f797bdf9cac1af9e94708e6e839527b"
href="https://assets.lab.test:18443/setup"
href="https://assets.lab.test:18443/setup/user"

من هو الزائر؟ APP_TRUSTED_PROXIES وحده لا يكفي

بعد إصلاح العنوان أكملنا معالج الإعداد، ثم سجلنا دخولاً خاطئاً ودخولاً صحيحاً بالسكربت login.sh في مجلد التجربة، ونظرنا في جدول محاولات الدخول، لأن Snipe-IT يسجل عنوان كل محاولة فيه:

sh login.sh itadmin wrong-pass
docker exec lab-sn-db mariadb -usnipeit -plab-db-pass-2026 snipeit \
  -e "select username, remote_ip, successful from login_attempts;"
POST /login -> 302 http://assets.lab.test:18443/login
username	remote_ip	successful
itadmin	10.242.17.4	0

هنا مشكلتان: العنوان المسجل هو عنوان ال Proxy نفسه 10.242.17.4 لكل الموظفين، والدخول الخاطئ أعاد الزائر إلى رابط يبدأ بـ http:// رغم أن APP_URL يبدأ بـ HTTPS، لأن هذه الإعادة تبنى من الطلب الذي يراه التطبيق، والتطبيق يرى طلب HTTP قادماً من ال Proxy. وقد شرحنا سبب ذلك في تطبيقك خلف Reverse Proxy. والحل المتوقع هو المتغير APP_TRUSTED_PROXIES الموجود في ملف المثال، فأضفناه بنطاق شبكة التجربة وأعدنا المحاولة:

POST /login -> 302 http://assets.lab.test:18443/login
username	remote_ip	successful
itadmin	10.242.17.4	0

لم يتغير شيء، والسبب في ملف TrustProxies في الإصدار نفسه: الترويسات التي يصدقها التطبيق من ال Proxy تأتي من متغير ثان اسمه APP_TRUSTED_HEADERS، وقيمته الافتراضية فارغة، فلا تصدق أي ترويسة حتى لو كان ال Proxy موثوقاً، وهذا المتغير غير موجود في ملف .env.docker أصلاً، وموجود فقط في ملف .env.example. فأضفنا الترويسات الأربع:

POST /login -> 302 https://assets.lab.test:18443/login
username	remote_ip	successful
itadmin	10.242.17.1	0

الآن الإعادة إلى HTTPS، والعنوان المسجل تغير. ولاحظ أن 10.242.17.1 هو بوابة شبكة Docker، لأن الطلب في التجربة جاء من الجهاز نفسه عبر المنفذ المنشور، أما في الإنتاج فسوف تجد هنا عنوان جهاز الموظف. وقد يتساءل البعض: لماذا لا أكتب * في APP_TRUSTED_PROXIES وأرتاح؟ والإجابة أن ذلك يجعل التطبيق يصدق ترويسة X-Forwarded-For من أي أحد يصل إليه مباشرة، فيكتب المهاجم في سجل محاولات الدخول العنوان الذي يريده، كما شرحنا في ترويسة يكتبها أي أحد، ولذلك اكتب عنوان ال Proxy أو شبكته فقط، ولا تنشر منفذ التطبيق نفسه.

الإعداد الأول والمدير

عند أول زيارة يحول Snipe-IT كل صفحة إلى /setup، وفي صفحة إنشاء المدير ثلاثة إعدادات تستحق التفكير قبل الضغط على الزر، لأن تغييرها لاحقاً مكلف:

  • بادئة أرقام الأصول وعدد خاناتها، واخترنا ARB- و6 خانات حسب قواعد الترقيم في الاستلام والترقيم.
  • العملة الافتراضية، واخترنا SAR.
  • دعم الشركات المتعددة Full Multiple Companies Support، وفعلناه لأننا سوف نجرب فرعين في الأدوار والشركات.

ويجدر الإشارة إلى أن الأمر snipeit:create-admin ينشئ مديراً من سطر الأوامر، ولكنه لا ينشئ صف الإعدادات، فبعد أن شغلناه بقي الموقع يحول إلى /setup، فاستخدم المعالج للمدير الأول، والأمر للمديرين الإضافيين أو لاستعادة الدخول.

البريد الصادر SMTP

البريد في Snipe-IT ليس كماليات، فعليه تقوم ثلاث وظائف أساسية: إقرار الاستلام الذي يرسل للموظف عند التسليم، والتنبيهات عن التراخيص والضمانات والمخزون، ورسائل استعادة كلمة المرور. والإعداد في متغيرات MAIL_* كما في الملف أعلاه، ومنها خيار MAIL_MAILER=microsoft-graph الذي أضافه الإصدار v8.8.0 للمؤسسات التي تستخدم Microsoft 365 دون SMTP AUTH. وقبل أي شيء آخر تأكد أن ال Container يستطيع الإرسال فعلاً، وفي مجلد التجربة سكربت صغير mailtest.php يرسل رسالة واحدة عبر إعدادات التطبيق نفسها:

docker cp mailtest.php lab-sn-snipe:/tmp/mailtest.php
docker exec -u docker lab-sn-snipe php /tmp/mailtest.php
curl -s http://127.0.0.1:18325/api/v1/messages | grep -o '"Subject":"[^"]*"'
sent
"Subject":"Snipe-IT SMTP check"

وفي الإنتاج يرسل التطبيق عبر خادم البريد أو خدمة إرسال، والفرق بين الإرسال المباشر والترحيل Relay شرحناه في الفرق بين SMTP والترحيل، وتأكد أن عنوان المرسل في MAIL_FROM_ADDR من نطاق له سجلات SPF وDKIM صحيحة، وإلا ذهبت إقرارات الاستلام إلى مجلد البريد المزعج.

مزامنة المستخدمين من LDAP

الطريقة البسيطة هي أن ينشئ الفني كل موظف يدوياً عند التسليم، وهذه الطريقة تعمل في الأسبوع الأول فقط، والسبب أن الأسماء والبريد والقسم تتغير في الدليل ولا تتغير في Snipe-IT، وأن الموظف المغادر يبقى نشطاً في نظام الأصول. ولذلك يربط Snipe-IT بالدليل، سواء كان Active Directory أو أي خادم LDAP آخر، والمزامنة تتم بالأمر snipeit:ldap-sync أو من صفحة الإعدادات، وتنشئ المستخدمين وتحدثهم، وتسمح لهم بالدخول بكلمة مرور الدليل كما في صفحة LDAP في التوثيق.

وفي التجربة أنشأنا في OpenLDAP أربعة حسابات من الملف people.ldif: سارة وعمر ولكل منهما الحقل employeeType: active، وهدى دون هذا الحقل، وحساب خدمة اسمه svc-snipeit يستخدمه Snipe-IT للربط Bind. ثم كتبنا إعدادات LDAP بالسكربت ldap-settings.php، وهو يكتب الحقول نفسها التي تحفظها صفحة Admin Settings ثم LDAP، ولاحظ أن كلمة مرور حساب الربط تحفظ مشفرة بمفتاح التطبيق:

$s->ldap_enabled = 1;
$s->ldap_server = 'ldap://ldap:389';
$s->ldap_uname = 'uid=svc-snipeit,ou=people,dc=lab,dc=test';
$s->ldap_pword = Illuminate\Support\Facades\Crypt::encrypt('Ldap-Bind-2026!');
$s->ldap_basedn = 'ou=people,dc=lab,dc=test';
$s->ldap_filter = '&(objectClass=inetOrgPerson)(employeeNumber=*)';
$s->ldap_username_field = 'uid';
$s->ldap_fname_field = 'givenname';
$s->ldap_lname_field = 'sn';
$s->ldap_email = 'mail';
$s->ldap_emp_num = 'employeenumber';
$s->ldap_auth_filter_query = 'uid=';
$s->ldap_version = 3;
$s->is_ad = 0;
$s->ldap_active_flag = $argv[1] ?? null;

وفي ال LDAP التجربة استخدمنا ldap:// دون تشفير لأن الشبكة داخلية، أما في الإنتاج فاستخدم ldaps:// أو StartTLS، وتذكر أن الإصدارات قبل 8.6.3 كان فيها خلل يجعل StartTLS يفشل بصمت فيرسل كلمة مرور الربط دون تشفير، كما في التنبيه الأمني GHSA-wx48-r3qh-jvh5.

مزامنة فشلت دون أن تقول شيئاً

أول تشغيل للمزامنة لم يطبع أي شيء، ورقم الخروج صفر:

docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:ldap-sync --summary; echo "exit=$?"
docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:ldap-sync --json_summary
exit=0
{"error":true,"error_message":"ldap_search(): Search: No such object","summary":[]}

الخيار --summary لم يعرض الخطأ ولم يغير رقم الخروج، بينما الخيار --json_summary أظهر السبب: البحث لم يجد الفرع ou=people. والفرع موجود، ولكن حساب الخدمة لا يملك صلاحية قراءة عليه، وخادم LDAP في هذه الحالة يرد بأن الكائن غير موجود بدلاً من أن يقول إنك لا تملك صلاحية، حتى لا يكشف ما في الدليل. وهذا بالضبط ما تحذر منه صفحة ربط Snipe-IT مع authentik حين تطلب إعطاء حساب الربط صلاحية البحث في الدليل. فأضفنا للحساب صلاحية قراءة على الحقول التي يحتاجها فقط بالملف bind-acl.ldif، ثم أعدنا المزامنة:

+----------+----------+---------+---------+---------+
| Username | Name     | Action  | Status  | Note    |
+----------+----------+---------+---------+---------+
| sara     | Sara Lab | UPDATED | SUCCESS | updated |
| omar     | Omar Lab | CREATED | SUCCESS | created |
| huda     | Huda Lab | CREATED | SUCCESS | created |
+----------+----------+---------+---------+---------+

ولاحظ أن سارة كانت موجودة قبل المزامنة لأننا أنشأناها عبر ال API، فحدثها النظام بدل أن ينشئ حساباً ثانياً، والسبب أن المطابقة باسم المستخدم. والدرس العملي أن تشغل المزامنة المجدولة بالخيار --json_summary وتفحص الحقل error، لأن المزامنة الصامتة أسوأ من المزامنة التي تفشل بصوت عال.

اكسرها: حقل التفعيل الذي عطل موظفة

في صفحة LDAP حقل اسمه «LDAP Active Flag»، والهدف منه أن يعطل Snipe-IT من يعطله الدليل. ولنفرض أن المدير قرر أن الحقل employeeType يمثل حالة الموظف لأنه يرى فيه القيمة active عند أغلب الموظفين، وقبل التغيير حاولنا أن نسلم لهدى حاسوباً. ثم ضبطنا الحقل وشغلنا المزامنة:

docker exec -u docker lab-sn-snipe php /tmp/ldap-settings.php employeetype
docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:ldap-sync --summary
sh login.sh huda 'Ldap-Huda-2026!'
ldap_active_flag=employeetype
| Username | Name     | Action  | Status  | Note    |
+----------+----------+---------+---------+---------+
| sara     | Sara Lab | UPDATED | SUCCESS | updated |
| omar     | Omar Lab | UPDATED | SUCCESS | updated |
| huda     | Huda Lab | UPDATED | SUCCESS | updated |
+----------+----------+---------+---------+---------+
username	activated	assets
sara	1	1
omar	1	0
huda	0	0
POST /login -> 302 https://assets.lab.test:18443/login

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

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

ثم أزلنا حقل التفعيل وأعدنا المزامنة، فبقيت هدى معطلة، والسبب أن المزامنة دون حقل تفعيل في دليل غير Active Directory لا تلمس حالة التفعيل أصلاً كما في الكود، فلا تعيد ما عطلته. ولذلك أعدنا تفعيلها يدوياً عبر ال API. والقاعدة هنا أن تختبر أي تغيير في إعدادات LDAP على نسخة من البيانات أولاً، وأن تستخدم في Active Directory حقل userAccountControl الذي يقرؤه Snipe-IT تلقائياً عندما يكون حقل التفعيل فارغاً، بدل أن تخترع حقلاً.

أما التسليم لهدى فقد فشل قبل كل ذلك برسالة Checkout target company and asset company do not match، والسبب أن المستخدمين المستوردين من LDAP لا ينتمون لأي شركة، والجهاز ينتمي لشركة، ودعم الشركات المتعددة مفعل. فإذا فعلت هذا الدعم فخطط من البداية كيف يأخذ كل مستخدم مستورد شركته، لأن المزامنة تربط المستخدم بالموقع Location حسب الفرع OU في الدليل كما في الكود، ولا تربطه بالشركة.

الدخول الموحد مع authentik

يدعم Snipe-IT v8.8.0 في واجهة الويب الدخول عبر SAML، والدخول بحساب Google، ولا يدعم الدخول عبر OIDC بشكل عام كما في ملف المسارات Routes في الإصدار نفسه، أما ميزة «OIDC access tokens» الجديدة في هذا الإصدار فهي لاستخدام توكنات OIDC مع ال API فقط كما في ملاحظات الإصدار. ولذلك فالربط مع authentik يكون كما تصفه صفحة الربط في توثيق authentik بجزأين:

  1. تطبيق LDAP في authentik مع LDAP Outpost وحساب ربط له صلاحية البحث، تقوم عليه المزامنة التي شرحناها أعلاه، فتنشئ المستخدمين في Snipe-IT.
  2. تطبيق SAML يسجل به هؤلاء المستخدمون دخولهم، ورابط ال ACS فيه ينتهي بـ /saml/acs.

وتنبه الصفحة نفسها إلى أمرين يتفقان مع ما رأيناه: اضبط APP_URL بعنوان HTTPS النهائي قبل تفعيل SAML، لأن Snipe-IT يبني منه معرف ال Entity ورابط ال ACS ورابط البيانات الوصفية Metadata، فإذا غيرته لاحقاً تعطل الربط، واحفظ الرابط /login?nosaml الذي يفتح نموذج الدخول المحلي عندما يكون SAML مفعلاً، فهو طريقك للدخول إذا تعطل مزود الهوية. ولم نشغل authentik في هذه التجربة حتى تبقى خفيفة على الجهاز، وقد شرحنا تثبيته وإدارة الأدوار عبر التطبيقات في:

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

الأدوار والصلاحيات والشركات

الصلاحيات في Snipe-IT تعطى للمجموعات Groups، والمجموعة قائمة صلاحيات مثل عرض الأصول وتسليمها وإرجاعها، والمستخدم يأخذ صلاحيات مجموعاته. أما دعم الشركات المتعددة Full Multiple Companies Support فهو طبقة فوق ذلك: كل أصل ومستخدم ينتمي لشركة، والمستخدم العادي يرى ما يخص شركته فقط. والمثال الشائع جهة لها فروع، ولكل فرع فريق دعم يدير أجهزته فقط. ولتجربة ذلك أنشأنا بالسكربت sn.py شركتين، ومجموعة «Branch IT» بأربع صلاحيات، ومستخدماً في فرع جدة داخل هذه المجموعة:

grp = one("/groups", "Branch IT") or write("POST", "/groups", {"name": "Branch IT",
      "permissions": json.dumps({"assets.view": "1", "assets.checkout": "1", "assets.checkin": "1", "users.view": "1"})})

ثم استلمنا ثلاثة حواسيب، اثنين للرياض وواحداً لجدة، وطلبنا قائمة الأصول مرة بتوكن المدير ومرة بتوكن فني فرع جدة:

sh run.sh receive
sh run.sh list
sh run.sh list faisal
created ARB-000201 PF5GDE01 HQ Riyadh
created ARB-000202 PF5GDE02 HQ Riyadh
created ARB-000203 PF5GDE03 Jeddah Branch
admin sees 3 asset(s): ARB-000201 (HQ Riyadh), ARB-000202 (HQ Riyadh), ARB-000203 (Jeddah Branch)
faisal sees 1 asset(s): ARB-000203 (Jeddah Branch)

والفصل يعمل عبر ال API أيضاً، وليس في الواجهة فقط، وهذا مهم لأن أي سكربت أو سير عمل يستخدم توكن مستخدم يرى ما يراه ذلك المستخدم فقط. ولكن لاحظ أن الفصل بين الشركات في Snipe-IT كانت فيه ثغرات أصلحت في إصدارات قريبة، مثل تسليم الحزم Kits الذي كان يتجاوز الفصل حتى الإصدار 8.7.2 كما في التنبيه CVE-2026-88894، فإذا كان الفصل بين الشركات متطلباً قانونياً عندك فهذا سبب إضافي لتبقى على آخر إصدار.

الفئات والموديلات ومجموعات الحقول

البيانات في Snipe-IT مبنية في طبقات، وكل طبقة تحدد شيئاً لما تحتها:

الطبقةماذا تحددمثال من التجربة
الفئة Categoryنوع العنصر، وهل يحتاج إقرار استلام، ونص الإقرار، وهل يرسل بريد عند التسليمفئة Laptops مع إقرار استلام
الموديل Modelالشركة المصنعة والفئة ومجموعة الحقول والإهلاك وعمر الجهازالموديل ThinkPad T14 Gen 5 بعمر 48 شهراً
مجموعة الحقول Fieldsetالحقول المخصصة التي تظهر لأصول هذا الموديل فقطمجموعة Laptop فيها الذاكرة RAM وكلمة مرور BIOS
الحقل المخصص Custom Fieldاسم الحقل وصيغته، وهل يحفظ مشفراًحقل BIOS Password المشفر

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

asset_tag	bios_in_db	ram
ARB-000201	eyJpdiI6ImhUeUJQUlJ3V2NrWDRack1mQ0dRV2c9PSIsInZhbHVlIjoiOU94	32
ARB-000202	eyJpdiI6ImtFKytyRTFVQUYzRUczVXlNLy85MHc9PSIsInZhbHVlIjoidk5p	32
ARB-000203	eyJpdiI6Ik1HREp3VktObHFaR0Vqczdod1MyVmc9PSIsInZhbHVlIjoiVlBa	32

ورؤية الحقول المشفرة صلاحية مستقلة في المجموعات، فيرى الفني العادي النجوم بدل القيمة كما في الصورة التالية لصفحة الأصل:

صفحة الأصل ARB-000201 في Snipe-IT: الحالة Ready to Deploy مع شارة Deployed مسلم إلى Sara Lab، والقيمة الحالية 4,850 ريالاً، وتاريخ نهاية العمر 2030-10-01، والحقل RAM (GB) بقيمة 32، والحقل BIOS Password يظهر نجوماً مع قفل، وفي الجانب الشركة HQ Riyadh والفئة Laptops والإهلاك Laptops 48 months
صفحة الأصل: الحقول المخصصة من مجموعة الحقول، والحقل المشفر يظهر نجوماً

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

الإهلاك والقيمة الحالية

الإهلاك Depreciation في Snipe-IT جدول بسيط باسم وعدد أشهر، تربطه بالموديل، فيحسب النظام القيمة الحالية لكل جهاز بطريقة القسط الثابت من تاريخ الشراء والتكلفة، وقد شرحنا معنى الإهلاك ولماذا يهم المالية في الميزانية وCapEx وOpEx. ولنرى الحساب أضفنا حاسوباً اشتري قبل سنتين تماماً بـ 4,800 ريال:

sh run.sh old
sh run.sh show ARB-000150
created ARB-000150
{"asset_tag": "ARB-000150", "serial": "PF4OLD50", "purchase_cost": "4,800.00", "book_value": "2,400.00", "warranty_expires": {"date": "2027-10-15", "formatted": "2027-10-15"}, "eol": "48 months", "asset_eol_date": {"date": "2028-10-15", "formatted": "2028-10-15"}, "custom_fields": {"RAM (GB)": "16", "BIOS Password": ""}}

بعد 24 شهراً من 48 أصبحت القيمة الدفترية نصف التكلفة، أي 2,400 ريال. والصورة التالية تبين تقرير الإهلاك لكل الأجهزة، وفيه القسط الشهري لكل جهاز ومجموع التكلفة والقيمة الحالية:

تقرير Depreciation Report في Snipe-IT فيه ثمانية حواسيب ThinkPad T14 Gen 5 من فئة Laptops بإهلاك Laptops 48 months، والحاسوب ARB-000150 المشترى في 2024-10-15 بتكلفة 4,800 قيمته الحالية 2,400 وقسطه الشهري 100، والبقية بتكلفة 4,850 وقيمة حالية 4,850 وقسط شهري 101.04، والمجموع 38,750 تكلفة و36,350 قيمة حالية
تقرير الإهلاك: القيمة الحالية والقسط الشهري محسوبان من الموديل وتاريخ الشراء

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

استيراد ملف CSV

عند الانتقال من جدول Excel إلى Snipe-IT يكون الاستيراد أول عمل كبير، وهو أيضاً أخطر عمل، والسبب سوف تراه في هذه التجربة. أعددنا ملف inventory.csv بأربعة أجهزة كما يكتبه موظف في الواقع، فيه اسم الموديل مكتوب بطريقة مختلفة مرة، واسم الفئة بصيغة المفرد مرة، وعمود الضمان باسم «Warranty Months»:

Asset Tag,Serial Number,Model Name,Category,Manufacturer,Company,Status,Purchase Date,Purchase Cost,Warranty Months,Location
ARB-000301,PF5CSV01,ThinkPad T14 Gen 5,Laptops,Lenovo,HQ Riyadh,Ready to Deploy,2026-09-20,4850,36,"Riyadh HQ, IT Store"
ARB-000302,PF5CSV02,ThinkPad T14 Gen 5,Laptops,Lenovo,HQ Riyadh,Ready to Deploy,2026-09-20,4850,36,"Riyadh HQ, IT Store"
ARB-000303,PF5CSV03,Thinkpad T14 Gen5,Laptops,Lenovo,Jeddah Branch,Ready to Deploy,2026-09-20,4850,36,"Jeddah Branch, IT Room"
ARB-000304,PF5CSV04,ThinkPad T14 Gen 5,Laptop,Lenovo,Jeddah Branch,Ready to Deploy,2026-09-20,4850,36,"Jeddah Branch, IT Room"
docker cp inventory.csv lab-sn-snipe:/tmp/inventory.csv
docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:import /tmp/inventory.csv \
  --item-type=Asset --user_id=2
 ======= Importing Items from /tmp/inventory.csv =========
 Import finished.

id	model	category	fieldset_id	depreciation_id	assets
1	ThinkPad T14 Gen 5	Laptops	2	1	7
2	Thinkpad T14 Gen5	Laptops	NULL	NULL	1

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

  • الموديل المكتوب بطريقة مختلفة أصبح موديلاً جديداً دون مجموعة حقول ودون إهلاك، فالجهاز ARB-000303 لا يظهر فيه حقل BIOS ولا تنقص قيمته أبداً.
  • عمود «Warranty Months» تجاهله الاستيراد بصمت، فكل الأجهزة الأربعة بلا ضمان، والسبب أن الاستيراد من سطر الأوامر يبحث عن عمود اسمه «Warranty» كما في كود ال Importer، وأي عمود لا يعرفه يتجاهله.
  • الفئة «Laptop» أنشئت فئة جديدة.

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

id	name	category_id	category	updated_at
1	ThinkPad T14 Gen 5	3	Laptop	2026-10-15 18:05:41
2	Thinkpad T14 Gen5	2	Laptops	2026-10-15 18:05:41

وسجل النشاط لا يحتوي أي سطر عن تغيير فئة الموديل، ففيه سطرا الإنشاء فقط. أي أن ملف CSV واحداً بحرف ناقص ألغى إقرار الاستلام لكل أجهزة هذا الموديل دون أي أثر في التاريخ. والإصلاح كان بإعادة الموديل إلى فئته، ونقل الجهاز من الموديل المكرر ثم حذفه، وحذف الفئة الزائدة، كما في الأمر fix-import في السكربت، وفي الإصدار v8.8.0 أداة جديدة لدمج الموديلات Model Merge Tool تنقل الأجهزة وسجلها إلى الموديل الصحيح من الواجهة. وقواعد الاستيراد الآمن هي:

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

التراخيص والملحقات والمستهلكات والقطع

إلى جانب الأصول يدير Snipe-IT أربعة أنواع أخرى، والفرق بينها في طريقة العد وفيمن تسلم إليه:

النوعكيف يعديسلم إلىهل يعود؟مثال
الترخيص Licenseمقاعد Seats مع تاريخ انتهاءموظف أو جهازنعم، المقعد يعاداشتراك برنامج مكتبي بثلاثة مقاعد
الملحق Accessoryكمية دون رقم أصل لكل قطعةموظف أو موقع أو جهازنعممحطة توصيل USB-C للحاسوب
المستهلك Consumableكمية تنقصموظفلاحبر الطابعة
القطعة Componentكمية تركب داخل أصلأصلنعم، تفك من الأصلشريحة ذاكرة 16GB تركب في حاسوب

وأنشأنا واحداً من كل نوع وسلمناه بالأمر items، والمخرج يبين المتبقي بعد التسليم:

licence seat: success
accessory: success
consumable: success
component: success
licenses     Office Suite (subscription)  total=3 left=2
accessories  USB-C Dock                   total=5 left=4
consumables  Toner HP 59A                 total=3 left=1
components   16GB DDR5 SO-DIMM            total=4 left=3

ولاحظ أن الحد الأدنى للكمية Min QTY الذي ضبطناه على الملحق والمستهلك هو ما يبني عليه تنبيه المخزون كما سيأتي، وأن مقاعد التراخيص لا تعود وحدها عند تعطيل الموظف، كما رأينا في المغادرة.

إقرار الاستلام EULA

عندما تطلب الفئة إقرار استلام، يرسل Snipe-IT للموظف عند التسليم بريداً فيه تفاصيل الجهاز ونص الإقرار ورابط القبول، ويبقى التسليم «بانتظار القبول» حتى يضغط الموظف الرابط ويوافق. وبعد إصلاح فئة الموديل سلمنا ARB-000201 لسارة، فوصلها البريد التالي، وقد اختصرناه إلى الأسطر المهمة، فسطرا To وSubject من واجهة Mailpit، وحذفنا الأسطر الفارغة وسطر الشعار وجملة الشرح قبل نص الإقرار والتحية في آخره:

To: [email protected]
Subject: Asset checked out: ARB-000201
# Hello Sara Lab,
A new item has been checked out under your name that requires acceptance, details are below.
| **Asset Tag** | ARB-000201 |
| **Category** | Laptops |
| **Model** | ThinkPad T14 Gen 5 |
| **Serial** | PF5GDE01 |
| **Additional Notes** | Joiner ticket #5120 |
I received this laptop in working condition and will return it when I leave or when asked by IT.
**[✔ I have read and agree to the terms of use, and have received this item.](https://assets.lab.test:18443/account/accept/1)**

وحذفنا أيضاً من جدول التفاصيل رأسه وصفوف الشركة المصنعة والتاريخ والحالة والمدير. ولاحظ أن رابط القبول مبني من APP_URL، وهذا سبب آخر لضبطه قبل أول تسليم. ثم سجلنا الدخول بحساب سارة ووافقنا من صفحة القبول مع ملاحظة، فحفظ النظام الإقرار ملفاً بصيغة PDF:

id	accepted_at	note	stored_eula_file
1	2026-10-15 18:13:59	Received with charger	accepted-sara-arb-000201-2026-10-15-06-13-59.pdf

وهذا الملف هو الإقرار الذي تحتاجه عند المغادرة إذا لم يعد الجهاز، وهو يحفظ في ال Volume ضمن الملفات الخاصة، ولذلك يدخل في النسخة الاحتياطية كما سوف نرى. وفي قائمة التقارير تقرير اسمه «Unaccepted Assets» يعرض التسليمات التي لم يوافق عليها أصحابها، ومنه ترسل تذكيراً.

التنبيهات

التنبيهات تعمل عندما تفعلها في إعدادات Alerts وتكتب بريد الاستلام، وال Container الرسمي يشغل مجدول المهام Scheduler بنفسه، فقد رأينا في سجله البرنامج run_schedule يعمل بجانب Apache، والمجدول في ملف Kernel.php في الإصدار v8.8.0 يشغل أوامر التنبيهات يومياً. وحتى لا ننتظر يوماً شغلناها يدوياً:

docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:expiring-alerts
docker exec -u docker lab-sn-snipe php /var/www/html/artisan snipeit:inventory-alerts
There are 0 assets with expiring warranties or that are reaching their end of life in the next 30 days.
There is 1 license expiring or terminating in the next 30 days.
There are 2 items that are below minimum inventory or will soon be low.

واختصرنا من المخرج الأول جدولاً يكرر اسم الترخيص وتاريخه. ووصل البريدان إلى عنوان التنبيهات، وهذا جزء من نص تقرير المخزون كما وصل:

There are 2 items that are below minimum inventory or will soon be low.
|Name |Type |Current QTY|Min QTY
|:-------------|:---------|:---------:|:---------:|
|Toner HP 59A|consumables |1 |2 |
|USB-C Dock|accessories |4 |2 |

الحبر تحت الحد الأدنى، أما محطة التوصيل فما زال عندها 4 والحد الأدنى 2، ولكنها ظهرت لأن إعداد «Alert Threshold» في التجربة كان 5، أي أن النظام ينبه قبل الوصول إلى الحد الأدنى بخمس قطع. ونافذة الثلاثين يوماً للتراخيص والضمانات هي إعداد «Alert Interval». فاضبط الاثنين بحسب الوقت الذي تحتاجه المشتريات لتشتري، كما شرحنا في من طلب الشراء إلى الدفع، وإلا وصلك التنبيه بعد فوات الوقت.

التقارير

في قائمة التقارير في الإصدار v8.8.0 تقارير جاهزة، أهمها كما في مسارات الكود:

  • تقرير الإهلاك Depreciation: كما في الصورة أعلاه.
  • تقرير التراخيص Licenses: المقاعد المستخدمة والمتبقية وتواريخ الانتهاء.
  • تقرير الملحقات Accessories.
  • التسليمات التي لم تقبل Unaccepted Assets: مع إرسال تذكير.
  • التقرير المخصص Custom Asset Report: تختار فيه الأعمدة والفلاتر وتصدره بصيغة CSV، ومنه تبني تقرير الجرد الدوري وتقرير الأجهزة التي ينتهي عمرها.

وكل ما في التقارير متاح عبر ال API أيضاً، فإذا كان عندك نظام تقارير مثل Metabase فيمكنه القراءة من ال API أو من نسخة قراءة من قاعدة البيانات.

ال API والتكامل

توكن ال API في Snipe-IT يعمل بصلاحيات المستخدم الذي أنشأه، وهذا ما رأيناه في فصل الشركات، ولذلك أنشئ لكل تكامل مستخدم خدمة بمجموعة صلاحيات تكفيه فقط، كما فعلنا في سير العمل من التذكرة إلى تسليم الجهاز، ولا تستخدم توكن المدير. وانتبه إلى أن ملف المثال يضبط API_TOKEN_EXPIRATION_YEARS=40، أي أن التوكن يعيش أربعين سنة، فقلل المدة، وسجل في مدير الأسرار متى أنشئ كل توكن ولأي تكامل.

أما مزامنة الأجهزة من أنظمة إدارة الأجهزة MDM فكانت تحتاج سكربتات يكتبها المجتمع، وفي الإصدار v8.8.0 أضاف المشروع محولات مزامنة Sync Adapters مدمجة، وهي بحسب ملاحظات الإصدار في مرحلة تجريبية Beta، ومنها في الكود محولات لـ Intune وJamf وFleet وKandji وMosyle وGoogle Workspace وغيرها، والمجدول يشغل السحب يومياً كما في Kernel.php. ولم نجربها في هذا الدليل، ولأنها تجريبية فجربها على نسخة قبل أن تشغلها على بياناتك، لأن ملاحظات الإصدار نفسها تقول إن خطأ ربط اسم الموديل في المزامنة قد ينشئ موديلات جديدة، وهي المشكلة نفسها التي رأيناها في الاستيراد. وفكرة إدارة الأجهزة نفسها شرحناها في إدارة أجهزة الموظفين MDM.

نسخة احتياطية تعمل فعلاً

في Snipe-IT أمر نسخ احتياطي مدمج هو snipeit:backup، والمجدول في ال Container يشغله أسبوعياً وحده كما في Kernel.php، وهذا جيد، ولكنه يعني أيضاً أن النسخة الأسبوعية قد تفشل كل أسبوع دون أن يلاحظ أحد. وهذا ما يحدث مع إعداد ملف المثال: ملف .env.docker يضبط DB_CONNECTION=mariadb ويقول في تعليقه إن ذلك ليستخدم أداة mariadb-dump، بينما ال Image نفسها مبنية على Ubuntu 24.04 وفيها حزمة mysql-client فقط، فلا توجد فيها هذه الأداة. وقد رأينا هذا الفشل في التجربة السابقة، وتكرر هنا كما هو:

Starting backup...
Dumping database snipeit...
Backup failed because: The dump process failed with a none successful exitcode.
Exitcode
========
127: Command not found

والحل هنا بسطر واحد في ملف المتغيرات، أي DB_CONNECTION=mysql، فيستخدم الأمر أداة mysqldump الموجودة، وهي الإصدار 8.0.46 في ال Image، ويتصل بها بخادم MariaDB 11.4.7 دون مشكلة في تجربتنا:

Starting backup...
Dumping database snipeit...
Determining files to backup...
Zipping 30 files and directories...
Created zip containing 30 files and directories. Size is 121.88 KB
Copying zip to disk named backup...
Successfully copied zip to disk named backup.
Backup completed!

ثم فتحنا الملف المضغوط في Container بايثون، وهذه الملفات التي فيه:

  136499  db-dumps/mysql-snipeit.sql
    3322  storage/oauth-private.key
     812  storage/oauth-public.key
    1717  .env
  104231  storage/private_uploads/eula-pdfs/accepted-sara-arb-000201-2026-10-15-06-13-59.pdf

قاعدة البيانات ومفاتيح OAuth وإقرار سارة موجودة، ولكن انظر إلى ملف .env، فقد بحثنا فيه عن المفتاح:

['#APP_KEY=Change_this_key_or_snipe_will_get_ya', '#APP_URL=http://127.0.0.1:32782', 'DB_PASSWORD=${MYSQL_PASSWORD}']

هذا ليس ملف إعداداتك، وإنما قالب نسخته ال Image داخلها عند البناء، والسبب أن متغيراتك في Docker تصل إلى التطبيق كمتغيرات بيئة من env_file، ولا تكتب في ملف .env داخل ال Container، فالنسخة الاحتياطية في Docker لا تحمل مفتاح التطبيق أبداً، حتى مع الإعداد BACKUP_ENV=true. والصورة التالية تلخص ذلك:

ثلاثة أجزاء: 1 الملف snipe-it-2026-10-15-18-18-27.zip من الأمر snipeit:backup وفيه علامة صح أمام db-dumps/mysql-snipeit.sql وstorage/oauth-private.key وstorage/oauth-public.key وprivate_uploads/eula-pdfs، وعلامة تحذير أمام .env لأنه قالب ال Image دون APP_KEY، 2 قفل باسم APP_KEY في env_file أو مدير الأسرار ولا يدخل في النسخة، وسهم أحمر متقطع من النسخة وحدها إلى صندوق أحمر استعادة بمفتاح جديد مع The MAC is invalid والحقول المشفرة وكلمة ربط LDAP لا يمكن قراءتها، وسهم أخضر من النسخة عبر المفتاح إلى صندوق أخضر 3 استعادة بالمفتاح الأصلي حيث الحقول المشفرة تقرأ وتوكنات ال API تعمل
النسخة تحمل البيانات والمفاتيح الخاصة ب OAuth، أما APP_KEY فيبقى خارجها

فالنسخة الاحتياطية الكاملة لـ Snipe-IT في Docker هي ثلاثة أشياء: الملف المضغوط من الأمر، وملف snipe.env أو على الأقل مفتاح التطبيق في مدير الأسرار، وملف Docker Compose برقم الإصدار نفسه. وانقل الملف المضغوط خارج السيرفر، لأن مجلد النسخ داخل ال Volume نفسه، كما شرحنا في النسخ الاحتياطي بقاعدة 3-2-1.

اختبار الاستعادة، واكسرها: مفتاح جديد

النسخة التي لم تجرب استعادتها ليست نسخة، فأنشأنا تثبيتاً ثانياً فارغاً كأنه سيرفر بديل، بالملف compose-restore.yaml على شبكته و Volumes خاصة به، وبمفتاح تطبيق جديد مولد كما يفعل من يبني السيرفر البديل دون أن يجد المفتاح القديم، ثم استعدنا النسخة بالأمر المدمج:

docker compose -f compose-restore.yaml -p lab-sn-r up -d
docker cp backup/snipe-it-2026-10-15-18-18-27.zip lab-sn-r-snipe:/tmp/backup.zip
docker exec lab-sn-r-snipe chown docker /tmp/backup.zip
docker exec -u docker lab-sn-r-snipe php /var/www/html/artisan snipeit:restore /tmp/backup.zip --force --no-progress
3 files were succesfully transferred
.env was skipped.

هذان آخر سطرين في المخرج، والسطر الثاني يتفق مع صفحة النسخ الاحتياطي التي تقول إن الاستعادة لا تغير ملف .env. ثم عددنا الأصول فوجدناها 8 والمستخدمين 5، أي أن البيانات عادت، فهل انتهينا؟ شغلنا السكربت decrypt-check.php الذي يحاول قراءة حقل BIOS المشفر وكلمة مرور ربط LDAP، ثم المزامنة:

ARB-000201 BIOS Password: Illuminate\Contracts\Encryption\DecryptException: The MAC is invalid.
LDAP bind password: Illuminate\Contracts\Encryption\DecryptException: The MAC is invalid.
{"error":true,"error_message":"Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.","summary":[]}

وعبر ال API ظهر الحقل المشفر هكذا، بينما عمل توكن المدير القديم دون مشكلة لأن مفاتيح OAuth عادت مع النسخة:

ARB-000201 {"RAM (GB)":"32","BIOS Password":"Error Decrypting: The MAC is invalid."}

كل ما شفر بالمفتاح القديم أصبح غير مقروء، والرسالة نفسها تطلب منك تسجيل الدخول بحساب محلي وإعادة كتابة كلمة مرور LDAP، أي أن الدخول بحسابات الدليل يتعطل في يوم الاستعادة بالذات. أما كلمات مرور BIOS فلا يوجد طريق لاستعادتها دون المفتاح القديم. ثم وضعنا المفتاح الأصلي في ملف متغيرات التثبيت البديل وأعدنا إنشاء ال Container:

ARB-000201 BIOS Password: OK (12 characters)
LDAP bind password: OK (15 characters)

ولاحظنا أمراً أخيراً في الاستعادة: ملف oauth-private.key عاد بصلاحية 644 بدل 600 التي كانت له، فأعد الصلاحية بعد الاستعادة حتى لا يقرأ المفتاح الخاص أي مستخدم على النظام. وتذكر أن الاستعادة تشغل محتوى الملف المضغوط على قاعدة البيانات، وقد كان في الإصدارات قبل 8.7.0 ثغرة عالية الخطورة تسمح بتنفيذ أوامر على السيرفر من ملف استعادة مصنوع، كما في التنبيه CVE-2026-86733، فلا تستعد إلا نسخاً أخذتها أنت ومن مكان تثق به.

التحديث إلى إصدار أحدث

التحديث في Docker هو تغيير رقم الإصدار في ملف Compose، وال Container يطبق ملفات الترحيل وحده عند التشغيل، فقد رأينا في startup.sh في ال Image الأمر php artisan migrate --force قبل تشغيل Apache. وخطواته الآمنة هي:

  1. اقرأ ملاحظات الإصدار الجديد، وانتبه لأي تغيير في متطلبات PHP أو لأي ميزة تجريبية.
  2. خذ نسخة احتياطية بالأمر، ونسخة من ملف المتغيرات، كما تطلب صفحة التحديث قبل أي تحديث.
  3. غير رقم الإصدار في سطر image، ولا تستخدم latest.
  4. شغل docker compose pull ثم docker compose up -d، وتابع سجل ال Container حتى تنتهي ملفات الترحيل.
  5. افتح صفحة أصل ونفذ تسليماً تجريبياً، وتأكد من رقم الإصدار عبر /api/v1/version.

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

تأمين التثبيت

أغلب ما يحمي Snipe-IT إعدادات بسيطة، وأهمها:

  • اجعل APP_DEBUG=false دائماً، لأن صفحة الخطأ في وضع التصحيح تكشف المتغيرات والمسارات.
  • لا تنشر منفذ التطبيق، واجعل الوصول عبر ال Proxy فقط، واضبط APP_TRUSTED_PROXIES على عنوانه فقط.
  • اضبط SECURE_COOKIES=true مع HTTPS، وفعل ENABLE_HSTS بعد أن تتأكد أن الموقع يعمل عبر HTTPS فقط، وراجع ترويسات الأمان.
  • أبق ALLOW_BACKUP_DELETE=false وALLOW_DATA_PURGE=false، حتى لا يستطيع حساب مدير مخترق أن يحذف النسخ أو يمسح البيانات من الواجهة.
  • فعل التحقق الثنائي Two-Factor Authentication من إعدادات الأمان، وسياسة كلمات المرور، وراجع حد محاولات الدخول LOGIN_MAX_ATTEMPTS.
  • قلل API_TOKEN_EXPIRATION_YEARS، وأنشئ مستخدم خدمة لكل تكامل.
  • تابع التنبيهات الأمنية للمشروع، فقد نشر في أغسطس وأكتوبر 2026 وحدهما عشرة تنبيهات على الأقل، منها ثغرة الاستعادة التي ذكرناها، وقبول الدخول بحساب Google لمستخدمين معطلين حتى الإصدار 8.6.3، وإعادة استخدام إقرار SAML حتى الإصدار 8.5.0.

كم يستهلك؟

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

lab-sn-snipe 145.7MiB / 31.29GiB 0.02%
lab-sn-db 144.9MiB / 31.29GiB 0.01%
lab-sn-proxy 22.89MiB / 31.29GiB 0.00%
lab-sn-mail 35.08MiB / 31.29GiB 0.00%
lab-sn-ldap 20.23MiB / 31.29GiB 0.00%

أي أن التطبيق وقاعدة بياناته معاً أقل من 300MB في تثبيت صغير، وسيرفر صغير يكفي لمؤسسة بمئات الأجهزة، والذي يكبر مع الوقت هو قاعدة البيانات وسجل النشاط والملفات المرفوعة.

إزالة التجربة

docker compose -f compose-restore.yaml -p lab-sn-r down -v
docker compose -p lab-sn down -v
docker volume rm lab-sn-pw
docker image rm snipe/snipe-it:v8.8.0 mariadb:11.4.7 axllent/mailpit:v1.31.5 osixia/openldap:1.5.0

أخطاء شائعة

  • نسخ APP_KEY من ملف المثال، أو عدم حفظه خارج السيرفر.
  • ترك APP_URL على قيمة المثال، أو تغييره بعد طباعة الملصقات وتفعيل SAML.
  • ضبط APP_TRUSTED_PROXIES دون APP_TRUSTED_HEADERS، أو كتابة * فيه.
  • الاعتماد على نسخة snipeit:backup الأسبوعية دون أن تتأكد أنها تنجح، مع DB_CONNECTION=mariadb.
  • الظن أن النسخة الاحتياطية في Docker تحمل مفتاح التطبيق.
  • تشغيل مزامنة LDAP بالخيار --summary وحده، فلا تظهر أخطاء البحث.
  • ضبط حقل التفعيل في LDAP على حقل لا يملكه كل المستخدمين.
  • تفعيل دعم الشركات المتعددة دون خطة لربط المستخدمين المستوردين بشركاتهم.
  • استيراد ملف CSV بأسماء موديلات وفئات غير موحدة، أو بخيار التحديث على أعمدة الفئة.
  • تشغيل محولات المزامنة التجريبية على بيانات الإنتاج مباشرة.

الخلاصة

  • ال Image الوحيدة المدعومة هي snipe/snipe-it برقم إصدار ثابت، ومفتاح التطبيق تولده بنفسك وتحفظه في مدير الأسرار، ومفاتيح OAuth تولدها مرة واحدة بعد التثبيت.
  • المتغير APP_URL هو مصدر كل الروابط والرسائل ورموز QR، وخلف ال Proxy تحتاج APP_TRUSTED_PROXIES وAPP_TRUSTED_HEADERS معاً.
  • مزامنة LDAP قد تفشل بصمت وقد تعطل موظفين بحقل تفعيل خاطئ، فراقب مخرجها بصيغة JSON، والدخول الموحد مع authentik يكون عبر LDAP للمزامنة وSAML للدخول.
  • الاستيراد من CSV بأسماء غير موحدة أنشأ موديلاً مكرراً وتجاهل عموداً ونقل موديلاً كاملاً إلى فئة دون إقرار استلام، دون أي أثر في سجل النشاط.
  • النسخة الاحتياطية المدمجة تعمل بـ DB_CONNECTION=mysql، ولكنها لا تحمل مفتاح التطبيق، والاستعادة بمفتاح جديد تفقد الحقول المشفرة وكلمة ربط LDAP.

وفي مقالات قادمة بإذن الله سوف نكمل أدوات هذه المرحلة، مثل Fleet مع osquery للجرد الآلي الذي يمكن ربطه بـ Snipe-IT، وقد قارنا Snipe-IT ومنصات الخدمة الأخرى بالحلول التجارية في:

دليلServiceNow وبدائله مفتوحة المصدر: ماذا تأخذ من كل منها؟ما الذي تشتريه المؤسسة مع ServiceNow، وأي أداة مفتوحة المصدر تغطي كل جزء منه، وكم يكلف ربط هذه الأدوات معاً.دليلدورة حياة أجهزة المؤسسة IT Asset Managementكيف تتابع كل جهاز في مؤسستك من الطلب والشراء إلى التسليم والجرد والإرجاع، ثم المسح والإتلاف بسجل كامل.دليلMDM: إدارة أجهزة الموظفين وتأمينها بأدوات مفتوحةكيف تدير الشركة حواسيب موظفيها وهواتفهم وتفرض عليها التشفير والتحديث، وما الذي تستطيع أن تراه فعلاً على كل جهاز.

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

  • أكتوبر 2026: كتابة الدليل مع تجربة حقيقية على Snipe-IT v8.8.0 (snipe/snipe-it:v8.8.0، PHP 8.3.6 وApache 2.4.58) مع MariaDB 11.4.7، وCaddy 2.11.7 كـ Reverse Proxy، وMailpit 1.31.5، وOpenLDAP 2.4.57 من osixia/openldap:1.5.0، وPython 3.13.16 للسكربت وPlaywright 1.64.0 للصور، على Docker Engine 29.8.0، ومعلومات التوثيق وملاحظات الإصدار والتنبيهات الأمنية وصفحة authentik كما هي في 15 أكتوبر 2026.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.