Spec-reviewer + code-reviewer findings applied:
Must-fix
- Filter combinator AND-semantics: tactic+technique+subtechnique now intersect
(one IN subquery per facet) instead of being pooled into one OR. Reviewers
flagged both the wrong default semantics and the theoretical UUID-collision
risk of pooling tactic/technique/sub UUIDs into a shared list across
three columns.
- Front-end mutation cache hygiene: updateMeta + setTests both
`onSettled: invalidate` so a partial failure leaves the cache consistent.
Should-fix
- Per-scenario pg_advisory_xact_lock on set_scenario_tests — serialises
concurrent reorders, mirrors M4 /mitre/sync pattern.
- Backend/front consistency on duplicate tests in a scenario: the
UNIQUE(scenario_id, position) constraint already allows the same
test_template multiple times (chained ops), so the catalogue picker no
longer excludes already-picked items.
Nice-to-have
- N+1 eradicated in test_template view rendering: _to_views_batch
builds {uuid → MitreRow} maps in 3 queries up-front; list endpoint
now issues 4 queries total regardless of list size.
- Wire-level item length caps on tags (64) and expected_iocs (255)
via Annotated[str, StringConstraints(...)] — returns 400 instead of
bubbling up StringDataRightTruncation.
- 4 new pytest covering the AND-filter, extra="forbid" rejection,
empty mitre_tags clearing, and the 65-char tag cap. Total now
81 pytest + 38 e2e pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.8 KiB
5.8 KiB
type, milestone, date, project
| type | milestone | date | project |
|---|---|---|---|
| testing | M5 | 2026-05-12 | Metamorph |
Testing M5 — Templates : tests unitaires & scénarios
1. Lancement de la stack
make clean
make up
make migrate
make seed-mitre # tag picker needs the catalogue
L'admin stable
admin@metamorph.local / AdminPass1234!est restauré automatiquement par le hookafterAlldu spec e2e M5 — mais la 1ʳᵉ fois, bootstrappe-le via/setupou laisse les tests faire le travail.
2. Tests automatisés
make test-api # 81 tests pytest dont 23 M5 (CRUD, perm, mitre tags, reorder, AND-semantics, extra="forbid", item caps, empty-clear)
make e2e # 38 tests Playwright dont 4 M5 (API CRUD + scenario reorder + SPA list/filter)
Rapport HTML : e2e/playwright-report/. JUnit : e2e/playwright-report/junit.xml.
3. Smoke navigateur
Pré-requis
- Stack
make up+ admin loggé. - MITRE seedé (vérifier via
/mitre).
3.1 Catalogue de tests (/admin/tests)
- Cliquer Tests dans la nav admin → page chargée.
- Cliquer + New test → modal s'ouvre avec :
- Champs : Name, Description, Objective, Procedure (markdown), Prerequisites, Red expected, Blue expected, OPSEC, Free tags, Expected IOCs.
- Sous-section MITRE ATT&CK tags : matrice complète, mêmes interactions que
/mitre.
- Remplir au minimum
Name=phish-link, OPSEC=low, ajouter 2 tags MITRE (ex.TA0001 + T1566) → Create → carte apparaît dans la liste avec chips OPSEC + MITRE. - Cliquer Edit sur la carte → modal pré-remplie, modifier OPSEC à
high→ Save → la card est repeinte avec l'accent rouge OPSEC. - Filtres en haut :
Search(full-text q sur nom/description)Tactic external_id(ex.TA0001)OPSEC(select : —all— / low / medium / high)Free tag(mot-clé libre)
- Cliquer Delete sur une carte → confirm popup → la card disparaît (soft-delete : visible via
?include_deleted=truecôté API).
3.2 Catalogue de scénarios (/admin/scenarios)
- Cliquer Scenarios dans la nav admin.
- + New scenario → modal.
- Champs Name + Description.
- Catalogue picker en bas : champ de recherche + liste des tests dispos (max 50).
- Cliquer 3 tests dans le catalogue → ils s'empilent dans la liste ordonnée avec leurs indices
01/02/03. - Drag-and-drop : empoigner la poignée
☰à gauche d'une ligne et glisser vers le haut/bas → la liste se réordonne. La grille met à jour les indices au relâchement. - Save → carte apparaît avec un Tag « N tests » + l'aperçu des 4 premiers tests dans l'ordre choisi.
- Re-ouvrir Edit → l'ordre est persisté côté serveur (vérifie le numéro 01, 02, 03 dans la modal).
- Supprimer un
test_templatedont un scénario dépend (via/admin/tests) → la card scénario marque le test en rose dans le résumé (test_template_deleted: true).
3.3 Permissions
- Inviter Bob via Admin > Invitations sans groupe → Bob peut se logger mais reçoit
403sur/api/v1/test-templates. - Lui attacher un groupe avec seulement
test_template.read→ Bob voit/admin/tests... non, l'UI gate suris_admin. La perm seule donne l'accès API ; l'UI ne l'expose pas pour les non-admins (par design M5). - Bob tente
POST /api/v1/test-templates→403(manquetest_template.create).
4. Smoke API
4.1 Login admin
ACCESS=$(curl -sX POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@metamorph.local","password":"AdminPass1234!"}' | jq -r .access_token)
4.2 Créer un test taggué MITRE
curl -sX POST http://localhost:8080/api/v1/test-templates \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d '{
"name": "lsass-dump",
"opsec_level": "high",
"tags": ["creds"],
"mitre_tags": [
{"kind":"technique","external_id":"T1003"},
{"kind":"subtechnique","external_id":"T1003.001"}
]
}' | jq
4.3 Créer un scénario ordonné
# Suppose 3 ids: $A $B $C
curl -sX POST http://localhost:8080/api/v1/scenario-templates \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"chained\",\"test_template_ids\":[\"$A\",\"$B\",\"$C\"]}" | jq
# Reorder (full replace)
curl -sX PUT http://localhost:8080/api/v1/scenario-templates/<scn_id>/tests \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d "{\"test_template_ids\":[\"$C\",\"$A\",\"$B\"]}" | jq
4.4 Filtre par tactic
curl -s "http://localhost:8080/api/v1/test-templates?tactic=TA0006" \
-H "Authorization: Bearer $ACCESS" | jq '.items[].name'
5. Points de contrôle critiques
POST /test-templatesrejette MITRE inconnu avec400 unknown_mitre_tag.POST /test-templatesrejette opsec horslow/medium/high.PUT /test-templates/{id}partial keeps unset fields.PUT /test-templates/{id}avecmitre_tagsremplace la collection (pas d'append).DELETE /test-templates/{id}soft-delete (visible avec?include_deleted=true).POST /scenario-templatesrejette test_template inconnu ou soft-deleted.PUT /scenario-templates/{id}/testsrewrite atomique (delete + re-insert, contrainte UNIQUE(position) honorée).- Un test soft-deleted après linking reste référencé :
test_template_deleted: truesur le scénario. - Filtres list:
q,tactic,technique,subtechnique,opsec,tagcumulatifs. - Perm gating :
test_template.{read,create,update,delete}+scenario_template.{read,create,update,delete}. /diag/resettruncate les 4 nouvelles tables (scenario_template_tests,scenario_templates,test_template_mitre_tags,test_templates) avant les tables MITRE.- UI : drag-and-drop @dnd-kit/sortable réordonne la liste, save persistant.