--- title: Security CVE Watch url: https://blog.guigpap.com/en/workflows/security-cve-watch/ url_md: https://blog.guigpap.com/en/workflows/security-cve-watch.md category: automation date: '2026-05-04' maturite: production techno: - docker - n8n - telegram application: - monitoring - operations --- # Security CVE Watch > Daily CVE monitoring on the VPS Docker images via Trivy, persistent dedup, and Telegram alerts ## 1. What? — Definition and context **Security CVE Watch** is an N8N workflow that scans the VPS Docker images daily for known vulnerabilities (CVE), deduplicates findings, and only fires an alert for new ones or severity escalations. > **Note - CVE and Trivy** > > A **CVE** (Common Vulnerabilities and Exposures) is a standard identifier for a public security flaw, paired with a severity (LOW/MEDIUM/HIGH/CRITICAL) and a CVSS score. **Trivy** is an open-source scanner that queries several databases (NVD, Red Hat, Alpine, Debian) and compares packages installed in an image against the known-vulnerable versions. ### Components | Workflow | ID | Nodes | Role | |----------|------|-------|------| | **CVE Watch (parent)** | `5mHMlcg2HqEa7pWJ` | 18 | Trigger + image loop + aggregation + notification | | **CVE Scanner (SW-1)** | `Tz40sFr3CMrw7X81` | 6 | Runs `trivy image` over SSH on a single image | | **CVE Processor (SW-2)** | `JCyc4Jkev8fQ1mwL` | 9 | Dedup, classification, historical comparison | ### Architecture diagram ```mermaid flowchart TD subgraph Triggers["Triggers"] direction TB Daily["Cron daily 06:00"] Weekly["Cron weekly Monday 09:00"] DIUN["DIUN webhook · new image"] end subgraph Parent["CVE Watch · 18 nodes"] direction TB GetImages["Get Docker Images · docker images"] LoopImages["Loop Over Images"] Aggregate["Aggregate Findings"] Notify["Notification Hub"] end subgraph Scanner["CVE Scanner · 6 nodes"] direction TB SSH["SSH Local VPS"] Trivy["docker exec trivy image <img>"] ParseJSON["Parse JSON output"] end subgraph Processor["CVE Processor · 9 nodes"] direction TB Dedup["Lookup cve_notifications"] Classify["Filter CRITICAL/HIGH"] Persist["Insert/Update DT"] end subgraph DT["Data Tables"] direction TB CVENotif["cve_notifications · Tfng3e4kUL0Ya4Lm"] ScanHist["cve_scan_history · elUlUt9HzqSSCr7L"] end Triggers --> Parent GetImages --> LoopImages --> Scanner Scanner --> Processor Processor <--> CVENotif Processor --> Aggregate --> Notify Parent --> ScanHist ``` ### Coverage About a dozen images of the VPS stacks are scanned: `n8nio/n8n`, `odoo:18`, `caddy:2-alpine`, `postgres:16`, `postgres:17`, `redis:7-alpine`, `qdrant/qdrant`, `crowdsecurity/crowdsec`, `prom/prometheus`, `grafana/grafana`. The list is derived from `docker images --format` at runtime, so any new stack is automatically picked up at the next scan. --- ## 2. Why? — Stakes and motivations ### The problem without active watch CVEs published on Docker images don't trigger anything automatically. A critical vulnerability on `postgres:16` can sit unnoticed for weeks if no one explicitly checks the CVE feed or reads the registry changelog. | Symptom without CVE Watch | Cause | Impact | |---------------------------|-------|--------| | **Vulnerability not detected** | No systematic scan | Extended exploitation window | | **Repeated false positives** | No dedup | Noise, alert desensitisation | | **Slow trigger** | Manual monitoring | No retry mechanism | | **No traceability** | No scan history | Can't prove past state | ### Why Trivy over Snyk or Docker Scout? | Criterion | Trivy | Snyk | Docker Scout | |-----------|-------|------|--------------| | **Open source** | Yes (Apache 2.0) | No | No | | **Self-hosted** | Yes | Partial | No | | **CVE sources** | NVD, Red Hat, Alpine, Debian, Ubuntu | NVD, Snyk DB | NVD, Snyk DB | | **Auth required** | No | Token + quota | Docker Hub login | | **Offline scan** | Yes (local DB) | No | No | | **JSON format** | Stable, documented | Stable | Stable | The Trivy choice aligns with the VPS self-hosted philosophy: no SaaS dependency, no API quota, no leak of the image list to a third party. ### Three complementary triggers | Trigger | Frequency | Use case | |---------|-----------|----------| | **Cron daily 06:00** | Every day before working hours | Catch CVEs published the previous day | | **Cron weekly Monday 09:00** | Once a week | Weekly recap with trends | | **DIUN webhook** | Every image pull | Scan a new version immediately | The pairing with [DIUN](/en/infrastructure/notify-stack/) closes the loop: when DIUN detects a new image is available, the CVE scan fires before the Telegram approval, so an update introducing a security regression can be rejected. --- ## 3. How? — Technical implementation ### A scan's journey **1. Image inventory** — The parent calls `docker images --format '{{.Repository}}:{{.Tag}}'` over local SSH to obtain the exact list of images loaded on the VPS at scan time. **2. Per-image loop** — For each one, the parent calls the CVE Scanner sub-workflow via Execute Workflow. **3. Trivy scan** — The Scanner SSHes into the VPS and runs `docker exec trivy image --format json --severity CRITICAL,HIGH `. The `trivy` container is pre-installed and its CVE DB cache is mounted on a volume to avoid re-downloading 150 MB on every scan. **4. Result parsing** — The Scanner parses the Trivy JSON and returns a normalised array `{cve_id, severity, package, installed, fixed, title, cvss}`. **5. Dedup and classification** — The CVE Processor compares each finding with the `cve_notifications` table. Three cases: | Case | Action | |------|--------| | **Unknown CVE** | Insert with `status=new`, add to the notification list | | **Already-notified CVE** | Skip (unless severity escalation) | | **CVE marked fixed** | Verify the fix is actually applied, otherwise re-open | **6. Aggregation** — The parent groups new findings by image and severity. **7. Notification** — If at least one CRITICAL is present, the parent fires an **immediate alert** to the [Notification Hub](/en/workflows/notification-hub/). Otherwise it waits for the weekly run for a digest. > **Caution - Trivy DB freshness** > > Trivy's CVE database refreshes automatically on every run (`--cache-dir` option). Without a shared cache, every scan would re-download the DB. The VPS `trivy` container mounts a persistent `/root/.cache/trivy` volume to share the cache across all scans. ### Data Tables #### `cve_notifications` (`Tfng3e4kUL0Ya4Lm`) | Column | Type | Description | |--------|------|-------------| | `cve_id` | string | `CVE-YYYY-NNNNN` | | `image` | string | `repo:tag` | | `severity` | string | CRITICAL / HIGH / MEDIUM / LOW | | `package` | string | Vulnerable package | | `installed_version` | string | Current version | | `fixed_version` | string | Version with the fix (or null) | | `first_seen` | datetime | First detection | | `notified_at` | datetime | Last notification time | | `status` | string | `new` / `notified` / `fixed` / `ignored` | #### `cve_scan_history` (`elUlUt9HzqSSCr7L`) | Column | Type | Description | |--------|------|-------------| | `scan_date` | datetime | Run timestamp | | `images_scanned` | number | Number of images analysed | | `critical_count` | number | CRITICAL CVEs detected | | `high_count` | number | HIGH CVEs detected | | `report_sent` | boolean | Notification sent? | ### Notification format **Immediate CRITICAL alert** routed via the Notification Hub: ``` 🚨 CRITICAL CVE DETECTED Image: postgres:16 CVE: CVE-2026-12345 Severity: CRITICAL (CVSS 9.8) Package: libpq Installed: 16.2 → Fixed in: 16.3 Affected stacks: - n8n-stack (n8n-postgres) - odoo-stack (odoo-db) [🔄 Update now] [📖 CVE details] [⏰ Defer] ``` **Weekly digest** sent every Monday at 09:00: ``` 📊 CVE Watch · Week 18 12 scans completed 0 CRITICAL · 3 HIGH · 8 MEDIUM (ignored) FIXES APPLIED: ✅ postgres:16.2 → 16.3 (CVE-2026-12345) ✅ caddy:2.8.4 → 2.8.5 (CVE-2026-12346) PENDING: ⏳ redis:7.2.3 → 7.2.4 (planned Sunday) ``` ### DIUN integration When [DIUN](/en/infrastructure/notify-stack/) detects that a new image version is available, its webhook also triggers CVE Watch on that specific image. The result is folded into the Telegram approval message: | Case | Approval button | |------|-----------------| | **New version, no CVE** | `[Update]` (green) | | **New version + resolved CVE** | `[Update fixes CVE-XXX]` (green) | | **New version + new CVEs** | `[Update despite CVE]` (orange) | The user makes the call with the risk fully visible. --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|------------| | **CVE feed lag** | A CVE published yesterday may not yet be in the Trivy DB | Pair with NVD RSS feed later | | **Application-level deps not scanned** | Trivy scans OS packages, not `package.json`/`requirements.txt` deps inside the image | Cover with a separate GitHub Dependabot workflow | | **No contextual scoring** | A CVSS 9.8 on an unused package is still flagged CRITICAL | Per-package whitelist via an `error_handling_config`-like setup (to design) | | **musl image false positives** | Trivy sometimes classifies Debian CVEs on Alpine (musl) images | Ignore via Trivy `--ignore-policy` | ### Evolution scenarios **If the daily cadence becomes insufficient**: - Drop to hourly scans for CRITICAL images - Subscribe CVE Watch to a near-real-time NVD feed - Pair with a Trivy server webhook (`--server` mode) **If notification noise grows**: - Add a per-CVE accepted whitelist (with justification) - Group CVEs by package rather than by CVE - Only notify if CVSS ≥ 9.0 in strict mode **If a compliance need shows up**: - Keep JSON reports in an audit bucket - Generate a signed monthly PDF - Expose a read-only API for an external dashboard ### Relevant metrics | Metric | Source | Interpretation | |--------|--------|----------------| | `cve_critical_open` | `cve_notifications WHERE status IN ('new','notified') AND severity='CRITICAL'` | Must stay at 0 | | `cve_mttr` | Δ between `first_seen` and `fixed_at` | Mean Time To Resolution | | `scan_failures` | `cve_scan_history WHERE images_scanned=0` | Scanner outage indicator | --- ## Related pages ### Infrastructure - [Security Stack](/en/infrastructure/security-stack/) — Caddy, CrowdSec, Trivy - [Notify Stack](/en/infrastructure/notify-stack/) — DIUN that fires hot scans - [VPS Architecture](/en/infrastructure/architecture-vps/) — Big picture ### Workflows - [Notification Hub](/en/workflows/notification-hub/) — CVE alert routing - [Docker Updates](/en/workflows/docker-updates/) — Approval workflow paired with CVE scans - [Monitoring Digests](/en/workflows/monitoring-digests/) — Weekly digest including CVEs ### Reference - [Glossary](/en/reference/glossary/) — CVE, CVSS, Trivy ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0