User feedback flagged that the doc didn't reflect the two hotfixes shipped after the M7 PR: - evidence whitelist surfaced in the dropzone + OS picker pre-filter - executed_at override fixed in non-UTC timezones (no more time-snap) Added a CHANGELOG entry per fix and a §3.5 in tasks/testing-m7.md walking through the timezone semantics of the datetime-local input. spec.md is left untouched — these are UX/implementation fixes, not contract changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.9 KiB
type, milestone, date, project
| type | milestone | date | project |
|---|---|---|---|
| testing | M7 | 2026-05-14 | Metamorph |
Testing M7 — Red & blue execution on a mission test
1. Lancement de la stack
make up
make migrate # applies the M7 last_actor_id migration (91a4e7c6d2f3)
Le boot seede automatiquement les 4 detection_levels par défaut
(detected_blocked / detected_alert / logged_only / not_detected) via
seed_detection_levels(). Si tu pars d'un stack pré-existant, un make restart (down+up) suffit — le seed est idempotent.
L'admin stable
admin@metamorph.local / AdminPass1234!est restauré par le hookafterAlldu spec e2e M7. La 1ʳᵉ fois, bootstrappe-le via/setup.
2. Tests automatisés
make test-api # 131 tests pytest, dont 25 M7 (perm gating, state machine, evidence, activity)
make e2e # 48 tests Playwright, dont 5 M7 (red/blue gating, 24/26 MB, SHA256, SPA)
Rapport HTML : e2e/playwright-report/.
Reminder :
make test-apietmake e2epartagent le Postgres dev. Lancer en milieu de session wipe les données — l'afterAllre-bootstrap l'admin stable, mais les missions/tests/uploads sur le disque créés à la main sont perdus.
3. Smoke navigateur
Pré-requis
- Stack
make up+ admin loggé. - Une mission existante avec au moins 1 scenario snapshotté contenant
≥ 1 test (voir
testing-m6.mdpour le chemin de création).
3.1 Page de test (/missions/<id>/tests/<test_id>)
- Depuis
/missions/<id>, onglet tests, cliquer une ligne (ou le nom du test). Redirection vers la page dédiée. - En-tête :
← Back to mission(linkdata-testid="back-to-mission").- Nom du test (snapshot).
- Ligne "Last touched Xs ago by Y" — vide à la création, remplie dès qu'un champ est sauvé.
- Status pill (
Pending/Executed/Reviewed/Skipped/Blocked). - Boutons de transitions autorisés depuis l'état courant (voir matrice en §6).
- Card metadata : MITRE chips, OPSEC tag, et 4
<details>pliés (Objective / Procedure / Expected red / Expected blue).
3.2 Zone Red (bordure rouge)
Command(mono,data-testid="red-command").Output(textarea mono multilign,data-testid="red-output").Comment(markdown,data-testid="red-comment").- Toggle Override executed-at + input datetime-local — disabled tant que
le test n'est pas
executed/reviewed_by_blue. - Bouton Save red fields :
- disabled si rien n'a changé ou si l'utilisateur n'a pas
mission.write_red_fields. - Sur succès, le bandeau "Last touched" se met à jour (cache invalidé).
- disabled si rien n'a changé ou si l'utilisateur n'a pas
3.3 Zone Blue (bordure cyan)
- Select
Detection level(sourcé de/detection-levels). Comment(markdown,data-testid="blue-comment").- Bouton Save blue fields (analogue à la zone red).
- Evidence dropzone :
- Drag & drop ou bouton Pick files (multi-fichiers).
- Sous la dropzone, la liste des extensions acceptées est affichée :
Accepted: .png · .jpg · .jpeg · .pdf · .txt · .log · .json · .csv · .evtx · .zip · max 25 MB / file(testidevidence-allowed-formats). - Le sélecteur OS est pré-filtré via l'attribut
accept— pas de "All files". - Un drag&drop d'une extension hors-liste est rejeté côté client avec
le message d'erreur en rouge ; le serveur re-vérifie quoi qu'il
arrive (
unsupported_extension/too_large). - Limite : 25 MB / fichier (côté client = garde-fou UX, côté serveur = stricte avec stream cap chunk-par-chunk).
- Table récap : nom · taille · uploader ·
sha256[:12]…· link download + bouton soft-delete.
3.5 Override executed-at — pièges timezone
- Le toggle Override executed-at timestamp affiche un input
datetime-localqui parle en heure locale du navigateur (ex. Europe/Paris UTC+2). L'état local du composant garde la valeur sous formeYYYY-MM-DDTHH:MM; la conversion en UTC ISO n'a lieu qu'au submit. Donc si tu tapes10:30à Paris, le serveur reçoit08:30:00+00:00— c'est attendu. - Le polling activity (15 s) ne re-sync l'état local que sur changement
d'identité du test (
useEffect([test.id])) — une frappe en cours n'est jamais écrasée par un refetch.
3.4 Indicateur d'activité
- À l'arrivée sur la page, le polling
GET /missions/<id>/activitydémarre (toutes les 15 s, gated surdocument.visibilityState === 'visible'). - Si un autre user édite le test, la query est invalidée → la page reload les champs (TanStack cache replaced).
- Le
server_timeest passé en?since=à l'appel suivant pour ne recevoir que ce qui a bougé depuis.
4. Vérifications fonctionnelles (DoD)
4.1 Red écrit en parallèle de Blue, sans conflit
- Sur un test
pending, login en red dans 1 onglet, en blue dans un autre. - Red : remplit
red_command+ sauve. - Blue : sélectionne
detected_alert+ commentaire + sauve. - Les 2 saves passent en 200, aucun conflit.
- Rafraîchir l'onglet red → les champs blue apparaissent (et réciproquement).
4.2 Perm gating field-level
| User | red_command | red_comment | blue_comment | detection_level | upload |
|---|---|---|---|---|---|
| red | ✓ | ✓ | 403 | 403 | 403 |
| blue | 403 | 403 | ✓ | ✓ | ✓ |
| red + blue | ✓ | ✓ | ✓ | ✓ | ✓ |
| admin | ✓ | ✓ | ✓ | ✓ | ✓ |
4.3 Evidence upload — limites
- Upload un fichier
.evtxde 24 MB → 201, body inclutsha256,size_bytes=25165824,mime=application/octet-stream. - Vérif
sha256côté client :sha256sum file24.evtx==body.sha256. - Upload un fichier
.evtxde 26 MB → 400{error:"too_large"}. - Upload un fichier
.exe(1 octet) → 400{error:"unsupported_extension"}. - Download via le lien
download→ bytes byte-for-byte identiques.
4.4 Soft delete d'evidence
- Upload un PDF, vérif qu'il apparaît dans la table.
- Cliquer delete → confirmation → row disparaît.
GET /evidence/<id>→ 404 (le row reste en DB avecdeleted_atset, mais le service l'occulte).- Sur disque,
/data/evidence/<mission_id>/<test_id>/<sha256>.pdfest conservé (purge physique = M12).
5. Vérification du state machine
| from | to | result | side requis |
|---|---|---|---|
| pending | executed | 200 | red |
| pending | skipped | 200 | any |
| pending | blocked | 200 | any |
| pending | reviewed_by_blue | 409 | — |
| executed | reviewed_by_blue | 200 | blue |
| executed | pending | 200 | red (reset) |
| reviewed_by_blue | executed | 200 | blue |
| reviewed_by_blue | pending | 409 | — |
| skipped | pending | 200 | any |
| blocked | pending | 200 | any |
| any | (same state) | 200 | — (no-op) |
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"target_state":"executed"}' \
http://localhost:8080/api/v1/missions/<mid>/tests/<tid>/transition
Side-effect attendu : target_state="executed" stamp executed_at=now() et
remet executed_at_overridden=false. Le retour à pending efface
executed_at.
6. Vérification override executed_at
- État
pending→ PUT{"executed_at": "...", "executed_at_overridden": true}→ 400 (refusé tant que le test n'a pas été marqué executed). - Transition
pending → executed→executed_atauto-stamp. - PUT
{"executed_at":"2026-05-14T10:00:00+00:00","executed_at_overridden":true}→ 200, body reflète la nouvelle date + override=true. - Blue user tente le même PUT → 403 (executed_at est red-side).
7. Vérification activity polling
# Snapshot t0
curl -H "Authorization: Bearer $T" \
http://localhost:8080/api/v1/missions/<mid>/activity \
| jq .server_time
# Mutate
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"red_comment_md":"poke"}' \
http://localhost:8080/api/v1/missions/<mid>/tests/<tid>
# Poll t1 (URL-encode the timestamp's `+`)
SINCE=$(python -c "import urllib.parse;print(urllib.parse.quote('${T0}'))")
curl -H "Authorization: Bearer $T" \
"http://localhost:8080/api/v1/missions/<mid>/activity?since=${SINCE}"
Réponse attendue : 1 entrée pour le test mis à jour, avec last_actor_email
peuplé.
8. Quick teardown
make down
# ou reset complet (test-only) :
curl -X POST http://localhost:8080/api/v1/diag/reset