feat(defenseurs): GET /defenseurs/state serves findings-state.json #28

Merged
maximus merged 2 commits from issue-27-defenseurs-state into main 2026-09-25 01:41:35 +00:00
2 changed files with 27 additions and 1 deletions
Showing only changes of commit 7c14e581cb - Show all commits

View file

@ -67,7 +67,7 @@ curl -H "Authorization: Bearer $(cat ~/.coolify-token)" \
- `__tests__/hosts.test.js` — matrice exhaustive des endpoints postes (155 cas : regex de route comme controle anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche, 413, `neverSeen`, fraicheur serveur sous horloge figee, degradation de lecture) - `__tests__/hosts.test.js` — matrice exhaustive des endpoints postes (155 cas : regex de route comme controle anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche, 413, `neverSeen`, fraicheur serveur sous horloge figee, degradation de lecture)
- `__tests__/agent.test.js` — agent poste (50 cas : contrat de payload verifie contre `sanitizeSnapshot()`, une seule tentative sans rejeu, timeout, codes de sortie, wrapper shell, et **le token absent de toute sortie d'echec, fragments de 5 caracteres compris**) - `__tests__/agent.test.js` — agent poste (50 cas : contrat de payload verifie contre `sanitizeSnapshot()`, une seule tentative sans rejeu, timeout, codes de sortie, wrapper shell, et **le token absent de toute sortie d'echec, fragments de 5 caracteres compris**)
- Runtime reste 0-dep ; vitest en devDep uniquement - Runtime reste 0-dep ; vitest en devDep uniquement
- `bash test-curl.sh` — passe au niveau socket, complementaire et non redondante (41 cas). Vitest couvre la logique ; ce script couvre ce que seuls un vrai client et un vrai TCP peuvent montrer : le 413 rendu **avant** que la connexion soit coupee, les deux tokens qui se refusent mutuellement leurs routes, et le 503 d'une instance demarree sans `HOSTS_INGEST_TOKEN` (elle tourne sur un second port, le temps du script). Il boote ses propres serveurs sur 3099 et 3098 — aucun service exterieur requis. - `bash test-curl.sh` — passe au niveau socket, complementaire et non redondante (45 cas). Vitest couvre la logique ; ce script couvre ce que seuls un vrai client et un vrai TCP peuvent montrer : le 413 rendu **avant** que la connexion soit coupee, les deux tokens qui se refusent mutuellement leurs routes, le fichier servi octet pour octet par `/defenseurs/state`, et le 503 d'une instance demarree sans `HOSTS_INGEST_TOKEN` (elle tourne sur un second port, le temps du script). Il boote ses propres serveurs sur 3099 et 3098 — aucun service exterieur requis.
## Gotchas ## Gotchas

View file

@ -126,11 +126,18 @@ JSON
# be writable — the same requirement the /data/hosts mount carries in prod. # be writable — the same requirement the /data/hosts mount carries in prod.
mkdir -p "$TMP_DIR/hosts" mkdir -p "$TMP_DIR/hosts"
# Sergent findings state, served verbatim by GET /defenseurs/state. The spacing
# is deliberately non-canonical: a server that parsed and re-serialised the file
# would not reproduce it byte for byte.
printf '%s\n' '{ "schemaVersion":1, "findings": {"a-deps-1": {"firstSeen":"2026-09-01T00:00:00.000Z"}},"history":[] }' \
> "$TMP_DIR/findings-state.json"
# Boot the server with the temp REPORTS_DIR and HOSTS_DIR. # Boot the server with the temp REPORTS_DIR and HOSTS_DIR.
PORT=3099 \ PORT=3099 \
HEALTH_TOKEN="$TOKEN" \ HEALTH_TOKEN="$TOKEN" \
REPORTS_DIR="$TMP_DIR/reports" \ REPORTS_DIR="$TMP_DIR/reports" \
HOSTS_DIR="$TMP_DIR/hosts" \ HOSTS_DIR="$TMP_DIR/hosts" \
DEFENSEURS_FINDINGS_STATE_PATH="$TMP_DIR/findings-state.json" \
HOSTS_ALLOWED_IDS="thinkpad,workbench" \ HOSTS_ALLOWED_IDS="thinkpad,workbench" \
HOSTS_INGEST_TOKEN="$INGEST_TOKEN" \ HOSTS_INGEST_TOKEN="$INGEST_TOKEN" \
LOGTO_HEALTH_URL="http://127.0.0.1:1/never" \ LOGTO_HEALTH_URL="http://127.0.0.1:1/never" \
@ -145,6 +152,7 @@ PORT=3098 \
HEALTH_TOKEN="$TOKEN" \ HEALTH_TOKEN="$TOKEN" \
REPORTS_DIR="$TMP_DIR/reports" \ REPORTS_DIR="$TMP_DIR/reports" \
HOSTS_DIR="$TMP_DIR/hosts-noingest" \ HOSTS_DIR="$TMP_DIR/hosts-noingest" \
DEFENSEURS_FINDINGS_STATE_PATH="$TMP_DIR/absent-findings-state.json" \
HOSTS_ALLOWED_IDS="thinkpad" \ HOSTS_ALLOWED_IDS="thinkpad" \
LOGTO_HEALTH_URL="http://127.0.0.1:1/never" \ LOGTO_HEALTH_URL="http://127.0.0.1:1/never" \
node "$(dirname "$0")/index.js" >/dev/null 2>&1 & node "$(dirname "$0")/index.js" >/dev/null 2>&1 &
@ -444,6 +452,24 @@ code=$(http_code -X POST -H "Authorization: Bearer $INGEST_TOKEN" -H "Content-Ty
code=$(http_code -H "Authorization: Bearer $TOKEN" "$NOINGEST_URL/hosts") code=$(http_code -H "Authorization: Bearer $TOKEN" "$NOINGEST_URL/hosts")
[[ "$code" == "200" ]] && pass "no HOSTS_INGEST_TOKEN -> GET /hosts still 200" || fail "read path on fail-closed instance -> got $code" [[ "$code" == "200" ]] && pass "no HOSTS_INGEST_TOKEN -> GET /hosts still 200" || fail "read path on fail-closed instance -> got $code"
# Case 32 : GET /defenseurs/state serves findings-state.json byte for byte.
# Compared through files so the trailing newline is part of the comparison
# (command substitution would strip it).
http_body -H "Authorization: Bearer $TOKEN" "$BASE_URL/defenseurs/state" > "$TMP_DIR/state-served.json"
cmp -s "$TMP_DIR/state-served.json" "$TMP_DIR/findings-state.json" \
&& pass "GET /defenseurs/state -> file served byte for byte" \
|| fail "GET /defenseurs/state -> body differs from the file"
# Absent file -> 200 no_data, the same contract as /defenseurs.
body=$(http_body -H "Authorization: Bearer $TOKEN" "$NOINGEST_URL/defenseurs/state")
[[ "$body" == '{"status":"no_data"}' ]] \
&& pass "GET /defenseurs/state, file absent -> no_data" \
|| fail "GET /defenseurs/state absent -> $body"
code=$(http_code "$BASE_URL/defenseurs/state")
[[ "$code" == "401" ]] && pass "GET /defenseurs/state no-auth -> 401" || fail "GET /defenseurs/state no-auth -> got $code"
# A read route: the ingest token must not open it.
code=$(http_code -H "Authorization: Bearer $INGEST_TOKEN" "$BASE_URL/defenseurs/state")
[[ "$code" == "401" ]] && pass "GET /defenseurs/state with ingest token -> 401" || fail "GET /defenseurs/state ingest token -> got $code"
echo echo
echo "=== Results: $PASS passed, $FAIL failed ===" echo "=== Results: $PASS passed, $FAIL failed ==="
[[ "$FAIL" == "0" ]] || exit 1 [[ "$FAIL" == "0" ]] || exit 1