diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..f17938a --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,17 @@ +# ShellCheck configuration for PFVCluster +# (used when running `shellcheck` directly; tests/shellcheck.sh applies the +# same disables via -e for consistent results under Docker) +# +# These checks are DISABLED because they flag intentional conventions of this +# codebase, not bugs: +# +# SC1090 / SC1091 — cannot follow dynamically-computed `source` paths. The KNEL +# framework (vendor/) and test harness source helpers via computed include +# dirs, which shellcheck cannot resolve statically. +# SC2029 — ssh orchestration (tests/remote.sh and perf/k8s/dns scripts) +# deliberately builds and expands the remote command on the CLIENT side before +# sending it. That is the whole point of the single-chokepoint remote pattern. +disable=SC1090,SC1091,SC2029 + +# Treat external-sourced files as bash (matches #!/usr/bin/env bash framework). +external-sources=true diff --git a/AGENTS.md b/AGENTS.md index 5833e25..1aca273 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,17 @@ vendor/ Vendored KNELShellFramework 2. **Atomic commits.** Each commit coherent on its own. 3. **Conventional format**: `feat(scope): desc`, `fix(scope): desc`, `docs: desc`, `refactor(scope): desc`, `test(scope): desc`. +4. **All shell scripts MUST pass `shellcheck` before commit.** No exceptions. + Run it via the wrapper: + ```bash + bash tests/shellcheck.sh # whole repo + bash tests/shellcheck.sh ups/*.sh # specific files + ``` + This invokes `koalaman/shellcheck:stable` through Docker (no native binary + needed). Fix every reported finding — including `info`-level — or add a + targeted `# shellcheck disable=SCxxxx # ` directive with a + justification. A script that emits any diagnostic is a protocol violation. + Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt. ## Automatic Gardening Protocol diff --git a/console/discover.sh b/console/discover.sh index 468eecd..34138b9 100644 --- a/console/discover.sh +++ b/console/discover.sh @@ -1,4 +1,5 @@ #!/usr/bin/bash +# shellcheck disable=SC2010,SC2012 # diagnostic script; ls|grep/ls -la on sysfs & log dirs is intentional for human-readable output # # console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4 # diff --git a/console/generate-config.sh b/console/generate-config.sh index 3a392a8..9d2ba78 100644 --- a/console/generate-config.sh +++ b/console/generate-config.sh @@ -33,8 +33,6 @@ CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}" UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules" SER2NET_CONF="/etc/ser2net.yaml" CONMAN_CONF="/etc/conman.conf" -CONMAN_CONSOLES="/etc/conman/console-consoles.conf" -CONSOLE_DEV_DIR="/dev/console" echo "============================================" echo " Console Config Generator" @@ -120,9 +118,11 @@ for entry in "${ENTRIES[@]}"; do # Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4" # The actual ID_PATH is like "pci-0000:00:1a.0-usb-0:1.5.4.4:1.0" # We match on the substring to be portable across PCI bus changes. - echo "" >> "$UDEV_RULES" - echo "# $name (TCP $tcp_port): $comment" >> "$UDEV_RULES" - echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\"" >> "$UDEV_RULES" + { + echo "" + echo "# $name (TCP $tcp_port): $comment" + echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\"" + } >> "$UDEV_RULES" done echo " Written: $UDEV_RULES" @@ -148,13 +148,13 @@ fi echo "#" echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients" echo "# negotiate proper telnet binary mode — this prevents CR stripping" - echo "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings." + printf '%s\n' "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings." echo "# Ports bound to Tailscale IP ($TS_IP) for secure remote access." echo "#" echo "# Direct telnet: telnet $TS_IP 2001" echo "# Via conman: conman -f " echo "" - echo "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n" + printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n" echo "" for entry in "${ENTRIES[@]}"; do diff --git a/console/setup.sh b/console/setup.sh index a8362c0..5da0536 100644 --- a/console/setup.sh +++ b/console/setup.sh @@ -1,4 +1,5 @@ #!/usr/bin/bash +# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional # # console/setup.sh — deploy console management on pfv-tsys4 # @@ -19,10 +20,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules" -SER2NET_CONF="/etc/ser2net.yaml" -CONMAN_CONSOLES="/etc/conman/console-consoles.conf" -CONSOLE_DEV_DIR="/dev/consoles" echo "============================================" echo " Console Management Setup" @@ -100,7 +97,7 @@ if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then line="${line%%#*}" line="$(echo "$line" | xargs)" [ -z "$line" ] && continue - IFS='|' read -r tcp_port name id_path baud comment <<< "$line" + IFS='|' read -r _ name id_path _ _ <<< "$line" # Find the ttyUSB whose ID_PATH contains the mapping's id_path substring for tty in /dev/ttyUSB*; do [ -e "$tty" ] || continue @@ -124,7 +121,7 @@ while IFS= read -r line; do line="${line%%#*}" line="$(echo "$line" | xargs)" [ -z "$line" ] && continue - IFS='|' read -r tcp_port name id_path baud comment <<< "$line" + IFS='|' read -r _ name id_path _ _ <<< "$line" if [ -e "/dev/consoles/$name" ]; then TARGET=$(readlink -f "/dev/consoles/$name") echo " [OK] /dev/consoles/$name -> $TARGET" diff --git a/console/validate-conman.sh b/console/validate-conman.sh index 0c37dff..10f1c00 100644 --- a/console/validate-conman.sh +++ b/console/validate-conman.sh @@ -1,4 +1,5 @@ #!/usr/bin/bash +# shellcheck disable=SC2012,SC2001 # diagnostic script; ls -la listings and sed line-prefixing are intentional # # console/validate-conman.sh — verify conman can actually reach devices via # ser2net TCP ports and is capturing log output to files. diff --git a/dns-cluster-setup/sync-zones.sh b/dns-cluster-setup/sync-zones.sh index 63100ca..7e190d4 100755 --- a/dns-cluster-setup/sync-zones.sh +++ b/dns-cluster-setup/sync-zones.sh @@ -35,7 +35,7 @@ mkdir -p "$ZONE_DIR" log "Syncing zones from $PRIMARY_HOST..." if rsync -az --delete --temp-dir=/tmp \ "${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then - zone_count=$(ls "$ZONE_DIR" | wc -l) + zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l) log "Sync complete: $zone_count zones" else log "ERROR: rsync failed (rc=$?)" diff --git a/dns-cluster-setup/verify.sh b/dns-cluster-setup/verify.sh index 43cd9f8..6a1ad00 100755 --- a/dns-cluster-setup/verify.sh +++ b/dns-cluster-setup/verify.sh @@ -69,9 +69,9 @@ echo " Production zones: $prod_zones" echo " Primary (01) zones: $pri_zones" echo " Secondary (02) zones: $sec_zones" -[ "$prod_zones" -gt 0 ] 2>/dev/null && ok "Production has $prod_zones zones" || fail "Production zone count invalid" -[ "$pri_zones" -gt 0 ] 2>/dev/null && ok "Primary has $pri_zones zones" || fail "Primary zone count invalid" -[ "$sec_zones" -gt 0 ] 2>/dev/null && ok "Secondary has $sec_zones zones" || fail "Secondary zone count invalid" +if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi +if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi +if [ "$sec_zones" -gt 0 ] 2>/dev/null; then ok "Secondary has $sec_zones zones"; else fail "Secondary zone count invalid"; fi if [ "$pri_zones" = "$prod_zones" ]; then ok "Primary zone count matches production ($pri_zones)" diff --git a/k8s/env.sh b/k8s/env.sh index f111d5b..8de10c8 100644 --- a/k8s/env.sh +++ b/k8s/env.sh @@ -1,4 +1,5 @@ #!/usr/bin/bash +# shellcheck disable=SC2034 # sourced config file; variables are consumed by scripts that source this # k8s/env.sh — shared config for all k8s scripts. Source this. # # All cluster communication goes over Tailscale IPs. No LAN IPs, ever. diff --git a/k8s/install-cp.sh b/k8s/install-cp.sh index ac7510f..bf0d96a 100644 --- a/k8s/install-cp.sh +++ b/k8s/install-cp.sh @@ -35,6 +35,7 @@ echo "============================================" echo "" echo "--- [1/5] Installing bootstrap node: $CNODE1_NAME ($CNODE1_IP) ---" +# shellcheck disable=SC2087 # heredoc intentionally expands local config (node IPs, k3s version) before sending to remote ssh "${SSH_OPTS[@]}" "${SSH_USER}@${CNODE1_IP}" "sudo -n bash -s" </dev/null | wc -w) # Subtract system pods SYSTEM_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -l k8s-app --no-headers 2>/dev/null | wc -l) -if [ "$USER_PODS" -le 10 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi +if [ "$((USER_PODS - SYSTEM_PODS))" -le 0 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi echo "" echo "============================================" diff --git a/netinfra/baseline.sh b/netinfra/baseline.sh index 5e1958a..a3a94e5 100755 --- a/netinfra/baseline.sh +++ b/netinfra/baseline.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2012 # diagnostic baseline script; ls -la listings are intentional # baseline.sh — quick read-only baseline of a target node. set -u hdr() { printf '\n=== %s ===\n' "$1"; } diff --git a/netinfra/deep-audit-netboot.sh b/netinfra/deep-audit-netboot.sh index 44a2728..3b0eb6b 100755 --- a/netinfra/deep-audit-netboot.sh +++ b/netinfra/deep-audit-netboot.sh @@ -37,9 +37,11 @@ for f in setupVars.conf pihole-FTL.conf adlists.list custom.list local.list rege echo "--- /etc/pihole/$f ---"; $DG exec pihole cat "/etc/pihole/$f" 2>&1 || true done echo "-- /etc/dnsmasq.d/* --" +# shellcheck disable=SC2016 # $f/$t expand inside the container's sh, not locally — single quotes are intentional $DG exec pihole sh -c 'for f in /etc/dnsmasq.d/*; do echo "--- $f ---"; cat "$f"; done' 2>&1 || true echo "-- pihole version --"; $DG exec pihole pihole -v 2>&1 || true echo "-- gravity row counts --" +# shellcheck disable=SC2016 # $t expands inside the container's sh, not locally $DG exec pihole sh -c 'for t in adlist domainlist client "group" info; do printf "%s=" "$t"; sqlite3 /etc/pihole/gravity.db "SELECT COUNT(*) FROM $t;" 2>/dev/null; done' 2>&1 || true echo "-- adlist addresses --" $DG exec pihole sqlite3 /etc/pihole/gravity.db "SELECT address,enabled,comment FROM adlist;" 2>&1 || true diff --git a/netinfra/gather-configs.sh b/netinfra/gather-configs.sh index f48c411..5f5c336 100755 --- a/netinfra/gather-configs.sh +++ b/netinfra/gather-configs.sh @@ -44,7 +44,7 @@ if command -v sqlite3 >/dev/null 2>&1; then echo "-- adlist count --"; sqlite3 -readonly "$G" "SELECT COUNT(*) FROM adlist;" 2>&1 echo "-- domainlist count by type --"; sqlite3 -readonly "$G" "SELECT type,COUNT(*) FROM domainlist GROUP BY type;" 2>&1 echo "-- domainlist (allow=0/allow_exact, deny=1/deny_exact, etc.) first 80 --"; sqlite3 -readonly "$G" "SELECT type,domain,enabled,comment FROM domainlist LIMIT 80;" 2>&1 - echo -- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1 + echo "-- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1 echo "-- group --"; sqlite3 -readonly "$G" "SELECT id,name,enabled,comment FROM 'group';" 2>&1 echo "-- info --"; sqlite3 -readonly "$G" "SELECT * FROM info;" 2>&1 else diff --git a/netinfra/setup-netinfra.sh b/netinfra/setup-netinfra.sh index fdb53fb..3cd3ba8 100755 --- a/netinfra/setup-netinfra.sh +++ b/netinfra/setup-netinfra.sh @@ -34,7 +34,6 @@ set -euo pipefail NETBOOT="localuser@pfv-netboot" SVC_ROOT="/home/localuser/services" -PIHOLE_PW='REDACTED_PASSWORD' # replicated verbatim from netboot compose log() { printf '\n\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; } warn() { printf '\n\033[1;33m[WARN %s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; } diff --git a/perf/scripts/lacp-retrans-cause.sh b/perf/scripts/lacp-retrans-cause.sh index 1efcb46..62d9163 100644 --- a/perf/scripts/lacp-retrans-cause.sh +++ b/perf/scripts/lacp-retrans-cause.sh @@ -41,6 +41,7 @@ trap 'rm -rf "$TMP"' EXIT # plus all error-like counters. Falls back gracefully if a counter doesn't # exist (different NIC drivers expose different names). nic_snapshot() { + # shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here for s in $(awk '/^Slave Interface:/{print $3}' "/proc/net/bonding/$BOND"); do [ -n "$s" ] || continue echo "[$s]" @@ -144,9 +145,10 @@ echo "Bond: $BOND" echo "" echo "--- bond0 hash + driver ---" grep -E "Bonding Mode|Transmit Hash|Number of ports|Partner Mac" /proc/net/bonding/$BOND +# shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here for s in $(awk '/^Slave Interface:/{print $3}' /proc/net/bonding/$BOND); do drv=$(ethtool -i "$s" 2>/dev/null | awk -F: '/^driver:/{print $2}' | sed 's/^ *//') - speed=$(cat /sys/class/net/$s/speed 2>/dev/null) + speed=$(cat "/sys/class/net/$s/speed" 2>/dev/null) ring=$(ethtool -g "$s" 2>/dev/null | awk '/RX:/{print $2; exit}') printf " %-8s driver=%-20s speed=%-6s current RX ring=%s\n" "$s" "$drv" "$speed" "$ring" done diff --git a/perf/scripts/sw-capture-remote.sh b/perf/scripts/sw-capture-remote.sh index 59e2858..a05e3c8 100755 --- a/perf/scripts/sw-capture-remote.sh +++ b/perf/scripts/sw-capture-remote.sh @@ -68,6 +68,7 @@ echo "[$(ts)] switch=$SWITCH device=$DEVICE baud=$BAUD host=$HOST" # 1. De-conflict: any local ssh to pfv-tsys4 in flight? echo "[$(ts)] checking for in-flight ssh to pfv-tsys4..." +# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args if ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys4|scp.*pfv-tsys4' | grep -v grep >/tmp/.swcap.ps 2>&1; then cat /tmp/.swcap.ps echo "[$(ts)] ABORT: another ssh/scp to pfv-tsys4 is running (other agent?)." >&2 diff --git a/perf/scripts/sw-conman-probe.sh b/perf/scripts/sw-conman-probe.sh index dac7bed..7540634 100644 --- a/perf/scripts/sw-conman-probe.sh +++ b/perf/scripts/sw-conman-probe.sh @@ -5,6 +5,7 @@ set -u # De-conflict: any ssh to pfv-tsys4 right now? echo "===== LOCAL ssh activity =====" +# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys' | grep -v grep || echo "(none to pfv-tsys4)" echo diff --git a/perf/scripts/sw-probe.sh b/perf/scripts/sw-probe.sh index 8d80541..a2b3fc0 100644 --- a/perf/scripts/sw-probe.sh +++ b/perf/scripts/sw-probe.sh @@ -5,6 +5,7 @@ set -u echo "===== LOCAL ssh/scp activity (other-agent de-confliction) =====" +# shellcheck disable=SC2009 # intentional: need full ps columns (etime,args) filtered by process args ps -eo pid,ppid,etime,user,args | grep -E 'ssh|scp' | grep -v grep || echo "(none)" echo diff --git a/perf/shellcheck.sh b/perf/shellcheck.sh index ccdceb9..43e987f 100755 --- a/perf/shellcheck.sh +++ b/perf/shellcheck.sh @@ -1,5 +1,5 @@ #!/bin/bash -# shellcheck.sh - permanent wrapper to lint every shell script in this project. +# Lint wrapper - permanent wrapper to lint every shell script in this project. # # Uses the koalaman/shellcheck:stable docker image so nothing is installed # on the host. Run from anywhere; lints scripts/ and any .sh under switches/. @@ -45,7 +45,7 @@ echo # Make paths relative to ROOT so docker volume maps cleanly REL_TARGETS=() for t in "${TARGETS[@]}"; do - rel="${t#$ROOT/}" + rel="${t#"$ROOT"/}" [ "$rel" = "$t" ] && rel="$t" REL_TARGETS+=("$rel") done @@ -61,5 +61,5 @@ if [ "$STRICT" -eq 1 ]; then exit $RC fi # Non-strict: only fail on parse errors / errors, not style notes. -# shellcheck exit code 1 means "findings"; re-run with severity to distinguish. +# (a shellcheck exit code of 1 means "findings"; re-run with severity to distinguish.) exit 0 diff --git a/perf/validate-vms.sh b/perf/validate-vms.sh index 2c01c51..1feab0b 100755 --- a/perf/validate-vms.sh +++ b/perf/validate-vms.sh @@ -75,8 +75,7 @@ for HOST in "$@"; do for vmid in $VM_LIST; do name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'") echo -n " [$vmid $name] starting... " - start_output=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1) - if [ $? -eq 0 ]; then + if start_output=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1); then echo "OK" else echo "FAILED: $start_output" @@ -115,8 +114,7 @@ for HOST in "$@"; do if [ "$agent" = "1" ]; then name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'") echo -n " [$vmid $name] agent ping... " - result=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "timeout 10 qm agent $vmid ping 2>&1") - if [ $? -eq 0 ]; then + if ssh "${SSH_OPTS[@]}" "root@$HOST" "timeout 10 qm agent $vmid ping" >/dev/null 2>&1; then echo "OK" else echo "no response (VM may still be booting)" diff --git a/powerman/setup.sh b/powerman/setup.sh index df0658a..0bb61d2 100644 --- a/powerman/setup.sh +++ b/powerman/setup.sh @@ -82,7 +82,7 @@ udevadm trigger --subsystem-match=tty 2>/dev/null || true sleep 1 if [ -e "/dev/${PDU_DEV_NAME}" ]; then - echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f /dev/${PDU_DEV_NAME})" + echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f "/dev/${PDU_DEV_NAME}")" else echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged." echo " Falling back to /dev/ttyUSB* discovery..." diff --git a/provisioning/Dell/Server/fixeth.sh b/provisioning/Dell/Server/fixeth.sh index 3f75aa9..21e4b94 100644 --- a/provisioning/Dell/Server/fixeth.sh +++ b/provisioning/Dell/Server/fixeth.sh @@ -5,17 +5,18 @@ #magic to detect main int echo "Determining management interface..." #export MAIN_INT=$(brctl show $(netstat -rn|grep 0.0.0.0|head -n1|awk '{print $NF}') | awk '{print $NF}'|tail -1|awk -F '.' '{print $1}') -export MAIN_INT=$(brctl show|grep vmbr0|awk '{print $NF}'|awk -F '.' '{print $1}') +MAIN_INT=$(brctl show|grep vmbr0|awk '{print $NF}'|awk -F '.' '{print $1}') +export MAIN_INT echo "Management interface is: $MAIN_INT" #fix the issue echo "Fixing management interface..." -ethtool -K $MAIN_INT tso off -ethtool -K $MAIN_INT gro off -ethtool -K $MAIN_INT gso off -ethtool -K $MAIN_INT tx off -ethtool -K $MAIN_INT rx off +ethtool -K "$MAIN_INT" tso off +ethtool -K "$MAIN_INT" gro off +ethtool -K "$MAIN_INT" gso off +ethtool -K "$MAIN_INT" tx off +ethtool -K "$MAIN_INT" rx off #https://forum.proxmox.com/threads/e1000-driver-hang.58284/ #https://serverfault.com/questions/616485/e1000e-reset-adapter-unexpectedly-detected-hardware-unit-hang diff --git a/provisioning/Modules/Auth/auth-cloudron-ldap.sh b/provisioning/Modules/Auth/auth-cloudron-ldap.sh index e69de29..da03f3d 100644 --- a/provisioning/Modules/Auth/auth-cloudron-ldap.sh +++ b/provisioning/Modules/Auth/auth-cloudron-ldap.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# auth-cloudron-ldap.sh — placeholder module (Cloudron LDAP auth integration). +# Intentionally empty; populated when the auth stack is deployed. +true diff --git a/provisioning/Modules/RandD/sslStackFromSource.sh b/provisioning/Modules/RandD/sslStackFromSource.sh index 30d222c..2e9e819 100644 --- a/provisioning/Modules/RandD/sslStackFromSource.sh +++ b/provisioning/Modules/RandD/sslStackFromSource.sh @@ -1,4 +1,5 @@ #!/bin/bash +# shellcheck disable=SC2103 # legacy R&D build script; cd/cd- sequence is intentional #Made from instructions at https://www.tunetheweb.com/performance/http2/ @@ -24,17 +25,17 @@ CURL_FILE="curl-7.60.0.tar.gz" #Download and install latest version of openssl wget $OPENSSL_URL_BASE/$OPENSSL_FILE tar xzf $OPENSSL_FILE -cd openssl-1.1.0h +cd openssl-1.1.0h || exit ./config enable-weak-ssl-ciphers shared zlib-dynamic -DOPENSSL_TLS_SECURITY_LEVEL=0 --prefix=/usr/local/custom-ssl/openssl-1.1.0h ; make ; make install ln -s /usr/local/custom-ssl/openssl-1.1.0h /usr/local/openssl -cd - +cd - || exit #Download and install nghttp2 (needed for mod_http2). wget $NGHTTP_URL_BASE/$NGHTTP_FILE tar xzf $NGHTTP_FILE -cd nghttp2-1.31.0 +cd nghttp2-1.31.0 || exit ./configure --prefix=/usr/local/custom-ssl/nghttp ; make ; make install -cd - +cd - || exit #Updated ldconfig so curl build @@ -48,34 +49,34 @@ ldconfig #Download and install curl wget $CURL_URL_BASE/$CURL_FILE tar xzf curl-7.60.0.tar.gz -cd curl-7.60.0 +cd curl-7.60.0 || exit ./configure --prefix=/usr/local/custom-ssl/curl --with-nghttp2=/usr/local/custom-ssl/nghttp/ --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ ; make ; make install -cd - +cd - || exit #Download and install latest apr wget $APR_URL_BASE/$APR_FILE tar xzf $APR_FILE -cd apr-1.6.3 +cd apr-1.6.3 || exit ./configure --prefix=/usr/local/custom-ssl/apr ; make ; make install -cd - +cd - || exit #Download and install latest apr-util wget $APR_UTIL_URL_BASE/$APR_UTIL_FILE tar xzf apr-util-1.6.1.tar.gz -cd apr-util-1.6.1 +cd apr-util-1.6.1 || exit ./configure --prefix=/usr/local/custom-ssl/apr-util --with-apr=/usr/local/custom-ssl/apr ; make; make install -cd - +cd - || exit #Download and install apache wget $APACHE_URL_BASE/$APACHE_FILE tar xzf httpd-2.4.33.tar.gz -cd httpd-2.4.33 +cd httpd-2.4.33 || exit cp -r ../apr-1.6.3 srclib/apr cp -r ../apr-util-1.6.1 srclib/apr-util ./configure --prefix=/usr/local/custom-ssl/apache --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ --with-pcre=/usr/bin/pcre-config --enable-unique-id --enable-ssl --enable-so --with-included-apr --enable-http2 --with-nghttp2=/usr/local/custom-ssl/nghttp/ make make install ln -s /usr/local/custom-ssl/apache /usr/local/apache -cd - +cd - || exit diff --git a/provisioning/Modules/Security/secharden-ssh.sh b/provisioning/Modules/Security/secharden-ssh.sh index a3e578f..71bde08 100644 --- a/provisioning/Modules/Security/secharden-ssh.sh +++ b/provisioning/Modules/Security/secharden-ssh.sh @@ -79,7 +79,7 @@ if [ "$SUBODEV_CHECK" = 1 ]; then fi export DEV_WORKSTATION_CHECK -DEV_WORKSTATION_CHECK="$(hostname | egrep -c 'subopi-dev|CharlesDevServer' || true)" +DEV_WORKSTATION_CHECK="$(hostname | grep -Ec 'subopi-dev|CharlesDevServer' || true)" if [ "$DEV_WORKSTATION_CHECK" -eq 0 ]; then diff --git a/provisioning/Project-Includes/PreflightCheck.sh b/provisioning/Project-Includes/PreflightCheck.sh index 9065bc2..94e736a 100644 --- a/provisioning/Project-Includes/PreflightCheck.sh +++ b/provisioning/Project-Includes/PreflightCheck.sh @@ -9,7 +9,7 @@ export user_check user_check="$(echo "$curr_user" | grep -c root)" -if [ $user_check -ne 1 ]; then +if [ "$user_check" -ne 1 ]; then print_error "Must run as root." error_out fi diff --git a/provisioning/Project-Includes/pi-detect.sh b/provisioning/Project-Includes/pi-detect.sh index b1837aa..29bce4d 100644 --- a/provisioning/Project-Includes/pi-detect.sh +++ b/provisioning/Project-Includes/pi-detect.sh @@ -1,7 +1,7 @@ - +# shellcheck shell=bash disable=SC2148 # sourced function file (no shebang by design) function pi-detect() { -print_info Now running "$FUNCNAME".... +print_info Now running "${FUNCNAME[0]}".... if [ -f /sys/firmware/devicetree/base/model ] ; then export IS_RASPI="1" fi @@ -9,5 +9,5 @@ fi if [ ! -f /sys/firmware/devicetree/base/model ] ; then export IS_RASPI="0" fi -print_info Completed running "$FUNCNAME" +print_info Completed running "${FUNCNAME[0]}" } \ No newline at end of file diff --git a/provisioning/SetupNewSystem.sh b/provisioning/SetupNewSystem.sh index cb6980a..00e8fd2 100644 --- a/provisioning/SetupNewSystem.sh +++ b/provisioning/SetupNewSystem.sh @@ -60,18 +60,18 @@ LOCALUSER_CHECK="$(getent passwd | grep -c localuser || true)" ####################### function global-oam() { - print_info "Now running $FUNCNAME...." + print_info "Now running ${FUNCNAME[0]}...." cat "$SCRIPTS_PATH/up2date.sh" >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh bash "$MODULES_PATH/OAM/oam-librenms.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function global-systemServiceConfigurationFiles() { - print_info "Now running $FUNCNAME...." + print_info "Now running ${FUNCNAME[0]}...." cat "$CONFIGFILES_PATH/ZSH/tsys-zshrc" >/etc/zshrc cat "$CONFIGFILES_PATH/SMTP/aliases" >/etc/aliases @@ -79,11 +79,11 @@ function global-systemServiceConfigurationFiles() { newaliases - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function global-installPackages() { - print_info "Now running $FUNCNAME...." + print_info "Now running ${FUNCNAME[0]}...." # Setup webmin repo, used for RBAC/2fa PAM @@ -195,7 +195,7 @@ function global-installPackages() { VIRT_TYPE="$(virt-what)" export IS_VIRT_GUEST - IS_VIRT_GUEST="$(echo "$VIRT_TYPE" | egrep -c 'hyperv|kvm' || true)" + IS_VIRT_GUEST="$(echo "$VIRT_TYPE" | grep -Ec 'hyperv|kvm' || true)" export IS_KVM_GUEST IS_KVM_GUEST="$(echo "$VIRT_TYPE" | grep -c 'kvm' || true)" @@ -227,12 +227,12 @@ function global-installPackages() { # vault cli - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function global-postPackageConfiguration() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" systemctl --now enable auditd @@ -253,7 +253,7 @@ function global-postPackageConfiguration() { #This is under test/dev and may fail echo "hi from root to root" | mail -s "hi directly to root from $(hostname)" root - chsh -s $(which zsh) root + chsh -s "$(which zsh)" root if [ "$LOCALUSER_CHECK" -gt 0 ]; then chsh -s "$(which zsh)" localuser @@ -312,7 +312,7 @@ function global-postPackageConfiguration() { fi export NTP_SERVER_CHECK - NTP_SERVER_CHECK="$(hostname | egrep -c 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)" + NTP_SERVER_CHECK="$(hostname | grep -Ec 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)" if [ "$NTP_SERVER_CHECK" -eq 0 ]; then @@ -341,7 +341,7 @@ function global-postPackageConfiguration() { tuned-adm profile virtual-guest fi - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } #################################################################################################### @@ -355,41 +355,41 @@ function global-postPackageConfiguration() { # SSH function secharden-ssh() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" bash "$MODULES_PATH/Security/secharden-ssh.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function secharden-wazuh() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" bash "$MODULES_PATH/Security/secharden-wazuh.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function secharden-2fa() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" bash "$MODULES_PATH/Security/secharden-2fa.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function secharden-scap-stig() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" bash "$MODULES_PATH/Security/secharden-scap-stig.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function secharden-agents() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" bash "$MODULES_PATH/Security/secharden-audit-agents.sh" - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } function secharden-auto-upgrades() { - print_info "Now running $FUNCNAME" + print_info "Now running ${FUNCNAME[0]}" #curl --silent ${DL_ROOT}/Modules/Security/secharden-ssh.sh|$(which bash) - print_info "Completed running $FUNCNAME" + print_info "Completed running ${FUNCNAME[0]}" } @@ -400,16 +400,16 @@ function secharden-auto-upgrades() { #################################################################################################### function auth-cloudron-ldap() { - print_info "Now running "$FUNCNAME"" + print_info "Now running ${FUNCNAME[0]}" #curl --silent ${DL_ROOT}/Modules/Auth/auth-cloudron-ldap.sh|$(which bash) - print_info "Completed running "$FUNCNAME"" + print_info "Completed running ${FUNCNAME[0]}" } #################################################################################################### # RUn the various functions in the correct order #################################################################################################### -echo >$LOGFILENAME +echo >"$LOGFILENAME" print_info "Execution starting at $CURRENT_TIMESTAMP..." diff --git a/provisioning/legacy/profiled-tmux.sh b/provisioning/legacy/profiled-tmux.sh index 8bd0650..7a51ea6 100644 --- a/provisioning/legacy/profiled-tmux.sh +++ b/provisioning/legacy/profiled-tmux.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ]; then tmux a -t default || exec tmux new -s default && exit; fi diff --git a/provisioning/legacy/profiled-tsys-shell.sh b/provisioning/legacy/profiled-tsys-shell.sh index 8197f0a..8df5b84 100644 --- a/provisioning/legacy/profiled-tsys-shell.sh +++ b/provisioning/legacy/profiled-tsys-shell.sh @@ -1 +1,2 @@ +# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment export HISTTIMEFORMAT="%m/%d/%Y %T " \ No newline at end of file diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 3867255..6d379a9 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -52,7 +52,8 @@ function run_test_suite() { function run_single_test() { local test_file="$1" - local test_name="$(basename "$test_file" .sh)" + local test_name + test_name="$(basename "$test_file" .sh)" print_info "Running test: $test_name" diff --git a/tests/security/2fa-validation.sh b/tests/security/2fa-validation.sh index 9c500e0..89cca44 100755 --- a/tests/security/2fa-validation.sh +++ b/tests/security/2fa-validation.sh @@ -5,8 +5,6 @@ set -euo pipefail -PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.." - function test_2fa_packages() { echo "🔍 Testing 2FA package installation..." @@ -235,7 +233,8 @@ function test_backup_existence() { if [[ -d "$backup_dir" ]]; then # Look for recent 2FA backups - local recent_backups=$(find "$backup_dir" -name "2fa-*" -type d -newer /etc/ssh/sshd_config 2>/dev/null | wc -l) + local recent_backups + recent_backups=$(find "$backup_dir" -name "2fa-*" -type d -newer /etc/ssh/sshd_config 2>/dev/null | wc -l) if [[ $recent_backups -gt 0 ]]; then echo "✅ Recent 2FA backup found in $backup_dir" diff --git a/tests/shellcheck.sh b/tests/shellcheck.sh new file mode 100644 index 0000000..e3105f9 --- /dev/null +++ b/tests/shellcheck.sh @@ -0,0 +1,92 @@ +#!/usr/bin/bash +# +# tests/shellcheck.sh — enforce shellcheck (via Docker) on all repo shell scripts +# +# Usage: +# bash tests/shellcheck.sh # lint every .sh in the repo +# bash tests/shellcheck.sh path/a.sh path/b.sh # lint specific files +# +# Uses the koalaman/shellcheck:stable image (no native binary required). +# Exits non-zero if ANY script emits a diagnostic. Skips non-bash scripts +# (e.g. PHP files with a .sh extension) and the vendored/ trees. +# +set -uo pipefail + +IMAGE="koalaman/shellcheck:stable" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# --- Gather target files ----------------------------------------------------- +mapfile -t FILES < <( + if [ "$#" -gt 0 ]; then + # Explicit args: resolve to repo-root-relative paths, keep only .sh + for f in "$@"; do + f_abs="$(cd "$(dirname "$f")" && pwd)/$(basename "$f")" + case "$f_abs" in + "$ROOT"/vendor/*) ;; # skip vendored + "$ROOT"/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts + *) case "$f_abs" in *.sh) echo "$f_abs";; esac ;; + esac + done + else + # Default: every .sh under the repo, minus vendor/ + while IFS= read -r f_abs; do + case "$f_abs" in + "$ROOT"/vendor/*) ;; # skip vendored + "$ROOT"/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts + *) echo "$f_abs" ;; + esac + done < <(find "$ROOT" -type f -name '*.sh' \ + -not -path '*/.git/*' -not -path "$ROOT/vendor/*") + fi +) + +if [ "${#FILES[@]}" -eq 0 ]; then + echo "shellcheck.sh: no .sh files to check." >&2 + exit 0 +fi + +# --- Filter out non-bash scripts (e.g. PHP with a .sh extension) -------------- +BASH_FILES=() +for f in "${FILES[@]}"; do + shebang=$(head -c 64 "$f" 2>/dev/null | head -n1) + case "$shebang" in + \#!/usr/bin/php*|\#!/usr/bin/env\ php*) ;; # PHP, skip + \#!*) BASH_FILES+=("$f") ;; # any other shebang → check + *) BASH_FILES+=("$f") ;; # no shebang → check anyway + esac +done + +if [ "${#BASH_FILES[@]}" -eq 0 ]; then + echo "shellcheck.sh: no bash scripts among targets." >&2 + exit 0 +fi + +# --- Run shellcheck in Docker (mount repo root, pass root-relative paths) ----- +# shellcheck disable=SC2012 # basename loop is intentional +REL=() +for f in "${BASH_FILES[@]}"; do REL+=("${f#"$ROOT"/}"); done + +# Disable checks that are intentional conventions of this codebase (not bugs): +# SC1090/SC1091 — cannot follow dynamically-computed `source` paths (KNEL framework) +# SC2029 — ssh orchestration deliberately expands the command client-side +DISABLES=( + -e SC1090 + -e SC1091 + -e SC2029 +) + +echo "Checking ${#BASH_FILES[@]} script(s) with $IMAGE:" +printf ' %s\n' "${REL[@]}" + +docker run --rm \ + -v "$ROOT:/mnt:ro" \ + -w /mnt \ + "$IMAGE" -x "${DISABLES[@]}" "${REL[@]}" +rc=$? + +if [ "$rc" -eq 0 ]; then + echo "shellcheck: PASS (${#BASH_FILES[@]} scripts clean)" +else + echo "shellcheck: FAIL (fix the findings above or add targeted disable directives)" >&2 +fi +exit "$rc" diff --git a/tests/validation/dns-ntp-redundancy.sh b/tests/validation/dns-ntp-redundancy.sh index 8c0366c..84b61f5 100644 --- a/tests/validation/dns-ntp-redundancy.sh +++ b/tests/validation/dns-ntp-redundancy.sh @@ -6,8 +6,6 @@ set -euo pipefail -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" - # The authoritative pair (pfv-netinfra-01 / pfv-netinfra-02). DNS_PRIMARY="192.168.3.252" DNS_SECONDARY="192.168.3.253" @@ -20,7 +18,6 @@ NTP_CONF="/etc/ntpsec/ntp.conf" # A name every recursive resolver must be able to resolve. DNS_PROBE_NAME="github.com" -failed=0 have() { command -v "$1" >/dev/null 2>&1; } # --- Configuration assertions ------------------------------------------------- diff --git a/tests/validation/system-requirements.sh b/tests/validation/system-requirements.sh index c0d3a63..01fe4ab 100755 --- a/tests/validation/system-requirements.sh +++ b/tests/validation/system-requirements.sh @@ -12,7 +12,8 @@ REQUIRED_COMMANDS=("curl" "wget" "git" "systemctl" "apt-get") # Test functions function test_memory_requirements() { - local total_mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') + local total_mem_kb + total_mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') local total_mem_gb=$((total_mem_kb / 1024 / 1024)) if [[ $total_mem_gb -ge $MIN_RAM_GB ]]; then @@ -25,7 +26,8 @@ function test_memory_requirements() { } function test_disk_space() { - local available_gb=$(df / | tail -1 | awk '{print int($4/1024/1024)}') + local available_gb + available_gb=$(df / | tail -1 | awk '{print int($4/1024/1024)}') if [[ $available_gb -ge $MIN_DISK_GB ]]; then echo "✅ Disk space requirement met: ${available_gb}GB >= ${MIN_DISK_GB}GB" @@ -53,8 +55,10 @@ function test_required_commands() { function test_os_compatibility() { if [[ -f /etc/os-release ]]; then - local os_id=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') - local os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') + local os_id + local os_version + os_id=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') + os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') case "$os_id" in ubuntu|debian)