Initial commit

This commit is contained in:
David Duran Rosich committed 2026-10-23 22:05:26 +02:00
commit 70dbff10b9
14 files changed
+1907

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
# docker-vuln-scan
Scan Docker images for vulnerabilities with [Trivy](https://trivy.dev) and get
an [Apprise](https://github.com/caronc/apprise) notification whenever a
**HIGH** or **CRITICAL** finding is detected.
By default it scans **the images used by running containers**, so a scheduled
run covers whatever is actually deployed. Specific images can still be passed
explicitly.
Everything runs from one container image (Trivy + Apprise + Python), so the
host only needs Docker. JSON reports are kept on disk for auditing.
## How it works
1. `scan.py` determines which images to scan (see
[Which images are scanned](#which-images-are-scanned)) and runs
`trivy image` for each of them, writing a JSON report to
`/reports/<image>.json`.
2. It counts findings per severity and checks them against two thresholds:
- `NOTIFY_MIN_SEVERITY` (default `HIGH`) – when to send an Apprise alert.
- `FAIL_ON` (default `HIGH`) – when the script should exit with code `1`
(useful for CI).
3. On a match it sends one notification per image listing the most relevant
CVEs, the affected package, the installed version, and the fixed version.
## Quick start
```bash
cd docker-vuln-scan
cp .env.example .env # set APPRISE_URLS; IMAGES is optional
docker compose build
docker compose run --rm vulnscan
```
Or without Compose:
```bash
docker build -t docker-vuln-scan:latest .
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v "$PWD/reports:/reports" \
-v trivy-cache:/var/lib/trivy \
-e APPRISE_URLS='discord://webhook_id/webhook_token' \
docker-vuln-scan:latest
```
Reports land in `./reports/`.
## Which images are scanned
The list is built from these sources, in order:
1. image arguments: `scan.py nginx:1.27-alpine redis:7-alpine`
2. the `IMAGES` environment variable (space or newline separated)
3. the `IMAGES_FILE` environment variable (one image per line, `#` comments)
4. **the images used by running containers** (the default when nothing is
configured)
For the last one the scanner asks the Docker Engine API on the Unix socket for
`/containers/json` and scans each `Image` reference, de-duplicated and in
listing order. Stopped containers are not included. The scanner's own container
image is skipped so a scheduled run does not report on itself; set
`INCLUDE_SELF=true` to include it.
The socket is looked up in this order:
- `DOCKER_SOCK` (explicit path)
- `DOCKER_HOST` when it is a `unix://` URL
- `/var/run/docker.sock` and `/run/docker.sock`
- `$XDG_RUNTIME_DIR/docker.sock`
- `/run/user/<uid>/docker.sock` and `/run/user/<uid>/podman/podman.sock`
(rootless Docker and Podman), plus any `/run/user/*/docker.sock` if
`/run/user` is mounted into the container
- `~/.docker/run/docker.sock` (Docker Desktop)
If no socket is reachable, the `docker` CLI is used as a fallback. When neither
works and no explicit images were given, the run logs a warning and exits `0`
with nothing to scan.
### Rootless Docker
Rootless Docker keeps its socket under the user runtime directory, so
`/var/run/docker.sock` does not exist on the host. The Compose file mounts
`/run/user` (read-only), so the scanner finds the socket automatically. If your
socket lives elsewhere, set `DOCKER_SOCK` to its path; Compose mounts it at the
same path inside the container:
```bash
# Find the socket path (strip the leading unix://)
docker context inspect --format '{{.Endpoints.docker.Host}}'
# unix:///run/user/1000/docker.sock
export DOCKER_SOCK=/run/user/1000/docker.sock
docker compose run --rm vulnscan
```
To make it permanent, add the same line to `.env`:
```dotenv
DOCKER_SOCK=/run/user/1000/docker.sock
```
Without Compose:
```bash
docker run --rm \
-v /run/user:/run/user:ro \
-v "$PWD/reports:/reports" \
docker-vuln-scan:latest
```
Scanning a registry image works without the Docker socket; the socket is
needed for auto-discovery and for images that exist **only** in the local
daemon. When no socket is reachable and `IMAGES` is empty, the scanner logs a
warning explaining exactly that and exits `0`.
## Komodo
Ready-to-apply Komodo Resource Sync definitions live in [`komodo/`](komodo/README.md):
a Stack that builds and runs the scanner, plus a nightly Procedure schedule.
Findings still go through Apprise; Komodo only triggers the job.
## Configuration
All configuration is done through environment variables.
| Variable | Default | Description |
| --- | --- | --- |
| `IMAGES` | *(images in use)* | Space/newline separated image references. When empty, running containers are scanned. |
| `IMAGES_FILE` | – | File with one image per line (`#` comments allowed). |
| `DOCKER_SOCK` | auto | Unix socket of the Docker daemon (or Podman). Also the Compose mount source. |
| `DOCKER_HOST` | – | Standard Docker variable; `unix://` paths are used for discovery. |
| `INCLUDE_SELF` | `false` | Also scan the scanner's own image when auto-discovering. |
| `APPRISE_URLS` | – | Space separated Apprise URLs (quote the value). |
| `APPRISE_CONFIG` | – | Alternative: path to an Apprise YAML config. |
| `APPRISE_TITLE_PREFIX` | `Vulnerability scan` | Prefix of notification titles. |
| `NOTIFY_MIN_SEVERITY` | `HIGH` | `NONE`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`. `NONE` disables alerts. |
| `FAIL_ON` | `HIGH` | Exit `1` when a finding at/above this severity exists. `NONE` never fails. |
| `IGNORE_UNFIXED` | `false` | Skip vulnerabilities without a fixed version. |
| `TRIVY_SEVERITIES` | *(all)* | Restrict severities Trivy reports, e.g. `HIGH,CRITICAL`. |
| `NOTIFY_ON_SCAN_ERROR` | `true` | Also alert when Trivy itself fails (e.g. image pull error). |
| `REPORT_DIR` | `/reports` | Where JSON reports are written. |
| `TRIVY_CACHE_DIR` | `/var/lib/trivy` | Trivy DB cache (mount a volume to avoid re-downloading). |
| `TRIVY_TIMEOUT` | `15m` | Passed to `trivy --timeout`. |
| `MAX_FINDINGS_IN_BODY` | `15` | How many findings are listed per notification. |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARN` or `ERROR`. |
Images can be passed as arguments, via `IMAGES`, or via `IMAGES_FILE`; the
sources are combined and de-duplicated. If all of them are empty, the images in
use are scanned.
### Apprise URLs
`APPRISE_URLS` accepts any [Apprise URL](https://github.com/caronc/apprise/wiki).
Common examples:
```bash
# Email (use an app password, not your account password)
APPRISE_URLS='mailto://user:app-password@gmail.com'
# Discord webhook
APPRISE_URLS='discord://webhook_id/webhook_token'
# Slack
APPRISE_URLS='slack://token_a/token_b/token_c/#security'
# Telegram
APPRISE_URLS='tgram://bot_token/chat_id'
# Several targets at once (quote the whole value)
APPRISE_URLS='mailto://user:app-password@gmail.com tgram://bot_token/chat_id'
```
For many targets or reusable credentials, mount an Apprise config file and set
`APPRISE_CONFIG`:
```yaml
# apprise.yml
urls:
- mailto://user:app-password@gmail.com
- discord://webhook_id/webhook_token
```
```bash
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v "$PWD/apprise.yml:/config/apprise.yml:ro" \
-e APPRISE_CONFIG=/config/apprise.yml \
docker-vuln-scan:latest nginx:1.27-alpine
```
## Exit codes
| Code | Meaning |
| --- | --- |
| `0` | All images scanned, no finding reached `FAIL_ON` (or nothing to scan). |
| `1` | At least one image reached `FAIL_ON`. |
| `2` | A scan or notification failed (bad image, registry error, Apprise error). |
## Scheduling and CI
Nightly scan of the images in use:
```cron
0 3 * * * cd /opt/docker-vuln-scan && docker compose run --rm vulnscan >> scan.log 2>&1
```
Scan every local image instead, but do not fail the job:
```bash
IMAGES="$(docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>')" \
FAIL_ON=NONE \
APPRISE_URLS='mailto://user:app-password@gmail.com' \
docker compose run --rm vulnscan
```
GitLab CI job:
```yaml
container_scan:
image: docker:27
services: [docker:27-dind]
script:
- docker build -t docker-vuln-scan:latest .
- >
docker run --rm
-e APPRISE_URLS="$APPRISE_URLS"
-e FAIL_ON=HIGH
-v /var/run/docker.sock:/var/run/docker.sock:ro
docker-vuln-scan:latest "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
```
## Development
`scan.py` uses only the Python standard library; tests need neither Docker nor
Trivy nor Apprise (a fake Docker socket and fake binaries are used):
```bash
python3 -m unittest discover -s tests -v
```
`tests/test_scan.py` covers socket discovery (including rootless paths and
self-skip), report parsing, thresholds, notification bodies, and full runs of
`main()` with a fake `trivy` and `apprise`.
## Notes and limitations
- The container mounts the Docker socket read-only, but the process still runs
as root inside the container. That is equivalent to root on the host – run it
only on trusted machines / CI runners.
- Rootless Docker uses a different socket path. The Compose file mounts
`/run/user` so it is found automatically; for sockets elsewhere, set
`DOCKER_SOCK` as shown above, otherwise the scanner sees no containers.
- Private registries: Trivy reads the standard variables (`TRIVY_USERNAME`,
`TRIVY_PASSWORD`, `TRIVY_INSECURE`, `DOCKER_CONFIG`). Pass them with `-e`.
- The first scan downloads the Trivy vulnerability database; keep
`/var/lib/trivy` on a volume and it is reused.
- `aquasec/trivy:latest` is used as the source of the Trivy binary. For
reproducible builds pin it: `docker build --build-arg TRIVY_IMAGE=aquasec/trivy:0.XX.Y .`
- Notifications are informational: a failed Apprise delivery makes the run exit
with `2` so monitoring can catch a broken alerting path.