summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorertopogo <erwin.t.pombett@gmail.com>2026-04-06 13:50:16 +0200
committerertopogo <erwin.t.pombett@gmail.com>2026-04-06 13:50:16 +0200
commit96615b46a72e7902f7ade2619b21649bf41b2b1b (patch)
treed33c565ddf80ca3e5b1809d361470dea29d86ea2
parent202f3256fa1bb60a72322ca1c4c3b5e6ffca212a (diff)
documentation zero trust
-rw-r--r--debug.log1
-rw-r--r--documentation/techno.md124
-rw-r--r--package-lock.json230
-rw-r--r--package.json1
-rw-r--r--src/app/(public)/demos/page.tsx9
-rw-r--r--src/app/(public)/demos/zero-trust/page.tsx43
-rw-r--r--src/components/demos/zero-trust/ZTFlowNode.tsx104
-rw-r--r--src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx282
-rw-r--r--src/components/demos/zero-trust/scenarios/classic-perimeter.ts123
-rw-r--r--src/components/demos/zero-trust/scenarios/east-west.ts126
-rw-r--r--src/components/demos/zero-trust/scenarios/index.ts18
-rw-r--r--src/components/demos/zero-trust/scenarios/zero-trust-access.ts143
-rw-r--r--src/components/demos/zero-trust/types.ts54
13 files changed, 1254 insertions, 4 deletions
diff --git a/debug.log b/debug.log
new file mode 100644
index 0000000..ee6f2f1
--- /dev/null
+++ b/debug.log
@@ -0,0 +1 @@
+[0405/122452.003:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: The system cannot find the file specified. (0x2)
diff --git a/documentation/techno.md b/documentation/techno.md
new file mode 100644
index 0000000..d03dd5b
--- /dev/null
+++ b/documentation/techno.md
@@ -0,0 +1,124 @@
+# Choix technologiques — Der-topogo
+
+Site de consulting IAM et sécurité informatique, avec contenu éditable via un CMS headless intégré à l’application.
+
+## Architecture générale
+
+- **Application full-stack** : [Next.js](https://nextjs.org/) (App Router) en TypeScript, avec sortie **standalone** pour des images Docker légères et un déploiement prévisible.
+- **CMS** : [Payload CMS](https://payloadcms.com/) v3, embarqué dans Next via `@payloadcms/next` (routes admin et API sous le même processus Node).
+- **Base de données** : [PostgreSQL](https://www.postgresql.org/) via l’adaptateur officiel `@payloadcms/db-postgres` (chaîne `DATABASE_URI` / variable d’environnement). La base n’est pas fournie par le `docker-compose` du dépôt : elle est attendue en externe ou sur un autre compose.
+- **Médias** : volume Docker `app_media` monté sur `/app/media` pour la persistance des fichiers uploadés depuis Payload.
+- **Reverse proxy / TLS** : [Caddy](https://caddyserver.com/) en production (TLS automatique Let’s Encrypt sur le domaine public, TLS interne pour l’accès LAN). Compression gzip/zstd, en-têtes de sécurité alignés avec ceux définis dans Next.
+
+## Choix détaillés
+
+| Domaine | Choix | Raison |
+|--------|--------|--------|
+| Framework UI | Next.js + React | SSR/SSG, intégration native avec Payload, écosystème mature. |
+| Langage | TypeScript (strict) | Typage des collections Payload et du front, maintenance à long terme. |
+| Styles | Tailwind CSS v4 + PostCSS | Utilitaires, cohérence visuelle, build via `@tailwindcss/postcss`. |
+| Éditeur riche | Lexical (`@payloadcms/richtext-lexical`) | Éditeur moderne supporté officiellement par Payload 3. |
+| Animations | Framer Motion | Animations déclaratives côté client sans réécrire la logique de layout. |
+| Icônes | Lucide React | Jeu d’icônes léger et cohérent avec React. |
+| Classes CSS conditionnelles | `clsx` + `tailwind-merge` | Composition de classes et fusion sans conflits avec Tailwind. |
+| Images | `sharp` | Requis / recommandé par Next pour l’optimisation d’images en production. |
+| API Payload / introspection | `graphql` | Dépendance utilisée par l’écosystème Payload pour GraphQL. |
+| Déploiement app | Docker multi-stage (Node 20 Alpine) | `npm ci`, build Next, image finale non-root (`nextjs`) exposant le port 3000. |
+| Télémétrie | `NEXT_TELEMETRY_DISABLED=1` | Désactivée dans l’image Docker. |
+
+## Sécurité HTTP (résumé)
+
+- En-têtes côté Next : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` (voir `next.config.ts`).
+- Caddy renforce HSTS sur le site public et reprend des en-têtes similaires sur les deux blocs de site.
+
+---
+
+# Paquets et logiciels installés / référencés
+
+Les **versions exactes** des paquets npm transitifs sont dans `package-lock.json` à la racine du dépôt. Après `npm install`, la commande `npm ls` liste l’arbre complet.
+
+## Dépendances de production (`dependencies` dans `package.json`)
+
+| Paquet | Rôle |
+|--------|------|
+| `payload` | Moteur CMS (collections, auth, API REST/GraphQL). |
+| `@payloadcms/next` | Intégration Next.js (handler, build). |
+| `@payloadcms/db-postgres` | Persistance PostgreSQL (Drizzle sous le capot côté Payload). |
+| `@payloadcms/richtext-lexical` | Champ rich text Lexical dans l’admin. |
+| `graphql` | Support GraphQL attendu par Payload. |
+| `sharp` | Traitement d’images pour `next/image`. |
+| `framer-motion` | Animations React. |
+| `lucide-react` | Icônes SVG. |
+| `clsx` | Concaténation conditionnelle de noms de classes. |
+| `tailwind-merge` | Fusion des classes Tailwind sans doublons contradictoires. |
+
+## Dépendances de développement (`devDependencies`)
+
+| Paquet | Rôle |
+|--------|------|
+| `next` | Framework (installé comme dépendance transitive via l’écosystème Next / ESLint ; version figée dans le lockfile). |
+| `react` / `react-dom` | Installés avec Next (versions dans le lockfile). |
+| `typescript` | Typage (souvent amené par Next selon le template). |
+| `tailwindcss` | Moteur Tailwind v4. |
+| `@tailwindcss/postcss` | Plugin PostCSS pour Tailwind 4. |
+| `postcss` | Pipeline CSS. |
+| `eslint` | Lint JavaScript/TypeScript. |
+| `eslint-config-next` | Règles ESLint alignées sur la version de Next utilisée. |
+
+> **Note** : Le fichier `package.json` ne liste pas explicitement `next` ni `react` : ils sont tirés par les outils (p. ex. `eslint-config-next`) et le scaffold Next ; les versions réellement installées sont celles du **lockfile**.
+
+## Infrastructure (hors `node_modules`)
+
+| Composant | Détail |
+|-----------|--------|
+| **Node.js** | Ligne de base des images : **20** (variante **Alpine** dans le `Dockerfile`). |
+| **npm** | Utilisé pour `npm ci` et les scripts (`build`, `start`, etc.). |
+| **Docker** | Build multi-étapes : `deps` → `builder` → `runner`. |
+| **PostgreSQL** | Attendu via `DATABASE_URI` ; non défini dans le compose minimal du repo. |
+| **Caddy** | Fichier `Caddyfile` à la racine : reverse proxy vers `app:3000`, logs, TLS. |
+
+## Fichiers de configuration utiles
+
+- `package.json` / `package-lock.json` — dépendances npm.
+- `next.config.ts` — Next + `withPayload`, standalone, en-têtes, alias Webpack.
+- `tsconfig.json` — chemins `@/*` et `@payload-config`.
+- `postcss.config.mjs` — Tailwind via PostCSS.
+- `src/payload.config.ts` — collections, Lexical, Postgres.
+- `Dockerfile` — image de production.
+- `docker-compose.yml` — service `app`, volume médias, `env_file: .env.local`.
+- `Caddyfile` — exposition HTTPS et proxy.
+
+## Flux Git de déploiement (mémo)
+
+Ce flux est déjà décrit en détail dans `DEPLOY.md`. Rappel opérationnel rapide :
+
+### 1) Depuis le poste de dev (push vers Chillka)
+
+```bash
+git add .
+git commit -m "message"
+git push origin main
+```
+
+### 2) Sur Huitral (pull + redéploiement)
+
+```bash
+ssh toshiro@huitral
+cd /var/www/der-topogo
+git pull origin main
+docker compose up -d --build
+```
+
+### 3) Vérification rapide
+
+```bash
+docker compose ps
+curl -Ik https://dt.arauco.online
+curl -Ik https://dt.huitral.ruka.lan
+```
+
+Si le certificat local est auto-signé, utiliser `curl -Ik` (avec `-k`) sur le domaine LAN.
+
+---
+
+*Document généré pour décrire l’état du dépôt ; mettre à jour ce fichier lors d’ajouts ou de montées de version majeures des dépendances.*
diff --git a/package-lock.json b/package-lock.json
index 90accc4..4c8ecd0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@payloadcms/db-postgres": "^3.77.0",
"@payloadcms/next": "^3.77.0",
"@payloadcms/richtext-lexical": "^3.77.0",
+ "@xyflow/react": "^12.10.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.2",
"graphql": "^16.12.0",
@@ -3248,6 +3249,55 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -3938,6 +3988,38 @@
"win32"
]
},
+ "node_modules/@xyflow/react": {
+ "version": "12.10.2",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
+ "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.76",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.76",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
+ "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -4580,6 +4662,12 @@
"node": ">=8"
}
},
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -4712,6 +4800,111 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -11399,6 +11592,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
@@ -11669,6 +11871,34 @@
"zod": "^3.25.0 || ^4.0.0"
}
},
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/package.json b/package.json
index c2e5b4c..06dfce4 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@payloadcms/db-postgres": "^3.77.0",
"@payloadcms/next": "^3.77.0",
"@payloadcms/richtext-lexical": "^3.77.0",
+ "@xyflow/react": "^12.10.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.2",
"graphql": "^16.12.0",
diff --git a/src/app/(public)/demos/page.tsx b/src/app/(public)/demos/page.tsx
index 7afee2a..3189e2d 100644
--- a/src/app/(public)/demos/page.tsx
+++ b/src/app/(public)/demos/page.tsx
@@ -32,7 +32,6 @@ const demos = [
description:
"Visualisation interactive des couches de sécurité Zero Trust. Explorez les différentes stratégies d'implémentation.",
href: "/demos/zero-trust",
- status: "Bientôt disponible",
},
];
@@ -67,9 +66,11 @@ export default function DemosPage() {
<div className="w-12 h-12 rounded-lg bg-cosmos-900 flex items-center justify-center">
<Icon className="w-6 h-6 text-araucaria-400" />
</div>
- <span className="px-3 py-1 text-xs font-medium bg-araucaria-50 text-araucaria-700 rounded-full border border-araucaria-200">
- {demo.status}
- </span>
+ {demo.status ? (
+ <span className="px-3 py-1 text-xs font-medium bg-araucaria-50 text-araucaria-700 rounded-full border border-araucaria-200">
+ {demo.status}
+ </span>
+ ) : null}
</div>
<h3 className="text-xl font-semibold text-cosmos-900">
{demo.title}
diff --git a/src/app/(public)/demos/zero-trust/page.tsx b/src/app/(public)/demos/zero-trust/page.tsx
new file mode 100644
index 0000000..d99f732
--- /dev/null
+++ b/src/app/(public)/demos/zero-trust/page.tsx
@@ -0,0 +1,43 @@
+import type { Metadata } from "next";
+import dynamic from "next/dynamic";
+
+const ZeroTrustScenarioViewer = dynamic(
+ () =>
+ import("@/components/demos/zero-trust/ZeroTrustScenarioViewer").then(
+ (m) => m.ZeroTrustScenarioViewer,
+ ),
+ { ssr: false },
+);
+
+export const metadata: Metadata = {
+ title: "Simulateur Zero Trust | Der-topogo",
+ description:
+ "Visualisez des scénarios de sécurité Zero Trust : flux de données, décisions de politique, micro-segmentation et bonnes pratiques.",
+};
+
+export default function ZeroTrustDemoPage() {
+ return (
+ <>
+ <section className="bg-cosmos-900 py-16 sm:py-20">
+ <div className="mx-auto max-w-7xl px-6 lg:px-8">
+ <div className="mx-auto max-w-3xl text-center">
+ <h1 className="text-3xl font-bold tracking-tight text-nieve sm:text-5xl">
+ Simulateur Zero Trust
+ </h1>
+ <p className="mt-6 text-lg text-cosmos-300">
+ Comparez un modèle périmétrique et des approches Zero Trust avec
+ des flux visuels entre identités, appareils, passerelles,
+ applications et données.
+ </p>
+ </div>
+ </div>
+ </section>
+
+ <section className="py-12 sm:py-16">
+ <div className="mx-auto max-w-7xl px-6 lg:px-8">
+ <ZeroTrustScenarioViewer />
+ </div>
+ </section>
+ </>
+ );
+}
diff --git a/src/components/demos/zero-trust/ZTFlowNode.tsx b/src/components/demos/zero-trust/ZTFlowNode.tsx
new file mode 100644
index 0000000..571f86c
--- /dev/null
+++ b/src/components/demos/zero-trust/ZTFlowNode.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { Handle, Position, type NodeProps } from "@xyflow/react";
+import {
+ Activity,
+ Database,
+ KeyRound,
+ Laptop,
+ Network,
+ Server,
+ Shield,
+ User,
+} from "lucide-react";
+import type { ZTNodeData } from "./types";
+import { cn } from "@/lib/utils";
+
+const kindIcon = {
+ user: User,
+ device: Laptop,
+ idp: KeyRound,
+ gateway: Shield,
+ workload: Server,
+ data: Database,
+ network: Network,
+ monitoring: Activity,
+};
+
+const handleClass =
+ "!h-2 !w-2 !border-cosmos-500 !bg-araucaria-400";
+
+export function ZTFlowNode({ data }: NodeProps) {
+ const d = data as ZTNodeData;
+ const Icon = kindIcon[d.kind];
+
+ return (
+ <div
+ className={cn(
+ "rounded-xl border-2 border-cosmos-600 bg-cosmos-900 px-3 py-2.5 shadow-lg",
+ "min-w-[130px] max-w-[180px] text-left",
+ )}
+ >
+ <Handle
+ type="target"
+ position={Position.Top}
+ id="top-t"
+ className={cn(handleClass, "!left-[25%]")}
+ />
+ <Handle
+ type="source"
+ position={Position.Top}
+ id="top-s"
+ className={cn(handleClass, "!left-[75%]")}
+ />
+ <Handle
+ type="target"
+ position={Position.Bottom}
+ id="bot-t"
+ className={cn(handleClass, "!left-[25%]")}
+ />
+ <Handle
+ type="source"
+ position={Position.Bottom}
+ id="bot-s"
+ className={cn(handleClass, "!left-[75%]")}
+ />
+ <Handle
+ type="target"
+ position={Position.Left}
+ id="left-t"
+ className={cn(handleClass, "!top-[35%]")}
+ />
+ <Handle
+ type="source"
+ position={Position.Left}
+ id="left-s"
+ className={cn(handleClass, "!top-[65%]")}
+ />
+ <Handle
+ type="target"
+ position={Position.Right}
+ id="right-t"
+ className={cn(handleClass, "!top-[35%]")}
+ />
+ <Handle
+ type="source"
+ position={Position.Right}
+ id="right-s"
+ className={cn(handleClass, "!top-[65%]")}
+ />
+
+ <div className="flex items-start gap-2">
+ <div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-cosmos-800 text-araucaria-400">
+ <Icon className="h-4 w-4" aria-hidden />
+ </div>
+ <div className="min-w-0">
+ <p className="text-[10px] font-medium uppercase tracking-wide text-cosmos-500">
+ {d.role}
+ </p>
+ <p className="text-sm font-semibold leading-snug text-nieve">{d.label}</p>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx b/src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx
new file mode 100644
index 0000000..7f7e221
--- /dev/null
+++ b/src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx
@@ -0,0 +1,282 @@
+"use client";
+
+import {
+ Background,
+ Controls,
+ MiniMap,
+ ReactFlow,
+ ReactFlowProvider,
+ useEdgesState,
+ useNodesState,
+ useReactFlow,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { ChevronLeft, ChevronRight, ShieldCheck } from "lucide-react";
+import { ZTFlowNode } from "./ZTFlowNode";
+import { ZERO_TRUST_SCENARIOS } from "./scenarios";
+import type { ZTScenarioDefinition, ZTScenarioStep } from "./types";
+import { PILLAR_LABELS } from "./types";
+import { cn } from "@/lib/utils";
+
+const nodeTypes = { ztNode: ZTFlowNode };
+
+const CROSS_CUTTING = [
+ "Never trust, always verify — aucune confiance implicite au réseau seul.",
+ "Moindre privilège sur les accès, comptes de service et flux.",
+ "Supposer la compromission : limiter le rayon d’explosion (blast radius).",
+ "Vérification continue : identité, contexte et signaux de risque.",
+ "Journalisation, corrélation et observabilité pour détecter les abus.",
+];
+
+function applyStepStyle(
+ scenario: ZTScenarioDefinition,
+ step: ZTScenarioStep | undefined,
+) {
+ const hn = step?.highlightNodes ?? [];
+ const he = step?.highlightEdges ?? [];
+ const nodeDim =
+ hn.length > 0 ? (id: string) => !hn.includes(id) : () => false;
+ const edgeDim =
+ he.length > 0 ? (id: string) => !he.includes(id) : () => false;
+
+ const nodes = scenario.nodes.map((n) => ({
+ ...n,
+ className: cn(
+ "transition-all duration-300",
+ nodeDim(n.id) ? "opacity-[0.38]" : "opacity-100",
+ hn.includes(n.id) &&
+ "ring-2 ring-araucaria-400 ring-offset-2 ring-offset-cosmos-950 rounded-xl",
+ ),
+ }));
+
+ const edges = scenario.edges.map((e) => {
+ const dimE = edgeDim(e.id);
+ const baseStyle = (e.style as Record<string, unknown> | undefined) ?? {};
+ return {
+ ...e,
+ animated: he.length === 0 ? e.animated : he.includes(e.id),
+ style: {
+ ...baseStyle,
+ opacity: dimE ? 0.32 : 1,
+ },
+ labelStyle: {
+ ...(e.labelStyle as object),
+ opacity: dimE ? 0.45 : 1,
+ },
+ zIndex: he.includes(e.id) ? 10 : 0,
+ };
+ });
+
+ return { nodes, edges };
+}
+
+function FitViewSync({
+ scenarioId,
+ stepIndex,
+}: {
+ scenarioId: string;
+ stepIndex: number;
+}) {
+ const { fitView } = useReactFlow();
+
+ const run = useCallback(() => {
+ const id = requestAnimationFrame(() => {
+ fitView({ padding: 0.18, duration: 280, maxZoom: 1.35 });
+ });
+ return () => cancelAnimationFrame(id);
+ }, [fitView]);
+
+ useEffect(() => {
+ const t = setTimeout(run, 60);
+ return () => clearTimeout(t);
+ }, [scenarioId, stepIndex, run]);
+
+ return null;
+}
+
+function FlowCanvas({
+ scenario,
+ step,
+}: {
+ scenario: ZTScenarioDefinition;
+ step: ZTScenarioStep | undefined;
+}) {
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(
+ () => applyStepStyle(scenario, step),
+ [scenario, step],
+ );
+
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ useEffect(() => {
+ const styled = applyStepStyle(scenario, step);
+ setNodes(styled.nodes);
+ setEdges(styled.edges);
+ }, [scenario, step, setNodes, setEdges]);
+
+ return (
+ <div className="h-[min(70vh,560px)] min-h-[420px] w-full rounded-xl border border-cosmos-700 bg-cosmos-950">
+ <ReactFlow
+ nodes={nodes}
+ edges={edges}
+ onNodesChange={onNodesChange}
+ onEdgesChange={onEdgesChange}
+ nodeTypes={nodeTypes}
+ fitView
+ attributionPosition="bottom-left"
+ proOptions={{ hideAttribution: true }}
+ className="bg-cosmos-950"
+ defaultEdgeOptions={{
+ type: "smoothstep",
+ }}
+ aria-label="Schéma interactif des flux et des composants de sécurité"
+ >
+ <FitViewSync
+ scenarioId={scenario.id}
+ stepIndex={step ? scenario.steps.indexOf(step) : 0}
+ />
+ <Background color="#475569" gap={20} size={1} />
+ <Controls
+ className="!m-3 !border-cosmos-600 !bg-cosmos-900 !fill-nieve [&_button]:!border-cosmos-600 [&_button:hover]:!bg-cosmos-800"
+ showInteractive={false}
+ />
+ <MiniMap
+ nodeStrokeWidth={2}
+ className="!m-3 !rounded-lg !border !border-cosmos-600 !bg-cosmos-900"
+ maskColor="rgba(15, 23, 42, 0.75)"
+ />
+ </ReactFlow>
+ </div>
+ );
+}
+
+export function ZeroTrustScenarioViewer() {
+ const [scenarioIndex, setScenarioIndex] = useState(0);
+ const [stepIndex, setStepIndex] = useState(0);
+
+ const scenario = ZERO_TRUST_SCENARIOS[scenarioIndex]!;
+ const step = scenario.steps[stepIndex];
+
+ useEffect(() => {
+ setStepIndex(0);
+ }, [scenarioIndex]);
+
+ const goPrev = () =>
+ setStepIndex((i) => Math.max(0, i - 1));
+ const goNext = () =>
+ setStepIndex((i) => Math.min(scenario.steps.length - 1, i + 1));
+
+ return (
+ <div className="space-y-8">
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
+ <div className="max-w-xl">
+ <label htmlFor="zt-scenario" className="sr-only">
+ Choisir un scénario
+ </label>
+ <select
+ id="zt-scenario"
+ value={scenario.id}
+ onChange={(e) => {
+ const idx = ZERO_TRUST_SCENARIOS.findIndex(
+ (s) => s.id === e.target.value,
+ );
+ if (idx >= 0) setScenarioIndex(idx);
+ }}
+ className="w-full rounded-lg border border-cosmos-600 bg-nieve px-4 py-2.5 text-sm font-medium text-cosmos-900 shadow-sm focus:border-araucaria-500 focus:outline-none focus:ring-2 focus:ring-araucaria-400 sm:max-w-md"
+ >
+ {ZERO_TRUST_SCENARIOS.map((s) => (
+ <option key={s.id} value={s.id}>
+ {s.title}
+ </option>
+ ))}
+ </select>
+ <p className="mt-2 text-sm text-muted">{scenario.subtitle}</p>
+ </div>
+ <div className="flex items-center gap-2">
+ <button
+ type="button"
+ onClick={goPrev}
+ disabled={stepIndex === 0}
+ className="inline-flex items-center gap-1 rounded-lg border border-cosmos-600 bg-nieve px-3 py-2 text-sm font-medium text-cosmos-800 shadow-sm transition hover:bg-cosmos-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+ <ChevronLeft className="h-4 w-4" aria-hidden />
+ Étape précédente
+ </button>
+ <button
+ type="button"
+ onClick={goNext}
+ disabled={stepIndex >= scenario.steps.length - 1}
+ className="inline-flex items-center gap-1 rounded-lg border border-cosmos-600 bg-nieve px-3 py-2 text-sm font-medium text-cosmos-800 shadow-sm transition hover:bg-cosmos-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+ Étape suivante
+ <ChevronRight className="h-4 w-4" aria-hidden />
+ </button>
+ </div>
+ </div>
+
+ <p className="text-sm leading-relaxed text-cosmos-700">{scenario.intro}</p>
+
+ <ReactFlowProvider>
+ <FlowCanvas scenario={scenario} step={step} />
+ </ReactFlowProvider>
+
+ <div className="grid gap-8 lg:grid-cols-2">
+ <div className="rounded-xl border border-border bg-nieve p-6 shadow-sm">
+ <div className="flex items-center gap-2 text-cosmos-900">
+ <ShieldCheck className="h-5 w-5 text-araucaria-600" aria-hidden />
+ <h3 className="text-lg font-semibold">
+ Étape {stepIndex + 1} — {step?.title}
+ </h3>
+ </div>
+ <p className="mt-3 text-sm leading-relaxed text-muted">
+ {step?.description}
+ </p>
+ {step && step.pillars.length > 0 && (
+ <div className="mt-4">
+ <p className="text-xs font-semibold uppercase tracking-wide text-cosmos-500">
+ Piliers NIST SP 800-207 (rappel)
+ </p>
+ <ul className="mt-2 flex flex-wrap gap-2">
+ {step.pillars.map((p) => (
+ <li
+ key={p}
+ className="rounded-full bg-araucaria-50 px-3 py-1 text-xs font-medium text-araucaria-900 ring-1 ring-araucaria-200"
+ >
+ {PILLAR_LABELS[p]}
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+ {step && (
+ <ul className="mt-4 list-disc space-y-2 pl-5 text-sm text-cosmos-800">
+ {step.practices.map((line, i) => (
+ <li key={i}>{line}</li>
+ ))}
+ </ul>
+ )}
+ </div>
+
+ <div className="rounded-xl border border-border bg-cosmos-900/5 p-6">
+ <h3 className="text-lg font-semibold text-cosmos-900">
+ Principes transverses Zero Trust
+ </h3>
+ <ul className="mt-4 space-y-3 text-sm leading-relaxed text-cosmos-800">
+ {CROSS_CUTTING.map((line, i) => (
+ <li key={i} className="flex gap-2">
+ <span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-araucaria-500" />
+ <span>{line}</span>
+ </li>
+ ))}
+ </ul>
+ <p className="mt-6 text-xs text-cosmos-500">
+ Référence : modèle en piliers décrit dans NIST SP 800-207 (Zero Trust
+ Architecture). Les libellés sont vulgarisés pour l’interface.
+ </p>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/demos/zero-trust/scenarios/classic-perimeter.ts b/src/components/demos/zero-trust/scenarios/classic-perimeter.ts
new file mode 100644
index 0000000..6a6347d
--- /dev/null
+++ b/src/components/demos/zero-trust/scenarios/classic-perimeter.ts
@@ -0,0 +1,123 @@
+import type { Edge, Node } from "@xyflow/react";
+import { MarkerType } from "@xyflow/react";
+import type { ZTNodeData, ZTScenarioDefinition } from "../types";
+
+const node = (
+ id: string,
+ x: number,
+ y: number,
+ data: ZTNodeData,
+): Node<ZTNodeData> => ({
+ id,
+ type: "ztNode",
+ position: { x, y },
+ data,
+});
+
+const edge = (
+ id: string,
+ source: string,
+ target: string,
+ label: string,
+ opts?: { dashed?: boolean; color?: string },
+): Edge => ({
+ id,
+ source,
+ target,
+ label,
+ animated: true,
+ markerEnd: { type: MarkerType.ArrowClosed, width: 18, height: 18 },
+ style: opts?.dashed
+ ? { strokeDasharray: "6 4", stroke: opts.color ?? "#94a3b8" }
+ : { stroke: opts?.color ?? "#64748b" },
+ labelStyle: { fill: "#334155", fontWeight: 500, fontSize: 11 },
+ labelBgStyle: { fill: "#f8fafc", fillOpacity: 0.95 },
+});
+
+export const classicPerimeterScenario: ZTScenarioDefinition = {
+ id: "classic-perimeter",
+ title: "Périmètre réseau classique",
+ subtitle: "Confiance implicite « à l’intérieur » du LAN",
+ intro:
+ "Modèle souvent associé au VPN : une fois le tunnel établi, le trafic interne est largement considéré comme fiable. Les déplacements latéraux (east-west) peuvent rester peu contrôlés.",
+ nodes: [
+ node("user", 40, 200, {
+ kind: "user",
+ label: "Utilisateur",
+ role: "Identité",
+ }),
+ node("laptop", 200, 200, {
+ kind: "device",
+ label: "Poste",
+ role: "Appareil",
+ }),
+ node("vpn", 360, 200, {
+ kind: "gateway",
+ label: "Passerelle / VPN",
+ role: "Réseau",
+ }),
+ node("lan", 560, 120, {
+ kind: "network",
+ label: "LAN « de confiance »",
+ role: "Segment interne",
+ }),
+ node("app", 560, 40, {
+ kind: "workload",
+ label: "Application",
+ role: "Charge de travail",
+ }),
+ node("db", 720, 200, {
+ kind: "data",
+ label: "Données",
+ role: "Stockage",
+ }),
+ ],
+ edges: [
+ edge("e-u-l", "user", "laptop", "Session locale"),
+ edge("e-l-v", "laptop", "vpn", "Tunnel VPN", { color: "#0d9488" }),
+ edge("e-v-lan", "vpn", "lan", "Accès réseau étendu"),
+ edge("e-lan-app", "lan", "app", "HTTP/S — confiance zone"),
+ edge("e-app-db", "app", "db", "SQL — souvent large confiance"),
+ ],
+ steps: [
+ {
+ id: "s1",
+ title: "Vue d’ensemble",
+ description:
+ "L’utilisateur joint le réseau via une passerelle. Le segment interne est souvent traité comme un tout.",
+ highlightNodes: ["user", "laptop", "vpn", "lan", "app", "db"],
+ highlightEdges: [],
+ pillars: ["network"],
+ practices: [
+ "Cartographier les flux réels (y compris east-west), pas seulement l’accès distant.",
+ "Ne pas confondre « chiffrement du tunnel » et « confiance des identités ».",
+ ],
+ },
+ {
+ id: "s2",
+ title: "Tunnel jusqu’au périmètre",
+ description:
+ "Le VPN chiffre le transport, mais ne remplace pas une décision d’accès fine à chaque ressource.",
+ highlightNodes: ["laptop", "vpn"],
+ highlightEdges: ["e-l-v"],
+ pillars: ["network", "device"],
+ practices: [
+ "Exiger posture et conformité des appareils (MDM, santé du poste).",
+ "Journaliser les accès au réseau et corréler avec les accès applicatifs.",
+ ],
+ },
+ {
+ id: "s3",
+ title: "Confiance plate en interne",
+ description:
+ "Une fois dans le LAN, l’application parle souvent à la base avec des comptes ou règles larges : surface d’attaque east-west élevée si un poste est compromis.",
+ highlightNodes: ["lan", "app", "db"],
+ highlightEdges: ["e-lan-app", "e-app-db"],
+ pillars: ["application", "data", "network"],
+ practices: [
+ "Segmenter et appliquer des politiques au plus près des charges (micro-segmentation).",
+ "Principe du moindre privilège sur les comptes de service et les flux base de données.",
+ ],
+ },
+ ],
+};
diff --git a/src/components/demos/zero-trust/scenarios/east-west.ts b/src/components/demos/zero-trust/scenarios/east-west.ts
new file mode 100644
index 0000000..1200da7
--- /dev/null
+++ b/src/components/demos/zero-trust/scenarios/east-west.ts
@@ -0,0 +1,126 @@
+import { MarkerType } from "@xyflow/react";
+import type { ZTNodeData, ZTScenarioDefinition } from "../types";
+
+const node = (
+ id: string,
+ x: number,
+ y: number,
+ data: ZTNodeData,
+): Node<ZTNodeData> => ({
+ id,
+ type: "ztNode",
+ position: { x, y },
+ data,
+});
+
+const edgeOk = (
+ id: string,
+ source: string,
+ target: string,
+ label: string,
+): Edge => ({
+ id,
+ source,
+ target,
+ label,
+ animated: true,
+ markerEnd: { type: MarkerType.ArrowClosed, width: 18, height: 18 },
+ style: { stroke: "#0d9488" },
+ labelStyle: { fill: "#134e4a", fontWeight: 500, fontSize: 11 },
+ labelBgStyle: { fill: "#ecfdf5", fillOpacity: 0.95 },
+});
+
+const edgeBlocked = (
+ id: string,
+ source: string,
+ target: string,
+ label: string,
+): Edge => ({
+ id,
+ source,
+ target,
+ label,
+ animated: false,
+ markerEnd: { type: MarkerType.ArrowClosed, width: 18, height: 18, color: "#dc2626" },
+ style: { stroke: "#dc2626", strokeDasharray: "6 4" },
+ labelStyle: { fill: "#991b1b", fontWeight: 600, fontSize: 11 },
+ labelBgStyle: { fill: "#fef2f2", fillOpacity: 0.95 },
+});
+
+export const eastWestScenario: ZTScenarioDefinition = {
+ id: "east-west",
+ title: "Micro-segmentation east-west",
+ subtitle: "Contrôler les flux latéraux entre charges",
+ intro:
+ "Sans segmentation, un compromis sur un service peut se propager. Les politiques réseau et applicatives limitent qui peut parler à qui, avec observabilité centralisée.",
+ nodes: [
+ node("svc-a", 80, 200, {
+ kind: "workload",
+ label: "Service A",
+ role: "Microservice",
+ }),
+ node("svc-b", 480, 200, {
+ kind: "workload",
+ label: "Service B",
+ role: "Microservice",
+ }),
+ node("seg", 280, 200, {
+ kind: "gateway",
+ label: "Segmentation / politique",
+ role: "Contrôle réseau",
+ }),
+ node("siem", 280, 40, {
+ kind: "monitoring",
+ label: "SIEM / observabilité",
+ role: "Journalisation",
+ }),
+ ],
+ edges: [
+ edgeOk("e-a-s", "svc-a", "seg", "Flux autorisé et inspecté"),
+ edgeOk("e-s-b", "seg", "svc-b", "Suite si politique OK"),
+ edgeBlocked("e-a-b", "svc-a", "svc-b", "Direct — refusé"),
+ edgeOk("e-a-m", "svc-a", "siem", "Événements"),
+ edgeOk("e-b-m", "svc-b", "siem", "Événements"),
+ ],
+ steps: [
+ {
+ id: "ew1",
+ title: "Flux latéraux contrôlés",
+ description:
+ "Le chemin autorisé passe par la couche de segmentation (pare-feu distribué, service mesh, politiques cloud).",
+ highlightNodes: ["svc-a", "seg", "svc-b"],
+ highlightEdges: ["e-a-s", "e-s-b"],
+ pillars: ["network", "application"],
+ practices: [
+ "Définir des groupes de charges et des règles explicites (allow-list), pas un « tout ouvert » en interne.",
+ "Réviser régulièrement les règles avec les propriétaires métier.",
+ ],
+ },
+ {
+ id: "ew2",
+ title: "Blocage du raccourci",
+ description:
+ "Une tentative de communication directe entre services sans passer par la politique est refusée et visible.",
+ highlightNodes: ["svc-a", "svc-b"],
+ highlightEdges: ["e-a-b"],
+ pillars: ["network", "data"],
+ practices: [
+ "Supposer la compromission : limiter le blast radius par défaut.",
+ "Tester les règles (red team / tests de fuite) pour valider la segmentation.",
+ ],
+ },
+ {
+ id: "ew3",
+ title: "Visibilité et corrélation",
+ description:
+ "Les journaux remontent vers une couche d’observabilité pour corréler identités, flux et alertes.",
+ highlightNodes: ["svc-a", "svc-b", "siem"],
+ highlightEdges: ["e-a-m", "e-b-m"],
+ pillars: ["network", "application"],
+ practices: [
+ "Never trust, always verify : la supervision continue valide que les politiques sont effectivement appliquées.",
+ "Alertes sur les flux non conformes ou les nouvelles dépendances applicatives.",
+ ],
+ },
+ ],
+};
diff --git a/src/components/demos/zero-trust/scenarios/index.ts b/src/components/demos/zero-trust/scenarios/index.ts
new file mode 100644
index 0000000..c8d557b
--- /dev/null
+++ b/src/components/demos/zero-trust/scenarios/index.ts
@@ -0,0 +1,18 @@
+import { classicPerimeterScenario } from "./classic-perimeter";
+import { eastWestScenario } from "./east-west";
+import { zeroTrustAccessScenario } from "./zero-trust-access";
+import type { ZTScenarioDefinition } from "../types";
+
+export const ZERO_TRUST_SCENARIOS: ZTScenarioDefinition[] = [
+ classicPerimeterScenario,
+ zeroTrustAccessScenario,
+ eastWestScenario,
+];
+
+export function getScenarioById(
+ id: string,
+): ZTScenarioDefinition | undefined {
+ return ZERO_TRUST_SCENARIOS.find((s) => s.id === id);
+}
+
+export { classicPerimeterScenario, zeroTrustAccessScenario, eastWestScenario };
diff --git a/src/components/demos/zero-trust/scenarios/zero-trust-access.ts b/src/components/demos/zero-trust/scenarios/zero-trust-access.ts
new file mode 100644
index 0000000..9dd3a4d
--- /dev/null
+++ b/src/components/demos/zero-trust/scenarios/zero-trust-access.ts
@@ -0,0 +1,143 @@
+import type { Edge, Node } from "@xyflow/react";
+import { MarkerType } from "@xyflow/react";
+import type { ZTNodeData, ZTScenarioDefinition } from "../types";
+
+const node = (
+ id: string,
+ x: number,
+ y: number,
+ data: ZTNodeData,
+): Node<ZTNodeData> => ({
+ id,
+ type: "ztNode",
+ position: { x, y },
+ data,
+});
+
+const edge = (
+ id: string,
+ source: string,
+ target: string,
+ label: string,
+ color?: string,
+ handles?: { sourceHandle?: string; targetHandle?: string },
+): Edge => ({
+ id,
+ source,
+ target,
+ label,
+ sourceHandle: handles?.sourceHandle,
+ targetHandle: handles?.targetHandle,
+ animated: true,
+ markerEnd: { type: MarkerType.ArrowClosed, width: 18, height: 18 },
+ style: { stroke: color ?? "#0f766e" },
+ labelStyle: { fill: "#134e4a", fontWeight: 500, fontSize: 11 },
+ labelBgStyle: { fill: "#ecfdf5", fillOpacity: 0.95 },
+});
+
+export const zeroTrustAccessScenario: ZTScenarioDefinition = {
+ id: "zero-trust-access",
+ title: "Accès avec vérification continue",
+ subtitle: "Chaque requête est évaluée : identité, contexte, politique",
+ intro:
+ "Approche alignée sur NIST SP 800-207 : pas de confiance implicite au réseau. L’identité et le contexte (appareil, risque) alimentent une décision d’accès avant d’atteindre l’application et les données.",
+ nodes: [
+ node("user", 40, 220, {
+ kind: "user",
+ label: "Utilisateur",
+ role: "Identité",
+ }),
+ node("device", 220, 220, {
+ kind: "device",
+ label: "Poste / mobile",
+ role: "Appareil",
+ }),
+ node("idp", 220, 40, {
+ kind: "idp",
+ label: "IdP (OIDC)",
+ role: "Authentification",
+ }),
+ node("pep", 420, 220, {
+ kind: "gateway",
+ label: "PEP / API GW",
+ role: "Application du contrôle",
+ }),
+ node("app", 600, 120, {
+ kind: "workload",
+ label: "Application",
+ role: "Charge de travail",
+ }),
+ node("data", 600, 300, {
+ kind: "data",
+ label: "Données",
+ role: "Stockage chiffré",
+ }),
+ ],
+ edges: [
+ edge("e-ud", "user", "device", "Interaction", undefined, {
+ sourceHandle: "right-s",
+ targetHandle: "left-t",
+ }),
+ edge("e-di", "device", "idp", "Authorization Code + PKCE", "#0369a1", {
+ sourceHandle: "top-s",
+ targetHandle: "bot-t",
+ }),
+ edge("e-id", "idp", "device", "Jetons (access, refresh)", "#0369a1", {
+ sourceHandle: "bot-s",
+ targetHandle: "top-t",
+ }),
+ edge("e-dp", "device", "pep", "Requête + jeton", "#0f766e", {
+ sourceHandle: "right-s",
+ targetHandle: "left-t",
+ }),
+ edge("e-pa", "pep", "app", "Autorisé si politique OK", "#0f766e", {
+ sourceHandle: "top-s",
+ targetHandle: "bot-t",
+ }),
+ edge("e-ad", "app", "data", "mTLS / chiffrement", "#7c3aed", {
+ sourceHandle: "bot-s",
+ targetHandle: "top-t",
+ }),
+ ],
+ steps: [
+ {
+ id: "z1",
+ title: "Authentification forte et contexte",
+ description:
+ "L’IdP émet des jetons après vérification de l’identité et du contexte (MFA, risque, appareil).",
+ highlightNodes: ["user", "device", "idp"],
+ highlightEdges: ["e-ud", "e-di", "e-id"],
+ pillars: ["identity", "device"],
+ practices: [
+ "MFA adaptatif ; pas de secrets long-terme exposés côté client (PKCE pour les clients publics).",
+ "Évaluer la posture de l’appareil avant d’accorder la session.",
+ ],
+ },
+ {
+ id: "z2",
+ title: "Décision de politique au point d’application",
+ description:
+ "Le PEP (Policy Enforcement Point) applique les règles : qui, quoi, depuis où, avec quel risque — avant d’atteindre la charge de travail.",
+ highlightNodes: ["device", "pep", "app"],
+ highlightEdges: ["e-dp", "e-pa"],
+ pillars: ["application", "network"],
+ practices: [
+ "Séparer clairement PDP (politique) et PEP (application) dans l’architecture cible.",
+ "Moindre privilège : scopes OAuth/OIDC minimaux, rôles applicatifs justifiés.",
+ ],
+ },
+ {
+ id: "z3",
+ title: "Protection des données jusqu’au stockage",
+ description:
+ "Les flux vers les données sont chiffrés et tracés ; l’accès reste soumis au contrôle continu (révocation, session courte, audit).",
+ highlightNodes: ["app", "data"],
+ highlightEdges: ["e-ad"],
+ pillars: ["data", "application"],
+ practices: [
+ "Chiffrement au repos et en transit ; classification des données sensibles.",
+ "Observabilité : journaux corrélés pour détecter les abus de jetons ou les accès anormaux.",
+ ],
+ },
+ ],
+};
diff --git a/src/components/demos/zero-trust/types.ts b/src/components/demos/zero-trust/types.ts
new file mode 100644
index 0000000..07dd5b7
--- /dev/null
+++ b/src/components/demos/zero-trust/types.ts
@@ -0,0 +1,54 @@
+import type { Edge, Node } from "@xyflow/react";
+
+export type ZTPillar =
+ | "identity"
+ | "device"
+ | "application"
+ | "network"
+ | "data";
+
+export type FlowState = "allowed" | "denied" | "challenge" | "encrypted";
+
+export type ZTNodeKind =
+ | "user"
+ | "device"
+ | "idp"
+ | "gateway"
+ | "workload"
+ | "data"
+ | "network"
+ | "monitoring";
+
+export type ZTNodeData = {
+ label: string;
+ role: string;
+ kind: ZTNodeKind;
+};
+
+export type ZTScenarioStep = {
+ id: string;
+ title: string;
+ description: string;
+ highlightNodes: string[];
+ highlightEdges: string[];
+ pillars: ZTPillar[];
+ practices: string[];
+};
+
+export type ZTScenarioDefinition = {
+ id: string;
+ title: string;
+ subtitle: string;
+ intro: string;
+ nodes: Node<ZTNodeData>[];
+ edges: Edge[];
+ steps: ZTScenarioStep[];
+};
+
+export const PILLAR_LABELS: Record<ZTPillar, string> = {
+ identity: "Identité",
+ device: "Appareil",
+ application: "Application / charge de travail",
+ network: "Réseau",
+ data: "Données",
+};