Security

Run a Dependency-Vulnerability Scanner as a Scheduled Job on a Droplet

July 26, 202615 min read

Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.

A one-off dependency scan tells you about the CVEs known today. But new vulnerabilities get disclosed against libraries you already shipped — code that was clean last week can be vulnerable this morning without you changing a line. This guide turns an on-demand scanner into a scheduled service on a DigitalOcean Droplet: it re-scans your projects on a timer, stores results, and alerts you only when something new appears.

Prerequisites:

  • Basic Linux comfort (SSH, sudo, systemd)
  • One or more Git repositories (or container images) to scan
  • An email relay or a Slack/Discord webhook for alerts

1. From On-Demand Tool to Scheduled Scanner

A manual scanner answers "is this vulnerable right now?" A scheduled scanner answers the more useful question: "has anything I depend on become vulnerable since I last looked?" The design:

code
systemd timer (daily)
   │
   ▼
Scan runner
   ├─ pull/refresh each tracked repo
   ├─ run Trivy/Grype against its lockfiles + images
   ├─ diff findings against last run
   ▼
Store results (JSON, dated)
   │
   └─ New CVE? → alert via email / webhook

The key is the diff: you don't want a daily email listing the same 40 known issues — you want a ping the moment a new one appears.


2. Provision the Droplet

Scanning is light and bursty — a small Droplet is plenty. Create one at DigitalOcean. Get $200 in free credits for 60 days when you sign up.

Workload RAM vCPUs Droplet
A handful of repos 1 GB 1 Basic $6/mo
Many repos + image scanning 2 GB 1 Basic $12/mo

Choose Ubuntu 24.04 LTS x64 and add your SSH key. Scanning container images pulls layers, so give it the 2 GB tier if you'll scan images heavily.


3. Base Setup and a Scanner

Create a dedicated user and install Trivy (a fast, well-maintained scanner for filesystems, lockfiles, and images):

bash
ssh root@YOUR_SERVER_IP
adduser scanner && usermod -aG sudo scanner
su - scanner

# Install Trivy
sudo apt-get install -y wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
  sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | \
  sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy jq git

Test a scan against a project directory:

bash
git clone https://github.com/your-org/your-repo.git ~/repos/your-repo
trivy fs --scanners vuln --format json --output /tmp/scan.json ~/repos/your-repo
jq '.Results[].Vulnerabilities | length' /tmp/scan.json

4. The Scan Runner Script

Create ~/scan/repos.txt — one repo per line:

code
https://github.com/your-org/api.git
https://github.com/your-org/web.git
https://github.com/your-org/worker.git

Create ~/scan/run-scans.sh:

bash
#!/bin/bash
set -euo pipefail

BASE=/home/scanner/scan
REPO_DIR=/home/scanner/repos
RESULT_DIR=$BASE/results
STATE_DIR=$BASE/state
STAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p "$REPO_DIR" "$RESULT_DIR" "$STATE_DIR"

NEW_FINDINGS=""

while IFS= read -r repo || [ -n "$repo" ]; do
  [ -z "$repo" ] && continue
  name=$(basename "$repo" .git)
  target="$REPO_DIR/$name"

  # Clone or refresh
  if [ -d "$target/.git" ]; then
    git -C "$target" fetch --quiet origin && git -C "$target" reset --hard --quiet origin/HEAD
  else
    git clone --quiet --depth 1 "$repo" "$target"
  fi

  # Scan -> JSON
  out="$RESULT_DIR/${name}_${STAMP}.json"
  trivy fs --scanners vuln --severity HIGH,CRITICAL --format json --quiet \
    --output "$out" "$target" || true

  # Extract the set of vulnerability IDs found now
  current=$(jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | sort | unique | .[]' "$out" 2>/dev/null || true)

  # Compare against the last known set for this repo
  state="$STATE_DIR/${name}.txt"
  touch "$state"
  new=$(comm -13 <(sort "$state") <(echo "$current" | sort) || true)

  if [ -n "$new" ]; then
    NEW_FINDINGS+=$'\n'"### $name"$'\n'"$new"$'\n'
  fi

  # Persist current state
  echo "$current" > "$state"
done < "$BASE/repos.txt"

# Alert only if something new appeared
if [ -n "$NEW_FINDINGS" ]; then
  "$BASE/alert.sh" "$NEW_FINDINGS"
fi

# Retain 30 days of raw results
find "$RESULT_DIR" -name '*.json' -mtime +30 -delete
bash
chmod +x ~/scan/run-scans.sh

The comm -13 line is the whole trick: it emits only CVE IDs present in this run that weren't in the last, so you're alerted on change, not on the standing backlog.


5. The Alert Script

Create ~/scan/alert.sh for a Slack/Discord webhook (swap the URL for email via mail if you prefer):

bash
#!/bin/bash
FINDINGS="$1"
WEBHOOK="${SCAN_WEBHOOK_URL:?set SCAN_WEBHOOK_URL}"

payload=$(jq -n --arg text "🚨 New HIGH/CRITICAL vulnerabilities detected:${FINDINGS}" '{text: $text}')
curl -s -X POST "$WEBHOOK" -H 'Content-type: application/json' -d "$payload" > /dev/null
bash
chmod +x ~/scan/alert.sh

For email instead, install a lightweight relay and swap the body of alert.sh:

bash
sudo apt install -y msmtp msmtp-mta
# configure ~/.msmtprc with your SMTP provider, then:
# printf "Subject: New CVEs\n\n%s" "$FINDINGS" | msmtp you@example.com

6. Schedule with a systemd Timer

systemd timers are more robust than cron — they log to journald, survive reboots, and can catch up on missed runs. Create /etc/systemd/system/depscan.service:

ini
[Unit]
Description=Dependency vulnerability scan
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=scanner
Environment=SCAN_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
ExecStart=/home/scanner/scan/run-scans.sh

Create /etc/systemd/system/depscan.timer:

ini
[Unit]
Description=Run dependency scan daily

[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

Enable it:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now depscan.timer

# Verify the schedule and run once now to seed baseline state
systemctl list-timers depscan.timer
sudo systemctl start depscan.service
sudo journalctl -u depscan.service -f

First run seeds the baseline — it records the current CVE set per repo without alerting on the entire backlog. From the second run on, you're only pinged about genuinely new disclosures.


7. Scanning Container Images Too

If you ship Docker images, scan those as well — a base image can develop CVEs independent of your code. Add to the runner loop:

bash
# Scan a published image by ref
trivy image --severity HIGH,CRITICAL --format json --quiet \
  --output "$RESULT_DIR/api-image_${STAMP}.json" \
  registry.example.com/your-org/api:latest || true

Trivy caches its vulnerability DB locally and refreshes it automatically, so scheduled runs always check against the latest advisories.


8. Secure the Box

This server holds source code and can reach your registry — lock it down:

bash
# Firewall: SSH only, no inbound services needed
sudo ufw allow OpenSSH
sudo ufw enable

# Harden SSH
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

# fail2ban
sudo apt install -y fail2ban && sudo systemctl enable --now fail2ban

Use read-only deploy keys for the repos and a read-only registry token — the scanner never needs write access to anything. Store secrets in the systemd unit's Environment= (or better, an EnvironmentFile= with chmod 600), never in the scripts.


9. Wiring It Into ToolsHubKit

If you use ToolsHubKit's dependency-scanner tool for ad-hoc checks, this scheduled job is its always-on counterpart: the tool for "check this project now," the Droplet job for "watch these projects forever." Point the alert webhook at the same channel your team already reads, and newly disclosed CVEs land in front of you without anyone remembering to run a scan.


Related Guides


Resources

Resource Link
DigitalOcean Droplets (+ $200 free credit) Sign up here
Trivy https://github.com/aquasecurity/trivy
Grype (alternative scanner) https://github.com/anchore/grype
systemd timers https://www.freedesktop.org/software/systemd/man/systemd.timer.html