diff --git a/html/dashboard.html b/html/dashboard.html index 4afad06..67a9291 100644 --- a/html/dashboard.html +++ b/html/dashboard.html @@ -85,11 +85,34 @@

Executive Overview

Evidence Ingestion

+ + +
+
+ + 60s +
+ +
+ 5s + 1h +
+

Gaps exceeding this threshold will be flagged as anomalies

+
+
- - + +
- +
@@ -128,10 +151,20 @@

Incident Registry

0 Flagged + + + +
+ diff --git a/js/dashboard.js b/js/dashboard.js index f13a878..cc55435 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -1,6 +1,8 @@ let chart; let lastScanResults = null; let flaggedIncidents = new Set(); +let batchResults = []; // Store results from multiple files +let currentThreshold = 60; // Default threshold value // 1. IMPROVED VERTICAL SCANNER PLUGIN const verticalLinePlugin = { @@ -33,13 +35,27 @@ window.addEventListener("DOMContentLoaded", () => { flaggedIncidents = new Set(JSON.parse(savedFlags)); updateFlagCount(); } + + // Load saved threshold + const savedThreshold = localStorage.getItem("analysis_threshold"); + if (savedThreshold) { + currentThreshold = parseInt(savedThreshold); + document.getElementById("thresholdSlider").value = currentThreshold; + updateThresholdDisplay(currentThreshold); + } + loadLastSession(); }); function loadLastSession() { const savedData = localStorage.getItem("last_forensic_scan"); const savedMeta = localStorage.getItem("last_scan_metadata"); - if (savedData && savedMeta) { + const savedBatch = localStorage.getItem("last_batch_results"); + + if (savedBatch) { + batchResults = JSON.parse(savedBatch); + renderBatchResults(batchResults); + } else if (savedData && savedMeta) { lastScanResults = JSON.parse(savedData); const meta = JSON.parse(savedMeta); const timeEl = document.getElementById("lastScanTime"); @@ -50,34 +66,84 @@ function loadLastSession() { } } +// Update threshold display +function updateThresholdDisplay(value) { + currentThreshold = parseInt(value); + const display = document.getElementById("thresholdValue"); + + // Format display based on value + let displayText; + if (currentThreshold >= 3600) { + displayText = (currentThreshold / 3600).toFixed(1) + "h"; + } else if (currentThreshold >= 60) { + displayText = Math.floor(currentThreshold / 60) + "m"; + } else { + displayText = currentThreshold + "s"; + } + + if (display) display.innerText = displayText; + + // Save to localStorage + localStorage.setItem("analysis_threshold", currentThreshold); +} + async function analyzeLogs(event) { const fileInput = document.getElementById("logFile"); - const file = fileInput.files[0]; - if (!file) return showToast("Critical: No source file selected"); + const files = Array.from(fileInput.files); + + if (files.length === 0) { + return showToast("Critical: No source file selected"); + } const overlay = document.getElementById("scanOverlay"); const statusText = document.getElementById("loaderStatus"); overlay.classList.remove("hidden"); - const formData = new FormData(); - formData.append("file", file); - formData.append("threshold", 60); - + const token = localStorage.getItem("access_token"); + batchResults = []; // Reset batch results + try { - const token = localStorage.getItem("access_token"); - const res = await fetch("http://localhost:8000/analyze", { - method: "POST", - headers: { "Authorization": `Bearer ${token}` }, - body: formData, - }); - if (res.status === 401) { - localStorage.removeItem("access_token"); - window.location.href = "index.html"; - return; + // Process each file sequentially + for (let i = 0; i < files.length; i++) { + const file = files[i]; + statusText.innerText = `Processing file ${i + 1}/${files.length}: ${file.name}`; + + const formData = new FormData(); + formData.append("file", file); + formData.append("threshold", currentThreshold); + + try { + const res = await fetch("http://localhost:8000/analyze", { + method: "POST", + headers: { "Authorization": `Bearer ${token}` }, + body: formData, + }); + + if (res.status === 401) { + localStorage.removeItem("access_token"); + window.location.href = "index.html"; + return; + } + + if (!res.ok) throw new Error("Connection Refused"); + + const data = await res.json(); + + // Add file metadata to results + batchResults.push({ + fileName: file.name, + timestamp: new Date().toLocaleString().toUpperCase(), + data: data, + threshold: currentThreshold + }); + + } catch (e) { + console.error(`Error processing ${file.name}:`, e); + showToast(`Error processing ${file.name}`); + } } - if (!res.ok) throw new Error("Connection Refused"); - const data = await res.json(); + // Animation steps const steps = [ "Hashing Payload...", "Mapping Voids...", @@ -89,23 +155,120 @@ async function analyzeLogs(event) { await new Promise((r) => setTimeout(r, 400)); } - const meta = { - timestamp: new Date().toLocaleString().toUpperCase(), - fileName: file.name, - }; - localStorage.setItem("last_forensic_scan", JSON.stringify(data)); - localStorage.setItem("last_scan_metadata", JSON.stringify(meta)); - - lastScanResults = data; - renderResults(data); - showToast("Analysis Finalized"); + // Save results + if (batchResults.length === 1) { + // Single file - use legacy format + const result = batchResults[0]; + const meta = { + timestamp: result.timestamp, + fileName: result.fileName, + }; + localStorage.setItem("last_forensic_scan", JSON.stringify(result.data)); + localStorage.setItem("last_scan_metadata", JSON.stringify(meta)); + localStorage.removeItem("last_batch_results"); + + lastScanResults = result.data; + renderResults(result.data); + } else { + // Multiple files - batch mode + localStorage.setItem("last_batch_results", JSON.stringify(batchResults)); + localStorage.removeItem("last_forensic_scan"); + localStorage.removeItem("last_scan_metadata"); + + renderBatchResults(batchResults); + } + + showToast(`Analysis Complete: ${batchResults.length} file(s) processed`); + } catch (e) { showToast("Backend Link Error: Ensure server is online"); + console.error(e); } finally { overlay.classList.add("hidden"); } } +function renderBatchResults(results) { + if (!results || results.length === 0) return; + + // Calculate aggregate statistics + let totalGaps = 0; + let totalScore = 0; + let allIncidents = []; + + results.forEach(result => { + totalGaps += result.data.total_gaps; + totalScore += result.data.integrity_score; + + // Add file name to each incident + result.data.incidents.forEach(incident => { + allIncidents.push({ + ...incident, + fileName: result.fileName + }); + }); + }); + + const avgScore = (totalScore / results.length).toFixed(1); + const avgRisk = (100 - avgScore).toFixed(1); + + // Update KPI cards with aggregate data + document.getElementById("integrityScoreCard").innerText = avgScore + "%"; + document.getElementById("financialRisk").innerText = avgRisk + "%"; + document.getElementById("gapCount").innerText = totalGaps; + + // Update metadata + document.getElementById("lastScanTime").innerText = results[0].timestamp; + document.getElementById("lastFileName").innerText = `${results.length} files analyzed`; + + // Show batch summary + const batchSummary = document.getElementById("batchSummary"); + const summaryContent = document.getElementById("batchSummaryContent"); + + if (batchSummary && summaryContent) { + batchSummary.classList.remove("hidden"); + + summaryContent.innerHTML = results.map(result => { + const score = result.data.integrity_score; + const scoreColor = score >= 90 ? "text-emerald-400" : score >= 70 ? "text-amber-400" : "text-red-400"; + + return ` +
+
+ +

${result.fileName}

+
+
+
+

Integrity

+

${score}%

+
+
+

Gaps

+

${result.data.total_gaps}

+
+
+
+ `; + }).join(''); + } + + // Update registry with all incidents + updateRegistryTable(allIncidents, true); + + // Update heatmap with first file's data + updateHeatmapBar(results[0].data.incidents); + + // Update chart with aggregate view + updateChart(allIncidents); + + // Hide signature card for batch mode + const signatureCard = document.getElementById("signatureCard"); + if (signatureCard) { + signatureCard.classList.add("hidden"); + } +} + function renderResults(data) { if (!data || !data.incidents) return; @@ -191,7 +354,7 @@ function renderResults(data) { updateChart(data.incidents); } -function updateRegistryTable(incidents) { +function updateRegistryTable(incidents, isBatch = false) { const tbody = document.getElementById("incidentBody"); if (!tbody) return; @@ -205,6 +368,14 @@ function updateRegistryTable(incidents) { return ` + ${isBatch ? ` + + ` : ''}
File Source Time Window Duration Analysis Signature
+
+ + ${inc.fileName || 'N/A'} +
+
@@ -369,21 +540,41 @@ function updateChart(incidents) { } function handleSortChange(criteria) { - if (!lastScanResults || !lastScanResults.incidents) { + let allIncidents; + let isBatch = false; + + // Determine if we're in batch mode or single file mode + if (batchResults.length > 0) { + isBatch = true; + allIncidents = []; + batchResults.forEach(result => { + result.data.incidents.forEach(incident => { + allIncidents.push({ + ...incident, + fileName: result.fileName + }); + }); + }); + } else if (lastScanResults && lastScanResults.incidents) { + allIncidents = lastScanResults.incidents; + } else { showToast("No data to sort"); return; } + const placeholder = document.getElementById("sortPlaceholder"); + if (criteria === "high") { - lastScanResults.incidents.sort((a, b) => b.duration - a.duration); + allIncidents.sort((a, b) => b.duration - a.duration); showToast("Prioritizing Critical Voids"); if (placeholder) placeholder.disabled = true; } else if (criteria === "low") { - lastScanResults.incidents.sort((a, b) => a.duration - b.duration); + allIncidents.sort((a, b) => a.duration - b.duration); showToast("Prioritizing Minor Anomalies"); if (placeholder) placeholder.disabled = true; } - updateRegistryTable(lastScanResults.incidents); + + updateRegistryTable(allIncidents, isBatch); } function toggleFlag(index) { @@ -419,11 +610,17 @@ function updateFileName() { const fileInput = document.getElementById("logFile"); const fileNameDisplay = document.getElementById("fileNameDisplay"); if (fileInput.files.length > 0) { - fileNameDisplay.innerText = fileInput.files[0].name; + if (fileInput.files.length === 1) { + fileNameDisplay.innerText = fileInput.files[0].name; + } else { + fileNameDisplay.innerText = `${fileInput.files.length} files selected`; + } fileNameDisplay.classList.remove("text-slate-500"); fileNameDisplay.classList.add("text-blue-400"); } else { - fileNameDisplay.innerText = "Select Log Source"; + fileNameDisplay.innerText = "Select Log Source(s)"; + fileNameDisplay.classList.remove("text-blue-400"); + fileNameDisplay.classList.add("text-slate-500"); } } @@ -433,21 +630,57 @@ function logout() { } function exportForensicJSON() { - if (!lastScanResults) return showToast("Critical: No scan data available"); - const report = { - header: { - session_id: `CERT-${Math.random().toString(36).substr(2, 9).toUpperCase()}`, - timestamp: new Date().toISOString(), - operator: "L1_ADMIN_04", - }, - integrity_summary: { - file_source: - document.getElementById("lastFileName")?.innerText || "Unknown", - score: document.getElementById("integrityScoreCard")?.innerText || "0%", - sha256_hash: `3A7C${Math.random().toString(16).substr(2, 12).toUpperCase()}`, - }, - void_data: lastScanResults.incidents, - }; + if (!lastScanResults && batchResults.length === 0) { + return showToast("Critical: No scan data available"); + } + + let report; + + if (batchResults.length > 0) { + // Batch mode export + report = { + header: { + session_id: `CERT-${Math.random().toString(36).substr(2, 9).toUpperCase()}`, + timestamp: new Date().toISOString(), + operator: "L1_ADMIN_04", + mode: "BATCH_ANALYSIS" + }, + batch_summary: { + total_files: batchResults.length, + threshold_used: currentThreshold, + files: batchResults.map(r => ({ + filename: r.fileName, + integrity_score: r.data.integrity_score, + total_gaps: r.data.total_gaps, + sha256_hash: `3A7C${Math.random().toString(16).substr(2, 12).toUpperCase()}` + })) + }, + detailed_results: batchResults.map(r => ({ + file_source: r.fileName, + analyzed_at: r.timestamp, + integrity_score: r.data.integrity_score, + void_data: r.data.incidents + })) + }; + } else { + // Single file mode export + report = { + header: { + session_id: `CERT-${Math.random().toString(36).substr(2, 9).toUpperCase()}`, + timestamp: new Date().toISOString(), + operator: "L1_ADMIN_04", + }, + integrity_summary: { + file_source: + document.getElementById("lastFileName")?.innerText || "Unknown", + score: document.getElementById("integrityScoreCard")?.innerText || "0%", + threshold_used: currentThreshold, + sha256_hash: `3A7C${Math.random().toString(16).substr(2, 12).toUpperCase()}`, + }, + void_data: lastScanResults.incidents, + }; + } + const blob = new Blob([JSON.stringify(report, null, 4)], { type: "application/json", }); @@ -460,12 +693,38 @@ function exportForensicJSON() { } function exportRegistryCSV() { - if (!lastScanResults || !lastScanResults.incidents.length) + let allIncidents = []; + let isBatch = false; + + if (batchResults.length > 0) { + isBatch = true; + batchResults.forEach(result => { + result.data.incidents.forEach(incident => { + allIncidents.push({ + ...incident, + fileName: result.fileName + }); + }); + }); + } else if (lastScanResults && lastScanResults.incidents.length) { + allIncidents = lastScanResults.incidents; + } else { return showToast("Notice: Incident Registry is empty"); - let csv = "Incident,Start,End,Duration(s),Severity\n"; - lastScanResults.incidents.forEach((inc, i) => { - csv += `VOID-${i + 1},${inc.start},${inc.end},${inc.duration},${inc.duration > 300 ? "CRITICAL" : "WARNING"}\n`; - }); + } + + let csv; + if (isBatch) { + csv = "File,Incident,Start,End,Duration(s),Severity\n"; + allIncidents.forEach((inc, i) => { + csv += `${inc.fileName},VOID-${i + 1},${inc.start},${inc.end},${inc.duration},${inc.duration > 300 ? "CRITICAL" : "WARNING"}\n`; + }); + } else { + csv = "Incident,Start,End,Duration(s),Severity\n"; + allIncidents.forEach((inc, i) => { + csv += `VOID-${i + 1},${inc.start},${inc.end},${inc.duration},${inc.duration > 300 ? "CRITICAL" : "WARNING"}\n`; + }); + } + const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -514,20 +773,43 @@ function exportChartAsJPG() { // Search/Filter functionality for Incident Registry function filterRegistry() { const searchTerm = document.getElementById("searchInput").value.toLowerCase(); - if (!lastScanResults || !lastScanResults.incidents) return; - let filteredIncidents = lastScanResults.incidents; + let allIncidents; + let isBatch = false; + + // Determine if we're in batch mode or single file mode + if (batchResults.length > 0) { + isBatch = true; + allIncidents = []; + batchResults.forEach(result => { + result.data.incidents.forEach(incident => { + allIncidents.push({ + ...incident, + fileName: result.fileName + }); + }); + }); + } else if (lastScanResults && lastScanResults.incidents) { + allIncidents = lastScanResults.incidents; + } else { + return; + } + + let filteredIncidents = allIncidents; if (searchTerm) { - filteredIncidents = lastScanResults.incidents.filter(inc => { + filteredIncidents = allIncidents.filter(inc => { // Search by timestamp (start or end time) const timeMatch = inc.start.toLowerCase().includes(searchTerm) || inc.end.toLowerCase().includes(searchTerm); // Search by duration const durationMatch = inc.duration.toString().includes(searchTerm); - return timeMatch || durationMatch; + // Search by filename (if in batch mode) + const fileMatch = isBatch && inc.fileName && inc.fileName.toLowerCase().includes(searchTerm); + + return timeMatch || durationMatch || fileMatch; }); } - updateRegistryTable(filteredIncidents); + updateRegistryTable(filteredIncidents, isBatch); } \ No newline at end of file diff --git a/main.py b/main.py index f990fd5..ca6eddb 100644 --- a/main.py +++ b/main.py @@ -94,6 +94,14 @@ async def upload_log( try: numeric_threshold = int(threshold) results = analyze_logs(temp_path, numeric_threshold) + + # Add metadata to response + results["metadata"] = { + "filename": file.filename, + "threshold_used": numeric_threshold, + "analyzed_at": datetime.utcnow().isoformat() + } + return results except Exception as e: print(f"Analysis Error: {e}") diff --git a/styles/dashboard.css b/styles/dashboard.css index 8ab3766..311db7e 100644 --- a/styles/dashboard.css +++ b/styles/dashboard.css @@ -167,6 +167,49 @@ header { border-radius: 0 12px 12px 0; } +/* ── Threshold Slider ────────────────────────────────────────────────────── */ +#thresholdSlider { + -webkit-appearance: none; + appearance: none; + background: var(--bg-secondary); + outline: none; + border-radius: 8px; + transition: all 0.3s ease; +} + +#thresholdSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--accent-blue); + cursor: pointer; + border-radius: 50%; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); + transition: all 0.2s ease; +} + +#thresholdSlider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 0 15px rgba(59, 130, 246, 0.8); +} + +#thresholdSlider::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--accent-blue); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); + transition: all 0.2s ease; +} + +#thresholdSlider::-moz-range-thumb:hover { + transform: scale(1.2); + box-shadow: 0 0 15px rgba(59, 130, 246, 0.8); +} + /* ── Light mode table rows ───────────────────────────────────────────────── */ html[data-theme="light"] #incidentBody tr:hover { background: rgba(37, 99, 235, 0.05) !important; diff --git a/test_logs/access.log b/test_logs/access.log new file mode 100644 index 0000000..5ace9d1 --- /dev/null +++ b/test_logs/access.log @@ -0,0 +1,10 @@ +2026-05-02 08:00:00 GET /api/status 200 15ms +2026-05-02 08:00:05 GET /api/users 200 23ms +2026-05-02 08:00:10 POST /api/login 200 45ms +2026-05-02 08:00:15 GET /api/dashboard 200 18ms +2026-05-02 08:01:00 GET /api/reports 200 120ms +2026-05-02 08:01:30 GET /api/analytics 200 89ms +2026-05-02 08:12:00 GET /api/status 200 12ms +2026-05-02 08:12:15 POST /api/data 201 67ms +2026-05-02 08:12:30 GET /api/export 200 234ms +2026-05-02 08:13:00 DELETE /api/cache 204 8ms diff --git a/test_logs/auth.log b/test_logs/auth.log new file mode 100644 index 0000000..e094286 --- /dev/null +++ b/test_logs/auth.log @@ -0,0 +1,11 @@ +2026-05-02 08:00:00 [INFO] User admin logged in from 192.168.1.100 +2026-05-02 08:00:15 [INFO] Authentication successful for user admin +2026-05-02 08:00:30 [INFO] Session created: session_12345 +2026-05-02 08:01:00 [INFO] User admin accessed /dashboard +2026-05-02 08:01:30 [INFO] User admin accessed /reports +2026-05-02 08:02:00 [INFO] User admin accessed /settings +2026-05-02 08:04:30 [WARN] Failed login attempt for user guest from 192.168.1.200 +2026-05-02 08:04:45 [WARN] Failed login attempt for user guest from 192.168.1.200 +2026-05-02 08:05:00 [WARN] IP 192.168.1.200 blocked due to multiple failed attempts +2026-05-02 08:15:00 [INFO] User admin logged out +2026-05-02 08:15:15 [INFO] Session terminated: session_12345 diff --git a/test_logs/syslog.log b/test_logs/syslog.log new file mode 100644 index 0000000..8d2cf46 --- /dev/null +++ b/test_logs/syslog.log @@ -0,0 +1,10 @@ +2026-05-02 08:00:00 [SYSTEM] Service started: web-server +2026-05-02 08:00:05 [SYSTEM] Service started: database +2026-05-02 08:00:10 [SYSTEM] Service started: cache-server +2026-05-02 08:05:00 [SYSTEM] Backup process initiated +2026-05-02 08:05:30 [SYSTEM] Backup completed successfully +2026-05-02 08:20:00 [ERROR] Database connection timeout +2026-05-02 08:20:15 [ERROR] Retry attempt 1/3 +2026-05-02 08:20:30 [INFO] Database connection restored +2026-05-02 08:25:00 [SYSTEM] Health check passed +2026-05-02 08:30:00 [SYSTEM] Health check passed