import {
  Callout,
  Card,
  CardBody,
  CardHeader,
  Code,
  Divider,
  Grid,
  H1,
  H2,
  H3,
  Pill,
  Row,
  Stack,
  Stat,
  Table,
  Text,
  useHostTheme,
} from 'cursor/canvas';

type Severity = 'critique' | 'haute' | 'moyenne' | 'faible' | 'info';
type Status = 'open' | 'fixed';
type Category =
  | 'config'
  | 'auth'
  | 'autorisation'
  | 'xss'
  | 'csrf'
  | 'idor'
  | 'upload'
  | 'sqli'
  | 'logs'
  | 'session'
  | 'rate-limit'
  | 'redirect'
  | 'headers'
  | 'cleanup';

type Finding = {
  id: string;
  title: string;
  severity: Severity;
  status: Status;
  category: Category;
  scope: 'public' | 'admin' | 'global';
  files: string[];
  description: string;
  exploit: string;
  fix: string;
};

const findings: Finding[] = [
  // ========== CRITIQUE ==========
  {
    id: 'C1',
    title: 'APP_DEBUG=true et APP_ENV=local activés',
    severity: 'critique',
    status: 'open',
    category: 'config',
    scope: 'global',
    files: ['.env'],
    description:
      "Le mode debug Laravel est actif : toute exception affiche la stack trace, le code source, les variables (incluant secrets, requêtes SQL, paramètres POST, sessions).",
    exploit:
      "Provoquer une exception (ex. paramètre invalide) pour récupérer .env, mots de passe DB, clé Stripe, structure interne. C'est la voie d'entrée n°1 d'un pentest réussi.",
    fix: "En production : APP_DEBUG=false, APP_ENV=production, exécuter php artisan config:cache.",
  },
  {
    id: 'C2',
    title: 'Identifiants MySQL par défaut (root, sans mot de passe)',
    severity: 'critique',
    status: 'open',
    category: 'config',
    scope: 'global',
    files: ['.env'],
    description:
      "DB_USERNAME=root et DB_PASSWORD est vide. Combiné à APP_DEBUG, une exception SQL expose les credentials et toute la base.",
    exploit:
      "Si le serveur MySQL écoute sur 0.0.0.0 (ou si l'attaquant a un accès local via une autre faille), connexion immédiate avec root/(vide).",
    fix: "Créer un user MySQL applicatif dédié (privilèges restreints à la base sfer), changer DB_USERNAME / DB_PASSWORD.",
  },
  {
    id: 'C3',
    title: 'Clés Stripe / Pusher / Mailtrap commitées dans .env',
    severity: 'critique',
    status: 'open',
    category: 'config',
    scope: 'global',
    files: ['.env'],
    description:
      "Le .env est versionné avec PUSHER_APP_SECRET=167953db37666782c268, MAIL_PASSWORD=30ded60b400bad, STRIPE_SECRET=sk_test_..., et la clé live commentée juste à côté.",
    exploit:
      "Un commit historique (ou ce fichier visible dans /storage si mal configuré) donne accès à Pusher (broadcast malveillant), Mailtrap (lecture des e-mails de test), Stripe TEST (création de paiements factices liés au compte).",
    fix: "Révoquer toutes les clés exposées, retirer .env du repo (.gitignore), et purger l'historique Git (git filter-repo).",
  },
  {
    id: 'C4',
    title: 'Désactivation du 2FA admin sans confirmation par mot de passe',
    severity: 'critique',
    status: 'fixed',
    category: 'auth',
    scope: 'admin',
    files: [
      'app/Http/Controllers/Admin/Auth/Admin2FAController.php',
      'resources/views/admin/auth/2fa.blade.php',
    ],
    description:
      "La méthode disable() supprimait two_factor_secret / two_factor_recovery_codes / two_factor_confirmed_at sans aucune re-authentification. Idem pour regenerateRecoveryCodes().",
    exploit:
      "Session volée → DELETE /cp-admin/profile/security/2fa/disable → 2FA retiré, attaquant peut poser sa propre clé.",
    fix: "CORRIGÉ : disable() exige maintenant {password, code TOTP en cours}, regenerateRecoveryCodes() exige le password. Rate limiter (5 tentatives / 10 min) ajouté sur les deux. Le modal admin a été mis à jour avec les champs nécessaires.",
  },

  // ========== HAUTE ==========
  {
    id: 'H1',
    title: "TinyMCEController accepte l'upload de fichiers SVG",
    severity: 'haute',
    status: 'fixed',
    category: 'upload',
    scope: 'admin',
    files: [
      'app/Http/Controllers/Admin/TinyMCEController.php',
      'app/Http/Controllers/Admin/PartnersController.php',
    ],
    description:
      "Validation initiale : 'file' => 'required|image|mimes:jpeg,png,jpg,gif,svg,webp|max:2048'. Les SVG étaient stockés tels quels dans /storage/uploads/general puis liés via asset().",
    exploit:
      "Un SVG peut contenir <script>...</script> ou foreignObject. Servi avec Content-Type: image/svg+xml depuis le même domaine, il s'exécute en contexte authentifié → vol de session admin, action CSRF, etc.",
    fix: "CORRIGÉ : retrait de 'svg' des règles mimes dans TinyMCEController et PartnersController. Aucun SVG préexistant dans storage/app/public à purger.",
  },
  {
    id: 'H2',
    title: "AddressController hors namespace Admin (reclassé après vérification)",
    severity: 'faible',
    status: 'fixed',
    category: 'autorisation',
    scope: 'admin',
    files: ['app/Http/Controllers/Admin/AddressController.php', 'routes/admin.php'],
    description:
      "Le contrôleur était situé dans App\\Http\\Controllers\\ au lieu de App\\Http\\Controllers\\Admin\\. Vérification approfondie : les routes étaient déjà protégées par admin.module.permission, et la permission 'address.*' n'existant pas dans la table modules, seuls les super_admin pouvaient y accéder via Gate::before. Comportement correct mais intention non explicite et convention de namespace cassée.",
    exploit:
      "Pas d'exploit direct (super_admin uniquement). Risque uniquement si quelqu'un crée un jour le module 'address' en BDD : l'accès s'ouvrirait à tout admin avec address.* — d'où l'intérêt de l'expliciter avec role:super_admin.",
    fix: "1) Contrôleur déplacé dans App\\Http\\Controllers\\Admin\\AddressController. 2) Import mis à jour dans routes/admin.php. 3) Routes adresses encapsulées dans Route::middleware('role:super_admin')->group(...) pour rendre l'intention explicite et éviter tout futur bypass via la création du module 'address' en base.",
  },
  {
    id: 'H3',
    title: 'Open redirect potentiel via redirect_to du login membre',
    severity: 'haute',
    status: 'fixed',
    category: 'redirect',
    scope: 'public',
    files: [
      'app/Http/Controllers/Member/MemberLoginController.php',
      'app/Http/Requests/Member/MemberLoginRequest.php',
    ],
    description:
      "store() écrivait $data['redirect_to'] dans session('url.intended') sans valider que c'était une URL relative ou interne. redirect()->intended() suivait ensuite cette valeur.",
    exploit:
      "Email de phishing : https://sfer.test/connexion?redirect_to=https://evil.com/login. L'utilisateur se connecte, est redirigé vers evil.com qui imite Sfer. Crédibilise l'attaque puisque l'URL initiale est légitime.",
    fix: "MemberLoginRequest valide redirect_to via isSafeInternalUrl() : refuse scheme://, // protocol-relative, host, caractères de contrôle. MemberLoginController::sanitizeRedirectTo() ré-applique le filtre avant put('url.intended', ...) et avant rendu de la vue (defense in depth). Seul le drapeau métier 'member_trainings' et les chemins relatifs débutant par '/' sont conservés.",
  },
  {
    id: 'H4',
    title: 'Routes /survey/* publiques sans rate limiting',
    severity: 'haute',
    status: 'fixed',
    category: 'rate-limit',
    scope: 'public',
    files: ['routes/web.php', 'app/Http/Controllers/SurveyController.php'],
    description:
      "Les routes survey.show, survey.validate, survey.questions, survey.save, survey.submit étaient toutes publiques sans throttle. validateParticipant testait si email + qrToken correspondaient à une inscription confirmée et renvoyait des messages d'erreur distincts (oracle d'énumération).",
    exploit:
      "Énumération massive d'emails inscrits à une session : essayer hello@gmail.com, contact@xxx.be, etc., et observer la différence entre 'inscrit' / 'pas inscrit'. Aussi : spam des SurveySubmission, DoS du formulaire.",
    fix: "Throttle ajouté sur toutes les routes : POST validate/submit → 5/min, POST save → 30/min, GET → 60/min. Messages d'erreur de validateParticipant unifiés (générique : 'Impossible de valider votre participation...') pour fermer l'oracle. Payload responses[] borné à count(questions actives)+5 dans saveProgress et submitResponses pour éviter le DoS DB.",
  },
  {
    id: 'H5',
    title: 'AdminLoginController authentifie l\'admin avant 2FA',
    severity: 'haute',
    status: 'fixed',
    category: 'auth',
    scope: 'admin',
    files: ['app/Http/Controllers/Admin/Auth/AdminLoginController.php'],
    description:
      "attemptLogin() exécutait Auth::guard('admin')->login($admin) AVANT de vérifier le 2FA. Le contrôleur faisait ensuite Auth::guard('admin')->logout() en cas de 2FA requis. Pendant cette fenêtre, n'importe quel autre middleware/observer/event lisait l'admin authentifié.",
    exploit:
      "Une exception entre login() et logout() laissait une session admin valide sans 2FA. Tout listener Auth::login (audit, notifications, derniers logins) considérait l'admin connecté à tort. Aussi : event Login dispatché 2x.",
    fix: "attemptLogin() refactoré pour retourner ?Admin sans appeler Auth::login(). Auth::guard('admin')->login() n'est plus appelé qu'UNE seule fois : soit immédiatement si pas de 2FA, soit après validation TOTP. Ajout d'un TTL de 300s sur le challenge 2FA (admin.2fa_login_at) pour éviter qu'une session pré-authentifiée traîne. Vérification supplémentaire $admin->status au moment du challenge.",
  },
  {
    id: 'H6',
    title: 'createTranslationFile() sans contrôle de path traversal',
    severity: 'haute',
    status: 'fixed',
    category: 'upload',
    scope: 'admin',
    files: ['app/Http/Controllers/Admin/SettingsController.php:717-770'],
    description:
      "Cette méthode validait 'filename' avec regex /^[a-zA-Z0-9_\\/-]+$/ mais n'appelait pas $this->assertPathWithinLangDir() comme les autres méthodes (updateTranslations, addTranslationKey, deleteTranslationKey, deleteTranslationFile). Vulnérable aux symlinks et à la création récursive de dossiers profonds.",
    exploit:
      "Symlink lang/fr/x → /var/www/html/public/ puis filename=x/shell crée un .php hors lang/. Aussi : filename=a/b/c/d/e/f/g... crée des dossiers à profondeur arbitraire (DoS disque, pollution arborescente). Régression-prone si la regex est assouplie demain.",
    fix: "1) max:100 ajouté sur filename. 2) Profondeur limitée à 1 sous-dossier (substr_count('/') > 1 → 422). 3) assertPathWithinLangDir() appelé sur fileDir AVANT création, puis sur filePath APRÈS (anti-TOCTOU symlinks). 4) catch InvalidArgumentException → 422 explicite. Cohérence rétablie avec les 4 autres méthodes du contrôleur.",
  },
  {
    id: 'H7',
    title: "Module::create($request->all()) — mass assignment",
    severity: 'haute',
    status: 'fixed',
    category: 'autorisation',
    scope: 'admin',
    files: ['app/Http/Controllers/Admin/ModuleController.php', 'app/Models/Module.php'],
    description:
      "store() validait name + description puis faisait Module::create($request->all()). Le $fillable contenait slug + is_active, donc un POST avec slug=admin (ou is_active=1) écrivait ces champs. Or le hook created() crée 4 permissions Spatie basées sur le slug → privilege escalation entre admins via collision de permissions.",
    exploit:
      "POST name=Foo&slug=admin → permissions 'admin.view'/'admin.create'/'admin.edit'/'admin.delete' créées avec un slug arbitraire. Exception UniqueConstraintViolation si la permission existe déjà → état DB incohérent (Module créé sans permissions). Aussi : slug=foo/bar pollue les noms de permissions.",
    fix: "1) ModuleController::store/update : $validated au lieu de $request->all(), validation explicite de is_active, max:100/1000 sur les champs string. 2) Module $fillable réduit à [name, description, is_active] + $guarded=[id,slug] (defense in depth). 3) Slug auto-généré ET sanitisé via preg_replace whitelist [a-z0-9_]. 4) Hook created() utilise firstOrCreate (idempotent) + transaction. 5) Hook updating() sanitise le slug et gère les collisions de permissions. 6) update() régénère le slug via assignation directe (mass assignment bloqué).",
  },
  {
    id: 'H8',
    title: 'Route publique /test-helper exposée',
    severity: 'haute',
    status: 'fixed',
    category: 'cleanup',
    scope: 'public',
    files: ['routes/web.php'],
    description:
      "Route::get('/test-helper', fn() => MenuHelper::renderSferMenu('main')); était active dans web.php sous le groupe localisation, accessible à n'importe qui.",
    exploit:
      "Surface d'attaque inutile, leak potentiel de la structure du menu (noms de routes internes, hiérarchie admin). Indiquait aussi un manque d'hygiène déploiement.",
    fix: "Route supprimée du groupe de localisation. Import use App\\Helpers\\MenuHelper retiré (devenu inutile dans routes/web.php).",
  },

  // ========== MOYENNE ==========
  {
    id: 'M1',
    title: 'XSS attribut via value="{!! old(...) !!}" dans 6 vues admin',
    severity: 'moyenne',
    status: 'fixed',
    category: 'xss',
    scope: 'admin',
    files: [
      'resources/views/admin/users/send_email.blade.php',
      'resources/views/admin/trainings/add_edit.blade.php',
      'resources/views/admin/faqs/add_edit.blade.php',
      'resources/views/admin/teams/add_edit.blade.php',
      'resources/views/admin/testimonials/add_edit.blade.php',
      'resources/views/admin/pages/add_edit.blade.php',
      'resources/views/admin/partners/add_edit.blade.php',
      'resources/views/components/admin/tinymce.blade.php (déjà {{ old($name, $value) }} dans le textarea)',
    ],
    description:
      "value=\"{!! old(...) !!}\" sur <x-admin.tinymce> injectait du HTML brut dans la prop du composant. Le composant applique déjà {{ old($name, $value) }} dans le corps du <textarea> — le {!! du parent était donc redondant et permettait un breakout attribut / XSS.",
    exploit:
      "Admin malveillant : saisie avec guillemets / balises pour casser l'attribut ou empoisonner la prop avant échappement dans le textarea.",
    fix: "Suppression de la prop value redondante pour send_email (défaut vide ; old géré par le composant). Remplacement par :value=\"...\" avec uniquement le contenu modèle (sans old) pour trainings/faqs/teams/testimonials/pages. Partners : {!! old(...) !!} dans <textarea> remplacé par {{ old(...) }}.",
  },
  {
    id: 'M2',
    title: 'XSS stocké via TinyMCE → vues show.blade.php',
    severity: 'moyenne',
    status: 'fixed',
    category: 'xss',
    scope: 'public',
    files: [
      'app/Support/SafeHtml.php',
      'app/Providers/AppServiceProvider.php',
      'resources/views/admin/*/show.blade.php (7)',
      'resources/views/team/show.blade.php',
      'resources/views/trainings/show.blade.php',
      'resources/views/pages/about.blade.php',
      'resources/views/faq/index.blade.php',
      'resources/views/home/index.blade.php',
    ],
    description:
      "Le contenu TinyMCE était rendu via {!! ... !!} sans purification sur les pages admin show et le site public.",
    exploit:
      "Admin compromis ou HTML malveillant collé : exécution JS chez tous les visiteurs.",
    fix: "1) Dépendance ezyang/htmlpurifier. 2) Classe App\\Support\\SafeHtml avec config HTML.Allowed + CSS.AllowedProperties (aligné usage CMS). 3) Directive Blade @sanitized(expr) enregistrée dans AppServiceProvider. 4) Remplacement {!! !!} par @sanitized sur toutes les vues listées ; accueil : <p> remplacé par <div> pour éviter <p> imbriqués. 5) Fichiers bonus : trainings/show.blade2.php, debug/teams.blade.php.",
  },
  {
    id: 'M3',
    title: 'redirect_to non validé dans MemberLoginController::create()',
    severity: 'moyenne',
    status: 'fixed',
    category: 'xss',
    scope: 'public',
    files: ['app/Http/Controllers/Member/MemberLoginController.php:57-63'],
    description:
      "$request->get('redirect_to') était passé tel quel à la vue 'redirectTo'. Si la vue le rend dans un input hidden sans échappement strict, on pouvait injecter des attributs.",
    exploit:
      "Combinable avec H3 (open redirect). La vue member.login n'utilise pas la variable directement, mais defense-in-depth est de rigueur.",
    fix: "create() applique sanitizeRedirectTo() avant transmission à la vue. Tout ce qui n'est pas un chemin interne ou le drapeau 'member_trainings' devient null.",
  },
  {
    id: 'M4',
    title: "Cookies de session sans flag Secure ni Same-Site explicite",
    severity: 'moyenne',
    status: 'fixed',
    category: 'session',
    scope: 'global',
    files: ['.env', '.env.example', 'config/session.php'],
    description:
      "SESSION_SECURE_COOKIE et SESSION_SAME_SITE documentés et renseignés ; secure par défaut suit le schéma HTTPS de APP_URL si la variable est absente (évite cookie non-Secure lorsque l'app est servie en HTTPS). Same-Site=lax explicite.",
    exploit:
      "En cas de page mixte (HTTP au lieu de HTTPS lors d'un downgrade), le cookie de session est exposé en clair sur le réseau.",
    fix: ".env + .env.example : SESSION_SECURE_COOKIE, SESSION_SAME_SITE=lax. config/session.php : fallback secure depuis APP_URL si SESSION_SECURE_COOKIE non défini.",
  },
  {
    id: 'M5',
    title: 'Énumération d\'emails via /inscription (checkEmail)',
    severity: 'moyenne',
    status: 'fixed',
    category: 'auth',
    scope: 'public',
    files: [
      'app/Http/Controllers/Member/MemberRegisterController.php',
      'app/Notifications/MemberRegistrationReuseAttemptNotification.php',
      'resources/views/trainings/partials/register-account-gateway.blade.php',
      'lang/fr/member.php',
      'lang/en/member.php',
    ],
    description:
      "checkEmail() renvoyait member_register_email_taken vs redirection avec jeton, permettant d’inférer si un e-mail existe.",
    exploit:
      "Bot patient pouvait tester en masse les adresses (throttle partiel seulement).",
    fix: "Compte existant : notification e-mail MemberRegistrationReuseAttemptNotification + même flash neutre member_register_step1_neutral_ack et texte sans oracle. Étape complete() : erreur générique registration_finish_blocked. Texte d’aide étape 1 adouci.",
  },
  {
    id: 'M6',
    title: 'Pas de check d\'autorisation entre admins (modèles edit cross-tenant)',
    severity: 'moyenne',
    status: 'open',
    category: 'idor',
    scope: 'admin',
    files: ['app/Http/Controllers/Admin/UsersController.php', 'app/Http/Controllers/Admin/RegistrationsController.php'],
    description:
      "Aucune notion de propriétaire / d'équipe / de scope sur les ressources admin. Tout admin avec la permission 'users.edit' peut éditer tout user, voir tous les emails, tous les chèques, etc.",
    exploit:
      "Un éditeur (rôle limité) peut accéder à toutes les inscriptions / utilisateurs si on lui donne juste 'users.view' — pas de séparation par formation/responsable. Risque de fuite RGPD entre équipes.",
    fix: "Définir des Policies (UserPolicy, RegistrationPolicy) avec ->authorize() basé sur l'admin connecté + scope (training_team_id, etc.) selon les besoins métier.",
  },

  // ========== FAIBLE ==========
  {
    id: 'F1',
    title: 'BCRYPT_ROUNDS=12 (acceptable) mais APP_KEY commitée',
    severity: 'faible',
    status: 'open',
    category: 'config',
    scope: 'global',
    files: ['.env'],
    description:
      "APP_KEY=base64:hnz+ePbWvUM2C/ucQG710ZYRi9XhTGSSZYm7rVbC7w8= est versionnée. Cette clé chiffre les sessions, cookies, two_factor_secret, password reset tokens, etc.",
    exploit:
      "Quiconque a la clé peut forger une session valide, déchiffrer les two_factor_secret stockés en DB, déchiffrer les codes de récupération.",
    fix: "Régénérer APP_KEY en prod (php artisan key:generate), invalider toutes les sessions existantes, re-stocker les 2FA secrets (les anciens ne se déchiffreront plus).",
  },
  {
    id: 'F2',
    title: 'Logs métier exposent stack traces en prod',
    severity: 'faible',
    status: 'open',
    category: 'logs',
    scope: 'global',
    files: ['app/Http/Controllers/Admin/UsersController.php:595', 'app/Http/Controllers/Admin/TrainingsController.php:280'],
    description:
      "Plusieurs Log::error() écrivent $e->getTraceAsString() dans storage/logs/laravel.log. Combiné à LOG_LEVEL=debug, le journal contient des chemins absolus, paramètres SQL, IP, etc.",
    exploit:
      "Un attaquant qui obtient un accès lecture (LFI, mauvaise config nginx) au dossier logs récupère un trésor de reconnaissance.",
    fix: "LOG_LEVEL=warning en prod ; ne logger que e->getMessage() (pas le trace) ; logrotate court + droits 0600 sur storage/logs.",
  },
  {
    id: 'F3',
    title: 'Headers Cache-Control absents sur les pages authentifiées',
    severity: 'faible',
    status: 'fixed',
    category: 'headers',
    scope: 'admin',
    files: ['app/Http/Middleware/SecurityHeaders.php'],
    description:
      "Pour les chemins cp-admin / cp-admin/* : Cache-Control no-store, no-cache, must-revalidate et Pragma no-cache appliqués sur la réponse.",
    exploit:
      "Sur un poste partagé, après logout admin, F5/back affiche les données en cache.",
    fix: "SecurityHeaders : si $request->is('cp-admin') ou cp-admin/*, poser Cache-Control et Pragma.",
  },
  {
    id: 'F4',
    title: 'Pas de Content-Security-Policy globale',
    severity: 'faible',
    status: 'fixed',
    category: 'headers',
    scope: 'global',
    files: ['app/Http/Middleware/SecurityHeaders.php', 'app/Http/Controllers/Admin/UsersController.php (viewEmailHtml conserve sa CSP locale)'],
    description:
      "SecurityHeaders pose une CSP globale si la réponse n’en définit pas déjà une — préserve la CSP stricte de viewEmailHtml(). Directives couvrant CDNs (TinyMCE, jQuery, jsDelivr, cdnjs, Tailwind CDN login, Pusher, Lordicon, fonts), iframes Maps/Stripe, unsafe-eval pour Tailwind CDN.",
    exploit:
      "Les XSS en rendu direct {!! !!} sur contenu éditeur sont réduits par M1/M2 ; une CSP (F4) limite encore l'exfiltration vers un domaine externe.",
    fix: "globalContentSecurityPolicy() + skip si Content-Security-Policy déjà présent.",
  },
  {
    id: 'F5',
    title: 'Stripe webhook ne contrôle pas l\'origine IP',
    severity: 'faible',
    status: 'fixed',
    category: 'csrf',
    scope: 'public',
    files: [
      'app/Http/Middleware/VerifyStripeWebhookSourceIp.php',
      'config/stripe_webhooks.php',
      'routes/web.php',
      'bootstrap/app.php',
    ],
    description:
      "Middleware stripe.webhook.ip : IP du client vérifiée contre la liste officielle (snapshot config/stripe_webhooks.php, issue de https://stripe.com/files/ips/ips_webhooks.json). Désactivable via STRIPE_WEBHOOK_VERIFY_IP pour stripe listen. Signature Stripe inchangée.",
    exploit:
      "Risque limité car la signature suffit normalement, mais defense in depth recommandée.",
    fix: "VerifyStripeWebhookSourceIp + config liste WEBHOOKS + .env.example.",
  },
  {
    id: 'F6',
    title: 'admin_intended URL stockée sans nettoyage des paramètres sensibles',
    severity: 'faible',
    status: 'fixed',
    category: 'session',
    scope: 'admin',
    files: ['app/Http/Middleware/AdminAuth.php', 'app/Http/Controllers/Admin/Auth/AdminLoginController.php'],
    description:
      "admin.intended stocke le path issu de Request::getRequestUri() sans query string. Après login, redirection uniquement si le path matche ^/cp-admin(/|$).",
    exploit:
      "Faible mais : dump de la table sessions + recherche admin.intended = exfiltration potentielle de tokens.",
    fix: "strtok(uri, '?') + sendLoginResponse basé sur parse_url(..., PHP_URL_PATH).",
  },

  // ========== INFO ==========
  {
    id: 'I1',
    title: 'composer audit non lancé (dépendances à vérifier en CI)',
    severity: 'info',
    status: 'open',
    category: 'config',
    scope: 'global',
    files: ['composer.json'],
    description:
      "Stack à jour : Laravel 13, Fortify 1.25, Spatie Permission 6.20, Stripe 16, Intervention Image 3.11. Aucune dépendance flagrement obsolète à l'œil nu, mais aucun rapport composer audit récent n'est dans le repo.",
    exploit:
      "Une CVE future (Symfony, Laravel, Stripe SDK) ne sera pas détectée sans CI.",
    fix: "Ajouter composer audit en pipeline CI (GitHub Action ou pre-commit) avec --abort-on-failure.",
  },
  {
    id: 'I2',
    title: 'Plusieurs blocs commentés (testVerification, simulate-visitor, clear-cache, dd, debug routes)',
    severity: 'info',
    status: 'open',
    category: 'cleanup',
    scope: 'global',
    files: [
      'app/Http/Controllers/Admin/Auth/Admin2FAController.php',
      'app/Http/Controllers/Admin/Auth/AdminLoginController.php',
      'routes/admin.php',
      'routes/web.php',
    ],
    description:
      "Le code est parsemé de blocs commentés (logs sensibles, dd($admin), routes de debug, méthodes de test). Tous neutralisés mais polluent la lecture et risquent un retour accidentel via un mauvais merge.",
    exploit: "Aucun à court terme.",
    fix: "Supprimer définitivement après vérification (les blocs sont conservés en historique Git).",
  },
  {
    id: 'I3',
    title: 'Permissions Spatie créées dynamiquement à chaque création de Module',
    severity: 'info',
    status: 'open',
    category: 'autorisation',
    scope: 'admin',
    files: ['app/Models/Module.php', 'routes/admin.php:380'],
    description:
      "Module::created génère 4 permissions par module (view/create/edit/delete). routes/admin.php fait Module::where('is_active', true)->get() au boot pour générer les routes resource. Lourd à chaque requête + couplage fort.",
    exploit: "Performance / lisibilité plutôt que sécurité directe.",
    fix: "Cacher la liste de modules (cache 1h) et générer les routes dans un ServiceProvider. Avoir une liste blanche statique de modules autorisés.",
  },
];

type FixedItem = { id: string; label: string; severity: Severity; passe: 'précédente' | 'courante' };

const fixedFindings: FixedItem[] = [
  // Passe courante (corrigés dans cette conversation)
  { id: 'C4', label: "2FA disable / regenerate → password + TOTP requis", severity: 'critique', passe: 'courante' },
  { id: 'H1', label: "TinyMCE / Partners : SVG retiré des mimes autorisés", severity: 'haute', passe: 'courante' },
  { id: 'H3', label: "Open redirect login membre → redirect_to validé (URL interne uniquement)", severity: 'haute', passe: 'courante' },
  { id: 'H4', label: "Routes /survey/* → throttle + message générique + borne payload responses[]", severity: 'haute', passe: 'courante' },
  { id: 'H5', label: "Admin login : Auth::login() uniquement après 2FA + TTL challenge 5 min", severity: 'haute', passe: 'courante' },
  { id: 'H6', label: "createTranslationFile() : assertPathWithinLangDir + limite profondeur + anti-TOCTOU", severity: 'haute', passe: 'courante' },
  { id: 'H7', label: "Module::create : $validated + slug sanitisé + permissions firstOrCreate transactionnelles", severity: 'haute', passe: 'courante' },
  { id: 'H8', label: "Route /test-helper supprimée + import MenuHelper nettoyé", severity: 'haute', passe: 'courante' },
  { id: 'H2', label: "AddressController déplacé dans Admin\\ + role:super_admin explicite (reclassé Faible)", severity: 'faible', passe: 'courante' },
  { id: 'M3', label: "redirect_to non validé dans create() → sanitizeRedirectTo() appliqué", severity: 'moyenne', passe: 'courante' },
  { id: 'M1', label: "Admin vues : suppression {!! old !!} sur tinymce + textarea partners en {{ }}", severity: 'moyenne', passe: 'courante' },
  { id: 'M2', label: "XSS TinyMCE show : HTMLPurifier + @sanitized (admin + public)", severity: 'moyenne', passe: 'courante' },
  { id: 'M4', label: "Cookies session : SESSION_SECURE_COOKIE + SESSION_SAME_SITE explicites + fallback secure depuis APP_URL", severity: 'moyenne', passe: 'courante' },
  { id: 'M5', label: "checkEmail : message neutre + e-mail au compte existant + complete() sans erreur « déjà pris »", severity: 'moyenne', passe: 'courante' },
  { id: 'F3', label: "SecurityHeaders : Cache-Control + Pragma no-cache pour cp-admin/*", severity: 'faible', passe: 'courante' },
  { id: 'F4', label: "CSP globale dans SecurityHeaders (sauf si déjà définie ; viewEmailHtml inchangé)", severity: 'faible', passe: 'courante' },
  { id: 'F5', label: "Webhook Stripe : liste IP officielle + middleware (désactivable en local)", severity: 'faible', passe: 'courante' },
  { id: 'F6', label: "admin.intended : path sans query + redirect post-login sur path /cp-admin validé", severity: 'faible', passe: 'courante' },
  // Passe précédente
  { id: 'V1', label: "XSS html_content (email-logs) → iframe sandboxé", severity: 'haute', passe: 'précédente' },
  { id: 'V2', label: "XSS notification.message → {{ }} échappé", severity: 'haute', passe: 'précédente' },
  { id: 'V3', label: "XSS emailLog->message (users) → iframe sandboxé", severity: 'haute', passe: 'précédente' },
  { id: 'V4', label: "addslashes toast → json_encode", severity: 'moyenne', passe: 'précédente' },
  { id: 'A6', label: "deleteSessionFile() → authorize('trainings.edit')", severity: 'haute', passe: 'précédente' },
  { id: 'A3-4', label: "Settings translations → assertPathWithinLangDir", severity: 'haute', passe: 'précédente' },
  { id: 'A9', label: "viewEmailHtml → CSP + X-Frame-Options", severity: 'moyenne', passe: 'précédente' },
  { id: 'N1', label: "SecurityHeaders middleware global créé", severity: 'faible', passe: 'précédente' },
  { id: 'S7', label: "Contact POST → throttle:5,1", severity: 'moyenne', passe: 'précédente' },
  { id: 'N3', label: "24 fichiers backup *1.php supprimés", severity: 'faible', passe: 'précédente' },
];

const sevColors: Record<Severity, 'critical' | 'warning' | 'info' | 'neutral' | 'success'> = {
  critique: 'critical',
  haute: 'critical',
  moyenne: 'warning',
  faible: 'info',
  info: 'neutral',
};

const sevLabel: Record<Severity, string> = {
  critique: 'CRITIQUE',
  haute: 'HAUTE',
  moyenne: 'MOYENNE',
  faible: 'FAIBLE',
  info: 'INFO',
};

const catLabel: Record<Category, string> = {
  config: 'Config',
  auth: 'Authentification',
  autorisation: 'Autorisation',
  xss: 'XSS',
  csrf: 'CSRF',
  idor: 'IDOR',
  upload: 'Upload',
  sqli: 'SQL Injection',
  logs: 'Logs',
  session: 'Session',
  'rate-limit': 'Rate limit',
  redirect: 'Redirection',
  headers: 'Headers HTTP',
  cleanup: 'Nettoyage',
};

function FindingCard({ f }: { f: Finding }) {
  const theme = useHostTheme();
  return (
    <Card>
      <CardHeader
        trailing={
          <Row gap={8} align="center">
            <Pill tone={sevColors[f.severity] as any}>{sevLabel[f.severity]}</Pill>
            <Pill tone="neutral">{catLabel[f.category]}</Pill>
            <Pill tone={f.scope === 'public' ? 'warning' : f.scope === 'admin' ? 'info' : 'neutral'}>
              {f.scope}
            </Pill>
          </Row>
        }
      >
        <Row gap={8} align="center">
          <Code>{f.id}</Code>
          <Text weight="bold">{f.title}</Text>
        </Row>
      </CardHeader>
      <CardBody>
        <Stack gap={12}>
          <Stack gap={4}>
            <Text size="small" tone="secondary">Fichier(s)</Text>
            <Stack gap={2}>
              {f.files.map((file) => (
                <Code key={file}>{file}</Code>
              ))}
            </Stack>
          </Stack>
          <Stack gap={4}>
            <Text size="small" tone="secondary">Description</Text>
            <Text>{f.description}</Text>
          </Stack>
          <Stack gap={4}>
            <Text size="small" tone="secondary">Scénario d'exploitation</Text>
            <Text>{f.exploit}</Text>
          </Stack>
          <Stack gap={4}>
            <Text size="small" tone="secondary">Correction recommandée</Text>
            <Text style={{ color: theme.colors.success }}>{f.fix}</Text>
          </Stack>
        </Stack>
      </CardBody>
    </Card>
  );
}

export default function SecuriteAuditComplet() {
  const open = findings.filter((f) => f.status === 'open');
  const counts = {
    critique: open.filter((f) => f.severity === 'critique').length,
    haute: open.filter((f) => f.severity === 'haute').length,
    moyenne: open.filter((f) => f.severity === 'moyenne').length,
    faible: open.filter((f) => f.severity === 'faible').length,
    info: open.filter((f) => f.severity === 'info').length,
  };

  const groups: Severity[] = ['critique', 'haute', 'moyenne', 'faible', 'info'];

  return (
    <Stack gap={24}>
      <Stack gap={8}>
        <H1>Audit de sécurité — Sfer (Laravel 13)</H1>
        <Text tone="secondary">
          {open.length} vulnérabilités ouvertes — {fixedFindings.length} corrigées (dont{' '}
          {fixedFindings.filter((f) => f.passe === 'courante').length} dans la session courante).
        </Text>
      </Stack>

      <Grid columns={6} gap={12}>
        <Stat label="Critique" value={String(counts.critique)} tone="critical" />
        <Stat label="Haute" value={String(counts.haute)} tone="critical" />
        <Stat label="Moyenne" value={String(counts.moyenne)} tone="warning" />
        <Stat label="Faible" value={String(counts.faible)} tone="info" />
        <Stat label="Info" value={String(counts.info)} tone="neutral" />
        <Stat label="Corrigées" value={String(fixedFindings.length)} tone="success" />
      </Grid>

      <Callout tone="warning" title="À traiter en priorité">
        <Text>
          C1 (APP_DEBUG), C2 (root MySQL) et C3 (clés exposées) doivent être corrigés AVANT toute
          mise en production. Combinés, ces trois points donnent une compromission triviale du serveur.
        </Text>
      </Callout>

      <Divider />

      <Stack gap={12}>
        <Row gap={8} align="center">
          <H2>Failles corrigées</H2>
          <Pill tone="success">{fixedFindings.length}</Pill>
        </Row>
        <Card>
          <CardHeader>
            <Text weight="bold">Session courante</Text>
          </CardHeader>
          <CardBody>
            <Stack gap={8}>
              {fixedFindings
                .filter((item) => item.passe === 'courante')
                .map((item) => (
                  <Row key={item.id} gap={8} align="center">
                    <Pill tone="success">FIX</Pill>
                    <Pill tone={sevColors[item.severity] as any}>{sevLabel[item.severity]}</Pill>
                    <Code>{item.id}</Code>
                    <Text size="small">{item.label}</Text>
                  </Row>
                ))}
            </Stack>
          </CardBody>
        </Card>
        <Card>
          <CardHeader>
            <Text weight="bold">Passe précédente</Text>
          </CardHeader>
          <CardBody>
            <Grid columns={2} gap={8}>
              {fixedFindings
                .filter((item) => item.passe === 'précédente')
                .map((item) => (
                  <Row key={item.id} gap={8} align="center">
                    <Pill tone="success">FIX</Pill>
                    <Code>{item.id}</Code>
                    <Text size="small">{item.label}</Text>
                  </Row>
                ))}
            </Grid>
          </CardBody>
        </Card>
      </Stack>

      <Divider />

      {groups.map((sev) => {
        const items = open.filter((f) => f.severity === sev);
        if (items.length === 0) return null;
        return (
          <Stack key={sev} gap={12}>
            <Row gap={8} align="center">
              <H2>{sevLabel[sev]}</H2>
              <Pill tone={sevColors[sev] as any}>{items.length}</Pill>
            </Row>
            <Stack gap={12}>
              {items.map((f) => (
                <FindingCard key={f.id} f={f} />
              ))}
            </Stack>
          </Stack>
        );
      })}

      <Divider />

      <Stack gap={12}>
        <H2>Synthèse par fichier (top hotspots)</H2>
        <Table
          headers={['Fichier', 'IDs liés', 'Sévérité max']}
          rows={[
            ['.env', 'C1, C2, C3, F1', 'Critique'],
            ['app/Http/Controllers/Admin/Auth/Admin2FAController.php', 'C4', 'Critique'],
            ['app/Http/Controllers/Admin/Auth/AdminLoginController.php', 'H5', 'Haute'],
            ['app/Http/Controllers/Admin/SettingsController.php', 'H6', 'Haute'],
            ['app/Http/Controllers/Admin/TinyMCEController.php', 'H1', 'Haute'],
            ['app/Http/Controllers/Admin/AddressController.php', 'H2', 'Faible'],
            ['app/Http/Controllers/Member/MemberLoginController.php', 'H3, M3', 'Haute'],
            ['app/Http/Controllers/Member/MemberRegisterController.php', '— (M5 corrigée)', '—'],
            ['app/Http/Controllers/Admin/ModuleController.php', 'H7', 'Haute'],
            ['app/Http/Controllers/SurveyController.php', 'H4', 'Haute'],
            ['routes/web.php', 'H8, — (F5 corrigée)', 'Haute'],
            ['routes/admin.php', 'H2, I3', 'Haute'],
            ['Vues admin add_edit + send_email', '—', '—'],
            ['Vues show (TinyMCE)', '— (M1+M2 corrigés)', '—'],
            ['app/Http/Middleware/SecurityHeaders.php', '— (F3+F4 corrigées)', '—'],
          ]}
        />
      </Stack>

      <Stack gap={12}>
        <H2>Plan d'action recommandé</H2>
        <Card>
          <CardBody>
            <Stack gap={16}>
              <Stack gap={4}>
                <H3>Sprint 1 — Avant mise en production (jour J)</H3>
                <Text>C1 + C2 + C3 + C4 + H6 (path traversal createTranslationFile)</Text>
              </Stack>
              <Stack gap={4}>
                <H3>Sprint 2 — Semaine 1</H3>
                <Text>H1 (SVG), H3 (open redirect), H4 (rate limit survey), H5 (login 2FA), H7 (mass assignment), H8 (test-helper)</Text>
              </Stack>
              <Stack gap={4}>
                <H3>Sprint 3 — Mois 1</H3>
                <Text>M6 (Policies)</Text>
              </Stack>
              <Stack gap={4}>
                <H3>Long terme</H3>
                <Text>F1 (rotation APP_KEY), F2 (logs), I1 (composer audit en CI), I2 (cleanup), I3 (cache modules)</Text>
              </Stack>
            </Stack>
          </CardBody>
        </Card>
      </Stack>
    </Stack>
  );
}
