feat(defenseurs): add GET /defenseurs/findings?project=X route #9

Merged
maximus merged 1 commit from issue-3-defenseurs-findings into main 2026-07-16 00:45:10 +00:00
Owner

Fixes #3

Summary

Adds GET /defenseurs/findings?project=X to drill into a project's Defenseur findings via HTTP+Bearer. Designed for the Vercel admin dashboard (no SSH/Tailscale path) and a future portable /analyse-vulnerabilite skill.

Behavior

Case Response
Missing/invalid token 401
Missing project param 400
Invalid category / severity 400
Unknown project (not in agents-map.json) 404
Project known, no report on file 200 {findings: [], status: "no_data"}
Project known, report present (clean) 200 {agent, project, timestamp, findings: []} (no status field)
Project known, report present (findings) 200 {agent, project, timestamp, findings: [...]}
agents-map.json unreadable / corrupted 500

Severity rule (asymmetric — see issue #3 clarif 2026-05-12)

  • No severity -> MEDIUM+HIGH+CRITICAL (default hides noise)
  • severity=LOW -> LOW+MEDIUM+HIGH+CRITICAL but always hides INFO
  • severity=INFO -> INFO only (explicit opt-in)

Implementation

  • New helpers: allowedSeverities(threshold), findLatestReportForAgent(agent) (scans REPORTS_DIR and REPORTS_DIR/archive, picks the freshest by timestamp)
  • New env var: DEFENSEURS_AGENTS_MAP_PATH (default /data/defenseurs/agents-map.json)
  • New route added to validRoutes
  • Module refactor: handler exported, server.listen guarded by require.main === module so tests can spin up ephemeral servers

Tests

Bootstraps vitest (devDep; runtime stays 0-dep). 14 tests, all green:

  • 401 (no header, wrong token)
  • 400 (no project)
  • 404 (unknown project)
  • 200 default → MEDIUM+HIGH+CRITICAL (hides LOW+INFO)
  • 200 ?category=deps exact match
  • 200 ?severity=HIGH → HIGH+CRITICAL
  • 200 ?severity=LOW → LOW+MEDIUM+HIGH+CRITICAL, hides INFO
  • 200 ?severity=INFO → INFO only
  • 200 combined filters
  • 200 latest report selection across archive + top-level
  • 200 scan clean → no status field
  • 200 no report → {findings:[], status:"no_data"}
  • 500 corrupt agents-map.json

Pre-merge checklist (operational, out of repo)

  • SSH check VPS : ls /home/defenseur/defenseurs/agents-map.json (j'ai pas pu valider — Tailscale SSH demande auth navigateur)
  • Coolify : ajouter le bind-mount RO /home/defenseur/defenseurs/agents-map.json -> /data/defenseurs/agents-map.json avant le deploy (sinon la route renvoie 500)

Test plan

  • npm test -> 14/14 vert
  • Verifier agents-map.json present sur le VPS
  • Configurer le bind-mount sur Coolify
  • Apres deploy : curl -H "Authorization: Bearer $TOKEN" "https://health.lacompagniemaximus.com/defenseurs/findings?project=la-suite-booking&severity=HIGH"

🤖 Generated with Claude Code

Fixes #3 ## Summary Adds `GET /defenseurs/findings?project=X` to drill into a project's Defenseur findings via HTTP+Bearer. Designed for the Vercel admin dashboard (no SSH/Tailscale path) and a future portable `/analyse-vulnerabilite` skill. ## Behavior | Case | Response | |------|----------| | Missing/invalid token | 401 | | Missing `project` param | 400 | | Invalid `category` / `severity` | 400 | | Unknown project (not in `agents-map.json`) | 404 | | Project known, no report on file | 200 `{findings: [], status: "no_data"}` | | Project known, report present (clean) | 200 `{agent, project, timestamp, findings: []}` (no `status` field) | | Project known, report present (findings) | 200 `{agent, project, timestamp, findings: [...]}` | | `agents-map.json` unreadable / corrupted | 500 | ### Severity rule (asymmetric — see issue #3 clarif 2026-05-12) - No `severity` -> `MEDIUM`+`HIGH`+`CRITICAL` (default hides noise) - `severity=LOW` -> `LOW`+`MEDIUM`+`HIGH`+`CRITICAL` but **always hides `INFO`** - `severity=INFO` -> `INFO` only (explicit opt-in) ## Implementation - New helpers: `allowedSeverities(threshold)`, `findLatestReportForAgent(agent)` (scans `REPORTS_DIR` and `REPORTS_DIR/archive`, picks the freshest by `timestamp`) - New env var: `DEFENSEURS_AGENTS_MAP_PATH` (default `/data/defenseurs/agents-map.json`) - New route added to `validRoutes` - Module refactor: handler exported, `server.listen` guarded by `require.main === module` so tests can spin up ephemeral servers ## Tests Bootstraps vitest (devDep; runtime stays 0-dep). 14 tests, all green: - 401 (no header, wrong token) - 400 (no project) - 404 (unknown project) - 200 default → MEDIUM+HIGH+CRITICAL (hides LOW+INFO) - 200 `?category=deps` exact match - 200 `?severity=HIGH` → HIGH+CRITICAL - 200 `?severity=LOW` → LOW+MEDIUM+HIGH+CRITICAL, hides INFO - 200 `?severity=INFO` → INFO only - 200 combined filters - 200 latest report selection across `archive` + top-level - 200 scan clean → no `status` field - 200 no report → `{findings:[], status:"no_data"}` - 500 corrupt `agents-map.json` ## Pre-merge checklist (operational, out of repo) - [ ] **SSH check VPS** : `ls /home/defenseur/defenseurs/agents-map.json` (j'ai pas pu valider — Tailscale SSH demande auth navigateur) - [ ] **Coolify** : ajouter le bind-mount RO `/home/defenseur/defenseurs/agents-map.json` -> `/data/defenseurs/agents-map.json` avant le deploy (sinon la route renvoie 500) ## Test plan - [x] `npm test` -> 14/14 vert - [ ] Verifier `agents-map.json` present sur le VPS - [ ] Configurer le bind-mount sur Coolify - [ ] Apres deploy : `curl -H "Authorization: Bearer $TOKEN" "https://health.lacompagniemaximus.com/defenseurs/findings?project=la-suite-booking&severity=HIGH"` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-05-13 00:57:15 +00:00
Drill-down endpoint exposing detailed findings per project. Resolves the
HTTP gap for the Vercel admin dashboard, which cannot SSH/Tailscale to
the VPS, plus a future portable /analyse-vulnerabilite skill.

- Project -> agent lookup via /data/defenseurs/agents-map.json (Sergent snapshot)
- findLatestReportForAgent scans REPORTS_DIR + REPORTS_DIR/archive (post-07:30 UTC rotation)
- Filters: category exact match, severity threshold inclusive upward
- Asymmetric severity rule: default hides LOW+INFO; ?severity=LOW returns
  LOW+MEDIUM+HIGH+CRITICAL but still hides INFO; INFO opt-in via explicit param
- Distinguishes "report present + scan clean" (no status field) from
  "no report at all" ({findings:[], status:"no_data"})
- Bootstraps vitest (devDep; runtime stays 0-dep), 14 tests covering auth,
  validation, filters, asymmetry, mtime selection, error paths
- Refactor: export handler so tests can spin up ephemeral servers; server.listen
  guarded by require.main === module

Closes #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
maximus added the
source:human
status:review
type:feature
labels 2026-05-13 00:57:20 +00:00
Author
Owner

Verdict: APPROVE

Clean implementation of /defenseurs/findings matching the issue #3 spec (asymmetric INFO severity rule included). 14 vitest tests cover auth, validation, filter combinations, latest-report-across-archive, scan-clean vs no_data, and corrupt agents-map.json — all green locally. No security regressions: path traversal is structurally prevented (project is a map key, never a path component); the agent value used as filename prefix comes from the server-controlled agents-map.json written by the Sergent.

Suggestions (non-blocking)

  1. .env.example missing DEFENSEURS_AGENTS_MAP_PATH — the new env var is documented in CLAUDE.md and README.md but not in .env.example. Worth adding for parity.
  2. _ts mutation in findLatestReportForAgent (index.js:195-205) — assigning then delete-ing _ts on the parsed report works but mutates the user-facing payload. A separate latestTs variable would keep the parsed object pristine. Cosmetic.
  3. 500 leaks err.message for corrupt agents-map.json (index.js:307-309). Consistent with the existing /reports/scans 500 branch — not a new regression, but worth either suppressing in both places or explicitly accepting as intentional for a Bearer-gated internal API.
  4. No-data response shape divergence{findings:[], status:"no_data"} omits agent/project/timestamp while the present-report case includes them. Spec-aligned and tested, but consumers (Vercel dashboard, /analyse-vulnerabilite) will need to handle the union. Worth flagging in the README field table.
  5. Pre-merge ops checklist (Coolify bind-mount + agents-map.json presence on VPS) is the real risk — code is correct but deploy without those surfaces as 500s. Verify both before merging.

Checks

  • Tests pass locally (npm test -> 14/14)
  • No secrets in diff
  • Conventional commit format (feat(defenseurs): ...)
  • References issue (#3)
  • Module refactor (require.main === module guard) is correctly scoped — only the server.listen is gated, the handler/exports are always available
  • Route ordering: /defenseurs/findings checked before the fallthrough to /health and not shadowed by /defenseurs (exact-equality routing)

Adversarial review by Claude Code.

## Verdict: APPROVE Clean implementation of `/defenseurs/findings` matching the issue #3 spec (asymmetric INFO severity rule included). 14 vitest tests cover auth, validation, filter combinations, latest-report-across-archive, scan-clean vs no_data, and corrupt `agents-map.json` — all green locally. No security regressions: path traversal is structurally prevented (`project` is a map key, never a path component); the `agent` value used as filename prefix comes from the server-controlled `agents-map.json` written by the Sergent. ### Suggestions (non-blocking) 1. **`.env.example` missing `DEFENSEURS_AGENTS_MAP_PATH`** — the new env var is documented in `CLAUDE.md` and `README.md` but not in `.env.example`. Worth adding for parity. 2. **`_ts` mutation in `findLatestReportForAgent`** (`index.js:195-205`) — assigning then `delete`-ing `_ts` on the parsed report works but mutates the user-facing payload. A separate `latestTs` variable would keep the parsed object pristine. Cosmetic. 3. **500 leaks `err.message`** for corrupt `agents-map.json` (`index.js:307-309`). Consistent with the existing `/reports/scans` 500 branch — not a new regression, but worth either suppressing in both places or explicitly accepting as intentional for a Bearer-gated internal API. 4. **No-data response shape divergence** — `{findings:[], status:"no_data"}` omits `agent/project/timestamp` while the present-report case includes them. Spec-aligned and tested, but consumers (Vercel dashboard, `/analyse-vulnerabilite`) will need to handle the union. Worth flagging in the README field table. 5. **Pre-merge ops checklist** (Coolify bind-mount + `agents-map.json` presence on VPS) is the real risk — code is correct but deploy without those surfaces as 500s. Verify both before merging. ### Checks - [x] Tests pass locally (`npm test` -> 14/14) - [x] No secrets in diff - [x] Conventional commit format (`feat(defenseurs): ...`) - [x] References issue (#3) - [x] Module refactor (`require.main === module` guard) is correctly scoped — only the `server.listen` is gated, the handler/exports are always available - [x] Route ordering: `/defenseurs/findings` checked before the fallthrough to `/health` and not shadowed by `/defenseurs` (exact-equality routing) Adversarial review by Claude Code.
Author
Owner

Review — APPROVE

Verdict : APPROVE

Résumé : Implémentation propre et conforme à l'issue #3 — l'asymétrie INFO est correctement codée et testée, l'auth fail-closed couvre la nouvelle route, et le refactor module (handler exporté, require.main === module) est fait sans régression. Suite relancée localement par le reviewer : 14/14 verts.

Vérifications clés

  • Sécurité : pas de secrets dans le diff ; pas de path traversal — project ne touche jamais le filesystem, seul agent (issu du fichier trusted agents-map.json) construit le préfixe de fichier ; auth Bearer placée avant toutes les routes.
  • Correctness : allowedSeverities juste pour les 6 cas ; isScanReport garantit Array.isArray(findings) donc pas de TypeError sur le .filter ; timestamps NaN skippés ; sélection du plus récent across top-level + archive validée par test.
  • Déploiement : le Dockerfile (COPY package.json index.js ./, pas de npm install) ignore le nouveau package-lock.json — le runtime reste 0-dep, aucun impact image.
  • Tests : 14 cas, pas de .skip/.only, serveurs éphémères sur port 0, cleanup tmpdir correct. Commit en format conventionnel, PR liée à #3.

Suggestions (non bloquantes)

  1. index.js:312 — lookup non défensif : const agent = agentsMap[project] est hors try. Deux edge cases : (a) un agents-map.json contenant littéralement null (JSON valide) passe le parse puis null[project] lance un TypeError dans le handler async → unhandled rejection → crash du process sous Node 22 ; (b) ?project=__proto__ ou ?project=constructor remonte un objet hérité du prototype (truthy) → 200 no_data au lieu de 404. Fix 2 lignes : const agent = agentsMap?.[project]; if (typeof agent !== "string") → 404. Fichier trusted + appelant authentifié = probabilité faible, donc non bloquant.
  2. Tests 400 manquants : le tableau du PR body annonce « Invalid category/severity → 400 » mais aucun test ne couvre ?category=bogus ni ?severity=bogus. Deux tests d'une ligne fermeraient l'écart contrat/couverture.
  3. err.message dans les 500 : expose le chemin du fichier. Pattern préexistant du repo, derrière Bearer — à durcir seulement si l'API devient publique.

Rappel opérationnel (déjà dans le PR body) : bind-mount agents-map.json sur Coolify avant le deploy, sinon 500 en prod.

## Review — APPROVE **Verdict** : APPROVE **Résumé** : Implémentation propre et conforme à l'issue #3 — l'asymétrie INFO est correctement codée et testée, l'auth fail-closed couvre la nouvelle route, et le refactor module (`handler` exporté, `require.main === module`) est fait sans régression. Suite relancée localement par le reviewer : 14/14 verts. ### Vérifications clés - **Sécurité** : pas de secrets dans le diff ; pas de path traversal — `project` ne touche jamais le filesystem, seul `agent` (issu du fichier trusted `agents-map.json`) construit le préfixe de fichier ; auth Bearer placée avant toutes les routes. - **Correctness** : `allowedSeverities` juste pour les 6 cas ; `isScanReport` garantit `Array.isArray(findings)` donc pas de TypeError sur le `.filter` ; timestamps NaN skippés ; sélection du plus récent across top-level + archive validée par test. - **Déploiement** : le Dockerfile (`COPY package.json index.js ./`, pas de `npm install`) ignore le nouveau `package-lock.json` — le runtime reste 0-dep, aucun impact image. - **Tests** : 14 cas, pas de `.skip`/`.only`, serveurs éphémères sur port 0, cleanup tmpdir correct. Commit en format conventionnel, PR liée à #3. ### Suggestions (non bloquantes) 1. **index.js:312 — lookup non défensif** : `const agent = agentsMap[project]` est hors try. Deux edge cases : (a) un `agents-map.json` contenant littéralement `null` (JSON valide) passe le parse puis `null[project]` lance un TypeError dans le handler async → unhandled rejection → crash du process sous Node 22 ; (b) `?project=__proto__` ou `?project=constructor` remonte un objet hérité du prototype (truthy) → 200 `no_data` au lieu de 404. Fix 2 lignes : `const agent = agentsMap?.[project]; if (typeof agent !== "string") → 404`. Fichier trusted + appelant authentifié = probabilité faible, donc non bloquant. 2. **Tests 400 manquants** : le tableau du PR body annonce « Invalid `category`/`severity` → 400 » mais aucun test ne couvre `?category=bogus` ni `?severity=bogus`. Deux tests d'une ligne fermeraient l'écart contrat/couverture. 3. **`err.message` dans les 500** : expose le chemin du fichier. Pattern préexistant du repo, derrière Bearer — à durcir seulement si l'API devient publique. Rappel opérationnel (déjà dans le PR body) : bind-mount `agents-map.json` sur Coolify avant le deploy, sinon 500 en prod.
maximus merged commit 17f071b1f7 into main 2026-07-16 00:45:10 +00:00
maximus deleted branch issue-3-defenseurs-findings 2026-07-16 00:45:10 +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#9
No description provided.