feat(hardening): add container security scanning with Trivy

- Add container-scanning.yml task file for vulnerability scans
- Add systemd timer and service for scheduled scans
- Add container-security-scan.sh script for manual scans
- Integrate Trivy for Docker image vulnerability detection

Implements: Automated container security monitoring
This commit is contained in:
Jeremie Fraeys 2026-03-06 14:27:20 -05:00
parent ac8b0b9abd
commit 0eb8c1b139
No known key found for this signature in database
4 changed files with 214 additions and 0 deletions

View file

@ -0,0 +1,70 @@
---
# Container Security Scanning with Trivy
- name: Install Trivy scanner
apt:
name:
- apt-transport-https
- gnupg
- lsb-release
state: present
update_cache: true
- name: Add Trivy GPG key
apt_key:
url: https://aquasecurity.github.io/trivy-repo/deb/public.key
state: present
- name: Add Trivy repository
apt_repository:
repo: "deb https://aquasecurity.github.io/trivy-repo/deb {{ ansible_distribution_release }} main"
state: present
update_cache: true
- name: Install Trivy
apt:
name: trivy
state: present
update_cache: true
- name: Create Trivy report directory
file:
path: /var/log/trivy-scans
state: directory
owner: root
group: root
mode: "0750"
- name: Install container scan script
copy:
src: container-security-scan.sh
dest: /usr/local/sbin/container-security-scan
owner: root
group: root
mode: "0750"
- name: Install systemd service for container scanning
template:
src: trivy-scan.service.j2
dest: /etc/systemd/system/trivy-scan.service
owner: root
group: root
mode: "0644"
- name: Install systemd timer for weekly scans
template:
src: trivy-scan.timer.j2
dest: /etc/systemd/system/trivy-scan.timer
owner: root
group: root
mode: "0644"
- name: Reload systemd
systemd:
daemon_reload: true
- name: Enable and start weekly scan timer
systemd:
name: trivy-scan.timer
enabled: true
state: started

View file

@ -0,0 +1,10 @@
[Unit]
Description=Container Security Scan with Trivy
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/container-security-scan --severity HIGH,CRITICAL
StandardOutput=journal
StandardError=journal
SyslogIdentifier=trivy-scan

View file

@ -0,0 +1,9 @@
[Unit]
Description=Weekly Container Security Scan with Trivy
[Timer]
OnCalendar=Sun 03:00
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
# Container Image Security Scanner
# Uses Trivy to scan images for CVEs
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT_PASSWORD_FILE="${VAULT_PASSWORD_FILE:-secrets/.vault_pass}"
REPORT_DIR="${REPORT_DIR:-/var/log/trivy-scans}"
SEVERITY="${SEVERITY:-HIGH,CRITICAL}"
usage() {
echo "Usage: $0 [options]"
echo "Options:"
echo " --scan-all Scan all running containers (default)"
echo " --scan-image IMAGE Scan specific image"
echo " --severity LEVEL Severity filter (default: HIGH,CRITICAL)"
echo " --report-only Exit 0 even if CVEs found (for CI reporting)"
echo " --help Show this help"
exit 1
}
SCAN_MODE="all"
TARGET_IMAGE=""
REPORT_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
--scan-all) SCAN_MODE="all"; shift ;;
--scan-image)
SCAN_MODE="single"
TARGET_IMAGE="$2"
shift 2
;;
--severity) SEVERITY="$2"; shift 2 ;;
--report-only) REPORT_ONLY=true; shift ;;
--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
# Check if Trivy is installed
if ! command -v trivy &> /dev/null; then
echo "Installing Trivy..."
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
fi
# Create report directory
mkdir -p "$REPORT_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_FILE="$REPORT_DIR/trivy-scan-$TIMESTAMP.json"
SUMMARY_FILE="$REPORT_DIR/trivy-scan-$TIMESTAMP.summary"
scan_image() {
local image="$1"
echo "Scanning: $image"
# Scan with Trivy
if trivy image \
--severity "$SEVERITY" \
--format json \
--output "$REPORT_FILE.tmp" \
--scanners vuln,config \
"$image" 2>/dev/null; then
# Check for vulnerabilities
if command -v jq &> /dev/null; then
CRITICAL=$(jq '[.Results[]?.Vulnerabilities? // empty | .[] | select(.Severity == "CRITICAL")] | length' "$REPORT_FILE.tmp" 2>/dev/null || echo "0")
HIGH=$(jq '[.Results[]?.Vulnerabilities? // empty | .[] | select(.Severity == "HIGH")] | length' "$REPORT_FILE.tmp" 2>/dev/null || echo "0")
echo " CRITICAL: $CRITICAL, HIGH: $HIGH"
if [[ "$CRITICAL" -gt 0 ]] && [[ "$REPORT_ONLY" == "false" ]]; then
echo "ERROR: CRITICAL vulnerabilities found in $image" >&2
return 1
fi
fi
cat "$REPORT_FILE.tmp" >> "$REPORT_FILE"
rm -f "$REPORT_FILE.tmp"
return 0
else
echo "WARNING: Failed to scan $image" >&2
return 0 # Continue scanning other images
fi
}
# Main scanning logic
echo "Starting Trivy container scan at $(date)"
echo "Severity filter: $SEVERITY"
echo ""
if [[ "$SCAN_MODE" == "single" ]]; then
scan_image "$TARGET_IMAGE"
else
# Get all unique images from running containers
IMAGES=$(docker ps --format '{{.Image}}' | sort -u)
TOTAL=0
FAILED=0
for image in $IMAGES; do
TOTAL=$((TOTAL + 1))
if ! scan_image "$image"; then
FAILED=$((FAILED + 1))
fi
done
echo ""
echo "Scan complete: $TOTAL images scanned, $FAILED failed"
fi
# Generate summary
echo ""
echo "Report saved to: $REPORT_FILE"
echo "To view: trivy convert --format table $REPORT_FILE"
# Cleanup old reports (keep last 30 days)
find "$REPORT_DIR" -name "trivy-scan-*.json" -mtime +30 -delete 2>/dev/null || true
if [[ "$FAILED" -gt 0 ]] && [[ "$REPORT_ONLY" == "false" ]]; then
exit 1
fi
exit 0