hts-ai's picture
🐳 12/07 - 21:54 - api les api en reel
972d51f verified
Raw
History Blame Contribute Delete
42.8 kB
<!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>
<!-- Puter.js retiré — IA cognitive via OpenRouter -->
<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>
<!-- Config & Monitoring -->
<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">
<!-- AMBIENT -->
<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">
<!-- TOP BAR -->
<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>
<!-- HERO -->
<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>
<!-- PHASE INDICATOR -->
<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>
<!-- MAIN LAYOUT -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<!-- LEFT: INPUT PANEL -->
<div class="lg:col-span-1 space-y-4 anim-up anim-up-3">
<!-- Ingestion -->
<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>
<!-- Raw HTML injection -->
<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>
<!-- RIGHT: RESULTS -->
<div class="lg:col-span-2 space-y-4 anim-up anim-up-4">
<!-- Live progress -->
<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>
<!-- Features panel -->
<div id="featuresPanel" class="hidden space-y-3">
<!-- Phase 2: Static -->
<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>
<!-- Phase 3: Dynamic -->
<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>
<!-- Phase 4: Fusion + Phase 5: IA -->
<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>
<!-- Phase 6: Decision -->
<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>
<!-- JSON Report -->
<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>
<!-- DETECTED SCRIPTS TABLE -->
<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>
<!-- TOAST -->
<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>
// ═══ STATE ═══════════════════════════════════════════════════════════════════
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',
];
// ═══ INIT ════════════════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => {
lucide.createIcons();
});
// ═══ TOAST ═══════════════════════════════════════════════════════════════════
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);
}
// ═══ PHASE INDICATOR ════════════════════════════════════════════════════════
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] || '';
}
// ═══ PROGRESS BAR ═══════════════════════════════════════════════════════════
function updateProgress(pct, detail) {
document.getElementById("progressBar").style.width = pct + "%";
if (detail) document.getElementById("progressDetail").textContent = detail;
}
// ═══ START PIPELINES ════════════════════════════════════════════════════════
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);
}
// ═══ PIPELINE FROM URL ══════════════════════════════════════════════════════
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 {
// ── Phase 1: Ingestion ──
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);
// ── Phase 2: Static Scan ──
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);
// ── Phase 3: Dynamic Scan ──
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);
// ── Phase 4+5: Fusion + IA ──
updateProgress(75, "Fusion cognitive + Analyse IA…");
setPhaseIndicator(4);
await runAIAnalysis();
updateProgress(90, "Analyse IA terminée");
await sleep(300);
// ── Phase 6: Decision ──
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();
}
// ═══ PIPELINE FROM RAW HTML ═════════════════════════════════════════════════
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();
}
// ═══ STATIC SCAN ════════════════════════════════════════════════════════════
function staticScan(html) {
const scripts = [];
const domains = new Set();
let inlineCount = 0;
let externalCount = 0;
let obfuscatedCount = 0;
let totalScriptSize = 0;
// Extract script blocks
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;
// Detect obfuscation
let obfuscationScore = 0;
const obfuscationHits = [];
OBFUSCATION_PATTERNS.forEach(pattern => {
if (pattern.test(body)) {
obfuscationScore++;
obfuscationHits.push(pattern.source.substring(0, 30));
}
});
// Minification detection (ratio)
const lines = body.split("\n").length;
const charsPerLine = lines > 0 ? size / lines : size;
const isMinified = size > 200 && charsPerLine > 200;
// Domain extraction
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,
});
}
// Iframe extraction
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) {}
}
});
// Link extraction (external resources)
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) {}
}
// Known tracker detection
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 };
}
// ═══ DYNAMIC SCAN (simulated) ══════════════════════════════════════════════
async function dynamicScan(url, html) {
// Simulate headless browser observations via fetch + analysis
const features = {
thirdPartyRequests: 0,
cookieCount: 0,
suspiciousCookies: 0,
redirectDetected: false,
iframeInjected: 0,
resourceTypes: {},
anomalyFlags: [],
};
// Count potential third-party resources in the HTML
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;
// Cookie detection from HTML meta/http-equiv
const cookieMatches = html.match(/set-cookie|document\.cookie|CookieConsent|cookieLaw/gi) || [];
features.cookieCount = cookieMatches.length;
// Suspicious cookies (third-party, no consent)
const suspiciousCookiePatterns = /(?:tracking|advertising|analytics|retargeting|social)/gi;
const suspiciousMatches = html.match(suspiciousCookiePatterns) || [];
features.suspiciousCookies = suspiciousMatches.length;
// Redirect detection
features.redirectDetected = /window\.location|meta.*refresh|http-equiv="refresh"/i.test(html);
// Hidden iframes
const hiddenIframes = html.match(/<iframe[^>]*?(?:hidden|display\s*:\s*none|width="0"|height="0")[^>]*>/gi) || [];
features.iframeInjected = hiddenIframes.length;
// Anomaly flags
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;
}
// ═══ RENDER 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("");
}
// ═══ AI ANALYSIS ════════════════════════════════════════════════════════════
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);
// Nettoyer les éventuels blocs markdown
text = text.replace(/