#!/bin/sh
# Verify the pam_pwquality enforcement base on a Debian 13 (trixie) host.
#
# Checks assert the controls, not this component's settings, so results stay true
# when the rule components are applied alongside it. Settings this component owns
# being set elsewhere is reported separately, as a conflict.
#
# Uses pwscore(1) against the live configuration. No account is created or
# modified, so this is safe on a production host.

set -u

PASS_COUNT=0
FAIL_COUNT=0

report() {
	case "$1" in PASS) PASS_COUNT=$((PASS_COUNT + 1)) ;; *) FAIL_COUNT=$((FAIL_COUNT + 1)) ;; esac
	printf '%-8s %-13s %s\n' "$1" "$2" "$3"
}

accepts() { printf '%s' "$1" | pwscore >/dev/null 2>&1; }

command -v pwscore >/dev/null 2>&1 || { echo "error: pwscore not found; install libpwquality-tools" >&2; exit 1; }

MINE=/etc/security/pwquality.conf.d/40-ism-base.conf
[ -f "$MINE" ] || { echo "error: $MINE is not installed; run the apply script" >&2; exit 1; }

FILLER=vaultottermangoquiltravenfjordzebraplinthwidgetcarbonhelixjunipermarrow
slice() { printf '%s%s' "$FILLER" "$FILLER" | cut -c1-"$1"; }

# The minimum in force, measured rather than read, so candidates below are never
# rejected for length when another rule is under test.
FLOOR=
n=1
while [ "$n" -le 128 ]; do
	if accepts "$(slice "$n")"; then FLOOR=$n; break; fi
	n=$((n + 1))
done
: "${FLOOR:=6}"

echo "Minimum in force: $FLOOR characters"
echo

# --- ism-2079 : maximum length limit is not less than 64 ---------------------
LONG_LEN=$((FLOOR > 64 ? FLOOR + 8 : 72))
LONG_OK=0
if accepts "$(slice "$LONG_LEN")"; then
	LONG_OK=1
	report PASS "ism-2079" "$LONG_LEN-character passphrase accepted; no maximum below 64"
else
	report FAIL "ism-2079" "$LONG_LEN-character passphrase rejected; a maximum below 64 is imposed"
fi

# --- ism-2081 : all ASCII printable characters are supported -----------------
# Punctuation interleaved with letters: the punctuation set is itself a monotonic
# codepoint run, which a maxsequence rule would rightly reject.
ASCII_PUNCT=' !"#$%&'"'"'()*+,-./:;<=>?@[\]^_`{|}~'
ASCII_MIX=''
i=1
while [ "$i" -le "${#ASCII_PUNCT}" ]; do
	ASCII_MIX="${ASCII_MIX}$(printf '%s' "$FILLER" | cut -c"$i")$(printf '%s' "$ASCII_PUNCT" | cut -c"$i")"
	i=$((i + 1))
done
[ "${#ASCII_MIX}" -ge "$FLOOR" ] || ASCII_MIX="${ASCII_MIX}$(slice $((FLOOR - ${#ASCII_MIX})))"
ASCII_OK=0
if accepts "$ASCII_MIX"; then
	ASCII_OK=1
	report PASS "ism-2081" "passphrase spanning the ASCII printable range accepted"
else
	report FAIL "ism-2081" "ASCII printable passphrase of ${#ASCII_MIX} characters rejected"
fi

# --- PAM stack wiring and the settings this component owns --------------------
STACK_OK=0
if grep -qE '^[^#]*requisite[[:space:]]+pam_pwquality\.so' /etc/pam.d/common-password; then
	STACK_OK=1
	report PASS "pam-stack" "pam_pwquality.so is requisite in /etc/pam.d/common-password"
else
	report FAIL "pam-stack" "pam_pwquality.so is not requisite in /etc/pam.d/common-password"
fi

OVERRIDES=''
for f in /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf; do
	[ -f "$f" ] && [ "$f" != "$MINE" ] || continue
	for key in enforcing enforce_for_root local_users_only retry; do
		grep -qE "^[[:space:]]*$key([[:space:]]*=|[[:space:]]*\$)" "$f" &&
			OVERRIDES="$OVERRIDES $key($(basename "$f"))"
	done
done
if [ -n "$OVERRIDES" ]; then
	report FAIL "settings" "settings this component owns are also set elsewhere:$OVERRIDES"
elif grep -qE '^[[:space:]]*enforcing[[:space:]]*=[[:space:]]*1' "$MINE"; then
	report PASS "settings" "enforcing = 1 and enforce_for_root are set here and nowhere else"
else
	report FAIL "settings" "enforcing is not set to 1 in $MINE"
fi

# --- IA-5(1) statements this component claims ---------------------------------
[ "$STACK_OK" -eq 1 ] &&
	report PASS "ia-5.1_smt.b" "every password change passes pam_pwquality before pam_unix" ||
	report FAIL "ia-5.1_smt.b" "pam_pwquality is not checked on password change"
[ "$LONG_OK" -eq 1 ] && [ "$ASCII_OK" -eq 1 ] &&
	report PASS "ia-5.1_smt.f" "long passphrases and the full ASCII printable range accepted" ||
	report FAIL "ia-5.1_smt.f" "long or ASCII printable passphrases rejected"
command -v pwmake >/dev/null 2>&1 &&
	report PASS "ia-5.1_smt.g" "pwmake(1) and pwscore(1) installed" ||
	report FAIL "ia-5.1_smt.g" "pwmake(1) is not installed"

echo
echo "passed: $PASS_COUNT  failed: $FAIL_COUNT"
[ "$FAIL_COUNT" -eq 0 ] || exit 1
