feat(reports): add GET /reports/scans endpoint for defenseur-auto #6

Merged
maximus merged 1 commit from feat/reports-scans-endpoint into main 2026-05-08 01:04:42 +00:00
Owner

Summary

Adds GET /reports/scans?date=YYYY-MM-DD so defenseur-auto (workstation cron) can fetch scan reports over HTTPS instead of SSH/rsync — the current Tailscale SSH session expiry breaks run-auto.sh pre-rsync intermittently.

  • HTTP native handler (~50 lines), same style as /health and /defenseurs
  • Auth bearer reuses HEALTH_TOKEN (no new secret)
  • Reads <REPORTS_DIR>/defenseur-*_<date>*.json with isScanReport filter (excludes defenseur-auto_*.json — same guard as defenseurs/src/report.ts)
  • Date param validated by regex (path traversal blocked before filesystem access)
  • 11 cases covered in test-curl.sh (run locally — no test runner installed in this repo)

Spike validated upstream : ~/claude-code/.spikes/archived/endpoint-reports-sur-vps-health-api-pour/ (Phase 3 implementation of P4d).

Coolify changes (manual, post-merge)

Apply via Coolify UI before redeploy is meaningful (default fallback /data/defenseurs/reports will work in absence, but explicit is better) :

  1. Add env var REPORTS_DIR=/data/defenseurs/reports (is_runtime=true, is_buildtime=false)
  2. Bind-mount already covers /data/defenseurs/ recursively (the Sergent writes status.json there) — no change needed
  3. Trigger a redeploy (or it will auto-rebuild on push)

Validation post-merge

TOKEN="$HEALTH_TOKEN"  # same value as on Coolify
curl -H "Authorization: Bearer $TOKEN" \
  "https://health.lacompagniemaximus.com/reports/scans?date=$(date -u +%Y-%m-%d)" \
  | jq '.count, (.reports | map(.agent))'

Expected : a positive count (Defenseurs scanners run at 01:00 UTC) and a list of agents (no defenseur-auto).

Risks

From the spike HANDOFF :

  1. File ownership — reports are owned defenseur:defenseur on the VPS. The container runs USER node (Dockerfile L5). If 200 + count=0 post-deploy, check container can read the bind-mount (workaround : chmod 644 cote sergent or relax USER node with a supplementary group).
  2. Failure mode silencieux — if endpoint returns 200 + reports=[] (date inexistante or mount cassé), the workstation pipeline runs with 0 findings without a warning. Mitigation pending in the consumer PR (logs).
  3. Dependency direction — workstation now depends on vps-health-api reachability. Same dependency as /defenseurs (already prod-critical for the home dashboard).

Test plan

  • test-curl.sh passes locally (11/11)
  • Max merges + applies Coolify env var
  • Curl validation in prod
  • Defenseurs PR consumes the endpoint, full e2e via cron
## Summary Adds `GET /reports/scans?date=YYYY-MM-DD` so `defenseur-auto` (workstation cron) can fetch scan reports over HTTPS instead of SSH/rsync — the current Tailscale SSH session expiry breaks `run-auto.sh` pre-rsync intermittently. - HTTP native handler (~50 lines), same style as `/health` and `/defenseurs` - Auth bearer reuses `HEALTH_TOKEN` (no new secret) - Reads `<REPORTS_DIR>/defenseur-*_<date>*.json` with `isScanReport` filter (excludes `defenseur-auto_*.json` — same guard as `defenseurs/src/report.ts`) - Date param validated by regex (path traversal blocked before filesystem access) - 11 cases covered in `test-curl.sh` (run locally — no test runner installed in this repo) Spike validated upstream : `~/claude-code/.spikes/archived/endpoint-reports-sur-vps-health-api-pour/` (Phase 3 implementation of P4d). ## Coolify changes (manual, post-merge) Apply via Coolify UI before redeploy is meaningful (default fallback `/data/defenseurs/reports` will work in absence, but explicit is better) : 1. Add env var `REPORTS_DIR=/data/defenseurs/reports` (`is_runtime=true`, `is_buildtime=false`) 2. Bind-mount already covers `/data/defenseurs/` recursively (the Sergent writes `status.json` there) — no change needed 3. Trigger a redeploy (or it will auto-rebuild on push) ## Validation post-merge ```bash TOKEN="$HEALTH_TOKEN" # same value as on Coolify curl -H "Authorization: Bearer $TOKEN" \ "https://health.lacompagniemaximus.com/reports/scans?date=$(date -u +%Y-%m-%d)" \ | jq '.count, (.reports | map(.agent))' ``` Expected : a positive count (Defenseurs scanners run at 01:00 UTC) and a list of agents (no `defenseur-auto`). ## Risks From the spike HANDOFF : 1. **File ownership** — reports are owned `defenseur:defenseur` on the VPS. The container runs `USER node` (Dockerfile L5). If 200 + `count=0` post-deploy, check container can read the bind-mount (workaround : `chmod 644` cote sergent or relax `USER node` with a supplementary group). 2. **Failure mode silencieux** — if endpoint returns 200 + `reports=[]` (date inexistante or mount cassé), the workstation pipeline runs with 0 findings without a warning. Mitigation pending in the consumer PR (logs). 3. **Dependency direction** — workstation now depends on `vps-health-api` reachability. Same dependency as `/defenseurs` (already prod-critical for the home dashboard). ## Test plan - [x] `test-curl.sh` passes locally (11/11) - [ ] Max merges + applies Coolify env var - [ ] Curl validation in prod - [ ] Defenseurs PR consumes the endpoint, full e2e via cron
maximus added 1 commit 2026-05-08 00:51:14 +00:00
Replaces the SSH/rsync canal between Max's workstation cron and the VPS
for fetching defenseur scan reports. The defenseur-auto orchestrator now
pulls reports/defenseur-X_<date>*.json over HTTPS, reusing HEALTH_TOKEN.

The handler mirrors the style of index.js (HTTP native, no framework),
includes the same isScanReport guard as defenseurs/src/report.ts (filters
out defenseur-auto_*.json run reports), and validates the date param
against /^\d{4}-\d{2}-\d{2}$/ to short-circuit path traversal before any
filesystem access.

Validated by test-curl.sh — 11 cases covering auth, validation, date
filter, isScanReport filter, sort order, GET-only and 404 paths.

Spike: ~/claude-code/.spikes/archived/endpoint-reports-sur-vps-health-api-pour/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

Review: APPROVE

Summary : implementation propre et bien delimitee. Le regex date court-circuite avant tout acces FS (path traversal bloque), isScanReport reproduit fidelement le guard de defenseurs/src/report.ts, et le double-check parsed.timestamp.startsWith(date) evite les faux positifs si un nom de fichier est forge. Style coherent avec /health et /defenseurs.

Verifications passees

  • Securite : regex ^\d{4}-\d{2}-\d{2}$ bloque traversal/injection ; bearer auth reutilise ; fail-closed preserve ; pas de secrets dans le diff.
  • Correction : URL parse avec host placeholder correct ; validRoutes.includes(pathname) filtre apres strip de la query string ; sort timestamp asc conforme a la convention amont ; defenseur-auto exclu via isScanReport (pas de findings[]).
  • Tests : test-curl.sh couvre 10 scenarios (auth, missing/invalid date, traversal, POST, sort, filter auto, date filter, unknown date). Pas de test runner dans le repo — script bash assume comme regression suite, c'est documente dans le header.
  • Qualite : commentaires expliquent les choix non-evidents (URL placeholder, rationale isScanReport, convention de nom de fichier) ; .env.example + CLAUDE.md a jour.

Suggestions non-bloquantes

  1. PR body dit "11/11" mais le script declare 10 cases dans son header (et fait ~11 pass calls car case 3 a deux assertions). Cosmetique.
  2. Pas de lien Fixes #N dans le PR body — empeche le label automation cote issue. Pour la suite, ajouter le numero d'issue de provenance (probablement issue 4 ou 5 dans defenseurs).
  3. TOCTOU benin entre existsSync et readdirSync : si quelqu'un supprime le dir entre les deux, ca jettera. Le try/catch autour du JSON.parse ne couvre pas le readdirSync. En pratique le bind-mount Coolify ne disparait pas — mais on pourrait juste droper le existsSync et laisser le try global au handler attraper l'ENOENT (if (err.code === 'ENOENT') return []). Pas urgent.
  4. Risque #1 du PR body (ownership) est reel — quand tu valides en prod post-merge, garde un oeil sur count=0 qui serait suspect (le sergent ecrit defenseur:defenseur, le container tourne USER node).

Rien ne bloque le merge. Bon a deployer.

## Review: APPROVE **Summary** : implementation propre et bien delimitee. Le regex date court-circuite avant tout acces FS (path traversal bloque), `isScanReport` reproduit fidelement le guard de `defenseurs/src/report.ts`, et le double-check `parsed.timestamp.startsWith(date)` evite les faux positifs si un nom de fichier est forge. Style coherent avec `/health` et `/defenseurs`. ### Verifications passees - Securite : regex `^\d{4}-\d{2}-\d{2}$` bloque traversal/injection ; bearer auth reutilise ; fail-closed preserve ; pas de secrets dans le diff. - Correction : URL parse avec host placeholder correct ; `validRoutes.includes(pathname)` filtre apres strip de la query string ; sort timestamp asc conforme a la convention amont ; defenseur-auto exclu via `isScanReport` (pas de `findings[]`). - Tests : `test-curl.sh` couvre 10 scenarios (auth, missing/invalid date, traversal, POST, sort, filter auto, date filter, unknown date). Pas de test runner dans le repo — script bash assume comme regression suite, c'est documente dans le header. - Qualite : commentaires expliquent les choix non-evidents (URL placeholder, rationale isScanReport, convention de nom de fichier) ; `.env.example` + `CLAUDE.md` a jour. ### Suggestions non-bloquantes 1. **PR body dit "11/11"** mais le script declare 10 cases dans son header (et fait ~11 pass calls car case 3 a deux assertions). Cosmetique. 2. **Pas de lien `Fixes #N`** dans le PR body — empeche le label automation cote issue. Pour la suite, ajouter le numero d'issue de provenance (probablement issue 4 ou 5 dans defenseurs). 3. **TOCTOU benin** entre `existsSync` et `readdirSync` : si quelqu'un supprime le dir entre les deux, ca jettera. Le `try/catch` autour du JSON.parse ne couvre pas le `readdirSync`. En pratique le bind-mount Coolify ne disparait pas — mais on pourrait juste droper le `existsSync` et laisser le `try` global au handler attraper l'ENOENT (`if (err.code === 'ENOENT') return []`). Pas urgent. 4. **Risque #1 du PR body (ownership)** est reel — quand tu valides en prod post-merge, garde un oeil sur `count=0` qui serait suspect (le sergent ecrit `defenseur:defenseur`, le container tourne `USER node`). Rien ne bloque le merge. Bon a deployer.
maximus merged commit 09a4ddeb34 into main 2026-05-08 01:04:42 +00:00
maximus deleted branch feat/reports-scans-endpoint 2026-05-08 01:04:42 +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#6
No description provided.