diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECT_BRIEF.md b/PROJECT_BRIEF.md new file mode 100644 index 0000000..222cb08 --- /dev/null +++ b/PROJECT_BRIEF.md @@ -0,0 +1,1003 @@ +# Brief Projet — Site d'analyse de risques IoT (Jeep Cherokee 2015) + +> Document de référence complet à fournir à Claude Code pour reconstruire le site à partir de l'artefact `iot_risk_briefing.jsx`. + +--- + +## 1. Contexte projet + +### Objectif +Site web pédagogique présentant une étude de cas complète du **Jeep Cherokee Hack de 2015** dans le cadre d'un cours d'analyse de risques IoT. Le site doit présenter 5 méthodologies d'analyse appliquées au même cas : + +1. **Dossier d'incident** — Récit technique de l'attaque +2. **Analyse de risques** — Matrice criticité P × I (5 risques contextualisés) +3. **AMDEC / FMEA** — Analyse des modes de défaillance (G × O × D = IPR) +4. **CVE / CVSS** — Référencement standardisé de la vulnérabilité +5. **RTO / RPO / PCA-PRA** — Plan de continuité d'activité + +### Public cible +- Étudiants en cybersécurité / IoT +- Professeurs (pour évaluation pédagogique) +- Professionnels intéressés par les méthodologies de risque + +### Ton et style +- Esthétique « briefing tactique / dossier de sécurité » +- Theme **dark exclusif** (bg zinc-950, accents amber + emerald + criticité colorée) +- Tone factuel, précis, technique mais accessible +- Bilingue ready (priorité FR, structure permettant ajout EN ultérieur) + +--- + +## 2. Stack technique recommandée + +### Choix préféré +``` +- Framework : Next.js 15 (App Router) avec TypeScript strict +- Runtime : Bun (cohérent avec la stack du dev) +- Styling : Tailwind CSS v3+ (classes utility uniquement, pas de @apply) +- Icônes : lucide-react +- Polices : geist / inter (variable fonts via next/font) +- Déploiement : Vercel (zero-config) ou self-hosted +``` + +### Alternative légère +``` +- Framework : Vite + React 18 + TypeScript +- Tooling : Bun +- Styling : Tailwind CSS +- Routing : react-router-dom v6 +``` + +### Contraintes +- Pas de localStorage / sessionStorage côté UI (état React in-memory) +- Tout rendu côté client OK (pas besoin de SSR sauf pour SEO) +- Pas de backend nécessaire — toutes les données sont statiques +- Bundle final < 200KB gzipped souhaitable + +--- + +## 3. Architecture du site + +### Routes +``` +/ → Redirige vers /dossier +/dossier → Page 1 : Dossier d'incident +/risques → Page 2 : Analyse de risques (matrice P×I) +/amdec → Page 3 : AMDEC / FMEA +/cve → Page 4 : CVE-2015-5611 / CVSS +/continuite → Page 5 : RTO / RPO / PCA-PRA +``` + +### Layout commun +``` +┌─────────────────────────────────────────────────┐ +│ HEADER global │ +│ Logo bouclier + "IoT RISK ANALYSIS" │ +│ Titre H1 + sous-titre │ +├─────────────────────────────────────────────────┤ +│ HERO du cas (toujours visible) │ +│ Codename CHRYSLER-2015-001 │ +│ Titre + année + sous-titre │ +│ Score criticité 12/CRITIQUE en encart à droite│ +│ 4 stats : 1.4M véhicules, 2 chercheurs, │ +│ Sprint réseau, ∞ distance │ +├─────────────────────────────────────────────────┤ +│ NAVIGATION SECONDAIRE (onglets soulignés) │ +│ 01 Dossier · 02 Risques · 03 AMDEC · │ +│ 04 CVE/CVSS · 05 Continuité │ +├─────────────────────────────────────────────────┤ +│ CONTENU DE LA PAGE │ +│ (fade-in 250ms au changement de route) │ +├─────────────────────────────────────────────────┤ +│ FOOTER │ +│ Version + standards référencés │ +└─────────────────────────────────────────────────┘ +``` + +--- + +## 4. Design system + +### Palette +```ts +const colors = { + // Backgrounds + bg: "#09090b", // zinc-950 + surface: "#18181b", // zinc-900 + surfaceAlt: "#27272a", // zinc-800 + + // Texte + textPrimary: "#fafafa", // zinc-50 / zinc-100 + textSecondary: "#a1a1aa", // zinc-400 + textMuted: "#71717a", // zinc-500 + textFaint: "#52525b", // zinc-600 + + // Accents + amber: "#facc15", // amber-400 (accent principal / méthodologie) + emerald: "#22c55e", // emerald-500 (mitigation / solutions) + + // Criticité (P×I et CVSS) + low: "#22c55e", // vert + medium: "#eab308", // jaune + high: "#f97316", // orange + critical: "#ef4444", // rouge + none: "#71717a", // gris (CVSS NONE) + + // Cas-specifique + jeepAccent: "#ff3b30", // rouge Jeep +}; +``` + +### Typographie +- **Sans (body)** : Inter ou Geist Sans +- **Mono (technique)** : JetBrains Mono ou Geist Mono ou Space Mono +- Pas de serif + +Hiérarchie : +- `text-3xl md:text-4xl font-bold` pour H1 (page principale) +- `text-2xl font-bold` pour H2 (sections principales) +- `text-lg font-semibold` pour H3 (sous-sections) +- `text-sm font-semibold` pour cartes +- `text-[10px] tracking-[0.25em] font-mono` pour les codes/labels +- `text-xs` pour le corps de texte secondaire +- `text-[11px]` pour les méta-infos + +### Patterns visuels +- **Bordures fines** : `border border-zinc-800` partout +- **Pas d'ombres** ni de gradients agressifs (sauf hero avec subtle blur ambient) +- **Espacement** : `gap-3` pour grilles, `mb-6` à `mb-8` entre sections +- **Coins droits** : pas de `rounded-*` (excepté éventuellement les badges en `rounded-full` mini) +- **Animations** : transitions de 200-250ms, fade-in subtle au changement de route + +### Composants récurrents +- **Section header** : icône carrée + code mono + titre +- **Stat card** : icône en haut, valeur grande, label en petites caps +- **Carte expandable** (RiskCard, AMDECRow) : en-tête cliquable + contenu replié/déplié +- **Badge** : `border + bg-color/10 + text-color + text-[9px] font-mono tracking-wider` +- **Bandeau info** : `border-l-2` + couleur d'accent + texte +- **Timeline step** : numéro en bordure ronde + ligne verticale + texte + +--- + +## 5. Structures de données (TypeScript) + +```ts +// === MATRICE P × I === +type ProbabilityScore = 1 | 2 | 3 | 4; +type ImpactScore = 1 | 2 | 3 | 4; +type CriticalityLevel = "FAIBLE" | "MODÉRÉ" | "ÉLEVÉ" | "CRITIQUE"; + +// === ÉCHELLES === +interface ScaleLevel { + score: number; + label: string; + description: string; +} + +// === CAS D'ÉTUDE === +interface CaseStudy { + id: "jeep"; + codename: string; // "CHRYSLER-2015-001" + title: string; // "Jeep Cherokee Hack" + year: string; // "2015" + subtitle: string; + accent: string; // hex color + stats: Stat[]; // 4 stats clés + + // P × I global + probability: ProbabilityScore; + impact: ImpactScore; + probReason: string; + impactReason: string; + + // Page 1 — Dossier + timeline: TimelineStep[]; // 5 étapes + why: RootCause[]; // 4 causes + problems: string[]; // ~5 problèmes + solutions: NamedItem[]; // ~5 solutions + + // Page 2 — Risques + risks: Risk[]; // 5 risques + + // Page 3 — AMDEC + amdec: AMDECRow[]; // 5 modes + + // Page 4 — CVE + cves: CVE[]; // 1 CVE + + // Page 5 — Continuité (NOUVEAU) + continuity: ContinuityAnalysis; +} + +interface Stat { + label: string; + value: string; + icon: string; // nom lucide-react +} + +interface TimelineStep { + phase: string; // "01" .. "05" + title: string; + text: string; +} + +interface RootCause { + title: string; + text: string; +} + +interface NamedItem { + title: string; + text: string; +} + +interface Risk { + id: string; // "R1" .. "R5" + title: string; + description: string; + probability: ProbabilityScore; + impact: ImpactScore; + probHypothesis: string; + impactHypothesis: string; + solution: string; +} + +interface AMDECRow { + id: string; // "AMD-01" .. "AMD-05" + component: string; + function: string; + failureMode: string; + cause: string; + effect: string; + severity: number; // G : 1-10 + occurrence: number; // O : 1-10 + detection: number; // D : 1-10 + action: string; + // IPR calculé : severity × occurrence × detection +} + +interface CVE { + id: string; // "CVE-2015-5611" + title: string; + publishedDate: string; + advisories: { id: string; source: string }[]; + affected: string; + description: string; + attackChain: string[]; + cvssV2: CVSSScore; + cvssV3: CVSSScore; + remediation: string[]; + references: { label: string; url: string }[]; +} + +interface CVSSScore { + score: number; + severity: "NONE" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; + vector: string; + note?: string; + metrics: CVSSMetric[]; +} + +interface CVSSMetric { + key: string; // "AV", "AC", etc. + label: string; // "Attack Vector" + value: string; // "A" + valueLabel: string; // "Adjacent" + detail: string; // justification spécifique au cas +} + +interface ContinuityAnalysis { + criticalComponent: { + name: string; + rationale: string; + alternatives: string[]; // autres composants envisagés + }; + rto: { + value: string; // "< 72h" / "6 mois" + realityHistorical: string; + target: string; + justification: string; + }; + rpo: { + value: string; + realityHistorical: string; + target: string; + justification: string; + }; + continuityMeasures: ContinuityMeasure[]; + retrospective: { + pcaPraExisted: boolean; + description: string; + consequences: string[]; + postIncidentLessons: string[]; + }; +} + +interface ContinuityMeasure { + type: "TECHNIQUE" | "ORGANISATIONNELLE"; + title: string; + description: string; +} +``` + +--- + +## 6. Page 1 — Dossier d'incident + +### Données du Hero +```ts +{ + codename: "CHRYSLER-2015-001", + title: "Jeep Cherokee Hack", + year: "2015", + subtitle: "Prise de contrôle à distance via réseau cellulaire", + accent: "#ff3b30", + probability: 3, + impact: 4, + // score = 12, niveau CRITIQUE (rouge) + 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" }, + ], +} +``` + +### Section : Comment l'attaque s'est déroulée (Timeline) + +**Phase 01 — Reconnaissance** +> 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 — Pivot Wi-Fi → Cellulaire** +> 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 — Compromission de la head unit** +> 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 — Pivot vers le CAN bus** +> 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 — Contrôle physique** +> 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. + +### Section : Pourquoi c'est arrivé (Causes racines) + +**01. Architecture plate sans segmentation** +> 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. + +**02. Modèle de menace obsolète** +> 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. + +**03. Services exposés sans authentification** +> D-Bus écoutait sur 0.0.0.0 sur un port joignable depuis Internet via Sprint. Aucune ACL, aucun TLS, aucun secret partagé. + +**04. Pas d'isolation opérateur** +> Sprint n'isolait pas les véhicules entre eux sur son réseau privé : un appareil quelconque pouvait scanner tous les Uconnect du pays. + +### Section : Problèmes identifiés + +- 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 + +### Section : Solutions et bonnes pratiques + +**Segmentation réseau interne (gateway CAN)** +> Insertion d'une passerelle filtrante entre l'infotainment et le CAN bus, avec liste blanche stricte des trames autorisées. + +**Firmware signé partout** +> Secure boot et signature cryptographique obligatoire sur tous les ECU, y compris les microcontrôleurs périphériques (V850 et équivalents). + +**OTA sécurisées (Uptane)** +> Adoption d'Uptane comme standard de mise à jour OTA résistant aux compromissions de serveur, déployé depuis chez les principaux constructeurs. + +**Isolation au niveau opérateur** +> 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. + +**ISO/SAE 21434 & UNECE R155** +> Standards apparus après l'incident : threat modeling cybersécurité obligatoire pour l'homologation des véhicules à partir de 2022. + +--- + +## 7. Page 2 — Analyse de risques (Matrice P × I) + +### Méthodologie (briefing en haut de page) + +**Activité 1 — Matrice de criticité** + +À partir du cas fil rouge assigné, réaliser les étapes suivantes : + +1. **Identifier 5 risques** — Risques précis et contextualisés, pas des catégories génériques +2. **Attribuer P et I** — Probabilité (1 à 4) et Impact (1 à 4), calculer la criticité +3. **Positionner dans la matrice** — Identifier le risque le plus et le moins critique +4. **Documenter les hypothèses** — Pour chaque score de probabilité attribué + +**À éviter** : "Risque réseau" (trop générique, non actionnable) +**Attendu** : "Interception du trafic MQTT entre capteurs et broker en raison de l'absence de chiffrement TLS" (précis, contextualisé) + +### Échelles + +**Probabilité (1-4)** +| Score | Niveau | Fréquence | +|-------|--------|-----------| +| 1 | Très improbable | Une fois en 10 ans ou plus | +| 2 | Improbable | Une fois tous les 2 à 3 ans | +| 3 | Probable | Une fois par an | +| 4 | Très probable | Plusieurs fois par an | + +**Impact (1-4)** +| Score | Niveau | Conséquences | +|-------|--------|--------------| +| 1 | Négligeable | Aucune interruption, aucune donnée compromise | +| 2 | Mineur | Gêne passagère, dégradation partielle du service | +| 3 | Majeur | Interruption de service, perte de données partielle | +| 4 | Critique | Danger physique, perte massive, sanction juridique, atteinte irréversible | + +**Niveaux de criticité** (Probabilité × Impact) +- 1-3 : **FAIBLE** (#22c55e) +- 4-6 : **MODÉRÉ** (#eab308) +- 8-9 : **ÉLEVÉ** (#f97316) +- 12-16 : **CRITIQUE** (#ef4444) + +### Les 5 risques contextualisés + +#### R1 — É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. +- **P = 4 (Très probable)** | **I = 1 (Négligeable)** | **Criticité = 4** +- **Hypothèse P** : 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. +- **Hypothèse I** : 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. +- **Mitigation** : 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. + +#### R2 — 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. +- **P = 3 (Probable)** | **I = 2 (Mineur)** | **Criticité = 6** +- **Hypothèse P** : 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. +- **Hypothèse I** : À 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. +- **Mitigation** : 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. + +#### R3 — 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. +- **P = 2 (Improbable)** | **I = 3 (Majeur)** | **Criticité = 6** +- **Hypothèse P** : 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. +- **Hypothèse I** : 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. +- **Mitigation** : 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. + +#### R4 — 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. +- **P = 2 (Improbable)** | **I = 4 (Critique)** | **Criticité = 8** +- **Hypothèse P** : 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. +- **Hypothèse I** : 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. +- **Mitigation** : 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). + +> **PLUS CRITIQUE** — Score 8 + +#### R5 — 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. +- **P = 2 (Improbable)** | **I = 2 (Mineur)** | **Criticité = 4** +- **Hypothèse P** : 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. +- **Hypothèse I** : 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. +- **Mitigation** : 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. + +> **MOINS CRITIQUE (ex-aequo avec R1)** — Score 4 + +### Synthèse pédagogique +Les scores cumulés ne sont pas tous concentrés en haut de la matrice — l'inventaire couvre intentionnellement plusieurs niveaux pour illustrer la dispersion d'un même incident en sous-risques de criticités diverses. Le pire **risque unitaire** (R4) atteint 8 (ÉLEVÉ), tandis que le score global de l'incident dans son ensemble est 12 (CRITIQUE). + +--- + +## 8. Page 3 — AMDEC / FMEA + +### Méthodologie (briefing) + +**A**nalyse des **M**odes de **D**éfaillance, de leurs **E**ffets et de leur **C**riticité (équivalent français de la FMEA — Failure Mode and Effects Analysis). Méthode systématique de priorisation des défaillances. + +Pour chaque composant, on identifie : fonction, mode de défaillance, cause, effet. Puis on score sur 3 axes : + +| Axe | Sens | Échelle | +|-----|------|---------| +| **G — Gravité** | Sévérité de l'effet sur le système ou l'utilisateur final | 1 (imperceptible) → 10 (critique, danger vital) | +| **O — Occurrence** | Fréquence d'apparition de la défaillance en conditions réelles | 1 (très rare) → 10 (quasi-certaine) | +| **D — Détection** | Difficulté à détecter la défaillance avant impact | 1 (détection immédiate) → 10 (indétectable) | + +**Formule** : `IPR = G × O × D` (Indice de Priorité du Risque, max 1000) + +**Niveaux d'IPR** +| Score | Niveau | Couleur | +|-------|--------|---------| +| < 100 | FAIBLE | #22c55e | +| 100-199 | MODÉRÉ | #eab308 | +| 200-399 | ÉLEVÉ | #f97316 | +| ≥ 400 | CRITIQUE | #ef4444 | + +### Échelles détaillées + +**Gravité** +- 1-2 : **Mineure** — Effet imperceptible ou très limité, pas de préjudice fonctionnel +- 3-4 : **Faible** — Dégradation perceptible mais sans conséquence sécuritaire +- 5-6 : **Modérée** — Interruption de service, mécontentement utilisateur +- 7-8 : **Importante** — Compromission système, données exposées +- 9-10 : **Critique** — Danger physique, atteinte irréversible, mise en jeu de vies + +**Occurrence** +- 1-2 : **Très rare** — Probabilité de défaillance quasi nulle (< 1 sur 100 000) +- 3-4 : **Rare** — Défaillance peu probable (1 sur 10 000) +- 5-6 : **Occasionnelle** — Défaillance possible (1 sur 1 000) +- 7-8 : **Fréquente** — Défaillance probable (1 sur 100) +- 9-10 : **Quasi-certaine** — Défaillance certaine en conditions d'exploitation + +**Détection** +- 1-2 : **Très probable** — Détectée immédiatement par les moyens existants +- 3-4 : **Probable** — Détectée avant impact significatif +- 5-6 : **Possible** — Détection possible mais incertaine +- 7-8 : **Peu probable** — Détection difficile, souvent a posteriori +- 9-10 : **Très improbable** — Quasiment indétectable avec les moyens en place + +### Tableau AMDEC complet (5 modes, triés par IPR descendant) + +#### AMD-05 (IPR = 320) — Protocole CAN bus +- **Composant** : Protocole CAN bus (ISO 11898) +- **Fonction** : Communication temps réel entre ECU pour les fonctions véhicule +- **Mode de défaillance** : 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 +- **Effet** : Direction, freins, accélérateur, transmission pilotables depuis n'importe quel ECU compromis, y compris l'infotainment +- **G = 10** | **O = 4** | **D = 8** | **IPR = 320 (ÉLEVÉ)** +- **Action corrective** : 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. + +> **PRIORITÉ MAXIMALE** — IPR = 320 + +#### AMD-03 (IPR = 288) — Microcontrôleur Renesas V850 +- **Composant** : Microcontrôleur Renesas V850 (passerelle CAN) +- **Fonction** : Faire le pont entre la head unit (OMAP) et le CAN bus du véhicule (ECU de sécurité) +- **Mode de défaillance** : 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 +- **Effet** : Pivot depuis l'infotainment compromis vers le CAN bus critique : injection de trames CAN arbitraires possible +- **G = 9** | **O = 4** | **D = 8** | **IPR = 288 (ÉLEVÉ)** +- **Action corrective** : 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. + +#### AMD-01 (IPR = 240) — APN Sprint +- **Composant** : Réseau cellulaire Sprint / APN constructeur +- **Fonction** : Fournir la connectivité du Uconnect aux serveurs FCA +- **Mode de défaillance** : 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 +- **Effet** : Énumération automatisée de la flotte (1.4M véhicules) depuis n'importe quel terminal Sprint, scan IP trivialisé +- **G = 6** | **O = 10** | **D = 4** | **IPR = 240 (ÉLEVÉ)** +- **Action corrective** : 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. + +#### AMD-04 (IPR = 210) — Architecture réseau interne +- **Composant** : Architecture réseau interne du véhicule +- **Fonction** : Interconnecter les ECU (infotainment, motorisation, freinage, direction, transmission) +- **Mode de défaillance** : 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 +- **Effet** : Une fois le V850 compromis (AMD-03), toutes les trames CAN sont à portée — contrôle potentiel des actuateurs critiques +- **G = 10** | **O = 3** | **D = 7** | **IPR = 210 (ÉLEVÉ)** +- **Action corrective** : 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. + +#### AMD-02 (IPR = 112) — Service D-Bus +- **Composant** : Service D-Bus de la head unit Uconnect +- **Fonction** : Communication inter-processus locale entre services de l'infotainment +- **Mode de défaillance** : 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é +- **Effet** : Exécution de code à distance sur le système Linux du Uconnect, compromission de l'infotainment (radio, GPS, climatisation) +- **G = 8** | **O = 7** | **D = 2** | **IPR = 112 (MODÉRÉ)** +- **Action corrective** : 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. + +--- + +## 9. Page 4 — CVE / CVSS + +### Briefing pédagogique + +**CVE** (Common Vulnerabilities and Exposures) attribue un identifiant unique à chaque vulnérabilité publiée, géré par MITRE. **CVSS** (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. + +### Identification de la vulnérabilité + +**CVE-2015-5611** — Missing Authorization in FCA Uconnect RA3/RA4 (Harman-Kardon Infotainment) +- **Publié** : 17 septembre 2015 +- **Advisories** : + - `ICSA-15-260-01` — CISA / ICS-CERT + - `VU#819439` — CERT/CC (Carnegie Mellon) +- **Produits affectés** : 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. + +### Chaîne d'attaque + +1. **Reconnaissance** : Scan IP sur le réseau cellulaire Sprint pour identifier les véhicules Uconnect (port 6667 ouvert) +2. **Exploitation** : Connexion D-Bus non authentifiée + exécution de commandes sur la head unit Linux +3. **Élévation** : Reflashing du microcontrôleur V850 avec un firmware modifié via SPI +4. **Impact** : Injection de trames CAN arbitraires — contrôle des freins, direction, accélérateur, transmission + +### CVSS v2.0 — OFFICIEL NVD + +- **Score** : **8.3 (HIGH)** +- **Vecteur** : `AV:A/AC:L/Au:N/C:C/I:C/A:C` + +| Clé | Métrique | Valeur | Label | Justification | +|-----|----------|--------|-------|---------------| +| **AV** | Access Vector | `A` | Adjacent Network | 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. | +| **AC** | Access Complexity | `L` | Low | Aucune condition particulière requise au-delà de la présence sur Sprint. Scan trivialement automatisable. | +| **Au** | Authentication | `N` | None | Aucune authentification requise pour interagir avec le service D-Bus exposé. | +| **C** | Confidentiality Impact | `C` | Complete | Lecture totale du système Linux du Uconnect (logs, GPS historique, contenu USB, configuration). | +| **I** | Integrity Impact | `C` | Complete | Modification arbitraire du système + chaîne d'attaque permettant de modifier le firmware du V850 et d'injecter dans le CAN bus. | +| **A** | Availability Impact | `C` | Complete | Perte totale de disponibilité possible : extinction du moteur, désactivation transmission, blocage du véhicule. | + +### CVSS v3.1 — RÉINTERPRÉTATION PÉDAGOGIQUE + +- **Score** : **9.6 (CRITICAL)** +- **Vecteur** : `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). + +| Clé | Métrique | Valeur | Label | Justification | +|-----|----------|--------|-------|---------------| +| **AV** | Attack Vector | `A` | Adjacent | Conservé en Adjacent : le réseau Sprint constitue un domaine logique partagé (pas Internet ouvert au sens strict). | +| **AC** | Attack Complexity | `L` | Low | Pas de condition spéciale, attaque répétable. | +| **PR** | Privileges Required | `N` | None | Aucun privilège requis sur le système cible. | +| **UI** | User Interaction | `N` | None | Aucune action utilisateur nécessaire — le conducteur n'a rien à cliquer ou installer. | +| **S** | Scope | `C` | Changed | 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. | +| **C** | Confidentiality | `H` | High | Exposition totale des données système et utilisateur. | +| **I** | Integrity | `H` | High | Modification arbitraire du firmware + injection CAN, compromettant l'intégrité fonctionnelle du véhicule. | +| **A** | Availability | `H` | High | Mise hors service du véhicule possible (coupure moteur, transmission). | + +### Différence v2 vs v3 — Pourquoi le score grimpe + +La métrique **Scope** 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 à **Changed (C)** 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 : *la propagation d'impact compte autant que l'impact local*. + +### Échelles CVSS + +**CVSS v3.1 — Niveaux de sévérité** +- 0.0 : **NONE** +- 0.1–3.9 : **LOW** (vert) +- 4.0–6.9 : **MEDIUM** (jaune) +- 7.0–8.9 : **HIGH** (orange) +- 9.0–10.0 : **CRITICAL** (rouge) + +### 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). + +### Références officielles (liens cliquables) + +- **NVD — CVE-2015-5611** : https://nvd.nist.gov/vuln/detail/CVE-2015-5611 +- **CISA ICSA-15-260-01** : https://www.cisa.gov/news-events/ics-advisories/icsa-15-260-01 +- **CERT VU#819439** : https://www.kb.cert.org/vuls/id/819439 +- **Miller & Valasek — Remote Exploitation of an Unaltered Passenger Vehicle (91p.)** : http://illmatics.com/Remote%20Car%20Hacking.pdf + +--- + +## 10. Page 5 — RTO / RPO / PCA-PRA (NOUVELLE) + +### Briefing pédagogique + +**Activité 3 — Plan de Continuité d'Activité (PCA) et Plan de Reprise d'Activité (PRA)** + +À partir du cas fil rouge assigné, réaliser les étapes suivantes : + +1. **Composant critique** — Identifier le composant le plus critique du système +2. **Définir le RTO** — RTO réaliste et justifié dans le contexte de l'incident réel +3. **Définir le RPO** — RPO réaliste et justifié selon les données en jeu +4. **Mesure de continuité** — Proposer une mesure concrète, technique ou organisationnelle +5. **Analyse rétrospective** — Un PCA/PRA existait-il lors de l'incident réel ? Avec quelles conséquences ? + +### Définitions clés + +- **PCA (Plan de Continuité d'Activité)** : Ensemble de procédures permettant à l'organisation de maintenir ses activités essentielles pendant un sinistre. +- **PRA (Plan de Reprise d'Activité)** : Ensemble de procédures pour remettre en service les systèmes après sinistre, dans des délais et avec des pertes de données acceptables. +- **RTO (Recovery Time Objective)** : Durée maximale acceptable d'interruption de service après sinistre. +- **RPO (Recovery Point Objective)** : Quantité maximale acceptable de données perdues, mesurée en temps avant le sinistre (≃ fréquence de sauvegarde minimale). + +### 1. Composant critique + +**Composant identifié** : **L'infrastructure de mise à jour à distance (OTA) du constructeur FCA** + +**Justification** : +Au moment de l'incident (2015), FCA ne disposait d'aucune capacité d'OTA déployée sur la flotte Jeep Cherokee. Cette absence est ce qui a rendu l'incident catastrophique d'un point de vue continuité d'activité — pas la vulnérabilité elle-même (qui aurait pu être patchée), mais l'**impossibilité de déployer le patch rapidement**. + +Le composant critique n'est donc pas un composant *du véhicule* mais un composant *de l'écosystème constructeur* : sans OTA, chaque véhicule devient un point d'intervention manuel, multipliant le délai de remediation par un facteur 100 à 1000. + +**Composants alternatifs envisagés** (et pourquoi ils sont moins critiques au sens BCP) : +- **Le CAN bus + ECU de sécurité** : composant le plus *dangereux* en cas de compromission, mais c'est un sujet de sécurité fonctionnelle, pas de continuité d'activité au sens classique. +- **Le service Uconnect / Sprint** : critique pour le service connecté, mais le véhicule reste utilisable sans (autoradio, GPS embarqué etc.). +- **La head unit Linux du Uconnect** : composant *vulnérable* mais non critique — son indisponibilité ne met pas le véhicule à l'arrêt. + +> **Conclusion** : Du point de vue PCA/PRA, le composant critique est l'**infrastructure OTA**, parce qu'elle conditionne la capacité du constructeur à reprendre le contrôle sur sa flotte en cas d'incident. + +### 2. Définition du RTO + +**RTO cible** : **< 72 heures pour atteindre 95% de la flotte patchée** + +**Réalité historique (2015)** : +- 16 juillet 2015 : patch disponible chez FCA +- 23 juillet 2015 : annonce du rappel formel pour 1.4M véhicules +- Septembre 2015 : poursuite du rappel via clés USB postales +- ~6 mois pour atteindre un taux de couverture significatif (estimé 70-80%) + +**Justification du RTO cible** : +- Une fenêtre de divulgation publique d'une vulnérabilité critique automotive ne doit pas dépasser quelques jours avant patch — sinon des attaquants peuvent industrialiser l'exploit (worm sur Sprint était plausible selon Miller). +- 72h correspond au délai standard adopté post-incident par les principaux constructeurs (Tesla, Ford, GM) pour les patches de sécurité critiques. +- Le NHTSA et l'UNECE R155 imposent depuis 2022 une capacité de réaction sous quelques jours pour les défauts cyber. + +**RTO par périmètre** : +| Périmètre | Réalité 2015 | Cible moderne | +|-----------|--------------|---------------| +| Véhicule individuel (intervention dealer) | ~2h | < 1h | +| Flotte complète (1.4M véhicules) | ~6 mois | < 72h via OTA | +| Service Uconnect côté serveur FCA | N/A (pas affecté) | < 4h | + +### 3. Définition du RPO + +**RPO cible** : **< 5 minutes pour les logs de sécurité, 0 pour la configuration ECU, 24h pour les données utilisateur** + +**Réalité historique (2015)** : +- Aucun système de télémétrie de sécurité côté véhicule → **RPO = total** (perte de tout l'historique des événements) +- Aucune sauvegarde de la configuration des ECU côté constructeur +- Logs CAN non remontés vers FCA → toute trace forensique perdue dès le redémarrage + +**Justification du RPO cible** (par type de donnée) : + +| Type de donnée | RPO cible | Justification | +|----------------|-----------|---------------| +| Logs CAN et événements de sécurité | < 5 minutes | Remontée continue vers un V-SOC pour détection d'attaques en cours. 5 min = compromis entre bande passante et réactivité. | +| Configuration ECU et firmware | 0 (instantané) | Une image signée et versionnée existe toujours côté constructeur — restauration possible sans perte. | +| Données utilisateur infotainment (contacts, navigation) | < 24h | Synchronisation cloud quotidienne suffisante, ce sont des données de confort sans criticité. | +| État véhicule (GPS, télémétrie d'usage) | < 1h | Pour traçabilité forensique et investigation post-incident. | + +**Hypothèses du RPO** : +- La cible suppose une connectivité quasi permanente du véhicule (true pour les modèles connectés post-2018). +- En cas de perte de connectivité prolongée, le véhicule stocke localement et synchronise au retour en ligne — buffer de quelques heures dimensionné côté ECU. + +### 4. Mesure de continuité concrète + +**Mesure proposée** : **Déploiement d'une infrastructure OTA Uptane couplée à un V-SOC centralisé** + +**Type** : **TECHNIQUE + ORGANISATIONNELLE** (hybride) + +**Composants techniques** : +1. **Uptane** (Update Framework for Automotive) : standard OTA résistant aux compromissions de serveur, déjà adopté par Toyota, Ford et la majorité des grands constructeurs depuis 2018. Garantit qu'un patch déployé n'a pas été altéré, même si l'infrastructure constructeur est compromise. +2. **V-SOC** (Vehicle Security Operations Center) : équipe et plateforme dédiée à la surveillance cybersécurité de la flotte. Reçoit les logs de sécurité de tous les véhicules connectés en temps quasi réel, détecte les anomalies et déclenche les playbooks d'incident. +3. **Gateway CAN filtrante** : sépare physiquement le domaine infotainment du domaine sécurité fonctionnelle. Même en cas de compromission de l'un, l'autre reste préservé — permet au véhicule de continuer à rouler en mode sécurité dégradée. + +**Composants organisationnels** : +4. **Procédure d'incident cyber automotive** : équipe d'astreinte 24/7 capable de pousser un patch en moins de 72h sur la flotte complète, en lien avec NHTSA / autorités équivalentes. +5. **Partage d'information** : adhésion à l'**Auto-ISAC** (Automotive Information Sharing and Analysis Center), créé en 2015 suite à l'incident Jeep — coopération entre constructeurs sur les menaces et IoC. + +**Mesure prioritaire si on devait n'en garder qu'une** : l'**OTA Uptane**. Sans elle, aucune des autres mesures ne peut atteindre son RTO. + +### 5. Analyse rétrospective + +**Un PCA/PRA cybersécurité existait-il chez FCA en 2015 ?** → **Non.** + +**Description de la situation** : +FCA disposait de plans de continuité classiques (cyber-attaques sur les SI internes, sinistres physiques sur les usines), mais **pas** de PCA/PRA spécifiquement dédié à la cybersécurité automotive embarquée. La menace de compromission à distance d'un véhicule en circulation n'était pas dans le modèle de menace utilisé pour la conception. + +**Conséquences concrètes de cette absence** : + +| Conséquence | Détail | +|-------------|--------| +| Rappel physique de 1.4M véhicules | Premier rappel automobile motivé par une vulnérabilité cyber. Pas d'OTA = pas d'autre option. | +| Distribution de patches via USB postaux | Clés USB physiques envoyées par courrier aux propriétaires entre juillet et septembre 2015. Couverture incertaine (les utilisateurs doivent brancher la clé). | +| Fermeture d'urgence côté Sprint | Sprint a dû fermer le port 6667 côté infrastructure en urgence, sans plan préétabli avec FCA. Mesure réactive, pas planifiée. | +| Enquête NHTSA | Le régulateur (National Highway Traffic Safety Administration) a ouvert une enquête sur l'efficacité du rappel, prolongeant l'exposition médiatique de FCA. | +| Coût direct estimé | ~100M$ entre opérations de rappel, communication, conseil juridique. Hors coût indirect (perte de confiance). | +| Impact boursier | Action FCA a chuté de ~2% sur la semaine suivant la divulgation. | +| Durée totale de remediation | ~6 mois pour atteindre une couverture significative — fenêtre pendant laquelle des copycats auraient pu exploiter la vulnérabilité (heureusement non observés). | + +**Leçons retenues et changements post-incident** (au niveau industrie) : + +1. **Création de l'Auto-ISAC** (Automotive Information Sharing and Analysis Center) — août 2015, suite directe de l'incident. Plateforme de partage d'IoC et de bonnes pratiques entre constructeurs. +2. **Adoption massive de l'OTA** : la plupart des constructeurs ont accéléré leur roadmap OTA. Tesla en avait depuis 2012 ; Ford, GM, BMW ont suivi 2016-2019. +3. **Émergence des V-SOC** : création de cellules dédiées à la sécurité automotive chez les grands constructeurs (Toyota, Ford, GM, FCA-Stellantis). +4. **Standard ISO/SAE 21434** (publié 2021) — impose un threat modeling cybersécurité formel sur tout le cycle de vie des véhicules. +5. **Règlement UNECE R155** (adopté 2020, obligatoire homologation juillet 2022) — impose un CSMS (Cyber Security Management System) à tout constructeur souhaitant vendre dans l'UE, Japon, Corée du Sud. Sans CSMS = pas d'homologation = pas de vente. +6. **Procédure de rappel cyber accélérée** chez NHTSA — protocole spécifique pour les vulnérabilités cyber, distinct des défauts mécaniques classiques. + +**Conclusion de l'analyse rétrospective** : +L'incident Jeep Cherokee de 2015 est un cas d'école de **PCA/PRA cybersécurité défaillant** dans l'automobile. L'industrie a tiré les conséquences sur dix ans, mais en 2015, FCA s'est trouvé sans aucun moyen de réagir vite à une vulnérabilité grave. Le coût d'une politique de continuité (OTA, V-SOC) est très inférieur au coût d'un rappel physique — et c'est l'enseignement principal de l'incident. + +--- + +## 11. Composants UI réutilisables à recréer + +Liste minimale des composants à créer côté Next.js / React. Voir l'artefact `iot_risk_briefing.jsx` pour les implémentations de référence. + +### Layout +- `` : header global + hero du cas + navigation secondaire + footer +- `` : carte de présentation du cas avec stats et score de criticité +- `` : barre d'onglets soulignés avec compteur (ex : "5 risques") +- `
` : conteneur de section avec en-tête (icône + code mono + titre) + +### Cartes +- `` : carte de stat avec icône + valeur grande + label +- `` : carte expandable pour un risque (avec hypothèses + mitigation) +- `` : ligne expandable pour un mode de défaillance AMDEC +- `` : carte d'une métrique CVSS avec clé, valeur, libellé, justification +- `` : panneau complet d'un scoring CVSS (v2 ou v3) +- `` : étape de chronologie avec numéro et trait vertical + +### Spécialisés +- `` : matrice 4×4 cliquable avec positionnement des risques en chips +- `` : score CVSS en cercle coloré selon la sévérité +- `` : barre de score 0-10 avec valeur numérique à droite +- `` : badge inline (`ÉTAPE 04`, `MITIGATION`, `OFFICIEL`, etc.) + +### Composants page 5 (nouveaux) +- `` : affichage du RTO avec valeur cible et comparaison historique +- `` : idem pour le RPO +- `` : carte d'une mesure de continuité avec badge type (technique/organisationnelle) +- `` : timeline post-incident des leçons retenues + +--- + +## 12. Fonctions utilitaires + +```ts +// Niveau de criticité P × I +function getCriticality(prob: number, impact: number) { + const score = prob * impact; + if (score <= 3) return { score, level: "FAIBLE", color: "#22c55e" }; + if (score <= 6) return { score, level: "MODÉRÉ", color: "#eab308" }; + if (score <= 9) return { score, level: "ÉLEVÉ", color: "#f97316" }; + return { score, level: "CRITIQUE", color: "#ef4444" }; +} + +// Niveau d'IPR AMDEC +function getIPRLevel(ipr: number) { + if (ipr < 100) return { label: "FAIBLE", color: "#22c55e" }; + if (ipr < 200) return { label: "MODÉRÉ", color: "#eab308" }; + if (ipr < 400) return { label: "ÉLEVÉ", color: "#f97316" }; + return { label: "CRITIQUE", color: "#ef4444" }; +} + +// Niveau CVSS +function getCVSSLevel(score: number) { + if (score === 0) return { label: "NONE", color: "#71717a" }; + if (score < 4.0) return { label: "LOW", color: "#22c55e" }; + if (score < 7.0) return { label: "MEDIUM", color: "#eab308" }; + if (score < 9.0) return { label: "HIGH", color: "#f97316" }; + return { label: "CRITICAL", color: "#ef4444" }; +} +``` + +--- + +## 13. Notes pour Claude Code + +### Bonnes pratiques attendues +- **TypeScript strict** : pas de `any`, types explicites partout +- **Composants Server / Client clairs** : marquer `"use client"` uniquement quand c'est nécessaire (interactivité, useState) +- **Lazy import** : pour les vues lourdes (CVSS, AMDEC), import dynamique avec `next/dynamic` si performance critique +- **Accessibilité** : ` + ); +} + +function StatCard({ label, value, icon: Icon }) { + return ( +
+
+ + METRIC +
+
{value}
+
{label}
+
+ ); +} + +function Section({ icon: Icon, label, code, children, accent = "#facc15" }) { + return ( +
+
+
+ +
+
+
+ {code} +
+

{label}

+
+
+ {children} +
+ ); +} + +function TimelineStep({ phase, title, text, accent }) { + return ( +
+
+
+ {phase} +
+
+
+
+

{title}

+

{text}

+
+
+ ); +} + +function WhyCard({ title, text, idx }) { + return ( +
+
+ 0{idx + 1} +

{title}

+
+

{text}

+
+ ); +} + +function SolutionCard({ title, text }) { + return ( +
+

+ + {title} +

+

{text}

+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+ {/* Matrice pleine largeur */} +
+
+
+
+ CRIT-MATRIX / P × I +
+
+ Probabilité (axe X) × Impact (axe Y) — cliquez une case pour l'analyser +
+
+
+ LEGEND: + {[ + { lvl: "FAIBLE", c: "#22c55e", range: "1–3" }, + { lvl: "MODÉRÉ", c: "#eab308", range: "4–6" }, + { lvl: "ÉLEVÉ", c: "#f97316", range: "8–9" }, + { lvl: "CRITIQUE", c: "#ef4444", range: "12–16" }, + ].map((l) => ( +
+
+ + {l.lvl} ({l.range}) + +
+ ))} +
+
+ +
+ {/* Header row */} +
+ + IMPACT ↓ / PROB → + +
+ {probs.map((p) => ( +
+
P={p}
+
+ {PROBABILITY_LEVELS[p - 1].label} +
+
+ ))} + + {/* Rows */} + {[...impacts].reverse().map((i) => ( + +
+
+
I={i}
+
+ {IMPACT_LEVELS[i - 1].label} +
+
+
+ {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 ( + + ); + })} +
+ ))} +
+
+ + {/* Bandeau sélection horizontal */} +
+
+ SELECTION ACTIVE +
+ {activeCrit ? ( +
+
+
+ PROBABILITÉ ({selectedProb}/4) +
+
+ {PROBABILITY_LEVELS[selectedProb - 1].label} +
+
+ {PROBABILITY_LEVELS[selectedProb - 1].freq} +
+
+
+
+ IMPACT ({selectedImpact}/4) +
+
+ {IMPACT_LEVELS[selectedImpact - 1].label} +
+
+ {IMPACT_LEVELS[selectedImpact - 1].conseq} +
+
+
+
+ CRITICITÉ = P × I +
+
+ {selectedProb} × {selectedImpact} = {activeCrit.score} +
+
+
+
+ NIVEAU {activeCrit.level} +
+
+
+ ) : ( +
+ Cliquez une cellule de la matrice ci-dessus pour analyser ses paramètres. +
+ )} +
+ + {/* Tables récapitulatives */} +
+
+
+ ÉCHELLE / PROBABILITÉ +
+
+ {PROBABILITY_LEVELS.map((p) => ( +
+
P={p.score}
+
{p.label}
+
{p.freq}
+
+ ))} +
+
+
+
+ ÉCHELLE / IMPACT +
+
+ {IMPACT_LEVELS.map((i) => ( +
+
I={i.score}
+
{i.label}
+
{i.conseq}
+
+ ))} +
+
+
+ +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+
+ {n} +
+

{title}

+

{text}

+
+ ); +} + +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 ( +
+ + + {open && ( +
+ {/* Version mobile des scores */} +
+
+ P + {risk.probability} +
+
+ I + {risk.impact} +
+
+ {crit.level} +
+
+ +
+ + ÉTAPE 04 + + + DOCUMENTATION DES HYPOTHÈSES + +
+ +
+
+ HYPOTHÈSE / PROBABILITÉ = {risk.probability} + + {" "} + ({PROBABILITY_LEVELS[risk.probability - 1].label}) + +
+

+ {risk.probHypothesis} +

+
+
+
+ HYPOTHÈSE / IMPACT = {risk.impact} + + {" "} + ({IMPACT_LEVELS[risk.impact - 1].label}) + +
+

+ {risk.impactHypothesis} +

+
+ + {risk.solution && ( +
+
+ + MITIGATION + + + SOLUTION SPÉCIFIQUE À CE RISQUE + +
+
+ +

+ {risk.solution} +

+
+
+ )} +
+ )} +
+ ); +} + +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 ( +
+ {/* Briefing activité */} +
+
+
+ ACTIVITÉ 01 / METHODOLOGY +
+
+

Matrice de criticité

+

+ À partir du cas fil rouge assigné ({data.title}, {data.year}), réalisez les étapes + suivantes : +

+
+ {ACTIVITY_STEPS.map((s) => ( + + ))} +
+
+
+
+ À éviter +
+
"Risque réseau"
+
+ Trop générique, non actionnable. +
+
+
+
+ Attendu +
+
+ "Interception du trafic MQTT entre capteurs et broker en raison de l'absence de + chiffrement TLS" +
+
+ Précis, contextualisé, désigne une vulnérabilité spécifique. +
+
+
+
+ + {/* Liste des risques */} +
+
+
+
+ INVENTAIRE / 5 RISQUES CONTEXTUALISÉS +
+
+ Chaque risque déplie sa{" "} + documentation d'hypothèses{" "} + (étape 4) et sa{" "} + mitigation spécifique, + directement sous son scoring P × I. +
+
+
+ + ÉTAPE 04 + + hypothèses + + + + MITIGATION + + solution +
+
+
+ {data.risks.map((r) => ( + + ))} +
+
+ Cliquez sur l'en-tête d'un risque pour replier/déplier sa carte. +
+
+ + {/* Synthèse plus/moins critique */} +
+
+
+ RISQUE LE PLUS CRITIQUE / SCORE {maxScore} +
+ {scored + .filter((r) => r.score === maxScore) + .map((r) => ( +
+ {r.id}{" "} + {r.title} +
+ ))} +
+
+
+ RISQUE LE MOINS CRITIQUE / SCORE {minScore} +
+ {scored + .filter((r) => r.score === minScore && !highestIds.includes(r.id)) + .map((r) => ( +
+ {r.id}{" "} + {r.title} +
+ ))} +
+
+ + {/* Matrice avec positionnement des risques */} +
+ POSITIONNEMENT / MATRICE P × I +
+ +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// CASE DETAIL VIEW +// ───────────────────────────────────────────────────────────── + +function CaseHero({ data }) { + const crit = getCriticality(data.probability, data.impact); + + return ( +
+
+
+
+
+ CASE FILE · {data.codename} +
+

+ {data.title}{" "} + / {data.year} +

+

{data.subtitle}

+
+
+
CRITICITÉ
+
+ {crit.score} +
+
+ {crit.level} +
+
+
+
+ {data.stats.map((s) => ( + + ))} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// SUB-NAVIGATION (sous-onglets internes au cas) +// ───────────────────────────────────────────────────────────── + +function SubTabs({ current, onChange, options, accent = "#facc15" }) { + return ( + + ); +} + +// ───────────────────────────────────────────────────────────── +// DOSSIER D'INCIDENT (page 1 par cas) +// ───────────────────────────────────────────────────────────── + +function CaseDossier({ data }) { + return ( +
+
+
+

+ Dossier technique de l'incident{" "} + {data.title} ({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. +

+
+ +
+
+ {data.timeline.map((t) => ( + + ))} +
+
+ +
+
+ {data.why.map((w, i) => ( + + ))} +
+
+ +
+
    + {data.problems.map((p, i) => ( +
  • + + {p} +
  • + ))} +
+
+ +
+
+ {data.solutions.map((s, i) => ( + + ))} +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+
+
+
+ + {value} + +
+ ); +} + +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 ( +
+ + + {open && ( +
+
+
+ FONCTION +
+

{row.function}

+
+
+
+ MODE DE DÉFAILLANCE +
+

{row.failureMode}

+
+
+
+ CAUSE +
+

{row.cause}

+
+
+
+ EFFET +
+

{row.effect}

+
+ +
+
+
+
GRAVITÉ
+
{row.severity}/10
+
+
+
OCCURRENCE
+
{row.occurrence}/10
+
+
+
DÉTECTION
+
{row.detection}/10
+
+
+
+ IPR = G × O × D = + {ipr} + · NIVEAU {level.label} +
+
+ +
+
+ + ACTION CORRECTIVE + +
+
+ +

+ {row.action} +

+
+
+
+ )} +
+ ); +} + +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 ( +
+ {/* Briefing pédagogique */} +
+
+ MÉTHODOLOGIE / ACTIVITÉ 02 +
+

AMDEC — FMEA

+

+ Analyse des{" "} + Modes de{" "} + Défaillance, de leurs{" "} + Effets et de leur{" "} + Criticité. 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 : +

+
+
+
G — GRAVITÉ
+
+ Sévérité de l'effet sur le système / l'utilisateur final. 1 = imperceptible, 10 = + critique (danger vital). +
+
+
+
O — OCCURRENCE
+
+ Fréquence d'apparition de la défaillance en conditions réelles. 1 = très rare, 10 = + quasi-certaine. +
+
+
+
D — DÉTECTION
+
+ Difficulté à détecter la défaillance avant impact. 1 = détection immédiate, 10 = + indétectable. +
+
+
+
+
+ IPR = G × O × D + (Indice de Priorité du Risque, max 1000) +
+
+ {[ + { 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) => ( +
+
{l.lvl}
+
{l.range}
+
+ ))} +
+
+
+ + {/* Tableau AMDEC */} +
+
+ TABLEAU AMDEC / {sortedRows.length} MODES DE DÉFAILLANCE (TRIÉS PAR IPR DESC) +
+
+ {sortedRows.map((row, idx) => ( + + ))} +
+
+ Cliquez sur une ligne pour déplier ses détails (fonction, cause, effet, action corrective). +
+
+ + {/* Synthèse */} +
+
+ PRIORITÉ MAXIMALE / IPR = {sortedRows[0].ipr} +
+
+ {sortedRows[0].id} · {sortedRows[0].component} +
+

{sortedRows[0].failureMode}

+

→ {sortedRows[0].action}

+
+ + {/* Échelles */} +
+ {[ + { 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) => ( +
+
+ {s.title} +
+
+ {AMDEC_SCALES[s.key].map((l) => ( +
+ {l.range} +
+
{l.label}
+
{l.desc}
+
+
+ ))} +
+
+ ))} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+
+
+
CVSS {version}
+
+ {score.toFixed(1)} +
+
+
+
+ {level.label} +
+
+ ); +} + +function CVSSMetric({ metric }) { + return ( +
+
+ + {metric.key} + + + {metric.label.toUpperCase()} + +
+
+ {metric.value} + · {metric.valueLabel} +
+

{metric.detail}

+
+ ); +} + +function CVSSPanel({ cvss, version, isOfficial }) { + return ( +
+
+
+
+ + CVSS {version} + + {isOfficial ? ( + + OFFICIEL · NVD + + ) : ( + + RÉINTERPRÉTATION PÉDAGOGIQUE + + )} +
+
{cvss.vector}
+ {cvss.note && ( +

{cvss.note}

+ )} +
+ +
+
+ {cvss.metrics.map((m) => ( + + ))} +
+
+ ); +} + +function CVEView({ data }) { + const cve = data.cves[0]; + + return ( +
+ {/* Briefing pédagogique */} +
+
+ STANDARDS / ACTIVITÉ 03 +
+

CVE & CVSS

+

+ CVE (Common Vulnerabilities and + Exposures) attribue un identifiant unique à chaque vulnérabilité publiée, géré par MITRE.{" "} + CVSS (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. +

+
+ + {/* Header CVE */} +
+
+
+
+ VULNÉRABILITÉ RÉFÉRENCÉE +
+

+ {cve.id} +

+

{cve.title}

+
+
+ Publié : {cve.publishedDate} +
+
+
+
+
+ ADVISORIES +
+
+ {cve.advisories.map((a) => ( +
+ {a.id} + · + {a.source} +
+ ))} +
+
+
+
+ PRODUITS AFFECTÉS +
+

{cve.affected}

+
+
+
+ + {/* Description et chaîne d'attaque */} +
+

{cve.description}

+
+ CHAÎNE D'ATTAQUE +
+
+ {cve.attackChain.map((step, i) => ( +
+ + {String(i + 1).padStart(2, "0")} + + {step} +
+ ))} +
+
+ + {/* Scoring CVSS — v2 officiel + v3 pédagogique */} +
+

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

+
+ + +
+ + {/* Comparaison */} +
+ Différence v2 vs v3 : + La métrique Scope 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 à{" "} + Changed (C) 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 : la propagation d'impact compte autant que l'impact local. +
+
+ + {/* Remediation */} +
+
    + {cve.remediation.map((r, i) => ( +
  • + + {r} +
  • + ))} +
+
+ + {/* Références */} +
+
+ {cve.references.map((ref) => ( + +
+ {ref.label} +
+
{ref.url}
+
+ ))} +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+ + +
+ {subTab === "dossier" && } + {subTab === "analyse" && } + {subTab === "amdec" && } + {subTab === "cve" && } +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// MAIN +// ───────────────────────────────────────────────────────────── + +export default function IoTRiskBriefing() { + return ( +
+ + {/* Header */} +
+
+
+ + + IoT RISK ANALYSIS / CASE STUDY · JEEP CHEROKEE 2015 + +
+ + CLASSIFICATION: PEDAGOGIQUE + +
+

+ Méthodologies d'analyse des risques IoT +

+

+ Étude du Jeep Cherokee Hack (2015) — + dossier d'incident, analyse de risques par{" "} + matrice P × I, 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. +

+
+ + {/* Contenu : le case layout est la page entière */} +
+ +
+ + {/* Footer */} +
+ // IOT-RISK-BRIEFING / JEEP-CHEROKEE-2015 / v2.0 / 2026 + + P × I matrix · AMDEC (FMEA) · CVE-2015-5611 · ISO/SAE 21434 · UN R155 + +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f7e3d12 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2737 @@ +{ + "name": "temp-vite", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "temp-vite", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.3.0", + "lucide-react": "^1.16.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a84dd3 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "temp-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.0", + "lucide-react": "^1.16.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..3ef2de0 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..09a82cb --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,141 @@ +import { useMemo } from 'react' +import { useMousePosition } from './hooks.js' +import IoTRiskBriefing from './IoTRiskBriefing.jsx' + +function MouseSpotlight() { + const { x, y } = useMousePosition() + return ( +
+ ) +} + +function Particles({ count = 18 }) { + const particles = useMemo(() => + Array.from({ length: count }, (_, i) => ({ + id: i, + left: `${5 + Math.random() * 90}%`, + delay: `${Math.random() * 15}s`, + duration: `${10 + Math.random() * 18}s`, + size: `${1 + Math.random() * 2}px`, + opacity: 0.15 + Math.random() * 0.3, + driftX: `${-20 + Math.random() * 40}px`, + })), + [count] + ) + + return ( + <> + {particles.map((p) => ( +
+ ))} + + ) +} + +function MorphBlobs() { + return ( + <> +
+
+
+ + ) +} + +function CircuitTraces() { + const traces = useMemo(() => + Array.from({ length: 5 }, (_, i) => ({ + id: i, + left: `${15 + Math.random() * 70}%`, + width: `${0.5 + Math.random() * 0.5}px`, + height: `${150 + Math.random() * 250}px`, + delay: `${Math.random() * 5}s`, + duration: `${5 + Math.random() * 5}s`, + top: `${Math.random() * 70}%`, + })), + [] + ) + + return ( +
+ {traces.map((t) => ( +
+ ))} +
+ ) +} + +export default function App() { + return ( + <> +
+ + +
+ + +
+
+ +
+ + ) +} diff --git a/src/IoTRiskBriefing.jsx b/src/IoTRiskBriefing.jsx new file mode 100644 index 0000000..ae7291f --- /dev/null +++ b/src/IoTRiskBriefing.jsx @@ -0,0 +1,3433 @@ +import React, { useState, useRef, useEffect } from "react"; +import { useScrollReveal, useTilt, useCountUp, useRipple } from "./hooks.js"; +import { + Shield, + AlertTriangle, + Radio, + Car, + Tv, + Bug, + Lock, + Wrench, + Target, + Zap, + ChevronRight, + Calendar, + MapPin, + Users, + TrendingUp, + Activity, + Skull, + Network, + HardDrive, + Clock, + Database, + Server, + RefreshCw, + FileWarning, + CheckCircle, + XCircle, + ArrowRight, + Timer, + CircleDot, + KeyRound, + Eye, +} from "lucide-react"; + +// ───────────────────────────────────────────────────────────── +// PILIERS DE SÉCURITÉ IoT — Framework canonique du cours +// ───────────────────────────────────────────────────────────── + +const PILLAR_ICONS = { Lock, KeyRound, Network, Shield, RefreshCw, Eye }; + +const PILLARS = [ + { + key: "chiffrement", + label: "Chiffrement", + icon: "Lock", + color: "#facc15", + riskFamilies: ["Réseau", "Sécurité"], + isTransversal: false, + description: + "Protection de la confidentialité et de l'intégrité des données en transit et au repos. TLS, chiffrement disque, gestion de clés (HSM/TPM), cryptographie à l'état de l'art.", + }, + { + key: "identites", + label: "Identités et permissions", + icon: "KeyRound", + color: "#a78bfa", + riskFamilies: ["Sécurité", "Humain"], + isTransversal: false, + description: + "Identité unique et vérifiable pour chaque device, utilisateur et service. Authentification forte (mTLS, MFA, certificats), autorisations granulaires, suppression des credentials par défaut.", + }, + { + key: "segmentation", + label: "Segmentation réseau", + icon: "Network", + color: "#22d3ee", + riskFamilies: ["Réseau", "Sécurité"], + isTransversal: false, + description: + "Séparation logique des domaines de criticité. VLAN, zero-trust, gateways filtrantes, architecture en zones et conduits (IEC 62443 / ISO 21434). Empêcher la latéralité d'un attaquant après compromission.", + }, + { + key: "hardening", + label: "Hardening (durcissement)", + icon: "Shield", + color: "#60a5fa", + riskFamilies: ["Sécurité", "Humain", "Logiciel"], + isTransversal: false, + description: + "Configuration sécurisée par défaut. Secure boot, signature de firmware, suppression des services non essentiels, principle of least functionality, désactivation des ports inutiles, durcissement OS et applications.", + }, + { + key: "maj", + label: "Mises à jour sécurisées", + icon: "RefreshCw", + color: "#34d399", + riskFamilies: ["Logiciel", "Sécurité"], + isTransversal: false, + description: + "Capacité à déployer rapidement et de manière vérifiable des patches sur l'ensemble de la flotte. OTA résistante (Uptane / TUF), signature des updates, rollback protection, gestion du cycle de vie.", + }, + { + key: "supervision", + label: "Supervision", + icon: "Eye", + color: "#fb923c", + riskFamilies: ["Réseau", "Sécurité", "Humain", "Logiciel"], + isTransversal: true, + description: + "Détection, journalisation, monitoring et réponse à incident. SOC / V-SOC, IDS embarqué, télémétrie sécurité, threat intelligence partagée (ISAC), procédures d'incident testées. Couche transversale qui adresse toutes les familles de risques.", + }, +]; + +function getPillar(key) { + return PILLARS.find((p) => p.key === key); +} + +const RISK_FAMILIES = [ + { key: "Réseau", description: "Connectivité, transport, isolation L2/L3" }, + { key: "Sécurité", description: "Cybersécurité technique stricte (auth, crypto, exploits)" }, + { key: "Humain", description: "Erreurs de config, ingénierie sociale, faute opérationnelle" }, + { key: "Logiciel", description: "Bugs, vulns applicatives, défauts de conception" }, +]; + +// ───────────────────────────────────────────────────────────── +// 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.", + pillars: ["segmentation"], + }, + { + 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).", + pillars: ["hardening"], + }, + { + 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.", + pillars: ["maj"], + }, + { + 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.", + pillars: ["segmentation"], + }, + { + 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.", + pillars: ["supervision", "hardening"], + }, + ], + 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.", + pillars: ["segmentation"], + 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.", + pillars: ["identites", "hardening"], + 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.", + pillars: ["hardening"], + 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.", + pillars: ["segmentation", "identites"], + 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.", + pillars: ["hardening", "supervision"], + 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é", + pillars: ["segmentation"], + 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)", + pillars: ["hardening", "identites", "chiffrement"], + 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", + pillars: ["hardening"], + 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", + pillars: ["segmentation"], + 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", + pillars: ["identites", "segmentation"], + 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" }, + ], + }, + ], + continuity: { + criticalComponent: { + name: "Infrastructure de mise à jour à distance (OTA) du constructeur FCA", + rationale: + "Au moment de l'incident (2015), FCA ne disposait d'aucune capacité d'OTA déployée sur la flotte Jeep Cherokee. Cette absence est ce qui a rendu l'incident catastrophique d'un point de vue continuité d'activité — pas la vulnérabilité elle-même (qui aurait pu être patchée), mais l'impossibilité de déployer le patch rapidement. Le composant critique n'est donc pas un composant du véhicule mais un composant de l'écosystème constructeur : sans OTA, chaque véhicule devient un point d'intervention manuel, multipliant le délai de remédiation par un facteur 100 à 1000.", + alternatives: [ + { + name: "CAN bus + ECU de sécurité", + reason: + "Composant le plus dangereux en cas de compromission, mais c'est un sujet de sécurité fonctionnelle, pas de continuité d'activité au sens classique.", + }, + { + name: "Service Uconnect / Sprint", + reason: + "Critique pour le service connecté, mais le véhicule reste utilisable sans (autoradio, GPS embarqué, etc.).", + }, + { + name: "Head unit Linux du Uconnect", + reason: + "Composant vulnérable mais non critique — son indisponibilité ne met pas le véhicule à l'arrêt.", + }, + ], + }, + rto: { + target: "< 72 heures pour atteindre 95% de la flotte patchée", + realityHistorical: + "16 juillet 2015 : patch disponible chez FCA. 23 juillet 2015 : annonce du rappel formel pour 1.4M véhicules. Septembre 2015 : poursuite du rappel via clés USB postales. ~6 mois pour atteindre un taux de couverture significatif (estimé 70-80%).", + justification: + "Une fenêtre de divulgation publique d'une vulnérabilité critique automotive ne doit pas dépasser quelques jours avant patch — sinon des attaquants peuvent industrialiser l'exploit (worm sur Sprint était plausible selon Miller). 72h correspond au délai standard adopté post-incident par les principaux constructeurs (Tesla, Ford, GM) pour les patches de sécurité critiques. Le NHTSA et l'UNECE R155 imposent depuis 2022 une capacité de réaction sous quelques jours pour les défauts cyber.", + breakdown: [ + { + scope: "Véhicule individuel (intervention dealer)", + reality2015: "~2h", + targetModern: "< 1h", + }, + { + scope: "Flotte complète (1.4M véhicules)", + reality2015: "~6 mois", + targetModern: "< 72h via OTA", + }, + { + scope: "Service Uconnect côté serveur FCA", + reality2015: "N/A (pas affecté)", + targetModern: "< 4h", + }, + ], + }, + rpo: { + target: + "< 5 minutes pour les logs de sécurité, 0 pour la configuration ECU, 24h pour les données utilisateur", + realityHistorical: + "Aucun système de télémétrie de sécurité côté véhicule → RPO = total (perte de tout l'historique des événements). Aucune sauvegarde de la configuration des ECU côté constructeur. Logs CAN non remontés vers FCA → toute trace forensique perdue dès le redémarrage.", + breakdown: [ + { + dataType: "Logs CAN et événements de sécurité", + target: "< 5 minutes", + justification: + "Remontée continue vers un V-SOC pour détection d'attaques en cours. 5 min = compromis entre bande passante et réactivité.", + }, + { + dataType: "Configuration ECU et firmware", + target: "0 (instantané)", + justification: + "Une image signée et versionnée existe toujours côté constructeur — restauration possible sans perte.", + }, + { + dataType: "Données utilisateur infotainment", + target: "< 24h", + justification: + "Synchronisation cloud quotidienne suffisante, ce sont des données de confort sans criticité.", + }, + { + dataType: "État véhicule (GPS, télémétrie d'usage)", + target: "< 1h", + justification: + "Pour traçabilité forensique et investigation post-incident.", + }, + ], + hypotheses: + "La cible suppose une connectivité quasi permanente du véhicule (vrai pour les modèles connectés post-2018). En cas de perte de connectivité prolongée, le véhicule stocke localement et synchronise au retour en ligne — buffer de quelques heures dimensionné côté ECU.", + }, + measures: [ + { + type: "TECHNIQUE", + title: "Uptane — Standard OTA automotive", + description: + "Update Framework for Automotive, résistant aux compromissions de serveur. Déjà adopté par Toyota, Ford et la majorité des grands constructeurs depuis 2018. Garantit qu'un patch déployé n'a pas été altéré, même si l'infrastructure constructeur est compromise.", + }, + { + type: "TECHNIQUE", + title: "V-SOC — Vehicle Security Operations Center", + description: + "Équipe et plateforme dédiée à la surveillance cybersécurité de la flotte. Reçoit les logs de sécurité de tous les véhicules connectés en temps quasi réel, détecte les anomalies et déclenche les playbooks d'incident.", + }, + { + type: "TECHNIQUE", + title: "Gateway CAN filtrante", + description: + "Sépare physiquement le domaine infotainment du domaine sécurité fonctionnelle. Même en cas de compromission de l'un, l'autre reste préservé — permet au véhicule de continuer à rouler en mode sécurité dégradée.", + }, + { + type: "ORGANISATIONNELLE", + title: "Procédure d'incident cyber automotive", + description: + "Équipe d'astreinte 24/7 capable de pousser un patch en moins de 72h sur la flotte complète, en lien avec NHTSA / autorités équivalentes.", + }, + { + type: "ORGANISATIONNELLE", + title: "Partage d'information — Auto-ISAC", + description: + "Adhésion à l'Automotive Information Sharing and Analysis Center, créé en 2015 suite à l'incident Jeep — coopération entre constructeurs sur les menaces et IoC.", + }, + ], + retrospective: { + pcaPraExisted: false, + description: + "FCA disposait de plans de continuité classiques (cyber-attaques sur les SI internes, sinistres physiques sur les usines), mais pas de PCA/PRA spécifiquement dédié à la cybersécurité automotive embarquée. La menace de compromission à distance d'un véhicule en circulation n'était pas dans le modèle de menace utilisé pour la conception.", + consequences: [ + { + title: "Rappel physique de 1.4M véhicules", + detail: + "Premier rappel automobile motivé par une vulnérabilité cyber. Pas d'OTA = pas d'autre option.", + }, + { + title: "Distribution de patches via USB postaux", + detail: + "Clés USB physiques envoyées par courrier aux propriétaires entre juillet et septembre 2015. Couverture incertaine.", + }, + { + title: "Fermeture d'urgence côté Sprint", + detail: + "Sprint a dû fermer le port 6667 côté infrastructure en urgence, sans plan préétabli avec FCA. Mesure réactive, pas planifiée.", + }, + { + title: "Enquête NHTSA", + detail: + "Le régulateur a ouvert une enquête sur l'efficacité du rappel, prolongeant l'exposition médiatique de FCA.", + }, + { + title: "Coût direct estimé : ~100M$", + detail: + "Entre opérations de rappel, communication, conseil juridique. Hors coût indirect (perte de confiance).", + }, + { + title: "Impact boursier", + detail: + "Action FCA a chuté de ~2% sur la semaine suivant la divulgation.", + }, + { + title: "Durée totale de remédiation : ~6 mois", + detail: + "Fenêtre pendant laquelle des copycats auraient pu exploiter la vulnérabilité (heureusement non observés).", + }, + ], + lessons: [ + { + year: "2015", + title: "Création de l'Auto-ISAC", + detail: + "Automotive Information Sharing and Analysis Center — suite directe de l'incident. Plateforme de partage d'IoC et de bonnes pratiques entre constructeurs.", + }, + { + year: "2016–2019", + title: "Adoption massive de l'OTA", + detail: + "La plupart des constructeurs ont accéléré leur roadmap OTA. Tesla en avait depuis 2012 ; Ford, GM, BMW ont suivi.", + }, + { + year: "2016–2020", + title: "Émergence des V-SOC", + detail: + "Création de cellules dédiées à la sécurité automotive chez les grands constructeurs (Toyota, Ford, GM, FCA-Stellantis).", + }, + { + year: "2021", + title: "Standard ISO/SAE 21434", + detail: + "Impose un threat modeling cybersécurité formel sur tout le cycle de vie des véhicules.", + }, + { + year: "2022", + title: "Règlement UNECE R155 obligatoire", + detail: + "Impose un CSMS (Cyber Security Management System) à tout constructeur souhaitant vendre dans l'UE, Japon, Corée du Sud. Sans CSMS = pas d'homologation = pas de vente.", + }, + { + year: "2022+", + title: "Procédure de rappel cyber accélérée NHTSA", + detail: + "Protocole spécifique pour les vulnérabilités cyber, distinct des défauts mécaniques classiques.", + }, + ], + conclusion: + "L'incident Jeep Cherokee de 2015 est un cas d'école de PCA/PRA cybersécurité défaillant dans l'automobile. L'industrie a tiré les conséquences sur dix ans, mais en 2015, FCA s'est trouvé sans aucun moyen de réagir vite à une vulnérabilité grave. Le coût d'une politique de continuité (OTA, V-SOC) est très inférieur au coût d'un rappel physique — et c'est l'enseignement principal de l'incident.", + }, + }, + synthesis: { + pillarsStatus: { + chiffrement: { + status: "failed", + severity: "major", + relatedItems: ["R2", "AMD-02"], + }, + identites: { + status: "failed", + severity: "critical", + relatedItems: ["R2", "R4", "AMD-02", "AMD-05", "CVE-2015-5611"], + }, + segmentation: { + status: "failed", + severity: "critical", + relatedItems: ["R1", "R4", "AMD-01", "AMD-04", "AMD-05"], + }, + hardening: { + status: "failed", + severity: "major", + relatedItems: ["R2", "R3", "R5", "AMD-02", "AMD-03"], + }, + maj: { + status: "absent", + severity: "critical", + relatedItems: [], + relatedPage: "continuite", + }, + supervision: { + status: "failed", + severity: "critical", + isTransversal: true, + relatedItems: "all", + }, + }, + }, + }, +}; + +// ───────────────────────────────────────────────────────────── +// 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 ( + + ); +} + +function ScrollReveal({ children, className = "", delay = 0 }) { + const [ref, isVisible] = useScrollReveal(); + return ( +
+ {children} +
+ ); +} + +function PillarBadge({ pillarKey, size = "xs", showLabel = false }) { + const p = getPillar(pillarKey); + if (!p) return null; + const Icon = PILLAR_ICONS[p.icon]; + const iconSize = size === "xs" ? 10 : 12; + return ( + + + {showLabel && {p.label.toUpperCase()}} + + ); +} + +function PillarBadges({ pillars, size = "xs", showLabel = false }) { + if (!pillars || pillars.length === 0) return null; + return ( + + {pillars.map((k) => ( + + ))} + + ); +} + +function AnimatedValue({ value }) { + const numMatch = String(value).match(/^([0-9.]+)(.*)/); + const [ref, count] = useCountUp(numMatch ? numMatch[1] : 0); + + if (!numMatch) return {value}; + + const suffix = numMatch[2] || ""; + const numStr = String(numMatch[1]); + const hasDecimal = numStr.includes("."); + + return ( + + {hasDecimal ? count.toFixed(1) : count} + {suffix} + + ); +} + +function StatCard({ label, value, icon: Icon }) { + const tiltRef = useTilt(10); + + return ( +
+
+
+
+
+ +
+
+
+ +
+
{label}
+
+
+ ); +} + +function Section({ icon: Icon, label, code, children, accent = "#facc15" }) { + const [ref, isVisible] = useScrollReveal(); + return ( +
+
+
+ +
+
+
+ {code} +
+

{label}

+
+
+ {children} +
+ ); +} + +function TimelineStep({ phase, title, text, accent }) { + const [ref, isVisible] = useScrollReveal({ threshold: 0.3 }); + return ( +
+
+
+ {phase} +
+
+
+
+

{title}

+

{text}

+
+
+ ); +} + +function WhyCard({ title, text, idx }) { + const tiltRef = useTilt(6); + return ( +
+
+ 0{idx + 1} +
+

{title}

+

{text}

+
+
+
+ ); +} + +function SolutionCard({ title, text, pillars }) { + return ( +
+
+

+ + {title} +

+

{text}

+ {pillars && pillars.length > 0 && ( +
+ +
+ )} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+ {/* Matrice pleine largeur */} +
+
+
+
+ CRIT-MATRIX / P × I +
+
+ Probabilité × Impact — cliquez une case +
+
+
+ LEGEND: + {[ + { lvl: "FAIBLE", c: "#22c55e", range: "1–3" }, + { lvl: "MODÉRÉ", c: "#eab308", range: "4–6" }, + { lvl: "ÉLEVÉ", c: "#f97316", range: "8–9" }, + { lvl: "CRITIQUE", c: "#ef4444", range: "12–16" }, + ].map((l) => ( +
+
+ + {l.lvl} ({l.range}) + +
+ ))} +
+
+ +
+ {/* Header row */} +
+ + I ↓ / P → + +
+ {probs.map((p) => ( +
+
P={p}
+
+ {PROBABILITY_LEVELS[p - 1].label} +
+
+ ))} + + {/* Rows */} + {[...impacts].reverse().map((i) => ( + +
+
+
I={i}
+
+ {IMPACT_LEVELS[i - 1].label} +
+
+
+ {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 ( + + ); + })} +
+ ))} +
+
+ + {/* Bandeau sélection horizontal */} +
+
+ SELECTION ACTIVE +
+ {activeCrit ? ( +
+
+
+ PROBABILITÉ ({selectedProb}/4) +
+
+ {PROBABILITY_LEVELS[selectedProb - 1].label} +
+
+ {PROBABILITY_LEVELS[selectedProb - 1].freq} +
+
+
+
+ IMPACT ({selectedImpact}/4) +
+
+ {IMPACT_LEVELS[selectedImpact - 1].label} +
+
+ {IMPACT_LEVELS[selectedImpact - 1].conseq} +
+
+
+
+ CRITICITÉ = P × I +
+
+ {selectedProb} × {selectedImpact} = {activeCrit.score} +
+
+
+
+ NIVEAU {activeCrit.level} +
+
+
+ ) : ( +
+ Cliquez une cellule de la matrice ci-dessus pour analyser ses paramètres. +
+ )} +
+ + {/* Tables récapitulatives */} +
+
+
+ ÉCHELLE / PROBABILITÉ +
+
+ {PROBABILITY_LEVELS.map((p) => ( +
+
P={p.score}
+
{p.label}
+
{p.freq}
+
+ ))} +
+
+
+
+ ÉCHELLE / IMPACT +
+
+ {IMPACT_LEVELS.map((i) => ( +
+
I={i.score}
+
{i.label}
+
{i.conseq}
+
+ ))} +
+
+
+ +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+
+ {n} +
+

{title}

+

{text}

+
+ ); +} + +function RiskCard({ risk, isHighest, isLowest }) { + const crit = getCriticality(risk.probability, risk.impact); + const [open, setOpen] = useState(true); + const [ref, isVisible] = useScrollReveal(); + + let tag = null; + if (isHighest) tag = { label: "PLUS CRITIQUE", color: "#ef4444" }; + else if (isLowest) tag = { label: "MOINS CRITIQUE", color: "#22c55e" }; + + return ( +
+ + + {open && ( +
+ {/* Version mobile des scores */} +
+
+ P + {risk.probability} +
+
+ I + {risk.impact} +
+
+ {crit.level} +
+
+ +
+ + ÉTAPE 04 + + + DOCUMENTATION DES HYPOTHÈSES + +
+ +
+
+ HYPOTHÈSE / PROBABILITÉ = {risk.probability} + + {" "} + ({PROBABILITY_LEVELS[risk.probability - 1].label}) + +
+

+ {risk.probHypothesis} +

+
+
+
+ HYPOTHÈSE / IMPACT = {risk.impact} + + {" "} + ({IMPACT_LEVELS[risk.impact - 1].label}) + +
+

+ {risk.impactHypothesis} +

+
+ + {risk.solution && ( +
+
+ + MITIGATION + + + SOLUTION SPÉCIFIQUE À CE RISQUE + +
+
+ +

+ {risk.solution} +

+
+
+ )} +
+ )} +
+ ); +} + +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 ( +
+ {/* Briefing activité */} +
+
+
+ ACTIVITÉ 01 / METHODOLOGY +
+
+

Matrice de criticité

+

+ À partir du cas fil rouge assigné ({data.title}, {data.year}), réalisez les étapes + suivantes : +

+
+ {ACTIVITY_STEPS.map((s) => ( + + ))} +
+
+
+
+ À éviter +
+
"Risque réseau"
+
+ Trop générique, non actionnable. +
+
+
+
+ Attendu +
+
+ "Interception du trafic MQTT entre capteurs et broker en raison de l'absence de + chiffrement TLS" +
+
+ Précis, contextualisé, désigne une vulnérabilité spécifique. +
+
+
+
+ + {/* Liste des risques */} +
+
+
+
+ INVENTAIRE / 5 RISQUES CONTEXTUALISÉS +
+
+ Chaque risque déplie sa{" "} + documentation d'hypothèses{" "} + (étape 4) et sa{" "} + mitigation spécifique, + directement sous son scoring P × I. +
+
+
+ + ÉTAPE 04 + + hypothèses + + + + MITIGATION + + solution +
+
+
+ {data.risks.map((r) => ( + + ))} +
+
+ Cliquez sur l'en-tête d'un risque pour replier/déplier sa carte. +
+
+ + {/* Synthèse plus/moins critique */} +
+
+
+
+
+
+ +
+
+ PLUS CRITIQUE · SCORE {maxScore} +
+
+ {scored + .filter((r) => r.score === maxScore) + .map((r) => ( +
+ {r.id} + {r.title} +
+ ))} +
+
+
+
+
+
+
+ +
+
+ MOINS CRITIQUE · SCORE {minScore} +
+
+ {scored + .filter((r) => r.score === minScore && !highestIds.includes(r.id)) + .map((r) => ( +
+ {r.id} + {r.title} +
+ ))} +
+
+
+ + {/* Matrice avec positionnement des risques */} +
+ POSITIONNEMENT / MATRICE P × I +
+ +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// CASE DETAIL VIEW +// ───────────────────────────────────────────────────────────── + +function CriticalityBadge({ score, level, color, bg }) { + const tiltRef = useTilt(12); + return ( +
+
+
+
Criticité
+
+ {score} +
+
+ {level} +
+
+
+ ); +} + +function CaseHero({ data }) { + const crit = getCriticality(data.probability, data.impact); + + return ( +
+
+
+
+
+
+ + + CASE FILE + + + {data.codename} + +
+

+ {data.title}{" "} + / {data.year} +

+

{data.subtitle}

+
+ +
+
+ {data.stats.map((s) => ( + + ))} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// SUB-NAVIGATION (sous-onglets internes au cas) +// ───────────────────────────────────────────────────────────── + +function SubTabs({ current, onChange, options, accent = "#facc15" }) { + return ( + + ); +} + +// ───────────────────────────────────────────────────────────── +// DOSSIER D'INCIDENT (page 1 par cas) +// ───────────────────────────────────────────────────────────── + +function CaseDossier({ data }) { + return ( +
+
+
+

+ Dossier technique de l'incident{" "} + {data.title} ({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. +

+
+ +
+
+ {data.timeline.map((t) => ( + + ))} +
+
+ +
+
+ {data.why.map((w, i) => ( + + ))} +
+
+ +
+
+ {data.problems.map((p, i) => ( +
+
+
+ +
+ {p} +
+ ))} +
+
+ +
+
+ {data.solutions.map((s, i) => ( + + ))} +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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 ( +
+
+
+
+ + {value} + +
+ ); +} + +function AMDECRow({ row, rank }) { + const ipr = row.severity * row.occurrence * row.detection; + const level = getIPRLevel(ipr); + const [open, setOpen] = useState(rank === 1); + const [ref, isVisible] = useScrollReveal(); + + return ( +
+ + + {open && ( +
+
+
+ FONCTION +
+

{row.function}

+
+
+
+ MODE DE DÉFAILLANCE +
+

{row.failureMode}

+
+
+
+ CAUSE +
+

{row.cause}

+
+
+
+ EFFET +
+

{row.effect}

+
+ +
+
+
+
GRAVITÉ
+
{row.severity}/10
+
+
+
OCCURRENCE
+
{row.occurrence}/10
+
+
+
DÉTECTION
+
{row.detection}/10
+
+
+
+ IPR = G × O × D = + {ipr} + · NIVEAU {level.label} +
+
+ +
+
+ + ACTION CORRECTIVE + +
+
+ +

+ {row.action} +

+
+
+
+ )} +
+ ); +} + +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 ( +
+ {/* Briefing pédagogique */} +
+
+ MÉTHODOLOGIE / ACTIVITÉ 02 +
+

AMDEC — FMEA

+

+ Analyse des{" "} + Modes de{" "} + Défaillance, de leurs{" "} + Effets et de leur{" "} + Criticité. 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 : +

+
+
+
G — GRAVITÉ
+
+ Sévérité de l'effet sur le système / l'utilisateur final. 1 = imperceptible, 10 = + critique (danger vital). +
+
+
+
O — OCCURRENCE
+
+ Fréquence d'apparition de la défaillance en conditions réelles. 1 = très rare, 10 = + quasi-certaine. +
+
+
+
D — DÉTECTION
+
+ Difficulté à détecter la défaillance avant impact. 1 = détection immédiate, 10 = + indétectable. +
+
+
+
+
+ IPR = G × O × D + (Indice de Priorité du Risque, max 1000) +
+
+ {[ + { 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) => ( +
+
{l.lvl}
+
{l.range}
+
+ ))} +
+
+
+ + {/* Tableau AMDEC */} +
+
+ TABLEAU AMDEC / {sortedRows.length} MODES DE DÉFAILLANCE (TRIÉS PAR IPR DESC) +
+
+ {sortedRows.map((row, idx) => ( + + ))} +
+
+ Cliquez sur une ligne pour déplier ses détails (fonction, cause, effet, action corrective). +
+
+ + {/* Synthèse */} +
+
+ PRIORITÉ MAXIMALE / IPR = {sortedRows[0].ipr} +
+
+ {sortedRows[0].id} · {sortedRows[0].component} +
+

{sortedRows[0].failureMode}

+

→ {sortedRows[0].action}

+
+ + {/* Échelles */} +
+ {[ + { 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) => ( +
+
+ {s.title} +
+
+ {AMDEC_SCALES[s.key].map((l) => ( +
+ {l.range} +
+
{l.label}
+
{l.desc}
+
+
+ ))} +
+
+ ))} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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); + const tiltRef = useTilt(15); + return ( +
+
+
+
CVSS {version}
+
+ {score.toFixed(1)} +
+
+
+
+ {level.label} +
+
+ ); +} + +function CVSSMetric({ metric }) { + return ( +
+
+ + {metric.key} + + + {metric.label.toUpperCase()} + +
+
+ {metric.value} + · {metric.valueLabel} +
+

{metric.detail}

+
+ ); +} + +function CVSSPanel({ cvss, version, isOfficial }) { + return ( +
+
+
+
+ + CVSS {version} + + {isOfficial ? ( + + OFFICIEL · NVD + + ) : ( + + RÉINTERPRÉTATION PÉDAGOGIQUE + + )} +
+
{cvss.vector}
+ {cvss.note && ( +

{cvss.note}

+ )} +
+ +
+
+ {cvss.metrics.map((m) => ( + + ))} +
+
+ ); +} + +function CVEView({ data }) { + const cve = data.cves[0]; + + return ( +
+ {/* Briefing pédagogique */} +
+
+ STANDARDS / ACTIVITÉ 03 +
+

CVE & CVSS

+

+ CVE (Common Vulnerabilities and + Exposures) attribue un identifiant unique à chaque vulnérabilité publiée, géré par MITRE.{" "} + CVSS (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. +

+
+ + {/* Header CVE */} +
+
+
+
+ VULNÉRABILITÉ RÉFÉRENCÉE +
+

+ {cve.id} +

+

{cve.title}

+
+
+ Publié : {cve.publishedDate} +
+
+
+
+
+ ADVISORIES +
+
+ {cve.advisories.map((a) => ( +
+ {a.id} + · + {a.source} +
+ ))} +
+
+
+
+ PRODUITS AFFECTÉS +
+

{cve.affected}

+
+
+
+ + {/* Description et chaîne d'attaque */} +
+

{cve.description}

+
+ CHAÎNE D'ATTAQUE +
+
+ {cve.attackChain.map((step, i) => ( +
+ + {String(i + 1).padStart(2, "0")} + + {step} +
+ ))} +
+
+ + {/* Scoring CVSS — v2 officiel + v3 pédagogique */} +
+

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

+
+ + +
+ + {/* Comparaison */} +
+ Différence v2 vs v3 : + La métrique Scope 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 à{" "} + Changed (C) 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 : la propagation d'impact compte autant que l'impact local. +
+
+ + {/* Remediation */} +
+
    + {cve.remediation.map((r, i) => ( +
  • + + {r} +
  • + ))} +
+
+ + {/* Références */} +
+
+ {cve.references.map((ref) => ( + +
+ {ref.label} +
+
{ref.url}
+
+ ))} +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// RTO / RPO / PCA-PRA — Plan de Continuité d'Activité +// ───────────────────────────────────────────────────────────── + +const CONTINUITY_STEPS = [ + { n: 1, title: "Composant critique", text: "Identifier le composant le plus critique du système." }, + { n: 2, title: "Définir le RTO", text: "RTO réaliste et justifié dans le contexte de l'incident réel." }, + { n: 3, title: "Définir le RPO", text: "RPO réaliste et justifié selon les données en jeu." }, + { n: 4, title: "Mesure de continuité", text: "Proposer une mesure concrète, technique ou organisationnelle." }, + { n: 5, title: "Analyse rétrospective", text: "Un PCA/PRA existait-il lors de l'incident réel ?" }, +]; + +const DEFINITIONS = [ + { + abbr: "PCA", + full: "Plan de Continuité d'Activité", + desc: "Ensemble de procédures permettant à l'organisation de maintenir ses activités essentielles pendant un sinistre.", + icon: Shield, + color: "#22c55e", + }, + { + abbr: "PRA", + full: "Plan de Reprise d'Activité", + desc: "Ensemble de procédures pour remettre en service les systèmes après sinistre, dans des délais et avec des pertes de données acceptables.", + icon: RefreshCw, + color: "#3b82f6", + }, + { + abbr: "RTO", + full: "Recovery Time Objective", + desc: "Durée maximale acceptable d'interruption de service après sinistre.", + icon: Timer, + color: "#f97316", + }, + { + abbr: "RPO", + full: "Recovery Point Objective", + desc: "Quantité maximale acceptable de données perdues, mesurée en temps avant le sinistre (≃ fréquence de sauvegarde minimale).", + icon: Database, + color: "#eab308", + }, +]; + +function ObjectiveGauge({ label, target, reality, color, icon: Icon }) { + return ( +
+
+
+
+
+ +
+
+
+ {label} +
+
{target}
+
+
+ +
+
+
+ + RÉALITÉ 2015 +
+

{reality}

+
+
+
+
+ ); +} + +function ContinuityMeasureCard({ measure, idx }) { + const isOrg = measure.type === "ORGANISATIONNELLE"; + const color = isOrg ? "#a78bfa" : "#22c55e"; + const Icon = isOrg ? Users : Server; + + return ( +
+
+
+ +
+
+
+ + {measure.type} + +

{measure.title}

+
+

{measure.description}

+
+
+
+ ); +} + +function ConsequenceCard({ consequence, idx }) { + return ( +
+
+ +
+

{consequence.title}

+

{consequence.detail}

+
+
+
+ ); +} + +function LessonTimeline({ lessons }) { + return ( +
+
+ {lessons.map((lesson, i) => ( +
+
+
+ {lesson.year} +

{lesson.title}

+
+

{lesson.detail}

+
+ ))} +
+ ); +} + +function ContinuityView({ data }) { + const cont = data.continuity; + const [activeSection, setActiveSection] = useState("component"); + + const sections = [ + { id: "component", label: "Composant critique", icon: Target, code: "01" }, + { id: "rto", label: "RTO", icon: Timer, code: "02" }, + { id: "rpo", label: "RPO", icon: Database, code: "03" }, + { id: "measures", label: "Mesures", icon: Shield, code: "04" }, + { id: "retrospective", label: "Rétrospective", icon: Clock, code: "05" }, + ]; + + return ( +
+ {/* Briefing pédagogique */} +
+
+ MÉTHODOLOGIE / ACTIVITÉ 03 +
+

+ RTO / RPO / PCA-PRA +

+

+ À partir du cas fil rouge assigné ({data.title}, {data.year}), réalisez + les étapes suivantes pour construire un Plan de Continuité d'Activité (PCA) + et un Plan de Reprise d'Activité (PRA) : +

+
+ {CONTINUITY_STEPS.map((s) => ( + + ))} +
+ + {/* Définitions clés */} +
+ {DEFINITIONS.map((d) => ( +
+
+ + + {d.abbr} + +
+
{d.full}
+

{d.desc}

+
+ ))} +
+
+ + {/* Mini-navigation interne */} +
+ {sections.map((s) => ( + + ))} +
+ + {/* Section 1 — Composant critique */} + {activeSection === "component" && ( +
+
+
+
+
+ +
+
+
COMPOSANT IDENTIFIÉ
+

{cont.criticalComponent.name}

+
+
+

+ {cont.criticalComponent.rationale} +

+
+ +
+ COMPOSANTS ALTERNATIFS ENVISAGÉS (ET POURQUOI ILS SONT MOINS CRITIQUES AU SENS BCP) +
+
+ {cont.criticalComponent.alternatives.map((alt, i) => ( +
+
+ +
+
+

{alt.name}

+

{alt.reason}

+
+
+ ))} +
+
+
+ )} + + {/* Section 2 — RTO */} + {activeSection === "rto" && ( +
+
+ + +
+ Justification : + {cont.rto.justification} +
+ +
+
+ RTO PAR PÉRIMÈTRE — COMPARAISON 2015 vs CIBLE MODERNE +
+ {/* Mobile: vertical cards */} +
+ {cont.rto.breakdown.map((row, i) => ( +
+
{row.scope}
+
+
+ 2015 : + {row.reality2015} +
+ +
+ Cible : + {row.targetModern} +
+
+
+ ))} +
+ {/* Desktop: table */} +
+ + + + + + + + + + {cont.rto.breakdown.map((row, i) => ( + + + + + + ))} + +
PÉRIMÈTRERÉALITÉ 2015CIBLE MODERNE
{row.scope} + {row.reality2015} + + {row.targetModern} +
+
+
+
+
+ )} + + {/* Section 3 — RPO */} + {activeSection === "rpo" && ( +
+
+ + +
+
+ RPO PAR TYPE DE DONNÉE +
+
+ {cont.rpo.breakdown.map((row, i) => ( +
+
+ + {row.dataType} +
+
{row.target}
+

{row.justification}

+
+ ))} +
+
+ +
+
+ +
+ Hypothèses du RPO : + {cont.rpo.hypotheses} +
+
+
+
+
+ )} + + {/* Section 4 — Mesures de continuité */} + {activeSection === "measures" && ( +
+
+
+
+ MESURE PRIORITAIRE +
+
+
+ +
+
+

OTA Uptane

+

+ Si une seule mesure devait être retenue : sans OTA, aucune des autres mesures ne peut atteindre son RTO. +

+
+
+
+ +
+ ENSEMBLE DES MESURES / {cont.measures.length} MESURES IDENTIFIÉES +
+
+ {cont.measures.map((m, i) => ( + + ))} +
+
+
+ )} + + {/* Section 5 — Rétrospective */} + {activeSection === "retrospective" && ( +
+
+ {/* Verdict */} +
+
+
+ +
+
+
+ PCA/PRA CYBERSÉCURITÉ CHEZ FCA EN 2015 +
+

INEXISTANT

+
+
+

+ {cont.retrospective.description} +

+
+ + {/* Conséquences */} +
+
+ CONSÉQUENCES CONCRÈTES DE L'ABSENCE DE PCA/PRA / {cont.retrospective.consequences.length} IMPACTS +
+
+ {cont.retrospective.consequences.map((c, i) => ( + + ))} +
+
+ + {/* Leçons retenues */} +
+
+ LEÇONS RETENUES ET CHANGEMENTS POST-INCIDENT +
+ +
+ + {/* Conclusion */} +
+
+
+
+
CONCLUSION
+

+ {cont.retrospective.conclusion} +

+
+
+
+
+
+ )} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// SYNTHÈSE — 6 piliers de sécurité IoT +// ───────────────────────────────────────────────────────────── + +const COVERAGE_MATRIX = { + chiffrement: { R1: false, R2: true, R3: false, R4: false, R5: false, "AMD-01": false, "AMD-02": true, "AMD-03": false, "AMD-04": false, "AMD-05": false, CVE: false }, + identites: { R1: false, R2: true, R3: false, R4: true, R5: false, "AMD-01": false, "AMD-02": true, "AMD-03": false, "AMD-04": false, "AMD-05": true, CVE: true }, + segmentation: { R1: true, R2: false, R3: false, R4: true, R5: false, "AMD-01": true, "AMD-02": false, "AMD-03": false, "AMD-04": true, "AMD-05": true, CVE: false }, + hardening: { R1: false, R2: true, R3: true, R4: false, R5: true, "AMD-01": false, "AMD-02": true, "AMD-03": true, "AMD-04": false, "AMD-05": false, CVE: false }, + maj: { R1: false, R2: false, R3: false, R4: false, R5: false, "AMD-01": false, "AMD-02": false, "AMD-03": false, "AMD-04": false, "AMD-05": false, CVE: false }, + supervision: { R1: true, R2: true, R3: true, R4: true, R5: true, "AMD-01": true, "AMD-02": true, "AMD-03": true, "AMD-04": true, "AMD-05": true, CVE: true }, +}; + +const PILLAR_NARRATIVES = { + chiffrement: { + statusLabel: "Défaillant", + statusEmoji: "❌", + problem: "Le service D-Bus exposé sur le port 6667 communiquait sans aucun chiffrement depuis le réseau cellulaire Sprint. Les communications internes au véhicule (CAN bus, SPI vers le V850) étaient en clair par conception. La cryptographie n'était utilisée nulle part dans la chaîne d'attaque.", + solution: "Imposer TLS mutuel sur tout port exposé au réseau. Chiffrer les communications entre la head unit et le V850 (SPI chiffré). Déployer SecOC (AUTOSAR) pour authentifier et chiffrer les trames CAN. Stocker les clés dans un HSM/TPM dédié côté véhicule.", + }, + identites: { + statusLabel: "Défaillant", + statusEmoji: "❌", + problem: "Le CVE-2015-5611 est littéralement intitulé \"Missing Authorization\". Le service D-Bus accepte des connexions non authentifiées (CVSS v2 : Au:N, v3 : PR:N). Le V850 accepte le reflashing sans vérifier l'identité du flasher. Les trames CAN ne portent aucune authentification — n'importe quel ECU compromis peut envoyer n'importe quelle commande.", + solution: "Authentification mutuelle (mTLS) sur le service D-Bus avec certificats véhicule. Vérification cryptographique de l'identité du flasher avant tout reflashing du V850. Déploiement de SecOC sur le CAN bus pour que chaque trame porte un MAC identifiant l'ECU émetteur — seuls les ECU habilités peuvent piloter un actuateur donné.", + }, + segmentation: { + statusLabel: "Défaillant (le plus défaillant)", + statusEmoji: "❌", + problem: "Aucune isolation L2/L3 entre clients Sprint : 1.4M véhicules mutuellement adressables depuis n'importe quel terminal du même opérateur. Architecture réseau interne au véhicule strictement plate, sans gateway filtrante entre l'infotainment (Uconnect) et le CAN bus de sécurité fonctionnelle (freins, direction). C'est l'absence de ce pilier qui rend l'attaque exploitable à l'échelle.", + solution: "Côté opérateur : cloisonner les véhicules sur un APN dédié avec isolation L2/L3 (pas de communication transverse entre véhicules). Côté véhicule : insérer une gateway CAN filtrante entre le domaine infotainment et le domaine safety-critical, avec liste blanche stricte des trames autorisées. Adopter une architecture en zones et conduits (IEC 62443 / ISO 21434).", + }, + hardening: { + statusLabel: "Défaillant", + statusEmoji: "❌", + problem: "Service D-Bus configuré pour écouter sur 0.0.0.0 au lieu de 127.0.0.1 (configuration par défaut conservée en production). Microcontrôleur V850 acceptant n'importe quel firmware via SPI, sans secure boot ni vérification cryptographique. Les défauts de hardening sont cumulatifs — chacun ouvre une porte, et leur addition forme la chaîne d'attaque complète.", + solution: "Bind D-Bus sur 127.0.0.1 uniquement. Implémenter le secure boot avec signature cryptographique (ECDSA P-256) sur le V850 et tous les ECU. Firewall iptables avec politique DROP par défaut sur la head unit. Fermer tous les ports non strictement nécessaires. Appliquer le principle of least functionality sur l'ensemble du stack embarqué.", + }, + maj: { + statusLabel: "Critique — Absent", + statusEmoji: "❌", + problem: "Pas de capacité OTA chez FCA en 2015. La remédiation a nécessité un rappel physique de 1.4M véhicules via clés USB postales (juillet → septembre 2015). Sans OTA, le RTO de la flotte explose à ~6 mois — transformant une vulnérabilité technique en désastre opérationnel et financier (~100M$).", + solution: "Déployer une infrastructure OTA basée sur Uptane (standard automotive résistant aux compromissions de serveur). Signature obligatoire de chaque update, rollback protection anti-downgrade, distribution progressive avec canary testing. Objectif : pouvoir patcher 95% de la flotte en moins de 72h après découverte d'une vulnérabilité critique.", + }, + supervision: { + statusLabel: "Absent", + statusEmoji: "❌", + problem: "Aucune télémétrie de sécurité côté véhicule, aucun V-SOC chez FCA, aucune procédure de détection d'intrusion. Les scans massifs de Miller & Valasek sur le réseau Sprint (probing du port 6667 sur des milliers d'IP) n'ont déclenché aucune alerte. La compromission du V850 et l'injection de trames CAN anormales sont restées totalement invisibles. Sans la divulgation responsable des chercheurs, l'attaque aurait pu être exploitée à grande échelle sans jamais être détectée.", + solution: "Monitoring réseau côté Sprint : détecter les scans anormaux du port 6667 (pattern de reconnaissance automatisable). IDS embarqué sur le CAN bus : identifier les trames à fréquence ou contenu anormaux (un ECU infotainment qui envoie des commandes freins = alerte immédiate). V-SOC centralisé recevant la télémétrie sécurité de la flotte en temps réel. Vérification d'intégrité firmware au boot de chaque ECU avec remontée d'alerte en cas d'anomalie. Adhésion à l'Auto-ISAC pour le partage d'IoC entre constructeurs.", + }, +}; + +function SynthesisView({ data }) { + const synthesis = data.synthesis; + const [expandedPillar, setExpandedPillar] = useState(null); + + const togglePillar = (key) => { + setExpandedPillar(expandedPillar === key ? null : key); + }; + + const matrixCols = ["R1", "R2", "R3", "R4", "R5", "AMD-01", "AMD-02", "AMD-03", "AMD-04", "AMD-05", "CVE"]; + + return ( +
+ {/* Briefing intro */} +
+
+ SYNTHÈSE / FRAMEWORK 6 PILIERS +
+

+ Synthèse — 6 piliers de sécurité IoT et familles de risques +

+
+

+ Chaque pilier adresse des familles de risques spécifiques. La supervision est la seule couche transversale du modèle : elle adresse les 4 familles à la fois (Réseau, Sécurité, Humain, Logiciel). +

+

+ Cette page identifie, pour chaque pilier, les problèmes constatés dans le cas Jeep Cherokee 2015 et les solutions qui auraient pu être mises en place. C'est la grille de lecture qui transforme une étude de cas en outil d'analyse réutilisable pour n'importe quel système IoT. +

+
+
+ + {/* 4 familles de risques */} + +
+ FAMILLES DE RISQUES / 4 CATÉGORIES +
+
+ {RISK_FAMILIES.map((f) => ( +
+
+
+

{f.key}

+
+

{f.description}

+
+ {PILLARS.filter((p) => p.riskFamilies.includes(f.key)).map((p) => ( + + ))} +
+
+ ))} +
+ + + {/* Diagramme des 6 piliers */} + +
+ FRAMEWORK / 6 PILIERS DE SÉCURITÉ IoT +
+
+ {/* 5 piliers preventifs en grille */} +
+ {PILLARS.filter((p) => !p.isTransversal).map((pillar) => { + const Icon = PILLAR_ICONS[pillar.icon]; + const status = synthesis.pillarsStatus[pillar.key]; + return ( +
+
+
+ +
+
+

{pillar.label}

+
+ + {status ? (status.status === "absent" ? "ABSENT" : "DÉFAILLANT") : "N/A"} + +
+
+
+

{pillar.description}

+
+ {pillar.riskFamilies.map((f) => ( + {f} + ))} +
+
+ ); + })} +
+ + {/* Supervision — transversale */} + {(() => { + const sup = PILLARS.find((p) => p.isTransversal); + const SupIcon = PILLAR_ICONS[sup.icon]; + return ( +
+
+ TRANSVERSALE +
+
+
+ +
+
+

{sup.label}

+
+ ABSENT + | + Adresse les 4 familles +
+
+
+

{sup.description}

+
+ {sup.riskFamilies.map((f) => ( + {f} + ))} +
+
+ ); + })()} +
+
+ + {/* 6 sections — une par pilier */} + +
+ ANALYSE PAR PILIER / PROBLÈMES ET SOLUTIONS +
+
+ {PILLARS.map((pillar, idx) => { + const Icon = PILLAR_ICONS[pillar.icon]; + const status = synthesis.pillarsStatus[pillar.key]; + const narr = PILLAR_NARRATIVES[pillar.key]; + const isOpen = expandedPillar === pillar.key; + const relatedItems = status.relatedItems === "all" + ? matrixCols + : status.relatedItems || []; + + return ( +
+ + + {isOpen && ( +
+ {/* Problème identifié */} +
+
+
+ +
+
+ PROBLÈME IDENTIFIÉ +
+
+

+ {narr.problem} +

+
+ + {/* Items liés */} + {relatedItems.length > 0 && ( +
+
+ ITEMS LIÉS +
+
+ {relatedItems.map((item) => ( + + {item} + + ))} +
+
+ )} + {status.relatedPage && ( +
+ Voir aussi : page {status.relatedPage} +
+ )} + + {/* Solution proposée */} +
+
+
+
+ +
+
+ SOLUTION PROPOSÉE +
+
+

+ {narr.solution} +

+
+
+ )} +
+ ); + })} +
+ + + {/* Tableau de couverture */} + +
+ MATRICE DE COUVERTURE / PILIERS × ITEMS +
+
+ + + + + {matrixCols.map((col) => ( + + ))} + + + + {PILLARS.map((pillar) => { + const Icon = PILLAR_ICONS[pillar.icon]; + const row = COVERAGE_MATRIX[pillar.key]; + return ( + + + {matrixCols.map((col) => ( + + ))} + + ); + })} + +
PILIER + {col} +
+
+ + + {pillar.label} + +
+
+ {row[col] ? ( + + ✕ + + ) : ( + + )} +
+
+ La ligne Supervision est entièrement cochée (couche transversale = défaillance générale). La ligne MAJ est vide car c'est un pilier organisationnel constructeur, traité dans la page Continuité. +
+
+
+ + {/* Conclusion finale */} + +
+
+
+
+
+
+
+ CONCLUSION / FRAMEWORK 6 PILIERS +
+

+ Pourquoi 5 piliers + 1 transversal, et pas 6 piliers à plat ? +

+
+

+ Le modèle place volontairement la Supervision à part : c'est le seul pilier qui adresse simultanément les 4 familles de risques (Réseau, Sécurité, Humain, Logiciel). C'est aussi le seul pilier réactif — les cinq autres sont des mesures préventives. La supervision ne prévient pas une attaque, mais elle la détecte et permet d'y répondre. +

+

+ Dans le cas Jeep, les 6 piliers étaient simultanément défaillants — c'est pourquoi l'attaque a pu réussir, persister 9 mois sans détection (entre la divulgation responsable à FCA et la publication à Black Hat), et coûter ~100M$ à remédier. Une organisation qui investirait correctement dans ne serait-ce qu'un seul des piliers — par exemple la supervision seule — aurait probablement détecté Miller & Valasek dès leurs premiers scans Sprint et stoppé l'incident bien avant 1.4M de véhicules rappelés. +

+
+
+ + GRILLE DE LECTURE PORTABLE +
+

+ Ce modèle à 6 piliers se réapplique à n'importe quel système IoT — capteurs industriels, objets connectés grand public, infrastructure smart city, IoMT médical. C'est l'aboutissement pédagogique du dossier Jeep : un cas particulier transformé en méthode d'analyse réutilisable. +

+
+
+
+
+
+
+ +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 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`, + }, + { + id: "continuite", + label: "Continuité", + icon: RefreshCw, + count: "PCA/PRA", + }, + { + id: "synthese", + label: "Synthèse", + icon: Eye, + count: "6 piliers", + }, + ]; + + return ( +
+ + +
+ {subTab === "dossier" && } + {subTab === "analyse" && } + {subTab === "amdec" && } + {subTab === "cve" && } + {subTab === "continuite" && } + {subTab === "synthese" && } +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// MAIN +// ───────────────────────────────────────────────────────────── + +export default function IoTRiskBriefing() { + return ( +
+
+ {/* Header */} +
+
+
+
+
+
+
+ +
+ + IoT RISK ANALYSIS / CASE STUDY · JEEP CHEROKEE 2015 + +
+ + PÉDAGOGIQUE + +
+

+ Méthodologies d'analyse des risques IoT +

+

+ Étude du Jeep Cherokee Hack (2015) — + dossier d'incident, analyse de risques par{" "} + matrice P × I, AMDEC formelle et + référencement CVE / CVSS. Trois méthodologies complémentaires appliquées + au même cas pour rendre l'analyse reproductible et justifiable. +

+
+
+ + {/* Contenu */} +
+ +
+ + {/* Footer */} +
+
+
+ IOT-RISK-BRIEFING / v3.0 / 2026 +
+ + P × I · AMDEC · CVE-2015-5611 · RTO/RPO · PCA-PRA · 6 Piliers + +
+
+
+ ); +} diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 0000000..185da22 --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,159 @@ +import { useState, useEffect, useRef, useCallback } from "react"; + +export function useScrollReveal(options = {}) { + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + observer.unobserve(el); + } + }, + { threshold: options.threshold ?? 0.15, rootMargin: options.rootMargin ?? "0px" } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return [ref, isVisible]; +} + +export function useTilt(intensity = 8) { + const ref = useRef(null); + + const handleMove = useCallback( + (e) => { + const el = ref.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width - 0.5; + const y = (e.clientY - rect.top) / rect.height - 0.5; + el.style.transform = `perspective(800px) rotateY(${x * intensity}deg) rotateX(${-y * intensity}deg) scale3d(1.02, 1.02, 1.02)`; + el.style.transition = "transform 0.1s ease-out"; + }, + [intensity] + ); + + const handleLeave = useCallback(() => { + const el = ref.current; + if (!el) return; + el.style.transform = "perspective(800px) rotateY(0deg) rotateX(0deg) scale3d(1, 1, 1)"; + el.style.transition = "transform 0.5s ease-out"; + }, []); + + useEffect(() => { + const el = ref.current; + if (!el) return; + el.addEventListener("mousemove", handleMove); + el.addEventListener("mouseleave", handleLeave); + return () => { + el.removeEventListener("mousemove", handleMove); + el.removeEventListener("mouseleave", handleLeave); + }; + }, [handleMove, handleLeave]); + + return ref; +} + +export function useCountUp(target, duration = 1800, startOnVisible = true) { + const [value, setValue] = useState(0); + const [started, setStarted] = useState(!startOnVisible); + const ref = useRef(null); + + useEffect(() => { + if (!startOnVisible) return; + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setStarted(true); + observer.unobserve(el); + } + }, + { threshold: 0.3 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, [startOnVisible]); + + useEffect(() => { + if (!started) return; + + const num = parseFloat(String(target).replace(/[^0-9.]/g, "")); + if (isNaN(num) || num === 0) { + setValue(target); + return; + } + + const startTime = performance.now(); + const step = (now) => { + const elapsed = now - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + setValue(Math.floor(num * eased)); + if (progress < 1) requestAnimationFrame(step); + else setValue(num); + }; + requestAnimationFrame(step); + }, [started, target, duration]); + + return [ref, value]; +} + +export function useMousePosition() { + const [pos, setPos] = useState({ x: 0, y: 0 }); + + useEffect(() => { + const handler = (e) => setPos({ x: e.clientX, y: e.clientY }); + window.addEventListener("mousemove", handler); + return () => window.removeEventListener("mousemove", handler); + }, []); + + return pos; +} + +export function useRipple() { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const handler = (e) => { + const rect = el.getBoundingClientRect(); + const ripple = document.createElement("span"); + const size = Math.max(rect.width, rect.height) * 2; + ripple.style.cssText = ` + position: absolute; + width: ${size}px; + height: ${size}px; + left: ${e.clientX - rect.left - size / 2}px; + top: ${e.clientY - rect.top - size / 2}px; + background: radial-gradient(circle, rgba(251,191,36,0.3) 0%, transparent 70%); + border-radius: 50%; + transform: scale(0); + animation: ripple-expand 0.6s ease-out forwards; + pointer-events: none; + z-index: 0; + `; + el.style.position = "relative"; + el.style.overflow = "hidden"; + el.appendChild(ripple); + setTimeout(() => ripple.remove(), 600); + }; + + el.addEventListener("click", handler); + return () => el.removeEventListener("click", handler); + }, []); + + return ref; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..3b767c4 --- /dev/null +++ b/src/index.css @@ -0,0 +1,491 @@ +@import "tailwindcss"; + +@theme { + --font-sans: 'Plus Jakarta Sans', system-ui, sans-serif; + --font-mono: 'DM Mono', ui-monospace, Consolas, monospace; + --font-display: 'Space Grotesk', system-ui, sans-serif; +} + +/* ═══════════════════════════════════════════════════ + DESIGN TOKENS — SIGINT Terminal + ═══════════════════════════════════════════════════ */ +:root { + /* Base surfaces — deep navy, NOT pure black */ + --surface-0: #060810; + --surface-1: #0b0d18; + --surface-2: #111422; + --surface-3: #171a2c; + --surface-4: #1e2238; + + /* Card surfaces — VISIBLE elevation */ + --card-bg: #131628; + --card-bg-strong: #161a30; + --card-bg-hover: #1a1e36; + --card-border: rgba(140, 150, 255, 0.08); + --card-border-hover: rgba(140, 150, 255, 0.16); + + /* Borders */ + --border-subtle: rgba(140, 150, 255, 0.06); + --border-default: rgba(140, 150, 255, 0.10); + --border-strong: rgba(140, 150, 255, 0.18); + + /* Accent glow colors — VISIBLE */ + --glow-amber: rgba(251, 191, 36, 0.35); + --glow-amber-soft: rgba(251, 191, 36, 0.12); + --glow-red: rgba(248, 80, 80, 0.30); + --glow-green: rgba(52, 211, 153, 0.30); +} + +/* ═══════════════════════════════════════════════════ + KEYFRAMES + ═══════════════════════════════════════════════════ */ + +@keyframes grid-scroll { + 0% { transform: translateY(0); } + 100% { transform: translateY(60px); } +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +@keyframes scan-line { + 0% { top: -2px; } + 100% { top: 100%; } +} + +@keyframes fadein { + from { opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadein-scale { + from { opacity: 0; transform: scale(0.96) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +@keyframes slide-up-big { + from { opacity: 0; transform: translateY(28px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes border-glow { + 0%, 100% { box-shadow: 0 0 0 rgba(251, 191, 36, 0), 0 4px 24px rgba(0,0,0,0.4); } + 50% { box-shadow: 0 0 28px rgba(251, 191, 36, 0.12), 0 4px 24px rgba(0,0,0,0.4); } +} + +@keyframes radar-sweep { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes particle-drift { + 0% { transform: translateY(0) translateX(0) scale(0); opacity: 0; } + 5% { transform: scale(1); opacity: 0.7; } + 50% { opacity: 0.4; } + 100% { transform: translateY(-100vh) translateX(var(--drift-x, 20px)) scale(0); opacity: 0; } +} + +@keyframes ripple-expand { + from { transform: scale(0); opacity: 1; } + to { transform: scale(1); opacity: 0; } +} + +@keyframes glitch-1 { + 0%, 100% { clip-path: inset(0); transform: translate(0); } + 20% { clip-path: inset(20% 0 60% 0); transform: translate(-2px, 1px); } + 40% { clip-path: inset(40% 0 30% 0); transform: translate(2px, -1px); } + 60% { clip-path: inset(70% 0 10% 0); transform: translate(-1px, 1px); } + 80% { clip-path: inset(10% 0 80% 0); transform: translate(1px, -1px); } +} + +@keyframes glitch-2 { + 0%, 100% { clip-path: inset(0); transform: translate(0); } + 20% { clip-path: inset(60% 0 10% 0); transform: translate(2px, -1px); } + 40% { clip-path: inset(10% 0 70% 0); transform: translate(-2px, 1px); } + 60% { clip-path: inset(30% 0 40% 0); transform: translate(1px, -1px); } + 80% { clip-path: inset(80% 0 5% 0); transform: translate(-1px, 1px); } +} + +@keyframes glitch-skew { + 0%, 100% { transform: skewX(0deg); } + 30% { transform: skewX(-0.7deg); } + 70% { transform: skewX(0.4deg); } +} + +@keyframes pulse-ring { + 0% { transform: scale(0.95); opacity: 0.5; } + 100% { transform: scale(1.5); opacity: 0; } +} + +@keyframes morph-blob { + 0%, 100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: rotate(0deg) scale(1); } + 25% { border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; transform: rotate(90deg) scale(1.05); } + 50% { border-radius: 50% 60% 30% 60% / 30% 60% 70% 40%; transform: rotate(180deg) scale(0.97); } + 75% { border-radius: 60% 30% 60% 40% / 70% 40% 50% 60%; transform: rotate(270deg) scale(1.03); } +} + +@keyframes data-flow { + 0% { background-position: 0% 0%; } + 100% { background-position: 0% 100%; } +} + +@keyframes scroll-reveal { + from { opacity: 0; transform: translateY(20px); filter: blur(2px); } + to { opacity: 1; transform: translateY(0); filter: blur(0); } +} + +@keyframes score-pop { + 0% { transform: scale(0.5); opacity: 0; } + 60% { transform: scale(1.08); } + 100% { transform: scale(1); opacity: 1; } +} + +@keyframes warning-flash { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes accent-line-shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } +} + +/* ═══════════════════════════════════════════════════ + BASE & BODY + ═══════════════════════════════════════════════════ */ + +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + background: var(--surface-0); + color: #c8cdd8; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: 0.01em; + line-height: 1.6; +} + +::selection { + background: rgba(251, 191, 36, 0.25); + color: #fff; +} + +/* Scrollbar — amber-tinted */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: var(--surface-0); } +::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, rgba(251,191,36,0.15), rgba(251,191,36,0.06)); + border-radius: 99px; +} +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, rgba(251,191,36,0.3), rgba(251,191,36,0.12)); +} + +/* ═══════════════════════════════════════════════════ + TYPOGRAPHY + ═══════════════════════════════════════════════════ */ + +.font-display { + font-family: 'Space Grotesk', system-ui, sans-serif; + letter-spacing: -0.02em; +} + +/* ═══════════════════════════════════════════════════ + ANIMATION UTILITIES + ═══════════════════════════════════════════════════ */ + +.animate-fadein { animation: fadein 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; } +.animate-fadein-scale { animation: fadein-scale 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; } +.animate-slide-up-big { animation: slide-up-big 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; } +.animate-float { animation: float 3.5s ease-in-out infinite; } +.animate-score-pop { animation: score-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; } +.animate-warning-flash { animation: warning-flash 2.5s ease-in-out infinite; } + +.animate-shimmer { + background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.05), transparent); + background-size: 200% 100%; + animation: shimmer 3s ease-in-out infinite; +} + +.animate-border-glow { animation: border-glow 3s ease-in-out infinite; } + +.scroll-hidden { opacity: 0; transform: translateY(20px); filter: blur(2px); } +.scroll-visible { animation: scroll-reveal 0.65s cubic-bezier(0.16, 1, 0.3, 1) both; } + +.stagger-children > * { animation: fadein 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; } +.stagger-children > *:nth-child(1) { animation-delay: 0ms; } +.stagger-children > *:nth-child(2) { animation-delay: 60ms; } +.stagger-children > *:nth-child(3) { animation-delay: 120ms; } +.stagger-children > *:nth-child(4) { animation-delay: 180ms; } +.stagger-children > *:nth-child(5) { animation-delay: 240ms; } +.stagger-children > *:nth-child(6) { animation-delay: 300ms; } +.stagger-children > *:nth-child(7) { animation-delay: 360ms; } +.stagger-children > *:nth-child(8) { animation-delay: 420ms; } +.stagger-children > *:nth-child(9) { animation-delay: 480ms; } +.stagger-children > *:nth-child(10) { animation-delay: 540ms; } + +/* ═══════════════════════════════════════════════════ + BACKGROUND LAYERS + ═══════════════════════════════════════════════════ */ + +.bg-grid { + position: fixed; inset: 0; z-index: 0; + pointer-events: none; overflow: hidden; +} + +.bg-grid::before { + content: ''; + position: absolute; inset: -60px; + background-image: + linear-gradient(rgba(160, 170, 255, 0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(160, 170, 255, 0.025) 1px, transparent 1px); + background-size: 60px 60px; + animation: grid-scroll 12s linear infinite; + mask-image: radial-gradient(ellipse at 50% 25%, black 20%, transparent 60%); + -webkit-mask-image: radial-gradient(ellipse at 50% 25%, black 20%, transparent 60%); +} + +.bg-grid::after { + content: ''; + position: absolute; inset: 0; + background: + radial-gradient(ellipse at 15% 0%, rgba(251, 191, 36, 0.08) 0%, transparent 40%), + radial-gradient(ellipse at 85% 90%, rgba(248, 80, 80, 0.04) 0%, transparent 35%), + radial-gradient(ellipse at 50% 40%, rgba(100, 110, 240, 0.03) 0%, transparent 45%); + animation: pulse-glow 8s ease-in-out infinite; +} + +.mouse-spotlight { + position: fixed; + width: clamp(250px, 35vw, 500px); + height: clamp(250px, 35vw, 500px); + border-radius: 50%; + pointer-events: none; z-index: 1; + transition: left 0.3s cubic-bezier(0.16, 1, 0.3, 1), top 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.5s; + background: radial-gradient(circle, rgba(251, 191, 36, 0.05) 0%, rgba(251, 191, 36, 0.015) 35%, transparent 65%); + transform: translate(-50%, -50%); +} + +.scan-line { + position: fixed; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.14) 40%, rgba(251, 191, 36, 0.14) 60%, transparent); + z-index: 1; pointer-events: none; + animation: scan-line 7s linear infinite; +} + +.particle { + position: fixed; border-radius: 50%; + pointer-events: none; z-index: 1; + animation: particle-drift linear infinite; + background: rgba(251, 191, 36, 0.5); + box-shadow: 0 0 8px rgba(251, 191, 36, 0.25); +} + +.morph-blob { + position: fixed; pointer-events: none; z-index: 0; + animation: morph-blob 20s ease-in-out infinite; + filter: blur(100px); +} + +.circuit-traces { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; } +.circuit-trace { + position: absolute; + background: linear-gradient(180deg, transparent, rgba(251, 191, 36, 0.05), transparent); + animation: data-flow 6s linear infinite; + background-size: 100% 200%; +} + +.noise-overlay { + position: fixed; inset: 0; z-index: 2; + pointer-events: none; opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 200px 200px; +} + +/* ═══════════════════════════════════════════════════ + SURFACE SYSTEM — THE KEY CHANGE + Cards are CLEARLY ELEVATED above background. + Navy-indigo card on deep space background = visible. + ═══════════════════════════════════════════════════ */ + +.glass { + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 16px; + box-shadow: + 0 2px 12px rgba(0,0,0,0.35), + 0 0 0 1px rgba(0,0,0,0.15) inset, + 0 1px 0 rgba(255,255,255,0.04) inset; + transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; +} +.glass:hover { + background: var(--card-bg-hover); + border-color: var(--card-border-hover); +} + +.glass-strong { + background: linear-gradient(160deg, var(--card-bg-strong) 0%, var(--card-bg) 100%); + border: 1px solid var(--border-strong); + border-radius: 20px; + box-shadow: + 0 10px 50px rgba(0,0,0,0.5), + 0 1px 0 rgba(255,255,255,0.06) inset, + 0 0 80px rgba(100, 110, 240, 0.02); +} + +.glass-subtle { + background: rgba(15, 17, 28, 0.8); + border: 1px solid var(--border-subtle); + border-radius: 14px; + box-shadow: 0 1px 4px rgba(0,0,0,0.2); +} + +/* ═══════════════════════════════════════════════════ + INTERACTIVE + ═══════════════════════════════════════════════════ */ + +.hover-lift { + border-radius: 16px; + transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.35s ease, border-color 0.3s ease, background 0.3s ease; +} +.hover-lift:hover { + transform: translateY(-3px); + box-shadow: + 0 20px 60px rgba(0,0,0,0.5), + 0 0 0 1px rgba(251, 191, 36, 0.1), + 0 0 40px rgba(251, 191, 36, 0.04); + border-color: rgba(251, 191, 36, 0.15) !important; +} + +.tilt-card { + transform-style: preserve-3d; + will-change: transform; + border-radius: 16px; +} + +.glow-hover { transition: text-shadow 0.4s ease, color 0.3s ease; } +.glow-hover:hover { + text-shadow: 0 0 20px rgba(251, 191, 36, 0.4), 0 0 50px rgba(251, 191, 36, 0.12); + color: #fef3c7; +} + +.press-effect { + transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease, box-shadow 0.2s ease; + position: relative; overflow: hidden; + border-radius: 12px; +} +.press-effect:active { transform: scale(0.97); } + +.glitch-text { position: relative; display: inline-block; } +.glitch-text::before, .glitch-text::after { + content: attr(data-text); + position: absolute; top: 0; left: 0; + width: 100%; height: 100%; opacity: 0; +} +.glitch-text:hover::before { animation: glitch-1 0.25s ease-in-out; color: #ff4444; opacity: 0.7; z-index: -1; } +.glitch-text:hover::after { animation: glitch-2 0.25s ease-in-out 0.03s; color: #34d399; opacity: 0.7; z-index: -1; } +.glitch-text:hover { animation: glitch-skew 0.25s ease-in-out; } + +.pulse-ring { position: relative; } +.pulse-ring::after { + content: ''; position: absolute; inset: -4px; + border: 1px solid currentColor; border-radius: inherit; + opacity: 0; animation: pulse-ring 3s ease-out infinite; +} + +.line-reveal { position: relative; overflow: hidden; } +.line-reveal::after { + content: ''; position: absolute; bottom: 0; left: 0; + width: 100%; height: 2px; + background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.5), transparent); + transform: translateX(-100%); + transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); +} +.line-reveal:hover::after { transform: translateX(0); } + +.radar-container { + position: relative; overflow: hidden; border-radius: 16px; + background: var(--card-bg); +} +.radar-container::before { + content: ''; position: absolute; top: 50%; left: 50%; + width: 150%; height: 150%; transform-origin: 0 0; + background: conic-gradient(from 0deg, transparent 0deg, rgba(251, 191, 36, 0.06) 25deg, transparent 50deg); + animation: radar-sweep 10s linear infinite; + pointer-events: none; z-index: 0; +} + +/* ═══════════════════════════════════════════════════ + ACCENT EDGES — VISIBLE luminous lines + ═══════════════════════════════════════════════════ */ + +.accent-edge { position: relative; overflow: hidden; } +.accent-edge::before { + content: ''; + position: absolute; top: 0; left: 5%; right: 5%; + height: 2px; + background: linear-gradient(90deg, transparent, var(--glow-amber), var(--glow-amber), transparent); + z-index: 2; + border-radius: 0 0 2px 2px; +} +.accent-edge::after { + content: ''; + position: absolute; top: 0; left: 15%; right: 15%; + height: 12px; + background: radial-gradient(ellipse at 50% 0%, rgba(251, 191, 36, 0.08) 0%, transparent 100%); + z-index: 1; + pointer-events: none; +} + +.accent-edge-red::before { + background: linear-gradient(90deg, transparent, var(--glow-red), var(--glow-red), transparent); +} +.accent-edge-red::after { + background: radial-gradient(ellipse at 50% 0%, rgba(248, 80, 80, 0.06) 0%, transparent 100%); +} + +.accent-edge-green::before { + background: linear-gradient(90deg, transparent, var(--glow-green), var(--glow-green), transparent); +} +.accent-edge-green::after { + background: radial-gradient(ellipse at 50% 0%, rgba(52, 211, 153, 0.06) 0%, transparent 100%); +} + +/* ═══════════════════════════════════════════════════ + PAGE LAYOUT — Rich background with depth + ═══════════════════════════════════════════════════ */ + +.page-wrapper { + min-height: 100vh; + background: + radial-gradient(ellipse at 50% -5%, rgba(251, 191, 36, 0.05) 0%, transparent 30%), + radial-gradient(ellipse at 0% 30%, rgba(80, 90, 200, 0.025) 0%, transparent 30%), + radial-gradient(ellipse at 100% 70%, rgba(248, 80, 80, 0.015) 0%, transparent 30%), + linear-gradient(180deg, #0c0e1a 0%, var(--surface-0) 12%, var(--surface-0) 100%); +} + +.page-inner { + max-width: 78rem; + margin: 0 auto; + padding: 1rem; +} +@media (min-width: 640px) { .page-inner { padding: 1.25rem 1.5rem; } } +@media (min-width: 768px) { .page-inner { padding: 2rem 2.5rem; } } +@media (min-width: 1024px) { .page-inner { padding: 2.5rem 3.5rem; } } +@media (min-width: 1280px) { .page-inner { padding: 3rem 4rem; } } diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..0616e59 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [react(), tailwindcss()], +})