Files
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

614 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
#########################################################################################
#### sbom-scanner.sh — Generate SBOMs and scan for vulnerabilities with Syft + Grype ####
#### Wrapper for container images, directories, and archives with CI-friendly output ####
#### Requires: bash 4+, optionally jq ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### ./sbom-scanner.sh --scan nginx:latest ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
TARGET=""
FORMAT="${SBOM_FORMAT:-table}"
OUTPUT_FILE="${SBOM_OUTPUT_FILE:-}"
MIN_SEVERITY="${SBOM_MIN_SEVERITY:-}"
FAIL_ON="${SBOM_FAIL_ON:-}"
IGNORE_FILE="${SBOM_IGNORE_FILE:-}"
SYFT_BIN="${SYFT_PATH:-syft}"
GRYPE_BIN="${GRYPE_PATH:-grype}"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
DB_UPDATE="${DB_UPDATE:-false}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
CRIT_COUNT=0
HIGH_COUNT=0
MED_COUNT=0
LOW_COUNT=0
NEG_COUNT=0
TOTAL_VULNS=0
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
# shellcheck disable=SC2034
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
# shellcheck disable=SC2034
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
# shellcheck disable=SC2034
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
die() {
err "$*"
exit 1
}
elapsed() {
local end_time
end_time=$(date +%s)
echo "$(( end_time - START_TIME ))s"
}
# ── Dependency checks ────────────────────────────────────────────────
check_tool() {
local tool="$1"
local bin_var="$2"
if ! command -v "$bin_var" &>/dev/null; then
err "${tool} is not installed (looked for '${bin_var}')"
err "Install with: ./$(basename "$0") --install"
exit 1
fi
verbose "${tool} found at $(command -v "$bin_var")"
}
# ── Detect target type ───────────────────────────────────────────────
detect_target_type() {
local t="$1"
if [[ "$t" == dir:* ]]; then
echo "directory"
elif [[ -f "$t" ]]; then
echo "archive"
else
echo "container image"
fi
}
# ── Severity level to number ─────────────────────────────────────────
severity_to_num() {
case "${1,,}" in
critical) echo 5 ;;
high) echo 4 ;;
medium) echo 3 ;;
low) echo 2 ;;
negligible) echo 1 ;;
*) echo 0 ;;
esac
}
severity_color() {
case "${1,,}" in
critical) echo "$RED" ;;
high) echo "$RED" ;;
medium) echo "$YELLOW" ;;
low) echo "$DIM" ;;
negligible) echo "$DIM" ;;
*) echo "$RESET" ;;
esac
}
# ══════════════════════════════════════════════════════════════════════
# INSTALL MODE
# ══════════════════════════════════════════════════════════════════════
do_install() {
log "Installing Syft and Grype to ${INSTALL_DIR}..."
if ! command -v curl &>/dev/null; then
die "curl is required for installation"
fi
# Install Syft
if command -v "$SYFT_BIN" &>/dev/null; then
log "Syft already installed: $(command -v "$SYFT_BIN")"
else
log "Installing Syft..."
verbose "curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b ${INSTALL_DIR}"
if curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b "$INSTALL_DIR" 2>/dev/null; then
echo -e " ${GREEN}${RESET} Syft installed to ${INSTALL_DIR}/syft"
else
err "Failed to install Syft"
fi
fi
# Install Grype
if command -v "$GRYPE_BIN" &>/dev/null; then
log "Grype already installed: $(command -v "$GRYPE_BIN")"
else
log "Installing Grype..."
verbose "curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b ${INSTALL_DIR}"
if curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b "$INSTALL_DIR" 2>/dev/null; then
echo -e " ${GREEN}${RESET} Grype installed to ${INSTALL_DIR}/grype"
else
err "Failed to install Grype"
fi
fi
log "Installation complete"
}
# ══════════════════════════════════════════════════════════════════════
# SBOM MODE
# ══════════════════════════════════════════════════════════════════════
do_sbom() {
check_tool "Syft" "$SYFT_BIN"
local target_type
target_type=$(detect_target_type "$TARGET")
log "Generating SBOM with Syft..."
log "Target: ${TARGET} (${target_type})"
local syft_args=("$TARGET")
case "$FORMAT" in
table) syft_args+=(-o syft-table) ;;
json) syft_args+=(-o syft-json) ;;
cyclonedx-json) syft_args+=(-o cyclonedx-json) ;;
*) syft_args+=(-o syft-table) ;;
esac
local output
if output=$("$SYFT_BIN" "${syft_args[@]}" 2>/dev/null); then
if [[ -n "$OUTPUT_FILE" ]]; then
echo "$output" > "$OUTPUT_FILE"
log "Output written to ${OUTPUT_FILE}"
else
echo ""
echo "$output"
fi
else
die "Syft failed to generate SBOM for ${TARGET}"
fi
# Count packages for summary
if [[ "$FORMAT" == "table" ]]; then
local pkg_count
pkg_count=$(echo "$output" | tail -n +2 | grep -c "" || true)
echo ""
log "Total packages: ${pkg_count}"
fi
log "Generated in $(elapsed)"
}
# ══════════════════════════════════════════════════════════════════════
# SCAN MODE
# ══════════════════════════════════════════════════════════════════════
do_scan() {
check_tool "Grype" "$GRYPE_BIN"
local target_type
target_type=$(detect_target_type "$TARGET")
log "Scanning for vulnerabilities with Grype..."
log "Target: ${TARGET} (${target_type})"
if [[ "$DB_UPDATE" == "true" ]]; then
log "Updating vulnerability database..."
"$GRYPE_BIN" db update 2>/dev/null || warn "Database update failed, using cached DB"
fi
local grype_args=("$TARGET" -o json)
[[ -n "$IGNORE_FILE" ]] && grype_args+=(--config "$IGNORE_FILE")
local raw_json
if ! raw_json=$("$GRYPE_BIN" "${grype_args[@]}" 2>/dev/null); then
die "Grype failed to scan ${TARGET}"
fi
# Parse vulnerability counts
if command -v jq &>/dev/null; then
CRIT_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Critical")] | length')
HIGH_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "High")] | length')
MED_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Medium")] | length')
LOW_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Low")] | length')
NEG_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Negligible")] | length')
TOTAL_VULNS=$(echo "$raw_json" | jq '[.matches[]?] | length')
else
TOTAL_VULNS=$(echo "$raw_json" | grep -c '"vulnerability"' || true)
fi
local min_sev_num=0
[[ -n "$MIN_SEVERITY" ]] && min_sev_num=$(severity_to_num "$MIN_SEVERITY")
case "$FORMAT" in
json)
if [[ -n "$OUTPUT_FILE" ]]; then
echo "$raw_json" > "$OUTPUT_FILE"
log "JSON output written to ${OUTPUT_FILE}"
else
echo "$raw_json"
fi
;;
prometheus)
print_prometheus_metrics
;;
*)
print_scan_table "$raw_json" "$min_sev_num"
;;
esac
print_scan_summary
# Check fail threshold
check_fail_threshold
}
print_scan_table() {
local raw_json="$1"
local min_sev_num="$2"
if ! command -v jq &>/dev/null; then
warn "jq not installed — cannot format table output, showing raw"
echo "$raw_json"
return
fi
echo ""
printf " ${BOLD}%-16s %-10s %-20s %-14s %s${RESET}\n" "VULNERABILITY" "SEVERITY" "PACKAGE" "VERSION" "FIXED-IN"
printf " %s\n" "$(printf '%.0s─' {1..75})"
echo "$raw_json" | jq -c '.matches[]?' | while IFS= read -r match; do
local vuln_id severity pkg_name pkg_ver fixed_in
vuln_id=$(echo "$match" | jq -r '.vulnerability.id')
severity=$(echo "$match" | jq -r '.vulnerability.severity')
pkg_name=$(echo "$match" | jq -r '.artifact.name')
pkg_ver=$(echo "$match" | jq -r '.artifact.version')
fixed_in=$(echo "$match" | jq -r '.vulnerability.fix.versions[0] // "(none)"')
local sev_num
sev_num=$(severity_to_num "$severity")
[[ "$sev_num" -lt "$min_sev_num" ]] && continue
local color
color=$(severity_color "$severity")
printf " %-16s ${color}%-10s${RESET} %-20s %-14s %s\n" \
"$vuln_id" "$severity" "${pkg_name:0:20}" "${pkg_ver:0:14}" "$fixed_in"
done
if [[ -n "$OUTPUT_FILE" ]]; then
echo "$raw_json" > "$OUTPUT_FILE"
log "Full JSON written to ${OUTPUT_FILE}"
fi
}
print_scan_summary() {
echo ""
echo -e " ${BOLD}Summary${RESET}"
echo -e " Total vulnerabilities: ${TOTAL_VULNS}"
[[ "$CRIT_COUNT" -gt 0 ]] && echo -e " ${RED}Critical: ${CRIT_COUNT}${RESET}" || echo " Critical: 0"
[[ "$HIGH_COUNT" -gt 0 ]] && echo -e " ${RED}High: ${HIGH_COUNT}${RESET}" || echo " High: 0"
[[ "$MED_COUNT" -gt 0 ]] && echo -e " ${YELLOW}Medium: ${MED_COUNT}${RESET}" || echo " Medium: 0"
echo " Low: ${LOW_COUNT}"
echo " Negligible: ${NEG_COUNT}"
log "Scanned in $(elapsed)"
}
print_prometheus_metrics() {
local ts
ts=$(date +%s)
cat <<EOF
# HELP sbom_scan_vulnerabilities_total Total vulnerabilities found
# TYPE sbom_scan_vulnerabilities_total gauge
sbom_scan_vulnerabilities_total{target="${TARGET}"} ${TOTAL_VULNS}
# HELP sbom_scan_vulnerabilities_by_severity Vulnerabilities by severity
# TYPE sbom_scan_vulnerabilities_by_severity gauge
sbom_scan_vulnerabilities_by_severity{target="${TARGET}",severity="critical"} ${CRIT_COUNT}
sbom_scan_vulnerabilities_by_severity{target="${TARGET}",severity="high"} ${HIGH_COUNT}
sbom_scan_vulnerabilities_by_severity{target="${TARGET}",severity="medium"} ${MED_COUNT}
sbom_scan_vulnerabilities_by_severity{target="${TARGET}",severity="low"} ${LOW_COUNT}
sbom_scan_vulnerabilities_by_severity{target="${TARGET}",severity="negligible"} ${NEG_COUNT}
# HELP sbom_scan_timestamp_seconds Scan timestamp
# TYPE sbom_scan_timestamp_seconds gauge
sbom_scan_timestamp_seconds{target="${TARGET}"} ${ts}
EOF
}
check_fail_threshold() {
if [[ -z "$FAIL_ON" ]]; then
return
fi
local threshold_num
threshold_num=$(severity_to_num "$FAIL_ON")
local exceeded=0
if [[ "$threshold_num" -le 5 && "$CRIT_COUNT" -gt 0 ]]; then exceeded=1; fi
if [[ "$threshold_num" -le 4 && "$HIGH_COUNT" -gt 0 ]]; then exceeded=1; fi
if [[ "$threshold_num" -le 3 && "$MED_COUNT" -gt 0 ]]; then exceeded=1; fi
if [[ "$threshold_num" -le 2 && "$LOW_COUNT" -gt 0 ]]; then exceeded=1; fi
if [[ "$threshold_num" -le 1 && "$NEG_COUNT" -gt 0 ]]; then exceeded=1; fi
if [[ "$exceeded" -eq 1 ]]; then
echo ""
err "Vulnerabilities at or above '${FAIL_ON}' severity found — failing"
exit 2
fi
}
# ══════════════════════════════════════════════════════════════════════
# FULL MODE
# ══════════════════════════════════════════════════════════════════════
do_full() {
check_tool "Syft" "$SYFT_BIN"
check_tool "Grype" "$GRYPE_BIN"
local target_type
target_type=$(detect_target_type "$TARGET")
log "Running full SBOM + vulnerability scan..."
log "Target: ${TARGET} (${target_type})"
if [[ "$DB_UPDATE" == "true" ]]; then
log "Updating vulnerability database..."
"$GRYPE_BIN" db update 2>/dev/null || warn "Database update failed, using cached DB"
fi
# Generate SBOM
log "Phase 1: Generating SBOM with Syft..."
local sbom_json
if ! sbom_json=$("$SYFT_BIN" "$TARGET" -o syft-json 2>/dev/null); then
die "Syft failed to generate SBOM"
fi
local pkg_count
if command -v jq &>/dev/null; then
pkg_count=$(echo "$sbom_json" | jq '[.artifacts[]?] | length')
else
pkg_count="?"
fi
log "SBOM complete: ${pkg_count} packages found"
# Scan with Grype using the SBOM as input
log "Phase 2: Scanning for vulnerabilities with Grype..."
local grype_args=(--sbom -)
grype_args+=(-o json)
[[ -n "$IGNORE_FILE" ]] && grype_args+=(--config "$IGNORE_FILE")
local raw_json
if ! raw_json=$(echo "$sbom_json" | "$GRYPE_BIN" "${grype_args[@]}" 2>/dev/null); then
die "Grype scan failed"
fi
# Parse counts
if command -v jq &>/dev/null; then
CRIT_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Critical")] | length')
HIGH_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "High")] | length')
MED_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Medium")] | length')
LOW_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Low")] | length')
NEG_COUNT=$(echo "$raw_json" | jq '[.matches[]? | select(.vulnerability.severity == "Negligible")] | length')
TOTAL_VULNS=$(echo "$raw_json" | jq '[.matches[]?] | length')
fi
local min_sev_num=0
[[ -n "$MIN_SEVERITY" ]] && min_sev_num=$(severity_to_num "$MIN_SEVERITY")
case "$FORMAT" in
json)
if [[ -n "$OUTPUT_FILE" ]]; then
echo "$raw_json" > "$OUTPUT_FILE"
log "JSON output written to ${OUTPUT_FILE}"
else
echo "$raw_json"
fi
;;
prometheus)
print_prometheus_metrics
;;
*)
print_scan_table "$raw_json" "$min_sev_num"
;;
esac
print_scan_summary
check_fail_threshold
}
# ══════════════════════════════════════════════════════════════════════
# HELP
# ══════════════════════════════════════════════════════════════════════
show_help() {
cat <<EOF
Usage: $SCRIPT_NAME [MODE] TARGET [OPTIONS]
Generate SBOMs and scan for vulnerabilities using Syft and Grype.
MODES:
--sbom TARGET Generate SBOM for target
--scan TARGET Scan target for vulnerabilities
--full TARGET Generate SBOM and scan in one pass
--install Install Syft and Grype
OPTIONS:
--format FORMAT Output: table (default), json, cyclonedx-json, prometheus
--output-file FILE Write output to file instead of stdout
--min-severity SEV Minimum severity to display (negligible, low, medium, high, critical)
--fail-on SEV Exit with code 2 if vulnerabilities at or above severity found
--ignore-file FILE Path to Grype ignore file for suppressing false positives
--db-update Force update vulnerability databases before scanning
--verbose Debug output
--no-color Disable colored output
--help, -h Show this help
TARGETS:
nginx:latest Container image (from registry or local)
dir:/path/to/project Local directory
/path/to/image.tar Archive file
ENVIRONMENT VARIABLES:
SBOM_FORMAT Output format (default: table)
SBOM_OUTPUT_FILE Write output to this file
SBOM_MIN_SEVERITY Minimum severity to display
SBOM_FAIL_ON Fail threshold severity
SBOM_IGNORE_FILE Path to Grype ignore rules
SYFT_PATH Path to Syft binary (default: syft)
GRYPE_PATH Path to Grype binary (default: grype)
INSTALL_DIR Install directory (default: /usr/local/bin)
DB_UPDATE Force database update (default: false)
VERBOSE Debug output (default: false)
NO_COLOR Disable colored output (default: false)
EXIT CODES:
0 Success
1 Runtime error
2 Vulnerability threshold exceeded (--fail-on)
EXAMPLES:
# Generate SBOM for a container image
./$SCRIPT_NAME --sbom nginx:latest
# Scan for vulnerabilities, show high and critical only
./$SCRIPT_NAME --scan nginx:latest --min-severity high
# CI mode — fail if critical vulns found
./$SCRIPT_NAME --scan nginx:latest --fail-on critical
# Full SBOM + scan with JSON output
./$SCRIPT_NAME --full nginx:latest --format json --output-file report.json
# Install Syft and Grype
./$SCRIPT_NAME --install
EOF
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
while [[ $# -gt 0 ]]; do
case "$1" in
--sbom)
RUN_MODE="sbom"
shift
if [[ $# -gt 0 && ! "$1" =~ ^-- ]]; then
TARGET="$1"; shift
fi
;;
--scan)
RUN_MODE="scan"
shift
if [[ $# -gt 0 && ! "$1" =~ ^-- ]]; then
TARGET="$1"; shift
fi
;;
--full)
RUN_MODE="full"
shift
if [[ $# -gt 0 && ! "$1" =~ ^-- ]]; then
TARGET="$1"; shift
fi
;;
--install)
RUN_MODE="install"
shift
;;
--format)
FORMAT="$2"; shift 2 ;;
--output-file)
OUTPUT_FILE="$2"; shift 2 ;;
--min-severity)
MIN_SEVERITY="$2"; shift 2 ;;
--fail-on)
FAIL_ON="$2"; shift 2 ;;
--ignore-file)
IGNORE_FILE="$2"; shift 2 ;;
--db-update)
DB_UPDATE="true"; shift ;;
--verbose)
VERBOSE="true"; shift ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
show_help; exit 0 ;;
*)
if [[ -z "$TARGET" && ! "$1" =~ ^-- ]]; then
TARGET="$1"; shift
else
die "Unknown option: $1 (see --help)"
fi
;;
esac
done
# Handle NO_COLOR env var
if [[ "${NO_COLOR:-}" == "true" ]]; then
COLOR="never"
fi
setup_colors
if [[ -z "$RUN_MODE" ]]; then err "No mode specified"; echo ""; show_help; exit 1; fi
if [[ "$RUN_MODE" != "install" && -z "$TARGET" ]]; then
die "No target specified. Provide a container image, dir:path, or archive path"
fi
START_TIME=$(date +%s)
echo ""
echo -e "${BOLD}SBOM Scanner${RESET}"
if [[ -n "$TARGET" ]]; then
echo "Target: ${TARGET}"
fi
echo "Mode: ${RUN_MODE}"
echo "Format: ${FORMAT}"
echo "Time: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo ""
case "$RUN_MODE" in
sbom) do_sbom ;;
scan) do_scan ;;
full) do_full ;;
install) do_install ;;
esac
}
main "$@"