Rendu_Securite_IoT-Jeep_Che.../iot_risk_briefing.jsx
2026-06-08 10:31:03 +02:00

2050 lines
89 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from "react";
import {
Shield,
AlertTriangle,
Radio,
Car,
Tv,
Bug,
Lock,
Wrench,
Target,
Zap,
ChevronRight,
Calendar,
MapPin,
Users,
TrendingUp,
Activity,
Skull,
Network,
HardDrive,
} from "lucide-react";
// ─────────────────────────────────────────────────────────────
// DATA — Deux cas d'étude IoT
// ─────────────────────────────────────────────────────────────
const CASES = {
jeep: {
id: "jeep",
codename: "CHRYSLER-2015-001",
title: "Jeep Cherokee Hack",
year: "2015",
subtitle: "Prise de contrôle à distance via réseau cellulaire",
icon: Car,
accent: "#ff3b30",
stats: [
{ label: "Véhicules rappelés", value: "1.4M", icon: Car },
{ label: "Chercheurs", value: "2", icon: Users },
{ label: "Réseau exploité", value: "Sprint", icon: Radio },
{ label: "Distance d'attaque", value: "∞ km", icon: Target },
],
timeline: [
{
phase: "01",
title: "Reconnaissance",
text: "Miller & Valasek identifient le système Uconnect (Harman-Kardon) comme cible. Tout véhicule Sprint connecté est scannable depuis n'importe quel téléphone du même opérateur — la flotte forme un sous-réseau adressable.",
},
{
phase: "02",
title: "Pivot Wi-Fi → Cellulaire",
text: "Première intrusion via le hotspot Wi-Fi du véhicule, puis exploitation d'un port D-Bus ouvert (port 6667) accessible directement depuis le réseau cellulaire Sprint — aucun firewall en sortie.",
},
{
phase: "03",
title: "Compromission de la head unit",
text: "Code arbitraire exécuté sur le système Linux du Uconnect via des services D-Bus non authentifiés. À ce stade : contrôle de la radio, GPS, climatisation.",
},
{
phase: "04",
title: "Pivot vers le CAN bus",
text: "Reflashing du microcontrôleur Renesas V850 via une liaison SPI depuis le processeur OMAP. Un firmware modifié permet d'envoyer des trames CAN arbitraires.",
},
{
phase: "05",
title: "Contrôle physique",
text: "Direction, freins, accélérateur, transmission, frein de parking : tout est pilotable à distance. Démonstration publique sur autoroute avec un journaliste de Wired comme cobaye.",
},
],
why: [
{
title: "Architecture plate sans segmentation",
text: "Aucune séparation logique entre le réseau infotainment (non critique) et le CAN bus (sécurité fonctionnelle). Le V850 acceptait du firmware non signé via SPI.",
},
{
title: "Modèle de menace obsolète",
text: "Les constructeurs raisonnaient en termes d'accès physique au véhicule. Le scénario « attaquant distant via opérateur télécom » n'était pas dans le threat model.",
},
{
title: "Services exposés sans authentification",
text: "D-Bus écoutait sur 0.0.0.0 sur un port joignable depuis Internet via Sprint. Aucune ACL, aucun TLS, aucun secret partagé.",
},
{
title: "Pas d'isolation opérateur",
text: "Sprint n'isolait pas les véhicules entre eux sur son réseau privé : un appareil quelconque pouvait scanner tous les Uconnect du pays.",
},
],
problems: [
"Surface d'attaque massive (1.4M véhicules joignables depuis n'importe quel téléphone Sprint)",
"Danger physique direct : freins, direction et accélérateur contrôlables à 110 km/h",
"Détection impossible côté conducteur — aucune télémétrie de sécurité",
"Patch impossible OTA en 2015 : rappel physique nécessaire pour 1.4M véhicules",
"Le V850 acceptait n'importe quel firmware via SPI — pas de signature cryptographique",
],
solutions: [
{
title: "Segmentation réseau interne (gateway CAN)",
text: "Insertion d'une passerelle filtrante entre l'infotainment et le CAN bus, avec liste blanche stricte des trames autorisées.",
},
{
title: "Firmware signé partout",
text: "Secure boot et signature cryptographique obligatoire sur tous les ECU, y compris les microcontrôleurs périphériques (V850 et équivalents).",
},
{
title: "OTA sécurisées (Uptane)",
text: "Adoption d'Uptane comme standard de mise à jour OTA résistant aux compromissions de serveur, déployé depuis chez les principaux constructeurs.",
},
{
title: "Isolation au niveau opérateur",
text: "Sprint a fermé l'accès au port 6667 côté APN et isolé les véhicules sur des sous-réseaux dédiés sans communication transverse.",
},
{
title: "ISO/SAE 21434 & UNECE R155",
text: "Standards apparus après l'incident : threat modeling cybersécurité obligatoire pour l'homologation des véhicules à partir de 2022.",
},
],
probability: 3,
impact: 4,
probReason:
"À la divulgation : 1.4M véhicules vulnérables, scan automatisable depuis n'importe quel terminal Sprint. Un PoC fonctionnel existait, l'exploit était reproductible — probable.",
impactReason:
"Contrôle distant des freins et de la direction à haute vitesse = danger physique direct, mort possible. Sanction juridique potentielle pour le constructeur. Atteinte irréversible (un mort) — critique.",
risks: [
{
id: "R1",
title: "Énumération à distance de la flotte Uconnect sur le réseau Sprint",
description:
"Scan d'IP automatisé depuis n'importe quel terminal Sprint pour cartographier les 1.4M véhicules joignables. Peut être incorporé dans un worm.",
probability: 4,
impact: 1,
probHypothesis:
"Aucun isolement L2/L3 entre clients Sprint. Le port D-Bus 6667 répond ou ferme selon la présence du Uconnect, c'est un signal trivialement détectable. Outillage de scan standard (nmap) suffit.",
impactHypothesis:
"Aucune compromission, aucune action sur le véhicule — seulement de la reconnaissance. Mais c'est le prérequis indispensable de toutes les attaques de masse, donc à ne pas sous-estimer dans l'analyse globale.",
solution:
"Isolation L2/L3 sur l'APN opérateur (Sprint a depuis cloisonné ses véhicules sur un sous-réseau dédié sans communication transverse). Côté véhicule : fermeture du port D-Bus en sortie, ACL stricte limitant les connexions entrantes aux serveurs constructeur authentifiés via TLS mutuel.",
},
{
id: "R2",
title: "Exécution de code à distance sur la head unit Linux via D-Bus port 6667",
description:
"Service D-Bus exposé sur 0.0.0.0 sans authentification depuis le réseau cellulaire. Permet d'obtenir un shell sur le système Linux du Uconnect.",
probability: 3,
impact: 2,
probHypothesis:
"Exploit fonctionnel documenté par Miller & Valasek. Reproductible pour qui dispose du PoC. Nécessite expertise mais pas génie — donc plus d'un acteur à l'horizon de la divulgation.",
impactHypothesis:
"À ce stade, seul l'infotainment est compromis : radio, GPS, climatisation, écran. Gêne notable pour le conducteur mais pas de danger physique direct. Dégradation partielle du service.",
solution:
"Service D-Bus en écoute locale uniquement (127.0.0.1), authentification mutuelle TLS sur tous les services exposés, désactivation des services non essentiels (principle of least privilege), firewall iptables/netfilter restrictif sur la head unit avec règles par défaut DROP.",
},
{
id: "R3",
title: "Reflashing du microcontrôleur Renesas V850 avec firmware non signé via SPI",
description:
"Depuis l'OMAP compromis, écriture d'un firmware modifié sur le V850 qui interface avec le CAN bus. Aucune vérification de signature côté V850.",
probability: 2,
impact: 3,
probHypothesis:
"Suppose la compromission préalable de la head unit (R2) ET la maîtrise du reverse engineering du V850. Compétences embarqué pointues — barrière à l'entrée réelle.",
impactHypothesis:
"Permet d'envoyer des trames CAN arbitraires : un attaquant peut désormais agir sur n'importe quel sous-système. Interruption de service possible, perte de fiabilité — majeur.",
solution:
"Secure boot avec vérification de signature cryptographique du firmware (RSA-2048 ou ECDSA P-256), HSM (Hardware Security Module) intégré au V850 pour stocker les clés publiques en lecture seule, rollback protection anti-downgrade pour empêcher la réinstallation d'une version antérieure vulnérable.",
},
{
id: "R4",
title: "Contrôle distant des freins, direction et accélérateur en conduite à haute vitesse",
description:
"Envoi de trames CAN ciblant les ECU de sécurité fonctionnelle pendant que le véhicule roule. Démontré sur autoroute à 110 km/h.",
probability: 2,
impact: 4,
probHypothesis:
"Nécessite toute la chaîne d'attaque (R1 → R2 → R3) ET la connaissance des trames CAN spécifiques au modèle ET un timing offensif. Pas trivial mais pas impossible pour un acteur étatique.",
impactHypothesis:
"Perte de contrôle direction/freins à vitesse autoroutière = accident potentiellement mortel. Sanction juridique massive pour le constructeur (FCA), atteinte irréversible en cas de décès — critique.",
solution:
"Défense en profondeur : (1) Gateway CAN filtrante entre l'infotainment et le CAN bus critique, avec liste blanche stricte des trames autorisées. (2) SecOC (AUTOSAR Secure Onboard Communication) pour authentifier chaque trame CAN via MAC. (3) IDS embarqué détectant les patterns de trames anormaux avec coupure automatique. (4) Conformité ISO/SAE 21434 et UNECE R155 (obligatoires depuis 2022 pour l'homologation des véhicules).",
},
{
id: "R5",
title: "Désactivation persistante du frein de parking pour immobilisation/sabotage",
description:
"Modification ciblée d'une fonction unique (parking brake) maintenue après reboot via firmware V850 modifié. Le véhicule devient inutilisable.",
probability: 2,
impact: 2,
probHypothesis:
"Mêmes prérequis que R4 (chaîne complète), mais cible un sous-système plus simple. Pas observé en pratique car la motivation criminelle est faible — l'ampleur du crime n'est pas proportionnelle au gain.",
impactHypothesis:
"Véhicule immobilisé, intervention dealer obligatoire — gêne réelle mais pas de danger vital. Préjudice économique modéré pour le propriétaire. Mineur.",
solution:
"Watchdog matériel sur les ECU critiques qui rétablit l'état nominal en cas d'incohérence détectée. Vérification d'intégrité du firmware au démarrage de chaque ECU via Trusted Platform Module (TPM). Journalisation tamper-evident des modifications de firmware avec audit obligatoire lors de chaque entretien dealer.",
},
],
amdec: [
{
id: "AMD-01",
component: "Réseau cellulaire Sprint / APN constructeur",
function: "Fournir la connectivité du Uconnect aux serveurs FCA",
failureMode:
"Absence d'isolation L2/L3 entre clients Sprint — les véhicules sont mutuellement adressables",
cause:
"Configuration laxiste de l'APN, pas de cloisonnement par VLAN, port D-Bus 6667 ouvert en entrée depuis tout le réseau cellulaire",
effect:
"Énumération automatisée de la flotte (1.4M véhicules) depuis n'importe quel terminal Sprint, scan IP trivialisé",
severity: 6,
occurrence: 10,
detection: 4,
action:
"Cloisonnement des véhicules sur un sous-réseau dédié sans communication transverse (mesure réellement appliquée par Sprint post-incident). Filtrage du port 6667 côté opérateur.",
},
{
id: "AMD-02",
component: "Service D-Bus de la head unit Uconnect",
function: "Communication inter-processus locale entre services de l'infotainment",
failureMode:
"Service D-Bus en écoute sur 0.0.0.0:6667 sans authentification, exposé à Internet via le modem cellulaire",
cause:
"Configuration par défaut conservée en production, pas de durcissement systémique, principe de moindre exposition non appliqué",
effect:
"Exécution de code à distance sur le système Linux du Uconnect, compromission de l'infotainment (radio, GPS, climatisation)",
severity: 8,
occurrence: 7,
detection: 2,
action:
"Bind D-Bus sur 127.0.0.1 uniquement. Authentification mutuelle TLS sur tous services exposés. Firewall iptables avec politique DROP par défaut. Audit systémique des ports ouverts avant homologation.",
},
{
id: "AMD-03",
component: "Microcontrôleur Renesas V850 (passerelle CAN)",
function:
"Faire le pont entre la head unit (OMAP) et le CAN bus du véhicule (ECU de sécurité)",
failureMode:
"Accepte du firmware non signé via la liaison SPI depuis l'OMAP — pas de vérification cryptographique",
cause:
"Pas de secure boot implémenté, pas de stockage de clés (HSM/TPM), conception centrée fonctionnalité sans modèle de menace cyber",
effect:
"Pivot depuis l'infotainment compromis vers le CAN bus critique : injection de trames CAN arbitraires possible",
severity: 9,
occurrence: 4,
detection: 8,
action:
"Secure boot avec signature cryptographique (RSA-2048 / ECDSA P-256). HSM intégré stockant les clés publiques en lecture seule. Rollback protection anti-downgrade. Attestation au boot vers une autorité constructeur.",
},
{
id: "AMD-04",
component: "Architecture réseau interne du véhicule",
function:
"Interconnecter les ECU (infotainment, motorisation, freinage, direction, transmission)",
failureMode:
"Architecture plate, pas de segmentation entre le domaine infotainment et le domaine sécurité fonctionnelle",
cause:
"Décisions de conception antérieures à la connectivité cellulaire généralisée, threat model centré sur l'accès physique uniquement",
effect:
"Une fois le V850 compromis (AMD-03), toutes les trames CAN sont à portée — contrôle potentiel des actuateurs critiques",
severity: 10,
occurrence: 3,
detection: 7,
action:
"Insertion d'une gateway CAN filtrante entre les domaines avec liste blanche stricte des trames autorisées. Architecture en zones et conduits (IEC 62443 / ISO 21434). IDS embarqué sur le bus CAN.",
},
{
id: "AMD-05",
component: "Protocole CAN bus (ISO 11898)",
function: "Communication temps réel entre ECU pour les fonctions véhicule",
failureMode:
"Aucune authentification des trames CAN — n'importe quel ECU sur le bus peut émettre n'importe quelle commande",
cause:
"Protocole CAN conçu en 1986 pour un environnement physiquement fermé, pas pour résister à un ECU compromis",
effect:
"Direction, freins, accélérateur, transmission pilotables depuis n'importe quel ECU compromis, y compris l'infotainment",
severity: 10,
occurrence: 4,
detection: 8,
action:
"Déploiement de SecOC (AUTOSAR Secure Onboard Communication) — MAC sur chaque trame CAN avec compteur anti-rejeu. Migration progressive vers CAN-FD authentifié. IDS comportemental sur le bus.",
},
],
cves: [
{
id: "CVE-2015-5611",
title:
"Missing Authorization in FCA Uconnect RA3/RA4 (Harman-Kardon Infotainment)",
publishedDate: "2015-09-17",
advisories: [
{ id: "ICSA-15-260-01", source: "CISA / ICS-CERT" },
{ id: "VU#819439", source: "CERT/CC (Carnegie Mellon)" },
],
affected:
"Uconnect 8.4AN / RA3 / RA4 dans Chrysler 200, 300, Charger, Challenger, Durango, Ram 1500/2500/3500, Jeep Cherokee/Grand Cherokee, Dodge Viper (modèles 2013-2015) — environ 1.4M véhicules rappelés.",
description:
"Vulnérabilité d'autorisation manquante dans le système Uconnect manufacturé par Harman-Kardon. Le service D-Bus écoutant sur le port 6667 (réseau cellulaire Sprint) accepte des connexions non authentifiées et permet l'exécution de commandes arbitraires sur la head unit. Le défaut permet à un attaquant distant de prendre le contrôle de l'infotainment, puis (via un firmware modifié injecté sur le microcontrôleur Renesas V850) d'envoyer des trames CAN au véhicule.",
attackChain: [
"Reconnaissance : scan IP sur le réseau cellulaire Sprint pour identifier les véhicules Uconnect (port 6667 ouvert)",
"Exploitation : connexion D-Bus non authentifiée + exécution de commandes sur la head unit Linux",
"Élévation : reflashing du microcontrôleur V850 avec un firmware modifié via SPI",
"Impact : injection de trames CAN arbitraires — contrôle des freins, direction, accélérateur, transmission",
],
cvssV2: {
score: 8.3,
severity: "HIGH",
vector: "AV:A/AC:L/Au:N/C:C/I:C/A:C",
metrics: [
{
key: "AV",
label: "Access Vector",
value: "A",
valueLabel: "Adjacent Network",
detail:
"Le réseau cellulaire Sprint est traité comme adjacent (pas un Internet ouvert) — l'attaquant doit posséder un terminal Sprint pour atteindre les véhicules.",
},
{
key: "AC",
label: "Access Complexity",
value: "L",
valueLabel: "Low",
detail:
"Aucune condition particulière requise au-delà de la présence sur Sprint. Scan trivialement automatisable.",
},
{
key: "Au",
label: "Authentication",
value: "N",
valueLabel: "None",
detail:
"Aucune authentification requise pour interagir avec le service D-Bus exposé.",
},
{
key: "C",
label: "Confidentiality Impact",
value: "C",
valueLabel: "Complete",
detail:
"Lecture totale du système Linux du Uconnect (logs, GPS historique, contenu USB, configuration).",
},
{
key: "I",
label: "Integrity Impact",
value: "C",
valueLabel: "Complete",
detail:
"Modification arbitraire du système + chaîne d'attaque permettant de modifier le firmware du V850 et d'injecter dans le CAN bus.",
},
{
key: "A",
label: "Availability Impact",
value: "C",
valueLabel: "Complete",
detail:
"Perte totale de disponibilité possible : extinction du moteur, désactivation transmission, blocage du véhicule.",
},
],
},
cvssV3: {
score: 9.6,
severity: "CRITICAL",
vector: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
note: "Réinterprétation pédagogique en CVSS v3.1 — le CVE original n'a été scoré qu'en CVSS v2 par NVD à l'époque (v3 publié fin 2015).",
metrics: [
{
key: "AV",
label: "Attack Vector",
value: "A",
valueLabel: "Adjacent",
detail:
"Conservé en Adjacent : le réseau Sprint constitue un domaine logique partagé (pas Internet ouvert au sens strict).",
},
{
key: "AC",
label: "Attack Complexity",
value: "L",
valueLabel: "Low",
detail: "Pas de condition spéciale, attaque répétable.",
},
{
key: "PR",
label: "Privileges Required",
value: "N",
valueLabel: "None",
detail: "Aucun privilège requis sur le système cible.",
},
{
key: "UI",
label: "User Interaction",
value: "N",
valueLabel: "None",
detail:
"Aucune action utilisateur nécessaire — le conducteur n'a rien à cliquer ou installer.",
},
{
key: "S",
label: "Scope",
value: "C",
valueLabel: "Changed",
detail:
"La compromission de l'infotainment franchit une frontière de sécurité majeure (vers le CAN bus / les actuateurs physiques). Scope Changed est ici essentiel.",
},
{
key: "C",
label: "Confidentiality",
value: "H",
valueLabel: "High",
detail: "Exposition totale des données système et utilisateur.",
},
{
key: "I",
label: "Integrity",
value: "H",
valueLabel: "High",
detail:
"Modification arbitraire du firmware + injection CAN, compromettant l'intégrité fonctionnelle du véhicule.",
},
{
key: "A",
label: "Availability",
value: "H",
valueLabel: "High",
detail:
"Mise hors service du véhicule possible (coupure moteur, transmission).",
},
],
},
remediation: [
"Patch firmware Uconnect distribué par FCA le 16 juillet 2015 (mise à jour USB + OTA progressive).",
"Rappel formel de 1.4M véhicules le 23 juillet 2015 — premier rappel automobile motivé par une vulnérabilité cyber.",
"Sprint a fermé le port 6667 côté infrastructure et cloisonné les véhicules sur un sous-réseau dédié.",
"À long terme : adoption de SecOC (AUTOSAR), conformité ISO/SAE 21434 et UNECE R155 (obligatoires en homologation depuis juillet 2022).",
],
references: [
{ label: "NVD — CVE-2015-5611", url: "https://nvd.nist.gov/vuln/detail/CVE-2015-5611" },
{ label: "CISA ICSA-15-260-01", url: "https://www.cisa.gov/news-events/ics-advisories/icsa-15-260-01" },
{ label: "CERT VU#819439", url: "https://www.kb.cert.org/vuls/id/819439" },
{ label: "Miller & Valasek — Remote Exploitation of an Unaltered Passenger Vehicle (91p.)", url: "http://illmatics.com/Remote%20Car%20Hacking.pdf" },
],
},
],
},
};
// ─────────────────────────────────────────────────────────────
// MATRICE DE CRITICITÉ
// ─────────────────────────────────────────────────────────────
const PROBABILITY_LEVELS = [
{ score: 1, label: "Très improbable", freq: "Une fois en 10 ans ou plus" },
{ score: 2, label: "Improbable", freq: "Une fois tous les 2 à 3 ans" },
{ score: 3, label: "Probable", freq: "Une fois par an" },
{ score: 4, label: "Très probable", freq: "Plusieurs fois par an" },
];
const IMPACT_LEVELS = [
{ score: 1, label: "Négligeable", conseq: "Aucune interruption, aucune donnée compromise" },
{ score: 2, label: "Mineur", conseq: "Gêne passagère, dégradation partielle du service" },
{ score: 3, label: "Majeur", conseq: "Interruption de service, perte de données partielle" },
{ score: 4, label: "Critique", conseq: "Danger physique, perte massive, sanction juridique, atteinte irréversible" },
];
function getCriticality(prob, impact) {
const score = prob * impact;
if (score <= 3) return { score, level: "FAIBLE", color: "#22c55e", bg: "rgba(34,197,94,0.15)" };
if (score <= 6) return { score, level: "MODÉRÉ", color: "#eab308", bg: "rgba(234,179,8,0.15)" };
if (score <= 9) return { score, level: "ÉLEVÉ", color: "#f97316", bg: "rgba(249,115,22,0.15)" };
return { score, level: "CRITIQUE", color: "#ef4444", bg: "rgba(239,68,68,0.15)" };
}
// ─────────────────────────────────────────────────────────────
// COMPONENTS
// ─────────────────────────────────────────────────────────────
function TabButton({ active, onClick, children, code }) {
return (
<button
onClick={onClick}
className={`group relative flex flex-col items-start gap-1 px-5 py-3 border transition-all ${
active
? "border-amber-400 bg-amber-400/10"
: "border-zinc-800 hover:border-zinc-600 bg-transparent"
}`}
>
<span
className={`text-[10px] tracking-[0.2em] font-mono ${
active ? "text-amber-400" : "text-zinc-500"
}`}
>
{code}
</span>
<span
className={`text-sm font-semibold tracking-wide ${
active ? "text-zinc-100" : "text-zinc-400 group-hover:text-zinc-200"
}`}
>
{children}
</span>
</button>
);
}
function StatCard({ label, value, icon: Icon }) {
return (
<div className="border border-zinc-800 bg-zinc-950/50 p-4 relative overflow-hidden">
<div className="flex items-start justify-between mb-2">
<Icon size={16} className="text-zinc-500" />
<span className="text-[9px] tracking-[0.2em] text-zinc-600 font-mono">METRIC</span>
</div>
<div className="text-2xl font-bold text-zinc-100 font-mono">{value}</div>
<div className="text-[11px] text-zinc-500 uppercase tracking-wider mt-1">{label}</div>
</div>
);
}
function Section({ icon: Icon, label, code, children, accent = "#facc15" }) {
return (
<section className="mb-10">
<div className="flex items-center gap-3 mb-5 pb-2 border-b border-zinc-800">
<div
className="w-7 h-7 flex items-center justify-center border"
style={{ borderColor: accent, color: accent }}
>
<Icon size={14} />
</div>
<div>
<div className="text-[10px] tracking-[0.25em] font-mono" style={{ color: accent }}>
{code}
</div>
<h2 className="text-lg font-semibold text-zinc-100 tracking-wide">{label}</h2>
</div>
</div>
{children}
</section>
);
}
function TimelineStep({ phase, title, text, accent }) {
return (
<div className="flex gap-4 relative pb-6">
<div className="flex flex-col items-center">
<div
className="w-10 h-10 flex items-center justify-center border-2 font-mono font-bold text-sm shrink-0"
style={{ borderColor: accent, color: accent }}
>
{phase}
</div>
<div className="flex-1 w-px bg-zinc-800 my-2" />
</div>
<div className="flex-1 pt-1">
<h3 className="text-zinc-100 font-semibold mb-1">{title}</h3>
<p className="text-zinc-400 text-sm leading-relaxed">{text}</p>
</div>
</div>
);
}
function WhyCard({ title, text, idx }) {
return (
<div className="border border-zinc-800 bg-zinc-950/30 p-4 hover:border-zinc-700 transition-colors">
<div className="flex items-baseline gap-3 mb-2">
<span className="text-[10px] font-mono text-zinc-600">0{idx + 1}</span>
<h3 className="text-zinc-100 font-semibold text-sm">{title}</h3>
</div>
<p className="text-zinc-400 text-sm leading-relaxed pl-7">{text}</p>
</div>
);
}
function SolutionCard({ title, text }) {
return (
<div className="border-l-2 border-emerald-500/40 pl-4 py-2">
<h3 className="text-emerald-400 font-semibold text-sm mb-1 flex items-center gap-2">
<Wrench size={12} />
{title}
</h3>
<p className="text-zinc-400 text-sm leading-relaxed">{text}</p>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// MATRICE INTERACTIVE
// ─────────────────────────────────────────────────────────────
function CriticalityMatrix({ highlightedCase = null, risks = null }) {
const [selectedProb, setSelectedProb] = useState(null);
const [selectedImpact, setSelectedImpact] = useState(null);
const probs = [1, 2, 3, 4];
const impacts = [1, 2, 3, 4];
// Position du cas (mode legacy)
const jeepPos = { prob: CASES.jeep.probability, impact: CASES.jeep.impact };
// Helper : obtenir les risques pour une cellule donnée
const risksAt = (p, i) => {
if (!risks) return [];
return risks.filter((r) => r.probability === p && r.impact === i);
};
const activeCrit =
selectedProb && selectedImpact ? getCriticality(selectedProb, selectedImpact) : null;
return (
<div className="space-y-6">
{/* Matrice pleine largeur */}
<div className="border border-zinc-800 bg-zinc-950/50 p-5">
<div className="flex flex-wrap items-end justify-between gap-3 mb-4">
<div>
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400">
CRIT-MATRIX / P × I
</div>
<div className="text-sm text-zinc-400">
Probabilité (axe X) × Impact (axe Y) cliquez une case pour l'analyser
</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-[11px]">
<span className="text-zinc-500 font-mono">LEGEND:</span>
{[
{ lvl: "FAIBLE", c: "#22c55e", range: "13" },
{ lvl: "MODÉRÉ", c: "#eab308", range: "46" },
{ lvl: "ÉLEVÉ", c: "#f97316", range: "89" },
{ lvl: "CRITIQUE", c: "#ef4444", range: "1216" },
].map((l) => (
<div key={l.lvl} className="flex items-center gap-1.5">
<div
className="w-3 h-3"
style={{ background: l.c + "30", border: `1px solid ${l.c}` }}
/>
<span className="text-zinc-400 font-mono">
{l.lvl} <span className="text-zinc-600">({l.range})</span>
</span>
</div>
))}
</div>
</div>
<div className="grid grid-cols-[140px_repeat(4,1fr)] gap-1">
{/* Header row */}
<div className="flex items-end justify-end pr-2 pb-1">
<span className="text-[9px] tracking-[0.2em] font-mono text-zinc-600">
IMPACT ↓ / PROB →
</span>
</div>
{probs.map((p) => (
<div
key={p}
className="text-center py-2 text-[10px] tracking-wider text-zinc-500 font-mono border-b border-zinc-800"
>
<div className="text-zinc-300 font-bold text-sm">P={p}</div>
<div className="text-[10px] text-zinc-500 mt-0.5">
{PROBABILITY_LEVELS[p - 1].label}
</div>
</div>
))}
{/* Rows */}
{[...impacts].reverse().map((i) => (
<React.Fragment key={i}>
<div className="flex items-center justify-end pr-3 text-right border-r border-zinc-800">
<div>
<div className="text-zinc-300 font-bold text-sm font-mono">I={i}</div>
<div className="text-[10px] text-zinc-500">
{IMPACT_LEVELS[i - 1].label}
</div>
</div>
</div>
{probs.map((p) => {
const crit = getCriticality(p, i);
const isSelected = selectedProb === p && selectedImpact === i;
const cellRisks = risksAt(p, i);
const isJeep =
!risks &&
highlightedCase === "jeep" &&
jeepPos.prob === p &&
jeepPos.impact === i;
const isHighlighted = isJeep;
return (
<button
key={`${p}-${i}`}
onClick={() => {
setSelectedProb(p);
setSelectedImpact(i);
}}
className={`h-20 md:h-24 relative flex items-center justify-center transition-all hover:brightness-125 ${
isSelected ? "ring-2 ring-zinc-100 ring-inset" : ""
} ${isHighlighted ? "ring-2 ring-amber-400 ring-inset animate-pulse" : ""} ${
cellRisks.length > 0 ? "ring-2 ring-amber-400/60 ring-inset" : ""
}`}
style={{
background: crit.bg,
borderColor: crit.color,
border: `1px solid ${crit.color}`,
}}
>
<span
className={`font-mono font-bold ${
cellRisks.length > 0
? "text-sm absolute top-1 left-1.5 opacity-60"
: "text-2xl md:text-3xl"
}`}
style={{ color: crit.color }}
>
{crit.score}
</span>
{cellRisks.length > 0 && (
<div className="flex flex-wrap gap-1 justify-center items-center px-1">
{cellRisks.map((r) => (
<span
key={r.id}
className="bg-zinc-950 border border-amber-400 text-amber-400 text-[11px] md:text-xs font-mono font-bold px-1.5 py-0.5 tracking-wider"
>
{r.id}
</span>
))}
</div>
)}
{isJeep && (
<div className="absolute top-1 right-1 bg-zinc-950 border border-amber-400 text-amber-400 text-[9px] font-mono px-1.5 py-0.5 tracking-wider">
JEEP
</div>
)}
</button>
);
})}
</React.Fragment>
))}
</div>
</div>
{/* Bandeau sélection horizontal */}
<div
className="border bg-zinc-950/50 p-4 transition-colors"
style={{
borderColor: activeCrit ? activeCrit.color + "80" : "rgb(39 39 42)",
}}
>
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-3">
SELECTION ACTIVE
</div>
{activeCrit ? (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<div className="text-[10px] tracking-wider text-zinc-500 font-mono mb-1">
PROBABILITÉ ({selectedProb}/4)
</div>
<div className="text-zinc-100 font-semibold">
{PROBABILITY_LEVELS[selectedProb - 1].label}
</div>
<div className="text-zinc-500 text-xs mt-0.5">
{PROBABILITY_LEVELS[selectedProb - 1].freq}
</div>
</div>
<div>
<div className="text-[10px] tracking-wider text-zinc-500 font-mono mb-1">
IMPACT ({selectedImpact}/4)
</div>
<div className="text-zinc-100 font-semibold">
{IMPACT_LEVELS[selectedImpact - 1].label}
</div>
<div className="text-zinc-500 text-xs mt-0.5">
{IMPACT_LEVELS[selectedImpact - 1].conseq}
</div>
</div>
<div className="md:border-l md:border-zinc-800 md:pl-4">
<div className="text-[10px] tracking-wider text-zinc-500 font-mono mb-1">
CRITICITÉ = P × I
</div>
<div
className="text-3xl font-bold font-mono leading-none"
style={{ color: activeCrit.color }}
>
{selectedProb} × {selectedImpact} = {activeCrit.score}
</div>
</div>
<div className="flex md:justify-end md:items-center">
<div
className="inline-block px-4 py-2 text-sm font-mono tracking-wider border font-semibold"
style={{
color: activeCrit.color,
borderColor: activeCrit.color,
background: activeCrit.bg,
}}
>
NIVEAU {activeCrit.level}
</div>
</div>
</div>
) : (
<div className="text-zinc-500 text-sm py-2">
Cliquez une cellule de la matrice ci-dessus pour analyser ses paramètres.
</div>
)}
</div>
{/* Tables récapitulatives */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="border border-zinc-800 bg-zinc-950/30 p-4">
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-3">
ÉCHELLE / PROBABILITÉ
</div>
<div className="space-y-2">
{PROBABILITY_LEVELS.map((p) => (
<div
key={p.score}
className="grid grid-cols-[40px_1fr_2fr] gap-3 py-2 border-b border-zinc-900 last:border-0"
>
<div className="font-mono text-zinc-500 text-sm">P={p.score}</div>
<div className="text-zinc-200 text-sm font-semibold">{p.label}</div>
<div className="text-zinc-500 text-xs">{p.freq}</div>
</div>
))}
</div>
</div>
<div className="border border-zinc-800 bg-zinc-950/30 p-4">
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-3">
ÉCHELLE / IMPACT
</div>
<div className="space-y-2">
{IMPACT_LEVELS.map((i) => (
<div
key={i.score}
className="grid grid-cols-[40px_1fr_2fr] gap-3 py-2 border-b border-zinc-900 last:border-0"
>
<div className="font-mono text-zinc-500 text-sm">I={i.score}</div>
<div className="text-zinc-200 text-sm font-semibold">{i.label}</div>
<div className="text-zinc-500 text-xs">{i.conseq}</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// ACTIVITÉ 1 — ANALYSE DES 5 RISQUES
// ─────────────────────────────────────────────────────────────
const ACTIVITY_STEPS = [
{
n: 1,
title: "Identifier 5 risques",
text: "Risques précis et contextualisés — pas des catégories génériques.",
},
{
n: 2,
title: "Attribuer P et I",
text: "Probabilité (1 à 4) et Impact (1 à 4), calculer la criticité.",
},
{
n: 3,
title: "Positionner dans la matrice",
text: "Identifier le risque le plus et le moins critique.",
},
{
n: 4,
title: "Documenter les hypothèses",
text: "Pour chaque score de probabilité attribué.",
},
];
function ActivityStepCard({ n, title, text }) {
return (
<div className="relative border border-emerald-500/40 bg-emerald-500/5 p-3 pt-5">
<div className="absolute -top-3 left-3 w-6 h-6 rounded-full bg-emerald-500 text-zinc-950 flex items-center justify-center font-bold text-xs font-mono">
{n}
</div>
<h4 className="text-zinc-100 font-semibold text-sm mb-1">{title}</h4>
<p className="text-zinc-400 text-xs leading-relaxed">{text}</p>
</div>
);
}
function RiskCard({ risk, isHighest, isLowest }) {
const crit = getCriticality(risk.probability, risk.impact);
const [open, setOpen] = useState(true);
let tag = null;
if (isHighest) tag = { label: "PLUS CRITIQUE", color: "#ef4444" };
else if (isLowest) tag = { label: "MOINS CRITIQUE", color: "#22c55e" };
return (
<div
className="border bg-zinc-950/40 transition-all"
style={{
borderColor: tag ? tag.color + "80" : "rgb(39 39 42)",
}}
>
<button
onClick={() => setOpen(!open)}
className="w-full text-left p-4 flex items-start gap-4 hover:bg-zinc-900/40 transition-colors"
>
{/* ID + score */}
<div className="flex flex-col items-center shrink-0 gap-1">
<div
className="w-12 h-12 flex items-center justify-center border-2 font-mono font-bold text-lg"
style={{ borderColor: crit.color, color: crit.color, background: crit.bg }}
>
{risk.id}
</div>
<div
className="text-xs font-mono font-bold"
style={{ color: crit.color }}
>
{crit.score}
</div>
</div>
{/* Titre + description */}
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h3 className="text-zinc-100 font-semibold text-sm">{risk.title}</h3>
{tag && (
<span
className="text-[9px] font-mono font-bold tracking-wider px-1.5 py-0.5 border"
style={{ color: tag.color, borderColor: tag.color }}
>
{tag.label}
</span>
)}
</div>
<p className="text-zinc-400 text-xs leading-relaxed">{risk.description}</p>
</div>
{/* Scores P / I / Criticité */}
<div className="hidden md:flex items-center gap-4 shrink-0 text-center">
<div>
<div className="text-[9px] font-mono text-zinc-500 tracking-wider">P</div>
<div className="text-zinc-200 font-mono font-bold">{risk.probability}</div>
</div>
<div className="text-zinc-700">×</div>
<div>
<div className="text-[9px] font-mono text-zinc-500 tracking-wider">I</div>
<div className="text-zinc-200 font-mono font-bold">{risk.impact}</div>
</div>
<div className="text-zinc-700">=</div>
<div
className="px-2 py-1 border text-xs font-mono font-bold tracking-wider"
style={{
color: crit.color,
borderColor: crit.color,
background: crit.bg,
}}
>
{crit.level}
</div>
</div>
<ChevronRight
size={16}
className={`text-zinc-500 shrink-0 mt-3 transition-transform ${
open ? "rotate-90" : ""
}`}
/>
</button>
{open && (
<div className="border-t border-zinc-800 px-4 py-4 space-y-3 text-xs">
{/* Version mobile des scores */}
<div className="md:hidden flex items-center gap-4 pb-3 border-b border-zinc-800">
<div>
<span className="text-[9px] font-mono text-zinc-500 tracking-wider">P </span>
<span className="text-zinc-200 font-mono font-bold">{risk.probability}</span>
</div>
<div>
<span className="text-[9px] font-mono text-zinc-500 tracking-wider">I </span>
<span className="text-zinc-200 font-mono font-bold">{risk.impact}</span>
</div>
<div
className="px-2 py-0.5 border text-[10px] font-mono font-bold tracking-wider"
style={{
color: crit.color,
borderColor: crit.color,
background: crit.bg,
}}
>
{crit.level}
</div>
</div>
<div className="flex items-center gap-2 mb-1">
<span className="text-[9px] font-mono font-bold tracking-[0.2em] text-emerald-400 bg-emerald-500/10 border border-emerald-500/30 px-1.5 py-0.5">
ÉTAPE 04
</span>
<span className="text-[10px] font-mono tracking-wider text-zinc-500">
DOCUMENTATION DES HYPOTHÈSES
</span>
</div>
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
HYPOTHÈSE / PROBABILITÉ = {risk.probability}
<span className="text-zinc-500">
{" "}
({PROBABILITY_LEVELS[risk.probability - 1].label})
</span>
</div>
<p className="text-zinc-300 leading-relaxed pl-3 border-l border-amber-400/40">
{risk.probHypothesis}
</p>
</div>
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
HYPOTHÈSE / IMPACT = {risk.impact}
<span className="text-zinc-500">
{" "}
({IMPACT_LEVELS[risk.impact - 1].label})
</span>
</div>
<p className="text-zinc-300 leading-relaxed pl-3 border-l border-amber-400/40">
{risk.impactHypothesis}
</p>
</div>
{risk.solution && (
<div className="pt-3 mt-3 border-t border-zinc-800">
<div className="flex items-center gap-2 mb-1">
<span className="text-[9px] font-mono font-bold tracking-[0.2em] text-emerald-400 bg-emerald-500/10 border border-emerald-500/30 px-1.5 py-0.5">
MITIGATION
</span>
<span className="text-[10px] font-mono tracking-wider text-zinc-500">
SOLUTION SPÉCIFIQUE À CE RISQUE
</span>
</div>
<div className="flex gap-2 items-start">
<Wrench size={12} className="text-emerald-400 mt-1 shrink-0" />
<p className="text-zinc-300 leading-relaxed pl-3 border-l border-emerald-500/40 flex-1">
{risk.solution}
</p>
</div>
</div>
)}
</div>
)}
</div>
);
}
function RiskAnalysisActivity({ data }) {
// Calcul plus/moins critique
const scored = data.risks.map((r) => ({
...r,
score: r.probability * r.impact,
}));
const maxScore = Math.max(...scored.map((r) => r.score));
const minScore = Math.min(...scored.map((r) => r.score));
const highestIds = scored.filter((r) => r.score === maxScore).map((r) => r.id);
const lowestIds = scored.filter((r) => r.score === minScore).map((r) => r.id);
return (
<div>
{/* Briefing activité */}
<div className="border border-emerald-500/30 bg-emerald-500/5 p-5 mb-6">
<div className="flex items-center gap-2 mb-1">
<div className="text-[10px] tracking-[0.25em] font-mono text-emerald-400">
ACTIVITÉ 01 / METHODOLOGY
</div>
</div>
<h2 className="text-xl font-bold text-zinc-100 mb-1">Matrice de criticité</h2>
<p className="text-zinc-400 text-sm mb-5">
À partir du cas fil rouge assigné ({data.title}, {data.year}), réalisez les étapes
suivantes :
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
{ACTIVITY_STEPS.map((s) => (
<ActivityStepCard key={s.n} {...s} />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div className="border border-red-500/40 bg-red-500/10 p-3">
<div className="flex items-center gap-2 mb-1 text-red-400 font-semibold">
<span>✕</span> À éviter
</div>
<div className="text-zinc-400 italic">"Risque réseau"</div>
<div className="text-zinc-600 mt-1 text-[11px]">
Trop générique, non actionnable.
</div>
</div>
<div className="border border-emerald-500/40 bg-emerald-500/10 p-3">
<div className="flex items-center gap-2 mb-1 text-emerald-400 font-semibold">
<span>✓</span> Attendu
</div>
<div className="text-zinc-300">
"Interception du trafic MQTT entre capteurs et broker en raison de l'absence de
chiffrement TLS"
</div>
<div className="text-zinc-600 mt-1 text-[11px]">
Précis, contextualisé, désigne une vulnérabilité spécifique.
</div>
</div>
</div>
</div>
{/* Liste des risques */}
<div className="mb-6">
<div className="flex flex-wrap items-end justify-between gap-2 mb-3">
<div>
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400">
INVENTAIRE / 5 RISQUES CONTEXTUALISÉS
</div>
<div className="text-xs text-zinc-500">
Chaque risque déplie sa{" "}
<span className="text-emerald-400 font-semibold">documentation d'hypothèses</span>{" "}
(étape 4) et sa{" "}
<span className="text-emerald-400 font-semibold">mitigation spécifique</span>,
directement sous son scoring P × I.
</div>
</div>
<div className="flex flex-wrap items-center gap-2 text-[10px] font-mono tracking-wider text-zinc-500">
<span className="px-1.5 py-0.5 border border-emerald-500/40 text-emerald-400">
ÉTAPE 04
</span>
<span>hypothèses</span>
<span className="text-zinc-700">+</span>
<span className="px-1.5 py-0.5 border border-emerald-500/40 text-emerald-400">
MITIGATION
</span>
<span>solution</span>
</div>
</div>
<div className="space-y-2">
{data.risks.map((r) => (
<RiskCard
key={r.id}
risk={r}
isHighest={highestIds.includes(r.id)}
isLowest={lowestIds.includes(r.id) && !highestIds.includes(r.id)}
/>
))}
</div>
<div className="text-[11px] text-zinc-500 italic mt-3 px-1">
Cliquez sur l'en-tête d'un risque pour replier/déplier sa carte.
</div>
</div>
{/* Synthèse plus/moins critique */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="border border-red-500/50 bg-red-500/10 p-4">
<div className="text-[10px] tracking-[0.25em] font-mono text-red-400 mb-2">
RISQUE LE PLUS CRITIQUE / SCORE {maxScore}
</div>
{scored
.filter((r) => r.score === maxScore)
.map((r) => (
<div key={r.id} className="mb-1">
<span className="font-mono font-bold text-red-400">{r.id}</span>{" "}
<span className="text-zinc-200 text-sm">{r.title}</span>
</div>
))}
</div>
<div className="border border-emerald-500/50 bg-emerald-500/10 p-4">
<div className="text-[10px] tracking-[0.25em] font-mono text-emerald-400 mb-2">
RISQUE LE MOINS CRITIQUE / SCORE {minScore}
</div>
{scored
.filter((r) => r.score === minScore && !highestIds.includes(r.id))
.map((r) => (
<div key={r.id} className="mb-1">
<span className="font-mono font-bold text-emerald-400">{r.id}</span>{" "}
<span className="text-zinc-200 text-sm">{r.title}</span>
</div>
))}
</div>
</div>
{/* Matrice avec positionnement des risques */}
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-3">
POSITIONNEMENT / MATRICE P × I
</div>
<CriticalityMatrix risks={data.risks} />
</div>
);
}
// ─────────────────────────────────────────────────────────────
// CASE DETAIL VIEW
// ─────────────────────────────────────────────────────────────
function CaseHero({ data }) {
const crit = getCriticality(data.probability, data.impact);
return (
<div className="border border-zinc-800 bg-gradient-to-br from-zinc-950 to-zinc-900/50 p-6 mb-6 relative overflow-hidden">
<div
className="absolute top-0 right-0 w-64 h-64 opacity-10 blur-3xl"
style={{ background: data.accent }}
/>
<div className="relative flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<div>
<div className="text-[10px] tracking-[0.3em] font-mono text-zinc-500 mb-1">
CASE FILE · {data.codename}
</div>
<h1 className="text-3xl md:text-4xl font-bold text-zinc-100 tracking-tight">
{data.title}{" "}
<span className="text-zinc-500 font-mono text-2xl">/ {data.year}</span>
</h1>
<p className="text-zinc-400 mt-2">{data.subtitle}</p>
</div>
<div
className="flex flex-col items-center justify-center border px-5 py-3 shrink-0"
style={{ borderColor: crit.color, background: crit.bg }}
>
<div className="text-[9px] tracking-[0.2em] font-mono text-zinc-500">CRITICITÉ</div>
<div
className="text-4xl font-bold font-mono leading-none my-1"
style={{ color: crit.color }}
>
{crit.score}
</div>
<div className="text-[10px] font-mono tracking-wider" style={{ color: crit.color }}>
{crit.level}
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{data.stats.map((s) => (
<StatCard key={s.label} {...s} />
))}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// SUB-NAVIGATION (sous-onglets internes au cas)
// ─────────────────────────────────────────────────────────────
function SubTabs({ current, onChange, options, accent = "#facc15" }) {
return (
<nav className="flex flex-wrap gap-1 mb-8 border-b border-zinc-800">
{options.map((opt, idx) => {
const active = current === opt.id;
return (
<button
key={opt.id}
onClick={() => onChange(opt.id)}
className={`group relative px-5 py-3 flex items-center gap-3 transition-all -mb-px border-b-2 ${
active ? "" : "border-transparent hover:bg-zinc-900/40"
}`}
style={
active
? { borderColor: accent }
: undefined
}
>
<span
className={`text-[10px] tracking-[0.2em] font-mono ${
active ? "" : "text-zinc-600"
}`}
style={active ? { color: accent } : undefined}
>
{String(idx + 1).padStart(2, "0")}
</span>
<span
className={`flex items-center gap-2 text-sm font-semibold ${
active ? "text-zinc-100" : "text-zinc-400 group-hover:text-zinc-200"
}`}
>
{opt.icon && <opt.icon size={14} />}
{opt.label}
</span>
{opt.count && (
<span
className={`text-[10px] font-mono px-1.5 py-0.5 border ${
active
? "text-zinc-300 border-zinc-700"
: "text-zinc-600 border-zinc-800"
}`}
>
{opt.count}
</span>
)}
</button>
);
})}
</nav>
);
}
// ─────────────────────────────────────────────────────────────
// DOSSIER D'INCIDENT (page 1 par cas)
// ─────────────────────────────────────────────────────────────
function CaseDossier({ data }) {
return (
<div>
<div className="mb-6 flex items-start gap-3 text-sm text-zinc-400 bg-zinc-900/30 border border-zinc-800 p-4">
<div
className="w-1 self-stretch shrink-0"
style={{ background: data.accent }}
/>
<p>
Dossier technique de l'incident{" "}
<span className="text-zinc-200 font-semibold">{data.title}</span> ({data.year}).
Chronologie de l'attaque, causes racines, problèmes structurels et remédiations adoptées.
Pour l'analyse de risques quantitative (P × I sur 5 risques), basculez sur l'onglet
suivant.
</p>
</div>
<Section
icon={Activity}
label="Comment l'attaque s'est déroulée"
code="// CHRONOLOGIE TECHNIQUE"
accent={data.accent}
>
<div className="pl-2">
{data.timeline.map((t) => (
<TimelineStep key={t.phase} {...t} accent={data.accent} />
))}
</div>
</Section>
<Section
icon={Bug}
label="Pourquoi c'est arrivé — causes racines"
code="// ROOT-CAUSE ANALYSIS"
accent={data.accent}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{data.why.map((w, i) => (
<WhyCard key={i} idx={i} {...w} />
))}
</div>
</Section>
<Section
icon={AlertTriangle}
label="Problèmes identifiés"
code="// ISSUES"
accent={data.accent}
>
<ul className="space-y-2">
{data.problems.map((p, i) => (
<li
key={i}
className="flex items-start gap-3 text-zinc-300 text-sm border-l-2 border-red-500/40 pl-3 py-1"
>
<Skull size={14} className="text-red-500/60 mt-1 shrink-0" />
<span>{p}</span>
</li>
))}
</ul>
</Section>
<Section
icon={Shield}
label="Solutions et bonnes pratiques"
code="// MITIGATIONS"
accent="#22c55e"
>
<div className="space-y-3">
{data.solutions.map((s, i) => (
<SolutionCard key={i} {...s} />
))}
</div>
</Section>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// AMDEC — Analyse des Modes de Défaillance, Effets et Criticité
// ─────────────────────────────────────────────────────────────
const AMDEC_SCALES = {
severity: [
{ range: "1-2", label: "Mineure", desc: "Effet imperceptible ou très limité, pas de préjudice fonctionnel" },
{ range: "3-4", label: "Faible", desc: "Dégradation perceptible mais sans conséquence sécuritaire" },
{ range: "5-6", label: "Modérée", desc: "Interruption de service, mécontentement utilisateur" },
{ range: "7-8", label: "Importante", desc: "Compromission système, données exposées" },
{ range: "9-10", label: "Critique", desc: "Danger physique, atteinte irréversible, mise en jeu de vies" },
],
occurrence: [
{ range: "1-2", label: "Très rare", desc: "Probabilité de défaillance quasi nulle (< 1 sur 100 000)" },
{ range: "3-4", label: "Rare", desc: "Défaillance peu probable (1 sur 10 000)" },
{ range: "5-6", label: "Occasionnelle", desc: "Défaillance possible (1 sur 1 000)" },
{ range: "7-8", label: "Fréquente", desc: "Défaillance probable (1 sur 100)" },
{ range: "9-10", label: "Quasi-certaine", desc: "Défaillance certaine en conditions d'exploitation" },
],
detection: [
{ range: "1-2", label: "Très probable", desc: "Détectée immédiatement par les moyens existants" },
{ range: "3-4", label: "Probable", desc: "Détectée avant impact significatif" },
{ range: "5-6", label: "Possible", desc: "Détection possible mais incertaine" },
{ range: "7-8", label: "Peu probable", desc: "Détection difficile, souvent a posteriori" },
{ range: "9-10", label: "Très improbable", desc: "Quasiment indétectable avec les moyens en place" },
],
};
function getIPRLevel(ipr) {
if (ipr < 100) return { label: "FAIBLE", color: "#22c55e", bg: "rgba(34,197,94,0.12)" };
if (ipr < 200) return { label: "MODÉRÉ", color: "#eab308", bg: "rgba(234,179,8,0.12)" };
if (ipr < 400) return { label: "ÉLEVÉ", color: "#f97316", bg: "rgba(249,115,22,0.12)" };
return { label: "CRITIQUE", color: "#ef4444", bg: "rgba(239,68,68,0.12)" };
}
function ScoreBar({ value, max = 10, color }) {
const pct = (value / max) * 100;
return (
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-zinc-900 relative overflow-hidden">
<div
className="h-full transition-all"
style={{ width: `${pct}%`, background: color }}
/>
</div>
<span className="font-mono font-bold text-sm w-6 text-right" style={{ color }}>
{value}
</span>
</div>
);
}
function AMDECRow({ row, rank }) {
const ipr = row.severity * row.occurrence * row.detection;
const level = getIPRLevel(ipr);
const [open, setOpen] = useState(rank === 1); // ouvre le top 1 par défaut
return (
<div
className="border bg-zinc-950/40 transition-all"
style={{ borderColor: rank === 1 ? level.color + "80" : "rgb(39 39 42)" }}
>
<button
onClick={() => setOpen(!open)}
className="w-full text-left p-4 flex items-start gap-4 hover:bg-zinc-900/40 transition-colors"
>
<div className="flex flex-col items-center shrink-0 gap-1">
<div
className="w-14 h-14 flex items-center justify-center border-2 font-mono font-bold"
style={{ borderColor: level.color, color: level.color, background: level.bg }}
>
<div className="text-center leading-none">
<div className="text-[9px] tracking-wider opacity-80">IPR</div>
<div className="text-lg">{ipr}</div>
</div>
</div>
<div className="text-[9px] font-mono text-zinc-500">#{rank}</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="text-[10px] font-mono tracking-wider text-zinc-500">
{row.id}
</span>
<h3 className="text-zinc-100 font-semibold text-sm">{row.component}</h3>
{rank === 1 && (
<span
className="text-[9px] font-mono font-bold tracking-wider px-1.5 py-0.5 border"
style={{ color: level.color, borderColor: level.color }}
>
PRIORITÉ MAXIMALE
</span>
)}
</div>
<p className="text-zinc-400 text-xs leading-relaxed">{row.failureMode}</p>
</div>
<div className="hidden md:flex flex-col gap-1 shrink-0 w-40">
<div className="grid grid-cols-[18px_1fr] gap-1.5 items-center">
<span className="text-[9px] font-mono text-zinc-500">G</span>
<ScoreBar value={row.severity} color="#ef4444" />
</div>
<div className="grid grid-cols-[18px_1fr] gap-1.5 items-center">
<span className="text-[9px] font-mono text-zinc-500">O</span>
<ScoreBar value={row.occurrence} color="#f97316" />
</div>
<div className="grid grid-cols-[18px_1fr] gap-1.5 items-center">
<span className="text-[9px] font-mono text-zinc-500">D</span>
<ScoreBar value={row.detection} color="#eab308" />
</div>
</div>
<ChevronRight
size={16}
className={`text-zinc-500 shrink-0 mt-3 transition-transform ${
open ? "rotate-90" : ""
}`}
/>
</button>
{open && (
<div className="border-t border-zinc-800 px-4 py-4 grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
FONCTION
</div>
<p className="text-zinc-300 leading-relaxed">{row.function}</p>
</div>
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
MODE DE DÉFAILLANCE
</div>
<p className="text-zinc-300 leading-relaxed">{row.failureMode}</p>
</div>
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
CAUSE
</div>
<p className="text-zinc-300 leading-relaxed">{row.cause}</p>
</div>
<div>
<div className="text-[10px] font-mono tracking-wider text-amber-400 mb-1">
EFFET
</div>
<p className="text-zinc-300 leading-relaxed">{row.effect}</p>
</div>
<div className="md:col-span-2 pt-3 border-t border-zinc-800">
<div className="grid grid-cols-3 gap-3 mb-3">
<div className="text-center border border-zinc-800 p-2">
<div className="text-[9px] font-mono tracking-wider text-zinc-500">GRAVITÉ</div>
<div className="font-mono font-bold text-red-500 text-xl">{row.severity}<span className="text-zinc-700 text-sm">/10</span></div>
</div>
<div className="text-center border border-zinc-800 p-2">
<div className="text-[9px] font-mono tracking-wider text-zinc-500">OCCURRENCE</div>
<div className="font-mono font-bold text-orange-500 text-xl">{row.occurrence}<span className="text-zinc-700 text-sm">/10</span></div>
</div>
<div className="text-center border border-zinc-800 p-2">
<div className="text-[9px] font-mono tracking-wider text-zinc-500">DÉTECTION</div>
<div className="font-mono font-bold text-yellow-500 text-xl">{row.detection}<span className="text-zinc-700 text-sm">/10</span></div>
</div>
</div>
<div
className="text-center font-mono py-2 px-3 border"
style={{ borderColor: level.color, background: level.bg, color: level.color }}
>
<span className="text-[10px] tracking-wider opacity-80">IPR = G × O × D = </span>
<span className="text-2xl font-bold">{ipr}</span>
<span className="text-[10px] tracking-wider opacity-80"> · NIVEAU {level.label}</span>
</div>
</div>
<div className="md:col-span-2">
<div className="flex items-center gap-2 mb-1">
<span className="text-[9px] font-mono font-bold tracking-[0.2em] text-emerald-400 bg-emerald-500/10 border border-emerald-500/30 px-1.5 py-0.5">
ACTION CORRECTIVE
</span>
</div>
<div className="flex gap-2 items-start">
<Wrench size={12} className="text-emerald-400 mt-1 shrink-0" />
<p className="text-zinc-300 leading-relaxed pl-3 border-l border-emerald-500/40 flex-1">
{row.action}
</p>
</div>
</div>
</div>
)}
</div>
);
}
function AMDECView({ data }) {
// Tri par IPR descendant
const sortedRows = [...data.amdec]
.map((r) => ({ ...r, ipr: r.severity * r.occurrence * r.detection }))
.sort((a, b) => b.ipr - a.ipr);
return (
<div>
{/* Briefing pédagogique */}
<div className="border border-emerald-500/30 bg-emerald-500/5 p-5 mb-6">
<div className="text-[10px] tracking-[0.25em] font-mono text-emerald-400 mb-1">
MÉTHODOLOGIE / ACTIVITÉ 02
</div>
<h2 className="text-xl font-bold text-zinc-100 mb-2">AMDEC FMEA</h2>
<p className="text-zinc-400 text-sm mb-4">
<span className="text-zinc-200 font-semibold">A</span>nalyse des{" "}
<span className="text-zinc-200 font-semibold">M</span>odes de{" "}
<span className="text-zinc-200 font-semibold">D</span>éfaillance, de leurs{" "}
<span className="text-zinc-200 font-semibold">E</span>ffets et de leur{" "}
<span className="text-zinc-200 font-semibold">C</span>riticité. Méthode systématique
d'analyse de défaillances (équivalent français de la FMEA — Failure Mode and Effects
Analysis). Pour chaque composant du système, on identifie : la fonction, le mode de
défaillance possible, sa cause, son effet, puis on score sur trois axes :
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
<div className="border border-red-500/30 bg-red-500/5 p-3">
<div className="text-red-400 font-bold font-mono text-sm mb-1">G — GRAVITÉ</div>
<div className="text-zinc-400">
Sévérité de l'effet sur le système / l'utilisateur final. 1 = imperceptible, 10 =
critique (danger vital).
</div>
</div>
<div className="border border-orange-500/30 bg-orange-500/5 p-3">
<div className="text-orange-400 font-bold font-mono text-sm mb-1">O — OCCURRENCE</div>
<div className="text-zinc-400">
Fréquence d'apparition de la défaillance en conditions réelles. 1 = très rare, 10 =
quasi-certaine.
</div>
</div>
<div className="border border-yellow-500/30 bg-yellow-500/5 p-3">
<div className="text-yellow-400 font-bold font-mono text-sm mb-1">D DÉTECTION</div>
<div className="text-zinc-400">
Difficulté à détecter la défaillance avant impact. 1 = détection immédiate, 10 =
indétectable.
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-zinc-800 text-sm">
<div className="font-mono text-amber-400 text-center">
IPR = G × O × D
<span className="text-zinc-500 text-xs"> (Indice de Priorité du Risque, max 1000)</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-3 text-[11px]">
{[
{ range: "< 100", lvl: "FAIBLE", c: "#22c55e" },
{ range: "100-199", lvl: "MODÉRÉ", c: "#eab308" },
{ range: "200-399", lvl: "ÉLEVÉ", c: "#f97316" },
{ range: "≥ 400", lvl: "CRITIQUE", c: "#ef4444" },
].map((l) => (
<div
key={l.lvl}
className="text-center py-2 border font-mono"
style={{
color: l.c,
borderColor: l.c + "60",
background: l.c + "10",
}}
>
<div className="font-bold">{l.lvl}</div>
<div className="text-[10px] opacity-80">{l.range}</div>
</div>
))}
</div>
</div>
</div>
{/* Tableau AMDEC */}
<div className="mb-6">
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-3">
TABLEAU AMDEC / {sortedRows.length} MODES DE DÉFAILLANCE (TRIÉS PAR IPR DESC)
</div>
<div className="space-y-2">
{sortedRows.map((row, idx) => (
<AMDECRow key={row.id} row={row} rank={idx + 1} />
))}
</div>
<div className="text-[11px] text-zinc-500 italic mt-3 px-1">
Cliquez sur une ligne pour déplier ses détails (fonction, cause, effet, action corrective).
</div>
</div>
{/* Synthèse */}
<div className="border border-red-500/40 bg-red-500/10 p-4 mb-4">
<div className="text-[10px] tracking-[0.25em] font-mono text-red-400 mb-2">
PRIORITÉ MAXIMALE / IPR = {sortedRows[0].ipr}
</div>
<div className="text-zinc-200 font-semibold mb-1">
{sortedRows[0].id} · {sortedRows[0].component}
</div>
<p className="text-zinc-400 text-xs">{sortedRows[0].failureMode}</p>
<p className="text-emerald-300 text-xs mt-2 italic"> {sortedRows[0].action}</p>
</div>
{/* Échelles */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{[
{ key: "severity", title: "ÉCHELLE / GRAVITÉ", color: "text-red-400" },
{ key: "occurrence", title: "ÉCHELLE / OCCURRENCE", color: "text-orange-400" },
{ key: "detection", title: "ÉCHELLE / DÉTECTION", color: "text-yellow-400" },
].map((s) => (
<div key={s.key} className="border border-zinc-800 bg-zinc-950/30 p-3 text-xs">
<div className={`text-[10px] tracking-[0.25em] font-mono ${s.color} mb-2`}>
{s.title}
</div>
<div className="space-y-1.5">
{AMDEC_SCALES[s.key].map((l) => (
<div key={l.range} className="grid grid-cols-[40px_1fr] gap-2">
<span className="font-mono text-zinc-500">{l.range}</span>
<div>
<div className="text-zinc-200 font-semibold">{l.label}</div>
<div className="text-zinc-500 text-[11px] leading-tight">{l.desc}</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// CVE / CVSS — Common Vulnerabilities and Exposures
// ─────────────────────────────────────────────────────────────
function getCVSSLevel(score) {
if (score === 0) return { label: "NONE", color: "#71717a", bg: "rgba(113,113,122,0.12)" };
if (score < 4.0) return { label: "LOW", color: "#22c55e", bg: "rgba(34,197,94,0.12)" };
if (score < 7.0) return { label: "MEDIUM", color: "#eab308", bg: "rgba(234,179,8,0.12)" };
if (score < 9.0) return { label: "HIGH", color: "#f97316", bg: "rgba(249,115,22,0.12)" };
return { label: "CRITICAL", color: "#ef4444", bg: "rgba(239,68,68,0.12)" };
}
function CVSSScoreCircle({ score, version }) {
const level = getCVSSLevel(score);
return (
<div className="flex flex-col items-center">
<div
className="w-28 h-28 rounded-full flex items-center justify-center border-4 relative"
style={{ borderColor: level.color, background: level.bg }}
>
<div className="text-center">
<div className="text-[10px] font-mono tracking-wider text-zinc-500">CVSS {version}</div>
<div className="text-3xl font-bold font-mono" style={{ color: level.color }}>
{score.toFixed(1)}
</div>
</div>
</div>
<div
className="mt-2 px-3 py-1 font-mono text-xs font-bold tracking-wider border"
style={{ color: level.color, borderColor: level.color, background: level.bg }}
>
{level.label}
</div>
</div>
);
}
function CVSSMetric({ metric }) {
return (
<div className="border border-zinc-800 bg-zinc-950/40 p-3">
<div className="flex items-baseline gap-2 mb-1">
<span className="text-xl font-mono font-bold text-amber-400 leading-none">
{metric.key}
</span>
<span className="text-[10px] font-mono tracking-wider text-zinc-500">
{metric.label.toUpperCase()}
</span>
</div>
<div className="flex items-baseline gap-2 mb-2">
<span className="text-sm font-mono font-bold text-zinc-100">{metric.value}</span>
<span className="text-xs text-zinc-400">· {metric.valueLabel}</span>
</div>
<p className="text-[11px] text-zinc-400 leading-relaxed">{metric.detail}</p>
</div>
);
}
function CVSSPanel({ cvss, version, isOfficial }) {
return (
<div className="border border-zinc-800 bg-zinc-950/30 p-5">
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] tracking-[0.25em] font-mono text-amber-400">
CVSS {version}
</span>
{isOfficial ? (
<span className="text-[9px] font-mono font-bold tracking-wider px-1.5 py-0.5 border border-emerald-500/50 text-emerald-400 bg-emerald-500/10">
OFFICIEL · NVD
</span>
) : (
<span className="text-[9px] font-mono font-bold tracking-wider px-1.5 py-0.5 border border-amber-500/50 text-amber-400 bg-amber-500/10">
RÉINTERPRÉTATION PÉDAGOGIQUE
</span>
)}
</div>
<div className="font-mono text-xs text-zinc-300 break-all">{cvss.vector}</div>
{cvss.note && (
<p className="text-[11px] text-zinc-500 italic mt-2 max-w-xl">{cvss.note}</p>
)}
</div>
<CVSSScoreCircle score={cvss.score} version={version} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{cvss.metrics.map((m) => (
<CVSSMetric key={m.key} metric={m} />
))}
</div>
</div>
);
}
function CVEView({ data }) {
const cve = data.cves[0];
return (
<div>
{/* Briefing pédagogique */}
<div className="border border-emerald-500/30 bg-emerald-500/5 p-5 mb-6">
<div className="text-[10px] tracking-[0.25em] font-mono text-emerald-400 mb-1">
STANDARDS / ACTIVITÉ 03
</div>
<h2 className="text-xl font-bold text-zinc-100 mb-2">CVE & CVSS</h2>
<p className="text-zinc-400 text-sm">
<span className="text-zinc-200 font-semibold">CVE</span> (Common Vulnerabilities and
Exposures) attribue un identifiant unique à chaque vulnérabilité publiée, géré par MITRE.{" "}
<span className="text-zinc-200 font-semibold">CVSS</span> (Common Vulnerability Scoring
System) la score sur une échelle de 0 à 10 selon des métriques standardisées. La version
officielle au moment de la divulgation Jeep (2015) était CVSS v2 ; CVSS v3.1 est la version
en vigueur depuis 2019.
</p>
</div>
{/* Header CVE */}
<div className="border border-zinc-800 bg-gradient-to-br from-zinc-950 to-zinc-900/50 p-5 mb-6">
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
<div>
<div className="text-[10px] tracking-[0.3em] font-mono text-amber-400 mb-1">
VULNÉRABILITÉ RÉFÉRENCÉE
</div>
<h2 className="text-2xl md:text-3xl font-bold text-zinc-100 font-mono tracking-tight">
{cve.id}
</h2>
<p className="text-zinc-300 mt-1">{cve.title}</p>
</div>
<div className="flex flex-col items-end gap-1 text-[10px] font-mono text-zinc-500">
<span>Publié : {cve.publishedDate}</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div className="border border-zinc-800 bg-zinc-950/40 p-3">
<div className="text-[10px] tracking-wider font-mono text-zinc-500 mb-1">
ADVISORIES
</div>
<div className="space-y-1">
{cve.advisories.map((a) => (
<div key={a.id} className="flex items-baseline gap-2">
<span className="font-mono text-amber-400">{a.id}</span>
<span className="text-zinc-500">·</span>
<span className="text-zinc-400">{a.source}</span>
</div>
))}
</div>
</div>
<div className="border border-zinc-800 bg-zinc-950/40 p-3">
<div className="text-[10px] tracking-wider font-mono text-zinc-500 mb-1">
PRODUITS AFFECTÉS
</div>
<p className="text-zinc-300 leading-relaxed">{cve.affected}</p>
</div>
</div>
</div>
{/* Description et chaîne d'attaque */}
<Section
icon={Bug}
label="Description de la vulnérabilité"
code="// DESCRIPTION"
accent="#facc15"
>
<p className="text-zinc-300 leading-relaxed text-sm mb-4">{cve.description}</p>
<div className="text-[10px] tracking-[0.25em] font-mono text-amber-400 mb-2">
CHAÎNE D'ATTAQUE
</div>
<div className="space-y-1">
{cve.attackChain.map((step, i) => (
<div
key={i}
className="flex gap-3 items-start border-l-2 border-amber-400/40 pl-3 py-1"
>
<span className="font-mono text-amber-400 font-bold text-sm shrink-0">
{String(i + 1).padStart(2, "0")}
</span>
<span className="text-zinc-300 text-sm">{step}</span>
</div>
))}
</div>
</Section>
{/* Scoring CVSS — v2 officiel + v3 pédagogique */}
<Section
icon={Target}
label="Scoring CVSS"
code="// CVSS v2 OFFICIEL + v3.1 INTERPRÉTATION"
accent="#facc15"
>
<p className="text-zinc-400 text-sm mb-4 max-w-3xl">
Le NVD a scoré ce CVE en CVSS v2 (version en vigueur en 2015). Le score CVSS v3.1
ci-dessous est une réinterprétation pédagogique appliquant les métriques modernes au même
défaut technique — utile pour comparer les deux générations de standards.
</p>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<CVSSPanel cvss={cve.cvssV2} version="v2.0" isOfficial={true} />
<CVSSPanel cvss={cve.cvssV3} version="v3.1" isOfficial={false} />
</div>
{/* Comparaison */}
<div className="mt-4 border-l-2 border-amber-400 pl-4 py-2 text-sm text-zinc-300">
<span className="text-amber-400 font-semibold">Différence v2 vs v3 : </span>
La métrique <span className="font-mono text-zinc-100">Scope</span> introduite en v3 est
déterminante ici. En v2, l'impact se mesure uniquement sur le composant vulnérable (la
head unit Uconnect). En v3, l'attaque traverse une frontière de sécurité majeure
(infotainment → CAN bus → actuateurs physiques), ce qui place le scope à{" "}
<span className="font-mono text-zinc-100">Changed (C)</span> et fait grimper le score de
8.3 (High) à 9.6 (Critical). C'est l'un des changements pédagogiques majeurs apportés par
CVSS v3 : <em>la propagation d'impact compte autant que l'impact local</em>.
</div>
</Section>
{/* Remediation */}
<Section
icon={Shield}
label="Remediation et leçons retenues"
code="// REMEDIATION"
accent="#22c55e"
>
<ul className="space-y-2">
{cve.remediation.map((r, i) => (
<li
key={i}
className="flex items-start gap-3 text-zinc-300 text-sm border-l-2 border-emerald-500/40 pl-3 py-1"
>
<Wrench size={14} className="text-emerald-400 mt-1 shrink-0" />
<span>{r}</span>
</li>
))}
</ul>
</Section>
{/* Références */}
<Section
icon={Network}
label="Références officielles"
code="// SOURCES"
accent="#facc15"
>
<div className="space-y-2">
{cve.references.map((ref) => (
<a
key={ref.url}
href={ref.url}
target="_blank"
rel="noreferrer"
className="block border border-zinc-800 bg-zinc-950/30 p-3 hover:border-amber-400/50 hover:bg-zinc-900/40 transition-colors group"
>
<div className="text-zinc-200 text-sm font-semibold group-hover:text-amber-400 transition-colors">
{ref.label}
</div>
<div className="text-[11px] font-mono text-zinc-500 mt-0.5 break-all">{ref.url}</div>
</a>
))}
</div>
</Section>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// CASE LAYOUT (Hero + sous-navigation + contenu)
// ─────────────────────────────────────────────────────────────
function CaseLayout({ data }) {
const [subTab, setSubTab] = useState("dossier");
const subOptions = [
{
id: "dossier",
label: "Dossier d'incident",
icon: Activity,
count: `${data.timeline.length} étapes`,
},
{
id: "analyse",
label: "Analyse de risques",
icon: Target,
count: `${data.risks.length} risques`,
},
{
id: "amdec",
label: "AMDEC",
icon: Bug,
count: `${data.amdec.length} modes`,
},
{
id: "cve",
label: "CVE / CVSS",
icon: AlertTriangle,
count: `${data.cves.length} CVE`,
},
];
return (
<div>
<CaseHero data={data} />
<SubTabs
current={subTab}
onChange={setSubTab}
options={subOptions}
accent={data.accent}
/>
<div key={subTab} className="animate-fadein">
{subTab === "dossier" && <CaseDossier data={data} />}
{subTab === "analyse" && <RiskAnalysisActivity data={data} />}
{subTab === "amdec" && <AMDECView data={data} />}
{subTab === "cve" && <CVEView data={data} />}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// MAIN
// ─────────────────────────────────────────────────────────────
export default function IoTRiskBriefing() {
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100 font-sans p-4 md:p-8">
<style>{`
@keyframes fadein {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadein {
animation: fadein 0.25s ease-out;
}
`}</style>
{/* Header */}
<header className="mb-8 pb-6 border-b border-zinc-800">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Shield size={16} className="text-amber-400" />
<span className="text-[10px] tracking-[0.3em] font-mono text-amber-400">
IoT RISK ANALYSIS / CASE STUDY · JEEP CHEROKEE 2015
</span>
</div>
<span className="text-[10px] tracking-[0.2em] font-mono text-zinc-600">
CLASSIFICATION: PEDAGOGIQUE
</span>
</div>
<h1 className="text-2xl md:text-3xl font-bold text-zinc-100 tracking-tight">
Méthodologies d'analyse des risques IoT
</h1>
<p className="text-zinc-400 text-sm mt-1 max-w-3xl">
Étude du <span className="text-zinc-200 font-semibold">Jeep Cherokee Hack (2015)</span> —
dossier d'incident, analyse de risques par{" "}
<span className="font-mono text-amber-400">matrice P × I</span>, AMDEC formelle et
référencement CVE / CVSS. Trois méthodologies complémentaires de priorisation appliquées
au même cas pour rendre l'analyse reproductible et justifiable.
</p>
</header>
{/* Contenu : le case layout est la page entière */}
<main className="max-w-6xl">
<CaseLayout data={CASES.jeep} />
</main>
{/* Footer */}
<footer className="mt-16 pt-6 border-t border-zinc-800 text-xs text-zinc-600 font-mono flex flex-wrap items-center justify-between gap-2">
<span>// IOT-RISK-BRIEFING / JEEP-CHEROKEE-2015 / v2.0 / 2026</span>
<span className="text-zinc-700">
P × I matrix · AMDEC (FMEA) · CVE-2015-5611 · ISO/SAE 21434 · UN R155
</span>
</footer>
</div>
);
}