diff --git a/dashboard.html b/dashboard.html index 5b4f774..813c054 100644 --- a/dashboard.html +++ b/dashboard.html @@ -123,130 +123,6 @@

Navigation

- + diff --git a/package-lock.json b/package-lock.json index 2e2ef3e..cf2731c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", "node-fetch": "^2.7.0" } }, @@ -302,6 +303,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -465,6 +484,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index c5a6b35..29581ce 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", "node-fetch": "^2.7.0" } } diff --git a/public/dashboard.js b/public/dashboard.js new file mode 100644 index 0000000..0ced2bb --- /dev/null +++ b/public/dashboard.js @@ -0,0 +1,174 @@ +// public/dashboard.js + +// Theme toggle +document.getElementById('themeToggle').addEventListener('click', () => { + const isLight = document.documentElement.classList.toggle('light-mode'); + localStorage.setItem('theme', isLight ? 'light' : 'dark'); +}); + +// State for sorting +let sortCol = 'time'; +let sortDesc = true; + +function renderTable(history) { + const recentEl = document.getElementById('recentScans'); + if (history.length === 0) { + recentEl.innerHTML = '

No scans recorded yet. Scan a URL to get started.

'; + return; + } + + // Sort logic + let sorted = [...history]; + sorted.sort((a, b) => { + if (sortCol === 'url') { + const uA = a.url || ''; + const uB = b.url || ''; + return sortDesc ? uB.localeCompare(uA) : uA.localeCompare(uB); + } else if (sortCol === 'status') { + const sA = a.status || ''; + const sB = b.status || ''; + return sortDesc ? sB.localeCompare(sA) : sA.localeCompare(sB); + } else { // time + const tA = new Date(a.timestamp || 0).getTime(); + const tB = new Date(b.timestamp || 0).getTime(); + return sortDesc ? tB - tA : tA - tB; + } + }); + + const recent = sorted.slice(0, 50); // display up to 50 + + const getSortIcon = (col) => { + if (sortCol !== col) return ''; + return sortDesc ? '' : ''; + }; + + recentEl.innerHTML = ` +
+ + + + + + + + + + ${recent.map(r => ` + + + + + `).join('')} + +
URL ${getSortIcon('url')}Result ${getSortIcon('status')}Scanned ${getSortIcon('time')}
+ ${r.url || '—'} + + ${r.status === 'safe' ? 'SAFE' : 'DANGER'} + + ${r.timestamp ? new Date(r.timestamp).toLocaleString(undefined, {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'}) : '—'} +
+
`; + + // Attach sorting events + document.querySelectorAll('.sort-header').forEach(th => { + th.addEventListener('click', () => { + const col = th.getAttribute('data-col'); + if (sortCol === col) { + sortDesc = !sortDesc; + } else { + sortCol = col; + sortDesc = (col === 'time'); // Default to desc for time, asc for others + } + renderTable(history); + }); + }); +} + +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 } + } + } + } + }); + + // Initial render + renderTable(history); + + // Clear button + document.getElementById('clearHistoryBtn').addEventListener('click', () => { + if (confirm('Permanently delete your entire scan history?')) { + localStorage.removeItem('cybershield_history'); + window.location.reload(); + } + }); +}); diff --git a/server.js b/server.js index 4f41058..12c97da 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,6 @@ const express = require('express'); const cors = require('cors'); +const { rateLimit } = require('express-rate-limit'); const dotenv = require('dotenv'); dotenv.config(); @@ -60,8 +61,17 @@ app.get('/', (req, res) => { res.json({ status: 'CyberShield backend running', port: PORT, version: '2.0' }); }); +// ─── Rate Limiter Setup ─── +const checkLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + limit: 100, // max 100 requests per windowMs + message: { error: 'Too many URL scan requests from this IP, please try again after 15 minutes.' }, + standardHeaders: 'draft-8', + legacyHeaders: false, +}); + // ─── URL Safe Browsing check ─── -app.post('/check', async (req, res) => { +app.post('/check', checkLimiter, async (req, res) => { const userUrl = req.body.url; if (!userUrl) return res.status(400).json({ error: 'No URL provided' });