Skip to content

Repository files navigation

openinc-cve-scanner

Monitors a list of container images via Docker Scout and sends a Telegram alert when new CVEs appear.

The scanner queries image manifests directly from the registry — it does not need access to a Docker daemon and does not mount the host docker socket.

How it works

On each scan cycle, for every monitored image:

  1. docker scout cves <image> --format sarif is executed against the registry reference.
  2. The returned CVE IDs are diffed against the last known baseline persisted in state.json.
  3. If new CVEs are detected, a single Telegram message is sent summarizing them (sorted by CVSS, capped at OI_MAX_CVES_PER_MESSAGE).
  4. If Parse history is configured and the scan result changed in any way (CVEs added, resolved, or re-scored), a new object is appended to the OD3_Infrastructure_CVE Parse class with the full scan result. Existing objects are never updated, so the class is an append-only history.
  5. The current CVE list becomes the new baseline.

The first time an image is seen, every CVE counts as "new" and is reported as an initial scan.

Quick start

  1. Create a .env next to docker-compose.yml:

    OI_MONITOR_BOT_TOKEN=123456:ABC...
    OI_MONITOR_CHAT_ID=-1001234567890
    
    # Optional: registry credentials for private repos.
    OI_REGISTRY=
    OI_REGISTRY_USER=your-dockerhub-user
    OI_REGISTRY_PASSWORD=dckr_pat_...
  2. Adjust OI_MONITORED_IMAGES in docker-compose.yml to the images you want to track.

  3. Start it:

    docker compose up -d
    docker compose logs -f cve-scanner

State persists in ./data/state.json (mounted as /data inside the container).

Configuration

All configuration is via environment variables.

Variable Required Default Description
OI_MONITOR_BOT_TOKEN yes Telegram bot token.
OI_MONITOR_CHAT_ID yes Telegram chat ID to send alerts to.
OI_MONITORED_IMAGES yes Comma- or whitespace-separated list of image refs (e.g. repo/img:tag).
OI_SCAN_INTERVAL no unset Loop interval in seconds. If unset, the scanner runs once and exits — useful for cron / systemd timers.
OI_MONITOR_INTERVAL_SECONDS no unset Per-image rate limit. If an image was scanned more recently than this, it's skipped on the current cycle. Unset = scan every loop.
OI_MAX_CVES_PER_MESSAGE no 30 Cap on CVEs listed in a single Telegram message. Excess are summarized as "…and N more".
OI_REGISTRY no "" (Docker Hub) Registry to authenticate against. Set to e.g. ghcr.io or registry.example.com:5000 for non-Hub registries.
OI_REGISTRY_USER no Registry username. Login is skipped entirely if user or password is missing (only public images can be scanned in that case).
OI_REGISTRY_PASSWORD no Registry password / personal access token.
OI_STATE_FILE no /data/state.json Path where CVE baselines are persisted.
DOCKER_SCOUT_HUB_USER no Optional: authenticate scout itself to Docker Hub for advisory lookups. Recommended for production to avoid anonymous rate limits.
DOCKER_SCOUT_HUB_PASSWORD no Companion to DOCKER_SCOUT_HUB_USER.
OI_PARSE_SERVER_URL no Parse Server base URL (e.g. https://parse.example.com/parse). Parse history is disabled unless URL, app ID and master key are all set.
OI_PARSE_APP_ID no Parse application ID.
OI_PARSE_MASTER_KEY no Parse master key, used to create history objects.
OI_PARSE_CLASS no OD3_Infrastructure_CVE Parse class the history entries are written to.

Deployment notes

  • No docker socket required. Scout reads SBOM/metadata directly from the registry, so this container can run anywhere with HTTPS egress to the registry and api.telegram.org.
  • Private images. Without OI_REGISTRY_USER / OI_REGISTRY_PASSWORD, the scanner only authenticates anonymously and will fail to scan private repositories. The login step writes credentials only to the container's ~/.docker/config.json — the host's docker config is not used.
  • State. The data/ directory in the working tree is bind-mounted at /data. It contains state.json, the CVE baseline per image. Deleting it triggers a fresh "initial scan" alert for every image on the next run.
  • Cron mode. Unset OI_SCAN_INTERVAL and run the container from a host-level scheduler (cron, systemd timer, k8s CronJob, etc.) for a single-shot scan-and-exit.

Parse history

When OI_PARSE_SERVER_URL, OI_PARSE_APP_ID and OI_PARSE_MASTER_KEY are set, every scan whose result differs from the previous one appends an object to the OD3_Infrastructure_CVE class (configurable via OI_PARSE_CLASS). Unchanged scans create nothing, and existing objects are never modified, so the class holds the full change history per image.

Each object contains:

Field Type Description
image String The scanned image reference.
scannedAt Date When the scan ran.
initial Boolean true for the first scan of an image (no baseline yet).
cveCount Number Total CVEs found in this scan.
counts Object Totals per severity bucket: critical / high / medium / low / unknown.
cves Array Full list of { id, score, level, title }, sorted by CVE ID.
newCves Array CVE IDs that were not present in the previous scan.
removedCves Array CVE IDs from the previous scan that are now resolved.
fingerprint String SHA-256 over the canonical CVE list; entries are only created when it changes.

Change detection covers more than new CVEs: a resolved CVE or a changed CVSS score/severity also produces a new history entry. The fingerprint is persisted in state.json, so restarts don't create duplicate entries.

Telegram alerts

The bot must already be a member of the target chat. Get the chat ID by sending a message in the chat, then querying https://api.telegram.org/bot<TOKEN>/getUpdates and reading result[].message.chat.id. Group/channel IDs are negative.

Messages use Markdown formatting and disable link previews. CVE entries are bucketed by CVSS:

  • 🔴 critical (≥ 9.0)
  • 🟠 high (≥ 7.0)
  • 🟡 medium (≥ 4.0)
  • 🟢 low (> 0)
  • ⚪ unknown / no score

Local development

node --check src/scanner.js   # syntax check
npm install                   # no runtime deps; just sets up package-lock if added
node src/scanner.js           # one-shot scan against env vars

The scanner shells out to docker scout, so a local install of the Docker CLI plus the scout plugin is required for non-containerized runs.

CI / CD

.github/workflows/ci.yml runs syntax checks and npm audit on every push and PR. On pushes to master, it builds the image, scans it with Trivy, and pushes openinc/cve-scanner:<date> and :latest to Docker Hub if the scan passes. CVE suppressions for vendored Go libraries inside the docker scout binary are documented in .trivyignore.

About

Scans for CVE in listed containers and sends alerts via telegram

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages