| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SentinelAI · Content Scan — Cognitive Pipeline</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| dark: { 950: '#0a0b0f', 900: '#0f1117', 800: '#161822', 700: '#1e2030', 600: '#272a3a', 500: '#3a3e52', 400: '#5c6078', 300: '#8b8fa6', 200: '#b4b7c8', 100: '#dfe0e8' }, |
| neon: { purple: '#a78bfa', green: '#34d399', blue: '#60a5fa', cyan: '#22d3ee', amber: '#fbbf24', red: '#f87171', pink: '#f472b6', orange: '#fb923c' }, |
| glass: { white: 'rgba(255,255,255,0.04)', border: 'rgba(255,255,255,0.08)', hover: 'rgba(255,255,255,0.07)' }, |
| }, |
| fontFamily: { |
| mono: ['"SF Mono"', '"Fira Code"', 'monospace'], |
| display: ['"Inter"', 'system-ui', 'sans-serif'], |
| } |
| } |
| } |
| } |
| </script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"> |
| <style> |
| * { box-sizing: border-box; } |
| body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; } |
| |
| @keyframes fadeInUp { from { opacity:0; transform:translateY(16px) } to { opacity:1; transform:translateY(0) } } |
| @keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:0.3} } |
| @keyframes shimmer { 0%{background-position:-200% 0} 100%{background-position:200% 0} } |
| @keyframes slide-down { from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:translateY(0)} } |
| @keyframes breathe { 0%,100%{opacity:0.6} 50%{opacity:1} } |
| |
| .anim-up { animation: fadeInUp 0.45s ease forwards; opacity: 0; } |
| .anim-up-1 { animation-delay: 0.05s; } |
| .anim-up-2 { animation-delay: 0.1s; } |
| .anim-up-3 { animation-delay: 0.15s; } |
| .anim-up-4 { animation-delay: 0.2s; } |
| .anim-up-5 { animation-delay: 0.25s; } |
| .anim-up-6 { animation-delay: 0.3s; } |
| |
| .glow-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07); backdrop-filter: blur(12px); } |
| .glow-card:hover { background: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.12); } |
| |
| .shimmer-bg { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent); background-size: 200% 100%; animation: shimmer 3s infinite; } |
| |
| ::-webkit-scrollbar { width: 5px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } |
| |
| .text-gradient-purple { |
| background: linear-gradient(135deg, #a78bfa, #60a5fa, #22d3ee); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .grid-bg { |
| background-image: |
| linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); |
| background-size: 40px 40px; |
| } |
| |
| .phase-dot-active { animation: breathe 1.5s ease-in-out infinite; } |
| </style> |
| |
| <script src="config.js"></script> |
| <script src="https://cdn.signalfx.com/o11y-gdi-rum/latest/splunk-otel-web.js" crossorigin="anonymous"></script> |
| <script> |
| (function() { |
| const cfg = window.SENTINEL_CONFIG?.splunkRUM; |
| if (cfg && cfg.token) { |
| SplunkRum.init({ |
| beaconUrl: cfg.beaconUrl, |
| rumAuth: cfg.token, |
| app: cfg.app, |
| environment: cfg.environment |
| }); |
| } |
| })(); |
| </script> |
| </head> |
| <body class="bg-black text-dark-100 min-h-screen grid-bg overflow-x-hidden"> |
|
|
| |
| <div class="fixed inset-0 pointer-events-none overflow-hidden z-0"> |
| <div class="absolute top-1/4 right-0 w-[500px] h-[500px] rounded-full bg-neon-purple/5 blur-[150px]"></div> |
| <div class="absolute bottom-0 left-1/4 w-[400px] h-[400px] rounded-full bg-neon-cyan/5 blur-[120px]"></div> |
| </div> |
|
|
| <div class="relative z-10 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 pb-20"> |
|
|
| |
| <div class="flex items-center justify-between py-4 border-b border-glass-border anim-up"> |
| <div class="flex items-center gap-3"> |
| <a href="dashboard.html" class="text-dark-400 hover:text-white transition" title="Dashboard"> |
| <i data-lucide="layout-dashboard" class="w-5 h-5"></i> |
| </a> |
| <a href="index.html" class="text-dark-400 hover:text-white transition" title="Fast Takedown"> |
| <i data-lucide="shield-alert" class="w-5 h-5"></i> |
| </a> |
| <a href="agent.html" class="text-dark-400 hover:text-white transition" title="Agent Hybride"> |
| <i data-lucide="brain" class="w-5 h-5"></i> |
| </a> |
| <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-neon-purple to-neon-cyan flex items-center justify-center shadow-lg shadow-neon-purple/20"> |
| <i data-lucide="scan-search" class="w-5 h-5 text-white"></i> |
| </div> |
| <div> |
| <div class="text-sm font-bold tracking-tight text-white">SentinelAI · Content Scan</div> |
| <div class="text-[10px] text-dark-400 tracking-[0.15em] uppercase">Pipeline Cognitif SEO/GEO + Sécurité</div> |
| </div> |
| </div> |
| <div class="flex items-center gap-3"> |
| <div class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg glow-card"> |
| <span class="w-2 h-2 rounded-full bg-neon-green animate-pulse"></span> |
| <span class="text-[11px] text-dark-300">Pipeline actif</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="pt-8 pb-4 anim-up anim-up-1"> |
| <h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight text-white mb-2"> |
| Content <span class="text-gradient-purple">Scan Pipeline</span> |
| </h1> |
| <p class="text-sm text-dark-400 max-w-2xl">Analyse cognitive multi-phase : statique → dynamique → fusion IA → décision. Détecte l'obfuscation, les trackers agressifs, les cookies non consentis, et les anomalies comportementales sur vos pages SEO/GEO.</p> |
| </div> |
|
|
| |
| <div class="flex items-center gap-1 mb-6 anim-up anim-up-2" id="phaseIndicator"> |
| <span class="phase-dot w-2 h-2 rounded-full bg-dark-500" data-phase="1"></span> |
| <span class="h-px w-6 bg-dark-500" data-phase="1-2"></span> |
| <span class="phase-dot w-2 h-2 rounded-full bg-dark-500" data-phase="2"></span> |
| <span class="h-px w-6 bg-dark-500" data-phase="2-3"></span> |
| <span class="phase-dot w-2 h-2 rounded-full bg-dark-500" data-phase="3"></span> |
| <span class="h-px w-6 bg-dark-500" data-phase="3-4"></span> |
| <span class="phase-dot w-2 h-2 rounded-full bg-dark-500" data-phase="4"></span> |
| <span class="h-px w-6 bg-dark-500" data-phase="4-5"></span> |
| <span class="phase-dot w-2 h-2 rounded-full bg-dark-500" data-phase="5"></span> |
| <span class="ml-2 text-[10px] text-dark-500" id="phaseLabel">Prêt</span> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6"> |
|
|
| |
| <div class="lg:col-span-1 space-y-4 anim-up anim-up-3"> |
| |
| <div class="glow-card rounded-xl p-4"> |
| <div class="flex items-center gap-2 mb-3"> |
| <div class="w-7 h-7 rounded-lg bg-neon-purple/10 flex items-center justify-center"> |
| <i data-lucide="file-input" class="w-4 h-4 text-neon-purple"></i> |
| </div> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">1. Ingestion</span> |
| </div> |
| <label class="block text-[10px] text-dark-400 mb-1.5">URL ou page HTML à scanner</label> |
| <input id="urlInput" type="text" |
| class="w-full px-3 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-sm placeholder:text-dark-500 outline-none focus:border-neon-purple transition mb-3" |
| placeholder="https://monsite-local.fr/page-seo" /> |
|
|
| <label class="block text-[10px] text-dark-400 mb-1.5">Métadonnées GEO/SEM (optionnel)</label> |
| <div class="grid grid-cols-2 gap-2 mb-3"> |
| <input id="geoCity" type="text" |
| class="px-3 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-purple transition" |
| placeholder="Ville" /> |
| <input id="geoQuartier" type="text" |
| class="px-3 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-purple transition" |
| placeholder="Quartier" /> |
| </div> |
| <input id="geoKeywords" type="text" |
| class="w-full px-3 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-purple transition mb-3" |
| placeholder="Mots-clés (séparés par des virgules)" /> |
|
|
| <button onclick="startPipeline()" id="startBtn" |
| class="w-full py-2.5 rounded-lg bg-gradient-to-r from-neon-purple to-neon-cyan text-dark-950 font-semibold text-sm hover:opacity-90 active:scale-[0.98] transition flex items-center justify-center gap-2"> |
| <i data-lucide="play" class="w-4 h-4"></i> Lancer le pipeline complet |
| </button> |
| </div> |
|
|
| |
| <div class="glow-card rounded-xl p-4"> |
| <div class="flex items-center gap-2 mb-3"> |
| <i data-lucide="code-2" class="w-4 h-4 text-neon-cyan"></i> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">Ou coller du HTML brut</span> |
| </div> |
| <textarea id="htmlInput" rows="6" |
| class="w-full px-3 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs font-mono placeholder:text-dark-500 outline-none focus:border-neon-cyan transition resize-vertical" |
| placeholder="<html>...collez votre code HTML ici...</html>"></textarea> |
| <button onclick="startPipelineFromHTML()" |
| class="w-full mt-3 py-2 rounded-lg border border-glass-border text-dark-300 text-xs hover:bg-white/5 transition flex items-center justify-center gap-1.5"> |
| <i data-lucide="file-code" class="w-3.5 h-3.5"></i> Scanner le HTML collé |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-2 space-y-4 anim-up anim-up-4"> |
| |
| <div id="progressPanel" class="glow-card rounded-xl p-4 hidden"> |
| <div class="flex items-center gap-2 mb-3"> |
| <i data-lucide="loader" class="w-4 h-4 text-neon-amber animate-spin"></i> |
| <span id="progressTitle" class="text-xs font-semibold text-white uppercase tracking-wider">Scan en cours…</span> |
| </div> |
| <div class="h-2 rounded-full bg-dark-700 overflow-hidden"> |
| <div id="progressBar" class="h-full rounded-full bg-gradient-to-r from-neon-purple to-neon-cyan transition-all duration-700 ease-out" style="width:0%"></div> |
| </div> |
| <div id="progressDetail" class="text-[10px] text-dark-400 mt-1.5">Initialisation…</div> |
| </div> |
|
|
| |
| <div id="featuresPanel" class="hidden space-y-3"> |
| |
| <div class="glow-card rounded-xl p-4 anim-fade"> |
| <div class="flex items-center gap-2 mb-3"> |
| <div class="w-7 h-7 rounded-lg bg-neon-green/10 flex items-center justify-center"> |
| <i data-lucide="file-search" class="w-4 h-4 text-neon-green"></i> |
| </div> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">2. Scan Statique</span> |
| </div> |
| <div class="grid grid-cols-2 sm:grid-cols-3 gap-3" id="staticGrid"></div> |
| </div> |
|
|
| |
| <div class="glow-card rounded-xl p-4 anim-fade"> |
| <div class="flex items-center gap-2 mb-3"> |
| <div class="w-7 h-7 rounded-lg bg-neon-blue/10 flex items-center justify-center"> |
| <i data-lucide="activity" class="w-4 h-4 text-neon-blue"></i> |
| </div> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">3. Scan Dynamique</span> |
| </div> |
| <div class="grid grid-cols-2 sm:grid-cols-3 gap-3" id="dynamicGrid"></div> |
| </div> |
|
|
| |
| <div class="glow-card rounded-xl p-4 anim-fade"> |
| <div class="flex items-center justify-between mb-3"> |
| <div class="flex items-center gap-2"> |
| <div class="w-7 h-7 rounded-lg bg-neon-amber/10 flex items-center justify-center"> |
| <i data-lucide="brain" class="w-4 h-4 text-neon-amber"></i> |
| </div> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">4+5. Fusion Cognitive & IA</span> |
| </div> |
| <span id="aiStatus" class="text-[10px] text-dark-500">En attente</span> |
| </div> |
| <div id="aiOutputPanel" class="p-3 rounded-lg bg-white/[0.03] border border-glass-border text-xs text-dark-300 leading-relaxed whitespace-pre-wrap hidden"></div> |
| <div id="aiPlaceholder" class="text-[11px] text-dark-500 italic">L'analyse IA se déclenche automatiquement après la fusion des features…</div> |
| </div> |
| </div> |
|
|
| |
| <div id="decisionPanel" class="hidden anim-fade space-y-3"> |
| <div class="glow-card rounded-xl p-4" id="decisionCard"> |
| <div class="flex items-center justify-between mb-3"> |
| <div class="flex items-center gap-2"> |
| <div class="w-7 h-7 rounded-lg flex items-center justify-center" id="decisionIconWrapper"> |
| <i data-lucide="gavel" class="w-4 h-4 text-white" id="decisionIcon"></i> |
| </div> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">6. Décision & Rapport</span> |
| </div> |
| <span id="decisionBadge" class="text-[10px] px-2 py-0.5 rounded-full font-semibold"></span> |
| </div> |
| <div id="decisionContent" class="text-xs text-dark-300 space-y-2"></div> |
| </div> |
|
|
| |
| <div class="glow-card rounded-xl p-4"> |
| <div class="flex items-center justify-between mb-3"> |
| <div class="flex items-center gap-2"> |
| <i data-lucide="braces" class="w-4 h-4 text-neon-green"></i> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">Rapport JSON</span> |
| </div> |
| <button onclick="copyReport()" class="text-[10px] text-neon-green/70 hover:text-neon-green transition flex items-center gap-1"> |
| <i data-lucide="copy" class="w-3 h-3"></i>Copier |
| </button> |
| </div> |
| <pre id="jsonReport" class="text-[11px] font-mono text-neon-green/80 leading-relaxed whitespace-pre-wrap max-h-[400px] overflow-y-auto"></pre> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="scriptsTablePanel" class="hidden anim-up anim-up-5"> |
| <div class="glow-card rounded-xl p-5"> |
| <div class="flex items-center gap-2 mb-3"> |
| <i data-lucide="list" class="w-4 h-4 text-neon-purple"></i> |
| <span class="text-xs font-semibold text-white uppercase tracking-wider">Scripts détectés</span> |
| <span id="scriptsCount" class="text-[10px] text-dark-500"></span> |
| </div> |
| <div class="overflow-x-auto"> |
| <table class="w-full text-left text-xs"> |
| <thead> |
| <tr class="text-dark-500 border-b border-glass-border"> |
| <th class="pb-2 font-medium">Source</th> |
| <th class="pb-2 font-medium">Type</th> |
| <th class="pb-2 font-medium">Obfuscation</th> |
| <th class="pb-2 font-medium">Taille</th> |
| <th class="pb-2 font-medium">Domaine</th> |
| </tr> |
| </thead> |
| <tbody id="scriptsTable" class="text-dark-300 divide-y divide-glass-border"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| |
| <div id="toast" class="fixed bottom-6 right-6 z-50 hidden"> |
| <div class="bg-dark-700 text-white text-xs font-medium px-4 py-2.5 rounded-lg shadow-lg border border-glass-border flex items-center gap-2"> |
| <i data-lucide="check-circle" class="w-4 h-4 text-neon-green"></i> |
| <span id="toastMsg"></span> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let scanState = { |
| url: '', |
| html: '', |
| geo: { city: '', quartier: '', keywords: [] }, |
| staticFeatures: {}, |
| dynamicFeatures: {}, |
| scripts: [], |
| aiResult: null, |
| decision: null, |
| report: null, |
| isRunning: false, |
| }; |
| |
| const OBFUSCATION_PATTERNS = [ |
| /eval\s*\(/i, |
| /\bFunction\s*\(/i, |
| /atob\s*\(/i, |
| /btoa\s*\(/i, |
| /fromCharCode/i, |
| /unescape\s*\(/i, |
| /\\x[0-9a-f]{2}/i, |
| /\\u[0-9a-f]{4}/i, |
| /[a-zA-Z]{20,}\s*=\s*[a-zA-Z]{20,}/, |
| /String\.fromCharCode/i, |
| /document\.write\s*\(/i, |
| ]; |
| |
| const KNOWN_TRACKERS = [ |
| 'google-analytics.com', 'googletagmanager.com', 'doubleclick.net', |
| 'facebook.com/tr', 'connect.facebook.net', 'analytics.twitter.com', |
| 'linkedin.com/px', 'bat.bing.com', 'pinterest.com', 'hotjar.com', |
| 'clarity.ms', 'mouseflow.com', 'crazyegg.com', 'fullstory.com', |
| 'cdn.segment.com', 'amplitude.com', 'mixpanel.com', 'heap.io', |
| 'scorecardresearch.com', 'outbrain.com', 'taboola.com', 'criteo.com', |
| 'adsrvr.org', 'adnxs.com', 'rubiconproject.com', 'pubmatic.com', |
| ]; |
| |
| |
| document.addEventListener("DOMContentLoaded", () => { |
| lucide.createIcons(); |
| }); |
| |
| |
| function showToast(msg) { |
| const t = document.getElementById("toast"); |
| document.getElementById("toastMsg").textContent = msg; |
| t.classList.remove("hidden"); |
| lucide.createIcons(); |
| clearTimeout(window._t); |
| window._t = setTimeout(() => t.classList.add("hidden"), 3500); |
| } |
| |
| |
| function setPhaseIndicator(phase) { |
| const labels = ['', 'Ingestion', 'Statique', 'Dynamique', 'Fusion+IA', 'Décision']; |
| document.querySelectorAll('#phaseIndicator .phase-dot').forEach((dot, i) => { |
| const p = i + 1; |
| if (p <= phase) { |
| dot.classList.add('bg-neon-purple', 'phase-dot-active'); |
| dot.classList.remove('bg-dark-500'); |
| } else { |
| dot.classList.add('bg-dark-500'); |
| dot.classList.remove('bg-neon-purple', 'phase-dot-active'); |
| } |
| }); |
| document.querySelectorAll('#phaseIndicator .h-px').forEach((line, i) => { |
| const p = i + 2; |
| if (p <= phase) line.classList.add('bg-neon-purple'); |
| else line.classList.remove('bg-neon-purple'); |
| }); |
| document.getElementById("phaseLabel").textContent = labels[phase] || ''; |
| } |
| |
| |
| function updateProgress(pct, detail) { |
| document.getElementById("progressBar").style.width = pct + "%"; |
| if (detail) document.getElementById("progressDetail").textContent = detail; |
| } |
| |
| |
| function startPipeline() { |
| const url = document.getElementById("urlInput").value.trim(); |
| if (!url) { showToast("Veuillez entrer une URL"); return; } |
| if (scanState.isRunning) return; |
| scanState.isRunning = true; |
| scanState.url = url; |
| scanState.geo.city = document.getElementById("geoCity").value.trim(); |
| scanState.geo.quartier = document.getElementById("geoQuartier").value.trim(); |
| scanState.geo.keywords = document.getElementById("geoKeywords").value.split(",").map(k => k.trim()).filter(Boolean); |
| resetUI(); |
| runPipeline(url); |
| } |
| |
| function startPipelineFromHTML() { |
| const html = document.getElementById("htmlInput").value.trim(); |
| if (!html) { showToast("Veuillez coller du HTML"); return; } |
| if (scanState.isRunning) return; |
| scanState.isRunning = true; |
| scanState.url = "HTML injecté manuellement"; |
| scanState.geo.city = document.getElementById("geoCity").value.trim(); |
| scanState.geo.quartier = document.getElementById("geoQuartier").value.trim(); |
| scanState.geo.keywords = document.getElementById("geoKeywords").value.split(",").map(k => k.trim()).filter(Boolean); |
| resetUI(); |
| runPipelineFromHTML(html); |
| } |
| |
| function resetUI() { |
| document.getElementById("progressPanel").classList.remove("hidden"); |
| document.getElementById("featuresPanel").classList.add("hidden"); |
| document.getElementById("decisionPanel").classList.add("hidden"); |
| document.getElementById("scriptsTablePanel").classList.add("hidden"); |
| updateProgress(0, "Initialisation…"); |
| setPhaseIndicator(1); |
| } |
| |
| |
| async function runPipeline(url) { |
| const startBtn = document.getElementById("startBtn"); |
| startBtn.disabled = true; |
| startBtn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-dark-950 border-t-transparent rounded-full animate-spin"></span>'; |
| |
| try { |
| |
| updateProgress(10, "Fetching HTML…"); |
| setPhaseIndicator(1); |
| const resp = await fetch(url, { headers: { 'Accept': 'text/html' } }); |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
| scanState.html = await resp.text(); |
| updateProgress(25, "HTML récupéré — Normalisation…"); |
| await sleep(400); |
| |
| |
| updateProgress(30, "Scan statique en cours…"); |
| setPhaseIndicator(2); |
| const staticResult = staticScan(scanState.html); |
| scanState.staticFeatures = staticResult.features; |
| scanState.scripts = staticResult.scripts; |
| updateProgress(50, "Scan statique terminé"); |
| renderStaticFeatures(staticResult.features); |
| renderScriptsTable(staticResult.scripts); |
| await sleep(500); |
| |
| |
| updateProgress(55, "Scan dynamique simulé…"); |
| setPhaseIndicator(3); |
| const dynamicResult = await dynamicScan(url, scanState.html); |
| scanState.dynamicFeatures = dynamicResult; |
| updateProgress(70, "Scan dynamique terminé"); |
| renderDynamicFeatures(dynamicResult); |
| await sleep(400); |
| |
| |
| updateProgress(75, "Fusion cognitive + Analyse IA…"); |
| setPhaseIndicator(4); |
| await runAIAnalysis(); |
| updateProgress(90, "Analyse IA terminée"); |
| await sleep(300); |
| |
| |
| updateProgress(95, "Génération du rapport…"); |
| setPhaseIndicator(5); |
| generateDecision(); |
| updateProgress(100, "Pipeline terminé ✓"); |
| await sleep(500); |
| document.getElementById("progressPanel").classList.add("hidden"); |
| |
| } catch (e) { |
| updateProgress(0, "Erreur: " + e.message); |
| showToast("Erreur pipeline: " + e.message); |
| document.getElementById("progressTitle").textContent = "Erreur"; |
| } |
| |
| scanState.isRunning = false; |
| startBtn.disabled = false; |
| startBtn.innerHTML = '<i data-lucide="play" class="w-4 h-4"></i> Lancer le pipeline complet'; |
| lucide.createIcons(); |
| } |
| |
| |
| async function runPipelineFromHTML(html) { |
| const startBtn = document.getElementById("startBtn"); |
| startBtn.disabled = true; |
| |
| updateProgress(20, "HTML reçu — Normalisation…"); |
| setPhaseIndicator(1); |
| scanState.html = html; |
| await sleep(300); |
| |
| updateProgress(35, "Scan statique en cours…"); |
| setPhaseIndicator(2); |
| const staticResult = staticScan(scanState.html); |
| scanState.staticFeatures = staticResult.features; |
| scanState.scripts = staticResult.scripts; |
| updateProgress(55, "Scan statique terminé"); |
| renderStaticFeatures(staticResult.features); |
| renderScriptsTable(staticResult.scripts); |
| await sleep(400); |
| |
| updateProgress(60, "Scan dynamique simulé…"); |
| setPhaseIndicator(3); |
| const dynamicResult = await dynamicScanHTML(scanState.html); |
| scanState.dynamicFeatures = dynamicResult; |
| updateProgress(72, "Scan dynamique terminé"); |
| renderDynamicFeatures(dynamicResult); |
| await sleep(300); |
| |
| updateProgress(78, "Fusion cognitive + Analyse IA…"); |
| setPhaseIndicator(4); |
| await runAIAnalysis(); |
| updateProgress(92, "Analyse IA terminée"); |
| await sleep(200); |
| |
| updateProgress(96, "Génération du rapport…"); |
| setPhaseIndicator(5); |
| generateDecision(); |
| updateProgress(100, "Pipeline terminé ✓"); |
| await sleep(400); |
| document.getElementById("progressPanel").classList.add("hidden"); |
| |
| scanState.isRunning = false; |
| startBtn.disabled = false; |
| startBtn.innerHTML = '<i data-lucide="play" class="w-4 h-4"></i> Lancer le pipeline complet'; |
| lucide.createIcons(); |
| } |
| |
| |
| function staticScan(html) { |
| const scripts = []; |
| const domains = new Set(); |
| let inlineCount = 0; |
| let externalCount = 0; |
| let obfuscatedCount = 0; |
| let totalScriptSize = 0; |
| |
| |
| const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi; |
| let match; |
| while ((match = scriptRegex.exec(html)) !== null) { |
| const attrs = match[1] || ''; |
| const body = match[2] || ''; |
| const srcMatch = attrs.match(/src=["']([^"']+)["']/i); |
| const src = srcMatch ? srcMatch[1] : null; |
| const isInline = !src; |
| const size = body.length; |
| |
| |
| let obfuscationScore = 0; |
| const obfuscationHits = []; |
| OBFUSCATION_PATTERNS.forEach(pattern => { |
| if (pattern.test(body)) { |
| obfuscationScore++; |
| obfuscationHits.push(pattern.source.substring(0, 30)); |
| } |
| }); |
| |
| |
| const lines = body.split("\n").length; |
| const charsPerLine = lines > 0 ? size / lines : size; |
| const isMinified = size > 200 && charsPerLine > 200; |
| |
| |
| let domain = "inline"; |
| if (src) { |
| try { |
| const u = new URL(src.startsWith("//") ? "https:" + src : src); |
| domain = u.hostname; |
| domains.add(domain); |
| } catch (e) { |
| domain = "externe (parse error)"; |
| domains.add("externe"); |
| } |
| } |
| |
| if (isInline) inlineCount++; |
| else externalCount++; |
| |
| if (obfuscationScore > 1) obfuscatedCount++; |
| totalScriptSize += size; |
| |
| scripts.push({ |
| src: src || "(inline)", |
| type: isInline ? "inline" : "externe", |
| domain, |
| size, |
| obfuscationScore, |
| obfuscationHits, |
| isMinified, |
| }); |
| } |
| |
| |
| const iframeMatches = html.match(/<iframe[^>]*>/gi) || []; |
| const iframeDomains = new Set(); |
| iframeMatches.forEach(tag => { |
| const srcMatch = tag.match(/src=["']([^"']+)["']/i); |
| if (srcMatch) { |
| try { |
| const u = new URL(srcMatch[1].startsWith("//") ? "https:" + srcMatch[1] : srcMatch[1]); |
| iframeDomains.add(u.hostname); |
| domains.add(u.hostname); |
| } catch(e) {} |
| } |
| }); |
| |
| |
| const linkRegex = /<link\b[^>]*href=["']([^"']+)["'][^>]*>/gi; |
| while ((match = linkRegex.exec(html)) !== null) { |
| const href = match[1]; |
| try { |
| const u = new URL(href.startsWith("//") ? "https:" + href : href); |
| domains.add(u.hostname); |
| } catch(e) {} |
| } |
| |
| |
| const knownTrackersDetected = []; |
| KNOWN_TRACKERS.forEach(tracker => { |
| if (html.toLowerCase().includes(tracker.toLowerCase())) { |
| knownTrackersDetected.push(tracker); |
| } |
| }); |
| |
| const tierDomains = [...domains].filter(d => d !== "inline" && d !== "externe"); |
| |
| const features = { |
| totalScripts: scripts.length, |
| inlineCount, |
| externalCount, |
| inlineRatio: scripts.length > 0 ? (inlineCount / scripts.length * 100).toFixed(1) : 0, |
| obfuscatedCount, |
| obfuscatedRatio: scripts.length > 0 ? (obfuscatedCount / scripts.length * 100).toFixed(1) : 0, |
| tierDomainCount: tierDomains.length, |
| tierDomains, |
| iframeCount: iframeMatches.length, |
| iframeDomains: [...iframeDomains], |
| knownTrackersCount: knownTrackersDetected.length, |
| knownTrackers: knownTrackersDetected, |
| totalScriptSize, |
| avgScriptSize: scripts.length > 0 ? Math.round(totalScriptSize / scripts.length) : 0, |
| }; |
| |
| return { features, scripts }; |
| } |
| |
| |
| async function dynamicScan(url, html) { |
| |
| const features = { |
| thirdPartyRequests: 0, |
| cookieCount: 0, |
| suspiciousCookies: 0, |
| redirectDetected: false, |
| iframeInjected: 0, |
| resourceTypes: {}, |
| anomalyFlags: [], |
| }; |
| |
| |
| const domain = extractDomain(url); |
| const allURLs = []; |
| const urlRegex = /(?:src|href|action|data-url|content)=["'](https?:\/\/[^"']+)["']/gi; |
| let match; |
| while ((match = urlRegex.exec(html)) !== null) { |
| allURLs.push(match[1]); |
| } |
| |
| const thirdParties = new Set(); |
| const resourceTypes = {}; |
| allURLs.forEach(u => { |
| try { |
| const parsed = new URL(u); |
| if (parsed.hostname !== domain && parsed.hostname !== "inline") { |
| thirdParties.add(parsed.hostname); |
| } |
| const ext = u.split(".").pop()?.split("?")[0] || "other"; |
| resourceTypes[ext] = (resourceTypes[ext] || 0) + 1; |
| } catch(e) {} |
| }); |
| |
| features.thirdPartyRequests = thirdParties.size; |
| features.resourceTypes = resourceTypes; |
| |
| |
| const cookieMatches = html.match(/set-cookie|document\.cookie|CookieConsent|cookieLaw/gi) || []; |
| features.cookieCount = cookieMatches.length; |
| |
| |
| const suspiciousCookiePatterns = /(?:tracking|advertising|analytics|retargeting|social)/gi; |
| const suspiciousMatches = html.match(suspiciousCookiePatterns) || []; |
| features.suspiciousCookies = suspiciousMatches.length; |
| |
| |
| features.redirectDetected = /window\.location|meta.*refresh|http-equiv="refresh"/i.test(html); |
| |
| |
| const hiddenIframes = html.match(/<iframe[^>]*?(?:hidden|display\s*:\s*none|width="0"|height="0")[^>]*>/gi) || []; |
| features.iframeInjected = hiddenIframes.length; |
| |
| |
| if (features.thirdPartyRequests > 10) features.anomalyFlags.push("Nombre élevé de domaines tiers (>10)"); |
| if (features.suspiciousCookies > 3) features.anomalyFlags.push("Cookies suspects sans consentement visible"); |
| if (features.redirectDetected) features.anomalyFlags.push("Redirection automatique détectée"); |
| if (features.iframeInjected > 0) features.anomalyFlags.push(`${features.iframeInjected} iframe(s) cachée(s) détectée(s)`); |
| if (scanState.staticFeatures.obfuscatedCount > 2) features.anomalyFlags.push("Multiple scripts obfusqués"); |
| |
| return features; |
| } |
| |
| async function dynamicScanHTML(html) { |
| const features = { |
| thirdPartyRequests: 0, |
| cookieCount: 0, |
| suspiciousCookies: 0, |
| redirectDetected: false, |
| iframeInjected: 0, |
| resourceTypes: {}, |
| anomalyFlags: [], |
| }; |
| |
| const allURLs = []; |
| const urlRegex = /(?:src|href|action|data-url|content)=["'](https?:\/\/[^"']+)["']/gi; |
| let match; |
| while ((match = urlRegex.exec(html)) !== null) { |
| allURLs.push(match[1]); |
| } |
| |
| const thirdParties = new Set(); |
| const resourceTypes = {}; |
| allURLs.forEach(u => { |
| try { |
| const parsed = new URL(u); |
| thirdParties.add(parsed.hostname); |
| const ext = u.split(".").pop()?.split("?")[0] || "other"; |
| resourceTypes[ext] = (resourceTypes[ext] || 0) + 1; |
| } catch(e) {} |
| }); |
| |
| features.thirdPartyRequests = thirdParties.size; |
| features.resourceTypes = resourceTypes; |
| |
| const cookieMatches = html.match(/set-cookie|document\.cookie|CookieConsent|cookieLaw/gi) || []; |
| features.cookieCount = cookieMatches.length; |
| const suspiciousMatches = html.match(/(?:tracking|advertising|analytics|retargeting|social)/gi) || []; |
| features.suspiciousCookies = suspiciousMatches.length; |
| features.redirectDetected = /window\.location|meta.*refresh|http-equiv="refresh"/i.test(html); |
| const hiddenIframes = html.match(/<iframe[^>]*?(?:hidden|display\s*:\s*none|width="0"|height="0")[^>]*>/gi) || []; |
| features.iframeInjected = hiddenIframes.length; |
| |
| if (features.thirdPartyRequests > 10) features.anomalyFlags.push("Nombre élevé de domaines tiers (>10)"); |
| if (features.suspiciousCookies > 3) features.anomalyFlags.push("Cookies suspects sans consentement visible"); |
| if (features.redirectDetected) features.anomalyFlags.push("Redirection automatique détectée"); |
| if (features.iframeInjected > 0) features.anomalyFlags.push(`${features.iframeInjected} iframe(s) cachée(s) détectée(s)`); |
| if (scanState.staticFeatures.obfuscatedCount > 2) features.anomalyFlags.push("Multiple scripts obfusqués"); |
| |
| return features; |
| } |
| |
| |
| function renderStaticFeatures(f) { |
| document.getElementById("featuresPanel").classList.remove("hidden"); |
| const grid = document.getElementById("staticGrid"); |
| const items = [ |
| { label: "Scripts totaux", value: f.totalScripts, icon: "file-code", color: "#a78bfa" }, |
| { label: "Inline / Externes", value: `${f.inlineCount} / ${f.externalCount}`, icon: "git-compare", color: "#60a5fa" }, |
| { label: "Ratio inline", value: `${f.inlineRatio}%`, icon: "percent", color: "#22d3ee" }, |
| { label: "Scripts obfusqués", value: `${f.obfuscatedCount}`, icon: "shield-close", color: f.obfuscatedCount > 1 ? "#f87171" : "#34d399" }, |
| { label: "Domaines tiers", value: f.tierDomainCount, icon: "globe", color: f.tierDomainCount > 5 ? "#fbbf24" : "#34d399" }, |
| { label: "Trackers connus", value: f.knownTrackersCount, icon: "eye-off", color: f.knownTrackersCount > 3 ? "#f87171" : "#34d399" }, |
| { label: "Iframes", value: f.iframeCount, icon: "layout-panel-left", color: "#fb923c" }, |
| { label: "Taille moyenne", value: `${(f.avgScriptSize / 1000).toFixed(1)} Ko`, icon: "hard-drive", color: "#f472b6" }, |
| ]; |
| grid.innerHTML = items.map(i => ` |
| <div class="p-2.5 rounded-lg bg-white/[0.03] border border-glass-border"> |
| <div class="flex items-center gap-1.5 mb-1"> |
| <i data-lucide="${i.icon}" class="w-3 h-3" style="color:${i.color}"></i> |
| <span class="text-[10px] text-dark-400">${i.label}</span> |
| </div> |
| <div class="text-xs font-semibold text-white">${i.value}</div> |
| </div> |
| `).join(""); |
| lucide.createIcons(); |
| } |
| |
| function renderDynamicFeatures(f) { |
| const grid = document.getElementById("dynamicGrid"); |
| const items = [ |
| { label: "Requêtes tierces", value: f.thirdPartyRequests, icon: "share-2", color: f.thirdPartyRequests > 8 ? "#fbbf24" : "#34d399" }, |
| { label: "Cookies", value: f.cookieCount, icon: "cookie", color: f.cookieCount > 5 ? "#fbbf24" : "#34d399" }, |
| { label: "Cookies suspects", value: f.suspiciousCookies, icon: "alert-triangle", color: f.suspiciousCookies > 3 ? "#f87171" : "#34d399" }, |
| { label: "Redirection", value: f.redirectDetected ? "OUI ⚠" : "Non", icon: "corner-up-right", color: f.redirectDetected ? "#f87171" : "#34d399" }, |
| { label: "Iframes cachées", value: f.iframeInjected, icon: "eye-off", color: f.iframeInjected > 0 ? "#f87171" : "#34d399" }, |
| { label: "Anomalies", value: f.anomalyFlags.length, icon: "alert-circle", color: f.anomalyFlags.length > 0 ? "#f87171" : "#34d399" }, |
| ]; |
| grid.innerHTML = items.map(i => ` |
| <div class="p-2.5 rounded-lg bg-white/[0.03] border border-glass-border"> |
| <div class="flex items-center gap-1.5 mb-1"> |
| <i data-lucide="${i.icon}" class="w-3 h-3" style="color:${i.color}"></i> |
| <span class="text-[10px] text-dark-400">${i.label}</span> |
| </div> |
| <div class="text-xs font-semibold text-white">${i.value}</div> |
| </div> |
| `).join(""); |
| lucide.createIcons(); |
| } |
| |
| function renderScriptsTable(scripts) { |
| document.getElementById("scriptsTablePanel").classList.remove("hidden"); |
| document.getElementById("scriptsCount").textContent = `(${scripts.length} scripts)`; |
| const tbody = document.getElementById("scriptsTable"); |
| if (scripts.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="5" class="py-3 text-center text-dark-500 italic">Aucun script détecté</td></tr>'; |
| return; |
| } |
| tbody.innerHTML = scripts.map(s => { |
| const obfuscationIcon = s.obfuscationScore > 1 |
| ? `<span class="text-neon-red font-bold">⚠ ${s.obfuscationScore}</span>` |
| : `<span class="text-neon-green">Aucune</span>`; |
| const sizeStr = s.size > 1000 ? `${(s.size/1000).toFixed(1)} Ko` : `${s.size} o`; |
| const domainDisplay = s.domain === "inline" ? `<span class="text-dark-500">inline</span>` : `<span class="text-neon-cyan">${s.domain}</span>`; |
| return `<tr class="hover:bg-white/[0.02] transition"> |
| <td class="py-2 font-mono text-[10px] text-dark-200 max-w-[140px] truncate" title="${s.src}">${s.src}</td> |
| <td class="py-2"><span class="px-1.5 py-0.5 rounded bg-dark-700 text-dark-300 text-[10px] border border-glass-border">${s.type}</span></td> |
| <td class="py-2">${obfuscationIcon}</td> |
| <td class="py-2 text-dark-300">${sizeStr}</td> |
| <td class="py-2">${domainDisplay}</td> |
| </tr>`; |
| }).join(""); |
| } |
| |
| |
| async function openRouterChat(messages, { model, temperature = 0.4, max_tokens = 2048 }) { |
| const cfg = window.SENTINEL_CONFIG?.openRouter; |
| if (!cfg?.apiKey) throw new Error("Clé API OpenRouter manquante — vérifiez config.js"); |
| |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { |
| method: "POST", |
| headers: { |
| "Authorization": `Bearer ${cfg.apiKey}`, |
| "Content-Type": "application/json", |
| "HTTP-Referer": cfg.httpReferer || window.location.href, |
| "X-OpenRouter-Title": cfg.appTitle || "SentinelAI", |
| }, |
| body: JSON.stringify({ model, messages, temperature, max_tokens }), |
| }); |
| |
| if (!response.ok) { |
| const err = await response.json().catch(() => ({})); |
| throw new Error(err.error?.message || `HTTP ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| return { message: { content: data.choices[0].message.content } }; |
| } |
| |
| async function runAIAnalysis() { |
| const aiPanel = document.getElementById("aiOutputPanel"); |
| const aiPlaceholder = document.getElementById("aiPlaceholder"); |
| const aiStatus = document.getElementById("aiStatus"); |
| |
| aiStatus.innerHTML = '<span class="inline-block w-2.5 h-2.5 border-2 border-neon-amber border-t-transparent rounded-full animate-spin"></span> Analyse…'; |
| aiStatus.className = "text-[10px] text-neon-amber"; |
| |
| const sf = scanState.staticFeatures || {}; |
| const df = scanState.dynamicFeatures || {}; |
| const geo = scanState.geo || {}; |
| |
| const fusionVector = { |
| scripts_total: sf.totalScripts || 0, |
| inline_ratio: sf.inlineRatio || "0", |
| obfuscated: sf.obfuscatedCount || 0, |
| tier_domains: sf.tierDomainCount || 0, |
| known_trackers: sf.knownTrackersCount || 0, |
| iframes: sf.iframeCount || 0, |
| third_party_requests: df.thirdPartyRequests || 0, |
| cookies: df.cookieCount || 0, |
| suspicious_cookies: df.suspiciousCookies || 0, |
| redirect: df.redirectDetected || false, |
| hidden_iframes: df.iframeInjected || 0, |
| anomalies: (df.anomalyFlags || []).length, |
| geo_city: geo.city || "N/A", |
| geo_quartier: geo.quartier || "N/A", |
| keywords: (geo.keywords || []).join(", ") || "N/A", |
| known_tracker_list: (sf.knownTrackers || []).slice(0, 5).join(", ") || "Aucun", |
| tier_domain_list: (sf.tierDomains || []).slice(0, 5).join(", ") || "Aucun", |
| anomaly_details: (df.anomalyFlags || []).join("; ") || "Aucune", |
| }; |
| |
| const systemPrompt = "Tu es un analyste de sécurité cognitive pour SentinelAI. Tu renvoies UNIQUEMENT du JSON valide."; |
| const prompt = `Analyse ce vecteur de features extrait d'une page web SEO/GEO : |
| |
| VECTEUR DE FEATURES : |
| ${JSON.stringify(fusionVector, null, 2)} |
| |
| URL : ${scanState.url} |
| |
| Réponds en JSON valide UNIQUEMENT : |
| { |
| "score": 0.XX, |
| "categorie": "propre|marketing|agressif|suspect", |
| "explication": "Brève analyse en français", |
| "features_dominantes": ["f1","f2"], |
| "recommandation": "Action recommandée" |
| }`; |
| |
| try { |
| const response = await openRouterChat([ |
| { role: "system", content: systemPrompt }, |
| { role: "user", content: prompt } |
| ], { model: "x-ai/grok-4-1-fast", temperature: 0.2 }); |
| |
| let text = ""; |
| if (response?.message?.content) text = response.message.content; |
| else if (typeof response === "string") text = response; |
| else text = JSON.stringify(response); |
| |
| |
| text = text.replace(/ |