#!/usr/bin/env bash
# scripts/creds-probe.sh -- D-137 tier 3 BEHAVIOURAL probes (operator-run, credential-using).
#
# WHY THIS IS A SEPARATE SCRIPT, and not part of creds-matrix.py.
# A behavioural probe answers "does this credential still WORK", which requires USING it --
# i.e. reading the secret value. `creds-matrix.py` is metadata-only by an enforced
# invariant: its harness greps the source for content-reading verbs (T22) and for
# content-TRANSFER verbs (T51). Folding probes in would have meant relaxing that guard, and
# a guard relaxed once stays relaxed. So the credential-using component is separated,
# small, and auditable on its own terms: THIS is the only file in the control permitted to
# touch a secret value, and it still never prints one.
#
# BOUNDARY (operator-ruled 2026-07-26): full scope INCLUDING OpenStack service credentials.
# EXCLUDED BY CONSTRUCTION, not by configuration: per-tenant credentials. Probing one means
# authenticating AS a tenant on a commercial multi-tenant cloud; D-069 keeps tenant custody
# off-repo and D-137 ruling 3 already excluded tenant surface from discovery for the same
# reason. A `per-tenant` row is REFUSED below even if a probe is declared for it.
#
# SAFETY PROPERTIES, each deliberate:
#   * NEVER RETRY a failed authentication. Retry loops are what lock accounts, and the MAAS
#     `admin` identity is load-bearing for 19 call sites. One attempt, then report.
#   * NEVER put a secret in argv (visible in `ps`) -- values are passed by file or stdin.
#   * NEVER echo a secret. Probes report an outcome and an HTTP/exit status, nothing else.
#   * AUTHENTICATION IS A SIDE EFFECT. Every probe writes a login/audit record on its
#     target. That is unavoidable and is why this is operator-run at stage close and DC
#     standup close, never a deploy gate. (Measured precedent: a probe called read-only
#     during the 2026-07-25 SEC-020 work wrote `last_login` twice.)
#   * STAGE-AWARE. A credential whose mint stage is `pending` in
#     creds-manifests/stages-reached is SKIPPED explicitly, never reported failed.
#
# Usage:
#   bash scripts/creds-probe.sh --list          # what would be probed, no authentication
#   bash scripts/creds-probe.sh --run           # probe (AUTHENTICATES; writes audit records)
# Exit: 0 all probed credentials answered | 1 any probe failed | 2 usage/config error.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/.." && pwd)"
MATRIX="${MATRIX:-$REPO/creds-matrix.tsv}"
PROBES="${PROBES:-$REPO/creds-manifests/probes}"
STAGES="${STAGES:-$REPO/creds-manifests/stages-reached}"
CREDS_ROOT="${CREDS_ROOT:-$HOME}"

MODE="${1:---list}"
case "$MODE" in --list|--run) ;; *) echo "usage: creds-probe.sh --list|--run"; exit 2;; esac
[ -f "$PROBES" ] || { echo "FAIL: no probe declaration at $PROBES"; exit 2; }
[ -f "$MATRIX" ] || { echo "FAIL: no matrix at $MATRIX"; exit 2; }

P=0; F=0; S=0
ok()   { echo "  [ok]   $1"; P=$((P+1)); }
bad()  { echo "  [FAIL] $1"; F=$((F+1)); }
skip() { echo "  [skip] $1"; S=$((S+1)); }

stage_reached() { # stage_reached <token> -> 0 if reached
  awk -v t="$1" '!/^#/ && $1==t {print $2}' "$STAGES" 2>/dev/null | grep -q '^reached$'
}
cardinality_of() { awk -v i="$1" '!/^#/ && NF==12 && $1==i {print $2; exit}' "$MATRIX"; }
stage_of()       { awk -v i="$1" '!/^#/ && NF==12 && $1==i {print $9; exit}' "$MATRIX"; }

echo "=== creds-probe: D-137 tier 3 behavioural (${MODE#--}) ==="
[ "$MODE" = "--run" ] && echo "  NOTE: this AUTHENTICATES. Each probe writes an audit/login record on its target."

while read -r ID KIND TARGET; do
  case "$ID" in ''|\#*) continue;; esac

  card="$(cardinality_of "$ID")"
  if [ -z "$card" ]; then
    bad "$ID: declared as a probe but is in NO matrix row -- stale probe declaration"; continue
  fi
  # Tenant surface is refused by construction, not by configuration.
  if [ "$card" = "per-tenant" ]; then
    bad "$ID: per-tenant credentials are REFUSED (D-069 / ruling 3) -- remove this probe"; continue
  fi
  st="$(stage_of "$ID")"
  if ! stage_reached "$st"; then
    skip "$ID: mint-stage '$st' is pending -- not yet minted, so not probed"; continue
  fi
  if [ "$MODE" = "--list" ]; then
    ok "$ID: would probe via '$KIND' -> $TARGET"; continue
  fi

  case "$KIND" in
    ssh)
      # Narrowest possible authentication: opens the connection, runs `true`, no payload.
      # `-n` (and the redirect) are load-bearing, not belt-and-braces: without them ssh
      # consumes the while-read loop's stdin and every subsequent probe is silently
      # skipped. Measured on the first real run -- 1 of 13 probes executed and the run
      # still exited 0, which is a false green in the tool that exists to prevent them.
      if ssh -n -o BatchMode=yes -o ConnectTimeout=10 -o NumberOfPasswordPrompts=0 \
             "$TARGET" true </dev/null >/dev/null 2>&1; then
        ok "$ID: ssh key authenticates to $TARGET"
      else
        bad "$ID: ssh key FAILED to authenticate to $TARGET (not retried -- retry loops lock accounts)"
      fi ;;
    https-head)
      # Unauthenticated reachability of the endpoint the credential is FOR. Proves the
      # service answers without spending the credential; pairs with a declared-state check.
      code="$(curl -s -o /dev/null -w '%{http_code}' -m 10 -k "$TARGET" 2>/dev/null)"
      if [ -n "$code" ] && [ "$code" != "000" ]; then
        ok "$ID: endpoint $TARGET answers (HTTP $code) -- credential NOT spent"
      else
        bad "$ID: endpoint $TARGET did not answer"
      fi ;;
    declared-only)
      skip "$ID: declared non-probeable ($TARGET) -- validity rests on V1/V2, by decision" ;;
    *)
      bad "$ID: unknown probe kind '$KIND'" ;;
  esac
done < "$PROBES"

# Total accounting: every declared probe must be reported in exactly one bucket. Without
# this a loop that ate its own stdin (see the ssh -n note) reports a clean partial run.
DECLARED=$(awk '!/^#/ && NF>=3' "$PROBES" | wc -l)
SEEN=$((P + F + S))
if [ "$SEEN" != "$DECLARED" ]; then
  echo "  [FAIL] ACCOUNTING: $DECLARED probes declared but only $SEEN reported -- the run"
  echo "         did not reach every declaration; treat this run as VOID, not as a pass."
  F=$((F+1))
fi

echo
echo "creds-probe: $P ok, $F failed, $S skipped"
[ "$F" = 0 ] || exit 1
exit 0
