Unverified Commit 703ae2f5 authored by Kourser's avatar Kourser
Browse files

Évolutions post-MVP : phases, fiches réflexes, observation, constats, autonomie

Huit évolutions issues d'une analyse comparative avec cybercrise.eu. Le cahier
des charges qui les décrit est un document de travail non versionné (exclu via
`.git/info/exclude`) : rien n'est arbitré, tout est amendable.

Le découpage en un seul commit est assumé : `styles.css`, `i18n.tsx`, le client
d'API et les types partagés sont touchés par TOUTES ces évolutions. Un commit par
fonctionnalité ne compilerait pas.

PHASES DE CRISE (EV-1). Un exercice porte des phases ordonnées. Deux bornes par
phase, et c'est le point de conception : `startsAtSec` est le PLAN, `startedAtSec`
le RÉEL, renseigné quand l'animation force le passage. La phase courante se lit
sur `startedAtSec ?? startsAtSec` — l'animation avance le déroulé sans réécrire le
chronogramme, et l'écart se relit au RETEX. Forcer une phase n'avance pas les
suivantes. Le rattachement d'un inject raisonne sur le plan, jamais sur le réel :
il décrit l'intention du concepteur.

FICHES RÉFLEXES (EV-4). Mission, risques et réflexes attendus par rôle. Deux
volets INDÉPENDANTS, tous deux fermés par défaut : un briefing à l'ouverture de
l'espace, et un onglet consultable pendant la crise. Ils ne servent pas le même
public — l'un rappelle son rôle à qui ne joue pas le sien, l'autre convient à un
public débutant et donnerait les réponses à un public confirmé.

GRILLE D'OBSERVATION (EV-3). Renseignée PENDANT l'exercice, navigable par phase,
échelle courte à trois niveaux. Distincte du questionnaire de RETEX, et il fallait
la distinguer : celui-là est adressé aux joueurs après et restitué anonymement,
celle-ci est remplie par l'observation et restituée AVEC ses auteurs — une
observation vaut par qui l'a faite. Une ligne par observateur et par critère :
deux observateurs se complètent au lieu de s'écraser.

POINTS D'ATTENTION DU RAPPORT (EV-6). Cinq constats déduits des données, chacun
portant ses preuves écrites en clair — un lien ne survit pas au PDF remis. Les
pistes d'action viennent d'un référentiel écrit à la main : un rapport remis à une
direction ne peut pas contenir une phrase dont personne ne répond. Aucun constat
ne nomme de personne physique, par construction : les preuves citent le
personnage, jamais le nom affiché du joueur. Chaque seuil a son cas de silence
testé, et les stimuli non lus ont un délai de grâce — sinon tout exercice
finirait sur un faux constat.

MODE AUTONOME (EV-5). Jouer sans animateur : cadence de diffusion régulière,
clôture automatique, et c'est LE JOUEUR qui lance depuis son espace. Le
démarrage automatique au premier accès a été retiré : il faisait tourner
l'horloge avant que le joueur ait lu sa mise en situation, et son annonce partait
avant que la socket ait rejoint la salle — d'où un espace bloqué en écriture sur
un exercice pourtant lancé. La cadence ne réécrit pas le chronogramme conçu.

Les presets de cadence sont livrés SANS attribution. La valeur de 2–3 minutes
souvent citée comme recommandation ANSSI n'a pas pu être confirmée à la source ;
le défaut reste le chronogramme.

BAC À SABLE PUBLIC (EV-7). Une route publique crée une organisation ÉPHÉMÈRE,
isolée par le même mécanisme que les organisations clientes, purgée à échéance
indépendamment du visiteur. DÉSACTIVÉ par défaut, pilotable depuis la console
d'instance, chaque bascule tracée. Fermé, la route répond 404 et non 403. Liste
blanche de kits dans le code, jamais un drapeau en base : la route ne peut pas
désigner un kit privé, même par erreur de requête.

La revue de sécurité dédiée RESTE DUE. Elle n'est pas remplacée par des tests qui
passent, et l'activation en production appartient à l'équipe.

KIT VITRINE (EV-10). Contenu original de ce dépôt : PME de 70 salariés,
rançongiciel puis fuite de données, 32 stimuli sur les cinq canaux, 3 phases, 6
documents. Le scénario est écrit pour PRODUIRE des constats — sauvegardes sur le
même réseau, dernier test de restauration il y a quatorze mois, annuaire de crise
stocké sur le serveur chiffré.

PWA ET THÈME (EV-9, EV-11). Manifeste installable, icônes à la marque, résilience
à une coupure brève : au retour de connexion l'écran recharge depuis l'API, seule
source de vérité. Le bandeau distingue une coupure réseau d'une fermeture par le
serveur — Socket.IO ne retente jamais dans le second cas, annoncer une reprise
automatique serait faux. Le thème suit la préférence système au premier accès,
posé par un script en ligne avant le premier rendu : sans attribut `data-theme`,
la feuille de style ne définissait aucune variable et la page clignotait.

CORRECTIFS TROUVÉS EN CLIQUANT. La coque joueur s'allongeait à chaque message et
les barres sortaient de l'écran. Le bac à sable installait le kit sans valeurs de
mise en situation, donc affichait `{{partenaire}}` à un visiteur. Les contenus des
catalogues étaient en ASCII dans un volet de lecture. La variable `--danger` était
utilisée à quatre endroits sans jamais être définie — bouton de suppression,
pastille d'état et bordure d'organisation suspendue rendaient une couleur héritée.
`/play/start` était refusé par le garde d'écriture : l'action qui fait commencer
l'exercice, bloquée au motif qu'il n'a pas commencé.

TESTS. 180 tests unitaires (20 suites), dont 51 nouveaux sur les fonctions pures :
phases, cadence, agrégat d'observation, détection des constats. Dix suites
d'intégration ont servi à la mise au point ; elles vivent hors du dépôt faute de
harnais de test sur base, ce qui reste à décider.

NON FAIT. L'évaluation des décisions et le score attendent deux arbitrages de
doctrine : l'évaluation porte-t-elle sur les rôles ou sur les personnes, et quel
statut donner à un score remis à une direction.

Co-Authored-By: Claude (RCA)
parent 128db63e
Loading
Loading
Loading
Loading
+17 −1
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

Conformément à la politique interne, toute dépendance tierce est documentée avec sa **licence** et une **évaluation de son niveau de maintenance**. Les versions sont **épinglées** (aucune version flottante) dans les `package.json` ; le fichier `pnpm-lock.yaml` fait foi.

_Dernière mise à jour : 2026-08-20 (socle multi-organisations)._
_Dernière mise à jour : 2026-08-21 (contenus embarqués : fiches réflexes, kit vitrine)._

> **nodemailer** est la seule dépendance capable d'émettre du trafic réseau sortant. Elle est
> confinée au module `platform-mail`, qui n'est **jamais** appelé depuis le moteur d'exercice, et
@@ -54,6 +54,22 @@ _Dernière mise à jour : 2026-08-20 (socle multi-organisations)._
| node | 22.11.0-slim | MIT (Node.js) | Base LTS |
| postgres | 16.4-alpine | PostgreSQL License (permissive) | Base de données |

## Contenus tiers embarqués

Le code n'est pas seul à être licencié : les contenus pédagogiques versionnés dans le dépôt le sont aussi.

| Contenu | Emplacement | Origine | Licence | Attribution |
|---|---|---|---|---|
| Kit vitrine « PME logistique, rançongiciel » | `apps/api/src/kits/data/vitrine-pme-rancongiciel.json` | **Contenu original de ce dépôt** | AGPL-3.0, comme le code | Aucune — rien n'est repris d'un tiers |
| Référentiel de fiches réflexes par rôle | `apps/api/src/engine/reflex-catalogue.ts` | Découpage des rôles et substance inspirés du pack pédagogique « Réflexes Cyber » publié par l'**ANSSI** et le **CDSE** | Les productions de l'ANSSI relèvent de la **Licence Ouverte v2.0** (mission Etalab) : réutilisation, adaptation et rediffusion libres sous réserve de mentionner la source et la date de dernière mise à jour | Mention affichée dans la console d'animation, sous le panneau des fiches réflexes |

> **Formulations.** Les textes du référentiel sont **rédigés pour ce dépôt** : aucune reprise
> littérale du pack, ni du simulateur tiers qui l'a également adapté. L'attribution reste due au
> titre de l'inspiration et du découpage des rôles.
>
> **Avant d'ajouter un contenu tiers**, vérifier sa licence, la mention d'attribution attendue et
> sa compatibilité avec l'AGPL-3.0 du dépôt — la licence du code ne couvre pas le contenu d'un tiers.

## Principe

Toutes les dépendances retenues sont sous licence **permissive** (MIT / Apache-2.0 / BSD / PostgreSQL), compatibles avec une distribution open-source et un usage commercial. Toute nouvelle dépendance doit être ajoutée à ce tableau avec sa licence et une évaluation de maintenance avant d'être introduite.
+41 −0
Original line number Diff line number Diff line
-- Fiches reflexes par role, et les deux reglages qui en commandent l'usage
-- cote joueur.
--
-- Les deux colonnes d'exercice sont FALSE par defaut : un exercice existant ne
-- change pas de comportement, aucun joueur ne voit apparaitre d'onglet ni de
-- fenetre. C'est l'animateur qui ouvre, exercice par exercice.

ALTER TABLE "Exercise" ADD COLUMN "reflexSheetsOpen" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Exercise" ADD COLUMN "reflexBriefOpen" BOOLEAN NOT NULL DEFAULT false;

CREATE TABLE "ReflexSheet" (
    "id" TEXT NOT NULL,
    "tenantId" TEXT NOT NULL,
    "exerciseId" TEXT NOT NULL,
    -- Nullable : une fiche du referentiel peut n'etre attribuee a personne dans
    -- cet exercice. ON DELETE SET NULL, pour qu'un personnage supprime ne fasse
    -- pas disparaitre le contenu redige.
    "characterId" TEXT,
    "role" TEXT NOT NULL,
    "mission" TEXT NOT NULL DEFAULT '',
    "risks" TEXT NOT NULL DEFAULT '',
    "reflexes" TEXT NOT NULL DEFAULT '',
    "position" INTEGER NOT NULL DEFAULT 0,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "ReflexSheet_pkey" PRIMARY KEY ("id")
);

-- Un personnage a au plus une fiche. Postgres autorise plusieurs NULL sous un
-- index unique : les fiches non attribuees coexistent donc sans contrainte.
CREATE UNIQUE INDEX "ReflexSheet_characterId_key" ON "ReflexSheet"("characterId");
CREATE INDEX "ReflexSheet_tenantId_idx" ON "ReflexSheet"("tenantId");
CREATE INDEX "ReflexSheet_exerciseId_idx" ON "ReflexSheet"("exerciseId");

ALTER TABLE "ReflexSheet" ADD CONSTRAINT "ReflexSheet_tenantId_fkey"
    FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReflexSheet" ADD CONSTRAINT "ReflexSheet_exerciseId_fkey"
    FOREIGN KEY ("exerciseId") REFERENCES "Exercise"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ReflexSheet" ADD CONSTRAINT "ReflexSheet_characterId_fkey"
    FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+26 −0
Original line number Diff line number Diff line
-- Transport des fiches reflexes par les kits (EV-4.5).
--
-- Un kit sans fiche installe un exercice sans fiche : aucun kit existant ne
-- change de comportement. Pas de colonne tenantId, comme les autres enfants
-- d'un kit — le cloisonnement passe par le kit parent.

CREATE TABLE "KitReflexSheet" (
    "id" TEXT NOT NULL,
    "kitId" TEXT NOT NULL,
    -- Ref d'un KitCharacter, resolue a l'installation. Volontairement une ref
    -- textuelle et non une cle etrangere : c'est la convention des kits
    -- (senderRef, targetRefs, attachmentRefs), qui survit a un export/import.
    "characterRef" TEXT,
    "role" TEXT NOT NULL,
    "mission" TEXT NOT NULL DEFAULT '',
    "risks" TEXT NOT NULL DEFAULT '',
    "reflexes" TEXT NOT NULL DEFAULT '',
    "position" INTEGER NOT NULL DEFAULT 0,

    CONSTRAINT "KitReflexSheet_pkey" PRIMARY KEY ("id")
);

CREATE INDEX "KitReflexSheet_kitId_idx" ON "KitReflexSheet"("kitId");

ALTER TABLE "KitReflexSheet" ADD CONSTRAINT "KitReflexSheet_kitId_fkey"
    FOREIGN KEY ("kitId") REFERENCES "Kit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+58 −0
Original line number Diff line number Diff line
-- Phases de crise (EV-1) : structure du deroule, prerequis de l'evaluation.
--
-- Non-regression : un exercice sans phase se comporte exactement comme avant.
-- `Inject.phaseId` reste nul dans ce cas, et la phase courante vaut « aucune ».
-- Le reglage de visibilite joueur est FALSE par defaut : personne ne voit
-- apparaitre un decoupage qu'il n'a pas demande.

ALTER TABLE "Exercise" ADD COLUMN "phasesVisibleToPlayers" BOOLEAN NOT NULL DEFAULT false;

CREATE TABLE "Phase" (
    "id" TEXT NOT NULL,
    "tenantId" TEXT NOT NULL,
    "exerciseId" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "description" TEXT,
    -- Le PLAN : T+ prevu par le concepteur, en secondes d'exercice.
    "startsAtSec" INTEGER NOT NULL DEFAULT 0,
    -- Le REEL : renseigne quand l'animation force le passage avant l'heure.
    -- Garder les deux permet d'avancer le deroule sans reecrire le chronogramme.
    "startedAtSec" INTEGER,
    "order" INTEGER NOT NULL DEFAULT 0,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Phase_pkey" PRIMARY KEY ("id")
);

CREATE INDEX "Phase_tenantId_idx" ON "Phase"("tenantId");
CREATE INDEX "Phase_exerciseId_order_idx" ON "Phase"("exerciseId", "order");

ALTER TABLE "Phase" ADD CONSTRAINT "Phase_tenantId_fkey"
    FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Phase" ADD CONSTRAINT "Phase_exerciseId_fkey"
    FOREIGN KEY ("exerciseId") REFERENCES "Exercise"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Rattachement explicite d'un inject. Null = deduit du T+ planifie.
-- SET NULL et non CASCADE : supprimer une phase ne doit pas supprimer des injects.
ALTER TABLE "Inject" ADD COLUMN "phaseId" TEXT;
CREATE INDEX "Inject_phaseId_idx" ON "Inject"("phaseId");
ALTER TABLE "Inject" ADD CONSTRAINT "Inject_phaseId_fkey"
    FOREIGN KEY ("phaseId") REFERENCES "Phase"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- Transport par les kits (EV-1.6). Bornes en MINUTES, unite des kits.
CREATE TABLE "KitPhase" (
    "id" TEXT NOT NULL,
    "kitId" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "description" TEXT,
    "startsAtMinutes" INTEGER NOT NULL DEFAULT 0,
    "order" INTEGER NOT NULL DEFAULT 0,

    CONSTRAINT "KitPhase_pkey" PRIMARY KEY ("id")
);

CREATE INDEX "KitPhase_kitId_idx" ON "KitPhase"("kitId");

ALTER TABLE "KitPhase" ADD CONSTRAINT "KitPhase_kitId_fkey"
    FOREIGN KEY ("kitId") REFERENCES "Kit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+61 −0
Original line number Diff line number Diff line
-- Grille d'observation remplie PENDANT l'exercice (EV-3).
--
-- Distincte du questionnaire de RETEX : celui-la est adresse aux joueurs apres
-- l'exercice, celle-ci est renseignee par l'observation en direct.
--
-- Non-regression : un exercice sans critere n'a pas de grille, et rien ne change.

CREATE TABLE "ObservationCriterion" (
    "id" TEXT NOT NULL,
    "tenantId" TEXT NOT NULL,
    "exerciseId" TEXT NOT NULL,
    -- Null = critere valable tout au long de l'exercice.
    "phaseId" TEXT,
    "theme" TEXT NOT NULL,
    "label" TEXT NOT NULL,
    "order" INTEGER NOT NULL DEFAULT 0,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "ObservationCriterion_pkey" PRIMARY KEY ("id")
);

CREATE INDEX "ObservationCriterion_tenantId_idx" ON "ObservationCriterion"("tenantId");
CREATE INDEX "ObservationCriterion_exerciseId_order_idx" ON "ObservationCriterion"("exerciseId", "order");
CREATE INDEX "ObservationCriterion_phaseId_idx" ON "ObservationCriterion"("phaseId");

ALTER TABLE "ObservationCriterion" ADD CONSTRAINT "ObservationCriterion_tenantId_fkey"
    FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ObservationCriterion" ADD CONSTRAINT "ObservationCriterion_exerciseId_fkey"
    FOREIGN KEY ("exerciseId") REFERENCES "Exercise"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- SET NULL : supprimer une phase ne supprime pas les criteres, ils redeviennent
-- valables tout au long de l'exercice.
ALTER TABLE "ObservationCriterion" ADD CONSTRAINT "ObservationCriterion_phaseId_fkey"
    FOREIGN KEY ("phaseId") REFERENCES "Phase"("id") ON DELETE SET NULL ON UPDATE CASCADE;

CREATE TABLE "ObservationEntry" (
    "id" TEXT NOT NULL,
    "tenantId" TEXT NOT NULL,
    "criterionId" TEXT NOT NULL,
    "authorUserId" TEXT NOT NULL,
    "authorLabel" TEXT NOT NULL,
    -- Echelle courte : 1 = a ameliorer, 2 = satisfaisant, 3 = point fort.
    "rating" INTEGER,
    "comment" TEXT,
    "atExerciseSec" INTEGER NOT NULL DEFAULT 0,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "ObservationEntry_pkey" PRIMARY KEY ("id")
);

-- Une ligne par observateur et par critere : deux observateurs se completent au
-- lieu de s'ecraser (EV-3.6). C'est aussi la cle de l'ecriture idempotente.
CREATE UNIQUE INDEX "ObservationEntry_criterionId_authorUserId_key"
    ON "ObservationEntry"("criterionId", "authorUserId");
CREATE INDEX "ObservationEntry_tenantId_idx" ON "ObservationEntry"("tenantId");

ALTER TABLE "ObservationEntry" ADD CONSTRAINT "ObservationEntry_tenantId_fkey"
    FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ObservationEntry" ADD CONSTRAINT "ObservationEntry_criterionId_fkey"
    FOREIGN KEY ("criterionId") REFERENCES "ObservationCriterion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Loading