Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ node server.js
## 🚀 Roadmap

- [ ] 🔐 Email breach checker (HIBP API)
- [ ] 📊 Threat analytics dashboard with charts
- [x] 📊 Threat analytics dashboard with charts
- [ ] 🌍 Browser extension
- [ ] 🤖 AI-based threat detection
- [ ] 📤 Export scan logs for offline analysis
Expand Down
135 changes: 8 additions & 127 deletions dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,13 @@ <h1>Threat<br><span>Analytics Dashboard</span></h1>
<div class="dashboard-grid">
<section class="dashboard-card" style="grid-column: span 2;">
<h2>Threat Distribution</h2>
<div style="position:relative; height:280px; width:100%">
<canvas id="analyticsChart"></canvas>
<div style="display:flex; gap:18px; align-items:stretch; height:280px; width:100%">
<div style="flex:1; min-width:200px; position:relative">
<canvas id="statusChart"></canvas>
</div>
<div style="flex:2; min-width:300px; position:relative">
<canvas id="threatBarChart"></canvas>
</div>
</div>
</section>

Expand Down Expand Up @@ -123,130 +128,6 @@ <h4>Navigation</h4>
</footer>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Theme toggle
document.getElementById('themeToggle').addEventListener('click', () => {
const isLight = document.documentElement.classList.toggle('light-mode');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
});

document.addEventListener('DOMContentLoaded', () => {
const history = JSON.parse(localStorage.getItem('cybershield_history') || '[]');

const total = history.length;
const safeSc = history.filter(r => r.status === 'safe').length;
const dangerSc = history.filter(r => r.status === 'danger').length;
const safeRatio = total > 0 ? Math.round((safeSc / total) * 100) : 0;

document.getElementById('dbTotalScans').textContent = total;
document.getElementById('dbThreatsBlocked').textContent = dangerSc;
document.getElementById('dbSafeRatio').textContent = `${safeRatio}%`;

// Threat type breakdown
let malware = 0, phishing = 0, unwanted = 0, harmful = 0;
history.forEach(r => {
if (r.threats && Array.isArray(r.threats)) {
r.threats.forEach(t => {
if (t === 'MALWARE') malware++;
if (t === 'SOCIAL_ENGINEERING') phishing++;
if (t === 'UNWANTED_SOFTWARE') unwanted++;
if (t === 'POTENTIALLY_HARMFUL_APPLICATION') harmful++;
});
}
});

// Resolve colors from CSS vars for chart
const isLight = document.documentElement.classList.contains('light-mode');
const gridColor = isLight ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)';
const tickColor = isLight ? '#3a5580' : '#4a6580';

const ctx = document.getElementById('analyticsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Safe Scans', 'Malware', 'Phishing', 'Unwanted / Harmful'],
datasets: [{
label: 'Count',
data: [safeSc, malware, phishing, (unwanted + harmful)],
backgroundColor: [
'rgba(0, 255, 136, 0.5)',
'rgba(255, 51, 102, 0.5)',
'rgba(255, 204, 0, 0.5)',
'rgba(0, 245, 255, 0.5)'
],
borderColor: [
'rgba(0, 255, 136, 1)',
'rgba(255, 51, 102, 1)',
'rgba(255, 204, 0, 1)',
'rgba(0, 245, 255, 1)'
],
borderWidth: 1,
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1, precision: 0, color: tickColor },
grid: { color: gridColor }
},
x: {
ticks: { color: tickColor },
grid: { display: false }
}
}
}
});

// Recent scans
const recentEl = document.getElementById('recentScans');
if (history.length === 0) {
recentEl.innerHTML = '<p style="color:var(--text-muted); text-align:center; padding:24px">No scans recorded yet. <a href="index.html" style="color:var(--accent-cyan)">Scan a URL</a> to get started.</p>';
} else {
const recent = [...history].reverse().slice(0, 10);
recentEl.innerHTML = `
<table style="width:100%; border-collapse:collapse">
<thead>
<tr style="border-bottom:1px solid var(--card-border)">
<th style="text-align:left; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600">URL</th>
<th style="text-align:center; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600; white-space:nowrap">Result</th>
<th style="text-align:right; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600; white-space:nowrap">Scanned</th>
</tr>
</thead>
<tbody>
${recent.map(r => `
<tr style="border-bottom:1px solid var(--card-border)">
<td style="padding:10px 8px; color:var(--text-sub); font-family:monospace; font-size:12px; max-width:340px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">
${r.url || '—'}
</td>
<td style="padding:10px 8px; text-align:center">
<span style="
display:inline-block; padding:3px 10px; border-radius:6px; font-size:11px; font-weight:700; letter-spacing:0.06em;
background:${r.status === 'safe' ? 'var(--safe-bg)' : 'var(--danger-bg)'};
color:${r.status === 'safe' ? 'var(--safe-color)' : 'var(--danger-color)'};
border:1px solid ${r.status === 'safe' ? 'var(--safe-border)' : 'var(--danger-border)'}
">${r.status === 'safe' ? 'SAFE' : 'DANGER'}</span>
</td>
<td style="padding:10px 8px; color:var(--text-muted); font-size:11px; text-align:right; white-space:nowrap">
${r.timestamp ? new Date(r.timestamp).toLocaleString(undefined, {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'}) : '—'}
</td>
</tr>`).join('')}
</tbody>
</table>`;
}

// Clear button
document.getElementById('clearHistoryBtn').addEventListener('click', () => {
if (confirm('Permanently delete your entire scan history?')) {
localStorage.removeItem('cybershield_history');
window.location.reload();
}
});
});
</script>
<script src="dashboard.js"></script>
</body>
</html>
176 changes: 176 additions & 0 deletions dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Dashboard JS: reads localStorage `cybershield_history` and renders charts + table

// Theme toggle handler
const themeToggleBtn = document.getElementById('themeToggle');
if (themeToggleBtn) {
themeToggleBtn.addEventListener('click', () => {
const isLight = document.documentElement.classList.toggle('light-mode');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
});
}

function formatDate(iso) {
try {
return iso ? new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
} catch (e) { return '—'; }
}

function safeInt(n){ return Number.isFinite(n) ? n : 0; }

function getHistory() {
try {
return JSON.parse(localStorage.getItem('cybershield_history') || '[]');
} catch (err) {
console.warn('Unable to parse scan history:', err);
return [];
}
}

document.addEventListener('DOMContentLoaded', () => {
const history = getHistory();

const total = history.length;
const safeSc = history.filter(r => r.status === 'safe').length;
const dangerSc = history.filter(r => r.status === 'danger').length;
const safeRatio = total > 0 ? Math.round((safeSc / total) * 100) : 0;

const elTotal = document.getElementById('dbTotalScans');
const elThreats = document.getElementById('dbThreatsBlocked');
const elRatio = document.getElementById('dbSafeRatio');
if (elTotal) elTotal.textContent = total;
if (elThreats) elThreats.textContent = dangerSc;
if (elRatio) elRatio.textContent = `${safeRatio}%`;

// Threat breakdown counts
let malware = 0, phishing = 0, unwanted = 0, harmful = 0;
history.forEach(r => {
if (r.threats && Array.isArray(r.threats)) {
r.threats.forEach(t => {
if (!t) return;
const key = String(t).toUpperCase();
if (key.includes('MALWARE')) malware++;
else if (key.includes('SOCIAL_ENGINEERING') || key.includes('PHISHING')) phishing++;
else if (key.includes('UNWANTED')) unwanted++;
else if (key.includes('POTENTIALLY_HARMFUL') || key.includes('HARMFUL')) harmful++;
});
}
});

// Resolve color hints
const isLight = document.documentElement.classList.contains('light-mode');
const gridColor = isLight ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)';
const tickColor = isLight ? '#3a5580' : '#4a6580';

// Pie chart: Safe vs Threat
const statusCtx = document.getElementById('statusChart');
if (statusCtx && window.Chart) {
const ctx1 = statusCtx.getContext('2d');
new Chart(ctx1, {
type: 'doughnut',
data: {
labels: ['Safe', 'Threats'],
datasets: [{
data: [safeInt(safeSc), safeInt(dangerSc)],
backgroundColor: ['rgba(0, 200, 120, 0.9)', 'rgba(255, 70, 90, 0.95)'],
borderWidth: 0,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { color: tickColor } }
}
}
});
}

// Bar chart: Threat types
const barCtx = document.getElementById('threatBarChart');
if (barCtx && window.Chart) {
const ctx2 = barCtx.getContext('2d');
new Chart(ctx2, {
type: 'bar',
data: {
labels: ['Malware', 'Phishing', 'Unwanted Software', 'Potentially Harmful'],
datasets: [{
label: 'Count',
data: [malware, phishing, unwanted, harmful],
backgroundColor: [
'rgba(255, 51, 102, 0.6)',
'rgba(255, 204, 0, 0.6)',
'rgba(0, 245, 255, 0.6)',
'rgba(120, 120, 255, 0.6)'
],
borderColor: [
'rgba(255, 51, 102, 1)',
'rgba(255, 204, 0, 1)',
'rgba(0, 245, 255, 1)',
'rgba(120, 120, 255, 1)'
],
borderWidth: 1,
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { color: tickColor }, grid: { color: gridColor } },
x: { ticks: { color: tickColor }, grid: { display: false } }
}
}
});
}

// Recent scans table
const recentEl = document.getElementById('recentScans');
if (recentEl) {
if (!history || history.length === 0) {
recentEl.innerHTML = '<p style="color:var(--text-muted); text-align:center; padding:24px">No scans recorded yet. <a href="index.html" style="color:var(--accent-cyan)">Scan a URL</a> to get started.</p>';
} else {
const recent = [...history].reverse().slice(0, 10);
recentEl.innerHTML = `
<table style="width:100%; border-collapse:collapse">
<thead>
<tr style="border-bottom:1px solid var(--card-border)">
<th style="text-align:left; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600">URL</th>
<th style="text-align:center; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600; white-space:nowrap">Result</th>
<th style="text-align:right; padding:10px 8px; font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.1em; font-weight:600; white-space:nowrap">Scanned</th>
</tr>
</thead>
<tbody>
${recent.map(r => `
<tr style="border-bottom:1px solid var(--card-border)">
<td style="padding:10px 8px; color:var(--text-sub); font-family:monospace; font-size:12px; max-width:340px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">
${r.url || '—'}
</td>
<td style="padding:10px 8px; text-align:center">
<span style="
display:inline-block; padding:3px 10px; border-radius:6px; font-size:11px; font-weight:700; letter-spacing:0.06em;
background:${r.status === 'safe' ? 'var(--safe-bg)' : 'var(--danger-bg)'};
color:${r.status === 'safe' ? 'var(--safe-color)' : 'var(--danger-color)'};
border:1px solid ${r.status === 'safe' ? 'var(--safe-border)' : 'var(--danger-border)'}
">${r.status === 'safe' ? 'SAFE' : 'DANGER'}</span>
</td>
<td style="padding:10px 8px; color:var(--text-muted); font-size:11px; text-align:right; white-space:nowrap">
${r.timestamp ? formatDate(r.timestamp) : '—'}
</td>
</tr>`).join('')}
</tbody>
</table>`;
}
}

// Clear history button
const clearBtn = document.getElementById('clearHistoryBtn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
if (confirm('Permanently delete your entire scan history?')) {
localStorage.removeItem('cybershield_history');
window.location.reload();
}
});
}
});
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"test-rate-limit": "node test-rate-limit.js"
},
"keywords": [],
"author": "",
Expand All @@ -14,6 +15,7 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"node-fetch": "^2.7.0"
"node-fetch": "^2.7.0",
"express-rate-limit": "^6.7.0"
}
}
Loading