#!/usr/bin/env bash ############################################################################### # goat-check.sh v1.1 - comprehensive hardware health diagnostics # Debian / Ubuntu / Pop!_OS and derivatives # # - Installs required tooling (pre-filtered against the archive) # - ~105 sandboxed checks: storage/SMART, RAM/ECC, CPU, thermal, battery, # GPU, audio, network, PCIe/AER, firmware, kernel logs, services # - Every check is isolated: a failure, hang or missing tool never aborts # - Dedupe-before-count log analysis; per-device SMART cache # - --sanitize strips serials, MACs, UUIDs, IPs, hostname, username # - Emits verbose colored terminal output + self-contained HTML report # with an in-page green Download button # sudo ./goat-check.sh # live run, full install path # sudo ./goat-check.sh --sanitize --open # redacted report, auto-opens # Created by GrapheneGoat.com and Deepseek v4.1 Flash ############################################################################### VERSION="1.1" PROGNAME="goat-check" set -uo pipefail # deliberately NOT -e : we want to survive failures #------------------------------------------------------------------ defaults -- TIMEOUT=120 DO_INSTALL=1 DO_STRESS=0 DO_MODPROBE=1 DO_SENSORS_DETECT=0 DO_OPEN=0 DO_SANITIZE=0 USE_COLOR=1 TERM_FULL=0 TERM_MAX_LINES=30 OUTDIR="" MEMTEST_SIZE="256M" BENCH_ROTATIONAL="${BENCH_ROTATIONAL:-0}" #--------------------------------------------------------------------- usage -- usage() { cat <<'USAGE' goat-check.sh - full system hardware health diagnostics -> HTML report USAGE: sudo ./goat-check.sh [options] OPTIONS: -o, --output DIR Directory for the report (default: ~/goat-check-reports) -t, --timeout SEC Per-check timeout in seconds (default: 120) -s, --sanitize Redact serial numbers, MAC addresses, UUIDs, IP addresses, WWNs, SSIDs, machine/boot IDs, the hostname and the username from BOTH the terminal output and the HTML report. Use this before sharing the report. --stress Also run load/benchmark tests: stress-ng, sysbench, memtester, fio, hdparm timings, SMART short self-tests. Includes memory-pressure and free-space safety guards. --memtest SIZE Size for memtester under --stress (default: 256M) --bench-hdd Include rotational disks in hdparm timing benchmarks --no-install Skip the apt package installation phase --no-modprobe Do not load sensor modules (coretemp/drivetemp/...) --sensors-detect Run 'sensors-detect --auto' (writes /etc/modules) --full-output Print complete output of every check to the terminal --no-color Disable ANSI colors --open Launch firefox on the report when finished -h, --help Show this help EXAMPLES: sudo ./goat-check.sh sudo ./goat-check.sh --sanitize --open sudo ./goat-check.sh --stress --bench-hdd sudo ./goat-check.sh --no-install -t 60 USAGE } #---------------------------------------------------------------- arg parsing -- while [[ $# -gt 0 ]]; do case "$1" in -o|--output) OUTDIR="${2:-}"; shift 2 ;; -t|--timeout) TIMEOUT="${2:-120}"; shift 2 ;; -s|--sanitize) DO_SANITIZE=1; shift ;; --stress) DO_STRESS=1; shift ;; --memtest) MEMTEST_SIZE="${2:-256M}"; shift 2 ;; --bench-hdd) BENCH_ROTATIONAL=1; shift ;; --no-install) DO_INSTALL=0; shift ;; --no-modprobe) DO_MODPROBE=0; shift ;; --sensors-detect) DO_SENSORS_DETECT=1; shift ;; --full-output) TERM_FULL=1; shift ;; --no-color) USE_COLOR=0; shift ;; --open) DO_OPEN=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1"; usage; exit 1 ;; esac done #-------------------------------------------------------------------- colors -- if [[ $USE_COLOR -eq 1 && -t 1 ]]; then C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m' C_RED=$'\033[1;31m'; C_GRN=$'\033[1;32m'; C_YEL=$'\033[1;33m' C_BLU=$'\033[1;34m'; C_MAG=$'\033[1;35m'; C_CYA=$'\033[1;36m' C_WHT=$'\033[1;37m'; C_GRY=$'\033[0;90m' else C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GRN=""; C_YEL="" C_BLU=""; C_MAG=""; C_CYA=""; C_WHT=""; C_GRY="" fi have() { command -v "$1" >/dev/null 2>&1; } say() { printf '%s\n' "$*"; } info() { printf '%s\n' "${C_CYA}[*]${C_RESET} $*"; } ok() { printf '%s\n' "${C_GRN}[+]${C_RESET} $*"; } warn() { printf '%s\n' "${C_YEL}[!]${C_RESET} $*"; } err() { printf '%s\n' "${C_RED}[x]${C_RESET} $*"; } step() { printf '\n%s\n' "${C_MAG}${C_BOLD}==> $*${C_RESET}"; } banner() { printf '\n%s\n' "${C_BLU}${C_BOLD}+==============================================================================+${C_RESET}" printf '%s\n' "${C_BLU}${C_BOLD}| $(printf '%-76s' "$1")|${C_RESET}" printf '%s\n\n' "${C_BLU}${C_BOLD}+==============================================================================+${C_RESET}" } #-------------------------------------------------------------- root handling -- if [[ ${EUID:-$(id -u)} -ne 0 ]]; then if have sudo; then printf '%s\n' "${C_YEL}[!]${C_RESET} Root privileges required for SMART/DMI/firmware checks - elevating..." exec sudo -E bash "$0" "$@" else printf '%s\n' "${C_YEL}[!]${C_RESET} Not root and sudo unavailable: many checks will be limited or skipped." fi fi #--------------------------------------------------------------- output paths -- REAL_USER="${SUDO_USER:-${USER:-root}}" REAL_UID="$(id -u "$REAL_USER" 2>/dev/null || echo 0)" REAL_HOME="$(getent passwd "$REAL_USER" 2>/dev/null | cut -d: -f6)" [[ -z "$REAL_HOME" ]] && REAL_HOME="${HOME:-/root}" [[ -z "$OUTDIR" ]] && OUTDIR="$REAL_HOME/${PROGNAME}-reports" mkdir -p "$OUTDIR" 2>/dev/null || { OUTDIR="/tmp"; mkdir -p "$OUTDIR"; } HOSTN="$(hostname 2>/dev/null || echo unknown)" STAMP="$(date +%Y%m%d-%H%M%S)" if [[ $DO_SANITIZE -eq 1 ]]; then SAFE_HOST="redacted-host" REPORT="$OUTDIR/${PROGNAME}-sanitized-${STAMP}.html" LATEST="$OUTDIR/latest-sanitized.html" else SAFE_HOST="$HOSTN" REPORT="$OUTDIR/${PROGNAME}-${HOSTN}-${STAMP}.html" LATEST="$OUTDIR/latest.html" fi REPORT_BASE="$(basename "$REPORT")" TMPD="$(mktemp -d /tmp/${PROGNAME}.XXXXXX)" BODY="$TMPD/body.html" FINDINGS="$TMPD/findings.html" TOC="$TMPD/toc.html" SAN_SED="$TMPD/sanitize.sed" SMARTDIR="$TMPD/smart" : > "$BODY"; : > "$FINDINGS"; : > "$TOC"; : > "$SAN_SED" mkdir -p "$SMARTDIR" export OUTDIR TMPD SMARTDIR REAL_USER REAL_UID BENCH_ROTATIONAL #------------------------------------------------------------------ counters -- CHECK_TOTAL=0; C_OK=0; C_WARN=0; C_FAIL=0; C_SKIP=0 F_OK=0; F_WARN=0; F_FAIL=0; F_INFO=0 CAT_INDEX=0; CAT_OPEN=0 START_EPOCH=$(date +%s) #=============================================================================# # SANITIZATION # #=============================================================================# # Escape a literal string for use on the left side of a sed -E s||| rule. sed_lit() { printf '%s' "$1" | sed -e 's/[\\^$.*+?()[\]{}|\/&]/\\&/g'; } add_literal_rule() { local lit="$1" repl="$2" min="${3:-5}" [[ -z "$lit" ]] && return 0 [[ ${#lit} -lt $min ]] && return 0 case "$lit" in *[Nn]ot\ [Ss]pecified*|*[Uu]nknown*|*[Dd]efault\ string*|*"To Be Filled"*|None|none|0|00000000) return 0 ;; esac printf 's|%s|%s|g\n' "$(sed_lit "$lit")" "$repl" >> "$SAN_SED" } build_sanitize_rules() { [[ $DO_SANITIZE -eq 1 ]] || return 0 : > "$SAN_SED" # ---- 1. protect values that are harmless and worth keeping readable { echo 's|\b127\.0\.0\.1\b|@@LO4@@|g' echo 's|\b0\.0\.0\.0\b|@@ANY4@@|g' echo 's|\b255\.255\.255\.255\b|@@BC4@@|g' echo 's|\b224\.0\.0\.251\b|@@MDNS@@|g' echo 's|\b1\.1\.1\.1\b|@@CF@@|g' echo 's|\b8\.8\.8\.8\b|@@GG@@|g' echo 's|\b169\.254\.0\.0\b|@@LL4@@|g' } >> "$SAN_SED" # ---- 2. dynamic literals discovered on this machine local s add_literal_rule "$HOSTN" "[HOST-REDACTED]" 3 add_literal_rule "$REAL_USER" "[USER-REDACTED]" 4 add_literal_rule "$REAL_HOME" "[HOME-REDACTED]" 4 while read -r s; do add_literal_rule "$s" "[SERIAL-REDACTED]" 5; done \ < <(lsblk -dn -o SERIAL 2>/dev/null | tr -d ' ' | sort -u) if have dmidecode; then for s in system-serial-number baseboard-serial-number chassis-serial-number system-uuid; do add_literal_rule "$(dmidecode -s "$s" 2>/dev/null | head -1 | tr -d ' ')" "[DMI-REDACTED]" 5 done fi if have nvme; then while read -r s; do add_literal_rule "$s" "[SERIAL-REDACTED]" 5; done \ < <(nvme list 2>/dev/null | awk 'NR>2{print $2}' | sort -u) fi while read -r s; do add_literal_rule "$s" "[SSID-REDACTED]" 2; done \ < <(iwgetid -r 2>/dev/null; nmcli -t -f NAME connection show 2>/dev/null | head -40) # ---- 3. generic pattern rules cat >> "$SAN_SED" <<'SANRULES' s|([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}|[MAC-REDACTED]|g s|([0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2}|[MAC-REDACTED]|g s|\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b|[UUID-REDACTED]|g s|\b((25[0-5]\|2[0-4][0-9]\|[01]?[0-9]?[0-9])\.){3}(25[0-5]\|2[0-4][0-9]\|[01]?[0-9]?[0-9])\b|[IPV4-REDACTED]|g s|\bfe80::[0-9a-fA-F:]{2,}|[IPV6-LL-REDACTED]|g s|\b([0-9a-fA-F]{1,4}:){4,7}[0-9a-fA-F]{1,4}\b|[IPV6-REDACTED]|g s|(Serial Number:[[:space:]]*).*|\1[REDACTED]|g s|(Serial number:[[:space:]]*).*|\1[REDACTED]|g s|(serial:[[:space:]]*).*|\1[REDACTED]|g s|(LU WWN Device Id:[[:space:]]*).*|\1[REDACTED]|g s|(Logical Unit id:[[:space:]]*).*|\1[REDACTED]|g s|(IEEE EUI-64:[[:space:]]*).*|\1[REDACTED]|g s|(Asset Tag:[[:space:]]*).*|\1[REDACTED]|g s|(Machine ID:[[:space:]]*).*|\1[REDACTED]|g s|(Boot ID:[[:space:]]*).*|\1[REDACTED]|g s|(ESSID:[[:space:]]*).*|\1[REDACTED]|g s|(SSID:[[:space:]]*).*|\1[REDACTED]|g s|(Access Point:[[:space:]]*).*|\1[REDACTED]|g s|(Device ID:[[:space:]]*)[0-9a-fA-F]{16,}|\1[REDACTED]|g s|\b[0-9a-f]{32}\b|[HEXID-REDACTED]|g s|\b[A-Za-z0-9+/]{42,}={0,2}\b|[KEY-REDACTED]|g s|@@LO4@@|127.0.0.1|g s|@@ANY4@@|0.0.0.0|g s|@@BC4@@|255.255.255.255|g s|@@MDNS@@|224.0.0.251|g s|@@CF@@|1.1.1.1|g s|@@GG@@|8.8.8.8|g s|@@LL4@@|169.254.0.0|g SANRULES } sanitize_text() { if [[ $DO_SANITIZE -eq 1 && -s "$SAN_SED" ]]; then sed -E -f "$SAN_SED" 2>/dev/null || cat else cat fi } sanitize_str() { printf '%s' "$1" | sanitize_text; } #------------------------------------------------------------------- helpers -- html_escape() { sed -e 's/&/\&/g' -e 's//\>/g'; } strip_ansi() { sed -e 's/\r$//' -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g'; } category() { local name="$1" close_category CAT_INDEX=$((CAT_INDEX+1)) local cid="cat${CAT_INDEX}" printf '%s\n' "$cid" "$(printf '%s' "$name" | html_escape)" >> "$TOC" { printf '
\n' "$cid" printf '

%s. %s

\n' "$CAT_INDEX" "$(printf '%s' "$name" | html_escape)" } >> "$BODY" CAT_OPEN=1 banner "$CAT_INDEX. $name" } close_category() { if [[ $CAT_OPEN -eq 1 ]]; then echo '
' >> "$BODY"; CAT_OPEN=0; fi } # verdict LEVEL AREA MESSAGE verdict() { local level="$1" area="$2" msg="$3" col="" cls="" area="$(sanitize_str "$area")" msg="$(sanitize_str "$msg")" case "$level" in FAIL) F_FAIL=$((F_FAIL+1)); col="$C_RED"; cls="fail" ;; WARN) F_WARN=$((F_WARN+1)); col="$C_YEL"; cls="warn" ;; OK) F_OK=$((F_OK+1)); col="$C_GRN"; cls="ok" ;; *) level="INFO"; F_INFO=$((F_INFO+1)); col="$C_CYA"; cls="info" ;; esac printf ' %s%-5s%s %s%s%s : %s\n' "$col" "$level" "$C_RESET" "$C_BOLD" "$area" "$C_RESET" "$msg" printf '%s%s%s\n' \ "$cls" "$cls" "$level" "$CAT_INDEX" \ "$(printf '%s' "$area" | html_escape)" \ "$(printf '%s' "$msg" | html_escape)" >> "$FINDINGS" } # run_check "Title" 'command' [timeout] [extra-ok-exit-codes] run_check() { local title="$1" req="$2" cmd="$3" to="${4:-$TIMEOUT}" okrc="${5:-}" local status out rc dur t0 t1 id CHECK_TOTAL=$((CHECK_TOTAL+1)) id="chk${CHECK_TOTAL}" printf '%s\n' "${C_WHT}${C_BOLD}--- [${CHECK_TOTAL}] ${title}${C_RESET}" printf '%s\n' " ${C_GRY}\$ ${cmd}${C_RESET}" if [[ "$req" != "-" ]] && ! have "$req"; then status="skip"; rc=127; dur=0 out="SKIPPED: required tool '${req}' is not installed or not in PATH." else t0=$(date +%s%N) out="$(timeout -k 5s "${to}s" bash -c "$cmd" 2>&1)"; rc=$? t1=$(date +%s%N); dur=$(( (t1 - t0) / 1000000 )) out="$(printf '%s' "$out" | strip_ansi)" if [[ -n "$okrc" ]] && [[ ",$okrc," == *",$rc,"* ]]; then status="ok" else case $rc in 0) status="ok" ;; 124|137) status="warn"; out="${out}"$'\n\n'"[goat-check] TIMEOUT after ${to}s - check aborted, run continues." ;; 127) status="skip"; out="${out}"$'\n\n'"[goat-check] Command or tool not found." ;; *) status="warn"; out="${out}"$'\n\n'"[goat-check] Command exited with code ${rc}." ;; esac fi [[ -z "${out//[[:space:]]/}" ]] && out="(no output)" fi out="$(printf '%s' "$out" | sanitize_text)" case "$status" in ok) C_OK=$((C_OK+1)); printf ' %s[OK]%s %sms\n' "$C_GRN" "$C_RESET" "$dur" ;; warn) C_WARN=$((C_WARN+1)); printf ' %s[WARN]%s rc=%s\n' "$C_YEL" "$C_RESET" "$rc" ;; fail) C_FAIL=$((C_FAIL+1)); printf ' %s[FAIL]%s rc=%s\n' "$C_RED" "$C_RESET" "$rc" ;; skip) C_SKIP=$((C_SKIP+1)); printf ' %s[SKIP]%s\n' "$C_GRY" "$C_RESET" ;; esac local nlines nlines=$(printf '%s\n' "$out" | wc -l) if [[ $TERM_FULL -eq 1 || $nlines -le $TERM_MAX_LINES ]]; then printf '%s\n' "$out" | sed 's/^/ /' else printf '%s\n' "$out" | head -n "$TERM_MAX_LINES" | sed 's/^/ /' printf ' %s... %s more lines - see HTML report ...%s\n' "$C_GRY" "$((nlines-TERM_MAX_LINES))" "$C_RESET" fi echo local openattr="" [[ "$status" != "ok" ]] && openattr=" open" { printf '
\n' "$status" "$id" printf '%s%s' \ "$openattr" "$status" "$(echo "$status" | tr 'a-z' 'A-Z')" "$(printf '%s' "$title" | html_escape)" printf '%s ms · rc=%s\n' "$dur" "$rc" printf '
$ %s
\n' "$(printf '%s' "$cmd" | html_escape)" printf '
%s
\n' "$(printf '%s' "$out" | html_escape)" printf '
\n' } >> "$BODY" } #=============================================================================# # Reusable report functions (exported to bash -c children) # #=============================================================================# # Print a fallback message when a pipeline produced no output. none_if_empty() { local msg="${1:-None found.}" out out="$(cat)" if [[ -z "${out//[[:space:]]/}" ]]; then printf '%s\n' "$msg"; else printf '%s\n' "$out"; fi } # Real physical disks only - excludes zram, loop, md, dm-, sr, ram, fd. disks_list() { lsblk -dn -o NAME,TYPE 2>/dev/null \ | awk '$2=="disk" && $1 !~ /^(zram|loop|md|dm-|sr|ram|fd|nbd)/ {print "/dev/"$1}' } # Run a command as the desktop user with a working session environment. as_user() { if [[ ${EUID:-$(id -u)} -eq 0 && "${REAL_USER}" != "root" ]]; then sudo -u "${REAL_USER}" env \ DISPLAY="${DISPLAY:-:0}" \ WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-}" \ XDG_RUNTIME_DIR="/run/user/${REAL_UID}" \ XAUTHORITY="${XAUTHORITY:-/home/${REAL_USER}/.Xauthority}" \ HOME="/home/${REAL_USER}" \ "$@" else "$@" fi } #------------------------------------------------------------ SMART subsystem - smart_key() { printf '%s' "$1" | tr '/' '_'; } # Probe each disk for a working smartctl device type (handles USB bridges). smart_probe() { local d key t mkdir -p "$SMARTDIR" for d in $(disks_list); do key="$(smart_key "$d")" [[ -f "$SMARTDIR/$key.type" ]] && continue printf 'UNSUPPORTED' > "$SMARTDIR/$key.type" for t in auto sat sat,12 sat,16 usbjmicron usbsunplus usbprolific usbcypress nvme scsi; do if [[ "$t" == "auto" ]]; then if smartctl -i -n standby,0 "$d" >/dev/null 2>&1; then printf '' > "$SMARTDIR/$key.type"; break; fi else if smartctl -i -n standby,0 -d "$t" "$d" >/dev/null 2>&1; then printf -- '-d %s' "$t" > "$SMARTDIR/$key.type"; break; fi fi done done } smart_type_of() { cat "$SMARTDIR/$(smart_key "$1").type" 2>/dev/null; } # Cache one -x (display) and one -i -H -A (parse) per device. -n standby,0 # means sleeping drives are never spun up just to be inspected. smart_cache() { local d key tp smart_probe for d in $(disks_list); do key="$(smart_key "$d")" tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" [[ "$tp" == "UNSUPPORTED" ]] && continue [[ -f "$SMARTDIR/$key.x" ]] || smartctl -x -n standby,0 $tp "$d" > "$SMARTDIR/$key.x" 2>&1 [[ -f "$SMARTDIR/$key.p" ]] || smartctl -i -H -A -n standby,0 $tp "$d" > "$SMARTDIR/$key.p" 2>&1 done } smart_report() { local d key tp found=0 for d in $(disks_list); do found=1 key="$(smart_key "$d")" tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" echo "===================================================================" echo "### DEVICE: $d [smartctl type: ${tp:-auto-detect}]" echo "===================================================================" if [[ "$tp" == "UNSUPPORTED" ]]; then echo "No working smartctl access method found for this device." echo "Tried: auto, sat, sat,12, sat,16, usbjmicron, usbsunplus," echo " usbprolific, usbcypress, nvme, scsi." echo "This is normal for some USB enclosures and hardware RAID volumes." elif [[ -f "$SMARTDIR/$key.x" ]]; then cat "$SMARTDIR/$key.x" else echo "(no cached SMART data)" fi echo done [[ $found -eq 0 ]] && echo "No physical disks found." return 0 } smart_summary() { local d key p model health temp poh tp printf '%-14s %-26s %-10s %-8s %-12s %s\n' DEVICE MODEL HEALTH TEMP_C POWER_ON_H ACCESS for d in $(disks_list); do key="$(smart_key "$d")"; p="$SMARTDIR/$key.p"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" if [[ "$tp" == "UNSUPPORTED" || ! -f "$p" ]]; then printf '%-14s %-26s %-10s %-8s %-12s %s\n' "$d" "-" "no-smart" "-" "-" "none" continue fi model="$(grep -iE '^(Device Model|Model Number|Product):' "$p" | head -1 | sed 's/.*: *//' | cut -c1-25)" health="$(grep -iE 'overall-health|SMART Health Status' "$p" | head -1 | sed 's/.*: *//')" temp="$(grep -iE 'Temperature_Celsius|^Temperature:' "$p" | head -1 | awk '{for(i=NF;i>0;i--) if($i ~ /^[0-9]+$/){print $i; exit}}')" poh="$(grep -iE 'Power_On_Hours|^Power On Hours' "$p" | head -1 | sed 's/.*: *//' | awk '{print $NF}' | tr -d ',')" printf '%-14s %-26s %-10s %-8s %-12s %s\n' "$d" "${model:-?}" "${health:-unknown}" \ "${temp:-?}" "${poh:-?}" "${tp:-auto}" done return 0 } smart_selftest_log() { local d key tp for d in $(disks_list); do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" echo "### $d" if [[ "$tp" == "UNSUPPORTED" ]]; then echo " (SMART not accessible)"; echo; continue; fi smartctl -l selftest -n standby,0 $tp "$d" 2>&1 | sed 's/^/ /' | head -n 16 echo done return 0 } smart_devstat() { local d key tp for d in $(disks_list); do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" echo "### $d" if [[ "$tp" == "UNSUPPORTED" ]]; then echo " (SMART not accessible)"; echo; continue; fi smartctl -l devstat -n standby,0 $tp "$d" 2>&1 | sed 's/^/ /' | head -n 60 echo done return 0 } drive_temps() { local d key p t for d in $(disks_list); do key="$(smart_key "$d")"; p="$SMARTDIR/$key.p" t="" [[ -f "$p" ]] && t="$(grep -iE 'Temperature_Celsius|^Temperature:|^Composite' "$p" | head -1 | sed 's/^[[:space:]]*//')" printf '%-16s %s\n' "$d" "${t:-n/a (no SMART temperature, or device asleep)}" done return 0 } nvme_report() { local d found=0 nvme list 2>&1 for d in /dev/nvme?n?; do [[ -e "$d" ]] || continue found=1 echo; echo "### $d -- SMART / health log" nvme smart-log "$d" 2>&1 echo; echo "### $d -- controller identify (abridged)" nvme id-ctrl "$d" 2>&1 | head -n 25 echo; echo "### $d -- error log (last entries)" nvme error-log "$d" 2>&1 | head -n 25 done [[ $found -eq 0 ]] && echo "No NVMe namespaces present." return 0 } power_report() { local p found=0 for p in /sys/class/power_supply/*; do [[ -e "$p" ]] || continue found=1 echo "### $(basename "$p") (type: $(cat "$p/type" 2>/dev/null || echo unknown))" grep -H . "$p"/* 2>/dev/null | sed "s|${p}/||" | sed 's/^/ /' echo done [[ $found -eq 0 ]] && echo "No power supply devices exposed in sysfs (typical for desktops/servers)." return 0 } thermal_report() { local z c f found=0 echo "--- thermal zones ---" for z in /sys/class/thermal/thermal_zone*; do [[ -d "$z" ]] || continue found=1 printf '%-20s %-26s %s\n' "$(basename "$z")" "$(cat "$z/type" 2>/dev/null)" \ "$(awk '{printf "%.1f C", $1/1000}' "$z/temp" 2>/dev/null || echo 'n/a')" done [[ $found -eq 0 ]] && echo "(no thermal zones exposed)" echo; echo "--- hwmon chips ---" for c in /sys/class/hwmon/hwmon*; do [[ -d "$c" ]] || continue printf '%-12s %s\n' "$(basename "$c")" "$(cat "$c/name" 2>/dev/null)" done echo; echo "--- cooling devices (fans / passive throttles) ---" for f in /sys/class/thermal/cooling_device*; do [[ -d "$f" ]] || continue printf '%-22s %-40s state %s / %s\n' "$(basename "$f")" \ "$(cat "$f/type" 2>/dev/null)" "$(cat "$f/cur_state" 2>/dev/null)" "$(cat "$f/max_state" 2>/dev/null)" done return 0 } fan_report() { local c n f label rpm found=0 for c in /sys/class/hwmon/hwmon*; do [[ -d "$c" ]] || continue n="$(cat "$c/name" 2>/dev/null)" for f in "$c"/fan*_input; do [[ -r "$f" ]] || continue found=1 rpm="$(cat "$f" 2>/dev/null)" label="$(cat "${f%_input}_label" 2>/dev/null || basename "$f" _input)" printf '%-16s %-20s %6s RPM\n' "$n" "$label" "$rpm" done done [[ $found -eq 0 ]] && echo "No fan tachometer inputs exposed via hwmon." echo echo "--- PWM controls ---" for c in /sys/class/hwmon/hwmon*/pwm[0-9]; do [[ -r "$c" ]] && echo "$c = $(cat "$c" 2>/dev/null)" done 2>/dev/null | none_if_empty "No PWM control nodes exposed." return 0 } throttle_report() { local f c tot=0 found=0 echo "--- per-core thermal throttle counters ---" for f in /sys/devices/system/cpu/cpu*/thermal_throttle/*_throttle_count; do [[ -r "$f" ]] || continue found=1 c="$(cat "$f" 2>/dev/null)" printf '%-62s %s\n' "${f#/sys/devices/system/cpu/}" "$c" [[ "$c" =~ ^[0-9]+$ ]] && tot=$((tot+c)) done [[ $found -eq 0 ]] && echo "No thermal_throttle counters exposed by this CPU/driver." echo; echo "TOTAL_THROTTLE_EVENTS=$tot" echo; echo "--- RAPL power caps ---" for f in /sys/class/powercap/intel-rapl:*/constraint_*_power_limit_uw \ /sys/class/powercap/intel-rapl:*/name; do [[ -r "$f" ]] && echo "${f#/sys/class/powercap/} = $(cat "$f" 2>/dev/null)" done 2>/dev/null | none_if_empty "No RAPL powercap nodes exposed." echo; echo "--- HWP / turbo state ---" [[ -r /sys/devices/system/cpu/intel_pstate/no_turbo ]] && \ echo "intel_pstate/no_turbo = $(cat /sys/devices/system/cpu/intel_pstate/no_turbo)" [[ -r /sys/devices/system/cpu/cpufreq/boost ]] && \ echo "cpufreq/boost = $(cat /sys/devices/system/cpu/cpufreq/boost)" return 0 } throttle_total() { local f tot=0 c for f in /sys/devices/system/cpu/cpu*/thermal_throttle/*_throttle_count; do [[ -r "$f" ]] || continue c="$(cat "$f" 2>/dev/null)" [[ "$c" =~ ^[0-9]+$ ]] && tot=$((tot+c)) done echo "$tot" } cpufreq_report() { local c printf '%-8s %-16s %-12s %-12s %-12s\n' CPU GOVERNOR CUR_MHZ MIN_MHZ MAX_MHZ for c in /sys/devices/system/cpu/cpu[0-9]*/cpufreq; do [[ -d "$c" ]] || continue printf '%-8s %-16s %-12s %-12s %-12s\n' \ "$(basename "$(dirname "$c")")" \ "$(cat "$c/scaling_governor" 2>/dev/null)" \ "$(awk '{printf "%.0f", $1/1000}' "$c/scaling_cur_freq" 2>/dev/null)" \ "$(awk '{printf "%.0f", $1/1000}' "$c/cpuinfo_min_freq" 2>/dev/null)" \ "$(awk '{printf "%.0f", $1/1000}' "$c/cpuinfo_max_freq" 2>/dev/null)" done echo; echo "--- /proc/cpuinfo (deduplicated) ---" grep -E 'model name|cache size|siblings|cpu cores' /proc/cpuinfo 2>/dev/null | sort | uniq -c return 0 } microcode_report() { echo "CPU model : $(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: *//')" echo "Running ucode : $(grep -m1 microcode /proc/cpuinfo | sed 's/.*: *//')" echo "CPU family/mod : $(awk -F: '/^cpu family/{f=$2} /^model[[:space:]]*:/{m=$2} /^stepping/{s=$2} END{printf "family%s model%s stepping%s", f, m, s}' /proc/cpuinfo)" echo echo "--- microcode package installed ---" dpkg -l intel-microcode amd64-microcode 2>/dev/null | grep -E '^ii' | none_if_empty \ "NO MICROCODE PACKAGE INSTALLED. On Intel/AMD this means you are running whatever microcode the motherboard firmware supplied. Install 'intel-microcode' or 'amd64-microcode' to receive CPU security and errata fixes at boot." echo echo "--- microcode messages this boot ---" dmesg 2>/dev/null | grep -i microcode | none_if_empty "No microcode messages in the kernel log." echo echo "--- vulnerabilities still reporting Vulnerable ---" grep -r . /sys/devices/system/cpu/vulnerabilities/ 2>/dev/null \ | grep -i 'vulnerable' | sed 's|.*vulnerabilities/||' \ | none_if_empty "None - every known vulnerability reports mitigated or not-affected." return 0 } taint_decode() { local t i bit desc t="$(cat /proc/sys/kernel/tainted 2>/dev/null || echo 0)" echo "Raw taint value: $t" if [[ "$t" == "0" ]]; then echo "Kernel is NOT tainted."; return 0; fi echo "Decoded flags:" local bits=( "0|proprietary module loaded" "1|module was force-loaded" "2|SMP with a CPU not designed for it" "3|module was force-unloaded" "4|MACHINE CHECK EXCEPTION occurred <-- HARDWARE" "5|BAD PAGE / memory corruption detected <-- HARDWARE" "6|user requested taint (e.g. ACPI override, /dev/mem)" "7|kernel OOPS or die() occurred <-- INVESTIGATE" "8|ACPI table overridden by user" "9|kernel issued a WARNING" "10|staging driver loaded" "11|firmware bug workaround applied" "12|out-of-tree module loaded (DKMS: VirtualBox, nvidia, vendor drivers)" "13|unsigned module loaded" "14|SOFT LOCKUP occurred <-- INVESTIGATE" "15|kernel has been live-patched" "16|auxiliary taint (vendor-defined)" "17|kernel built with struct randomization" "18|an in-kernel test module was loaded" ) for i in "${bits[@]}"; do bit="${i%%|*}"; desc="${i##*|}" if (( t & (1 << bit) )); then printf ' bit %-2s : %s\n' "$bit" "$desc"; fi done echo echo "Note: bits 0, 6, 12 and 13 are routine on desktops with DKMS or" echo "proprietary drivers and do NOT indicate a hardware problem." return 0 } edac_report() { local f found=0 for f in /sys/devices/system/edac/mc/mc*/ce_count /sys/devices/system/edac/mc/mc*/ue_count \ /sys/devices/system/edac/mc/mc*/csrow*/ce_count /sys/devices/system/edac/mc/mc*/csrow*/ue_count \ /sys/devices/system/edac/mc/mc*/dimm*/dimm_ce_count /sys/devices/system/edac/mc/mc*/dimm*/dimm_ue_count; do [[ -f "$f" ]] || continue found=1 echo "$f = $(cat "$f" 2>/dev/null)" done if [[ $found -eq 0 ]]; then echo "No EDAC counters present." echo "This normally means the RAM is non-ECC (consumer desktop/laptop), so" echo "single-bit memory errors are silently uncorrectable and undetectable." echo "Use --stress (memtester) or a boot-time memtest86+ pass to test RAM." fi if command -v ras-mc-ctl >/dev/null 2>&1; then echo; echo "--- ras-mc-ctl --error-count ---"; ras-mc-ctl --error-count 2>&1 echo; echo "--- ras-mc-ctl --summary ---"; ras-mc-ctl --summary 2>&1 fi return 0 } aer_report() { local d n sum found=0 for d in /sys/bus/pci/devices/*; do for n in aer_dev_correctable aer_dev_fatal aer_dev_nonfatal; do [[ -r "$d/$n" ]] || continue sum="$(awk '{s+=$2} END{print s+0}' "$d/$n" 2>/dev/null)" if [[ "$sum" != "0" && -n "$sum" ]]; then found=1 echo "### $(basename "$d") [$n] total=$sum" lspci -s "$(basename "$d" | sed 's/^0000://')" 2>/dev/null | sed 's/^/ device: /' awk '$2 != 0 {printf " %-28s %s\n", $1, $2}' "$d/$n" 2>/dev/null echo fi done done [[ $found -eq 0 ]] && echo "No non-zero PCIe AER error counters. All PCIe links are error-free." echo echo "--- AER / PCIe messages in the kernel log ---" dmesg -T 2>/dev/null | grep -iE 'aer|pcieport.*error|Corrected error|Uncorrected error|bad (TLP|DLLP)' \ | tail -n 25 | none_if_empty "No PCIe AER messages in the kernel log." return 0 } pcie_link_report() { local s out cap sta name flag echo "Link speed/width: capability vs. current state." echo "Downtraining at idle is normal (ASPM power saving); a link stuck low" echo "under load, or a width far below capability, can mean a reseat is needed." echo printf '%-13s %-26s %-26s %s\n' BDF CAPABILITY CURRENT DEVICE for s in $(lspci -D 2>/dev/null | awk '{print $1}'); do out="$(lspci -s "$s" -vv 2>/dev/null)" grep -q 'LnkCap:' <<<"$out" || continue cap="$(grep -m1 'LnkCap:' <<<"$out" | sed -E 's/.*Speed ([^,]+), Width ([^,]+).*/\1 \2/')" sta="$(grep -m1 'LnkSta:' <<<"$out" | sed -E 's/.*Speed ([^,(]+).*Width ([^,(]+).*/\1 \2/')" [[ -z "$cap" || -z "$sta" ]] && continue name="$(lspci -s "$s" 2>/dev/null | cut -d' ' -f2- | cut -c1-44)" flag="" [[ "$(tr -d ' ' <<<"$cap")" != "$(tr -d ' ' <<<"$sta")" ]] && flag=" *" printf '%-13s %-26s %-26s %s%s\n' "$s" "$cap" "$sta" "$name" "$flag" done echo echo "* = current state differs from capability" return 0 } net_report() { local i for i in /sys/class/net/*; do i="$(basename "$i")" [[ "$i" == "lo" ]] && continue echo "=================== $i ===================" echo "operstate: $(cat "/sys/class/net/$i/operstate" 2>/dev/null) mac: $(cat "/sys/class/net/$i/address" 2>/dev/null) mtu: $(cat "/sys/class/net/$i/mtu" 2>/dev/null)" if command -v ethtool >/dev/null 2>&1; then ethtool "$i" 2>&1 | sed 's/^/ /' echo " --- driver ---" ethtool -i "$i" 2>&1 | sed 's/^/ /' echo " --- error / drop counters (non-zero only) ---" ethtool -S "$i" 2>/dev/null | grep -iE 'err|drop|fail|crc|collision|missed|discard' \ | awk -F: '{gsub(/ /,"",$2); if ($2+0 != 0) print " " $0}' \ | none_if_empty " (all error counters are zero)" fi echo done return 0 } fs_report() { local dev tgt fs findmnt -rn -o SOURCE,TARGET,FSTYPE 2>/dev/null | while read -r dev tgt fs; do case "$fs" in ext2|ext3|ext4) ;; *) continue ;; esac [[ -b "$dev" ]] || continue echo "### $dev -> $tgt ($fs)" tune2fs -l "$dev" 2>&1 | grep -iE 'Filesystem state|Errors behavior|Mount count|Maximum mount|Last checked|Check interval|Next check|Lifetime writes|Free blocks|Block count|FS Error count|First error|Last error' | sed 's/^/ /' echo done return 0 } raid_report() { echo "--- /proc/mdstat ---" cat /proc/mdstat 2>/dev/null || echo "(none)" echo if command -v mdadm >/dev/null 2>&1; then echo "--- mdadm --detail --scan ---" mdadm --detail --scan 2>&1 | none_if_empty "No mdadm arrays configured." fi if command -v zpool >/dev/null 2>&1; then echo; echo "--- zpool status ---"; zpool status 2>&1 fi if command -v pvs >/dev/null 2>&1; then echo; echo "--- LVM ---"; pvs 2>&1; vgs 2>&1; lvs 2>&1 fi if command -v cryptsetup >/dev/null 2>&1; then echo; echo "--- active dm-crypt / LUKS volumes ---" dmsetup ls --target crypt 2>/dev/null | none_if_empty "No active dm-crypt volumes." fi return 0 } trim_report() { echo "--- discard / TRIM support ---" lsblk -D -o NAME,DISC-ALN,DISC-GRAN,DISC-MAX,DISC-ZERO,MOUNTPOINT 2>/dev/null echo; echo "--- fstrim.timer ---" systemctl status fstrim.timer --no-pager 2>&1 | head -n 12 \ | none_if_empty "fstrim.timer not present." echo; echo "--- recent fstrim runs ---" journalctl -u fstrim.service --no-pager -n 10 2>/dev/null \ | none_if_empty "No fstrim service history in the journal." echo; echo "--- filesystems mounted with discard ---" findmnt -rn -o TARGET,OPTIONS 2>/dev/null | grep -i discard \ | none_if_empty "No filesystems use the 'discard' mount option (periodic fstrim is preferred)." return 0 } log_usage_report() { echo "--- journal disk usage ---" journalctl --disk-usage 2>&1 echo; echo "--- /var subtree sizes ---" du -sh /var/log /var/crash /var/cache /var/lib /var/tmp 2>/dev/null | sort -h echo; echo "--- crash dumps present ---" ls -lh /var/crash 2>/dev/null | tail -n +2 | none_if_empty "/var/crash is empty." echo; echo "--- journal retention config ---" grep -E '^[^#]*(SystemMaxUse|MaxRetentionSec|SystemMaxFiles)' /etc/systemd/journald.conf 2>/dev/null \ | none_if_empty "journald is using defaults (max 10% of the filesystem)." return 0 } audio_report() { echo "--- sound cards (/proc/asound/cards) ---" cat /proc/asound/cards 2>/dev/null | none_if_empty "No ALSA sound cards registered." echo; echo "--- playback devices ---" aplay -l 2>&1 | none_if_empty "aplay unavailable or no playback devices." echo; echo "--- capture devices ---" arecord -l 2>&1 | none_if_empty "arecord unavailable or no capture devices." echo; echo "--- codecs ---" for c in /proc/asound/card*/codec#*; do [[ -r "$c" ]] && { echo "### $c"; head -n 6 "$c"; echo; } done 2>/dev/null | none_if_empty "No HDA codec information exposed." echo "--- PulseAudio / PipeWire (as desktop user) ---" as_user pactl info 2>&1 | head -n 12 | none_if_empty "pactl unavailable or no audio session." echo as_user pactl list cards short 2>&1 | none_if_empty "No audio cards visible to the sound server." return 0 } seg_summary() { journalctl -b --no-pager 2>/dev/null | grep -i 'segfault' \ | sed -E 's/.* in ([^ []+)\[.*/\1/; t; s/.*/unidentified/' \ | sort | uniq -c | sort -rn } dmesg_dedup() { local level="${1:-err,crit,alert,emerg}" dmesg -l "$level" 2>/dev/null \ | sed -E 's/^\[[^]]*\][[:space:]]*//; s/\b[0-9a-f]{6,}\b/HEX/g; s/[0-9]+/N/g' \ | sort | uniq -c | sort -rn } dmesg_dedup_report() { echo "Kernel messages this boot, collapsed to unique patterns." echo "Numbers and hex values are normalised so a repeating message counts once." echo echo "=== ERROR and above ===" dmesg_dedup "err,crit,alert,emerg" | head -n 20 | none_if_empty "No error-level messages." echo echo "=== WARNINGS ===" dmesg_dedup "warn" | head -n 20 | none_if_empty "No warning-level messages." return 0 } #---------------------------------------------------- stress / benchmark jobs - mem_stress() { local totalmb availmb usemb hogs totalmb=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 )) availmb=$(( $(awk '/^MemAvailable:/{print $2}' /proc/meminfo) / 1024 )) echo "MemTotal=${totalmb}MB MemAvailable=${availmb}MB" hogs="$(ps -eo pmem,rss,comm --sort=-pmem 2>/dev/null | awk 'NR>1 && $1+0 > 10 {printf " %5s%% %8s KB %s\n", $1, $2, $3}' | head -5)" if [[ -n "$hogs" ]]; then echo echo "WARNING: processes are already holding large amounts of RAM:" echo "$hogs" echo "A memory stressor could push these into swap or trigger the OOM killer." fi if (( availmb < 4096 )); then echo echo "SKIPPING VM stressor: only ${availmb}MB available, safety floor is 4096MB." return 0 fi usemb=$(( availmb * 40 / 100 )) (( usemb > totalmb / 4 )) && usemb=$(( totalmb / 4 )) (( usemb < 256 )) && usemb=256 echo echo "Allocating ${usemb}MB total across 2 workers for 30s (40% of available, capped at 25% of total)." stress-ng --vm 2 --vm-bytes "$(( usemb / 2 ))M" --timeout 30s --metrics-brief 2>&1 return 0 } memtester_guarded() { local want="${1:-256M}" wantmb availmb case "$want" in *G|*g) wantmb=$(( ${want%[Gg]} * 1024 )) ;; *M|*m) wantmb=${want%[Mm]} ;; *) wantmb=$want ;; esac availmb=$(( $(awk '/^MemAvailable:/{print $2}' /proc/meminfo) / 1024 )) echo "Requested ${wantmb}MB, MemAvailable=${availmb}MB" if (( wantmb > availmb / 2 )); then echo "SKIPPING memtester: request exceeds half of available RAM." echo "Re-run with a smaller --memtest value, or use memtest86+ from the boot menu" echo "which is the only way to test ALL of RAM including kernel-resident pages." return 0 fi echo "NOTE: memtester can only test unallocated userspace RAM. A clean pass does" echo " not prove the modules are good - memtest86+ at boot is authoritative." echo memtester "${wantmb}M" 1 2>&1 return 0 } bench_disk() { local dir="${OUTDIR:-/tmp}" f freemb freemb=$(df -Pm "$dir" 2>/dev/null | awk 'NR==2{print $4}') if [[ ! "$freemb" =~ ^[0-9]+$ ]] || (( freemb < 2048 )); then echo "SKIPPING fio: only ${freemb:-unknown}MB free in $dir (2048MB required)." return 0 fi f="$dir/.${PROGNAME:-goat-check}-fio-test" fio --name=randread --filename="$f" --size=256M --rw=randread --bs=4k \ --iodepth=16 --numjobs=1 --runtime=15 --time_based --direct=1 --group_reporting 2>&1 rm -f "$f" return 0 } bench_hdparm() { local d n rota tran for d in $(disks_list); do n="${d#/dev/}" rota="$(cat "/sys/block/$n/queue/rotational" 2>/dev/null || echo 1)" tran="$(lsblk -dn -o TRAN "$d" 2>/dev/null | tr -d ' ')" if [[ "$tran" == "usb" ]]; then echo "### $d - skipped (USB-attached; timings are meaningless over a bridge)"; echo; continue fi if [[ "$rota" == "1" && "${BENCH_ROTATIONAL:-0}" != "1" ]]; then echo "### $d - skipped (rotational; pass --bench-hdd to include, it spins up and takes ~20s)"; echo; continue fi echo "### $d" hdparm -tT "$d" 2>&1 echo done return 0 } smart_selftest() { local d key tp waited=0 remaining started=() for d in $(disks_list); do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" if [[ "$tp" == "UNSUPPORTED" ]]; then echo "### $d - SMART not accessible, skipping self-test."; continue fi echo "### starting SMART short self-test on $d" smartctl -t short $tp "$d" 2>&1 | tail -n 4 started+=("$d") done if (( ${#started[@]} == 0 )); then echo; echo "No eligible devices."; return 0; fi echo echo "Polling for completion (up to 10 minutes)..." while (( waited < 600 )); do remaining=0 for d in "${started[@]}"; do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" if smartctl -c $tp "$d" 2>/dev/null | grep -qiE 'in progress|remaining'; then remaining=$((remaining+1)) fi done (( remaining == 0 )) && break printf ' %3ss elapsed, %s drive(s) still testing...\n' "$waited" "$remaining" sleep 15; waited=$((waited+15)) done echo echo "Completed after ${waited}s. Results:" echo for d in "${started[@]}"; do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" echo "### $d self-test log" smartctl -l selftest $tp "$d" 2>&1 | head -n 14 | sed 's/^/ /' echo done return 0 } export -f have none_if_empty disks_list as_user smart_key smart_probe smart_type_of \ smart_cache smart_report smart_summary smart_selftest_log smart_devstat \ drive_temps nvme_report power_report thermal_report fan_report \ throttle_report throttle_total cpufreq_report microcode_report taint_decode \ edac_report aer_report pcie_link_report net_report fs_report raid_report \ trim_report log_usage_report audio_report seg_summary dmesg_dedup \ dmesg_dedup_report mem_stress memtester_guarded bench_disk bench_hdparm \ smart_selftest export PROGNAME #=============================================================================# # ANALYZERS # #=============================================================================# analyze_smart() { if ! have smartctl; then verdict INFO "SMART" "smartctl unavailable - drive health could not be assessed."; return; fi local devs d devs="$(disks_list)" if [[ -z "$devs" ]]; then verdict INFO "SMART" "No physical disks detected."; return; fi for d in $devs; do local key p tp out v pair label key="$(smart_key "$d")"; p="$SMARTDIR/$key.p"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" if [[ "$tp" == "UNSUPPORTED" ]]; then verdict INFO "SMART $d" "Device exposes no SMART interface through any known access method (USB bridge or RAID volume). Health cannot be assessed from this host." continue fi [[ -f "$p" ]] || continue out="$(cat "$p")" if grep -qiE 'self-assessment test result: *PASSED|SMART Health Status: *OK' <<<"$out"; then verdict OK "SMART $d" "Overall health self-assessment: PASSED${tp:+ (via ${tp})}" elif grep -qiE 'self-assessment test result: *FAILED|SMART Health Status: *(FAILED|NOT OK)' <<<"$out"; then verdict FAIL "SMART $d" "Overall health self-assessment: FAILED - back up immediately and replace this drive." elif grep -qi 'Device is in STANDBY' <<<"$out"; then verdict INFO "SMART $d" "Drive is in standby; not spun up for inspection." else verdict WARN "SMART $d" "SMART health line could not be parsed from the device response." fi for pair in "Reallocated_Sector_Ct:Reallocated sectors" \ "Current_Pending_Sector:Pending (unreadable) sectors" \ "Offline_Uncorrectable:Offline uncorrectable sectors" \ "Reported_Uncorrect:Reported uncorrectable errors"; do label="${pair##*:}" v="$(awk -v k="${pair%%:*}" '$2==k {print $10}' <<<"$out" | head -1 | tr -d ',')" if [[ "$v" =~ ^[0-9]+$ ]]; then if (( v > 50 )); then verdict FAIL "SMART $d" "$label = $v - the drive is actively degrading, replace it." elif (( v > 0 )); then verdict WARN "SMART $d" "$label = $v - monitor closely and plan replacement." fi fi done v="$(awk '$2=="UDMA_CRC_Error_Count" {print $10}' <<<"$out" | head -1 | tr -d ',')" [[ "$v" =~ ^[0-9]+$ ]] && (( v > 0 )) && \ verdict WARN "SMART $d" "UDMA CRC errors = $v - this is a cable/connector fault, not the platter. Reseat the SATA cable." if grep -qi 'Percentage Used' <<<"$out"; then v="$(grep -i 'Percentage Used' <<<"$out" | head -1 | sed 's/.*: *//' | tr -d ' %,')" if [[ "$v" =~ ^[0-9]+$ ]]; then if (( v >= 90 )); then verdict FAIL "NVMe $d" "Endurance used = ${v}% - at end of rated write life." elif (( v >= 70 )); then verdict WARN "NVMe $d" "Endurance used = ${v}% - plan replacement." else verdict OK "NVMe $d" "Endurance used = ${v}% of rated write life." fi fi v="$(grep -i 'Available Spare:' <<<"$out" | head -1 | sed 's/.*: *//' | tr -d ' %,')" [[ "$v" =~ ^[0-9]+$ ]] && (( v < 20 )) && verdict FAIL "NVMe $d" "Available spare blocks = ${v}% - critical." v="$(grep -i 'Media and Data Integrity Errors' <<<"$out" | head -1 | sed 's/.*: *//' | tr -d ' ,')" [[ "$v" =~ ^[0-9]+$ ]] && (( v > 0 )) && verdict FAIL "NVMe $d" "Media / data integrity errors = $v." v="$(grep -i 'Critical Warning' <<<"$out" | head -1 | sed 's/.*: *//' | tr -d ' ,')" [[ -n "$v" && "$v" != "0x00" && "$v" != "0" ]] && verdict FAIL "NVMe $d" "Controller critical warning flag set: $v." v="$(grep -iE 'Unsafe Shutdowns' <<<"$out" | head -1 | sed 's/.*: *//' | tr -d ' ,')" [[ "$v" =~ ^[0-9]+$ ]] && (( v > 100 )) && \ verdict INFO "NVMe $d" "Unsafe shutdowns = $v - frequent power loss without clean flush; check PSU and shutdown behaviour." fi done } analyze_selftest_age() { have smartctl || return local d key tp out for d in $(disks_list); do key="$(smart_key "$d")"; tp="$(cat "$SMARTDIR/$key.type" 2>/dev/null)" [[ "$tp" == "UNSUPPORTED" ]] && continue out="$(smartctl -l selftest -n standby,0 $tp "$d" 2>/dev/null)" if grep -qi 'No self-tests have been logged' <<<"$out"; then verdict INFO "SMART $d" "No self-test has ever been logged. Run 'sudo smartctl -t short $d' or use --stress." elif grep -qiE 'Completed: read failure|Completed: unknown failure|Completed: electrical|Completed: servo|Completed: handling damage' <<<"$out"; then verdict FAIL "SMART $d" "A logged SMART self-test FAILED - see the self-test history section." fi done } analyze_disk_space() { local flagged=0 fsys size used avail pct mnt class while read -r fsys size used avail pct mnt; do pct="${pct%\%}" [[ "$pct" =~ ^[0-9]+$ ]] || continue class="system" case "$mnt" in /media/*|/mnt/*|/run/media/*|/media) class="removable" ;; /snap/*|/var/snap/*|/var/lib/snapd/*) class="snap" ;; /boot|/boot/efi|/efi|/recovery) class="boot" ;; esac case "$class" in system) if (( pct >= 95 )); then verdict FAIL "Disk space" "$mnt is ${pct}% full (${avail} free) - critically low on a system filesystem."; flagged=1 elif (( pct >= 85 )); then verdict WARN "Disk space" "$mnt is ${pct}% full (${avail} free)."; flagged=1 fi ;; boot) if (( pct >= 95 )); then verdict FAIL "Disk space" "$mnt is ${pct}% full (${avail} free) - a full boot/EFI partition breaks kernel updates."; flagged=1 elif (( pct >= 80 )); then verdict WARN "Disk space" "$mnt is ${pct}% full (${avail} free) - small partition, clean out old kernels."; flagged=1 fi ;; removable) if (( pct >= 98 )); then verdict WARN "Removable media" "$mnt is ${pct}% full (${avail} free) - no room left for new backups."; flagged=1 elif (( pct >= 90 )); then verdict INFO "Removable media" "$mnt is ${pct}% full (${avail} free)." fi ;; snap) : ;; esac done < <(df -P -h -x tmpfs -x devtmpfs -x squashfs -x overlay -x efivarfs -x fuse.portal 2>/dev/null | tail -n +2) (( flagged == 0 )) && verdict OK "Disk space" "No system or boot filesystem is above its warning threshold." local ipct imnt while read -r fsys size used avail ipct imnt; do ipct="${ipct%\%}" [[ "$ipct" =~ ^[0-9]+$ ]] || continue (( ipct >= 90 )) && verdict WARN "Inodes" "$imnt has used ${ipct}% of its inodes." done < <(df -Pi -x tmpfs -x devtmpfs -x squashfs -x overlay 2>/dev/null | tail -n +2) local ro ro="$(findmnt -rn -o TARGET,FSTYPE,OPTIONS 2>/dev/null \ | awk '$3 ~ /(^|,)ro(,|$)/ \ && $2 !~ /^(squashfs|iso9660|cramfs|erofs|autofs|ramfs|efivarfs)/ \ && $2 !~ /^fuse/ \ && $1 !~ /^\/(snap|run\/credentials|var\/lib\/snapd|tmp\/\.mount_|sys|proc)/ {print $1}' \ | tr '\n' ' ')" if [[ -n "${ro// /}" ]]; then verdict WARN "Filesystems" "Real filesystems mounted read-only (possible errors=remount-ro after an I/O fault): $ro" else verdict OK "Filesystems" "No writable filesystem has been force-remounted read-only." fi local fserr fserr="$(dmesg 2>/dev/null | grep -icE 'EXT4-fs error|XFS.*(corruption|error)|BTRFS.*(error|csum)|Remounting filesystem read-only' || true)" [[ "$fserr" =~ ^[0-9]+$ ]] && (( fserr > 0 )) && \ verdict FAIL "Filesystems" "$fserr filesystem corruption/error messages in the kernel log - run fsck from a live environment." } analyze_trim() { local ssd=0 d n for d in $(disks_list); do n="${d#/dev/}" [[ "$(cat "/sys/block/$n/queue/rotational" 2>/dev/null)" == "0" ]] && ssd=1 done (( ssd == 0 )) && return if have systemctl; then if systemctl is-enabled fstrim.timer >/dev/null 2>&1; then verdict OK "TRIM" "SSDs present and fstrim.timer is enabled (weekly discard)." else verdict WARN "TRIM" "SSDs are present but fstrim.timer is not enabled. Run: sudo systemctl enable --now fstrim.timer" fi fi } analyze_battery() { local b found=0 for b in /sys/class/power_supply/*; do [[ -r "$b/type" ]] || continue [[ "$(cat "$b/type" 2>/dev/null)" == "Battery" ]] || continue found=1 local name full design pct cyc st cap name="$(basename "$b")" if [[ -r "$b/energy_full" ]]; then full="$(cat "$b/energy_full")"; design="$(cat "$b/energy_full_design" 2>/dev/null || echo 0)" elif [[ -r "$b/charge_full" ]]; then full="$(cat "$b/charge_full")"; design="$(cat "$b/charge_full_design" 2>/dev/null || echo 0)" else full=0; design=0; fi st="$(cat "$b/status" 2>/dev/null || echo unknown)" cap="$(cat "$b/capacity" 2>/dev/null || echo '?')" cyc="$(cat "$b/cycle_count" 2>/dev/null || echo '?')" if [[ "$full" =~ ^[0-9]+$ && "$design" =~ ^[0-9]+$ ]] && (( design > 0 )); then pct=$(( full * 100 / design )) if (( pct >= 80 )); then verdict OK "Battery $name" "Health ${pct}% of design capacity (charge ${cap}%, ${st}, ${cyc} cycles)." elif (( pct >= 60 )); then verdict WARN "Battery $name" "Health ${pct}% of design capacity - noticeable wear (charge ${cap}%, ${cyc} cycles)." else verdict FAIL "Battery $name" "Health ${pct}% of design capacity - worn out, consider replacement (${cyc} cycles)." fi else verdict INFO "Battery $name" "Present (charge ${cap}%, ${st}) but design capacity is not reported; wear cannot be computed." fi [[ "$cyc" =~ ^[0-9]+$ ]] && (( cyc > 1000 )) && verdict WARN "Battery $name" "High cycle count: ${cyc}." done (( found == 0 )) && verdict INFO "Battery" "No battery present - desktop or server." } analyze_temps() { local max=-1 src="" v t z if have sensors; then while read -r v; do [[ "$v" =~ ^-?[0-9]+ ]] || continue t="${v%%.*}" (( t > max )) && { max="$t"; src="lm-sensors"; } done < <(sensors -u 2>/dev/null | awk -F: '/temp[0-9]*_input/ {gsub(/ /,"",$2); print $2}') fi for z in /sys/class/thermal/thermal_zone*/temp; do [[ -r "$z" ]] || continue v="$(cat "$z" 2>/dev/null)" [[ "$v" =~ ^[0-9]+$ ]] || continue t=$(( v / 1000 )) (( t > max )) && { max="$t"; src="$(cat "$(dirname "$z")/type" 2>/dev/null)"; } done if (( max < 0 )); then verdict INFO "Temperature" "No temperature sensors readable (try --sensors-detect, or this is a VM)." elif (( max >= 95 )); then verdict FAIL "Temperature" "Peak sensor reading ${max} C (${src}) - at or near thermal shutdown. Check fans, dust and thermal paste now." elif (( max >= 85 )); then verdict WARN "Temperature" "Peak sensor reading ${max} C (${src}) - running hot." else verdict OK "Temperature" "Peak sensor reading ${max} C (${src}) - within normal range." fi local thr thr="$(throttle_total)" if [[ "$thr" =~ ^[0-9]+$ ]]; then if (( thr > 1000 )); then verdict WARN "Thermal throttling" "$thr throttle events recorded by the CPU - sustained cooling or power-limit constraint." elif (( thr > 0 )); then verdict INFO "Thermal throttling" "$thr throttle events recorded since boot (brief turbo limiting is normal)." else verdict OK "Thermal throttling" "Zero CPU throttle events since boot." fi fi # Fan sanity: a chip reporting >60C whose own fan reads 0 RPM local c n f rpm temps for c in /sys/class/hwmon/hwmon*; do [[ -d "$c" ]] || continue n="$(cat "$c/name" 2>/dev/null)" temps="$(cat "$c"/temp*_input 2>/dev/null | sort -rn | head -1)" [[ "$temps" =~ ^[0-9]+$ ]] || continue (( temps / 1000 < 60 )) && continue for f in "$c"/fan*_input; do [[ -r "$f" ]] || continue rpm="$(cat "$f" 2>/dev/null)" [[ "$rpm" == "0" ]] && verdict WARN "Cooling" "hwmon '$n' reports $(( temps / 1000 )) C while its fan tachometer reads 0 RPM - fan stopped, unplugged, or not reporting." done done } analyze_memory() { local total avail pct swtotal swused total="$(awk '/^MemTotal:/{print $2}' /proc/meminfo 2>/dev/null)" avail="$(awk '/^MemAvailable:/{print $2}' /proc/meminfo 2>/dev/null)" if [[ "$total" =~ ^[0-9]+$ && "$avail" =~ ^[0-9]+$ && $total -gt 0 ]]; then pct=$(( avail * 100 / total )) if (( pct < 5 )); then verdict FAIL "Memory" "Only ${pct}% of RAM available ($((avail/1024)) MB of $((total/1024)) MB) - severe pressure." elif (( pct < 15 )); then verdict WARN "Memory" "Only ${pct}% of RAM available ($((avail/1024)) MB of $((total/1024)) MB)." else verdict OK "Memory" "${pct}% of RAM available ($((avail/1024)) MB of $((total/1024)) MB)." fi fi swtotal="$(awk '/^SwapTotal:/{print $2}' /proc/meminfo 2>/dev/null)" if [[ "$swtotal" =~ ^[0-9]+$ && $swtotal -gt 0 ]]; then swused=$(( swtotal - $(awk '/^SwapFree:/{print $2}' /proc/meminfo) )) pct=$(( swused * 100 / swtotal )) if (( pct > 60 )); then verdict WARN "Swap" "Swap is ${pct}% used - the system is short on RAM." else verdict OK "Swap" "Swap ${pct}% used of $((swtotal/1024)) MB." fi else verdict INFO "Swap" "No swap space configured." fi local ce=0 ue=0 f for f in /sys/devices/system/edac/mc/mc*/ce_count; do [[ -r "$f" ]] && ce=$(( ce + $(cat "$f" 2>/dev/null || echo 0) )); done for f in /sys/devices/system/edac/mc/mc*/ue_count; do [[ -r "$f" ]] && ue=$(( ue + $(cat "$f" 2>/dev/null || echo 0) )); done if (( ue > 0 )); then verdict FAIL "ECC memory" "$ue uncorrectable ECC errors reported by EDAC - a DIMM is failing." elif (( ce > 0 )); then verdict WARN "ECC memory" "$ce correctable ECC errors reported by EDAC - watch for a failing DIMM." fi if have dmidecode; then local ecc ecc="$(dmidecode -t 16 2>/dev/null | grep -i 'Error Correction Type' | head -1 | sed 's/.*: *//')" [[ -n "$ecc" ]] && verdict INFO "Memory type" "Memory array error-correction type: ${ecc}." fi local oom oom="$(dmesg 2>/dev/null | grep -icE 'out of memory: kill|oom-killer' || true)" [[ "$oom" =~ ^[0-9]+$ ]] && (( oom > 0 )) && verdict WARN "Memory" "$oom OOM-killer events in the kernel ring buffer." } analyze_cpu_load() { local cores l1 int cores="$(nproc 2>/dev/null || echo 1)" l1="$(awk '{print $1}' /proc/loadavg 2>/dev/null)" int="${l1%%.*}" [[ "$int" =~ ^[0-9]+$ ]] || return if (( int > cores * 2 )); then verdict WARN "CPU load" "1-min load ${l1} on ${cores} logical CPUs - heavily loaded; benchmark numbers will be unreliable." else verdict OK "CPU load" "1-min load ${l1} on ${cores} logical CPUs." fi local vlist vcount vlist="$(grep -rl 'Vulnerable' /sys/devices/system/cpu/vulnerabilities/ 2>/dev/null | xargs -r -n1 basename | tr '\n' ' ')" vcount="$(wc -w <<<"$vlist")" if [[ "$vcount" =~ ^[0-9]+$ ]] && (( vcount > 0 )); then local ucode_pkg="" have dpkg && dpkg -l intel-microcode amd64-microcode 2>/dev/null | grep -q '^ii' && ucode_pkg="installed" if [[ -z "$ucode_pkg" ]]; then verdict WARN "CPU security" "$vcount unmitigated vulnerabilities (${vlist% }) and NO microcode package is installed. Install intel-microcode or amd64-microcode." else verdict WARN "CPU security" "$vcount unmitigated vulnerabilities (${vlist% }). Microcode package is installed, so a newer CPU microcode or kernel may still be pending - check for updates and reboot." fi else verdict OK "CPU security" "All known CPU vulnerabilities report mitigated or not-affected." fi local mce mce="$(dmesg 2>/dev/null | grep -icE 'machine check|\bmce:|hardware error' || true)" [[ "$mce" =~ ^[0-9]+$ ]] && (( mce > 0 )) && verdict FAIL "CPU/MCE" "$mce machine-check / hardware-error messages in the kernel log - investigate immediately." local t t="$(cat /proc/sys/kernel/tainted 2>/dev/null || echo 0)" if [[ "$t" =~ ^[0-9]+$ ]] && (( t > 0 )); then local hw="" (( t & 16 )) && hw="${hw}machine-check " (( t & 32 )) && hw="${hw}bad-page " (( t & 128 )) && hw="${hw}oops " (( t & 16384 )) && hw="${hw}soft-lockup " if [[ -n "$hw" ]]; then verdict FAIL "Kernel taint" "Taint value $t includes hardware/stability flags: ${hw%% }. See the taint decode section." else verdict INFO "Kernel taint" "Taint value $t is from out-of-tree/unsigned modules or user actions only - not a hardware signal." fi else verdict OK "Kernel taint" "Kernel is not tainted." fi } analyze_pcie() { local ce=0 ue=0 d n sum for d in /sys/bus/pci/devices/*; do for n in aer_dev_correctable; do [[ -r "$d/$n" ]] || continue sum="$(awk '{s+=$2} END{print s+0}' "$d/$n" 2>/dev/null)" [[ "$sum" =~ ^[0-9]+$ ]] && ce=$((ce+sum)) done for n in aer_dev_fatal aer_dev_nonfatal; do [[ -r "$d/$n" ]] || continue sum="$(awk '{s+=$2} END{print s+0}' "$d/$n" 2>/dev/null)" [[ "$sum" =~ ^[0-9]+$ ]] && ue=$((ue+sum)) done done if (( ue > 0 )); then verdict FAIL "PCIe" "$ue fatal/non-fatal PCIe AER errors - a card, riser or slot is failing." elif (( ce > 1000 )); then verdict WARN "PCIe" "$ce correctable PCIe AER errors - high rate; reseat the card or check the riser." elif (( ce > 0 )); then verdict INFO "PCIe" "$ce correctable PCIe AER errors (auto-recovered; low counts are normal)." else verdict OK "PCIe" "No PCIe AER errors recorded." fi } analyze_kernel_logs() { local total distinct dd topline topcount topmsg total="$(dmesg -l err,crit,alert,emerg 2>/dev/null | wc -l)" [[ "$total" =~ ^[0-9]+$ ]] || total=0 if (( total == 0 )); then verdict OK "Kernel log" "No error-level kernel messages this boot." else dd="$(dmesg_dedup 'err,crit,alert,emerg')" distinct="$(grep -c . <<<"$dd")" topline="$(head -1 <<<"$dd")" topcount="$(awk '{print $1}' <<<"$topline")" topmsg="$(sed -E 's/^[[:space:]]*[0-9]+[[:space:]]+//' <<<"$topline" | cut -c1-110)" if (( distinct <= 3 )); then verdict WARN "Kernel log" "$total error messages but only $distinct distinct pattern(s) - one repeating fault. Most frequent (x${topcount}): \"${topmsg}\"" elif (( total > 25 )); then verdict WARN "Kernel log" "$total error messages across $distinct distinct patterns. Most frequent (x${topcount}): \"${topmsg}\"" else verdict INFO "Kernel log" "$total error messages across $distinct distinct patterns. Most frequent (x${topcount}): \"${topmsg}\"" fi fi local io io="$(dmesg 2>/dev/null | grep -icE 'I/O error|blk_update_request|buffer I/O error|medium error' || true)" [[ "$io" =~ ^[0-9]+$ ]] && (( io > 0 )) && \ verdict FAIL "Storage I/O" "$io block-layer I/O error messages - a disk, cable or controller is failing." # ATA: separate genuine faults from spin-up-after-resume chatter local resumed=0 ata_slow ata_real dmesg 2>/dev/null | grep -qiE 'PM: suspend exit|Waking up from system sleep' && resumed=1 ata_slow="$(dmesg 2>/dev/null | grep -ci 'link is slow to respond' || true)" ata_real="$(dmesg 2>/dev/null | grep -iE 'ata[0-9]+[.:].*(failed command|exception Emask|SError|hard resetting link|COMRESET failed)' | grep -civ 'link is slow to respond' || true)" [[ "$ata_slow" =~ ^[0-9]+$ ]] || ata_slow=0 [[ "$ata_real" =~ ^[0-9]+$ ]] || ata_real=0 if (( ata_real > 0 )); then verdict WARN "SATA/ATA link" "$ata_real genuine ATA exceptions (failed commands / link resets) - check cabling and drive health." fi if (( ata_slow > 0 )); then if (( resumed == 1 )); then verdict INFO "SATA/ATA link" "$ata_slow 'link is slow to respond' messages, and this boot includes a resume from sleep - consistent with a spinning disk waking up, not a fault." else verdict WARN "SATA/ATA link" "$ata_slow 'link is slow to respond' messages with no suspend/resume in this boot - check the SATA cable and drive." fi fi if (( ata_real == 0 && ata_slow == 0 )); then verdict OK "SATA/ATA link" "No ATA link errors this boot." fi local usbc usbmsg usbc="$(dmesg 2>/dev/null | grep -icE 'usb .*(device descriptor read|Cannot enable|disconnect, reason|error -71|error -110|unable to enumerate)' || true)" if [[ "$usbc" =~ ^[0-9]+$ ]] && (( usbc > 0 )); then usbmsg="$(dmesg 2>/dev/null | grep -iE 'usb .*(device descriptor read|Cannot enable|error -71|error -110|unable to enumerate)' \ | sed -E 's/^\[[^]]*\][[:space:]]*//; s/[0-9]+/N/g' | sort | uniq -c | sort -rn | head -1 \ | sed -E 's/^[[:space:]]*[0-9]+[[:space:]]+//' | cut -c1-100)" if (( usbc > 100 )); then verdict WARN "USB" "$usbc USB enumeration errors, dominated by: \"${usbmsg}\". A device or port is repeatedly failing to come up - unplug devices one at a time to isolate, or check for a shorted/damaged port." else verdict INFO "USB" "$usbc USB enumeration messages (top: \"${usbmsg}\") - usually a flaky cable, hub or port." fi fi } analyze_segfaults() { local total grp topcount topobj distinct total="$(journalctl -b --no-pager 2>/dev/null | grep -ci 'segfault' || true)" [[ "$total" =~ ^[0-9]+$ ]] || total=0 (( total == 0 )) && { verdict OK "Userspace" "No segfaults logged this boot."; return; } grp="$(seg_summary)" distinct="$(grep -c . <<<"$grp")" topcount="$(head -1 <<<"$grp" | awk '{print $1}')" topobj="$(head -1 <<<"$grp" | awk '{print $2}')" [[ "$topcount" =~ ^[0-9]+$ ]] || topcount=0 if (( total >= 5 && distinct <= 2 )) || { (( total >= 5 )) && (( topcount * 100 / total >= 80 )); }; then verdict INFO "Userspace" "$total segfaults this boot, $topcount of them inside '${topobj}'. Concentrated in one component - a software bug, not failing RAM." elif (( total > 10 )); then verdict WARN "Userspace" "$total segfaults across $distinct distinct binaries (top: ${topobj} x${topcount}) - scattered crashes can indicate memory errors. Consider --stress or memtest86+." else verdict INFO "Userspace" "$total segfaults this boot across $distinct binaries (top: ${topobj})." fi } analyze_services() { have systemctl || return local n names n="$(systemctl --failed --no-legend 2>/dev/null | grep -c . || true)" [[ "$n" =~ ^[0-9]+$ ]] || n=0 if (( n > 0 )); then names="$(systemctl --failed --no-legend 2>/dev/null | awk '{print $1}' | tr '\n' ' ')" verdict WARN "systemd" "$n failed unit(s): ${names% }" else verdict OK "systemd" "No failed units." fi } analyze_network() { local i up=0 down="" for i in /sys/class/net/*; do i="$(basename "$i")" [[ "$i" == "lo" ]] && continue case "$(cat "/sys/class/net/$i/operstate" 2>/dev/null)" in up) up=$((up+1)) ;; *) down="$down $i" ;; esac done if (( up > 0 )); then verdict OK "Network" "$up interface(s) up.${down:+ Down/idle:$down}" else verdict WARN "Network" "No network interface is up.${down:+ Down:$down}" fi if have ethtool; then for i in /sys/class/net/*; do i="$(basename "$i")" [[ "$i" == "lo" ]] && continue [[ "$(cat "/sys/class/net/$i/operstate" 2>/dev/null)" == "up" ]] || continue local errs errs="$(ethtool -S "$i" 2>/dev/null | grep -iE 'crc|rx_error|tx_error|align_error' \ | awk -F: '{gsub(/ /,"",$2); s+=$2} END{print s+0}')" [[ "$errs" =~ ^[0-9]+$ ]] && (( errs > 0 )) && \ verdict WARN "Network $i" "$errs CRC/alignment/PHY errors on the wire - suspect the cable or the switch port." done fi if have ping; then if ping -c1 -W3 1.1.1.1 >/dev/null 2>&1; then verdict OK "Connectivity" "IPv4 reachability to 1.1.1.1 confirmed." else verdict WARN "Connectivity" "Cannot reach 1.1.1.1 (offline, filtered, or routing problem)."; fi fi } analyze_firmware_age() { have dmidecode || return local d ts now age yrs vendor ver d="$(dmidecode -s bios-release-date 2>/dev/null | head -1)" ver="$(dmidecode -s bios-version 2>/dev/null | head -1)" vendor="$(dmidecode -s system-manufacturer 2>/dev/null | head -1)" [[ -z "$d" ]] && return ts="$(date -d "$d" +%s 2>/dev/null)" [[ "$ts" =~ ^[0-9]+$ ]] || return now="$(date +%s)" age=$(( (now - ts) / 86400 )) yrs=$(( age / 365 )) local extra="" case "$vendor" in *System76*) extra=" Note: System76 ships firmware through system76-firmware-daemon, not LVFS, so fwupdmgr will report no updates. Check with: sudo system76-firmware-cli schedule" ;; *Dell*) extra=" Dell publishes to LVFS; fwupdmgr should see updates." ;; *Lenovo*) extra=" Lenovo publishes to LVFS; fwupdmgr should see updates." ;; esac if (( age > 1460 )); then verdict WARN "Firmware age" "BIOS/UEFI version ${ver} is dated ${d} (~${yrs} years old). Missing several years of microcode and errata fixes.${extra}" elif (( age > 730 )); then verdict INFO "Firmware age" "BIOS/UEFI version ${ver} is dated ${d} (~${yrs} years old). Worth checking for an update.${extra}" else verdict OK "Firmware age" "BIOS/UEFI version ${ver} dated ${d} - reasonably current." fi } analyze_updates() { [[ -f /var/run/reboot-required ]] && verdict WARN "Updates" "A reboot is required to finish applying updates." if have apt-get; then local n sec n="$(apt-get -s -o Debug::NoLocking=1 upgrade 2>/dev/null | grep -c '^Inst ' || true)" sec="$(apt-get -s -o Debug::NoLocking=1 upgrade 2>/dev/null | grep '^Inst ' | grep -ci security || true)" [[ "$n" =~ ^[0-9]+$ ]] || n=0 [[ "$sec" =~ ^[0-9]+$ ]] || sec=0 if (( sec > 0 )); then verdict WARN "Updates" "$n package upgrade(s) available, $sec of them from a security pocket." elif (( n > 0 )); then verdict INFO "Updates" "$n package upgrade(s) available." else verdict OK "Updates" "No pending package upgrades." fi fi if have dpkg; then local b b="$(dpkg -C 2>/dev/null | grep -c '^ ' || true)" [[ "$b" =~ ^[0-9]+$ ]] && (( b > 0 )) && verdict WARN "Packages" "dpkg reports partially installed or broken packages." fi } analyze_logs_disk() { local ju crash if have journalctl; then ju="$(journalctl --disk-usage 2>/dev/null | grep -oE '[0-9.]+[KMG]' | tail -1)" [[ -n "$ju" ]] && case "$ju" in *G) local g="${ju%G}"; (( ${g%%.*} >= 2 )) && verdict INFO "Logs" "systemd journal is using ${ju}. Trim with: sudo journalctl --vacuum-size=500M" ;; esac fi if [[ -d /var/crash ]]; then crash="$(du -sm /var/crash 2>/dev/null | awk '{print $1}')" [[ "$crash" =~ ^[0-9]+$ ]] && (( crash > 200 )) && \ verdict INFO "Crash dumps" "/var/crash holds ${crash} MB of crash reports. Safe to clear: sudo rm -rf /var/crash/*" fi } #=============================================================================# # HTML OUTPUT # #=============================================================================# emit_head() { local overall="HEALTHY" ocls="ok" if (( F_FAIL > 0 )); then overall="ACTION REQUIRED"; ocls="fail" elif (( F_WARN > 0 )); then overall="ATTENTION ADVISED"; ocls="warn"; fi local dur=$(( $(date +%s) - START_EPOCH )) local sanbadge="" [[ $DO_SANITIZE -eq 1 ]] && sanbadge='SANITIZED' cat < goat-check - ${SAFE_HOST} - ${STAMP}
Saves this complete report as a single self-contained .html file wherever you choose.

goat-check Hardware Diagnostics ${overall}${sanbadge}

Host ${SAFE_HOST} · $(date '+%Y-%m-%d %H:%M:%S %Z') · kernel $(uname -r) · runtime ${dur}s · v${VERSION}
Findings FAIL: ${F_FAIL} WARN: ${F_WARN} OK: ${F_OK} INFO: ${F_INFO} Checks run: ${CHECK_TOTAL} completed ${C_OK} problems ${C_WARN} skipped ${C_SKIP}
' echo '

Findings summary

' echo '
Click any finding name to jump to the evidence section it came from.
' echo '' if [[ -s "$FINDINGS" ]]; then cat "$FINDINGS"; else echo ''; fi echo '
No findings were generated.
' } emit_tail() { local sannote="" if [[ $DO_SANITIZE -eq 1 ]]; then sannote='

Sanitized report. Serial numbers, MAC addresses, UUIDs, WWNs, IP addresses, SSIDs, machine/boot IDs, the hostname and the username have been redacted. Redaction is pattern-based and best-effort - review before publishing, especially free-text log excerpts.

' fi cat < ${sannote}

Generated by goat-check.sh v${VERSION}. Evidence blocks are raw tool output; the findings table applies heuristics (SMART thresholds, NVMe endurance, battery wear, temperature and throttle limits, ECC and PCIe AER counters, deduplicated log patterns). A FAIL on SMART, ECC, media errors or filesystem corruption should be treated as urgent - back up data first.

Repeating kernel messages are collapsed to unique patterns before being counted, so a single fault that logs thousands of lines is reported as one finding rather than thousands.

↑ Top HTMLTAIL } FINALIZED=0 finalize() { [[ $FINALIZED -eq 1 ]] && return FINALIZED=1 close_category { emit_head; cat "$BODY"; emit_tail; } > "$REPORT" 2>/dev/null ln -sfn "$REPORT" "$LATEST" 2>/dev/null chown "$REAL_USER":"$REAL_USER" "$REPORT" 2>/dev/null chown -h "$REAL_USER":"$REAL_USER" "$LATEST" 2>/dev/null chmod 644 "$REPORT" 2>/dev/null rm -rf "$TMPD" 2>/dev/null local overall ocol if (( F_FAIL > 0 )); then overall="ACTION REQUIRED"; ocol="$C_RED" elif (( F_WARN > 0 )); then overall="ATTENTION ADVISED"; ocol="$C_YEL" else overall="HEALTHY"; ocol="$C_GRN"; fi echo printf '%s\n' "${C_BOLD}${C_WHT}================================ SUMMARY ================================${C_RESET}" printf ' Overall verdict : %s%s%s\n' "$ocol$C_BOLD" "$overall" "$C_RESET" printf ' Findings : %sFAIL %s%s %sWARN %s%s %sOK %s%s %sINFO %s%s\n' \ "$C_RED" "$F_FAIL" "$C_RESET" "$C_YEL" "$F_WARN" "$C_RESET" "$C_GRN" "$F_OK" "$C_RESET" "$C_CYA" "$F_INFO" "$C_RESET" printf ' Checks executed : %s total (%s%s ok%s, %s%s problem%s, %s%s skipped%s)\n' \ "$CHECK_TOTAL" "$C_GRN" "$C_OK" "$C_RESET" "$C_YEL" "$C_WARN" "$C_RESET" "$C_GRY" "$C_SKIP" "$C_RESET" printf ' Elapsed : %s seconds\n' "$(( $(date +%s) - START_EPOCH ))" [[ $DO_SANITIZE -eq 1 ]] && printf ' Sanitization : %sENABLED%s - serials, MACs, UUIDs, IPs, hostname and username redacted\n' "$C_CYA$C_BOLD" "$C_RESET" printf '%s\n' "${C_BOLD}${C_WHT}=========================================================================${C_RESET}" echo printf '%s\n' "${C_GRN}${C_BOLD}##########################################################################${C_RESET}" printf '%s\n' "${C_GRN}${C_BOLD}# HTML REPORT IS READY #${C_RESET}" printf '%s\n' "${C_GRN}${C_BOLD}##########################################################################${C_RESET}" echo printf ' %sReport file:%s %s%s%s\n' "$C_BOLD" "$C_RESET" "$C_CYA$C_BOLD" "$REPORT" "$C_RESET" printf ' %sAlways-latest symlink:%s %s\n' "$C_DIM" "$C_RESET" "$LATEST" echo printf ' %sOpen it in Firefox with:%s\n' "$C_BOLD" "$C_RESET" echo printf ' %s%sfirefox "file://%s"%s\n' "$C_YEL" "$C_BOLD" "$REPORT" "$C_RESET" echo printf ' %s(alternatives: xdg-open "%s" | firefox "file://%s")%s\n' "$C_DIM" "$REPORT" "$LATEST" "$C_RESET" printf ' %sInside the report, the green "Download report" button (top right) saves a copy anywhere.%s\n' "$C_DIM" "$C_RESET" echo printf '%s\n' "${C_GRN}${C_BOLD}##########################################################################${C_RESET}" echo if [[ $DO_OPEN -eq 1 ]] && have firefox; then info "Launching Firefox as ${REAL_USER}..." as_user nohup firefox "file://$REPORT" >/dev/null 2>&1 & fi } trap 'echo; warn "Interrupted - writing the partial report..."; finalize; exit 130' INT TERM trap 'finalize' EXIT #=============================================================================# # PHASE 1: TOOLING # #=============================================================================# banner "goat-check v${VERSION} - full hardware health diagnostics" info "Host : $HOSTN" info "Kernel : $(uname -r)" info "Date : $(date)" info "Report file : $REPORT" info "Mode : $([[ $DO_STRESS -eq 1 ]] && echo 'FULL + stress/benchmark tests' || echo 'standard (use --stress for load tests)')" info "Sanitize : $([[ $DO_SANITIZE -eq 1 ]] && echo 'ENABLED - identifying data will be redacted' || echo 'disabled (use --sanitize before sharing)')" info "Per-check timeout: ${TIMEOUT}s" # hddtemp removed: gone from Ubuntu 22.04+, superseded by the drivetemp module. PKGS=( smartmontools nvme-cli hdparm sdparm lsscsi util-linux parted gdisk lm-sensors fancontrol dmidecode lshw pciutils usbutils hwinfo inxi cpuid procps psmisc sysstat iotop memtester stress-ng sysbench fio upower acpi acpitool powertop ethtool iproute2 net-tools dnsutils iputils-ping curl wget wireless-tools iw mdadm lvm2 xfsprogs e2fsprogs btrfs-progs cryptsetup-bin fwupd rasdaemon edac-utils alsa-utils pulseaudio-utils systemd-coredump bc gawk file jq mesa-utils vainfo ) if [[ $DO_INSTALL -eq 1 ]]; then step "PHASE 1/3 - installing diagnostic tooling" export DEBIAN_FRONTEND=noninteractive if [[ ${EUID:-$(id -u)} -ne 0 ]]; then warn "Not root - skipping package installation." elif ! have apt-get; then warn "apt-get not found - this does not look like a Debian-based system. Skipping installation." else info "Refreshing package lists (apt-get update)..." if apt-get update -qq 2>&1 | tail -n 3; then ok "Package lists updated." else warn "apt-get update reported problems - continuing with cached lists."; fi info "Checking which of the ${#PKGS[@]} packages exist in this release's archive..." AVAIL=(); UNAVAIL=() for p in "${PKGS[@]}"; do if apt-cache show "$p" >/dev/null 2>&1; then AVAIL+=("$p"); else UNAVAIL+=("$p"); fi done if (( ${#UNAVAIL[@]} > 0 )); then warn "Not in this release's archive, will be skipped: ${UNAVAIL[*]}" fi ok "${#AVAIL[@]} package(s) available for installation." info "Installing (single batch)..." if apt-get install -y -qq --no-install-recommends "${AVAIL[@]}" >/dev/null 2>&1; then ok "All available packages installed successfully in one pass." else warn "Batch install failed - falling back to per-package installation." FAILED=() for p in "${AVAIL[@]}"; do if dpkg -s "$p" >/dev/null 2>&1; then printf ' %s[have]%s %s\n' "$C_GRY" "$C_RESET" "$p" elif apt-get install -y -qq --no-install-recommends "$p" >/dev/null 2>&1; then printf ' %s[ ok ]%s %s\n' "$C_GRN" "$C_RESET" "$p" else printf ' %s[fail]%s %s\n' "$C_YEL" "$C_RESET" "$p"; FAILED+=("$p") fi done (( ${#FAILED[@]} > 0 )) && warn "Failed to install: ${FAILED[*]}" || ok "All packages present after fallback pass." fi fi else step "PHASE 1/3 - package installation skipped (--no-install)" fi if [[ $DO_MODPROBE -eq 1 && ${EUID:-$(id -u)} -eq 0 ]]; then info "Loading common sensor modules (non-persistent)..." for m in coretemp k10temp drivetemp nct6775 it87 acpi_power_meter; do modprobe -q "$m" 2>/dev/null && printf ' %s[loaded]%s %s\n' "$C_GRN" "$C_RESET" "$m" done fi if [[ $DO_SENSORS_DETECT -eq 1 ]] && have sensors-detect; then info "Running sensors-detect --auto (this writes /etc/modules)..." yes '' | sensors-detect --auto >/dev/null 2>&1 && ok "sensors-detect finished." || warn "sensors-detect failed." have systemctl && systemctl restart kmod 2>/dev/null fi have rasdaemon && systemctl start rasdaemon 2>/dev/null if [[ $DO_SANITIZE -eq 1 ]]; then info "Building sanitization ruleset from this machine's identifiers..." build_sanitize_rules ok "$(grep -c . "$SAN_SED" 2>/dev/null || echo 0) redaction rules active." fi if have smartctl; then info "Probing storage devices for a working SMART access method (USB bridges included)..." smart_cache for d in $(disks_list); do t="$(smart_type_of "$d")" if [[ "$t" == "UNSUPPORTED" ]]; then printf ' %s[none]%s %s - no SMART access method found\n' "$C_YEL" "$C_RESET" "$d" else printf ' %s[ ok ]%s %s via %s\n' "$C_GRN" "$C_RESET" "$d" "${t:-auto-detect}" fi done fi step "PHASE 2/3 - running diagnostics" #=============================================================================# # PHASE 2: DIAGNOSTICS # #=============================================================================# #--------------------------------------------------------- 1. SYSTEM OVERVIEW - category "System Overview & Identity" run_check "Operating system release" - 'cat /etc/os-release; echo; lsb_release -a 2>/dev/null' run_check "Host, kernel & architecture" - 'hostnamectl 2>/dev/null; echo; uname -a; echo; cat /proc/version' run_check "Uptime, load average & users" - 'uptime; echo; cat /proc/loadavg; echo; who -a 2>/dev/null | head -n 20' run_check "Virtualization detection" - 'systemd-detect-virt 2>/dev/null || true; grep -iE "hypervisor" /proc/cpuinfo | head -1 | none_if_empty "No hypervisor CPU flag - running on bare metal."' run_check "DMI: system / baseboard / chassis" dmidecode 'dmidecode -t system -t baseboard -t chassis' run_check "DMI: BIOS / firmware" dmidecode 'dmidecode -t bios' run_check "Hardware inventory (lshw)" lshw 'lshw -short' run_check "Full hardware detail (lshw)" lshw 'lshw -sanitize' 180 run_check "System summary (inxi)" inxi 'inxi -Fxxxz --no-host -c 0' 120 run_check "Boot history" - 'journalctl --list-boots --no-pager 2>/dev/null | tail -n 15; echo; last -x reboot 2>/dev/null | head -n 15' #-------------------------------------------------------------------- 2. CPU -- category "CPU / Processor" run_check "CPU topology & features (lscpu)" lscpu 'lscpu; echo; lscpu -e 2>/dev/null' run_check "CPU frequency & governors" - 'cpufreq_report' run_check "CPU details from DMI" dmidecode 'dmidecode -t processor' run_check "Speculative-execution vulnerabilities" - 'grep -r . /sys/devices/system/cpu/vulnerabilities/ 2>/dev/null | sed "s|/sys/devices/system/cpu/vulnerabilities/||" | sort' run_check "Microcode revision & currency" - 'microcode_report' run_check "Thermal & power throttle counters" - 'throttle_report' run_check "Machine-check / hardware errors in log" - 'dmesg -T 2>/dev/null | grep -iE "machine check|\bmce\b|hardware error|thermal throttl|temperature above" | tail -n 40 | none_if_empty "No machine-check or hardware-error messages this boot."' run_check "Top CPU consumers" ps 'ps -eo pid,user,pcpu,pmem,etime,stat,comm --sort=-pcpu | head -n 16' run_check "CPU statistics (mpstat)" mpstat 'mpstat -P ALL 1 3' run_check "Interrupts & context switches" - 'vmstat 1 3; echo; head -n 25 /proc/interrupts' if [[ $DO_STRESS -eq 1 ]]; then run_check "STRESS: all-core CPU load (30s)" stress-ng 'stress-ng --cpu 0 --cpu-method matrixprod --timeout 30s --metrics-brief --times' 120 run_check "BENCH: sysbench CPU (single + multi)" sysbench 'sysbench cpu --cpu-max-prime=20000 --threads=1 run | tail -n 20; echo; sysbench cpu --cpu-max-prime=20000 --threads=$(nproc) run | tail -n 20' 180 run_check "Post-load temperatures" sensors 'sensors' run_check "Post-load throttle counters" - 'throttle_report' fi analyze_cpu_load #----------------------------------------------------------------- 3. MEMORY -- category "Memory (RAM / Swap / ECC)" run_check "Memory usage overview" - 'free -h; echo; free -m' run_check "/proc/meminfo" - 'cat /proc/meminfo' run_check "Installed DIMMs (DMI type 17)" dmidecode 'dmidecode -t memory' run_check "Memory array capabilities" dmidecode 'dmidecode -t 16' run_check "Swap configuration" - 'swapon --show 2>/dev/null; echo; cat /proc/swaps; echo; echo "vm.swappiness = $(cat /proc/sys/vm/swappiness 2>/dev/null)"' run_check "ECC / EDAC error counters" - 'edac_report' run_check "OOM-killer history" - 'dmesg -T 2>/dev/null | grep -iE "out of memory|oom-killer|killed process" | tail -n 25 | none_if_empty "No OOM-killer events this boot."' run_check "Top memory consumers" ps 'ps -eo pid,user,pmem,rss,vsz,etime,comm --sort=-pmem | head -n 16' run_check "Memory pressure / slab summary" - 'vmstat -s 2>/dev/null | head -n 25; echo; slabtop -o 2>/dev/null | head -n 15' if [[ $DO_STRESS -eq 1 ]]; then run_check "STRESS: memtester (guarded)" memtester "memtester_guarded ${MEMTEST_SIZE}" 900 run_check "STRESS: stress-ng VM stressor (guarded)" stress-ng 'mem_stress' 150 fi analyze_memory #--------------------------------------------------- 4. STORAGE & FILESYSTEMS - category "Storage: Disks, SMART & Filesystems" run_check "Block device tree" lsblk 'lsblk -o NAME,TYPE,SIZE,FSTYPE,LABEL,MOUNTPOINT,ROTA,TRAN,MODEL,SERIAL' run_check "SMART health summary (all drives)" smartctl 'smart_summary' run_check "SMART device scan" smartctl 'smartctl --scan-open' run_check "SMART full attribute dump" smartctl 'smart_report' 240 run_check "SMART self-test history" smartctl 'smart_selftest_log' run_check "SMART device statistics" smartctl 'smart_devstat' run_check "NVMe controller & health logs" nvme 'nvme_report' 180 run_check "Disk parameters (hdparm -I)" hdparm 'for d in $(disks_list); do echo "### $d"; hdparm -I "$d" 2>&1 | head -n 45; echo; done' run_check "TRIM / discard configuration" - 'trim_report' run_check "Filesystem usage (space)" df 'df -hT -x tmpfs -x devtmpfs' run_check "Filesystem usage (inodes)" df 'df -ihT -x tmpfs -x devtmpfs' run_check "Mounted filesystems & options" findmnt 'findmnt --real -o TARGET,SOURCE,FSTYPE,OPTIONS' run_check "ext2/3/4 filesystem state" tune2fs 'fs_report' run_check "Partition tables" parted 'for d in $(disks_list); do echo "### $d"; parted -s "$d" print 2>&1; echo; done' run_check "RAID / LVM / ZFS / LUKS status" - 'raid_report' run_check "Storage I/O errors in kernel log" - 'dmesg -T 2>/dev/null | grep -iE "I/O error|blk_update_request|buffer I/O|ata[0-9]+[.:].*(error|failed|exception)|hard resetting link|medium error|SError|EXT4-fs error" | tail -n 40 | none_if_empty "No storage I/O errors this boot."' run_check "Disk I/O statistics" iostat 'iostat -xh 1 3' run_check "Log & cache disk consumption" - 'log_usage_report' if [[ $DO_STRESS -eq 1 ]]; then run_check "BENCH: sequential/cached read (hdparm)" hdparm 'bench_hdparm' 300 run_check "BENCH: random 4k read IOPS (fio)" fio 'bench_disk' 180 run_check "STRESS: SMART short self-test (polled)" smartctl 'smart_selftest' 700 fi analyze_smart analyze_selftest_age analyze_disk_space analyze_trim analyze_logs_disk #--------------------------------------------------------- 5. THERMAL & FANS -- category "Thermal, Fans & Cooling" run_check "Sensor readings (lm-sensors)" sensors 'sensors -A' run_check "Raw sensor values" sensors 'sensors -u' run_check "Thermal zones & cooling devices" - 'thermal_report' run_check "Fan tachometers & PWM" - 'fan_report' run_check "Drive temperatures" smartctl 'drive_temps' run_check "Thermal events in kernel log" - 'dmesg -T 2>/dev/null | grep -iE "thermal|temperature|critical temp" | tail -n 30 | none_if_empty "No thermal messages in the kernel log."' analyze_temps #------------------------------------------------------- 6. BATTERY & POWER --- category "Battery & Power Supply" run_check "Power supply sysfs dump" - 'power_report' run_check "Battery details (upower)" upower 'upower -e 2>/dev/null | while read -r p; do echo "### $p"; upower -i "$p"; echo; done | none_if_empty "upower reports no power devices."' run_check "ACPI battery/thermal/adapter" acpi 'acpi -V 2>&1' run_check "AC adapter state" - 'grep -H . /sys/class/power_supply/A{C,DP,C0}*/online 2>/dev/null | none_if_empty "No AC adapter node exposed (normal on desktops)."' run_check "Power consumption estimate" powertop 'timeout 25 powertop --time=15 --csv=/dev/stdout 2>/dev/null | head -n 60' 60 run_check "Suspend/resume & power events" - 'journalctl -b --no-pager 2>/dev/null | grep -iE "suspend|resume|hibernat|battery|acpi" | tail -n 30 | none_if_empty "No suspend/resume or ACPI power events this boot."' analyze_battery #--------------------------------------------------------- 7. GPU & DISPLAY --- category "GPU & Display" run_check "Graphics controllers (lspci)" lspci 'lspci -nnk | grep -iA3 -E "vga|3d|display"' run_check "DRM devices" - 'ls -l /sys/class/drm 2>/dev/null; echo; for c in /sys/class/drm/card*/device/{vendor,device}; do [ -r "$c" ] && echo "$c = $(cat $c)"; done 2>/dev/null' run_check "OpenGL renderer (glxinfo)" glxinfo 'as_user glxinfo -B 2>&1' run_check "NVIDIA GPU status" nvidia-smi 'nvidia-smi -q 2>&1 | head -n 80' run_check "AMD/Intel GPU sysfs telemetry" - 'for f in /sys/class/drm/card*/device/gpu_busy_percent /sys/class/drm/card*/device/mem_info_vram_used /sys/class/drm/card*/device/hwmon/hwmon*/temp1_input /sys/class/drm/card*/device/hwmon/hwmon*/power1_average; do [ -r "$f" ] && echo "$f = $(cat $f)"; done 2>/dev/null | none_if_empty "No AMD/Intel GPU telemetry nodes exposed."' run_check "VA-API video acceleration" vainfo 'as_user vainfo 2>&1 | head -n 30' run_check "GPU / DRM errors in kernel log" - 'dmesg -T 2>/dev/null | grep -iE "drm|nouveau|amdgpu|i915|xe |nvidia" | grep -iE "error|fail|warn|reset|hang|GPU HANG" | tail -n 25 | none_if_empty "No GPU or DRM errors this boot."' #---------------------------------------------------------------- 8. AUDIO ---- category "Audio Devices" run_check "Audio hardware & sound server" - 'audio_report' 90 run_check "Audio errors in kernel log" - 'dmesg -T 2>/dev/null | grep -iE "snd|azx|hda|alsa|sof-audio" | grep -iE "error|fail|timeout|no response" | tail -n 20 | none_if_empty "No audio subsystem errors this boot."' #---------------------------------------------------------------- 9. NETWORK -- category "Network Interfaces & Connectivity" run_check "Interface addresses & routes" ip 'ip -br addr; echo; ip route; echo; ip -6 route 2>/dev/null | head -n 10' run_check "Link statistics (errors/drops)" ip 'ip -s -s link' run_check "Per-NIC driver, speed & errors" - 'net_report' run_check "Wireless status" - 'iw dev 2>/dev/null; echo; iwconfig 2>/dev/null; echo; rfkill list 2>/dev/null' run_check "DNS configuration" - 'cat /etc/resolv.conf 2>/dev/null; echo; resolvectl status 2>/dev/null | head -n 30' run_check "Connectivity test (ICMP/DNS/HTTP)" - 'ping -c3 -W3 1.1.1.1 2>&1; echo; getent hosts deb.debian.org 2>&1; echo; curl -sSI -m 10 https://deb.debian.org 2>&1 | head -n 5' 60 run_check "Listening sockets" ss 'ss -tulpn 2>/dev/null | head -n 40' run_check "Network errors in kernel log" - 'dmesg -T 2>/dev/null | grep -iE "eth[0-9]|enp|wlp|wlan|link is|carrier|NIC Link" | tail -n 30 | none_if_empty "No network interface messages this boot."' analyze_network #------------------------------------------- 10. PERIPHERALS, PCIe & FIRMWARE - category "Peripherals, PCIe Buses & Firmware" run_check "PCI devices with drivers" lspci 'lspci -nnk' run_check "PCI bus tree" lspci 'lspci -tv' run_check "PCIe link speed vs capability" lspci 'pcie_link_report' 120 run_check "PCIe AER error counters" - 'aer_report' run_check "USB device tree" lsusb 'lsusb -t; echo; lsusb' run_check "USB enumeration errors" - 'dmesg -T 2>/dev/null | grep -iE "usb.*(cannot enable|device descriptor read|unable to enumerate|error -71|error -110|over-current)" | tail -n 20 | none_if_empty "No USB enumeration errors this boot."' run_check "SCSI / storage controllers" lsscsi 'lsscsi -s 2>&1; echo; lsblk -S 2>/dev/null' run_check "Input devices" - 'cat /proc/bus/input/devices 2>/dev/null | grep -E "^N:|^H:|^P:" | head -n 50' run_check "Firmware devices (fwupd)" fwupdmgr 'fwupdmgr get-devices 2>&1' 90 2 run_check "Available firmware updates" fwupdmgr 'fwupdmgr get-updates 2>&1' 90 2 run_check "Missing firmware blobs in log" - 'dmesg -T 2>/dev/null | grep -iE "firmware|microcode" | grep -iE "fail|missing|error|direct load" | tail -n 25 | none_if_empty "No firmware load failures."' run_check "Loaded kernel modules" lsmod 'lsmod | head -n 45' run_check "Kernel taint decode" - 'taint_decode' analyze_pcie analyze_firmware_age #----------------------------------------------- 11. KERNEL, LOGS & SERVICES -- category "Kernel Logs, Services & Software Health" run_check "Kernel messages, deduplicated" - 'dmesg_dedup_report' run_check "Kernel errors (dmesg err+)" - 'dmesg -T -l err,crit,alert,emerg 2>/dev/null | tail -n 60 | none_if_empty "No error-level kernel messages."' run_check "Kernel warnings (dmesg warn)" - 'dmesg -T -l warn 2>/dev/null | tail -n 40 | none_if_empty "No warning-level kernel messages."' run_check "Journal errors, this boot" journalctl 'journalctl -p 3 -b --no-pager 2>/dev/null | tail -n 60 | none_if_empty "No priority-3 journal entries this boot."' run_check "Journal errors, previous boot" journalctl 'journalctl -p 3 -b -1 --no-pager 2>/dev/null | tail -n 40 | none_if_empty "No previous boot recorded, or no errors in it."' run_check "Failed systemd units" systemctl 'systemctl --failed --no-pager; echo; systemctl list-units --state=failed --no-pager' run_check "Slowest boot units" systemd-analyze 'systemd-analyze 2>&1; echo; systemd-analyze blame 2>/dev/null | head -n 20' run_check "Core dumps" coredumpctl 'coredumpctl list --no-pager 2>&1 | tail -n 20' run_check "Segfaults grouped by component" - 'echo "count faulting object"; seg_summary | none_if_empty "No segfaults this boot."' run_check "Kernel BUG / oops / panic" - 'journalctl -b --no-pager 2>/dev/null | grep -iE "general protection|kernel BUG|Oops|kernel panic|call trace|soft lockup|hard LOCKUP" | tail -n 30 | none_if_empty "No kernel BUG, oops, panic or lockup this boot."' run_check "Pending package upgrades" apt 'apt list --upgradable 2>/dev/null | head -n 40' 90 run_check "Package database integrity" dpkg 'dpkg -C 2>&1 | head -n 30 | none_if_empty "dpkg database is consistent - no broken or half-configured packages."' run_check "Reboot required?" - '[ -f /var/run/reboot-required ] && { echo "YES - reboot required"; cat /var/run/reboot-required.pkgs 2>/dev/null; } || echo "No reboot flag present."' analyze_kernel_logs analyze_segfaults analyze_services analyze_updates step "PHASE 3/3 - building HTML report" finalize exit 0