رفتن به محتوا
LoopX

اسکن آسیب‌پذیری

توی این درس یاد می‌گیری image ات چه آسیب‌پذیری‌های شناخته‌شده‌ای دارد و چطور پیدایشان کنی. CVE چیست و شدت (LOW تا CRITICAL) یعنی چه، با Trivy یک image را اسکن می‌کنی (از داخل کانتینر و بدون دادن docker.sock)، image قدیمی را با جدید مقایسه می‌کنی، با --exit-code در CI جلوی انتشار image ناامن را می‌گیری، با trivy config خود Dockerfile را بررسی می‌کنی و با Docker Scout آشنا می‌شوی. تمرین اصلی: یک image قدیمی را اسکن و به‌روزرسانی می‌کنی.

مسئله: کد دیگران در image توست

Section titled “مسئله: کد دیگران در image توست”

image تو فقط کد خودت نیست: سیستم‌عامل پایه، کتابخانه‌ها، و پکیج‌ها همه کد دیگران‌اند. هر روز در این کدها آسیب‌پذیری تازه کشف می‌شود. اگر image دو سال پیش ساخته شده و دوباره ساخته نشده، احتمالاً حفره‌های شناخته‌شده دارد که مهاجمان خودکار دنبالشان می‌گردند.

اسکن آسیب‌پذیری مثل بازرس ایمنی ساختمان است: ساختمان (image) را با فهرست حفره‌های شناخته‌شده‌ی ساختمان‌های مشابه (پایگاه داده‌ی CVE) تطبیق می‌دهد و می‌گوید «این در قدیمی است، این سیم‌کشی». بازرس نمی‌گوید ساختمان امن است؛ فقط می‌گوید چه چیزهای شناخته‌شدهای هست. حفره‌های ناشناخته را نمی‌بیند.

Trivy بسته‌های نصب‌شده را از روی لایه‌های image استخراج می‌کند (بدون اجرا) و با پایگاه داده‌ی آسیب‌پذیری تطبیق می‌دهد. خروجی: فهرست CVE با نسخه‌ی نصب‌شده و نسخه‌ی رفع‌شده.

CVE (Common Vulnerabilities and Exposures) یک شناسه‌ی عمومی برای هر آسیب‌پذیری است: CVE-سال-شماره، مثل CVE-2022-37434. هر CVE یک شدت دارد (از امتیاز CVSS ساخته می‌شود):

شدت معنی اقدام
CRITICAL اجرای کد از راه دور یا خسارت بزرگ؛ سوءاستفاده ساده فوراً رفع کن؛ در CI شکست بده
HIGH خطر جدی، معمولاً به شرایط خاص وابسته به‌زودی رفع کن
MEDIUM خطر متوسط در برنامه‌ی به‌روزرسانی
LOW خطر کم یا سخت قابل‌سوءاستفاده پیگیری
UNKNOWN هنوز امتیاز ندارد بررسی دستی

دو ستون مهم گزارش: Installed Version (نسخه‌ی موجود در image) و Fixed Version (نسخه‌ای که مشکل را رفع کرده؛ اگر خالی باشد، هنوز رفعی منتشر نشده و فقط باید جلو را بگیری).

مثال ۱: اجرای Trivy با داکر (بدون نصب)

Section titled “مثال ۱: اجرای Trivy با داکر (بدون نصب)”

Trivy یک ابزار متن‌باز است که مثل هر ابزار دیگری می‌توان به‌صورت یک image اجرایش کرد. به‌جای دادن docker.sock (که در درس امنیت دیدی خطرناک است)، image را در یک فایل tar ذخیره می‌کنیم و Trivy فقط همان فایل را می‌بیند:

Terminal window
mkdir -p vs && cd vs
docker save -o p38.tar python:3.8-slim
docker save -o p312.tar python:3.12-slim
ls -l *.tar | awk '{printf "%s %.0f MB\n", $9, $5/1000000}'
خروجی
p312.tar 49 MB
p38.tar 50 MB

تابعی می‌نویسیم که Trivy را با دو volume اجرا کند: پوشه‌ی فعلی (برای tar) و یک volume نام‌دار برای کش پایگاه داده تا هر بار دانلود نشود:

Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
trivy image --quiet --input /w/p312.tar --severity CRITICAL,HIGH --format table 2>&1 | grep -E 'Total|Report Summary|python' | head -4
خروجی
Report Summary
│ usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA │ python-pkg │ 0 │ - │
Total: 45 (HIGH: 45, CRITICAL: 0)

نکته‌ی --db-repository: Trivy پایگاه داده را از چند آدرس می‌گیرد؛ آدرس پیش‌فرض اول یک میرور گوگل است (mirror.gcr.io) که روی شبکه‌ی من 403 می‌داد (در درس «محدودیت Docker Hub» همین الگو را دیدی). با این flag مستقیم از ghcr.io گرفتم و کار کرد. پایگاه داده بار اول چند ده مگابایت دانلود می‌شود و در volume می‌ماند.

⚡ بررسی سریع

در گزارش Trivy اگر ستون Fixed Version مقدار دارد، یعنی چه؟

مثال ۲: image قدیمی در برابر جدید

Section titled “مثال ۲: image قدیمی در برابر جدید”

برای شمردن به‌جای خواندن صدها خط جدول، قالب خروجی را عوض می‌کنیم: فقط شدت هر آسیب‌پذیری را چاپ می‌کنیم و می‌شماریم:

Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
count() { trivy image --quiet --input /w/$1.tar --format template --template '{{ range . }}{{ range .Vulnerabilities }}{{ println .Severity }}{{ end }}{{ end }}' 2>/dev/null | sort | uniq -c | sort -rn | awk '{printf " %s: %s\n", $2, $1}'; }
echo "python:3.8-slim (قدیمی):"; count p38
echo "python:3.12-slim (جدید):"; count p312
خروجی
python:3.8-slim (قدیمی):
MEDIUM: 216
LOW: 155
HIGH: 111
CRITICAL: 9
UNKNOWN: 5
python:3.12-slim (جدید):
MEDIUM: 65
LOW: 61
HIGH: 45
UNKNOWN: 2

تفاوت بزرگ: image قدیمی چندین CRITICAL و صدها HIGH دارد؛ image جدید بدون CRITICAL و با تعداد خیلی کمتر. همان زبان پایتون، ولی base و کتابخانه‌های قدیمی. نتیجه: تازه نگه داشتن base مؤثرترین کار امنیتی ارزان است.

مثال ۳: یک آسیب‌پذیری را بخوان

Section titled “مثال ۳: یک آسیب‌پذیری را بخوان”
Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
trivy image --quiet --input /w/p38.tar --severity CRITICAL --format template \
--template '{{ range . }}{{ range .Vulnerabilities }}{{ .PkgName }} | {{ .VulnerabilityID }} | نصب‌شده {{ .InstalledVersion }} | رفع در {{ if .FixedVersion }}{{ .FixedVersion }}{{ else }}هنوز نیست{{ end }}{{ println }}{{ end }}{{ end }}' 2>/dev/null | head -5
خروجی
libgnutls30 | CVE-2026-33845 | نصب‌شده 3.7.9-2+deb12u3 | رفع در 3.7.9-2+deb12u7
libgnutls30 | CVE-2026-42010 | نصب‌شده 3.7.9-2+deb12u3 | رفع در 3.7.9-2+deb12u7
libsqlite3-0 | CVE-2025-7458 | نصب‌شده 3.40.1-2 | رفع در هنوز نیست
libssl3 | CVE-2026-31789 | نصب‌شده 3.0.14-1~deb12u2 | رفع در 3.0.19-1~deb12u2
openssl | CVE-2026-31789 | نصب‌شده 3.0.14-1~deb12u2 | رفع در 3.0.19-1~deb12u2

هر ردیف: کدام بسته (PkgName)، کدام CVE، نسخه‌ی نصب‌شده، و نسخه‌ای که رفع کرده. برای بسته‌ای که «رفع در» دارد، راه‌حل فقط به‌روزرسانی است. برای «هنوز نیست»، باید ببینی مسیر سوءاستفاده در اپ تو واقعاً وجود دارد یا نه (و گاهی بسته را حذف کنی).

مثال ۴: رفع، به‌روزرسانی بسته‌ها یا base

Section titled “مثال ۴: رفع، به‌روزرسانی بسته‌ها یا base”

مثال با یک image قدیمی Alpine: اسکن، رفع با apk upgrade در Dockerfile، و اسکن دوباره:

vs/fix/Dockerfile
FROM alpine:3.12
RUN apk upgrade --no-cache
Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
docker save -o vs/a312.tar alpine:3.12
docker build -q -t lx-vs-fixed vs/fix >/dev/null
docker save -o vs/fixed.tar lx-vs-fixed
for t in a312 fixed; do
echo "== $t: $(trivy image --quiet --input /w/$t.tar --severity CRITICAL,HIGH --format template --template '{{ range . }}{{ range .Vulnerabilities }}{{ println .VulnerabilityID }}{{ end }}{{ end }}' 2>/dev/null | grep -c CVE) مورد HIGH/CRITICAL"
done
خروجی
== a312: 1 مورد HIGH/CRITICAL
== fixed: 0 مورد HIGH/CRITICAL

بعد از apk upgrade بسته‌ی آسیب‌پذیر به نسخه‌ی رفع‌شده رسید و گزارش پاک شد. دو راه رفع، به ترتیب اولویت: ۱) base جدیدتر (FROM python:3.12-slim)، که همه‌ی بسته‌ها را یکجا به‌روز می‌کند؛ ۲) به‌روزرسانی بسته‌ها داخل Dockerfile (apk upgrade، apt-get upgrade)، که مفید است ولی نتیجه به زمان build وابسته می‌شود و تکرارپذیری را کم می‌کند. و بعد از رفع همیشه دوباره build و اسکن کن.

مثال ۵: شکستن CI با --exit-code

Section titled “مثال ۵: شکستن CI با --exit-code”

اسکنر باید در CI جلوی انتشار image ناامن را بگیرد. با --exit-code 1 اگر آسیب‌پذیری با شدت انتخاب‌شده پیدا شود، کد خروج غیرصفر می‌دهد و pipeline می‌شکند:

Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
for t in p38 p312; do
trivy image --quiet --input /w/$t.tar --severity CRITICAL --exit-code 1 >/dev/null 2>&1
echo "$t: کد خروج $?"
done
خروجی
p38: کد خروج 1
p312: کد خروج 0

image قدیمی با کد 1 رد می‌شود (CI می‌شکند) و image جدید با 0 عبور می‌کند. معمول: برای CRITICAL (و گاهی HIGH) بشکن، و بقیه را فقط گزارش کن. همچنین --ignore-unfixed آسیب‌پذیری‌هایی که هنوز رفع ندارند را نادیده می‌گیرد، چون کاری از دست تو برنمی‌آید:

Terminal window
trivy() { docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
for f in "" "--ignore-unfixed"; do
n=$(trivy image --quiet --input /w/p312.tar $f --severity HIGH,CRITICAL --format template --template '{{ range . }}{{ range .Vulnerabilities }}{{ println .VulnerabilityID }}{{ end }}{{ end }}' 2>/dev/null | grep -c CVE)
echo "p312 ${f:-(همه)}: $n مورد HIGH/CRITICAL"
done
خروجی
p312 (همه): 45 مورد HIGH/CRITICAL
p312 --ignore-unfixed: 1 مورد HIGH/CRITICAL

با --ignore-unfixed عدد از ۴۵ به ۱ می‌رسد: بیشتر مواردِ python:3.12-slim در این روز هنوز رفعی در توزیع ندارند، پس کار عملی کمی از دست تو برمی‌آید و بقیه را فقط پایش می‌کنی. (این عددها با پایگاه داده‌ی امروز عوض می‌شوند.)

trivy config به‌جای image، فایل‌های تنظیم (Dockerfile، Compose، Kubernetes) را برای اشتباهات امنیتی بررسی می‌کند:

vs/cfg/Dockerfile
FROM python:3.12-slim
COPY . .
CMD ["python", "app.py"]
Terminal window
docker run --rm -v "$PWD/vs":/w aquasec/trivy:latest config --quiet /w/cfg 2>&1 | grep -E '^DS-|Failures:'
خروجی
Failures: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile

trivy config پایگاه داده‌ی CVE لازم ندارد (قاعده‌ها داخل خود برنامه‌اند)، پس --db-repository نمی‌خواهد. دو اشتباه همان‌هایی هستند که در درس‌های قبل گفتیم: بدون USER (اجرا با root) و بدون HEALTHCHECK. اسکن config در CI قبل از build، ارزان‌ترین بازرسی است.

Docker Scout ابزار رسمی داکر برای همین کار است و در Docker Desktop و CLI هست. نیاز به ورود (docker login) دارد و اطلاعات image (فهرست بسته‌ها) را برای تحلیل به سرویس Docker می‌فرستد؛ دسترسی به آن از شبکه‌ی تو ممکن است محدود باشد. من بدون ورود نمی‌توانم اجرایش کنم، پس این بخش «نمونه» است:

نمونه (اجرا نشده)
docker login
docker scout quickview myapp:1.0 # خلاصه: تعداد CVE به تفکیک شدت
docker scout cves myapp:1.0 # فهرست کامل
docker scout cves --only-severity critical,high myapp:1.0
docker scout recommendations myapp:1.0 # پیشنهاد base بهتر
نمونه‌ی ظاهر خروجی (اجرا نشده)
Target │ myapp:1.0 │ 1C 6H 9M 14L
digest │ ...

در عمل Trivy (محلی و بدون ارسال داده به بیرون) و Scout (یکپارچه با Docker Hub/Desktop) مکمل‌اند؛ اگر شبکه یا سیاست سازمان اجازه نمی‌دهد، Trivy کافی است.

Trivy image را اجرا نمی‌کند. لایه‌ها را باز می‌کند و فهرست بسته‌های نصب‌شده را از روی پایگاه‌های داده‌ی بسته‌ها (/var/lib/dpkg/status، /lib/apk/db/installed، فایل‌های requirements.txt و package-lock.json و…) می‌خواند، سپس نسخه‌ها را با پایگاه داده‌ی آسیب‌پذیری (ترکیبی از NVD و داده‌ی هر توزیع) تطبیق می‌دهد. پایگاه داده در یک OCI artifact است که از یک registry دانلود می‌شود (برای همین مسئله‌ی دسترسی به registry اینجا هم پیش آمد) و کش می‌شود.

چند نکته‌ی مهم برای تفسیر نتایج:

  • مثبت کاذب: وجود یک بسته‌ی آسیب‌پذیر یعنی خطر بالقوه؛ ممکن است اپ تو آن قسمت از کد را اصلاً استفاده نکند. ولی نباید همه را نادیده گرفت.
  • اسکن یک عکس لحظه‌ای است: image امروز تمیز است ولی هفته‌ی بعد CVE تازه کشف می‌شود. اسکن باید تکرار شود (CI و اسکن دوره‌ای image های منتشرشده).
  • Distroless و scratch (درس کوچک کردن image) بسته‌ی کمتری دارند، پس آسیب‌پذیری کمتری هم گزارش می‌شود.
  • اسکنر فقط آسیب‌پذیری شناخته‌شده را می‌گیرد؛ رمزهای لورفته (trivy image --scanners secret) و پیکربندی بد (trivy config) را جدا می‌گیرد.
می‌خواهم… دستور
اسکن image ذخیره‌شده trivy image --input x.tar
فقط شدت‌های خاص --severity CRITICAL,HIGH
نادیده گرفتن بی‌راه‌حل‌ها --ignore-unfixed
شکستن CI --exit-code 1
خروجی ماشین‌خوان --format json / --format template
اسکن Dockerfile و Compose trivy config DIR
اسکن رازهای داخل image --scanners secret
آدرس پایگاه داده‌ی دیگر --db-repository ghcr.io/aquasecurity/trivy-db:2
تصمیم معیار
CRITICAL با رفع موجود فوراً به‌روز کن
HIGH در بسته‌ی استفاده‌شده در همین هفته
بدون رفع (unfixed) ارزیابی، mitigations، یا base دیگر
فقط در ابزار ساخت multi-stage؛ از image نهایی حذف کن

۱) فکر کردن «اسکن تمیز = امن»

Section titled “۱) فکر کردن «اسکن تمیز = امن»”

اسکنر فقط حفره‌های شناخته‌شده را می‌بیند. راه‌حل: اسکن را با اصول درس امنیت (کاربر غیر root، حداقل دسترسی) ترکیب کن.

آسیب‌پذیری‌های جدید هر روز می‌آیند. راه‌حل: اسکن در CI برای هر build، و اسکن دوره‌ای image های در حال اجرا.

mount کردن /var/run/docker.sock به یک ابزار اسکن مرسوم است ولی (درس امنیت) کنترل کامل daemon را می‌دهد. راه‌حل: docker save و --input (مثال ۱).

۴) خطای دانلود پایگاه داده

Section titled “۴) خطای دانلود پایگاه داده”
Terminal window
docker run --rm aquasec/trivy:latest image --quiet --input /nonexistent.tar --db-repository localhost:5999/no/db:2 2>&1 | grep -oE 'FATAL.*' | cut -c1-200 | head -1
خروجی
FATAL Fatal error run error: init error: DB error: failed to download vulnerability DB: OCI artifact error: failed to download vulnerability DB: failed to download artifact from localhost:5999/no/db:2

وقتی پایگاه داده دانلود نشود (آدرس اشتباه، registry مسدود)، اسکن شروع نمی‌شود. راه‌حل: --db-repository دیگر، proxy برای daemon/کانتینر، یا کش پایگاه داده روی یک ماشین دارای دسترسی و انتقالش (مثل درس انتقال آفلاین).

۵) رد کردن همه‌ی هشدارها

Section titled “۵) رد کردن همه‌ی هشدارها”

گذاشتن همه‌ی CVE ها در لیست نادیده‌گرفته (.trivyignore) بدون بررسی. راه‌حل: فقط مواردی را نادیده بگیر که واقعاً بررسی و دلیلش را نوشته‌ای.

✎ تمرینآسان

بنویس CVE چیست و فرق CRITICAL با LOW در یک جمله؛ سپس با Trivy تعداد آسیب‌پذیری‌های python:3.12-slim را با --severity CRITICAL بشمار.

دیدن جواب

CVE شناسه‌ی عمومی یک آسیب‌پذیری شناخته‌شده است؛ CRITICAL خطر بسیار بالا و ساده برای سوءاستفاده و LOW خطر کم.

Terminal window
docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --input /w/p312.tar --severity CRITICAL --format template --template '{{ range . }}{{ range .Vulnerabilities }}{{ println .VulnerabilityID }}{{ end }}{{ end }}' --db-repository ghcr.io/aquasecurity/trivy-db:2 2>/dev/null | grep -c CVE
خروجی
0
✎ تمرینمتوسط

تمرین اصلی: یک image قدیمی را اسکن و به‌روزرسانی کن. alpine:3.12 را با Trivy اسکن کن و CRITICAL را بیاب، یک Dockerfile بنویس که با apk upgrade آن را رفع کند، دوباره build و اسکن کن و نشان بده تعداد CRITICAL صفر شد.

دیدن جواب
Terminal window
mkdir -p e2 && printf 'FROM alpine:3.12\nRUN apk upgrade --no-cache\n' > e2/Dockerfile
T() { docker run --rm -v "$PWD":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest "$@" --db-repository ghcr.io/aquasecurity/trivy-db:2; }
N() { T image --quiet --input /w/$1 --severity CRITICAL --format template --template '{{ range . }}{{ range .Vulnerabilities }}{{ println .VulnerabilityID }}{{ end }}{{ end }}' 2>/dev/null | grep -c CVE; }
docker save -o old.tar alpine:3.12
echo "قبل: $(N old.tar) مورد CRITICAL"
docker build -q -t lx-vs-e2 e2 >/dev/null && docker save -o new.tar lx-vs-e2
echo "بعد: $(N new.tar) مورد CRITICAL"
docker rmi lx-vs-e2 >/dev/null
خروجی
قبل: 1 مورد CRITICAL
بعد: 0 مورد CRITICAL
✎ تمرینسخت

یک «دروازه‌ی CI» بنویس: اسکریپتی که برای یک فهرست image (tar) اسکن می‌کند و اگر هرکدام CRITICAL داشت، نام آن را چاپ و در پایان با کد 1 تمام می‌شود. روی p38.tar و p312.tar تست کن (باید p38 رد و p312 قبول شود).

دیدن جواب
Terminal window
gate() {
fail=0
for t in "$@"; do
if docker run --rm -v "$PWD/vs":/w -v lx-trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --input /w/$t.tar --severity CRITICAL --exit-code 1 --db-repository ghcr.io/aquasecurity/trivy-db:2 >/dev/null 2>&1; then
echo "قبول: $t"
else
echo "رد: $t (CRITICAL دارد)"; fail=1
fi
done
return $fail
}
gate p38 p312; echo "کد خروج دروازه: $?"
خروجی
رد: p38 (CRITICAL دارد)
قبول: p312
کد خروج دروازه: 1
؟ آزمونک
  1. CVE چیست؟

  2. ستون Fixed Version چه می‌گوید؟

  3. مؤثرترین کار ارزان برای کم کردن CVE ها؟

  4. برای شکستن CI با Trivy کدام flag را می‌گذاری؟

  5. چرا ترجیحاً docker.sock را به اسکنر نمی‌دهیم؟

  6. اسکن تمیز یعنی…

  • CVE شناسه‌ی آسیب‌پذیری شناخته‌شده؛ شدت: CRITICAL، HIGH، MEDIUM، LOW. به Fixed Version نگاه کن.
  • Trivy: docker save ← trivy image --input x.tar؛ بدون docker.sock. پایگاه داده از یک registry می‌آید (--db-repository) و کش می‌شود.
  • مؤثرترین رفع: base جدیدتر/کوچک‌تر و دوباره build؛ بعد دوباره اسکن.
  • در CI: --exit-code 1 --severity CRITICAL؛ --ignore-unfixed برای هشدارهای بی‌راه‌حل.
  • trivy config Dockerfile و Compose را بررسی می‌کند (USER، HEALTHCHECK).
  • Docker Scout مکمل است ولی به ورود و ارسال داده نیاز دارد. اسکن «امن‌بودن» را ثابت نمی‌کند و باید تکرار شود.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker save -o app.tar myapp:1.0ذخیره‌ی image برای اسکن
trivy image --input app.tarاسکن image
trivy image --severity CRITICAL,HIGH IMGفقط شدت‌های بالا
trivy image --ignore-unfixed IMGبدون موارد بی‌رفع
trivy image --exit-code 1 --severity CRITICAL IMGشکستن CI
trivy config DIRاسکن Dockerfile و Compose
--db-repository ghcr.io/aquasecurity/trivy-db:2آدرس پایگاه داده
docker scout cves IMGDocker Scout (نیاز به login) — نمونه