feat: dynamic summary, --hosts filter, and --json output (#24)
All checks were successful
Auto-merge docs-only PRs / auto-merge-docs (pull_request) Successful in 2s

Closes #24

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-04-02 22:04:00 -05:00
parent a97f443f60
commit 2918081ab2

View File

@ -30,6 +30,9 @@ MEM_WARN=85
ZOMBIE_WARN=1 ZOMBIE_WARN=1
SWAP_WARN=512 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 while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--output-dir) --output-dir)
@ -40,6 +43,18 @@ while [[ $# -gt 0 ]]; do
REPORT_DIR="$2" REPORT_DIR="$2"
shift 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 echo "Unknown option: $1" >&2
exit 1 exit 1
@ -50,6 +65,7 @@ done
mkdir -p "$REPORT_DIR" mkdir -p "$REPORT_DIR"
SSH_FAILURES_LOG="$REPORT_DIR/ssh-failures.log" SSH_FAILURES_LOG="$REPORT_DIR/ssh-failures.log"
FINDINGS_FILE="$REPORT_DIR/findings.txt" FINDINGS_FILE="$REPORT_DIR/findings.txt"
AUDITED_HOSTS=() # populated in main; used by generate_summary for per-host counts
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Remote collector script # Remote collector script
@ -281,6 +297,18 @@ generate_summary() {
printf " Critical : %d\n" "$crit_count" printf " Critical : %d\n" "$crit_count"
echo "==============================" 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 if ((warn_count + crit_count > 0)); then
echo "" echo ""
echo "Findings:" echo "Findings:"
@ -293,6 +321,9 @@ generate_summary() {
grep '^SSH_FAILURE' "$SSH_FAILURES_LOG" | awk '{print " " $2 " (" $3 ")"}' grep '^SSH_FAILURE' "$SSH_FAILURES_LOG" | awk '{print " " $2 " (" $3 ")"}'
fi fi
echo ""
printf "Total: %d warning(s), %d critical across %d host(s)\n" \
"$warn_count" "$crit_count" "$host_count"
echo "" echo ""
echo "Reports: $REPORT_DIR" echo "Reports: $REPORT_DIR"
} }
@ -383,6 +414,69 @@ check_cert_expiry() {
done 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 # Main
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -390,22 +484,46 @@ main() {
echo "Starting homelab audit — $(date)" echo "Starting homelab audit — $(date)"
echo "Report dir: $REPORT_DIR" echo "Report dir: $REPORT_DIR"
echo "STUCK_PROC_CPU_WARN threshold: ${STUCK_PROC_CPU_WARN}%" echo "STUCK_PROC_CPU_WARN threshold: ${STUCK_PROC_CPU_WARN}%"
[[ -n "$HOSTS_FILTER" ]] && echo "Host filter: $HOSTS_FILTER"
echo "" echo ""
>"$FINDINGS_FILE" >"$FINDINGS_FILE"
echo " Checking Proxmox backup recency..."
check_backup_recency
local host_count=0 local host_count=0
while read -r label addr; do
echo " Auditing $label ($addr)..." if [[ -n "$HOSTS_FILTER" ]]; then
parse_and_report "$label" "$addr" # --hosts mode: audit specified hosts directly, skip Proxmox inventory
check_cert_expiry "$label" "$addr" local check_proxmox=0
((host_count++)) || true IFS=',' read -ra filter_hosts <<<"$HOSTS_FILTER"
done < <(collect_inventory) 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" generate_summary "$host_count"
[[ "$JSON_OUTPUT" -eq 1 ]] && write_json_report "$host_count"
} }
main "$@" main "$@"