🐳 15/07 - 00:15 - apiApiKey: l7xx5b9f4a053aaf43d8bc05bcc266dd8532googleMapApiKey: AIzaSyBZpFbAwSwce9SSxC_CingLRtc_iHqVrvUwgsApiKey: AIzaSyBbl6gcuSky-zngKf1xATiOj4xEudOC3rM rprebWP
f1dcbc1 verified | // ===================================================== | |
| // SentinelAI Command Center - Main Controller | |
| // ===================================================== | |
| // ---- State ---- | |
| const state = { | |
| startTime: Date.now(), | |
| scanQueue: 0, | |
| threatCount: 247, | |
| scanCount: 1832, | |
| vulnCount: 54, | |
| assetCount: 128, | |
| alerts: [], | |
| logs: [], | |
| discoveries: [], | |
| targets: [], | |
| sparklineData: [], | |
| scanning: false | |
| }; | |
| // ---- Threat Map Data (simulated world coordinates) ---- | |
| const threatZones = [ | |
| { name: 'North America', lat: 39.8, lng: -98.6, threats: 45, level: 'high' }, | |
| { name: 'Europe', lat: 50.1, lng: 9.8, threats: 38, level: 'medium' }, | |
| { name: 'East Asia', lat: 35.9, lng: 104.2, threats: 52, level: 'critical' }, | |
| { name: 'South Asia', lat: 20.6, lng: 78.9, threats: 21, level: 'medium' }, | |
| { name: 'Middle East', lat: 29.5, lng: 45.5, threats: 15, level: 'high' }, | |
| { name: 'South America', lat: -14.2, lng: -51.9, threats: 12, level: 'low' }, | |
| { name: 'Africa', lat: 6.6, lng: 20.9, threats: 8, level: 'low' }, | |
| { name: 'Oceania', lat: -25.3, lng: 133.8, threats: 6, level: 'low' }, | |
| { name: 'Russia', lat: 61.5, lng: 105.3, threats: 33, level: 'high' }, | |
| { name: 'SE Asia', lat: 1.4, lng: 103.8, threats: 18, level: 'medium' }, | |
| ]; | |
| // ---- Alert Templates ---- | |
| const alertTemplates = [ | |
| { severity: 'critical', icon: 'shield-alert', messages: [ | |
| 'SQL Injection attempt detected on /api/v2/users', | |
| 'Brute force attack on SSH port 22 from 185.x.x.x', | |
| 'Data exfiltration anomaly detected - 2.3GB outbound', | |
| 'Zero-day exploit CVE-2024-XXXX detected in wild', | |
| ]}, | |
| { severity: 'high', icon: 'alert-triangle', messages: [ | |
| 'Unusual login pattern from geographic outlier', | |
| 'DDoS attack mitigation active - 15Gbps filtered', | |
| 'Exposed .git repository found on subdomain', | |
| 'Credential stuffing attack on login endpoint', | |
| ]}, | |
| { severity: 'medium', icon: 'info', messages: [ | |
| 'New subdomain discovered: dev.internal.target.com', | |
| 'SSL certificate expiring in 14 days', | |
| 'Port 8080 newly opened on monitored asset', | |
| 'API rate limit threshold reached for key X', | |
| ]}, | |
| { severity: 'low', icon: 'bell', messages: [ | |
| 'Scheduled scan completed for sector A', | |
| 'DNS record change detected', | |
| 'New HTTP header observed on endpoint', | |
| 'Cookie security flag missing on staging', | |
| ]} | |
| ]; | |
| // ---- Discovery Templates ---- | |
| const discoveryTemplates = [ | |
| { type: 'subdomain', icon: 'globe', color: 'cyan', msg: 'Subdomain found: {sub}.target.com' }, | |
| { type: 'port', icon: 'plug', color: 'amber', msg: 'Open port {port} detected on {ip}' }, | |
| { type: 'vuln', icon: 'bug', color: 'red', msg: 'XSS vulnerability in /search?q=' }, | |
| { type: 'dork', icon: 'search', color: 'purple', msg: 'Google Dork match: ext:sql site:target.com' }, | |
| { type: 'config', icon: 'file-warning', color: 'orange', msg: 'Exposed config file: /.env' }, | |
| { type: 'api', icon: 'code', color: 'blue', msg: 'Unauthenticated API endpoint: /api/v1/admin' }, | |
| ]; | |
| // ---- Target Data ---- | |
| const targetData = [ | |
| { name: 'prod-server-01', ip: '10.0.1.15', hits: 847, severity: 'critical' }, | |
| { name: 'web-gateway', ip: '10.0.1.1', hits: 623, severity: 'high' }, | |
| { name: 'db-cluster-master', ip: '10.0.2.10', hits: 412, severity: 'high' }, | |
| { name: 'api-load-balancer', ip: '10.0.1.50', hits: 289, severity: 'medium' }, | |
| { name: 'cdn-edge-node-03', ip: '10.0.3.22', hits: 156, severity: 'medium' }, | |
| { name: 'staging-env', ip: '10.0.4.5', hits: 98, severity: 'low' }, | |
| { name: 'dev-sandbox', ip: '10.0.5.12', hits: 34, severity: 'low' }, | |
| ]; | |
| // ---- Initialize ---- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| initCounters(); | |
| initAlerts(); | |
| initDiscoveries(); | |
| initTargets(); | |
| initSparkline(); | |
| initThreatMap(); | |
| startClock(); | |
| startOperationalMode(); | |
| }); | |
| // ---- Clock & Uptime ---- | |
| function startClock() { | |
| function update() { | |
| const now = new Date(); | |
| const timeEl = document.getElementById('systemTime'); | |
| if (timeEl) timeEl.textContent = now.toUTCString().split(' ')[4] + ' UTC'; | |
| const uptimeEl = document.getElementById('uptimeCounter'); | |
| if (uptimeEl) { | |
| const diff = Date.now() - state.startTime; | |
| const d = Math.floor(diff / 86400000); | |
| const h = Math.floor((diff % 86400000) / 3600000); | |
| const m = Math.floor((diff % 3600000) / 60000); | |
| uptimeEl.textContent = `${d}d ${h}h ${m}m`; | |
| } | |
| } | |
| update(); | |
| setInterval(update, 1000); | |
| } | |
| // ---- Counter Animation ---- | |
| function animateCounter(el, target, duration = 1500) { | |
| if (!el) return; | |
| const start = parseInt(el.textContent) || 0; | |
| const startTime = performance.now(); | |
| function step(currentTime) { | |
| const elapsed = currentTime - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| const eased = 1 - Math.pow(1 - progress, 3); | |
| el.textContent = Math.floor(start + (target - start) * eased).toLocaleString(); | |
| if (progress < 1) requestAnimationFrame(step); | |
| } | |
| requestAnimationFrame(step); | |
| } | |
| function initCounters() { | |
| animateCounter(document.getElementById('threatCount'), state.threatCount); | |
| animateCounter(document.getElementById('scanCount'), state.scanCount); | |
| animateCounter(document.getElementById('vulnCount'), state.vulnCount); | |
| animateCounter(document.getElementById('assetCount'), state.assetCount); | |
| } | |
| // ---- Alerts ---- | |
| async function initAlerts() { | |
| await fetchAndAddAlerts(); | |
| setInterval(fetchAndAddAlerts, 8000); | |
| } | |
| function addAlert() { | |
| const template = alertTemplates[Math.floor(Math.random() * alertTemplates.length)]; | |
| const message = template.messages[Math.floor(Math.random() * template.messages.length)]; | |
| const severity = template.severity; | |
| const icon = template.icon; | |
| const now = new Date(); | |
| const timeStr = now.toTimeString().split(' ')[0]; | |
| const colorMap = { | |
| critical: { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', badge: 'bg-red-500/20 text-red-400' }, | |
| high: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', badge: 'bg-amber-500/20 text-amber-400' }, | |
| medium: { bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', text: 'text-cyan-400', badge: 'bg-cyan-500/20 text-cyan-400' }, | |
| low: { bg: 'bg-slate-500/10', border: 'border-slate-700/30', text: 'text-slate-400', badge: 'bg-slate-500/20 text-slate-400' }, | |
| }; | |
| const c = colorMap[severity] || colorMap.low; | |
| const alertObj = { severity, message, time: timeStr, icon, colors: c }; | |
| state.alerts.unshift(alertObj); | |
| if (state.alerts.length > 20) state.alerts.pop(); | |
| renderAlerts(); | |
| updateAlertCount(); | |
| } | |
| function renderAlerts() { | |
| const list = document.getElementById('alertsList'); | |
| if (!list) return; | |
| list.innerHTML = state.alerts.map((a, i) => ` | |
| <div class="alert-item ${a.colors.bg} border ${a.colors.border} rounded-lg p-3" style="animation-delay: ${i * 50}ms"> | |
| <div class="flex items-start gap-2"> | |
| <i data-lucide="${a.icon}" class="w-3.5 h-3.5 ${a.colors.text} mt-0.5 shrink-0"></i> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-xs text-slate-300 leading-relaxed">${a.message}</p> | |
| <div class="flex items-center gap-2 mt-1.5"> | |
| <span class="text-[10px] font-mono ${a.colors.badge} px-1.5 py-0.5 rounded uppercase font-semibold">${a.severity}</span> | |
| <span class="text-[10px] font-mono text-slate-600">${a.time}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function updateAlertCount() { | |
| const el = document.getElementById('alertCount'); | |
| if (el) { | |
| const critCount = state.alerts.filter(a => a.severity === 'critical' || a.severity === 'high').length; | |
| el.textContent = critCount; | |
| } | |
| } | |
| // ---- Activity Log ---- | |
| function addLog(message, type = 'info') { | |
| const now = new Date(); | |
| const timeStr = now.toTimeString().split(' ')[0]; | |
| const colorMap = { | |
| info: 'text-slate-400', | |
| success: 'text-green-400', | |
| warning: 'text-amber-400', | |
| error: 'text-red-400', | |
| system: 'text-cyan-400', | |
| }; | |
| const prefixMap = { | |
| info: 'INF', | |
| success: 'OK ', | |
| warning: 'WRN', | |
| error: 'ERR', | |
| system: 'SYS', | |
| }; | |
| state.logs.unshift({ message, type, time: timeStr }); | |
| if (state.logs.length > 50) state.logs.pop(); | |
| renderLogs(); | |
| } | |
| function renderLogs() { | |
| const el = document.getElementById('activityLog'); | |
| if (!el) return; | |
| const colorMap = { | |
| info: 'text-slate-400', | |
| success: 'text-green-400', | |
| warning: 'text-amber-400', | |
| error: 'text-red-400', | |
| system: 'text-cyan-400', | |
| }; | |
| const prefixMap = { | |
| info: 'INF', | |
| success: 'OK ', | |
| warning: 'WRN', | |
| error: 'ERR', | |
| system: 'SYS', | |
| }; | |
| el.innerHTML = state.logs.map(l => ` | |
| <div class="log-entry flex gap-2"> | |
| <span class="text-slate-600 shrink-0">${l.time}</span> | |
| <span class="${colorMap[l.type] || 'text-slate-400'} shrink-0">[${prefixMap[l.type] || 'INF'}]</span> | |
| <span class="text-slate-300">${l.message}</span> | |
| </div> | |
| `).join(''); | |
| } | |
| function clearLogs() { | |
| state.logs = []; | |
| renderLogs(); | |
| showToast('Logs cleared'); | |
| } | |
| // ---- Discoveries ---- | |
| function initDiscoveries() { | |
| // Discoveries are already populated by fetchAndAddAlerts, no need for simulated ones. | |
| setInterval(fetchAndAddAlerts, 12000); | |
| } | |
| function addDiscovery() { | |
| const template = discoveryTemplates[Math.floor(Math.random() * discoveryTemplates.length)]; | |
| const subs = ['dev', 'staging', 'api', 'admin', 'cdn', 'mail', 'vpn', 'git', 'app', 'test']; | |
| const ports = [22, 80, 443, 3306, 5432, 8080, 8443, 27017, 6379, 9200]; | |
| const ips = ['10.0.1.15', '10.0.2.10', '192.168.1.1', '172.16.0.5']; | |
| let msg = template.msg | |
| .replace('{sub}', subs[Math.floor(Math.random() * subs.length)]) | |
| .replace('{port}', ports[Math.floor(Math.random() * ports.length)]) | |
| .replace('{ip}', ips[Math.floor(Math.random() * ips.length)]); | |
| const now = new Date(); | |
| const timeStr = now.toTimeString().split(' ')[0].substring(0, 5); | |
| state.discoveries.unshift({ type: template.type, icon: template.icon, color: template.color, msg, time: timeStr }); | |
| if (state.discoveries.length > 15) state.discoveries.pop(); | |
| renderDiscoveries(); | |
| } | |
| function renderDiscoveries() { | |
| const el = document.getElementById('discoveriesList'); | |
| if (!el) return; | |
| const colorClasses = { | |
| cyan: 'text-cyan-400 bg-cyan-500/10', | |
| amber: 'text-amber-400 bg-amber-500/10', | |
| red: 'text-red-400 bg-red-500/10', | |
| purple: 'text-purple-400 bg-purple-500/10', | |
| orange: 'text-orange-400 bg-orange-500/10', | |
| blue: 'text-blue-400 bg-blue-500/10', | |
| }; | |
| el.innerHTML = state.discoveries.map(d => { | |
| const cc = colorClasses[d.color] || colorClasses.cyan; | |
| const [textC, bgC] = cc.split(' '); | |
| return ` | |
| <div class="discovery-item flex items-start gap-2.5 p-2.5 rounded-lg cursor-default"> | |
| <div class="w-7 h-7 ${bgC} rounded-md flex items-center justify-center shrink-0"> | |
| <i data-lucide="${d.icon}" class="w-3.5 h-3.5 ${textC}"></i> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-xs text-slate-300 leading-relaxed">${d.msg}</p> | |
| <span class="text-[10px] font-mono text-slate-600">${d.time}</span> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| lucide.createIcons(); | |
| } | |
| // ---- Top Targets ---- | |
| async function initTargets() { | |
| // Utiliser des IPs réelles | |
| const ips = ['8.8.8.8', '1.1.1.1', '208.67.222.222', '9.9.9.9', '8.8.4.4', '1.0.0.1', '208.67.220.220']; | |
| const results = await SentinelAPI.batchLookup(ips); | |
| state.targets = results.map((r, i) => ({ | |
| name: r.org ? r.org.split(' ')[0] : `Target-${i+1}`, | |
| ip: r.ip || ips[i], | |
| hits: Math.floor(Math.random() * 800 + 100), | |
| severity: r.country ? 'medium' : 'low', | |
| })); | |
| renderTargets(); | |
| } | |
| function renderTargets() { | |
| const el = document.getElementById('topTargetsList'); | |
| if (!el) return; | |
| const sevColors = { | |
| critical: 'text-red-400 bg-red-500/10 border-red-500/20', | |
| high: 'text-amber-400 bg-amber-500/10 border-amber-500/20', | |
| medium: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/20', | |
| low: 'text-slate-400 bg-slate-500/10 border-slate-700/30', | |
| }; | |
| const maxHits = Math.max(...state.targets.map(t => t.hits)); | |
| el.innerHTML = state.targets.map(t => { | |
| const sc = sevColors[t.severity] || sevColors.low; | |
| const [textC, bgC, borderC] = sc.split(' '); | |
| const barWidth = (t.hits / maxHits * 100); | |
| const barColor = t.severity === 'critical' ? 'bg-red-500' : t.severity === 'high' ? 'bg-amber-500' : t.severity === 'medium' ? 'bg-cyan-500' : 'bg-slate-500'; | |
| return ` | |
| <div class="target-item p-2.5 rounded-lg cursor-default"> | |
| <div class="flex items-center justify-between mb-1.5"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="server" class="w-3.5 h-3.5 ${textC}"></i> | |
| <span class="text-xs font-medium text-slate-200">${t.name}</span> | |
| </div> | |
| <span class="text-[10px] font-mono ${textC} px-1.5 py-0.5 rounded ${bgC} border ${borderC} uppercase font-semibold">${t.severity}</span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-[10px] font-mono text-slate-600">${t.ip}</span> | |
| <div class="flex-1 bg-slate-800 rounded-full h-1"> | |
| <div class="${barColor} h-1 rounded-full" style="width: ${barWidth}%"></div> | |
| </div> | |
| <span class="text-[10px] font-mono text-slate-500">${t.hits}</span> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| lucide.createIcons(); | |
| } | |
| // ---- Threat Map (Leaflet) ---- | |
| let threatMap = null; | |
| let pulseRings = []; | |
| function initThreatMap() { | |
| const mapEl = document.getElementById('threatMap'); | |
| if (!mapEl) return; | |
| // Initialize Leaflet map with dark theme | |
| threatMap = L.map('threatMap', { | |
| center: [20, 0], | |
| zoom: 2, | |
| minZoom: 2, | |
| maxZoom: 8, | |
| zoomControl: false, | |
| attributionControl: false, | |
| worldCopyJump: true, | |
| scrollWheelZoom: true, | |
| }); | |
| // Dark theme tiles (CartoDB Dark Matter) | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { | |
| subdomains: 'abcd', | |
| maxZoom: 19, | |
| }).addTo(threatMap); | |
| // Zoom control bottom-right | |
| L.control.zoom({ position: 'bottomright' }).addTo(threatMap); | |
| // Color map for threat levels | |
| const colorMap = { | |
| critical: '#dc2626', | |
| high: '#f59e0b', | |
| medium: '#06b6d4', | |
| low: '#64748b', | |
| }; | |
| // Add threat markers | |
| threatZones.forEach(zone => { | |
| const color = colorMap[zone.level] || colorMap.low; | |
| const intensity = Math.min(zone.threats / 60, 1); | |
| const markerRadius = 6 + intensity * 6; | |
| // Glow circle (semi-transparent fill) | |
| L.circle([zone.lat, zone.lng], { | |
| radius: 400000 + intensity * 600000, | |
| fillColor: color, | |
| color: color, | |
| weight: 0, | |
| opacity: 0, | |
| fillOpacity: 0.08, | |
| }).addTo(threatMap); | |
| // Main marker | |
| const marker = L.circleMarker([zone.lat, zone.lng], { | |
| radius: markerRadius, | |
| fillColor: color, | |
| color: '#0c1222', | |
| weight: 2, | |
| opacity: 1, | |
| fillOpacity: 0.9, | |
| }).addTo(threatMap); | |
| // Popup | |
| marker.bindPopup(` | |
| <div style="font-family: 'Inter', sans-serif; min-width: 140px;"> | |
| <div style="font-weight: 700; color: ${color}; font-size: 13px; margin-bottom: 4px;">${zone.name}</div> | |
| <div style="font-size: 11px; color: #94a3b8; font-family: 'JetBrains Mono', monospace;"> | |
| <span style="text-transform: uppercase; font-weight: 600; color: ${color};">${zone.level}</span> · ${zone.threats} threats | |
| </div> | |
| </div> | |
| `); | |
| // Tooltip on hover | |
| marker.bindTooltip(`${zone.name} — ${zone.threats} threats`, { | |
| direction: 'top', | |
| offset: [0, -markerRadius - 4], | |
| className: 'threat-tooltip', | |
| }); | |
| // Pulse ring (animated) | |
| const pulseRing = L.circle([zone.lat, zone.lng], { | |
| radius: 100000, | |
| fillColor: color, | |
| color: color, | |
| weight: 1.5, | |
| opacity: 0.5, | |
| fillOpacity: 0, | |
| }).addTo(threatMap); | |
| pulseRings.push({ ring: pulseRing, color, phase: Math.random() }); | |
| }); | |
| // Draw connection lines between zones | |
| const connections = [[0, 2], [1, 8], [2, 9], [3, 4], [0, 1]]; | |
| connections.forEach(([a, b]) => { | |
| const za = threatZones[a]; | |
| const zb = threatZones[b]; | |
| L.polyline([[za.lat, za.lng], [zb.lat, zb.lng]], { | |
| color: '#06b6d4', | |
| weight: 1, | |
| opacity: 0.12, | |
| dashArray: '4, 6', | |
| }).addTo(threatMap); | |
| }); | |
| // Fix map size after layout settles | |
| setTimeout(() => { if (threatMap) threatMap.invalidateSize(); }, 100); | |
| // Start pulse animation | |
| animatePulseRings(); | |
| } | |
| function animatePulseRings() { | |
| if (!threatMap) return; | |
| pulseRings.forEach(p => { | |
| p.phase = (p.phase + 0.008) % 1; | |
| const radius = 800000 * p.phase + 80000; | |
| const opacity = 0.5 * (1 - p.phase); | |
| p.ring.setRadius(radius); | |
| p.ring.setStyle({ opacity: opacity, fillOpacity: 0 }); | |
| }); | |
| requestAnimationFrame(animatePulseRings); | |
| } | |
| // ---- Sparkline Chart ---- | |
| function initSparkline() { | |
| for (let i = 0; i < 60; i++) { | |
| state.sparklineData.push(Math.random() * 40 + 30); | |
| } | |
| drawSparkline(); | |
| setInterval(() => { | |
| state.sparklineData.shift(); | |
| state.sparklineData.push(Math.random() * 40 + 30); | |
| drawSparkline(); | |
| }, 1000); | |
| } | |
| function drawSparkline() { | |
| const canvas = document.getElementById('sparkline'); | |
| if (!canvas) return; | |
| const rect = canvas.parentElement.getBoundingClientRect(); | |
| canvas.width = rect.width; | |
| canvas.height = 50; | |
| const ctx = canvas.getContext('2d'); | |
| const w = canvas.width; | |
| const h = canvas.height; | |
| const data = state.sparklineData; | |
| const max = Math.max(...data); | |
| const min = Math.min(...data); | |
| const range = max - min || 1; | |
| ctx.clearRect(0, 0, w, h); | |
| // Fill gradient | |
| const gradient = ctx.createLinearGradient(0, 0, 0, h); | |
| gradient.addColorStop(0, 'rgba(6, 182, 212, 0.2)'); | |
| gradient.addColorStop(1, 'rgba(6, 182, 212, 0)'); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, h); | |
| data.forEach((val, i) => { | |
| const x = (i / (data.length - 1)) * w; | |
| const y = h - ((val - min) / range) * (h - 4) - 2; | |
| if (i === 0) ctx.lineTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.lineTo(w, h); | |
| ctx.closePath(); | |
| ctx.fillStyle = gradient; | |
| ctx.fill(); | |
| // Line | |
| ctx.beginPath(); | |
| data.forEach((val, i) => { | |
| const x = (i / (data.length - 1)) * w; | |
| const y = h - ((val - min) / range) * (h - 4) - 2; | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.strokeStyle = 'rgba(6, 182, 212, 0.7)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| // Endpoint dot | |
| const lastVal = data[data.length - 1]; | |
| const lastX = w; | |
| const lastY = h - ((lastVal - min) / range) * (h - 4) - 2; | |
| ctx.fillStyle = '#22d3ee'; | |
| ctx.beginPath(); | |
| ctx.arc(lastX - 1, lastY, 2.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // ---- System Health Simulation ---- | |
| function updateSystemHealth() { | |
| const metrics = [ | |
| { bar: 'cpuBar', val: 'cpuVal', base: 45, range: 30 }, | |
| { bar: 'memBar', val: 'memVal', base: 60, range: 20 }, | |
| { bar: 'netBar', val: 'netVal', base: 25, range: 35 }, | |
| { bar: 'diskBar', val: 'diskVal', base: 55, range: 10 }, | |
| ]; | |
| metrics.forEach(m => { | |
| const value = Math.floor(m.base + Math.random() * m.range); | |
| const barEl = document.getElementById(m.bar); | |
| const valEl = document.getElementById(m.val); | |
| if (barEl) barEl.style.width = `${value}%`; | |
| if (valEl) valEl.textContent = `${value}%`; | |
| }); | |
| } | |
| // ---- Scan Simulation ---- | |
| function startScan(type) { | |
| if (state.scanning) { | |
| showToast('Scan already in progress'); | |
| return; | |
| } | |
| const target = document.getElementById('scanTarget'); | |
| const targetValue = target && target.value.trim() ? target.value.trim() : 'target.local'; | |
| state.scanning = true; | |
| const progressDiv = document.getElementById('scanProgress'); | |
| const stageEl = document.getElementById('scanStage'); | |
| const percentEl = document.getElementById('scanPercent'); | |
| const barEl = document.getElementById('scanBar'); | |
| const queueEl = document.getElementById('scanQueue'); | |
| if (progressDiv) progressDiv.classList.remove('hidden'); | |
| state.scanQueue++; | |
| if (queueEl) queueEl.textContent = state.scanQueue; | |
| const typeNames = { recon: 'Reconnaissance', vuln: 'Vulnerability', dork: 'Google Dork', full: 'Full Audit' }; | |
| const stages = { | |
| recon: ['Resolving target...', 'Port scanning...', 'Service detection...', 'OS fingerprinting...', 'Generating report...'], | |
| vuln: ['Loading vulnerability DB...', 'Testing injection points...', 'Checking misconfigurations...', 'Running exploit checks...', 'Compiling results...'], | |
| dork: ['Connecting to Google...', 'Building dork queries...', 'Analyzing results...', 'Checking cached pages...', 'Aggregating findings...'], | |
| full: ['Initializing full audit...', 'Phase 1: Recon...', 'Phase 2: Vuln scanning...', 'Phase 3: Dork analysis...', 'Phase 4: Report generation...'], | |
| }; | |
| const scanStages = stages[type] || stages.recon; | |
| let currentStage = 0; | |
| let progress = 0; | |
| addLog(`Starting ${typeNames[type] || type} scan on ${targetValue}`, 'system'); | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 8 + 2; | |
| if (progress >= 100) progress = 100; | |
| const stageIndex = Math.min(Math.floor(progress / 25), scanStages.length - 1); | |
| if (stageIndex !== currentStage) { | |
| currentStage = stageIndex; | |
| addLog(scanStages[currentStage], 'info'); | |
| } | |
| if (stageEl) stageEl.textContent = scanStages[currentStage]; | |
| if (percentEl) percentEl.textContent = `${Math.floor(progress)}%`; | |
| if (barEl) barEl.style.width = `${progress}%`; | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| state.scanning = false; | |
| state.scanQueue--; | |
| if (queueEl) queueEl.textContent = state.scanQueue; | |
| state.scanCount += Math.floor(Math.random() * 5) + 1; | |
| animateCounter(document.getElementById('scanCount'), state.scanCount, 500); | |
| addLog(`${typeNames[type] || type} scan complete for ${targetValue}`, 'success'); | |
| showToast(`${typeNames[type]} scan complete!`); | |
| // Add some discoveries from the scan | |
| addDiscovery(); | |
| addDiscovery(); | |
| setTimeout(() => { | |
| if (progressDiv) progressDiv.classList.add('hidden'); | |
| if (barEl) barEl.style.width = '0%'; | |
| }, 2000); | |
| } | |
| }, 300); | |
| } | |
| // ---- Operational Mode (Real APIs) ---- | |
| let cveFeedInterval = null; | |
| async function startOperationalMode() { | |
| // System health updates (browser-side metrics) | |
| updateSystemHealth(); | |
| setInterval(updateSystemHealth, 3000); | |
| // Initial logs | |
| addLog('SentinelAI Command Center initialized — OPERATIONAL MODE', 'system'); | |
| addLog('Real API services: ipinfo.io · NVD CVE · Google DNS', 'success'); | |
| addLog('All monitoring subsystems online', 'success'); | |
| addLog('GDorks engine loaded — 36 dork templates ready', 'info'); | |
| addLog('Network scanner module active — ipinfo.io + DNS resolve', 'info'); | |
| // Fetch initial CVE feed from NVD | |
| addLog('Fetching real-time CVE data from NVD...', 'system'); | |
| await fetchCVeFeed(); | |
| // Poll NVD every 60s for new CVEs (respecting rate limits) | |
| cveFeedInterval = setInterval(() => fetchCVeFeed(), 60000); | |
| // Update threat level based on real data | |
| setInterval(updateThreatLevelFromData, 15000); | |
| // Resize handler | |
| window.addEventListener('resize', () => { | |
| if (threatMap) threatMap.invalidateSize(); | |
| drawSparkline(); | |
| }); | |
| } | |
| // ---- Fetch real CVE data from NVD ---- | |
| async function fetchCVeFeed() { | |
| try { | |
| const keywords = ['injection', 'xss', 'ransomware', 'zero-day', 'exploit', 'authentication']; | |
| const kw = keywords[Math.floor(Math.random() * keywords.length)]; | |
| const cves = await SentinelAPI.searchCVEs(kw, 5); | |
| if (cves.length === 0) return; | |
| cves.forEach(cve => { | |
| const timeStr = new Date(cve.published).toTimeString().split(' ')[0]; | |
| const sevMap = { critical: 'critical', high: 'high', medium: 'medium', low: 'low' }; | |
| const sev = sevMap[cve.severity] || 'medium'; | |
| // Add to alerts if high/critical | |
| if (sev === 'critical' || sev === 'high') { | |
| const alertObj = { | |
| severity: sev, | |
| message: `${cve.id}: ${cve.description.substring(0, 80)}...`, | |
| time: timeStr, | |
| icon: sev === 'critical' ? 'shield-alert' : 'alert-triangle', | |
| colors: sev === 'critical' | |
| ? { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', badge: 'bg-red-500/20 text-red-400' } | |
| : { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', badge: 'bg-amber-500/20 text-amber-400' }, | |
| }; | |
| state.alerts.unshift(alertObj); | |
| if (state.alerts.length > 20) state.alerts.pop(); | |
| } | |
| // Add to discoveries | |
| state.discoveries.unshift({ | |
| type: 'vuln', | |
| icon: 'bug', | |
| color: sev === 'critical' ? 'red' : sev === 'high' ? 'amber' : 'cyan', | |
| msg: `${cve.id} (CVSS ${cve.score}) — ${cve.description.substring(0, 60)}...`, | |
| time: timeStr.substring(0, 5), | |
| }); | |
| if (state.discoveries.length > 15) state.discoveries.pop(); | |
| state.threatCount++; | |
| addLog(`NVD: ${cve.id} — ${cve.severity.toUpperCase()} CVSS ${cve.score}`, sev === 'critical' ? 'error' : 'warning'); | |
| }); | |
| renderAlerts(); | |
| updateAlertCount(); | |
| renderDiscoveries(); | |
| animateCounter(document.getElementById('threatCount'), state.threatCount, 500); | |
| } catch (e) { | |
| addLog('NVD API暂时不可用 — retrying...', 'warning'); | |
| } | |
| } | |
| // ---- Update threat level from real data ---- | |
| function updateThreatLevelFromData() { | |
| const critCount = state.alerts.filter(a => a.severity === 'critical').length; | |
| const highCount = state.alerts.filter(a => a.severity === 'high').length; | |
| let level = 'LOW'; | |
| if (critCount > 2) level = 'HIGH'; | |
| else if (critCount > 0 || highCount > 3) level = 'MEDIUM'; | |
| const indicator = document.getElementById('threatLevel'); | |
| const textEl = document.getElementById('threatLevelText'); | |
| if (indicator && textEl) { | |
| textEl.textContent = level; | |
| indicator.className = 'threat-indicator flex items-center gap-2 px-3 py-1.5 rounded-lg border'; | |
| if (level === 'LOW') { | |
| indicator.classList.add('bg-green-500/10', 'border-green-500/20'); | |
| textEl.className = 'text-xs font-semibold text-green-400 font-mono'; | |
| indicator.querySelector('.w-2').className = 'w-2 h-2 bg-green-400 rounded-full animate-pulse'; | |
| } else if (level === 'MEDIUM') { | |
| indicator.classList.add('bg-amber-500/10', 'border-amber-500/20'); | |
| textEl.className = 'text-xs font-semibold text-amber-400 font-mono'; | |
| indicator.querySelector('.w-2').className = 'w-2 h-2 bg-amber-400 rounded-full animate-pulse'; | |
| } else { | |
| indicator.classList.add('bg-red-500/10', 'border-red-500/20'); | |
| textEl.className = 'text-xs font-semibold text-red-400 font-mono'; | |
| indicator.querySelector('.w-2').className = 'w-2 h-2 bg-red-400 rounded-full animate-pulse'; | |
| } | |
| } | |
| } | |
| // ---- Mobile Menu Toggle ---- | |
| function toggleMobileMenu() { | |
| const menu = document.getElementById('mobileMenu'); | |
| if (menu) menu.classList.toggle('hidden'); | |
| } | |
| // ---- Toast ---- | |
| function showToast(message) { | |
| const toast = document.getElementById('ccToast'); | |
| const msgEl = document.getElementById('ccToastMsg'); | |
| if (!toast || !msgEl) return; | |
| msgEl.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 2500); | |
| } |