#!/bin/sh
# Check the server's hardening (ADR-006 §5). Reads only; changes nothing.
#
#   tools/server-check            # on the server; as root it can see a little more
#
# Prints one line per item: ok, WARN (should be fixed), FAIL (must be fixed). Exit status 1
# if anything FAILed. docs/procedures/server-setup.md says how to fix each item.
set -u

: "${CONTINUITY_ENV:=/etc/continuity/continuity.env}"
# shellcheck disable=SC1090
[ -r "$CONTINUITY_ENV" ] && . "$CONTINUITY_ENV"
: "${CONTINUITY_DATA:=/home/continuity/continuity-data}"
: "${CONTINUITY_USER:=continuity}"

fails=0
ok()   { printf 'ok    %s\n' "$*"; }
warn() { printf 'WARN  %s\n' "$*"; }
fail() { printf 'FAIL  %s\n' "$*"; fails=$((fails + 1)); }
have() { command -v "$1" >/dev/null 2>&1; }

# 1. Swap: the index and the app hold plaintext in RAM (ADR-003 §4).
swaps=$(awk 'NR > 1 { print $1 }' /proc/swaps)
if [ -z "$swaps" ]; then
    ok "no swap"
else
    plain=""
    for s in $swaps; do
        case $s in /dev/mapper/*|/dev/dm-*) ;; *) plain="$plain $s" ;; esac
    done
    if [ -z "$plain" ]; then ok "swap is on device-mapper (check that it is dm-crypt):$swaps"
    else fail "unencrypted swap:$plain (turn it off: swapoff -a, and remove it from /etc/fstab)"
    fi
fi

# 2. Core dumps would write plaintext to disk. The unit sets LimitCORE=0; check the system too.
pattern=$(cat /proc/sys/kernel/core_pattern 2>/dev/null || echo "")
case $pattern in
    "|/bin/false"*|"|/usr/bin/false"*) ok "core dumps are discarded (core_pattern)" ;;
    *systemd-coredump*)
        if grep -rqsE '^[[:space:]]*Storage[[:space:]]*=[[:space:]]*none' /etc/systemd/coredump.conf /etc/systemd/coredump.conf.d
        then ok "systemd-coredump stores nothing (Storage=none)"
        else warn "systemd-coredump may store core dumps on disk (set Storage=none)"
        fi ;;
    *) warn "core dumps go to '$pattern'; the unit disables them, but set kernel.core_pattern=|/bin/false" ;;
esac

# 3. SSH: keys only, no root login.
sshcfg=""
if have sshd && sshcfg=$(sshd -T 2>/dev/null) && [ -n "$sshcfg" ]; then :; else
    sshcfg=$(cat /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null | tr 'A-Z' 'a-z')
fi
sshval() { printf '%s\n' "$sshcfg" | tr 'A-Z' 'a-z' | awk -v k="$1" '$1 == k { print $2; exit }'; }
case $(sshval passwordauthentication) in
    no) ok "SSH password login is off" ;;
    *) fail "SSH password login is on or not set (PasswordAuthentication no)" ;;
esac
case $(sshval permitrootlogin) in
    no) ok "SSH root login is off" ;;
    prohibit-password|without-password) warn "SSH root login with keys is allowed (PermitRootLogin no is better)" ;;
    *) fail "SSH root login is allowed (PermitRootLogin no)" ;;
esac

# 4. The vault folder: owned by the continuity user, nobody else can enter.
if [ -d "$CONTINUITY_DATA" ]; then
    set -- $(stat -c '%U %a' "$CONTINUITY_DATA")
    if [ "$1" = "$CONTINUITY_USER" ] && [ "$2" = 700 ]; then ok "$CONTINUITY_DATA is $1, mode $2"
    else fail "$CONTINUITY_DATA is $1, mode $2 (want $CONTINUITY_USER, 700)"
    fi
else
    warn "$CONTINUITY_DATA does not exist (set CONTINUITY_DATA, or see server-setup.md)"
fi

# 5. The vault mounts: never allow_other, so other users' processes cannot read them.
mounts=$(awk -v m="$CONTINUITY_DATA/mnt/" 'index($2, m) == 1 { print $2 " " $4 }' /proc/self/mounts)
if [ -z "$mounts" ]; then
    ok "the vault is locked right now (no mounts)"
elif printf '%s\n' "$mounts" | grep -q 'allow_other'; then
    fail "a vault mount allows other users (allow_other): $(printf '%s' "$mounts" | grep allow_other | cut -d' ' -f1 | tr '\n' ' ')"
else
    ok "vault mounts are private to $CONTINUITY_USER ($(printf '%s\n' "$mounts" | wc -l | tr -d ' ') mounted)"
fi
if grep -qsE '^[[:space:]]*user_allow_other' /etc/fuse.conf; then
    warn "/etc/fuse.conf has user_allow_other (not needed; remove it)"
fi

# 6. Listening ports: the app and the index on loopback only; outside, only 22, 80, 443.
if have ss; then
    listen=$(ss -Hltn 2>/dev/null | awk '{ print $4 }')
    for port in 8437 5437; do
        exposed=$(printf '%s\n' "$listen" | grep -E ":$port\$" | grep -vE '^(127\.[0-9.]+|\[::1\]):' || true)
        if [ -n "$exposed" ]; then fail "port $port is reachable from outside: $exposed (must be 127.0.0.1 only)"
        else ok "port $port is loopback only (or closed)"
        fi
    done
    others=$(printf '%s\n' "$listen" | grep -vE '^(127\.[0-9.]+|\[::1\]|\[::ffff:127\.[0-9.]+\]):' \
        | sed 's/.*://' | grep -vxE '22|80|443' | sort -un | tr '\n' ' ')
    if [ -n "$others" ]; then warn "other ports listen on public addresses: $others(close or firewall them)"
    else ok "only 22, 80, 443 listen on public addresses"
    fi
else
    warn "cannot list ports (ss is missing)"
fi

# 7. A firewall.
if have ufw && ufw status 2>/dev/null | grep -q 'Status: active'; then ok "firewall (ufw) is active"
elif have nft && [ "$(nft list ruleset 2>/dev/null | wc -l)" -gt 0 ]; then ok "firewall rules (nftables) are loaded"
elif [ "$(id -u)" -ne 0 ]; then warn "cannot check the firewall without root"
else warn "no firewall found (ufw allow 22,80,443, then ufw enable)"
fi

# 8. Automatic security updates.
if grep -qsE 'Unattended-Upgrade[[:space:]]+"1"' /etc/apt/apt.conf.d/*; then ok "automatic security updates (unattended-upgrades)"
elif have systemctl && systemctl is-enabled -q dnf-automatic.timer 2>/dev/null; then ok "automatic updates (dnf-automatic)"
else warn "automatic security updates are not set up"
fi

# 9. Never started at boot: a reboot must leave the vault locked (charter §8).
if have systemctl; then
    state=$(systemctl is-enabled continuity.service 2>/dev/null || true)
    case $state in
        enabled|enabled-runtime|alias) fail "continuity.service starts at boot ($state); run: systemctl disable continuity.service" ;;
        ""|not-found) warn "continuity.service is not installed" ;;
        *) ok "continuity.service is not started at boot ($state)" ;;
    esac
fi

echo
if [ $fails -gt 0 ]; then echo "$fails item(s) FAILED."; exit 1; fi
echo "No failures."
