diff options
| author | ertopogo <erwin.t.pombett@gmail.com> | 2026-04-06 13:50:16 +0200 |
|---|---|---|
| committer | ertopogo <erwin.t.pombett@gmail.com> | 2026-04-06 13:50:16 +0200 |
| commit | 96615b46a72e7902f7ade2619b21649bf41b2b1b (patch) | |
| tree | d33c565ddf80ca3e5b1809d361470dea29d86ea2 | |
| parent | 202f3256fa1bb60a72322ca1c4c3b5e6ffca212a (diff) | |
documentation zero trust
| -rw-r--r-- | debug.log | 1 | ||||
| -rw-r--r-- | documentation/techno.md | 124 | ||||
| -rw-r--r-- | package-lock.json | 230 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/app/(public)/demos/page.tsx | 9 | ||||
| -rw-r--r-- | src/app/(public)/demos/zero-trust/page.tsx | 43 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/ZTFlowNode.tsx | 104 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx | 282 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/scenarios/classic-perimeter.ts | 123 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/scenarios/east-west.ts | 126 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/scenarios/index.ts | 18 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/scenarios/zero-trust-access.ts | 143 | ||||
| -rw-r--r-- | src/components/demos/zero-trust/types.ts | 54 |
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", +}; |
