diff --git a/monitoring/scripts/homelab-audit.sh b/monitoring/scripts/homelab-audit.sh index da2cf7d..92d4609 100755 --- a/monitoring/scripts/homelab-audit.sh +++ b/monitoring/scripts/homelab-audit.sh @@ -30,6 +30,9 @@ MEM_WARN=85 ZOMBIE_WARN=1 SWAP_WARN=512 +HOSTS_FILTER="" # comma-separated host list from --hosts; empty = audit all +JSON_OUTPUT=0 # set to 1 by --json + while [[ $# -gt 0 ]]; do case "$1" in --output-dir) @@ -40,6 +43,18 @@ while [[ $# -gt 0 ]]; do REPORT_DIR="$2" shift 2 ;; + --hosts) + if [[ $# -lt 2 ]]; then + echo "Error: --hosts requires an argument" >&2 + exit 1 + fi + HOSTS_FILTER="$2" + shift 2 + ;; + --json) + JSON_OUTPUT=1 + shift + ;; *) echo "Unknown option: $1" >&2 exit 1 @@ -50,6 +65,7 @@ done mkdir -p "$REPORT_DIR" SSH_FAILURES_LOG="$REPORT_DIR/ssh-failures.log" FINDINGS_FILE="$REPORT_DIR/findings.txt" +AUDITED_HOSTS=() # populated in main; used by generate_summary for per-host counts # --------------------------------------------------------------------------- # Remote collector script @@ -281,6 +297,18 @@ generate_summary() { printf " Critical : %d\n" "$crit_count" echo "==============================" + if [[ ${#AUDITED_HOSTS[@]} -gt 0 ]] && ((warn_count + crit_count > 0)); then + echo "" + printf " %-30s %8s %8s\n" "Host" "Warnings" "Critical" + printf " %-30s %8s %8s\n" "----" "--------" "--------" + for host in "${AUDITED_HOSTS[@]}"; do + local hw hc + hw=$(grep -c "^WARN ${host}:" "$FINDINGS_FILE" 2>/dev/null || true) + hc=$(grep -c "^CRIT ${host}:" "$FINDINGS_FILE" 2>/dev/null || true) + ((hw + hc > 0)) && printf " %-30s %8d %8d\n" "$host" "$hw" "$hc" + done + fi + if ((warn_count + crit_count > 0)); then echo "" echo "Findings:" @@ -293,6 +321,9 @@ generate_summary() { grep '^SSH_FAILURE' "$SSH_FAILURES_LOG" | awk '{print " " $2 " (" $3 ")"}' fi + echo "" + printf "Total: %d warning(s), %d critical across %d host(s)\n" \ + "$warn_count" "$crit_count" "$host_count" echo "" echo "Reports: $REPORT_DIR" } @@ -383,6 +414,69 @@ check_cert_expiry() { done } +# --------------------------------------------------------------------------- +# JSON report — writes findings.json to $REPORT_DIR when --json is used +# --------------------------------------------------------------------------- +write_json_report() { + local host_count="$1" + local json_file="$REPORT_DIR/findings.json" + local ssh_failure_count=0 + local warn_count=0 + local crit_count=0 + + [[ -f "$SSH_FAILURES_LOG" ]] && + ssh_failure_count=$(grep -c '^SSH_FAILURE' "$SSH_FAILURES_LOG" 2>/dev/null || true) + [[ -f "$FINDINGS_FILE" ]] && + warn_count=$(grep -c '^WARN' "$FINDINGS_FILE" 2>/dev/null || true) + [[ -f "$FINDINGS_FILE" ]] && + crit_count=$(grep -c '^CRIT' "$FINDINGS_FILE" 2>/dev/null || true) + + python3 - "$json_file" "$host_count" "$ssh_failure_count" \ + "$warn_count" "$crit_count" "$FINDINGS_FILE" <<'PYEOF' +import sys, json, datetime + +json_file = sys.argv[1] +host_count = int(sys.argv[2]) +ssh_failure_count = int(sys.argv[3]) +warn_count = int(sys.argv[4]) +crit_count = int(sys.argv[5]) +findings_file = sys.argv[6] + +findings = [] +try: + with open(findings_file) as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(None, 2) + if len(parts) < 3: + continue + severity, host_colon, message = parts[0], parts[1], parts[2] + findings.append({ + "severity": severity, + "host": host_colon.rstrip(":"), + "message": message, + }) +except FileNotFoundError: + pass + +output = { + "timestamp": datetime.datetime.utcnow().isoformat() + "Z", + "hosts_audited": host_count, + "warnings": warn_count, + "critical": crit_count, + "ssh_failures": ssh_failure_count, + "total_findings": warn_count + crit_count, + "findings": findings, +} + +with open(json_file, "w") as f: + json.dump(output, f, indent=2) +print(f"JSON report: {json_file}") +PYEOF +} + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -390,22 +484,46 @@ main() { echo "Starting homelab audit — $(date)" echo "Report dir: $REPORT_DIR" echo "STUCK_PROC_CPU_WARN threshold: ${STUCK_PROC_CPU_WARN}%" + [[ -n "$HOSTS_FILTER" ]] && echo "Host filter: $HOSTS_FILTER" echo "" >"$FINDINGS_FILE" - echo " Checking Proxmox backup recency..." - check_backup_recency - local host_count=0 - while read -r label addr; do - echo " Auditing $label ($addr)..." - parse_and_report "$label" "$addr" - check_cert_expiry "$label" "$addr" - ((host_count++)) || true - done < <(collect_inventory) + + if [[ -n "$HOSTS_FILTER" ]]; then + # --hosts mode: audit specified hosts directly, skip Proxmox inventory + local check_proxmox=0 + IFS=',' read -ra filter_hosts <<<"$HOSTS_FILTER" + for host in "${filter_hosts[@]}"; do + [[ "$host" == "proxmox" ]] && check_proxmox=1 + done + if ((check_proxmox)); then + echo " Checking Proxmox backup recency..." + check_backup_recency + fi + for host in "${filter_hosts[@]}"; do + echo " Auditing $host..." + parse_and_report "$host" "$host" + check_cert_expiry "$host" "$host" + AUDITED_HOSTS+=("$host") + ((host_count++)) || true + done + else + echo " Checking Proxmox backup recency..." + check_backup_recency + + while read -r label addr; do + echo " Auditing $label ($addr)..." + parse_and_report "$label" "$addr" + check_cert_expiry "$label" "$addr" + AUDITED_HOSTS+=("$label") + ((host_count++)) || true + done < <(collect_inventory) + fi generate_summary "$host_count" + [[ "$JSON_OUTPUT" -eq 1 ]] && write_json_report "$host_count" } main "$@"