mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:27:02 +01:00
Two files under the cap. test_security_hardening.sh (423) keeps its filename -- shell-tests.yml runs the suite by name -- and its five sections move into three sourced libs. They are top-level blocks, not functions, so each is wrapped and called in the original order; the counters and LIVE_SECTION stay in the entry, which sourcing shares. Verified by diffing a full run against the same suite from a detached worktree at HEAD: byte-identical output, same exit status, still 18/1/5 on this host. The one failure is pre-existing. Linting a lib alone turns a bare LIVE_SECTION=1 into SC2034, since its only reader is test_result in the entry. A live_section() setter makes the write a real reference, so no suppression was needed. The mutation-driver split was already in the tree from the previous session and did not run: the operators had moved to _session_autopsy_mutations.py but the driver still named Mutation without importing it, so mypy and pylint both reported undefined-variable. Restored the import; the script now reports all 12 tests killed. block_compulsive_opening.sh (705) is deliberately untouched and now says so in the contract. install_all copies the running script to /usr/local/bin, so an entry sourcing lib/ would resolve SCRIPT_DIR there, find nothing, and leave beeper, signal-desktop and discord unlaunchable with a failing pacman hook. Splitting it means teaching the installer to copy the libs -- a refactor, not a line-count split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
6.9 KiB
Bash
Executable File
161 lines
6.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Security hardening tests: hosts guard + shutdown schedule.
|
|
# Sourced by test_security_hardening.sh; inherits its strict mode,
|
|
# colors, counters (PASS/FAIL/SKIP), LIVE_SECTION and test_result().
|
|
|
|
sec_tests_hosts_guard() {
|
|
# ==================================================================
|
|
# HOSTS GUARD TESTS
|
|
# ==================================================================
|
|
live_section 1 # live-host checks begin
|
|
echo "--- HOSTS GUARD ---"
|
|
|
|
# Test 1: /etc/hosts is immutable
|
|
if [[ -f /etc/hosts ]]; then
|
|
if lsattr /etc/hosts 2>/dev/null | grep -q '^....i'; then
|
|
test_result "/etc/hosts is immutable" "pass"
|
|
else
|
|
test_result "/etc/hosts is immutable" "fail" "chattr +i not set"
|
|
fi
|
|
else
|
|
test_result "/etc/hosts is immutable" "skip" "File not found"
|
|
fi
|
|
|
|
# Test 2: the hosts file watcher is active
|
|
# The 2026-07-17 guard-lib refactor replaced the standalone hosts-guard.path
|
|
# with the templated guard-file@hosts.path. Accept either, so this passes on a
|
|
# migrated host and on one still running the legacy unit. Checking only the old
|
|
# name reported the protection as down when it was simply renamed.
|
|
if systemctl is-active --quiet guard-file@hosts.path 2>/dev/null ||
|
|
systemctl is-active --quiet hosts-guard.path 2>/dev/null; then
|
|
test_result "hosts file watcher is active" "pass"
|
|
else
|
|
test_result "hosts file watcher is active" "fail" \
|
|
"neither guard-file@hosts.path nor hosts-guard.path is running"
|
|
fi
|
|
|
|
# Test 3: /etc/hosts is bind-mounted read-only
|
|
# guard-bind-mount@hosts.service superseded hosts-bind-mount.service. The mount
|
|
# itself is the thing that matters, so check that too — it is the real
|
|
# invariant, independent of which unit established it.
|
|
if systemctl is-active --quiet guard-bind-mount@hosts.service 2>/dev/null ||
|
|
systemctl is-active --quiet hosts-bind-mount.service 2>/dev/null ||
|
|
findmnt -no OPTIONS /etc/hosts 2>/dev/null | grep -q '\bro\b'; then
|
|
test_result "/etc/hosts bind-mounted read-only" "pass"
|
|
else
|
|
test_result "/etc/hosts bind-mounted read-only" "fail" \
|
|
"no bind-mount unit active and /etc/hosts is not mounted read-only"
|
|
fi
|
|
|
|
# Test 4: Canonical hosts copy exists
|
|
if [[ -f /usr/local/share/locked-hosts ]]; then
|
|
test_result "Canonical hosts copy exists" "pass"
|
|
else
|
|
test_result "Canonical hosts copy exists" "fail" "Not found at /usr/local/share/locked-hosts"
|
|
fi
|
|
|
|
# Test 5: the nsswitch watcher is active
|
|
# guard-file@nsswitch.path superseded nsswitch-guard.path. The legacy unit is
|
|
# left in a failed state on migrated hosts (it and the new watcher both
|
|
# enforced the same file, so it retriggered itself into systemd's start
|
|
# limit) — that is obsolete-unit noise, not a lapse in protection.
|
|
if systemctl is-active --quiet guard-file@nsswitch.path 2>/dev/null ||
|
|
systemctl is-active --quiet nsswitch-guard.path 2>/dev/null; then
|
|
test_result "nsswitch watcher is active" "pass"
|
|
else
|
|
test_result "nsswitch watcher is active" "fail" \
|
|
"neither guard-file@nsswitch.path nor nsswitch-guard.path is running"
|
|
fi
|
|
|
|
# Test 6: /etc/nsswitch.conf is immutable (NEW)
|
|
if [[ -f /etc/nsswitch.conf ]]; then
|
|
if lsattr /etc/nsswitch.conf 2>/dev/null | grep -q '^....i'; then
|
|
test_result "/etc/nsswitch.conf is immutable" "pass"
|
|
else
|
|
test_result "/etc/nsswitch.conf is immutable" "fail" "chattr +i not set"
|
|
fi
|
|
else
|
|
test_result "/etc/nsswitch.conf is immutable" "skip" "File not found"
|
|
fi
|
|
|
|
# Test 7: nsswitch.conf has correct hosts line
|
|
if [[ -f /etc/nsswitch.conf ]]; then
|
|
hosts_line=$(grep "^hosts:" /etc/nsswitch.conf 2>/dev/null || true)
|
|
if echo "$hosts_line" | grep -q 'files.*dns\|files.*myhostname'; then
|
|
test_result "nsswitch.conf has 'files' before 'dns'" "pass"
|
|
elif [[ -z "$hosts_line" ]]; then
|
|
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "No hosts: line found"
|
|
else
|
|
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "hosts line: $hosts_line"
|
|
fi
|
|
else
|
|
test_result "nsswitch.conf has 'files' before 'dns'" "skip" "File not found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
}
|
|
|
|
sec_tests_shutdown_schedule() {
|
|
# ==================================================================
|
|
# SHUTDOWN SCHEDULE TESTS
|
|
# ==================================================================
|
|
echo "--- SHUTDOWN SCHEDULE ---"
|
|
|
|
# Test 8: shutdown-schedule.conf is immutable
|
|
if [[ -f /etc/shutdown-schedule.conf ]]; then
|
|
if lsattr /etc/shutdown-schedule.conf 2>/dev/null | grep -q '^....i'; then
|
|
test_result "/etc/shutdown-schedule.conf is immutable" "pass"
|
|
else
|
|
test_result "/etc/shutdown-schedule.conf is immutable" "fail" "chattr +i not set"
|
|
fi
|
|
else
|
|
test_result "/etc/shutdown-schedule.conf is immutable" "skip" "Not installed"
|
|
fi
|
|
|
|
# Test 9: shutdown timer is active
|
|
if systemctl is-active --quiet day-specific-shutdown.timer 2>/dev/null; then
|
|
test_result "day-specific-shutdown.timer is active" "pass"
|
|
else
|
|
test_result "day-specific-shutdown.timer is active" "fail" "Timer not running"
|
|
fi
|
|
|
|
# Test 10: shutdown schedule guard
|
|
# The shutdown flow no longer powers the machine off — day-specific-shutdown
|
|
# now hands over to night-lockdown-enter.sh, which tears down the GUI and masks
|
|
# the TTY login while leaving the servers up. This guard was retired with the
|
|
# old poweroff path, so a *disabled* unit is the intended state, not a fault.
|
|
# An enabled-but-inactive unit would still mean something is wrong, so those
|
|
# two cases are distinguished rather than both being tolerated.
|
|
# `systemctl is-enabled` prints the state on stdout *and* exits non-zero when
|
|
# that state is "disabled", so `|| echo disabled` would append a second line
|
|
# and never compare equal. Capture stdout and discard the exit status instead.
|
|
shutdown_guard_enabled=$(systemctl is-enabled shutdown-schedule-guard.path 2>/dev/null || true)
|
|
[[ -n "$shutdown_guard_enabled" ]] || shutdown_guard_enabled=unknown
|
|
if systemctl is-active --quiet guard-file@shutdown-schedule.path 2>/dev/null ||
|
|
systemctl is-active --quiet shutdown-schedule-guard.path 2>/dev/null; then
|
|
# guard-file@shutdown-schedule.path is the guard-lib replacement and is the
|
|
# unit actually protecting the schedule today, so this is a pass, not a
|
|
# concession that the schedule went unguarded.
|
|
test_result "shutdown schedule is guarded" "pass"
|
|
elif [[ "$shutdown_guard_enabled" == "disabled" ]]; then
|
|
test_result "shutdown-schedule-guard.path is active" "skip" \
|
|
"intentionally retired with the old poweroff flow (night lockdown replaced it)"
|
|
else
|
|
test_result "shutdown-schedule-guard.path is active" "fail" \
|
|
"unit is $shutdown_guard_enabled but not running"
|
|
fi
|
|
|
|
# Test 11: Unlock script has obscure name (no helpful path)
|
|
if [[ -f /usr/local/sbin/.sd-sched-mgmt ]]; then
|
|
test_result "Unlock script uses obscure name" "pass"
|
|
elif [[ -f /usr/local/sbin/unlock-shutdown-schedule ]]; then
|
|
test_result "Unlock script uses obscure name" "fail" "Still using obvious name"
|
|
else
|
|
test_result "Unlock script uses obscure name" "skip" "Not installed"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
}
|