feat: route GET /defenseurs/findings?project=X #3

Open
opened 2026-04-23 23:48:16 +00:00 by maximus · 0 comments
Owner

Contexte

Exposer les findings detailles d'un Defenseur via HTTP authentifie. Aujourd'hui la route GET /defenseurs (vps-health-api/index.js:210-221) sert uniquement /data/defenseurs/status.json (resume executif). On veut une seconde route exposant le detail des findings (id, description, recommendation, location, status).

Issue d'origine : spike analyse-vulnerabilite (archive 2026-04-23).

Consommateurs identifies

  1. Admin dashboard Vercel (la-compagnie-maximus) — src/app/api/admin/monitoring/route.ts:255-279 consomme deja /defenseurs via HTTP+Bearer (VPS_HEALTH_TOKEN). Vercel ne peut PAS faire SSH/Tailscale au VPS, donc HTTP est la seule voie pour le drill-down findings. Aucun drill-down par projet n'existe encore cote consommateur (route 100% greenfield cote Vercel).
  2. Skill /warmup — Tourne en local, mais HTTP > SSH (latence ~100ms vs ~1-2s).
  3. Futur skill /analyse-vulnerabilite — Devient portable hors Tailscale.

Etat des prerequis defenseurs (verifie 2026-05-03)

  • src/lookup.ts (PR #51 bf47f6b)
  • CLI npm run findings (PR #52 9cc0b3f)
  • Sergent ecrit agents-map.json : defenseurs/src/sergent.ts:588-599 writeAgentsMap(), mergee via PR #54 (9b4e432 feat(sergent): write agents-map.json snapshot for vps-health-api)
  • Verification VPS au debut de /fix-issue : ssh ubuntu@vps-b0826277.tail3c811f.ts.net 'ls -la /home/defenseur/defenseurs/agents-map.json' — confirmer presence. Si absent, attendre prochain run cron du Sergent ou trigger manuel. Merge bloque tant que prod pas prete.

Decouverte critique (exploration 2026-04-26)

Le container vps-health-api est isole du filesystem VPS : il n'a acces qu'a /data/defenseurs/status.json via bind-mount Coolify. Cela elimine :

  • Option 1 (CLI subprocess) : impossible sans installer defenseurs dans le container + cold-start tsx ~1-2s
  • Option 2 (import direct) : casse l'invariant 0-dep (apporte tsx, nostr-tools, @noble pour un microservice 127 lignes)

Decision : Option 3 — lecture fichier brut via nouveaux bind-mounts.

Implementation

Coolify : bind-mounts read-only

  • /home/defenseur/defenseurs/reports/ -> /data/defenseurs/reports/ (deja en place via PR #6 /reports/scans)
  • /home/defenseur/defenseurs/agents-map.json -> /data/defenseurs/agents-map.json (NOUVEAU, a verifier en debut de /fix-issue)

vps-health-api/index.js — route GET /defenseurs/findings

  • Auth : meme Bearer HEALTH_TOKEN que les autres routes
  • Query params :
    • project=<X> (obligatoire) — ex. la-suite-booking, la-compagnie-maximus
    • category=<deps|secrets|code|acces|infra> (optionnel, exact match)
    • severity=<CRITICAL|HIGH|MEDIUM|LOW|INFO> (optionnel, threshold inclusif vers le haut)
  • Default sans param severity : retourne MEDIUM+HIGH+CRITICAL (cache LOW+INFO, bruit)
  • Clarification 2026-05-12 : ?severity=LOW retourne LOW+MEDIUM+HIGH+CRITICAL — INFO toujours considere comme bruit et accessible uniquement via ?severity=INFO explicite (asymetrie volontaire avec la regle "inclusif vers le haut")
  • Pipeline :
    1. Lire DEFENSEURS_AGENTS_MAP_PATH (default /data/defenseurs/agents-map.json)
    2. Mapper project -> agent. 404 si inconnu.
    3. Helper partage findLatestReportForAgent(agent) (factorise depuis /reports/scans + isScanReport) -> report le plus recent par mtime
    4. Appliquer filtres category (exact) + severity (threshold inclusif vers le haut, avec asymetrie INFO ci-dessus)
    5. Reponse 200 { agent, project, timestamp, findings: Finding[] }

Reuse depuis PR #6

Factoriser modestement avec /reports/scans :

  • isScanReport() (index.js:88-96) reutilise tel quel
  • Nouveau helper interne findLatestReportForAgent(agent) extrait du pattern readdir + filter + sort mtime deja present dans collectScanReportsFromDir / readScanReportsForDate
  • Pas d'abstraction premature : minimum partage, routes restent decouplees au niveau handler

Format Finding (verifie : defenseurs/src/types.ts:3-13)

{
  id: string,
  severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO",
  category: "deps" | "secrets" | "code" | "acces" | "infra",
  title: string,
  description: string,
  location?: string,
  recommendation?: string,
  firstSeen: string,
  status: "open" | "resolved" | "wontfix"
}

Reponses

  • 200 + { agent, project, timestamp, findings: Finding[] } si report present (findings peut etre vide => scan clean, pas de champ status)
  • 200 + { findings: [], status: "no_data" } si pas de report du tout pour l'agent (distinguer "scan clean" vs "pas de data")
  • 400 si project manquant
  • 401 sans token / token invalide
  • 404 si projet inconnu dans agents-map.json
  • 500 sur fichier corrompu

Tests (bootstrap vitest)

vps-health-api n'a actuellement aucun test. Bootstraper vitest (devDep, runtime reste 0-dep) et couvrir :

  • 401 sans Authorization
  • 401 token invalide
  • 400 sans param project
  • 404 projet inconnu
  • 200 + payload complet
  • 200 + ?category=deps
  • 200 + ?severity=HIGH (threshold : retourne HIGH+CRITICAL)
  • 200 + ?severity=LOW (retourne LOW+MEDIUM+HIGH+CRITICAL, cache INFO)
  • 200 + default sans severity (retourne MEDIUM+HIGH+CRITICAL)
  • 200 + filtres combines
  • 200 + report present, findings vide (scan clean) : payload normal sans status
  • 200 + {findings: [], status: "no_data"} si report absent
  • 500 sur JSON corrompu

Mocks node:fs (pattern existant cote defenseurs/src/__tests__).
Modeles vitest dispo : la-suite-booking/vitest.config.ts, maximus-api/vitest.config.ts.

Mapping projet -> agent

Decision : snapshot JSON ecrit par le Sergent (regenere a chaque run, ecriture en remplacement).

{
  "la-suite-booking": "defenseur-booking",
  "la-compagnie-maximus": "defenseur-maximus",
  "vps-health-api": "defenseur-vps",
  ...
}

Source de verite unique cote defenseurs. vps-health-api reste sans connaissance interne du layout defenseurs.

Acceptation

  • Route GET /defenseurs/findings?project=la-suite-booking retourne le report defenseur-booking detaille
  • Auth requise (401 sans/avec mauvais token, 200 avec token valide)
  • 400 sur param project manquant
  • 404 propre sur projet inconnu
  • Filtre ?category= exact match
  • Filtre ?severity=X threshold inclusif vers le haut (HIGH retourne HIGH+CRITICAL, LOW retourne LOW+MEDIUM+HIGH+CRITICAL en cachant INFO)
  • Default sans ?severity= : retourne MEDIUM+HIGH+CRITICAL (cache LOW+INFO)
  • Report present + findings vide (scan clean) : 200 + payload normal sans champ status
  • Report absent : 200 + {findings: [], status: "no_data"}
  • 11+ tests vitest verts
  • Bind-mount agents-map.json configure et verifie en prod (SSH check au debut de /fix-issue)
  • agents-map.json present sur le VPS (verifier apres prochain run du Sergent)
  • README.md + CLAUDE.md documentent la nouvelle route et les bind-mounts

Complexite

Medium — code applicatif simple (~50 lignes), mais 4 sources de friction :

  1. Bootstrap test suite (premier dans ce repo)
  2. Modification Coolify (manuelle, hors repo)
  3. Dependance externe (agents-map.json doit etre present sur le VPS)
  4. Coordination du deploiement (bind-mounts doivent exister avant le code qui les lit)

Decisions retenues (resolues via /analyze 2026-05-07 + 2026-05-12)

  • Bind-mount agents-map.json : a verifier ensemble au debut de /fix-issue via SSH ; merge bloque tant que prod pas prete
  • Severity default : sans param -> MEDIUM+HIGH+CRITICAL (cache bruit LOW+INFO)
  • Severity threshold : ?severity=X -> threshold inclusif vers le haut (HIGH retourne HIGH+CRITICAL, LOW retourne LOW+MEDIUM+HIGH+CRITICAL mais cache toujours INFO)
  • Severity INFO : accessible uniquement via ?severity=INFO explicite
  • Reuse : factoriser isScanReport (tel quel) + nouveau helper findLatestReportForAgent(agent) partage avec /reports/scans ; minimum, sans abstraction premature
  • Report absent (no data) : 200 + {findings: [], status: "no_data"}
  • Report present + findings vide (scan clean) : 200 + {agent, project, timestamp, findings: []} SANS champ status (distingue les deux cas cote consommateur)
  • URL : sous-chemin /defenseurs/findings (plus explicite que query param)
  • Auth : HEALTH_TOKEN partage (a reevaluer si nouveaux consommateurs externes)
  • Test runner : vitest (devDep, runtime reste 0-dep)

Notes d'implementation

  • fs.promises async pour ne pas bloquer l'event loop (~quelques KB par report, plusieurs lectures par requete).
  • Le glob {agent}_*.json peut retourner plusieurs fichiers (purge writeReport est sur agent uniquement, voir defenseurs/src/report.ts:10-27) — prendre le plus recent par mtime.
  • agents-map.json est regenere par le Sergent a chaque run (ecriture en remplacement, pas mise a jour en place).
  • Pour findLatestReportForAgent, scanner REPORTS_DIR ET REPORTS_DIR/archive (cf. PR #7 2e75655 — archive fallback) puisque le Sergent rotation post-07:30 UTC ; sinon ~22h/jour le report ne serait visible que dans archive.
## Contexte Exposer les findings detailles d'un Defenseur via HTTP authentifie. Aujourd'hui la route `GET /defenseurs` (`vps-health-api/index.js:210-221`) sert uniquement `/data/defenseurs/status.json` (resume executif). On veut une seconde route exposant le detail des findings (`id`, `description`, `recommendation`, `location`, `status`). Issue d'origine : spike `analyse-vulnerabilite` (archive 2026-04-23). ## Consommateurs identifies 1. **Admin dashboard Vercel** (`la-compagnie-maximus`) — `src/app/api/admin/monitoring/route.ts:255-279` consomme deja `/defenseurs` via HTTP+Bearer (`VPS_HEALTH_TOKEN`). Vercel ne peut PAS faire SSH/Tailscale au VPS, donc HTTP est la seule voie pour le drill-down findings. **Aucun drill-down par projet n'existe encore cote consommateur** (route 100% greenfield cote Vercel). 2. **Skill `/warmup`** — Tourne en local, mais HTTP > SSH (latence ~100ms vs ~1-2s). 3. **Futur skill `/analyse-vulnerabilite`** — Devient portable hors Tailscale. ## Etat des prerequis defenseurs (verifie 2026-05-03) - [x] `src/lookup.ts` (PR #51 `bf47f6b`) - [x] CLI `npm run findings` (PR #52 `9cc0b3f`) - [x] **Sergent ecrit `agents-map.json`** : `defenseurs/src/sergent.ts:588-599 writeAgentsMap()`, mergee via PR #54 (`9b4e432 feat(sergent): write agents-map.json snapshot for vps-health-api`) - [ ] **Verification VPS au debut de /fix-issue** : `ssh ubuntu@vps-b0826277.tail3c811f.ts.net 'ls -la /home/defenseur/defenseurs/agents-map.json'` — confirmer presence. Si absent, attendre prochain run cron du Sergent ou trigger manuel. Merge bloque tant que prod pas prete. ## Decouverte critique (exploration 2026-04-26) Le container `vps-health-api` est **isole du filesystem VPS** : il n'a acces qu'a `/data/defenseurs/status.json` via bind-mount Coolify. Cela elimine : - **Option 1 (CLI subprocess)** : impossible sans installer `defenseurs` dans le container + cold-start tsx ~1-2s - **Option 2 (import direct)** : casse l'invariant 0-dep (apporte tsx, nostr-tools, @noble pour un microservice 127 lignes) **Decision : Option 3 — lecture fichier brut via nouveaux bind-mounts.** ## Implementation ### Coolify : bind-mounts read-only - `/home/defenseur/defenseurs/reports/` -> `/data/defenseurs/reports/` (deja en place via PR #6 `/reports/scans`) - `/home/defenseur/defenseurs/agents-map.json` -> `/data/defenseurs/agents-map.json` (NOUVEAU, a verifier en debut de /fix-issue) ### vps-health-api/index.js — route `GET /defenseurs/findings` - Auth : meme `Bearer HEALTH_TOKEN` que les autres routes - Query params : - `project=<X>` (obligatoire) — ex. `la-suite-booking`, `la-compagnie-maximus` - `category=<deps|secrets|code|acces|infra>` (optionnel, exact match) - `severity=<CRITICAL|HIGH|MEDIUM|LOW|INFO>` (optionnel, threshold inclusif vers le haut) - Default sans param `severity` : retourne MEDIUM+HIGH+CRITICAL (cache LOW+INFO, bruit) - **Clarification 2026-05-12** : `?severity=LOW` retourne LOW+MEDIUM+HIGH+CRITICAL — INFO toujours considere comme bruit et accessible uniquement via `?severity=INFO` explicite (asymetrie volontaire avec la regle "inclusif vers le haut") - Pipeline : 1. Lire `DEFENSEURS_AGENTS_MAP_PATH` (default `/data/defenseurs/agents-map.json`) 2. Mapper `project` -> `agent`. 404 si inconnu. 3. Helper partage `findLatestReportForAgent(agent)` (factorise depuis `/reports/scans` + `isScanReport`) -> report le plus recent par mtime 4. Appliquer filtres `category` (exact) + `severity` (threshold inclusif vers le haut, avec asymetrie INFO ci-dessus) 5. Reponse 200 `{ agent, project, timestamp, findings: Finding[] }` ### Reuse depuis PR #6 Factoriser modestement avec `/reports/scans` : - `isScanReport()` (`index.js:88-96`) reutilise tel quel - Nouveau helper interne `findLatestReportForAgent(agent)` extrait du pattern `readdir + filter + sort mtime` deja present dans `collectScanReportsFromDir` / `readScanReportsForDate` - Pas d'abstraction premature : minimum partage, routes restent decouplees au niveau handler ### Format Finding (verifie : `defenseurs/src/types.ts:3-13`) ```typescript { id: string, severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO", category: "deps" | "secrets" | "code" | "acces" | "infra", title: string, description: string, location?: string, recommendation?: string, firstSeen: string, status: "open" | "resolved" | "wontfix" } ``` ### Reponses - 200 + `{ agent, project, timestamp, findings: Finding[] }` si report present (findings peut etre vide => scan clean, **pas de champ `status`**) - 200 + `{ findings: [], status: "no_data" }` si **pas de report du tout** pour l'agent (distinguer "scan clean" vs "pas de data") - 400 si `project` manquant - 401 sans token / token invalide - 404 si projet inconnu dans `agents-map.json` - 500 sur fichier corrompu ### Tests (bootstrap vitest) `vps-health-api` n'a actuellement **aucun test**. Bootstraper `vitest` (devDep, runtime reste 0-dep) et couvrir : - 401 sans `Authorization` - 401 token invalide - 400 sans param `project` - 404 projet inconnu - 200 + payload complet - 200 + `?category=deps` - 200 + `?severity=HIGH` (threshold : retourne HIGH+CRITICAL) - 200 + `?severity=LOW` (retourne LOW+MEDIUM+HIGH+CRITICAL, cache INFO) - 200 + default sans severity (retourne MEDIUM+HIGH+CRITICAL) - 200 + filtres combines - 200 + report present, findings vide (scan clean) : payload normal sans `status` - 200 + `{findings: [], status: "no_data"}` si report absent - 500 sur JSON corrompu Mocks `node:fs` (pattern existant cote `defenseurs/src/__tests__`). Modeles vitest dispo : `la-suite-booking/vitest.config.ts`, `maximus-api/vitest.config.ts`. ## Mapping projet -> agent **Decision** : snapshot JSON ecrit par le Sergent (regenere a chaque run, ecriture en remplacement). ```json { "la-suite-booking": "defenseur-booking", "la-compagnie-maximus": "defenseur-maximus", "vps-health-api": "defenseur-vps", ... } ``` Source de verite unique cote `defenseurs`. `vps-health-api` reste sans connaissance interne du layout `defenseurs`. ## Acceptation - [ ] Route `GET /defenseurs/findings?project=la-suite-booking` retourne le report `defenseur-booking` detaille - [ ] Auth requise (401 sans/avec mauvais token, 200 avec token valide) - [ ] 400 sur param `project` manquant - [ ] 404 propre sur projet inconnu - [ ] Filtre `?category=` exact match - [ ] Filtre `?severity=X` threshold inclusif vers le haut (HIGH retourne HIGH+CRITICAL, LOW retourne LOW+MEDIUM+HIGH+CRITICAL en cachant INFO) - [ ] Default sans `?severity=` : retourne MEDIUM+HIGH+CRITICAL (cache LOW+INFO) - [ ] Report present + findings vide (scan clean) : 200 + payload normal sans champ `status` - [ ] Report absent : 200 + `{findings: [], status: "no_data"}` - [ ] 11+ tests vitest verts - [ ] Bind-mount `agents-map.json` configure et verifie en prod (SSH check au debut de /fix-issue) - [ ] `agents-map.json` present sur le VPS (verifier apres prochain run du Sergent) - [ ] `README.md` + `CLAUDE.md` documentent la nouvelle route et les bind-mounts ## Complexite **Medium** — code applicatif simple (~50 lignes), mais 4 sources de friction : 1. Bootstrap test suite (premier dans ce repo) 2. Modification Coolify (manuelle, hors repo) 3. Dependance externe (`agents-map.json` doit etre present sur le VPS) 4. Coordination du deploiement (bind-mounts doivent exister avant le code qui les lit) ## Decisions retenues (resolues via /analyze 2026-05-07 + 2026-05-12) - **Bind-mount `agents-map.json`** : a verifier ensemble au debut de `/fix-issue` via SSH ; merge bloque tant que prod pas prete - **Severity default** : sans param -> MEDIUM+HIGH+CRITICAL (cache bruit LOW+INFO) - **Severity threshold** : `?severity=X` -> threshold inclusif vers le haut (`HIGH` retourne HIGH+CRITICAL, `LOW` retourne LOW+MEDIUM+HIGH+CRITICAL **mais cache toujours INFO**) - **Severity INFO** : accessible uniquement via `?severity=INFO` explicite - **Reuse** : factoriser `isScanReport` (tel quel) + nouveau helper `findLatestReportForAgent(agent)` partage avec `/reports/scans` ; minimum, sans abstraction premature - **Report absent (no data)** : 200 + `{findings: [], status: "no_data"}` - **Report present + findings vide (scan clean)** : 200 + `{agent, project, timestamp, findings: []}` SANS champ `status` (distingue les deux cas cote consommateur) - **URL** : sous-chemin `/defenseurs/findings` (plus explicite que query param) - **Auth** : `HEALTH_TOKEN` partage (a reevaluer si nouveaux consommateurs externes) - **Test runner** : vitest (devDep, runtime reste 0-dep) ## Notes d'implementation - `fs.promises` async pour ne pas bloquer l'event loop (~quelques KB par report, plusieurs lectures par requete). - Le glob `{agent}_*.json` peut retourner plusieurs fichiers (purge `writeReport` est sur agent uniquement, voir `defenseurs/src/report.ts:10-27`) — prendre le plus recent par mtime. - `agents-map.json` est regenere par le Sergent a chaque run (ecriture en remplacement, pas mise a jour en place). - Pour `findLatestReportForAgent`, scanner REPORTS_DIR ET REPORTS_DIR/archive (cf. PR #7 `2e75655` — archive fallback) puisque le Sergent rotation post-07:30 UTC ; sinon ~22h/jour le report ne serait visible que dans archive.
maximus added the
status:ready
type:feature
source:human
labels 2026-04-23 23:48:16 +00:00
maximus changed title from feat: route GET /defenseurs/findings?project=X (optionnel) to feat: route GET /defenseurs/findings?project=X 2026-04-26 19:19:53 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-05-13 00:57:21 +00:00
maximus added
status:approved
and removed
status:in-progress
labels 2026-05-13 01:54:53 +00:00
Sign in to join this conversation.
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#3
No description provided.