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
Owner

Fixes #27

Summary

  • New read route GET /defenseurs/state serving the Sergent's findings-state.json verbatim (DEFENSEURS_FINDINGS_STATE_PATH, default /data/defenseurs/findings-state.json, already covered by the /data/defenseurs mount).
  • serveJsonFile() extracted and shared with /defenseurs: raw content, or 200 {"status":"no_data"} on any read error. No parse, no freshness check (decisions from the /analyze pass).
  • Routing/auth untouched: one new ROUTES descriptor with tokenKind: "read".

Tests

  • npm test: 263/263 (new __tests__/state.test.js, 9 cases; /defenseurs/state added to the auth.test.js 401/404 matrix, +3).
  • bash test-curl.sh: 45/45. Rebased on #26 (merged), then 4 socket-level cases added for /defenseurs/state: file served byte for byte (compared with cmp), no_data when absent, 401 without token, 401 with the ingest token.
  • Mutations: state handler pointed at status.json -> 2 red; parse + re-serialise in serveJsonFile() -> 2 red in vitest, 1 red in test-curl.sh; restored, suites green.

Docs

README and CLAUDE.md: endpoint, config vars (DEFENSEURS_STATUS_PATH was undocumented, added too), mount section, test counts. The README mount list claimed per-file mounts from /home/defenseur/...; it now matches the real /data/defenseurs directory mount described in CLAUDE.md.

Deploy

No auto-deploy: after merge, force a Coolify redeploy, then curl -H "Authorization: Bearer $TOKEN" https://health.lacompagniemaximus.com/defenseurs/state should answer 200 (no_data until Sergent v2 has run).

🤖 Generated with Claude Code

Fixes #27 ## Summary - New read route `GET /defenseurs/state` serving the Sergent's `findings-state.json` verbatim (`DEFENSEURS_FINDINGS_STATE_PATH`, default `/data/defenseurs/findings-state.json`, already covered by the `/data/defenseurs` mount). - `serveJsonFile()` extracted and shared with `/defenseurs`: raw content, or 200 `{"status":"no_data"}` on any read error. No parse, no freshness check (decisions from the /analyze pass). - Routing/auth untouched: one new `ROUTES` descriptor with `tokenKind: "read"`. ## Tests - `npm test`: 263/263 (new `__tests__/state.test.js`, 9 cases; `/defenseurs/state` added to the `auth.test.js` 401/404 matrix, +3). - `bash test-curl.sh`: 45/45. Rebased on #26 (merged), then 4 socket-level cases added for `/defenseurs/state`: file served byte for byte (compared with `cmp`), `no_data` when absent, 401 without token, 401 with the ingest token. - Mutations: state handler pointed at `status.json` -> 2 red; parse + re-serialise in `serveJsonFile()` -> 2 red in vitest, 1 red in `test-curl.sh`; restored, suites green. ## Docs README and CLAUDE.md: endpoint, config vars (`DEFENSEURS_STATUS_PATH` was undocumented, added too), mount section, test counts. The README mount list claimed per-file mounts from `/home/defenseur/...`; it now matches the real `/data/defenseurs` directory mount described in CLAUDE.md. ## Deploy No auto-deploy: after merge, force a Coolify redeploy, then `curl -H "Authorization: Bearer $TOKEN" https://health.lacompagniemaximus.com/defenseurs/state` should answer 200 (`no_data` until Sergent v2 has run). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-09-25 00:31:38 +00:00
The Sergent now persists per-finding state (firstSeen, unverified, 30-day
history) in findings-state.json next to status.json. The admin dashboard
needs it for the drill-down, since it is the only source carrying a
finding's age.

- New read route GET /defenseurs/state, path from
  DEFENSEURS_FINDINGS_STATE_PATH (default /data/defenseurs/findings-state.json),
  already covered by the /data/defenseurs mount.
- Extract serveJsonFile() so /defenseurs and /defenseurs/state share one
  contract: raw content, or 200 {status:"no_data"} on any read error.
- Tests: new state.test.js (byte-identical body, no_data, path isolation,
  401 x3, 404 on POST) and the route added to the auth.test.js matrix.
- Docs: README and CLAUDE.md endpoint, config and mount sections; the README
  mount list now matches the real /data/defenseurs directory mount.

Fixes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus force-pushed issue-27-defenseurs-state from 64e4669619 to 7c14e581cb 2026-09-25 00:42:59 +00:00 Compare
Author
Owner

Revue adversariale — verdict : APPROVE

Remplace la revue du 2026-09-25 00:34Z (commentaire supprime) : elle precedait le rebase sur #26 et le commit 7c14e58 (passe socket).

Resume : un seul descripteur ROUTES (tokenKind: "read") ajoute la route ; l'ordre routage -> auth reste intact et l'extraction de serveJsonFile() ne change pas le comportement de /defenseurs. Le tip 7c14e58 (base = main apres #26) tient sous mutation, en vitest comme en passe socket.

Verifications (sur le tip 7c14e58)

  • npm test : 263/263. bash test-curl.sh : 45/45 (41 herites de #26 + 4 nouveaux, ports 3099/3098 libres avant lancement).
  • Mutations sur index.js (sans modification non commitee avant ; backup puis restauration, git diff --stat vide a la fin) :
    • handler state pointe sur DEFENSEURS_STATUS_PATH -> 2 rouges (octet pour octet, isolation des chemins) ;
    • tokenKind: "ingest" sur la route -> 4 rouges ;
    • parse puis re-serialisation dans serveJsonFile() -> 2 rouges (le JSON non canonique des fixtures fait son travail) ;
    • descripteur retire de ROUTES -> 8 rouges.
  • test-curl.sh : la comparaison passe par cmp sur fichiers, le newline final entre donc dans le controle (une substitution de commande l'aurait mange). L'instance 3098 recoit un chemin absent pour couvrir no_data sans fixture supplementaire.
  • Securite : chemin tire de l'env seulement, aucun input utilisateur, pas de traversee possible ; Content-Type: application/json pose globalement. Pas de secret dans le diff.
  • Commits conventionnels, Fixes #27 dans le message de a5d1bcd.

Suggestions (non bloquantes)

  1. Corps de PR perime : il annonce encore « test-curl.sh : 17/17. Left unchanged on purpose: PR #26 rewrites it » et un conflit attendu sur CLAUDE.md avec #26. Or #26 est merge, la branche est rebasee dessus, 7c14e58 modifie test-curl.sh et CLAUDE.md annonce 45 cas (exact). Mettre le corps a jour pour que l'historique de la PR dise ce qui a ete merge.
  2. serveJsonFile() : readFileSync a chaque requete sur un fichier qui porte 30 jours d'historique. Negligeable pour un seul consommateur ; a surveiller si le fichier depasse quelques Mo.
  3. L'absence de parse repose sur une garantie du Sergent (ecriture atomique, quarantaine .corrupt-*) qui n'est testee que dans defenseurs. S'assurer qu'un test l'y tient.
  4. state.test.js « 404 (not 401) on POST » recoupe la matrice d'auth.test.js. Redondance sans cout.
## Revue adversariale — verdict : APPROVE *Remplace la revue du 2026-09-25 00:34Z (commentaire supprime) : elle precedait le rebase sur #26 et le commit `7c14e58` (passe socket).* **Resume** : un seul descripteur `ROUTES` (`tokenKind: "read"`) ajoute la route ; l'ordre routage -> auth reste intact et l'extraction de `serveJsonFile()` ne change pas le comportement de `/defenseurs`. Le tip `7c14e58` (base = main apres #26) tient sous mutation, en vitest comme en passe socket. ### Verifications (sur le tip `7c14e58`) - `npm test` : **263/263**. `bash test-curl.sh` : **45/45** (41 herites de #26 + 4 nouveaux, ports 3099/3098 libres avant lancement). - **Mutations** sur `index.js` (sans modification non commitee avant ; backup puis restauration, `git diff --stat` vide a la fin) : - handler state pointe sur `DEFENSEURS_STATUS_PATH` -> **2 rouges** (octet pour octet, isolation des chemins) ; - `tokenKind: "ingest"` sur la route -> **4 rouges** ; - parse puis re-serialisation dans `serveJsonFile()` -> **2 rouges** (le JSON non canonique des fixtures fait son travail) ; - descripteur retire de `ROUTES` -> **8 rouges**. - `test-curl.sh` : la comparaison passe par `cmp` sur fichiers, le newline final entre donc dans le controle (une substitution de commande l'aurait mange). L'instance 3098 recoit un chemin absent pour couvrir `no_data` sans fixture supplementaire. - Securite : chemin tire de l'env seulement, aucun input utilisateur, pas de traversee possible ; `Content-Type: application/json` pose globalement. Pas de secret dans le diff. - Commits conventionnels, `Fixes #27` dans le message de `a5d1bcd`. ### Suggestions (non bloquantes) 1. **Corps de PR perime** : il annonce encore « `test-curl.sh` : 17/17. Left unchanged on purpose: PR #26 rewrites it » et un conflit attendu sur `CLAUDE.md` avec #26. Or #26 est merge, la branche est rebasee dessus, `7c14e58` modifie `test-curl.sh` et `CLAUDE.md` annonce 45 cas (exact). Mettre le corps a jour pour que l'historique de la PR dise ce qui a ete merge. 2. `serveJsonFile()` : `readFileSync` a chaque requete sur un fichier qui porte 30 jours d'historique. Negligeable pour un seul consommateur ; a surveiller si le fichier depasse quelques Mo. 3. L'absence de parse repose sur une garantie du Sergent (ecriture atomique, quarantaine `.corrupt-*`) qui n'est testee que dans `defenseurs`. S'assurer qu'un test l'y tient. 4. `state.test.js` « 404 (not 401) on POST » recoupe la matrice d'`auth.test.js`. Redondance sans cout.
maximus merged commit 8eef50d517 into main 2026-09-25 01:41:35 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#28
No description provided.