From 96615b46a72e7902f7ade2619b21649bf41b2b1b Mon Sep 17 00:00:00 2001 From: ertopogo Date: Mon, 6 Apr 2026 13:50:16 +0200 Subject: documentation zero trust --- src/components/demos/zero-trust/ZTFlowNode.tsx | 104 ++++++++ .../demos/zero-trust/ZeroTrustScenarioViewer.tsx | 282 +++++++++++++++++++++ .../zero-trust/scenarios/classic-perimeter.ts | 123 +++++++++ .../demos/zero-trust/scenarios/east-west.ts | 126 +++++++++ src/components/demos/zero-trust/scenarios/index.ts | 18 ++ .../zero-trust/scenarios/zero-trust-access.ts | 143 +++++++++++ src/components/demos/zero-trust/types.ts | 54 ++++ 7 files changed, 850 insertions(+) create mode 100644 src/components/demos/zero-trust/ZTFlowNode.tsx create mode 100644 src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx create mode 100644 src/components/demos/zero-trust/scenarios/classic-perimeter.ts create mode 100644 src/components/demos/zero-trust/scenarios/east-west.ts create mode 100644 src/components/demos/zero-trust/scenarios/index.ts create mode 100644 src/components/demos/zero-trust/scenarios/zero-trust-access.ts create mode 100644 src/components/demos/zero-trust/types.ts (limited to 'src/components/demos/zero-trust') 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 ( +
+ + + + + + + + + +
+
+ +
+
+

+ {d.role} +

+

{d.label}

+
+
+
+ ); +} 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 | 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 ( +
+ + + + + + +
+ ); +} + +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 ( +
+
+
+ + +

{scenario.subtitle}

+
+
+ + +
+
+ +

{scenario.intro}

+ + + + + +
+
+
+ +

+ Étape {stepIndex + 1} — {step?.title} +

+
+

+ {step?.description} +

+ {step && step.pillars.length > 0 && ( +
+

+ Piliers NIST SP 800-207 (rappel) +

+
    + {step.pillars.map((p) => ( +
  • + {PILLAR_LABELS[p]} +
  • + ))} +
+
+ )} + {step && ( +
    + {step.practices.map((line, i) => ( +
  • {line}
  • + ))} +
+ )} +
+ +
+

+ Principes transverses Zero Trust +

+
    + {CROSS_CUTTING.map((line, i) => ( +
  • + + {line} +
  • + ))} +
+

+ 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. +

+
+
+
+ ); +} 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 => ({ + 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 => ({ + 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 => ({ + 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[]; + edges: Edge[]; + steps: ZTScenarioStep[]; +}; + +export const PILLAR_LABELS: Record = { + identity: "Identité", + device: "Appareil", + application: "Application / charge de travail", + network: "Réseau", + data: "Données", +}; -- cgit v1.2.3