Files
linux-scripts/trivy-cve-auditor.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

321 lines
9.5 KiB
Bash

#!/bin/bash
################################################################################
# Script Name: trivy-cve-auditor.sh
# Version: 1.0
# Description: Prometheus exporter that scans all local container images with
# Trivy and outputs vulnerability metrics by severity. Supports
# stdout, node_exporter textfile collector, and HTTP server modes.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
# - trivy installed and in PATH
# - docker or podman (auto-detected)
# - jq for JSON parsing
# - nc (netcat) for HTTP mode
#
# Usage:
# # Output to stdout (default)
# ./trivy-cve-auditor.sh
#
# # Write to node_exporter textfile collector
# ./trivy-cve-auditor.sh --textfile
#
# # HTTP server mode
# ./trivy-cve-auditor.sh --http
#
# Environment Variables:
# TRIVY_SEVERITY Severity levels to scan (default: HIGH,CRITICAL)
# SKIP_IMAGES Regex pattern to exclude images
# TRIVY_TIMEOUT Per-image scan timeout in seconds (default: 300)
# PROM_PORT HTTP listen port for --http mode (default: 9199)
#
################################################################################
set -euo pipefail
# ============================================================================
# CONFIGURATION
# ============================================================================
TRIVY_SEVERITY="${TRIVY_SEVERITY:-HIGH,CRITICAL}"
SKIP_IMAGES="${SKIP_IMAGES:-}"
TRIVY_TIMEOUT="${TRIVY_TIMEOUT:-300}"
PROM_PORT="${PROM_PORT:-9199}"
TEXTFILE_DIR="/var/lib/node_exporter/textfile"
TEXTFILE_PATH="${TEXTFILE_DIR}/trivy_cve.prom"
# Runtime
MODE="stdout"
CONTAINER_CMD=""
FIRST_SCAN=true
# ============================================================================
# HELPERS
# ============================================================================
log() { echo "# $*" >&2; }
show_usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Scan all local container images with Trivy and export Prometheus metrics.
MODES:
(default) Print metrics to stdout
--textfile Write to node_exporter textfile collector
--http Serve metrics on HTTP port (default: $PROM_PORT)
OPTIONS:
-h, --help Show this help message
ENVIRONMENT VARIABLES:
TRIVY_SEVERITY Severity levels (default: HIGH,CRITICAL)
SKIP_IMAGES Regex to exclude images from scanning
TRIVY_TIMEOUT Per-image timeout in seconds (default: 300)
PROM_PORT HTTP port for --http mode (default: 9199)
EXAMPLES:
$(basename "$0") # stdout
$(basename "$0") --textfile # textfile collector
$(basename "$0") --http # HTTP server on :9199
PROM_PORT=9200 $(basename "$0") --http # custom port
SKIP_IMAGES="pause|kindest" $(basename "$0") # skip images
EOF
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--textfile) MODE="textfile"; shift ;;
--http) MODE="http"; shift ;;
-h|--help) show_usage ;;
*) log "Unknown option: $1"; exit 1 ;;
esac
done
}
detect_container_runtime() {
if command -v podman &>/dev/null; then
CONTAINER_CMD="podman"
elif command -v docker &>/dev/null; then
CONTAINER_CMD="docker"
else
log "ERROR: neither docker nor podman found in PATH"
exit 1
fi
log "Detected container runtime: $CONTAINER_CMD"
}
check_dependencies() {
if ! command -v trivy &>/dev/null; then
log "ERROR: trivy not found in PATH"
exit 1
fi
if ! command -v jq &>/dev/null; then
log "ERROR: jq not found in PATH"
exit 1
fi
if [[ "$MODE" == "http" ]] && ! command -v nc &>/dev/null; then
log "ERROR: nc (netcat) required for HTTP mode"
exit 1
fi
}
# Get list of local images as "repository:tag" one per line
get_local_images() {
$CONTAINER_CMD images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | \
grep -v '<none>' | \
sort -u
}
# ============================================================================
# METRICS GENERATION
# ============================================================================
generate_metrics() {
local scan_start
scan_start=$(date +%s)
local images_scanned=0
local images_with_critical=0
local image_list
image_list=$(get_local_images)
if [[ -z "$image_list" ]]; then
log "No local images found"
fi
# HELP/TYPE headers
cat <<'HEADER'
# HELP trivy_image_vulnerabilities Number of vulnerabilities per image per severity
# TYPE trivy_image_vulnerabilities gauge
# HELP trivy_image_last_scan_timestamp Unix timestamp of last scan per image
# TYPE trivy_image_last_scan_timestamp gauge
HEADER
local db_flag=""
while IFS= read -r image; do
[[ -z "$image" ]] && continue
# Skip images matching exclusion pattern
if [[ -n "$SKIP_IMAGES" ]] && echo "$image" | grep -qE "$SKIP_IMAGES"; then
log "Skipping excluded image: $image"
continue
fi
log "Scanning: $image"
# After first scan, skip DB update to save time
if [[ "$FIRST_SCAN" == true ]]; then
db_flag=""
FIRST_SCAN=false
else
db_flag="--skip-db-update"
fi
local json_output
if ! json_output=$(trivy image \
--format json \
--severity "$TRIVY_SEVERITY" \
--timeout "${TRIVY_TIMEOUT}s" \
--quiet \
$db_flag \
"$image" 2>/dev/null); then
log "WARN: failed to scan $image"
continue
fi
# Parse vulnerability counts by severity
local high_count=0
local critical_count=0
if echo "$json_output" | jq -e '.Results' &>/dev/null; then
high_count=$(echo "$json_output" | \
jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "HIGH")] | length')
critical_count=$(echo "$json_output" | \
jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length')
fi
# Sanitize image name for Prometheus label (replace special chars)
local label_image
label_image="${image//\"/\\\"}"
local now
now=$(date +%s)
echo "trivy_image_vulnerabilities{image=\"${label_image}\",severity=\"HIGH\"} ${high_count}"
echo "trivy_image_vulnerabilities{image=\"${label_image}\",severity=\"CRITICAL\"} ${critical_count}"
echo "trivy_image_last_scan_timestamp{image=\"${label_image}\"} ${now}"
images_scanned=$((images_scanned + 1))
if [[ "$critical_count" -gt 0 ]]; then
images_with_critical=$((images_with_critical + 1))
fi
done <<< "$image_list"
echo ""
# Summary metrics
local scan_end
scan_end=$(date +%s)
local duration=$((scan_end - scan_start))
cat <<EOF
# HELP trivy_scan_duration_seconds Total time to scan all images
# TYPE trivy_scan_duration_seconds gauge
trivy_scan_duration_seconds ${duration}
# HELP trivy_images_scanned_total Number of images scanned
# TYPE trivy_images_scanned_total gauge
trivy_images_scanned_total ${images_scanned}
# HELP trivy_images_with_critical_total Images with at least one CRITICAL CVE
# TYPE trivy_images_with_critical_total gauge
trivy_images_with_critical_total ${images_with_critical}
EOF
}
# ============================================================================
# OUTPUT MODES
# ============================================================================
run_stdout() {
generate_metrics
}
run_textfile() {
if [[ ! -d "$TEXTFILE_DIR" ]]; then
log "Creating textfile directory: $TEXTFILE_DIR"
mkdir -p "$TEXTFILE_DIR"
fi
local temp_file
temp_file=$(mktemp "${TEXTFILE_DIR}/.trivy_cve.XXXXXX")
if ! generate_metrics > "$temp_file" 2>/dev/null; then
rm -f "$temp_file"
log "ERROR: failed to generate metrics"
exit 1
fi
chmod 644 "$temp_file"
mv -f "$temp_file" "$TEXTFILE_PATH"
log "Metrics written to $TEXTFILE_PATH"
}
run_http() {
log "Starting Trivy CVE Auditor on port $PROM_PORT..."
if ! command -v nc &>/dev/null; then
log "ERROR: nc (netcat) required for HTTP mode"
exit 1
fi
while true; do
{
read -r request
if [[ "$request" =~ ^GET\ /metrics ]]; then
local body
body=$(generate_metrics 2>/dev/null)
local length=${#body}
echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: ${length}\r\n\r"
echo "$body"
else
local html="<html><head><title>Trivy CVE Auditor</title></head><body><h1>Trivy CVE Auditor</h1><p><a href=\"/metrics\">Metrics</a></p></body></html>"
local length=${#html}
echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: ${length}\r\n\r"
echo "$html"
fi
} | nc -l -p "$PROM_PORT" -q 1 2>/dev/null
done
}
# ============================================================================
# MAIN
# ============================================================================
main() {
parse_args "$@"
check_dependencies
detect_container_runtime
case "$MODE" in
stdout) run_stdout ;;
textfile) run_textfile ;;
http) run_http ;;
esac
}
main "$@"