diff --git a/dashboard.html b/dashboard.html index 58547e7..8510594 100644 --- a/dashboard.html +++ b/dashboard.html @@ -28,47 +28,40 @@ CyberShield Analytics

Threat Analytics Dashboard

-

Track your historical URL scans, threat detection trends, and export logs for deeper analysis.

+

Track your historical URL scans, threat detection trends, and real-time safe metrics.

-
+
Back to URL Scanner - 💬 Scam Detector Chat +
-
-
-

Feature Description

-

CyberShield currently scans URLs in real time and reports whether they are safe or malicious. The Threat Analytics Dashboard adds historical visibility and actionable insight to that workflow.

- -
    -
  • View past scan results with timestamps and outcomes
  • -
  • Track frequency of phishing and malware detections over time
  • -
  • Visualize data with charts such as bar graphs and line graphs
  • -
  • Export logs for further offline analysis
  • -
- -

Proposed Solution

-
    -
  • Create a new dashboard page: dashboard.html
  • -
  • Use Chart.js or D3.js for visualization
  • -
  • Store scan results in local storage for demo purposes
  • -
  • Plan for backend DB persistence in a future release
  • -
  • Add a navigation link from the main page to the dashboard
  • -
-
+
+
+
0
+
Total Historical Scans
+
+
+
0%
+
Safe Scan Ratio
+
+
+
0
+
Total Threats Intercepted
+
+
-
-

Demo Visualization

-
-

Interactive scanner threat analysis charts:

- +
+
+

Interactive Threat Distribution

+
+
-

Demo Mode

-

In the demo version, scan records are stored locally in the browser. A backend database can be added later to persist results and support long-term analytics.

+

Persistence Engine Details

+

Telemetry metrics are extracted directly from secure local repository storage structures. Clear your browser cache or use the provided control to purge logs.

@@ -82,7 +75,6 @@

Demo Mode

@@ -94,14 +86,42 @@

Navigation

+ + diff --git a/script.js b/script.js index d3e254d..5a4b937 100644 --- a/script.js +++ b/script.js @@ -107,25 +107,7 @@ let dangerCount = 0; function isValidUrl(urlString) { try { - const urlObj = new URL(urlString); - const hostname = urlObj.hostname; - - // To allow local testing - if (hostname === 'localhost' || hostname === '127.0.0.1') { - return true; - } - - const parts = hostname.split('.'); - if (parts.length < 2) { - return false; - } - - const tld = parts[parts.length - 1]; - // TLD must be at least 2 chars & consist of only letters - if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) { - return false; - } - + new URL(urlString); return true; } catch (e) { return false; @@ -158,15 +140,6 @@ function formatAndValidateUrl(input) { url = 'https://' + url; } - // Validate URL length - if (url.length > 2048) { - return { - valid: false, - error: 'URL exceeds maximum length of 2048 characters.', - url: null - }; - } - if (!isValidUrl(url)) { return { @@ -218,24 +191,28 @@ function fillExample(url) { } -// Update Stats - -function updateStats(type) { - - totalScans++; - - if (type === 'safe') { - safeCount++; - } - - if (type === 'danger') { - dangerCount++; +function updateStats(type, threatTypes = []) { + //Extracts tracking array or generate empty container + const history = JSON.parse(localStorage.getItem('cybershield_history')) || []; + const scanRecord = { + timestamp: new Date().toISOString(), + status: type, + threats: threatTypes + }; + + history.push(scanRecord); + localStorage.setItem('cybershield_history', JSON.stringify(history)); + + //dynamically balancing real time counts for index homepage elements if they exist + const total = history.length; + const safe = history.filter(item => item.status === 'safe').length; + const danger = history.filter(item => item.status === 'danger').length; + + if (document.getElementById('totalScans')) { + document.getElementById('totalScans').textContent = total; + document.getElementById('safeCount').textContent = safe; + document.getElementById('dangerCount').textContent = danger; } - - document.getElementById('totalScans').textContent = totalScans; - document.getElementById('safeCount').textContent = safeCount; - document.getElementById('dangerCount').textContent = dangerCount; - } function calculateRiskScore(url, isThreat) { @@ -288,7 +265,7 @@ function calculateRiskScore(url, isThreat) { score += 10; breakdown.push({ text: 'Excessive subdomains used', type: 'warning' }); } else { - breakdown.push({ text: 'Domain structure appears normal', type: 'safe' }); + breakdown.push({ text: 'Domain structure appears normal', type: 'safe' }); } } catch (e) { score += 20; @@ -297,8 +274,8 @@ function calculateRiskScore(url, isThreat) { score = Math.min(100, score); - let confidence = isThreat ? 99 : 88; - if (!isThreat && score > 20) confidence -= 12; + let confidence = isThreat ? 99 : 88; + if (!isThreat && score > 20) confidence -= 12; return { score, confidence, breakdown }; } @@ -318,7 +295,7 @@ function showResult(type, title, desc, url, threats) { if ((type === 'safe' || type === 'danger') && url) { riskData = calculateRiskScore(url, isThreat); - + let meterColor = 'var(--accent-1)'; if (riskData.score > 30) meterColor = '#fbbf24'; if (riskData.score > 60) meterColor = '#f87171'; @@ -362,12 +339,11 @@ function showResult(type, title, desc, url, threats) {
- ${type === 'loading' - ? '
' - : type === 'scan-loading' - ? '' - : `${icons[type]}` - } + ${ + type === 'loading' + ? '
' + : `${icons[type]}` + }
@@ -376,13 +352,13 @@ function showResult(type, title, desc, url, threats) {
${desc}
${url ? `
${url}
` : ''} ${threats && threats.length - ? `
${threats.map(t => - `${t}`).join('')}
` - : ''} + ? `
${threats.map(t => + `${t}`).join('')}
` + : ''} ${(type === 'safe' || type === 'danger') ? ` -
- - +
+ +
` : ''}
`; @@ -460,70 +436,23 @@ async function checkSecurity() { const url = validation.url; - const typoCard = document.getElementById('typosquattingCard'); - if (typoCard) { - typoCard.classList.add('hidden'); - } - const btn = document.getElementById('scanBtn'); btn.disabled = true; - // Loading State — enhanced scan animation - - document.getElementById('result').innerHTML = ` -
-
-
-
-
-
-
- - - -
-
-
-
Scanning URL...
-
${url}
-
-
-
-
-
-
- Checking threat databases... -
-
-
-
-
`; + // Loading State - // Animate status text steps - const statusEl = document.querySelector('.scan-status-step'); - const steps = [ - 'Connecting to Safe Browsing API...', - 'Analyzing URL patterns...', - 'Checking threat databases...', - 'Finalizing results...' - ]; - let stepIndex = 0; - const stepTimer = setInterval(() => { - stepIndex++; - if (stepIndex < steps.length && statusEl) { - statusEl.textContent = steps[stepIndex]; - } else { - clearInterval(stepTimer); - } - }, 800); + showResult( + 'loading', + 'Scanning...', + 'Checking against threat databases. Please wait.', + url, + [] + ); try { - const apiHost = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') - ? 'http://localhost:3002' - : 'https://cybershield-sxz0.onrender.com'; - const response = await fetch(`${apiHost}/check`, { + const response = await fetch('https://cybershield-sxz0.onrender.com/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) @@ -542,7 +471,8 @@ async function checkSecurity() { const hasUnwanted = allThreats.includes('UNWANTED_SOFTWARE'); const hasHarmful = allThreats.includes('POTENTIALLY_HARMFUL_APPLICATION'); const threats = [...new Set(allThreats.map(t => t.replace(/_/g, ' ')))]; - updateStats('danger'); + const allThreats = data.matches.map(m => m.threatType); + updateStats('danger', allThreats); showResult('danger', 'Threat Detected!', `This URL is flagged as dangerous. Do not visit it.

@@ -563,7 +493,7 @@ async function checkSecurity() {
`, url, threats); } else { - updateStats('safe'); + updateStats('safe', []); const urlObj = new URL(url); const isHttps = urlObj.protocol === 'https:'; const allThreats = data.matches ? data.matches.map(m => m.threatType) : []; @@ -593,14 +523,6 @@ async function checkSecurity() { `, url, []); } - if (data.typosquatting) { - const typoCard = document.getElementById('typosquattingCard'); - if (typoCard) { - typoCard.classList.remove('hidden'); - renderFamilyTree(data.typosquatting); - } - } - } catch (err) { showResult('error', 'Scan Error', `An unexpected error occurred.
@@ -670,324 +592,6 @@ async function downloadPDF() { pdf.addImage(imgData, 'PNG', 0, 20, pageWidth, imgHeight); pdf.save('cybershield-report.pdf'); } - -// ───────────────────────────── -// TYPOSQUATTING FAMILY TREE -// ───────────────────────────── - -function renderFamilyTree(typosquattingData) { - const container = document.getElementById('treeContainer'); - if (!container) return; - - container.innerHTML = ''; - - const original = typosquattingData.original; - const variants = typosquattingData.variants; - - if (!variants || variants.length === 0) { - container.innerHTML = '
No typosquatting variants generated for this domain.
'; - return; - } - - const categoriesMap = { - homoglyph: { name: "Homoglyphs", children: [] }, - tld: { name: "TLD Swaps", children: [] }, - substitution: { name: "Substitutions", children: [] }, - hyphen: { name: "Hyphen Injections", children: [] } - }; - - variants.forEach(v => { - if (categoriesMap[v.category]) { - categoriesMap[v.category].children.push({ - name: v.domain, - type: "variant", - threat: v.threat, - distance: v.distance, - category: v.category - }); - } - }); - - const rootChildren = Object.values(categoriesMap).filter(cat => cat.children.length > 0); - - const rootData = { - name: original.domain, - type: "original", - threat: original.threat, - children: rootChildren.map(cat => ({ - name: cat.name, - type: "category", - children: cat.children - })) - }; - - const margin = { top: 20, right: 120, bottom: 20, left: 120 }; - const width = container.clientWidth || 800; - const height = 450; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const svg = d3.select(container) - .append("svg") - .attr("width", "100%") - .attr("height", height) - .attr("viewBox", `0 0 ${width} ${height}`) - .style("overflow", "visible") - .append("g") - .attr("transform", `translate(${margin.left}, ${margin.top})`); - - let i = 0; - const treemap = d3.tree().size([innerHeight, innerWidth]); - - const root = d3.hierarchy(rootData); - root.x0 = innerHeight / 2; - root.y0 = 0; - - let tooltip = document.getElementById("tree-tooltip"); - if (!tooltip) { - tooltip = document.createElement("div"); - tooltip.id = "tree-tooltip"; - tooltip.className = "tree-tooltip"; - tooltip.style.cssText = ` - position: absolute; - opacity: 0; - pointer-events: none; - background: rgba(15, 23, 42, 0.95); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - padding: 12px; - color: #f1f5f9; - font-size: 0.85rem; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); - z-index: 1000; - transition: opacity 0.2s ease; - min-width: 220px; - `; - container.appendChild(tooltip); - } - - function highlightHomoglyphs(domain, originalDomain) { - let highlightedHtml = ''; - let hasHomoglyphHighlight = false; - - for (let idx = 0; idx < domain.length; idx++) { - const char = domain[idx]; - const code = char.charCodeAt(0); - - const isNonAscii = code > 127; - const isDifferent = originalDomain && originalDomain[idx] !== char; - - if (isNonAscii && isDifferent) { - highlightedHtml += `${char}`; - hasHomoglyphHighlight = true; - } else { - highlightedHtml += char; - } - } - return { html: highlightedHtml, detected: hasHomoglyphHighlight }; - } - - function showTooltip(event, data) { - const rect = container.getBoundingClientRect(); - const x = event.clientX - rect.left + 15; - const y = event.clientY - rect.top + 15; - - let threatText = ""; - let threatColor = ""; - let desc = ""; - - if (data.type === "original") { - threatText = data.threat === "malicious" ? "Malicious (Flagged)" : "Safe"; - threatColor = data.threat === "malicious" ? "#ef4444" : "#10b981"; - desc = "The original target domain analyzed by Google Safe Browsing."; - } else { - threatText = data.threat === "malicious" ? "Malicious (Flagged)" : "Suspicious Typosquatting Candidate"; - threatColor = data.threat === "malicious" ? "#ef4444" : "#f59e0b"; - - const catDescriptions = { - homoglyph: "Visual look-alike representations (IDN homograph attack vector) designed to deceive.", - tld: "TLD replacement targeting keyboard extensions or common domain mistakes.", - substitution: "Common keyboard layout substitutions targeting spelling typos.", - hyphen: "Plausible spacing or prefix hyphen variations." - }; - desc = catDescriptions[data.category] || "Variant domain variation."; - } - - let displayName = data.name; - let homoglyphNote = ''; - if (data.type === "variant" && data.category === "homoglyph") { - const highlightResult = highlightHomoglyphs(data.name, original.domain); - if (highlightResult.detected) { - displayName = highlightResult.html; - homoglyphNote = `
⚠️ Underlined character is a look-alike Unicode homoglyph.
`; - } - } - - tooltip.innerHTML = ` -
${displayName}
-
- - ${data.type === "original" ? "Original Target" : "Variant"} - -
-
- Threat Level: ${threatText} -
- ${data.type !== "original" ? `
Edit Distance: Levenshtein ${data.distance}
` : ""} -
${desc}
- ${homoglyphNote} - `; - - tooltip.style.left = `${x}px`; - tooltip.style.top = `${y}px`; - tooltip.style.opacity = "1"; - } - - function hideTooltip() { - tooltip.style.opacity = "0"; - } - - function update(source) { - const treeData = treemap(root); - const nodes = treeData.descendants(); - const links = treeData.descendants().slice(1); - - nodes.forEach(d => { d.y = d.depth * 180; }); - - const node = svg.selectAll("g.node") - .data(nodes, d => d.id || (d.id = ++i)); - - const nodeEnter = node.enter().append("g") - .attr("class", "node") - .attr("transform", d => `translate(${source.y0},${source.x0})`) - .on("click", (event, d) => { - if (d.data.type === "category") { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; - } - update(d); - } - }); - - nodeEnter.append("circle") - .attr("class", d => `node-circle ${d.data.threat || ""} ${d.data.type}`) - .attr("r", d => d.data.type === "original" ? 10 : d.data.type === "category" ? 7 : 5) - .style("cursor", d => d.data.type === "category" ? "pointer" : "default") - .style("fill", d => { - if (d.data.type === "original") return "var(--accent-1)"; - if (d.data.type === "category") return "#94a3b8"; - if (d.data.threat === "malicious") return "#ef4444"; - if (d.data.threat === "suspicious") return "#f59e0b"; - return "#10b981"; - }) - .style("stroke", d => { - if (d.data.type === "original") return "rgba(0, 255, 180, 0.4)"; - if (d.data.type === "category") return "rgba(255, 255, 255, 0.2)"; - return "none"; - }) - .style("stroke-width", "4px"); - - nodeEnter.append("text") - .attr("dy", ".35em") - .attr("x", d => d.children || d._children ? -13 : 13) - .attr("text-anchor", d => d.children || d._children ? "end" : "start") - .text(d => d.data.name) - .style("fill", "#f1f5f9") - .style("font-family", "sans-serif") - .style("font-size", d => d.data.type === "original" ? "12px" : d.data.type === "category" ? "11px" : "10px") - .style("font-weight", d => d.data.type === "original" || d.data.type === "category" ? "bold" : "normal") - .style("pointer-events", "none") - .style("text-shadow", "0 1px 3px rgba(0,0,0,0.8)"); - - const nodeUpdate = nodeEnter.merge(node); - - nodeUpdate.transition() - .duration(500) - .attr("transform", d => `translate(${d.y},${d.x})`); - - nodeUpdate.select("circle") - .attr("r", d => d.data.type === "original" ? 10 : d.data.type === "category" ? 7 : 5) - .style("fill", d => { - if (d.data.type === "original") return "var(--accent-1)"; - if (d.data.type === "category") { - return d._children ? "var(--accent-1)" : "#64748b"; - } - if (d.data.threat === "malicious") return "#ef4444"; - if (d.data.threat === "suspicious") return "#f59e0b"; - return "#10b981"; - }); - - const nodeExit = node.exit().transition() - .duration(500) - .attr("transform", d => `translate(${source.y},${source.x})`) - .remove(); - - nodeExit.select("circle").attr("r", 0); - nodeExit.select("text").style("fill-opacity", 0); - - const link = svg.selectAll("path.link") - .data(links, d => d.id); - - const linkEnter = link.enter().insert("path", "g") - .attr("class", "link") - .attr("d", d => { - const o = { x: source.x0, y: source.y0 }; - return diagonal(o, o); - }) - .style("fill", "none") - .style("stroke", "rgba(148, 163, 184, 0.2)") - .style("stroke-width", "1.5px"); - - const linkUpdate = linkEnter.merge(link); - - linkUpdate.transition() - .duration(500) - .attr("d", d => diagonal(d, d.parent)) - .style("stroke", d => { - if (d.data.threat === "malicious") return "rgba(239, 68, 68, 0.3)"; - if (d.data.threat === "suspicious") return "rgba(245, 158, 11, 0.3)"; - if (d.data.type === "category") return "rgba(148, 163, 184, 0.15)"; - return "rgba(16, 185, 129, 0.3)"; - }); - - link.exit().transition() - .duration(500) - .attr("d", d => { - const o = { x: source.x, y: source.y }; - return diagonal(o, o); - }) - .remove(); - - nodes.forEach(d => { - d.x0 = d.x; - d.y0 = d.y; - }); - - nodeUpdate - .filter(d => d.data.type === "variant" || d.data.type === "original") - .on("mouseover", (event, d) => { - showTooltip(event, d.data); - }) - .on("mouseout", () => { - hideTooltip(); - }); - } - - function diagonal(s, d) { - return `M ${s.y} ${s.x} - C ${(s.y + d.y) / 2} ${s.x}, - ${(s.y + d.y) / 2} ${d.x}, - ${d.y} ${d.x}`; - } - - update(root); -} - // ───────────────────────────── // THEME TOGGLE // ───────────────────────────── @@ -1018,3 +622,13 @@ function renderFamilyTree(typosquattingData) { } })(); + +//initializing counters on landing page load sequence here +(function initIndexCounters() { + const history = JSON.parse(localStorage.getItem('cybershield_history')) || []; + if (document.getElementById('totalScans')) { + document.getElementById('totalScans').textContent = history.length; + document.getElementById('safeCount').textContent = history.filter(i => i.status === 'safe').length; + document.getElementById('dangerCount').textContent = history.filter(i => i.status === 'danger').length; + } +})();