#!/bin/sh
# Verify the ISM minimum passphrase length on a Debian 13 (trixie) host.
#
# The minimum is measured from behaviour, not read from minlen, so the result stays
# true when another component or a local policy also sets a minimum: a stricter one
# still satisfies the control. Where the minimum in force differs from what this
# component configured, something overrides a setting it owns, and that is reported
# separately, as a conflict.
#
# Uses pwscore(1). 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=$(ls /etc/security/pwquality.conf.d/50-ism-minlen-*.conf 2>/dev/null)
[ -n "$MINE" ] || { echo "error: no ISM minimum-length drop-in; run the apply script" >&2; exit 1; }
[ "$(printf '%s\n' "$MINE" | wc -l)" -eq 1 ] || {
	echo "error: more than one ISM minimum is installed; exactly one may be live:" >&2
	printf '         %s\n' $MINE >&2
	exit 1
}

# The control that sets the minimum, and the minimum it requires, follow from the
# applicability the installed drop-in records.
case "$(basename "$MINE")" in
	50-ism-minlen-nc-os-p.conf) CONTROL=ism-0421 THRESHOLD=15 APPLIES="NC, OS, P" ;;
	50-ism-minlen-s.conf)       CONTROL=ism-1557 THRESHOLD=17 APPLIES="S" ;;
	50-ism-minlen-ts.conf)      CONTROL=ism-0422 THRESHOLD=20 APPLIES="TS" ;;
	*) echo "error: unrecognised drop-in $MINE" >&2; exit 1 ;;
esac
CONFIGURED=$(awk -F= '/^[[:space:]]*minlen[[:space:]]*=/ {gsub(/[[:space:]]/, "", $2); print $2}' "$MINE")

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

EFFECTIVE=
n=1
while [ "$n" -le 128 ]; do
	if accepts "$(slice "$n")"; then EFFECTIVE=$n; break; fi
	n=$((n + 1))
done

echo "Configured minlen $CONFIGURED; minimum in force ${EFFECTIVE:-unmeasurable}; $CONTROL requires $THRESHOLD for $APPLIES"
echo

# A shorter candidate using every character class catches credits lowering the
# minimum, which an all-lowercase probe cannot see.
CREDITED="$(slice $((THRESHOLD - 5)))Q7#k"
if [ -z "$EFFECTIVE" ]; then
	report FAIL "$CONTROL" "no all-lowercase passphrase of up to 128 characters is accepted; the minimum cannot be measured"
elif [ "$EFFECTIVE" -lt "$THRESHOLD" ]; then
	report FAIL "$CONTROL" "a $EFFECTIVE-character passphrase is accepted; $APPLIES requires at least $THRESHOLD"
elif accepts "$CREDITED"; then
	report FAIL "$CONTROL" "a $((THRESHOLD - 1))-character passphrase using every character class is accepted; credits lower the minimum (apply debian-13-pwquality-no-complexity)"
else
	report PASS "$CONTROL" "nothing shorter than $EFFECTIVE characters is accepted; $APPLIES requires at least $THRESHOLD"
fi

if [ -z "$EFFECTIVE" ]; then
	report FAIL "minlen" "cannot compare configured minlen $CONFIGURED with the minimum in force"
elif [ "$EFFECTIVE" -ne "$CONFIGURED" ]; then
	report FAIL "minlen" "configured $CONFIGURED in $(basename "$MINE"), but $EFFECTIVE is in force; another configuration overrides it"
else
	report PASS "minlen" "configured $CONFIGURED in $(basename "$MINE") is the minimum in force"
fi

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