summaryrefslogtreecommitdiff
path: root/src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx')
-rw-r--r--src/components/demos/zero-trust/ZeroTrustScenarioViewer.tsx282
1 files changed, 282 insertions, 0 deletions
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>
+ );
+}