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

Open
maximus wants to merge 1 commit from issue-3-defenseurs-findings into main
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.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-3-defenseurs-findings:issue-3-defenseurs-findings
git checkout issue-3-defenseurs-findings
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.