a1a17e81a1
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.
369 lines
14 KiB
Bash
369 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### ssl-cert-checker.sh — Check SSL certificate expiry across multiple endpoints ####
|
|
#### Reads hosts from CLI args, a file, or stdin and reports days remaining ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./ssl-cert-checker.sh example.com google.com:443 ####
|
|
#### ./ssl-cert-checker.sh --file hosts.txt ####
|
|
#### echo "example.com" | ./ssl-cert-checker.sh ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
CERT_CHECK_TIMEOUT="${CERT_CHECK_TIMEOUT:-5}"
|
|
WARN_DAYS="${WARN_DAYS:-30}"
|
|
CRIT_DAYS="${CRIT_DAYS:-7}"
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
JSON_OUTPUT="${JSON_OUTPUT:-false}"
|
|
HOST_FILE=""
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
HOSTS=()
|
|
COUNT_OK=0
|
|
COUNT_WARN=0
|
|
COUNT_CRIT=0
|
|
COUNT_EXPIRED=0
|
|
COUNT_ERROR=0
|
|
COUNT_TOTAL=0
|
|
JSON_RESULTS=()
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${CYAN}[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; }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
section_header() {
|
|
echo ""
|
|
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
field() {
|
|
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
|
}
|
|
|
|
field_color() {
|
|
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# CERTIFICATE CHECK
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
check_cert() {
|
|
local host="$1"
|
|
local port="$2"
|
|
|
|
verbose "Connecting to ${host}:${port} (timeout ${CERT_CHECK_TIMEOUT}s)"
|
|
|
|
local cert_output
|
|
if ! cert_output=$(openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null <<< "" | openssl x509 -noout -subject -issuer -dates 2>/dev/null); then
|
|
err "Failed to retrieve certificate from ${host}:${port}"
|
|
COUNT_ERROR=$((COUNT_ERROR + 1))
|
|
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
|
JSON_RESULTS+=("{\"host\":\"${host}\",\"port\":${port},\"status\":\"ERROR\",\"error\":\"connection failed\"}")
|
|
fi
|
|
return
|
|
fi
|
|
|
|
local subject issuer not_after
|
|
subject=$(echo "$cert_output" | grep "^subject=" | sed 's/^subject=//')
|
|
issuer=$(echo "$cert_output" | grep "^issuer=" | sed 's/^issuer=//')
|
|
not_after=$(echo "$cert_output" | grep "^notAfter=" | sed 's/^notAfter=//')
|
|
|
|
if [[ -z "$not_after" ]]; then
|
|
err "Could not parse certificate dates for ${host}:${port}"
|
|
COUNT_ERROR=$((COUNT_ERROR + 1))
|
|
return
|
|
fi
|
|
|
|
local expiry_epoch now_epoch days_remaining
|
|
expiry_epoch=$(date -d "$not_after" +%s 2>/dev/null)
|
|
now_epoch=$(date +%s)
|
|
days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))
|
|
|
|
local status color
|
|
if [[ "$days_remaining" -lt 0 ]]; then
|
|
status="EXPIRED"
|
|
color="$RED"
|
|
COUNT_EXPIRED=$((COUNT_EXPIRED + 1))
|
|
elif [[ "$days_remaining" -le "$CRIT_DAYS" ]]; then
|
|
status="CRITICAL"
|
|
color="$RED"
|
|
COUNT_CRIT=$((COUNT_CRIT + 1))
|
|
elif [[ "$days_remaining" -le "$WARN_DAYS" ]]; then
|
|
status="WARNING"
|
|
color="$YELLOW"
|
|
COUNT_WARN=$((COUNT_WARN + 1))
|
|
else
|
|
status="OK"
|
|
color="$GREEN"
|
|
COUNT_OK=$((COUNT_OK + 1))
|
|
fi
|
|
|
|
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
|
# Escape double quotes in subject/issuer for JSON
|
|
local json_subject json_issuer
|
|
json_subject="${subject//\"/\\\"}"
|
|
json_issuer="${issuer//\"/\\\"}"
|
|
JSON_RESULTS+=("{\"host\":\"${host}\",\"port\":${port},\"subject\":\"${json_subject}\",\"issuer\":\"${json_issuer}\",\"expiry\":\"${not_after}\",\"days_remaining\":${days_remaining},\"status\":\"${status}\"}")
|
|
else
|
|
echo ""
|
|
field "Host:" "${host}:${port}"
|
|
field "Subject:" "$subject"
|
|
field "Issuer:" "$issuer"
|
|
field "Expiry:" "$not_after"
|
|
field_color "Days remaining:" "${color}${days_remaining}${RESET}"
|
|
field_color "Status:" "${color}${status}${RESET}"
|
|
fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# INPUT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_host() {
|
|
local entry="$1"
|
|
local host port
|
|
|
|
# Strip whitespace and skip empty/comment lines
|
|
entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
[[ -z "$entry" || "$entry" == \#* ]] && return
|
|
|
|
if [[ "$entry" == *:* ]]; then
|
|
host="${entry%%:*}"
|
|
port="${entry##*:}"
|
|
else
|
|
host="$entry"
|
|
port="443"
|
|
fi
|
|
|
|
HOSTS+=("${host}:${port}")
|
|
}
|
|
|
|
load_hosts_from_file() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
err "File not found: $file"
|
|
exit 1
|
|
fi
|
|
while IFS= read -r line; do
|
|
parse_host "$line"
|
|
done < "$file"
|
|
}
|
|
|
|
load_hosts_from_stdin() {
|
|
while IFS= read -r line; do
|
|
parse_host "$line"
|
|
done
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USAGE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${SCRIPT_NAME} — Check SSL certificate expiry across multiple endpoints
|
|
|
|
USAGE:
|
|
${SCRIPT_NAME} [OPTIONS] [HOST[:PORT] ...]
|
|
${SCRIPT_NAME} --file hosts.txt
|
|
echo "example.com" | ${SCRIPT_NAME}
|
|
|
|
OPTIONS:
|
|
--file FILE Read hosts from file (one host:port per line)
|
|
--warn-days N Warning threshold in days (default: ${WARN_DAYS})
|
|
--crit-days N Critical threshold in days (default: ${CRIT_DAYS})
|
|
--json Output results as JSON
|
|
--verbose Enable debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
CERT_CHECK_TIMEOUT Connection timeout in seconds (default: 5)
|
|
WARN_DAYS Warning threshold (default: 30)
|
|
CRIT_DAYS Critical threshold (default: 7)
|
|
|
|
EXAMPLES:
|
|
# Check a single host
|
|
./ssl-cert-checker.sh example.com
|
|
|
|
# Check multiple hosts with custom port
|
|
./ssl-cert-checker.sh example.com:443 mail.example.com:993
|
|
|
|
# Check from a file
|
|
./ssl-cert-checker.sh --file hosts.txt
|
|
|
|
# JSON output with custom thresholds
|
|
./ssl-cert-checker.sh --json --warn-days 60 --crit-days 14 example.com
|
|
EOF
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ARGUMENT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--file)
|
|
HOST_FILE="$2"; shift 2 ;;
|
|
--warn-days)
|
|
WARN_DAYS="$2"; shift 2 ;;
|
|
--crit-days)
|
|
CRIT_DAYS="$2"; shift 2 ;;
|
|
--json)
|
|
JSON_OUTPUT="true"; shift ;;
|
|
--verbose)
|
|
VERBOSE="true"; shift ;;
|
|
--no-color)
|
|
COLOR="never"; shift ;;
|
|
--help|-h)
|
|
setup_colors
|
|
usage
|
|
exit 0 ;;
|
|
-*)
|
|
err "Unknown option: $1"
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1 ;;
|
|
*)
|
|
parse_host "$1"; shift ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
# Require openssl
|
|
if ! command -v openssl &>/dev/null; then
|
|
err "openssl is required but not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Load hosts from file if specified
|
|
if [[ -n "$HOST_FILE" ]]; then
|
|
load_hosts_from_file "$HOST_FILE"
|
|
fi
|
|
|
|
# Load from stdin if no hosts yet and stdin is not a terminal
|
|
if [[ ${#HOSTS[@]} -eq 0 ]] && ! [[ -t 0 ]]; then
|
|
load_hosts_from_stdin
|
|
fi
|
|
|
|
if [[ ${#HOSTS[@]} -eq 0 ]]; then
|
|
err "No hosts specified"
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1
|
|
fi
|
|
|
|
COUNT_TOTAL=${#HOSTS[@]}
|
|
|
|
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}SSL Certificate Check${RESET}"
|
|
echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
|
|
echo -e "${DIM}Thresholds: warn=${WARN_DAYS}d, crit=${CRIT_DAYS}d, timeout=${CERT_CHECK_TIMEOUT}s${RESET}"
|
|
section_header "Certificates"
|
|
fi
|
|
|
|
for entry in "${HOSTS[@]}"; do
|
|
local host port
|
|
host="${entry%%:*}"
|
|
port="${entry##*:}"
|
|
check_cert "$host" "$port"
|
|
done
|
|
|
|
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
|
echo "{"
|
|
echo " \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
|
|
echo " \"summary\": {"
|
|
echo " \"total\": ${COUNT_TOTAL},"
|
|
echo " \"ok\": ${COUNT_OK},"
|
|
echo " \"warning\": ${COUNT_WARN},"
|
|
echo " \"critical\": ${COUNT_CRIT},"
|
|
echo " \"expired\": ${COUNT_EXPIRED},"
|
|
echo " \"error\": ${COUNT_ERROR}"
|
|
echo " },"
|
|
echo " \"results\": ["
|
|
local i=0
|
|
for result in "${JSON_RESULTS[@]}"; do
|
|
i=$((i + 1))
|
|
if [[ $i -lt ${#JSON_RESULTS[@]} ]]; then
|
|
echo " ${result},"
|
|
else
|
|
echo " ${result}"
|
|
fi
|
|
done
|
|
echo " ]"
|
|
echo "}"
|
|
else
|
|
section_header "Summary"
|
|
field "Total checked:" "$COUNT_TOTAL"
|
|
field_color "OK:" "${GREEN}${COUNT_OK}${RESET}"
|
|
if [[ "$COUNT_WARN" -gt 0 ]]; then
|
|
field_color "Warning:" "${YELLOW}${COUNT_WARN}${RESET}"
|
|
else
|
|
field "Warning:" "$COUNT_WARN"
|
|
fi
|
|
if [[ "$COUNT_CRIT" -gt 0 ]]; then
|
|
field_color "Critical:" "${RED}${COUNT_CRIT}${RESET}"
|
|
else
|
|
field "Critical:" "$COUNT_CRIT"
|
|
fi
|
|
if [[ "$COUNT_EXPIRED" -gt 0 ]]; then
|
|
field_color "Expired:" "${RED}${COUNT_EXPIRED}${RESET}"
|
|
else
|
|
field "Expired:" "$COUNT_EXPIRED"
|
|
fi
|
|
if [[ "$COUNT_ERROR" -gt 0 ]]; then
|
|
field_color "Errors:" "${RED}${COUNT_ERROR}${RESET}"
|
|
else
|
|
field "Errors:" "$COUNT_ERROR"
|
|
fi
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
main "$@"
|