Spaces:
Running
Running
// Display Results Module | |
// This module handles the display of analysis results in the UI | |
// Main namespace to avoid global pollution | |
const displayResults = (function() { | |
/** | |
* Displays risk factors in the Key Risk Factors section | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displayRiskFactors(data) { | |
const riskFactorsContainer = document.getElementById('topFeatures'); | |
if (!riskFactorsContainer) { | |
console.error("Risk factors container not found with ID 'topFeatures'"); | |
return; | |
} | |
riskFactorsContainer.innerHTML = ''; | |
console.log("Displaying risk factors from data:", data); | |
let factors = []; | |
// Try to get features from feature_table (primary source) | |
if (data.feature_table && Array.isArray(data.feature_table)) { | |
console.log("Found feature_table:", data.feature_table); | |
factors = data.feature_table | |
.filter(f => Math.abs(f.impact || 0) > 0.01) | |
.sort((a, b) => Math.abs(b.impact || 0) - Math.abs(a.impact || 0)) | |
.slice(0, 5) | |
.map(f => ({ | |
name: f.feature, | |
value: f.value, | |
percentage: f.impact || 0, | |
color_class: f.color_class || 'success' | |
})); | |
} | |
// Fallback to feature_contributions if available | |
else if (data.feature_contributions && Array.isArray(data.feature_contributions)) { | |
console.log("Using feature_contributions:", data.feature_contributions); | |
factors = data.feature_contributions | |
.filter(f => f.section === "Key Risk Factors" && Math.abs(f.percentage || 0) > 0.01) | |
.sort((a, b) => Math.abs(b.percentage || 0) - Math.abs(a.percentage || 0)) | |
.slice(0, 5); | |
} | |
if (factors.length > 0) { | |
// Create and append risk factor elements | |
factors.forEach(factor => { | |
// Determine color class based on percentage | |
const percentage = parseFloat(factor.percentage || 0); | |
let colorClass = 'success'; | |
if (percentage > 30) { | |
colorClass = 'danger'; | |
} else if (percentage > 15) { | |
colorClass = 'warning'; | |
} | |
const factorEl = document.createElement('div'); | |
factorEl.className = 'contribution-bar'; | |
factorEl.style.marginBottom = '16px'; | |
// Create header with name and value | |
const headerEl = document.createElement('div'); | |
headerEl.className = 'contribution-label'; | |
headerEl.style.display = 'flex'; | |
headerEl.style.justifyContent = 'space-between'; | |
headerEl.style.marginBottom = '6px'; | |
const nameEl = document.createElement('span'); | |
nameEl.className = 'factor-name'; | |
nameEl.textContent = formatFeatureName(factor.name || factor.feature_name); | |
nameEl.style.fontWeight = '500'; | |
const valueEl = document.createElement('span'); | |
valueEl.className = `contribution-value ${colorClass}`; | |
valueEl.textContent = `${percentage.toFixed(1)}%`; | |
valueEl.style.fontWeight = '600'; | |
if (colorClass === 'danger') { | |
valueEl.style.color = '#ef4444'; | |
} else if (colorClass === 'warning') { | |
valueEl.style.color = '#f59e0b'; | |
} else { | |
valueEl.style.color = '#10b981'; | |
} | |
headerEl.appendChild(nameEl); | |
headerEl.appendChild(valueEl); | |
// Create enhanced bar chart visualization | |
const barContainerEl = document.createElement('div'); | |
barContainerEl.className = 'bar-container'; | |
barContainerEl.style.position = 'relative'; | |
barContainerEl.style.height = '12px'; | |
barContainerEl.style.borderRadius = '6px'; | |
barContainerEl.style.overflow = 'hidden'; | |
barContainerEl.style.backgroundColor = 'rgba(30, 41, 59, 0.4)'; | |
const barEl = document.createElement('div'); | |
barEl.className = `bar-fill ${colorClass}`; | |
barEl.style.height = '100%'; | |
barEl.style.width = `${Math.min(Math.abs(percentage), 100)}%`; | |
barEl.style.borderRadius = '6px'; | |
barEl.style.transition = 'width 1s cubic-bezier(0.22, 1, 0.36, 1)'; | |
// Add gradient to the bar | |
if (colorClass === 'danger') { | |
barEl.style.background = 'linear-gradient(90deg, #ef4444, #b91c1c)'; | |
} else if (colorClass === 'warning') { | |
barEl.style.background = 'linear-gradient(90deg, #f59e0b, #d97706)'; | |
} else { | |
barEl.style.background = 'linear-gradient(90deg, #10b981, #059669)'; | |
} | |
barContainerEl.appendChild(barEl); | |
// Feature value display | |
const valueDisplayEl = document.createElement('div'); | |
valueDisplayEl.className = 'feature-value-display'; | |
valueDisplayEl.style.display = 'flex'; | |
valueDisplayEl.style.justifyContent = 'flex-end'; | |
valueDisplayEl.style.marginTop = '4px'; | |
valueDisplayEl.style.fontSize = '0.85rem'; | |
valueDisplayEl.style.color = '#94a3b8'; | |
// Format and display the feature value | |
let displayValue = factor.value; | |
if (typeof factor.value === 'boolean') { | |
displayValue = factor.value ? 'Yes' : 'No'; | |
} else if (factor.value === 0 || factor.value === 1) { | |
displayValue = factor.value === 1 ? 'Yes' : 'No'; | |
} | |
valueDisplayEl.textContent = `Value: ${displayValue}`; | |
// Assemble the components | |
factorEl.appendChild(headerEl); | |
factorEl.appendChild(barContainerEl); | |
factorEl.appendChild(valueDisplayEl); | |
// Add to container | |
riskFactorsContainer.appendChild(factorEl); | |
}); | |
} else { | |
// No risk factors found | |
const noFactorsEl = document.createElement('p'); | |
noFactorsEl.textContent = 'No significant risk factors detected.'; | |
noFactorsEl.style.textAlign = 'center'; | |
noFactorsEl.style.padding = '20px'; | |
noFactorsEl.style.color = '#94a3b8'; | |
riskFactorsContainer.appendChild(noFactorsEl); | |
} | |
} | |
/** | |
* Displays domain information in the Domain Information section | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displayDomainInfo(data) { | |
const domainInfoContainer = document.getElementById('domainInfo'); | |
if (!domainInfoContainer) { | |
console.error("Domain info container not found with ID 'domainInfo'"); | |
return; | |
} | |
domainInfoContainer.innerHTML = ''; | |
// Extract domain info from data | |
const domainInfo = data.domain_info || {}; | |
// Get URL parts | |
const url = data.url || ''; | |
const parsed = new URL(url); | |
const domain = parsed.hostname; | |
const protocol = parsed.protocol.replace(':', ''); | |
// Check if IP address could not be resolved | |
const ipNotResolved = !domainInfo.ip_address || | |
domainInfo.ip_address === 'Unknown' || | |
domainInfo.ip_address === 'Could not resolve'; | |
// If IP address couldn't be resolved, increase section risk | |
if (ipNotResolved) { | |
increaseIPResolutionRisk(data); | |
} | |
// Calculate domain age if available | |
let domainAge = null; | |
let domainAgeStatus = 'neutral'; | |
if (domainInfo.created && domainInfo.created !== 'Unknown') { | |
try { | |
const createdDate = new Date(domainInfo.created); | |
const now = new Date(); | |
const ageInDays = Math.floor((now - createdDate) / (1000 * 60 * 60 * 24)); | |
// Format the creation date | |
const formattedDate = createdDate.toLocaleDateString('en-US', { | |
year: 'numeric', | |
month: 'short', | |
day: 'numeric' | |
}); | |
if (ageInDays < 30) { | |
domainAge = `${ageInDays} days (Created: ${formattedDate})`; | |
domainAgeStatus = 'danger'; | |
} else if (ageInDays < 90) { | |
domainAge = `${Math.floor(ageInDays / 30)} months (Created: ${formattedDate})`; | |
domainAgeStatus = 'warning'; | |
} else if (ageInDays < 365) { | |
domainAge = `${Math.floor(ageInDays / 30)} months (Created: ${formattedDate})`; | |
domainAgeStatus = 'neutral'; | |
} else { | |
const years = Math.floor(ageInDays / 365); | |
domainAge = `${years} ${years === 1 ? 'year' : 'years'} (Created: ${formattedDate})`; | |
domainAgeStatus = 'safe'; | |
} | |
} catch (e) { | |
console.error('Error calculating domain age:', e); | |
domainAge = domainInfo.created; | |
} | |
} | |
// Check TLD type | |
let tldType = null; | |
let tldSuspicious = false; | |
let tldDescription = ''; | |
if (domain) { | |
try { | |
const tld = domain.split('.').pop().toLowerCase(); | |
const commonTlds = ['com', 'org', 'net', 'edu', 'gov', 'io', 'co', 'me', 'app', 'dev']; | |
const suspiciousTlds = ['tk', 'ml', 'ga', 'cf', 'gq', 'top', 'xyz', 'online', 'site', 'club', 'icu']; | |
if (commonTlds.includes(tld)) { | |
tldType = `Common TLD (.${tld})`; | |
tldDescription = 'Well-established and trusted top-level domain'; | |
} else if (suspiciousTlds.includes(tld)) { | |
tldType = `Suspicious TLD (.${tld})`; | |
tldSuspicious = true; | |
tldDescription = 'This TLD is commonly associated with malicious websites'; | |
} else if (tld.length === 2) { | |
tldType = `Country Code (.${tld})`; | |
tldDescription = 'A country-specific top-level domain'; | |
} else { | |
tldType = `Generic TLD (.${tld})`; | |
tldDescription = 'A less common top-level domain'; | |
} | |
} catch (e) { | |
console.error('Error determining TLD type:', e); | |
} | |
} | |
// Determine if WHOIS privacy is enabled based on organization name | |
const whoisPrivacy = domainInfo.organization && | |
/privacy|protect|proxy|private|whois/i.test(domainInfo.organization) ? 'Enabled' : null; | |
// Check if we have location data for the map | |
const hasLocationData = (domainInfo.latitude && domainInfo.longitude && | |
(domainInfo.latitude !== 0 || domainInfo.longitude !== 0)) || | |
(domainInfo.country && domainInfo.country !== 'Unknown'); | |
// Create info items list with actual data | |
const infoItems = []; | |
// Only add items with valid values | |
if (domain) { | |
infoItems.push({ | |
label: 'Domain Name', | |
value: domain, | |
icon: 'fa-globe', | |
description: 'The registered domain name of the website' | |
}); | |
} | |
if (protocol) { | |
infoItems.push({ | |
label: 'Protocol', | |
value: protocol.toUpperCase(), | |
icon: protocol === 'https' ? 'fa-lock' : 'fa-unlock', | |
isSecure: protocol === 'https', | |
description: protocol === 'https' ? | |
'Secure connection with encryption' : | |
'Insecure connection without encryption' | |
}); | |
} | |
if (domainInfo.organization && domainInfo.organization !== 'Unknown') { | |
infoItems.push({ | |
label: 'Organization', | |
value: domainInfo.organization, | |
icon: 'fa-building', | |
description: 'The organization that owns this domain' | |
}); | |
} | |
if (domainInfo.country && domainInfo.country !== 'Unknown') { | |
infoItems.push({ | |
label: 'Location', | |
value: domainInfo.city && domainInfo.city !== 'Unknown' ? | |
`${domainInfo.city}, ${domainInfo.country}` : | |
domainInfo.country, | |
icon: 'fa-location-dot', | |
description: 'Geographic location of the hosting server' | |
}); | |
} | |
if (domainAge) { | |
infoItems.push({ | |
label: 'Domain Age', | |
value: domainAge, | |
icon: 'fa-calendar-alt', | |
isNew: domainAgeStatus === 'danger' || domainAgeStatus === 'warning', | |
description: domainAgeStatus === 'danger' ? 'Very recently registered domain - high risk' : | |
domainAgeStatus === 'warning' ? 'Recently registered domain - potential risk' : | |
domainAgeStatus === 'safe' ? 'Well-established domain - lower risk' : | |
'How long the domain has been registered' | |
}); | |
} | |
if (tldType) { | |
infoItems.push({ | |
label: 'TLD Type', | |
value: tldType, | |
icon: 'fa-tag', | |
isSuspicious: tldSuspicious, | |
description: tldDescription || 'The type of top-level domain used by this website' | |
}); | |
} | |
if (whoisPrivacy) { | |
infoItems.push({ | |
label: 'WHOIS Privacy', | |
value: whoisPrivacy, | |
icon: 'fa-user-shield', | |
description: 'Whether the domain uses privacy protection to hide owner information' | |
}); | |
} | |
if (data.ssl_info) { | |
infoItems.push({ | |
label: 'SSL Certificate', | |
value: data.ssl_info.has_ssl ? 'Valid' : 'Not found', | |
icon: data.ssl_info.has_ssl ? 'fa-shield-check' : 'fa-shield-exclamation', | |
isSecure: data.ssl_info.has_ssl, | |
description: data.ssl_info.has_ssl ? | |
'SSL certificate is valid and trusted' : | |
'No SSL certificate found, connection is not encrypted' | |
}); | |
} | |
// Only create the IP and location section if there's an IP to display | |
if (domainInfo.ip_address && domainInfo.ip_address !== 'Unknown') { | |
// Create a card style container for IP and location | |
const ipLocationCard = document.createElement('div'); | |
ipLocationCard.className = 'domain-info-card'; | |
ipLocationCard.style.marginBottom = '15px'; | |
ipLocationCard.style.padding = '15px'; | |
ipLocationCard.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
ipLocationCard.style.borderRadius = '10px'; | |
ipLocationCard.style.border = '1px solid var(--border-color)'; | |
// Create IP Address header | |
const ipHeader = document.createElement('div'); | |
ipHeader.className = 'ip-header'; | |
ipHeader.style.marginBottom = '10px'; | |
ipHeader.style.display = 'flex'; | |
ipHeader.style.alignItems = 'center'; | |
ipHeader.style.justifyContent = 'space-between'; | |
const ipLabel = document.createElement('div'); | |
ipLabel.style.color = 'var(--text-muted)'; | |
ipLabel.style.fontSize = '0.9rem'; | |
ipLabel.style.fontWeight = '500'; | |
ipLabel.innerHTML = '<i class="fas fa-network-wired" style="margin-right: 8px; color: #3b82f6;"></i> IP Address'; | |
const ipValue = document.createElement('div'); | |
ipValue.style.fontWeight = 'bold'; | |
ipValue.style.fontSize = '1.1rem'; | |
// Apply red color if IP could not be resolved | |
if (domainInfo.ip_address === 'Could not resolve') { | |
ipValue.style.color = '#ef4444'; | |
} | |
ipValue.textContent = domainInfo.ip_address; | |
ipHeader.appendChild(ipLabel); | |
ipHeader.appendChild(ipValue); | |
ipLocationCard.appendChild(ipHeader); | |
// Only add the map if we have location data | |
if (hasLocationData) { | |
// Add a server location label with icon | |
const serverLocationLabel = document.createElement('div'); | |
serverLocationLabel.className = 'server-location-label'; | |
serverLocationLabel.style.marginBottom = '10px'; | |
serverLocationLabel.style.fontWeight = '500'; | |
serverLocationLabel.style.display = 'flex'; | |
serverLocationLabel.style.alignItems = 'center'; | |
serverLocationLabel.innerHTML = '<i class="fas fa-map-marker-alt" style="margin-right: 8px; color: #ef4444;"></i> Server Location'; | |
ipLocationCard.appendChild(serverLocationLabel); | |
// Add geolocation map | |
const serverLocationDiv = document.createElement('div'); | |
serverLocationDiv.id = 'server-location-map'; | |
serverLocationDiv.style.width = '100%'; | |
serverLocationDiv.style.height = '180px'; | |
serverLocationDiv.style.borderRadius = '8px'; | |
serverLocationDiv.style.overflow = 'hidden'; | |
serverLocationDiv.style.border = '1px solid var(--border-color)'; | |
ipLocationCard.appendChild(serverLocationDiv); | |
// Add the IP location card to the container (only if it has content) | |
domainInfoContainer.appendChild(ipLocationCard); | |
// Initialize map with the server location | |
try { | |
setTimeout(() => { | |
initMap('server-location-map', domainInfo, false); | |
}, 100); | |
} catch (e) { | |
console.error('Error initializing map:', e); | |
serverLocationDiv.innerHTML = '<div style="padding: 10px; text-align: center;">Error loading map</div>'; | |
} | |
} else { | |
// Add the IP location card without map if it has the IP but no location | |
domainInfoContainer.appendChild(ipLocationCard); | |
} | |
} else if (!domainInfo.ip_address || domainInfo.ip_address === 'Unknown') { | |
// Create a card showing that IP address could not be resolved | |
const ipLocationCard = document.createElement('div'); | |
ipLocationCard.className = 'domain-info-card'; | |
ipLocationCard.style.marginBottom = '15px'; | |
ipLocationCard.style.padding = '15px'; | |
ipLocationCard.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
ipLocationCard.style.borderRadius = '10px'; | |
ipLocationCard.style.border = '1px solid var(--border-color)'; | |
// Create IP Address header | |
const ipHeader = document.createElement('div'); | |
ipHeader.className = 'ip-header'; | |
ipHeader.style.marginBottom = '10px'; | |
ipHeader.style.display = 'flex'; | |
ipHeader.style.alignItems = 'center'; | |
ipHeader.style.justifyContent = 'space-between'; | |
const ipLabel = document.createElement('div'); | |
ipLabel.style.color = 'var(--text-muted)'; | |
ipLabel.style.fontSize = '0.9rem'; | |
ipLabel.style.fontWeight = '500'; | |
ipLabel.innerHTML = '<i class="fas fa-network-wired" style="margin-right: 8px; color: #3b82f6;"></i> IP Address'; | |
const ipValue = document.createElement('div'); | |
ipValue.style.fontWeight = 'bold'; | |
ipValue.style.fontSize = '1.1rem'; | |
ipValue.style.color = '#ef4444'; // Red color to indicate error | |
ipValue.textContent = 'Could not resolve'; | |
// Add warning message about IP resolution failure | |
const warningMessage = document.createElement('div'); | |
warningMessage.style.marginTop = '10px'; | |
warningMessage.style.padding = '8px 12px'; | |
warningMessage.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; | |
warningMessage.style.borderRadius = '6px'; | |
warningMessage.style.border = '1px solid rgba(239, 68, 68, 0.3)'; | |
warningMessage.style.color = '#ef4444'; | |
warningMessage.style.fontSize = '0.85rem'; | |
warningMessage.innerHTML = '<i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> Unable to resolve IP address. This may indicate domain masking or a newly registered domain, which increases risk.'; | |
ipHeader.appendChild(ipLabel); | |
ipHeader.appendChild(ipValue); | |
ipLocationCard.appendChild(ipHeader); | |
ipLocationCard.appendChild(warningMessage); | |
// Add the IP card to the container | |
domainInfoContainer.appendChild(ipLocationCard); | |
} | |
// Only create domain details card if we have valid items to display | |
if (infoItems.length > 0) { | |
// Create domain details card | |
const domainDetailsCard = document.createElement('div'); | |
domainDetailsCard.className = 'domain-details-card'; | |
domainDetailsCard.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
domainDetailsCard.style.borderRadius = '10px'; | |
domainDetailsCard.style.border = '1px solid var(--border-color)'; | |
domainDetailsCard.style.overflow = 'hidden'; | |
// Add domain details header | |
const domainDetailsHeader = document.createElement('div'); | |
domainDetailsHeader.className = 'domain-details-header'; | |
domainDetailsHeader.style.padding = '12px 15px'; | |
domainDetailsHeader.style.borderBottom = '1px solid var(--border-color)'; | |
domainDetailsHeader.style.backgroundColor = 'rgba(15, 23, 42, 0.5)'; | |
domainDetailsHeader.style.fontWeight = 'bold'; | |
domainDetailsHeader.innerHTML = '<i class="fas fa-server" style="margin-right: 8px;"></i> Domain Details'; | |
domainDetailsCard.appendChild(domainDetailsHeader); | |
// Create domain details content | |
const domainDetailsList = document.createElement('ul'); | |
domainDetailsList.className = 'domain-details-list'; | |
domainDetailsList.style.listStyle = 'none'; | |
domainDetailsList.style.padding = '0'; | |
domainDetailsList.style.margin = '0'; | |
// Add each info item | |
infoItems.forEach(item => { | |
const listItem = document.createElement('li'); | |
listItem.className = 'domain-info-item'; | |
listItem.style.display = 'flex'; | |
listItem.style.alignItems = 'center'; | |
listItem.style.padding = '12px 15px'; | |
listItem.style.borderBottom = '1px solid rgba(71, 85, 105, 0.2)'; | |
// Add hover effect | |
listItem.addEventListener('mouseover', () => { | |
listItem.style.backgroundColor = 'rgba(30, 41, 59, 0.3)'; | |
}); | |
listItem.addEventListener('mouseout', () => { | |
listItem.style.backgroundColor = ''; | |
}); | |
// Icon part | |
const iconDiv = document.createElement('div'); | |
iconDiv.className = 'info-icon'; | |
iconDiv.style.marginRight = '12px'; | |
iconDiv.style.width = '24px'; | |
iconDiv.style.height = '24px'; | |
iconDiv.style.display = 'flex'; | |
iconDiv.style.alignItems = 'center'; | |
iconDiv.style.justifyContent = 'center'; | |
const iconElement = document.createElement('i'); | |
iconElement.className = `fas ${item.icon}`; | |
// Set icon color based on item properties | |
if (item.isSecure) { | |
iconElement.style.color = '#10b981'; // Green for secure items | |
} else if (item.isSuspicious || item.isNew) { | |
iconElement.style.color = '#f59e0b'; // Amber for suspicious or new items | |
} else if (item.label === 'SSL Certificate' && item.value === 'Not found') { | |
iconElement.style.color = '#ef4444'; // Red for missing SSL | |
} else { | |
iconElement.style.color = '#3b82f6'; // Default blue | |
} | |
iconDiv.appendChild(iconElement); | |
listItem.appendChild(iconDiv); | |
// Content part | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'info-content'; | |
contentDiv.style.flex = '1'; | |
// Label | |
const labelDiv = document.createElement('div'); | |
labelDiv.className = 'info-label'; | |
labelDiv.style.fontSize = '0.85rem'; | |
labelDiv.style.color = 'var(--text-muted)'; | |
labelDiv.textContent = item.label; | |
contentDiv.appendChild(labelDiv); | |
// Value | |
const valueDiv = document.createElement('div'); | |
valueDiv.className = 'info-value'; | |
valueDiv.style.fontWeight = '500'; | |
// Add visual indicators for certain values | |
if (item.label === 'Protocol') { | |
if (item.value === 'HTTPS') { | |
valueDiv.innerHTML = `<span style="color: #10b981;">${item.value}</span>`; | |
} else { | |
valueDiv.innerHTML = `<span style="color: #ef4444;">${item.value}</span>`; | |
} | |
} else if (item.label === 'TLD Type' && item.isSuspicious) { | |
valueDiv.innerHTML = `<span style="color: #f59e0b;">${item.value}</span> <span class="badge" style="background-color: #f59e0b; color: white; font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; margin-left: 5px;">SUSPICIOUS</span>`; | |
} else if (item.label === 'Domain Age' && item.isNew) { | |
valueDiv.innerHTML = `<span style="color: #f59e0b;">${item.value}</span>`; | |
} else if (item.label === 'SSL Certificate') { | |
if (item.value === 'Valid') { | |
valueDiv.innerHTML = `<span style="color: #10b981;">${item.value}</span>`; | |
} else { | |
valueDiv.innerHTML = `<span style="color: #ef4444;">${item.value}</span>`; | |
} | |
} else { | |
valueDiv.textContent = item.value; | |
} | |
contentDiv.appendChild(valueDiv); | |
listItem.appendChild(contentDiv); | |
domainDetailsList.appendChild(listItem); | |
}); | |
// Append the list to the card and the card to the container | |
domainDetailsCard.appendChild(domainDetailsList); | |
domainInfoContainer.appendChild(domainDetailsCard); | |
} | |
// If there's no content at all, add a no-info message | |
if (domainInfoContainer.childNodes.length === 0) { | |
const noInfoMsg = document.createElement('div'); | |
noInfoMsg.className = 'no-domain-info'; | |
noInfoMsg.style.padding = '20px'; | |
noInfoMsg.style.textAlign = 'center'; | |
noInfoMsg.style.color = 'var(--text-muted)'; | |
noInfoMsg.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
noInfoMsg.style.borderRadius = '10px'; | |
noInfoMsg.style.border = '1px solid var(--border-color)'; | |
noInfoMsg.innerHTML = '<i class="fas fa-info-circle" style="font-size: 24px; margin-bottom: 10px;"></i><br>No domain information available.'; | |
domainInfoContainer.appendChild(noInfoMsg); | |
} | |
} | |
/** | |
* Increases the section risk when IP address cannot be resolved | |
* @param {Object} data - The analysis data to modify | |
*/ | |
function increaseIPResolutionRisk(data) { | |
// Only proceed if we have section_totals | |
if (!data.section_totals) { | |
data.section_totals = {}; | |
} | |
// Get the current domain information section risk or default to 0 | |
const currentDomainRisk = data.section_totals["Domain Information"] || 0; | |
// Increase the risk by a significant amount (5-15% range) | |
const additionalRisk = 7.5; | |
data.section_totals["Domain Information"] = Math.min(100, currentDomainRisk + additionalRisk); | |
// Also add to total Section Risk if it exists | |
if (data.section_totals["Section Risk"] !== undefined) { | |
const currentSectionRisk = data.section_totals["Section Risk"] || 0; | |
data.section_totals["Section Risk"] = Math.min(100, currentSectionRisk + additionalRisk / 2); | |
} | |
// If there's a feature_contributions array, add or update the IP resolution entry | |
if (data.feature_contributions && Array.isArray(data.feature_contributions)) { | |
// Check if IP resolution feature already exists | |
const ipFeatureIndex = data.feature_contributions.findIndex( | |
f => (f.name || "").toLowerCase().includes("ip_resolution") || | |
(f.name || "").toLowerCase().includes("ip_address") | |
); | |
if (ipFeatureIndex >= 0) { | |
// Update existing feature | |
data.feature_contributions[ipFeatureIndex].value = "Could not resolve"; | |
data.feature_contributions[ipFeatureIndex].percentage = | |
(parseFloat(data.feature_contributions[ipFeatureIndex].percentage) || 0) + additionalRisk; | |
data.feature_contributions[ipFeatureIndex].color_class = "danger"; | |
} else { | |
// Add new feature | |
data.feature_contributions.push({ | |
name: "IP Resolution", | |
value: "Could not resolve", | |
percentage: additionalRisk, | |
color_class: "danger" | |
}); | |
} | |
// Sort by percentage (importance) | |
data.feature_contributions.sort((a, b) => | |
parseFloat(b.percentage || 0) - parseFloat(a.percentage || 0)); | |
} | |
// Update the overall score | |
if (data.score !== undefined) { | |
data.score = Math.min(100, data.score + additionalRisk / 3); | |
} | |
} | |
/** | |
* Initializes a map to show the geolocation of the domain | |
* @param {string} containerId - The ID of the container element for the map | |
* @param {Object} domainInfo - Domain information including location data | |
* @param {boolean} hasExactCoords - Whether we have exact coordinates | |
*/ | |
function initMap(containerId, domainInfo, hasExactCoords) { | |
// Default coordinates (center of world map) | |
let lat = 0; | |
let lng = 0; | |
let zoom = 1; | |
if (hasExactCoords) { | |
// Use exact coordinates if available | |
lat = parseFloat(domainInfo.latitude); | |
lng = parseFloat(domainInfo.longitude); | |
zoom = 10; // Zoom in more for exact location | |
} else if (domainInfo.country) { | |
// Use approximate country location | |
const countryCenters = { | |
'United States': [37.0902, -95.7129], | |
'Russia': [61.5240, 105.3188], | |
'China': [35.8617, 104.1954], | |
'India': [20.5937, 78.9629], | |
'Brazil': [-14.2350, -51.9253], | |
'Australia': [-25.2744, 133.7751], | |
'Canada': [56.1304, -106.3468], | |
'Germany': [51.1657, 10.4515], | |
'Japan': [36.2048, 138.2529], | |
'United Kingdom': [55.3781, -3.4360], | |
'France': [46.2276, 2.2137], | |
'Italy': [41.8719, 12.5674], | |
'South Korea': [35.9078, 127.7669], | |
'Spain': [40.4637, -3.7492], | |
'Mexico': [23.6345, -102.5528], | |
'Indonesia': [-0.7893, 113.9213], | |
'Netherlands': [52.1326, 5.2913], | |
'Switzerland': [46.8182, 8.2275], | |
'Saudi Arabia': [23.8859, 45.0792], | |
'Turkey': [38.9637, 35.2433] | |
}; | |
if (countryCenters[domainInfo.country]) { | |
[lat, lng] = countryCenters[domainInfo.country]; | |
zoom = 4; // Country level zoom | |
} | |
} | |
// Remove the IP address label above the map - we'll show it directly on the map | |
const mapContainer = document.getElementById(containerId); | |
// Check if the map library is available | |
if (typeof L !== 'undefined') { | |
try { | |
// Initialize the map | |
const map = L.map(containerId, { | |
center: [lat, lng], | |
zoom: zoom, | |
zoomControl: true, | |
attributionControl: true | |
}); | |
// Add the tile layer (OpenStreetMap) | |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', | |
maxZoom: 18 | |
}).addTo(map); | |
// Add a marker if we have a location | |
if (lat !== 0 || lng !== 0) { | |
// Create a marker with a popup | |
const marker = L.marker([lat, lng]).addTo(map); | |
// Prepare popup content | |
let popupContent = '<div style="text-align: center; padding: 5px;">'; | |
if (domainInfo.ip_address && domainInfo.ip_address !== 'Unknown') { | |
popupContent += `<strong>IP:</strong> ${domainInfo.ip_address}<br>`; | |
} | |
if (domainInfo.organization && domainInfo.organization !== 'Unknown') { | |
popupContent += `<strong>Organization:</strong> ${domainInfo.organization}<br>`; | |
} | |
if (domainInfo.city && domainInfo.city !== 'Unknown') { | |
popupContent += `<strong>City:</strong> ${domainInfo.city}<br>`; | |
} | |
if (domainInfo.country && domainInfo.country !== 'Unknown') { | |
popupContent += `<strong>Country:</strong> ${domainInfo.country}`; | |
} | |
popupContent += '</div>'; | |
// Add popup to marker | |
marker.bindPopup(popupContent); | |
// Open popup by default | |
marker.openPopup(); | |
// Add a better label with the IP address directly on the map | |
if (domainInfo.ip_address && domainInfo.ip_address !== 'Unknown') { | |
const customLabel = L.divIcon({ | |
className: 'ip-address-map-label', | |
html: `<div style="background: rgba(15, 23, 42, 0.9); color: white; padding: 6px 10px; border-radius: 6px; font-size: 13px; font-weight: bold; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.2);">IP: ${domainInfo.ip_address}</div>`, | |
iconSize: [140, 30], | |
iconAnchor: [70, 35] | |
}); | |
L.marker([lat, lng], { icon: customLabel }).addTo(map); | |
} | |
} | |
// Force map to resize to container | |
setTimeout(() => { | |
map.invalidateSize(); | |
}, 100); | |
// Remove any unused space by setting a fixed height | |
mapContainer.style.height = '180px'; | |
mapContainer.style.marginBottom = '0'; | |
} catch (e) { | |
console.error('Error creating map:', e); | |
document.getElementById(containerId).innerHTML = | |
'<div style="padding: 10px; text-align: center; color: var(--text-muted);">' + | |
'<i class="fas fa-map-marked-alt" style="font-size: 24px; margin-bottom: 10px;"></i><br>' + | |
'Map data not available</div>'; | |
} | |
} else { | |
console.error('Leaflet library not available'); | |
document.getElementById(containerId).innerHTML = | |
'<div style="padding: 10px; text-align: center; color: var(--text-muted);">' + | |
'<i class="fas fa-exclamation-circle" style="font-size: 24px; margin-bottom: 10px;"></i><br>' + | |
'Map library not available</div>'; | |
} | |
} | |
/** | |
* Displays suspicious patterns in the Suspicious Patterns section | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displaySuspiciousPatterns(data) { | |
const suspiciousPatternsContainer = document.getElementById('suspiciousPatterns'); | |
if (!suspiciousPatternsContainer) return; | |
// Clear previous content | |
suspiciousPatternsContainer.innerHTML = ''; | |
// Get patterns from data | |
const patterns = data.suspicious_patterns || []; | |
// When no patterns are found, the section risk should always be 0% | |
// This will override whatever might be in the data | |
let sectionRiskValue = patterns && patterns.length > 0 ? | |
(data.section_totals && data.section_totals["Suspicious Patterns"] || 0) : 0; | |
// Create container | |
const patternContainer = document.createElement('div'); | |
patternContainer.className = 'suspicious-patterns-container'; | |
const header = document.createElement('div'); | |
header.className = 'section-header'; | |
// Add icon and title | |
const titleContainer = document.createElement('div'); | |
titleContainer.className = 'section-title-container'; | |
const icon = document.createElement('i'); | |
icon.className = 'fas fa-exclamation-triangle'; | |
icon.style.color = '#ff6b6b'; | |
icon.style.marginRight = '10px'; | |
const title = document.createElement('h3'); | |
title.className = 'section-title'; | |
title.textContent = 'Suspicious Patterns'; | |
titleContainer.appendChild(icon); | |
titleContainer.appendChild(title); | |
header.appendChild(titleContainer); | |
patternContainer.appendChild(header); | |
// Create content container | |
const contentContainer = document.createElement('div'); | |
contentContainer.className = 'patterns-content'; | |
if (patterns && patterns.length > 0) { | |
// Calculate total risk score from patterns | |
const totalPatternRiskScore = patterns.reduce((total, pattern) => total + (pattern.risk_score || 0), 0); | |
// Add risk overview section | |
const riskOverviewSection = document.createElement('div'); | |
riskOverviewSection.className = 'risk-overview-section'; | |
riskOverviewSection.style.marginBottom = '20px'; | |
riskOverviewSection.style.padding = '15px'; | |
riskOverviewSection.style.backgroundColor = 'rgba(30, 41, 59, 0.3)'; | |
riskOverviewSection.style.borderRadius = '8px'; | |
riskOverviewSection.style.border = '1px solid rgba(79, 99, 135, 0.2)'; | |
riskOverviewSection.innerHTML = ` | |
<div style="display: flex; align-items: center; margin-bottom: 10px;"> | |
<i class="fas fa-chart-pie" style="color: #f59e0b; margin-right: 8px;"></i> | |
<strong style="font-size: 1rem;">Risk Overview</strong> | |
</div> | |
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> | |
<span>Total patterns detected:</span> | |
<strong>${patterns.length}</strong> | |
</div> | |
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> | |
<span>Combined pattern risk score:</span> | |
<strong>${totalPatternRiskScore} points</strong> | |
</div> | |
<div style="display: flex; justify-content: space-between;"> | |
<span>Section risk contribution:</span> | |
<strong style="color: #f59e0b;">${sectionRiskValue.toFixed(1)}%</strong> | |
</div> | |
`; | |
contentContainer.appendChild(riskOverviewSection); | |
// Sort patterns by severity | |
patterns.sort((a, b) => { | |
const severityOrder = { high: 0, medium: 1, low: 2 }; | |
return severityOrder[a.severity] - severityOrder[b.severity]; | |
}); | |
// Add count of patterns by severity | |
const severityCounts = { | |
high: patterns.filter(p => p.severity === 'high').length, | |
medium: patterns.filter(p => p.severity === 'medium').length, | |
low: patterns.filter(p => p.severity === 'low').length | |
}; | |
// Only add severity badges if we have patterns with that severity | |
const severityBadgesContainer = document.createElement('div'); | |
severityBadgesContainer.className = 'severity-badges'; | |
severityBadgesContainer.style.display = 'flex'; | |
severityBadgesContainer.style.marginBottom = '15px'; | |
if (severityCounts.high > 0) { | |
severityBadgesContainer.appendChild(createSeverityBadge(severityCounts.high, 'high', 'High')); | |
} | |
if (severityCounts.medium > 0) { | |
severityBadgesContainer.appendChild(createSeverityBadge(severityCounts.medium, 'medium', 'Medium')); | |
} | |
if (severityCounts.low > 0) { | |
severityBadgesContainer.appendChild(createSeverityBadge(severityCounts.low, 'low', 'Low')); | |
} | |
contentContainer.appendChild(severityBadgesContainer); | |
// Create patterns list with details for each pattern | |
const patternsList = document.createElement('div'); | |
patternsList.className = 'patterns-list'; | |
patterns.forEach(pattern => { | |
const patternItem = document.createElement('div'); | |
patternItem.className = `pattern-item ${getSeverityClass(pattern.severity)}`; | |
patternItem.style.marginBottom = '12px'; | |
patternItem.style.padding = '15px'; | |
patternItem.style.borderRadius = '8px'; | |
patternItem.style.backgroundColor = 'rgba(30, 41, 59, 0.5)'; | |
patternItem.style.borderLeft = `4px solid ${pattern.severity === 'high' ? '#ff6b6b' : pattern.severity === 'medium' ? '#ffba08' : '#90be6d'}`; | |
const patternHeader = document.createElement('div'); | |
patternHeader.className = 'pattern-header'; | |
patternHeader.style.display = 'flex'; | |
patternHeader.style.alignItems = 'center'; | |
patternHeader.style.justifyContent = 'space-between'; | |
const patternTitleWrapper = document.createElement('div'); | |
patternTitleWrapper.style.display = 'flex'; | |
patternTitleWrapper.style.alignItems = 'center'; | |
const patternIcon = document.createElement('i'); | |
patternIcon.className = pattern.severity === 'high' ? 'fas fa-exclamation-circle' : pattern.severity === 'medium' ? 'fas fa-exclamation' : 'fas fa-info-circle'; | |
patternIcon.style.marginRight = '8px'; | |
patternIcon.style.color = pattern.severity === 'high' ? '#ff6b6b' : pattern.severity === 'medium' ? '#ffba08' : '#90be6d'; | |
patternIcon.style.fontSize = '16px'; | |
const patternTitle = document.createElement('div'); | |
patternTitle.className = 'pattern-title'; | |
patternTitle.style.fontWeight = 'bold'; | |
patternTitle.style.fontSize = '1rem'; | |
patternTitle.textContent = pattern.pattern; | |
patternTitleWrapper.appendChild(patternIcon); | |
patternTitleWrapper.appendChild(patternTitle); | |
// Add risk score badge | |
const riskScoreBadge = document.createElement('div'); | |
riskScoreBadge.className = 'risk-score-badge'; | |
riskScoreBadge.style.backgroundColor = pattern.severity === 'high' ? 'rgba(239, 68, 68, 0.2)' : pattern.severity === 'medium' ? 'rgba(245, 158, 11, 0.2)' : 'rgba(16, 185, 129, 0.2)'; | |
riskScoreBadge.style.color = pattern.severity === 'high' ? '#f87171' : pattern.severity === 'medium' ? '#fbbf24' : '#34d399'; | |
riskScoreBadge.style.fontWeight = 'bold'; | |
riskScoreBadge.style.padding = '4px 8px'; | |
riskScoreBadge.style.borderRadius = '12px'; | |
riskScoreBadge.style.fontSize = '0.8rem'; | |
riskScoreBadge.style.display = 'flex'; | |
riskScoreBadge.style.alignItems = 'center'; | |
riskScoreBadge.style.justifyContent = 'center'; | |
riskScoreBadge.innerHTML = `<i class="fas fa-exclamation-triangle" style="margin-right: 4px;"></i> ${pattern.risk_score || 0} points`; | |
patternHeader.appendChild(patternTitleWrapper); | |
patternHeader.appendChild(riskScoreBadge); | |
// Only add the pattern item header (no description or technical details) | |
patternItem.appendChild(patternHeader); | |
patternsList.appendChild(patternItem); | |
}); | |
contentContainer.appendChild(patternsList); | |
// REMOVED: The duplicate section risk meter has been removed | |
} else { | |
// Display a message when no patterns are found with a green checkmark | |
const safeContainer = document.createElement('div'); | |
safeContainer.className = 'safe-container'; | |
safeContainer.style.display = 'flex'; | |
safeContainer.style.flexDirection = 'column'; | |
safeContainer.style.alignItems = 'center'; | |
safeContainer.style.justifyContent = 'center'; | |
safeContainer.style.padding = '30px 20px'; | |
safeContainer.style.backgroundColor = 'rgba(15, 30, 50, 0.3)'; | |
safeContainer.style.borderRadius = '12px'; | |
safeContainer.style.textAlign = 'center'; | |
safeContainer.style.margin = '10px 0'; | |
// Large checkmark icon | |
const checkIcon = document.createElement('div'); | |
checkIcon.innerHTML = '<i class="fas fa-check-circle" style="font-size: 48px; color: #10b981; margin-bottom: 15px;"></i>'; | |
// No patterns message | |
const noPatternTitle = document.createElement('h4'); | |
noPatternTitle.textContent = 'No Suspicious Patterns Detected'; | |
noPatternTitle.style.fontSize = '1.2rem'; | |
noPatternTitle.style.fontWeight = '600'; | |
noPatternTitle.style.marginBottom = '10px'; | |
const noPatternMessage = document.createElement('p'); | |
noPatternMessage.textContent = 'This website does not contain any known suspicious patterns or risky behaviors.'; | |
noPatternMessage.style.opacity = '0.8'; | |
noPatternMessage.style.fontSize = '0.95rem'; | |
noPatternMessage.style.maxWidth = '380px'; | |
noPatternMessage.style.lineHeight = '1.6'; | |
safeContainer.appendChild(checkIcon); | |
safeContainer.appendChild(noPatternTitle); | |
safeContainer.appendChild(noPatternMessage); | |
contentContainer.appendChild(safeContainer); | |
} | |
patternContainer.appendChild(contentContainer); | |
suspiciousPatternsContainer.appendChild(patternContainer); | |
} | |
/** | |
* Displays feature details in the Feature Details section | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displayFeatureDetails(data) { | |
const featureDetailsContainer = document.getElementById('featureDetails'); | |
if (!featureDetailsContainer) { | |
console.error("Feature details container not found with ID 'featureDetails'"); | |
return; | |
} | |
featureDetailsContainer.innerHTML = ''; | |
console.log("Displaying feature details from data:", data); | |
let features = []; | |
// Try to get features from feature_table (primary source) | |
if (data.feature_table && Array.isArray(data.feature_table)) { | |
console.log("Found feature_table:", data.feature_table); | |
features = data.feature_table; | |
} | |
// Fallback to feature_contributions if available | |
else if (data.feature_contributions && Array.isArray(data.feature_contributions)) { | |
console.log("Using feature_contributions as fallback:", data.feature_contributions); | |
features = data.feature_contributions.map(f => ({ | |
feature: f.name || f.feature_name, | |
value: f.value, | |
impact: f.percentage || 0, | |
color_class: f.color_class || 'success' | |
})); | |
} | |
// Fallback to feature_values as last resort | |
else if (data.feature_values && typeof data.feature_values === 'object') { | |
console.log("Using feature_values as fallback:", data.feature_values); | |
features = Object.entries(data.feature_values).map(([key, value]) => ({ | |
feature: key, | |
value: value, | |
impact: 0, | |
color_class: 'success' | |
})); | |
} | |
// Clear any existing description headers (to prevent duplication) | |
const existingDescHeaders = document.querySelectorAll('.feature-description-header'); | |
existingDescHeaders.forEach(header => header.remove()); | |
// Create a description header with improved explanation | |
const descriptionHeader = document.createElement('div'); | |
descriptionHeader.className = 'feature-description-header'; | |
descriptionHeader.style.marginBottom = '25px'; | |
descriptionHeader.style.fontSize = '0.95rem'; | |
descriptionHeader.style.color = 'var(--text-color)'; | |
descriptionHeader.style.display = 'flex'; | |
descriptionHeader.style.alignItems = 'flex-start'; | |
descriptionHeader.style.gap = '15px'; | |
descriptionHeader.style.backgroundColor = 'rgba(15, 30, 50, 0.4)'; | |
descriptionHeader.style.padding = '18px 20px'; | |
descriptionHeader.style.borderRadius = '12px'; | |
descriptionHeader.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; | |
descriptionHeader.style.border = '1px solid rgba(79, 99, 135, 0.2)'; | |
const infoIcon = document.createElement('i'); | |
infoIcon.className = 'fas fa-info-circle'; | |
infoIcon.style.color = '#60a5fa'; | |
infoIcon.style.marginTop = '3px'; | |
infoIcon.style.fontSize = '20px'; | |
const infoText = document.createElement('div'); | |
infoText.style.flex = '1'; | |
infoText.style.lineHeight = '1.6'; | |
infoText.innerHTML = ` | |
These are the URL and domain features analyzed to determine the risk score. | |
Each feature contributes differently to the overall risk assessment and is categorized into one of three sections: | |
<ul style="margin-top: 10px; margin-bottom: 0; padding-left: 20px;"> | |
<li><strong style="color: #60a5fa;">Key Risk Factors (AI Predict):</strong> Core URL and domain characteristics (40% of score)</li> | |
<li><strong style="color: #10b981;">Domain Information:</strong> Registration and hosting details (10% of score)</li> | |
<li><strong style="color: #f59e0b;">Suspicious Patterns:</strong> Security issues and content analysis (50% of score)</li> | |
</ul> | |
`; | |
descriptionHeader.appendChild(infoIcon); | |
descriptionHeader.appendChild(infoText); | |
// Insert the description at the beginning of the main section-content container | |
const sectionContent = featureDetailsContainer.closest('.section-content'); | |
if (sectionContent) { | |
sectionContent.insertBefore(descriptionHeader, sectionContent.firstChild); | |
} | |
// Create explanations for common features | |
const featureExplanations = { | |
// Key Risk Factors | |
"https_present": "Websites without HTTPS are insecure and allow data interception. Phishing sites often lack proper encryption.", | |
"domain_length": "Unusually long domain names may be trying to impersonate legitimate sites or hide suspicious elements.", | |
"domain_entropy": "Higher entropy (randomness) in domain names is associated with algorithmically generated phishing domains.", | |
"special_characters": "An abnormal number of special characters is commonly seen in deceptive URLs trying to confuse users.", | |
"tld_score": "Certain top-level domains are more commonly associated with malicious websites due to lower registration restrictions.", | |
"url_length": "Excessively long URLs often attempt to hide the true destination or include suspicious parameters.", | |
"subdomain_count": "Multiple subdomains can be used to make a URL appear legitimate while hiding the actual domain.", | |
"numeric_path": "Paths consisting only of numbers are uncommon in legitimate websites and may indicate an automated attack.", | |
"digit_percentage": "High percentage of digits in a URL is unusual and often associated with malicious sites.", | |
"keyword_count": "Presence of terms like 'login', 'verify', 'account' may indicate phishing attempts.", | |
"path_length": "Unusually long URL paths can be used to hide malicious destinations.", | |
"query_length": "Excessively long query parameters may contain obfuscated malicious code.", | |
"fragment_length": "Long URL fragments (#) may be used to evade security scanning.", | |
// Domain Information | |
"ip_resolution": "Domains that can't be resolved to an IP address may be newly registered for phishing or no longer active.", | |
"rep_domain_age_category": "Newly registered domains have a higher likelihood of being used for malicious purposes.", | |
"whois_recently_registered": "Domains registered in the past 30 days have a higher risk of being used for fraud.", | |
"geo_suspicious_country": "Some countries have higher rates of hosting malicious websites due to lax cybercrime enforcement.", | |
"ct_suspicious_cert_pattern": "Certificate Transparency logs show suspicious certificate issuance patterns for this domain.", | |
"ip_blacklisted": "The IP address hosting this domain is on known malware or spam blacklists.", | |
"domain_blacklisted": "This domain appears on reputation blacklists for previous malicious activity.", | |
// Content Features | |
"favicon_present": "Legitimate sites typically have a favicon (site icon). Phishing sites often neglect this detail.", | |
"content_form_count": "Multiple forms may indicate attempts to collect sensitive information.", | |
"content_password_field_count": "Password fields on unexpected pages may indicate credential harvesting.", | |
"content_external_resources_count": "High numbers of external resources can indicate content loaded from malicious sources.", | |
"content_js_to_html_ratio": "Excessive JavaScript relative to HTML content may indicate obfuscation techniques.", | |
"content_title_brand_mismatch": "Mismatch between page title and domain name is common in impersonation attempts.", | |
"content_similar_domain_redirect": "Redirects to similar but different domains may indicate a bait-and-switch attack.", | |
"html_security_score": "Overall security analysis of the HTML content, including forms, scripts, and iframe usage.", | |
"html_risk_factor_count": "Number of suspicious elements detected in the HTML content.", | |
"html_has_password_field": "Presence of password fields may indicate an attempt to steal credentials.", | |
"html_has_obfuscated_js": "Obfuscated JavaScript is often used to hide malicious code from detection." | |
}; | |
if (features.length > 0) { | |
// Sort features by impact | |
features.sort((a, b) => Math.abs(b.impact || 0) - Math.abs(a.impact || 0)); | |
// Create feature categories | |
const featureCategories = { | |
url: ['url_length', 'path_length', 'query_length', 'fragment_length', 'special_char_count', 'numeric_path'], | |
domain: ['domain_length', 'domain_entropy', 'subdomain_count', 'tld_score', 'ip_url'], | |
security: ['https_present', 'ssl_valid', 'hsts_present'], | |
content: ['keyword_count', 'digit_percentage', 'letter_percentage', 'suspicious_keywords'] | |
}; | |
// Create a direct display (non-accordion) for each category | |
const createCategorySection = (title, icon, features, accentColor) => { | |
if (features.length === 0) return null; | |
const sectionContainer = document.createElement('div'); | |
sectionContainer.className = 'feature-category-section'; | |
sectionContainer.style.marginBottom = '30px'; | |
sectionContainer.style.backgroundColor = 'rgba(15, 30, 50, 0.3)'; | |
sectionContainer.style.borderRadius = '12px'; | |
sectionContainer.style.overflow = 'hidden'; | |
sectionContainer.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.1)'; | |
sectionContainer.style.border = '1px solid rgba(79, 99, 135, 0.15)'; | |
// Create header | |
const headerEl = document.createElement('div'); | |
headerEl.className = 'category-header'; | |
headerEl.style.padding = '16px 20px'; | |
headerEl.style.backgroundColor = 'rgba(15, 30, 50, 0.6)'; | |
headerEl.style.borderBottom = `1px solid rgba(79, 99, 135, 0.2)`; | |
headerEl.style.display = 'flex'; | |
headerEl.style.alignItems = 'center'; | |
headerEl.style.gap = '12px'; | |
const iconEl = document.createElement('i'); | |
iconEl.className = `fas ${icon}`; | |
iconEl.style.width = '24px'; | |
iconEl.style.height = '24px'; | |
iconEl.style.display = 'flex'; | |
iconEl.style.alignItems = 'center'; | |
iconEl.style.justifyContent = 'center'; | |
iconEl.style.color = accentColor; | |
iconEl.style.fontSize = '16px'; | |
const titleEl = document.createElement('span'); | |
titleEl.style.fontWeight = '600'; | |
titleEl.style.fontSize = '1.05rem'; | |
titleEl.textContent = title; | |
const countEl = document.createElement('span'); | |
countEl.style.fontSize = '0.85rem'; | |
countEl.style.backgroundColor = 'rgba(79, 99, 135, 0.3)'; | |
countEl.style.color = '#e2e8f0'; | |
countEl.style.padding = '3px 8px'; | |
countEl.style.borderRadius = '12px'; | |
countEl.style.marginLeft = '8px'; | |
countEl.textContent = `${features.length}`; | |
headerEl.appendChild(iconEl); | |
headerEl.appendChild(titleEl); | |
headerEl.appendChild(countEl); | |
// Create content container | |
const contentEl = document.createElement('div'); | |
contentEl.className = 'category-content'; | |
contentEl.style.padding = '5px'; | |
// Create table for features | |
const tableEl = document.createElement('table'); | |
tableEl.className = 'feature-table'; | |
tableEl.style.width = '100%'; | |
tableEl.style.borderCollapse = 'collapse'; | |
tableEl.style.margin = '0'; | |
// Add header row | |
const theadEl = document.createElement('thead'); | |
const headerRowEl = document.createElement('tr'); | |
// Check if this is the Content Features section | |
const isContentFeatures = title === 'Content Features'; | |
// Define headers based on section type | |
const headers = isContentFeatures | |
? ['Feature', 'Why It Matters'] | |
: ['Feature', 'Value', 'Impact on Risk', 'Why It Matters']; | |
headers.forEach((headerText, index) => { | |
const th = document.createElement('th'); | |
th.style.padding = '14px 16px'; | |
th.style.textAlign = 'left'; | |
th.style.color = '#94a3b8'; | |
th.style.fontSize = '0.85rem'; | |
th.style.fontWeight = '600'; | |
th.style.letterSpacing = '0.025em'; | |
th.style.textTransform = 'uppercase'; | |
th.style.borderBottom = '1px solid rgba(79, 99, 135, 0.15)'; | |
th.textContent = headerText; | |
// Set column widths | |
if (isContentFeatures) { | |
// For Content Features section with only 2 columns | |
if (index === 0) { | |
th.style.width = '25%'; | |
} else { | |
th.style.width = '75%'; | |
} | |
} else { | |
// For other sections with 4 columns | |
if (index === 0) { | |
th.style.width = '18%'; | |
} else if (index === 1) { | |
th.style.width = '10%'; | |
} else if (index === 2) { | |
th.style.width = '15%'; | |
} else { | |
th.style.width = '57%'; | |
} | |
} | |
headerRowEl.appendChild(th); | |
}); | |
theadEl.appendChild(headerRowEl); | |
tableEl.appendChild(theadEl); | |
// Add body rows | |
const tbodyEl = document.createElement('tbody'); | |
features.forEach((feature, index) => { | |
const row = document.createElement('tr'); | |
row.style.transition = 'background-color 0.2s ease, transform 0.1s ease'; | |
row.style.backgroundColor = index % 2 === 0 ? 'rgba(30, 41, 59, 0.2)' : 'transparent'; | |
// Add hover effect | |
row.addEventListener('mouseover', () => { | |
row.style.backgroundColor = 'rgba(51, 65, 85, 0.4)'; | |
row.style.transform = 'translateX(3px)'; | |
}); | |
row.addEventListener('mouseout', () => { | |
row.style.backgroundColor = index % 2 === 0 ? 'rgba(30, 41, 59, 0.2)' : 'transparent'; | |
row.style.transform = 'translateX(0)'; | |
}); | |
// Feature name cell | |
const nameCell = document.createElement('td'); | |
nameCell.style.padding = '12px 16px'; | |
nameCell.style.borderBottom = '1px solid rgba(79, 99, 135, 0.1)'; | |
nameCell.style.fontSize = '0.9rem'; | |
const nameContent = document.createElement('div'); | |
nameContent.style.display = 'flex'; | |
nameContent.style.alignItems = 'center'; | |
nameContent.style.gap = '10px'; | |
// Add feature icon based on category | |
const featureIconEl = document.createElement('div'); | |
featureIconEl.style.width = '28px'; | |
featureIconEl.style.height = '28px'; | |
featureIconEl.style.minWidth = '28px'; | |
featureIconEl.style.borderRadius = '6px'; | |
featureIconEl.style.display = 'flex'; | |
featureIconEl.style.alignItems = 'center'; | |
featureIconEl.style.justifyContent = 'center'; | |
const iconI = document.createElement('i'); | |
if (title === 'URL Features') { | |
featureIconEl.style.backgroundColor = 'rgba(59, 130, 246, 0.15)'; | |
iconI.className = 'fas fa-link'; | |
iconI.style.color = '#60a5fa'; | |
} else if (title === 'Domain Features') { | |
featureIconEl.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; | |
iconI.className = 'fas fa-globe'; | |
iconI.style.color = '#10b981'; | |
} else if (title === 'Security Features') { | |
featureIconEl.style.backgroundColor = 'rgba(245, 158, 11, 0.15)'; | |
iconI.className = 'fas fa-shield-alt'; | |
iconI.style.color = '#f59e0b'; | |
} else { | |
featureIconEl.style.backgroundColor = 'rgba(139, 92, 246, 0.15)'; | |
iconI.className = 'fas fa-file-alt'; | |
iconI.style.color = '#8b5cf6'; | |
} | |
featureIconEl.appendChild(iconI); | |
const nameTextEl = document.createElement('span'); | |
nameTextEl.style.fontWeight = '500'; | |
nameTextEl.textContent = formatFeatureName(feature.feature); | |
nameContent.appendChild(featureIconEl); | |
nameContent.appendChild(nameTextEl); | |
nameCell.appendChild(nameContent); | |
row.appendChild(nameCell); | |
// Only add Value and Impact on Risk columns for non-Content Features sections | |
if (!isContentFeatures) { | |
// Feature value cell | |
const valueCell = document.createElement('td'); | |
valueCell.style.padding = '12px 16px'; | |
valueCell.style.borderBottom = '1px solid rgba(79, 99, 135, 0.1)'; | |
valueCell.style.fontSize = '0.9rem'; | |
// Format value based on feature type | |
let formattedValue = feature.value; | |
const featureName = feature.feature.toLowerCase(); | |
if (featureName.includes('present') || featureName.includes('valid') || featureName === 'ip_url') { | |
// Boolean features - show Yes/No | |
const boolValue = parseFloat(feature.value) > 0; | |
formattedValue = boolValue ? 'Yes' : 'No'; | |
const valueSpan = document.createElement('span'); | |
valueSpan.style.padding = '3px 8px'; | |
valueSpan.style.borderRadius = '4px'; | |
valueSpan.style.fontSize = '0.85rem'; | |
valueSpan.style.fontWeight = '500'; | |
if ((featureName.includes('present') || featureName.includes('valid')) && !boolValue) { | |
// Missing security feature - show as risky | |
valueSpan.style.backgroundColor = 'rgba(239, 68, 68, 0.15)'; | |
valueSpan.style.color = '#f87171'; | |
} else if (featureName === 'ip_url' && boolValue) { | |
// IP as URL is risky | |
valueSpan.style.backgroundColor = 'rgba(239, 68, 68, 0.15)'; | |
valueSpan.style.color = '#f87171'; | |
} else { | |
// Normal state | |
valueSpan.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; | |
valueSpan.style.color = '#34d399'; | |
} | |
valueSpan.textContent = formattedValue; | |
valueCell.appendChild(valueSpan); | |
} else if (featureName.includes('percentage')) { | |
// Percentage values | |
const percentValue = parseFloat(feature.value); | |
formattedValue = percentValue.toFixed(1) + '%'; | |
valueCell.textContent = formattedValue; | |
} else if (featureName === 'tld_score') { | |
// TLD score is 0-1 | |
const tldValue = parseFloat(feature.value); | |
const tldSpan = document.createElement('span'); | |
tldSpan.style.padding = '2px 6px'; | |
tldSpan.style.borderRadius = '4px'; | |
tldSpan.style.fontSize = '0.85rem'; | |
// Color based on TLD risk | |
if (tldValue > 0.6) { | |
tldSpan.style.backgroundColor = 'rgba(239, 68, 68, 0.15)'; | |
tldSpan.style.color = '#f87171'; | |
} else if (tldValue > 0.3) { | |
tldSpan.style.backgroundColor = 'rgba(245, 158, 11, 0.15)'; | |
tldSpan.style.color = '#fbbf24'; | |
} else { | |
tldSpan.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; | |
tldSpan.style.color = '#34d399'; | |
} | |
tldSpan.textContent = tldValue.toFixed(2); | |
valueCell.appendChild(tldSpan); | |
} else if (featureName === 'domain_entropy') { | |
// Domain entropy (higher is riskier) | |
const entropyValue = parseFloat(feature.value); | |
const entropySpan = document.createElement('span'); | |
entropySpan.style.padding = '2px 6px'; | |
entropySpan.style.borderRadius = '4px'; | |
entropySpan.style.fontSize = '0.85rem'; | |
// Color based on entropy value | |
if (entropyValue > 4) { | |
entropySpan.style.backgroundColor = 'rgba(239, 68, 68, 0.15)'; | |
entropySpan.style.color = '#f87171'; | |
} else if (entropyValue > 3) { | |
entropySpan.style.backgroundColor = 'rgba(245, 158, 11, 0.15)'; | |
entropySpan.style.color = '#fbbf24'; | |
} else { | |
entropySpan.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; | |
entropySpan.style.color = '#34d399'; | |
} | |
entropySpan.textContent = entropyValue.toFixed(2); | |
valueCell.appendChild(entropySpan); | |
} else { | |
// Default formatting | |
valueCell.textContent = formattedValue; | |
} | |
row.appendChild(valueCell); | |
// Feature impact cell | |
const impactCell = document.createElement('td'); | |
impactCell.style.padding = '12px 16px'; | |
impactCell.style.borderBottom = '1px solid rgba(79, 99, 135, 0.1)'; | |
const impact = parseFloat(feature.impact || 0); | |
if (!isNaN(impact) && impact > 0) { | |
// Create impact container | |
const impactContainer = document.createElement('div'); | |
impactContainer.style.display = 'flex'; | |
impactContainer.style.alignItems = 'center'; | |
impactContainer.style.gap = '10px'; | |
// Create impact value | |
const impactValueEl = document.createElement('div'); | |
impactValueEl.style.minWidth = '45px'; | |
impactValueEl.style.fontWeight = '600'; | |
impactValueEl.style.fontSize = '0.9rem'; | |
impactValueEl.style.padding = '2px 6px'; | |
impactValueEl.style.borderRadius = '4px'; | |
impactValueEl.style.textAlign = 'center'; | |
// Set color based on impact | |
let colorClass = feature.color_class || 'success'; | |
if (impact > 60) { | |
colorClass = 'danger'; | |
impactValueEl.style.backgroundColor = 'rgba(239, 68, 68, 0.15)'; | |
impactValueEl.style.color = '#f87171'; | |
} else if (impact > 20) { | |
colorClass = 'warning'; | |
impactValueEl.style.backgroundColor = 'rgba(245, 158, 11, 0.15)'; | |
impactValueEl.style.color = '#fbbf24'; | |
} else { | |
impactValueEl.style.backgroundColor = 'rgba(16, 185, 129, 0.15)'; | |
impactValueEl.style.color = '#34d399'; | |
} | |
impactValueEl.textContent = `${impact.toFixed(1)}%`; | |
// Create impact bar container | |
const barContainerEl = document.createElement('div'); | |
barContainerEl.style.flex = '1'; | |
barContainerEl.style.height = '8px'; | |
barContainerEl.style.backgroundColor = 'rgba(30, 41, 59, 0.5)'; | |
barContainerEl.style.borderRadius = '4px'; | |
barContainerEl.style.overflow = 'hidden'; | |
barContainerEl.style.position = 'relative'; | |
barContainerEl.style.cursor = 'pointer'; | |
// Create impact bar | |
const barEl = document.createElement('div'); | |
barEl.style.height = '100%'; | |
barEl.style.width = `${Math.min(impact * 1.5, 100)}%`; | |
barEl.style.borderRadius = '4px'; | |
barEl.style.transition = 'width 1s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.3s ease'; | |
// Set bar color with gradient | |
if (colorClass === 'danger') { | |
barEl.style.background = 'linear-gradient(90deg, #ef4444, #b91c1c)'; | |
barEl.style.boxShadow = '0 0 8px rgba(239, 68, 68, 0.3)'; | |
} else if (colorClass === 'warning') { | |
barEl.style.background = 'linear-gradient(90deg, #f59e0b, #d97706)'; | |
barEl.style.boxShadow = '0 0 8px rgba(245, 158, 11, 0.3)'; | |
} else { | |
barEl.style.background = 'linear-gradient(90deg, #10b981, #059669)'; | |
barEl.style.boxShadow = '0 0 8px rgba(16, 185, 129, 0.3)'; | |
} | |
// Create tooltip for additional impact information | |
const tooltip = document.createElement('div'); | |
tooltip.style.position = 'absolute'; | |
tooltip.style.bottom = '100%'; | |
tooltip.style.left = '50%'; | |
tooltip.style.transform = 'translateX(-50%)'; | |
tooltip.style.backgroundColor = 'rgba(15, 23, 42, 0.95)'; | |
tooltip.style.color = '#f8fafc'; | |
tooltip.style.padding = '8px 12px'; | |
tooltip.style.borderRadius = '6px'; | |
tooltip.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'; | |
tooltip.style.fontSize = '0.85rem'; | |
tooltip.style.whiteSpace = 'nowrap'; | |
tooltip.style.zIndex = '10'; | |
tooltip.style.marginBottom = '8px'; | |
tooltip.style.border = '1px solid rgba(79, 99, 135, 0.3)'; | |
tooltip.style.display = 'none'; | |
tooltip.style.textAlign = 'center'; | |
tooltip.style.backdropFilter = 'blur(4px)'; | |
const featureName = formatFeatureName(feature.feature); | |
let riskLevel = 'Low'; | |
if (impact > 60) riskLevel = 'High'; | |
else if (impact > 20) riskLevel = 'Medium'; | |
tooltip.innerHTML = ` | |
<div style="font-weight: 600; margin-bottom: 4px;">${featureName}</div> | |
<div>Impact: <span style="font-weight: 600;">${impact.toFixed(1)}%</span></div> | |
<div>Risk Level: <span style="font-weight: 600; color: ${colorClass === 'danger' ? '#f87171' : colorClass === 'warning' ? '#fbbf24' : '#34d399'}">${riskLevel}</span></div> | |
`; | |
// Add arrow to tooltip | |
const tooltipArrow = document.createElement('div'); | |
tooltipArrow.style.position = 'absolute'; | |
tooltipArrow.style.bottom = '-5px'; | |
tooltipArrow.style.left = '50%'; | |
tooltipArrow.style.transform = 'translateX(-50%) rotate(45deg)'; | |
tooltipArrow.style.width = '10px'; | |
tooltipArrow.style.height = '10px'; | |
tooltipArrow.style.backgroundColor = 'rgba(15, 23, 42, 0.95)'; | |
tooltipArrow.style.borderRight = '1px solid rgba(79, 99, 135, 0.3)'; | |
tooltipArrow.style.borderBottom = '1px solid rgba(79, 99, 135, 0.3)'; | |
tooltip.appendChild(tooltipArrow); | |
// Toggle tooltip on hover | |
barContainerEl.addEventListener('mouseover', () => { | |
tooltip.style.display = 'block'; | |
barEl.style.filter = 'brightness(1.2)'; | |
}); | |
barContainerEl.addEventListener('mouseout', () => { | |
tooltip.style.display = 'none'; | |
barEl.style.filter = 'brightness(1)'; | |
}); | |
barContainerEl.appendChild(barEl); | |
barContainerEl.appendChild(tooltip); | |
// Ensure proper assembly of elements | |
impactContainer.appendChild(impactValueEl); | |
impactContainer.appendChild(barContainerEl); | |
impactCell.appendChild(impactContainer); | |
} else { | |
// No impact data - create empty bar instead of just text | |
const impactContainer = document.createElement('div'); | |
impactContainer.style.display = 'flex'; | |
impactContainer.style.alignItems = 'center'; | |
impactContainer.style.gap = '10px'; | |
// Create impact value with "No impact" label | |
const impactValueEl = document.createElement('div'); | |
impactValueEl.style.minWidth = '70px'; | |
impactValueEl.style.fontSize = '0.85rem'; | |
impactValueEl.style.padding = '2px 6px'; | |
impactValueEl.style.borderRadius = '4px'; | |
impactValueEl.style.backgroundColor = 'rgba(100, 116, 139, 0.2)'; | |
impactValueEl.style.color = '#94a3b8'; | |
impactValueEl.style.textAlign = 'center'; | |
impactValueEl.textContent = 'No impact'; | |
// Create empty bar container | |
const barContainerEl = document.createElement('div'); | |
barContainerEl.style.flex = '1'; | |
barContainerEl.style.height = '8px'; | |
barContainerEl.style.backgroundColor = 'rgba(30, 41, 59, 0.5)'; | |
barContainerEl.style.borderRadius = '4px'; | |
barContainerEl.style.overflow = 'hidden'; | |
impactContainer.appendChild(impactValueEl); | |
impactContainer.appendChild(barContainerEl); | |
impactCell.appendChild(impactContainer); | |
} | |
row.appendChild(impactCell); | |
} | |
// Add explanation cell | |
const explanationCell = document.createElement('td'); | |
explanationCell.style.padding = '12px 16px'; | |
explanationCell.style.borderBottom = '1px solid rgba(79, 99, 135, 0.1)'; | |
explanationCell.style.fontSize = '0.85rem'; | |
explanationCell.style.lineHeight = '1.5'; | |
explanationCell.style.color = '#e2e8f0'; | |
// For Content Features, make the explanation cell larger | |
if (isContentFeatures) { | |
explanationCell.style.fontSize = '0.9rem'; | |
explanationCell.style.lineHeight = '1.6'; | |
} | |
// Get explanation for this feature | |
let featureKey = feature.feature.toLowerCase(); | |
let explanation = ''; | |
// Look for exact match first | |
if (featureExplanations[featureKey]) { | |
explanation = featureExplanations[featureKey]; | |
} else { | |
// Look for partial matches in the keys | |
for (const [key, value] of Object.entries(featureExplanations)) { | |
if (featureKey.includes(key) || key.includes(featureKey)) { | |
explanation = value; | |
break; | |
} | |
} | |
// If still no match, provide generic explanations based on feature name | |
if (!explanation) { | |
if (featureKey.includes('https') || featureKey.includes('ssl') || featureKey.includes('security')) { | |
explanation = "Security features help protect user data. Missing security features indicate higher risk."; | |
} else if (featureKey.includes('domain')) { | |
explanation = "Domain characteristics can indicate potential phishing or deception attempts."; | |
} else if (featureKey.includes('content') || featureKey.includes('html')) { | |
explanation = "Website content analysis can reveal suspicious elements used in phishing pages."; | |
} else { | |
explanation = "This feature is analyzed as part of the machine learning model's risk assessment."; | |
} | |
} | |
} | |
// Highlight key terms in the explanation | |
const highlightTerms = ["phishing", "malicious", "suspicious", "risk", "fraud", "deceptive", "insecure"]; | |
let highlightedExplanation = explanation; | |
highlightTerms.forEach(term => { | |
const regex = new RegExp(`\\b${term}\\b`, 'gi'); | |
highlightedExplanation = highlightedExplanation.replace( | |
regex, | |
`<span style="color: #fcd34d; font-weight: 500;">$&</span>` | |
); | |
}); | |
explanationCell.innerHTML = highlightedExplanation; | |
row.appendChild(explanationCell); | |
tbodyEl.appendChild(row); | |
}); | |
tableEl.appendChild(tbodyEl); | |
contentEl.appendChild(tableEl); | |
sectionContainer.appendChild(headerEl); | |
sectionContainer.appendChild(contentEl); | |
return sectionContainer; | |
}; | |
// Categorize features | |
const categorizedFeatures = { | |
url: [], | |
domain: [], | |
security: [], | |
content: [] | |
}; | |
features.forEach(feature => { | |
const featureName = feature.feature.toLowerCase(); | |
// Check which category this feature belongs to | |
for (const [category, featureList] of Object.entries(featureCategories)) { | |
if (featureList.some(f => featureName.includes(f))) { | |
categorizedFeatures[category].push(feature); | |
return; | |
} | |
} | |
// Default to content category if not found | |
categorizedFeatures.content.push(feature); | |
}); | |
// Create sections for each category with specific accent colors | |
const urlSection = createCategorySection('URL Features', 'fa-link', categorizedFeatures.url, '#60a5fa'); | |
const domainSection = createCategorySection('Domain Features', 'fa-globe', categorizedFeatures.domain, '#10b981'); | |
const securitySection = createCategorySection('Security Features', 'fa-shield-alt', categorizedFeatures.security, '#f59e0b'); | |
const contentSection = createCategorySection('Content Features', 'fa-file-alt', categorizedFeatures.content, '#8b5cf6'); | |
// Add animation style for staggered appearance | |
const animationStyle = document.createElement('style'); | |
animationStyle.textContent = ` | |
@keyframes featureSectionFadeIn { | |
from { | |
opacity: 0; | |
transform: translateY(15px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
`; | |
document.head.appendChild(animationStyle); | |
// Add sections to container with staggered animations | |
if (urlSection) { | |
urlSection.style.animation = 'featureSectionFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards'; | |
featureDetailsContainer.appendChild(urlSection); | |
} | |
if (domainSection) { | |
domainSection.style.animation = 'featureSectionFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.1s forwards'; | |
domainSection.style.opacity = '0'; | |
featureDetailsContainer.appendChild(domainSection); | |
} | |
if (securitySection) { | |
securitySection.style.animation = 'featureSectionFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.2s forwards'; | |
securitySection.style.opacity = '0'; | |
featureDetailsContainer.appendChild(securitySection); | |
} | |
if (contentSection) { | |
contentSection.style.animation = 'featureSectionFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.3s forwards'; | |
contentSection.style.opacity = '0'; | |
featureDetailsContainer.appendChild(contentSection); | |
} | |
} else { | |
// Add a message if no features found | |
const noFeaturesEl = document.createElement('div'); | |
noFeaturesEl.style.padding = '40px 20px'; | |
noFeaturesEl.style.textAlign = 'center'; | |
noFeaturesEl.style.color = '#94a3b8'; | |
noFeaturesEl.style.backgroundColor = 'rgba(15, 30, 50, 0.3)'; | |
noFeaturesEl.style.borderRadius = '12px'; | |
noFeaturesEl.style.border = '1px solid rgba(79, 99, 135, 0.15)'; | |
noFeaturesEl.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; | |
const icon = document.createElement('i'); | |
icon.className = 'fas fa-info-circle'; | |
icon.style.fontSize = '32px'; | |
icon.style.marginBottom = '15px'; | |
icon.style.color = '#60a5fa'; | |
const text = document.createElement('div'); | |
text.textContent = 'No feature details available for this analysis.'; | |
text.style.fontSize = '1rem'; | |
noFeaturesEl.appendChild(icon); | |
noFeaturesEl.appendChild(text); | |
featureDetailsContainer.appendChild(noFeaturesEl); | |
} | |
} | |
/** | |
* Displays HTML security information in the Content Security Analysis section | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displayHtmlSecurity(data) { | |
const securityRisksContainer = document.getElementById('htmlSecurityRisks'); | |
const securityChecksContainer = document.getElementById('htmlSecurityChecks'); | |
const securityScoreElement = document.getElementById('htmlSecurityScore'); | |
const scoreCircle = document.getElementById('htmlSecurityScoreCircle'); | |
if (!securityRisksContainer || !securityChecksContainer || !securityScoreElement || !scoreCircle) { | |
console.error("HTML Security containers not found"); | |
return; | |
} | |
securityRisksContainer.innerHTML = ''; | |
securityChecksContainer.innerHTML = ''; | |
// Remove any existing info notes to avoid duplication | |
const securityContentContainer = securityScoreElement.closest('.security-analysis-content'); | |
if (securityContentContainer) { | |
const existingInfoNotes = securityContentContainer.querySelectorAll('.content-security-info-note'); | |
existingInfoNotes.forEach(note => note.remove()); | |
} | |
// Add modern informational card about this section | |
const infoNote = document.createElement('div'); | |
infoNote.className = 'content-security-info-note info-note'; | |
infoNote.style.fontSize = '0.9rem'; | |
infoNote.style.color = '#cbd5e1'; | |
infoNote.style.marginTop = '15px'; | |
infoNote.style.marginBottom = '20px'; | |
infoNote.style.padding = '12px 16px'; | |
infoNote.style.backgroundColor = 'rgba(30, 41, 59, 0.4)'; | |
infoNote.style.borderRadius = '10px'; | |
infoNote.style.border = '1px solid rgba(71, 85, 105, 0.2)'; | |
infoNote.style.display = 'flex'; | |
infoNote.style.alignItems = 'center'; | |
infoNote.style.gap = '10px'; | |
infoNote.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)'; | |
infoNote.style.animation = 'fadeIn 0.5s ease-out'; | |
// Create info icon | |
const infoIcon = document.createElement('div'); | |
infoIcon.style.display = 'flex'; | |
infoIcon.style.alignItems = 'center'; | |
infoIcon.style.justifyContent = 'center'; | |
infoIcon.style.width = '32px'; | |
infoIcon.style.height = '32px'; | |
infoIcon.style.borderRadius = '50%'; | |
infoIcon.style.backgroundColor = 'rgba(59, 130, 246, 0.2)'; | |
infoIcon.style.color = '#60a5fa'; | |
infoIcon.style.flexShrink = '0'; | |
infoIcon.innerHTML = '<i class="fas fa-info" style="font-size: 14px;"></i>'; | |
const infoText = document.createElement('div'); | |
infoText.style.flexGrow = '1'; | |
infoText.style.lineHeight = '1.5'; | |
infoText.textContent = 'This analysis examines the website\'s HTML content for security issues. This is shown for informational purposes only and is not included in the overall risk score.'; | |
infoNote.appendChild(infoIcon); | |
infoNote.appendChild(infoText); | |
// Insert the info note after the score display | |
if (securityContentContainer) { | |
securityContentContainer.insertBefore(infoNote, securityContentContainer.children[1]); | |
} | |
// Case 1: HTML security data is not available | |
if (!data.html_security) { | |
const noDataContainer = document.createElement('div'); | |
noDataContainer.className = 'no-data-container'; | |
noDataContainer.style.textAlign = 'center'; | |
noDataContainer.style.padding = '25px 20px'; | |
noDataContainer.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
noDataContainer.style.borderRadius = '12px'; | |
noDataContainer.style.margin = '15px 0'; | |
const noDataIcon = document.createElement('div'); | |
noDataIcon.innerHTML = '<i class="fas fa-database-slash" style="font-size: 42px; color: #64748b; margin-bottom: 15px;"></i>'; | |
const noDataTitle = document.createElement('h4'); | |
noDataTitle.textContent = 'No Content Analysis Available'; | |
noDataTitle.style.fontSize = '1.1rem'; | |
noDataTitle.style.fontWeight = '600'; | |
noDataTitle.style.marginBottom = '8px'; | |
const noDataText = document.createElement('p'); | |
noDataText.textContent = 'The system was unable to analyze the website content.'; | |
noDataText.style.fontSize = '0.9rem'; | |
noDataText.style.opacity = '0.8'; | |
noDataContainer.appendChild(noDataIcon); | |
noDataContainer.appendChild(noDataTitle); | |
noDataContainer.appendChild(noDataText); | |
securityRisksContainer.appendChild(noDataContainer); | |
// Set default score | |
securityScoreElement.textContent = "N/A"; | |
scoreCircle.className = 'score-circle'; | |
return; | |
} | |
// Case 2: HTML security analysis encountered an error | |
if (data.html_security.risk_factors && data.html_security.risk_factors.length > 0 && | |
data.html_security.risk_factors[0].includes("Error analyzing content")) { | |
// Display error message with better styling | |
const errorContainer = document.createElement('div'); | |
errorContainer.className = 'error-container'; | |
errorContainer.style.display = 'flex'; | |
errorContainer.style.padding = '16px'; | |
errorContainer.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; | |
errorContainer.style.borderRadius = '10px'; | |
errorContainer.style.borderLeft = '4px solid #ef4444'; | |
errorContainer.style.margin = '10px 0'; | |
errorContainer.style.alignItems = 'flex-start'; | |
errorContainer.style.gap = '12px'; | |
const errorIcon = document.createElement('div'); | |
errorIcon.className = 'error-icon'; | |
errorIcon.innerHTML = '<i class="fas fa-exclamation-triangle" style="color: #ef4444; font-size: 20px;"></i>'; | |
const errorContent = document.createElement('div'); | |
errorContent.className = 'error-content'; | |
errorContent.style.flexGrow = '1'; | |
const errorTitle = document.createElement('div'); | |
errorTitle.className = 'error-title'; | |
errorTitle.textContent = 'Content Analysis Error'; | |
errorTitle.style.fontWeight = '600'; | |
errorTitle.style.marginBottom = '4px'; | |
errorTitle.style.fontSize = '1rem'; | |
const errorDetails = document.createElement('div'); | |
errorDetails.className = 'error-details'; | |
errorDetails.textContent = data.html_security.risk_factors[0]; | |
errorDetails.style.fontSize = '0.9rem'; | |
errorDetails.style.opacity = '0.9'; | |
errorDetails.style.lineHeight = '1.5'; | |
errorContent.appendChild(errorTitle); | |
errorContent.appendChild(errorDetails); | |
errorContainer.appendChild(errorIcon); | |
errorContainer.appendChild(errorContent); | |
securityRisksContainer.appendChild(errorContainer); | |
// Update score to reflect the error risk | |
const errorScore = data.html_security.content_score || 25; | |
securityScoreElement.textContent = errorScore; | |
// Update the progress circle | |
updateScoreCircle(errorScore); | |
return; | |
} | |
// Case 3: Normal operation - Update the score | |
const contentScore = data.html_security.content_score || 0; | |
securityScoreElement.textContent = contentScore; | |
// Update the progress circle | |
updateScoreCircle(contentScore); | |
// Display risk factors with improved styles | |
const riskFactors = data.html_security.risk_factors || []; | |
if (riskFactors.length === 0) { | |
// Create a nice "no issues found" card | |
const safeContainer = document.createElement('div'); | |
safeContainer.className = 'safe-container'; | |
safeContainer.style.display = 'flex'; | |
safeContainer.style.flexDirection = 'column'; | |
safeContainer.style.alignItems = 'center'; | |
safeContainer.style.justifyContent = 'center'; | |
safeContainer.style.padding = '30px 20px'; | |
safeContainer.style.backgroundColor = 'rgba(16, 185, 129, 0.1)'; | |
safeContainer.style.borderRadius = '12px'; | |
safeContainer.style.textAlign = 'center'; | |
safeContainer.style.margin = '15px 0'; | |
safeContainer.style.border = '1px solid rgba(16, 185, 129, 0.3)'; | |
safeContainer.style.animation = 'fadeIn 0.6s ease-out'; | |
const checkIcon = document.createElement('div'); | |
checkIcon.innerHTML = '<i class="fas fa-shield-check" style="font-size: 48px; color: #10b981; margin-bottom: 15px;"></i>'; | |
const safeTitle = document.createElement('h4'); | |
safeTitle.textContent = 'No Content Security Issues Detected'; | |
safeTitle.style.fontSize = '1.2rem'; | |
safeTitle.style.fontWeight = '600'; | |
safeTitle.style.marginBottom = '10px'; | |
const safeDescription = document.createElement('p'); | |
safeDescription.textContent = 'The website\'s HTML content appears to be safe with no suspicious elements found.'; | |
safeDescription.style.fontSize = '0.95rem'; | |
safeDescription.style.maxWidth = '380px'; | |
safeDescription.style.lineHeight = '1.6'; | |
safeDescription.style.opacity = '0.9'; | |
safeContainer.appendChild(checkIcon); | |
safeContainer.appendChild(safeTitle); | |
safeContainer.appendChild(safeDescription); | |
securityRisksContainer.appendChild(safeContainer); | |
} else { | |
// Create a styled container for risk factors | |
const riskFactorsListContainer = document.createElement('div'); | |
riskFactorsListContainer.className = 'risk-factors-list'; | |
riskFactorsListContainer.style.animation = 'fadeIn 0.6s ease-out'; | |
riskFactors.forEach((risk, index) => { | |
const riskItem = document.createElement('div'); | |
riskItem.className = 'risk-item'; | |
riskItem.style.backgroundColor = 'rgba(30, 41, 59, 0.4)'; | |
riskItem.style.borderRadius = '10px'; | |
riskItem.style.padding = '16px'; | |
riskItem.style.marginBottom = '12px'; | |
riskItem.style.display = 'flex'; | |
riskItem.style.alignItems = 'flex-start'; | |
riskItem.style.gap = '14px'; | |
riskItem.style.transition = 'transform 0.2s ease'; | |
riskItem.style.cursor = 'default'; | |
riskItem.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.05)'; | |
riskItem.style.animation = `fadeIn 0.${6 + index}s ease-out`; | |
// Add hover effect | |
riskItem.onmouseover = function() { | |
this.style.transform = 'translateY(-2px)'; | |
this.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.08)'; | |
}; | |
riskItem.onmouseout = function() { | |
this.style.transform = 'translateY(0)'; | |
this.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.05)'; | |
}; | |
// Determine severity based on content | |
const isHighSeverity = risk.toLowerCase().includes("password") || | |
risk.toLowerCase().includes("form") || | |
risk.toLowerCase().includes("insecure") || | |
index === 0; | |
const isMediumSeverity = risk.toLowerCase().includes("script") || | |
risk.toLowerCase().includes("obfuscated") || | |
risk.toLowerCase().includes("iframe") || | |
risk.toLowerCase().includes("external"); | |
let severityColor, severityIconClass, severityIconBg; | |
if (isHighSeverity) { | |
severityColor = '#ef4444'; | |
severityIconClass = 'fa-exclamation-circle'; | |
severityIconBg = 'rgba(239, 68, 68, 0.2)'; | |
riskItem.style.borderLeft = '4px solid #ef4444'; | |
} else if (isMediumSeverity) { | |
severityColor = '#f59e0b'; | |
severityIconClass = 'fa-exclamation'; | |
severityIconBg = 'rgba(245, 158, 11, 0.2)'; | |
riskItem.style.borderLeft = '4px solid #f59e0b'; | |
} else { | |
severityColor = '#3b82f6'; | |
severityIconClass = 'fa-info-circle'; | |
severityIconBg = 'rgba(59, 130, 246, 0.2)'; | |
riskItem.style.borderLeft = '4px solid #3b82f6'; | |
} | |
// Create icon container | |
const iconContainer = document.createElement('div'); | |
iconContainer.className = 'risk-icon'; | |
iconContainer.style.width = '36px'; | |
iconContainer.style.height = '36px'; | |
iconContainer.style.borderRadius = '50%'; | |
iconContainer.style.backgroundColor = severityIconBg; | |
iconContainer.style.display = 'flex'; | |
iconContainer.style.alignItems = 'center'; | |
iconContainer.style.justifyContent = 'center'; | |
iconContainer.style.flexShrink = '0'; | |
iconContainer.innerHTML = `<i class="fas ${severityIconClass}" style="color: ${severityColor}; font-size: 16px;"></i>`; | |
// Create content container | |
const contentContainer = document.createElement('div'); | |
contentContainer.className = 'risk-content'; | |
contentContainer.style.flexGrow = '1'; | |
// Create risk type label | |
const riskTypeLabel = document.createElement('div'); | |
riskTypeLabel.className = 'risk-type'; | |
riskTypeLabel.textContent = isHighSeverity ? 'High Risk Factor' : isMediumSeverity ? 'Medium Risk Factor' : 'Low Risk Factor'; | |
riskTypeLabel.style.fontSize = '0.8rem'; | |
riskTypeLabel.style.color = severityColor; | |
riskTypeLabel.style.fontWeight = '600'; | |
riskTypeLabel.style.marginBottom = '6px'; | |
riskTypeLabel.style.textTransform = 'uppercase'; | |
riskTypeLabel.style.letterSpacing = '0.03em'; | |
// Create risk description | |
const riskDescription = document.createElement('div'); | |
riskDescription.className = 'risk-description'; | |
riskDescription.textContent = risk; | |
riskDescription.style.fontSize = '0.95rem'; | |
riskDescription.style.lineHeight = '1.5'; | |
// Assemble the components | |
contentContainer.appendChild(riskTypeLabel); | |
contentContainer.appendChild(riskDescription); | |
riskItem.appendChild(iconContainer); | |
riskItem.appendChild(contentContainer); | |
riskFactorsListContainer.appendChild(riskItem); | |
}); | |
securityRisksContainer.appendChild(riskFactorsListContainer); | |
} | |
// Add a style tag for animations | |
const styleTag = document.createElement('style'); | |
styleTag.textContent = ` | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(8px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
@keyframes slideIn { | |
from { opacity: 0; transform: translateX(-10px); } | |
to { opacity: 1; transform: translateX(0); } | |
} | |
`; | |
document.head.appendChild(styleTag); | |
// Display security checks with improved styles | |
const securityChecks = data.html_security.security_checks || []; | |
// Create a container for security checks | |
const checksContainer = document.createElement('div'); | |
checksContainer.className = 'security-checks-container'; | |
checksContainer.style.display = 'flex'; | |
checksContainer.style.flexDirection = 'column'; | |
checksContainer.style.gap = '10px'; | |
checksContainer.style.marginTop = '10px'; | |
if (securityChecks.length > 0) { | |
securityChecks.forEach((check, index) => { | |
const checkItem = document.createElement('div'); | |
checkItem.className = 'security-check-item'; | |
checkItem.style.display = 'flex'; | |
checkItem.style.alignItems = 'center'; | |
checkItem.style.gap = '12px'; | |
checkItem.style.padding = '10px 12px'; | |
checkItem.style.backgroundColor = 'rgba(16, 185, 129, 0.1)'; | |
checkItem.style.borderRadius = '8px'; | |
checkItem.style.animation = `slideIn 0.${4 + index}s ease-out`; | |
const checkIcon = document.createElement('div'); | |
checkIcon.className = 'check-icon'; | |
checkIcon.style.width = '28px'; | |
checkIcon.style.height = '28px'; | |
checkIcon.style.borderRadius = '50%'; | |
checkIcon.style.backgroundColor = 'rgba(16, 185, 129, 0.2)'; | |
checkIcon.style.display = 'flex'; | |
checkIcon.style.alignItems = 'center'; | |
checkIcon.style.justifyContent = 'center'; | |
checkIcon.style.flexShrink = '0'; | |
checkIcon.innerHTML = '<i class="fas fa-check" style="color: #10b981; font-size: 14px;"></i>'; | |
const checkText = document.createElement('div'); | |
checkText.className = 'check-text'; | |
checkText.textContent = check; | |
checkText.style.fontSize = '0.9rem'; | |
checkText.style.lineHeight = '1.4'; | |
checkItem.appendChild(checkIcon); | |
checkItem.appendChild(checkText); | |
checksContainer.appendChild(checkItem); | |
}); | |
} else { | |
const noChecksItem = document.createElement('div'); | |
noChecksItem.className = 'no-checks-item'; | |
noChecksItem.style.display = 'flex'; | |
noChecksItem.style.alignItems = 'center'; | |
noChecksItem.style.gap = '12px'; | |
noChecksItem.style.padding = '12px 14px'; | |
noChecksItem.style.backgroundColor = 'rgba(71, 85, 105, 0.2)'; | |
noChecksItem.style.borderRadius = '8px'; | |
noChecksItem.style.animation = 'slideIn 0.4s ease-out'; | |
const infoIcon = document.createElement('div'); | |
infoIcon.className = 'info-icon'; | |
infoIcon.style.width = '28px'; | |
infoIcon.style.height = '28px'; | |
infoIcon.style.borderRadius = '50%'; | |
infoIcon.style.backgroundColor = 'rgba(71, 85, 105, 0.2)'; | |
infoIcon.style.display = 'flex'; | |
infoIcon.style.alignItems = 'center'; | |
infoIcon.style.justifyContent = 'center'; | |
infoIcon.style.flexShrink = '0'; | |
infoIcon.innerHTML = '<i class="fas fa-info" style="color: #94a3b8; font-size: 14px;"></i>'; | |
const infoText = document.createElement('div'); | |
infoText.className = 'info-text'; | |
infoText.textContent = 'No specific security checks passed'; | |
infoText.style.fontSize = '0.9rem'; | |
infoText.style.color = '#cbd5e1'; | |
noChecksItem.appendChild(infoIcon); | |
noChecksItem.appendChild(infoText); | |
checksContainer.appendChild(noChecksItem); | |
} | |
securityChecksContainer.appendChild(checksContainer); | |
} | |
/** | |
* Updates the score circle visualization for HTML security analysis | |
* @param {number} score - The content risk score | |
*/ | |
function updateScoreCircle(score) { | |
const circle = document.getElementById('htmlSecurityScoreCircle'); | |
if (!circle) return; | |
// Ensure the progress value is properly set | |
const progressValue = score + '%'; | |
// Define risk level | |
if (score < 30) { | |
circle.className = 'score-circle low-risk'; | |
circle.style.setProperty('--progress', progressValue); | |
circle.style.setProperty('--color', 'var(--success-color)'); | |
} else if (score < 70) { | |
circle.className = 'score-circle medium-risk'; | |
circle.style.setProperty('--progress', progressValue); | |
circle.style.setProperty('--color', 'var(--warning-color)'); | |
} else { | |
circle.className = 'score-circle high-risk'; | |
circle.style.setProperty('--progress', progressValue); | |
circle.style.setProperty('--color', 'var(--danger-color)'); | |
} | |
// Force reflow to ensure the animation takes effect | |
void circle.offsetWidth; | |
// Make sure the score text is set and visible | |
const scoreText = document.getElementById('htmlSecurityScore'); | |
if (scoreText) { | |
scoreText.textContent = score; | |
scoreText.style.visibility = 'visible'; | |
} | |
} | |
/** | |
* Displays all results from the analysis | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displayAllResults(data) { | |
console.log("Received data for display:", data); | |
// Validate data before attempting to display | |
if (!data || typeof data !== 'object') { | |
console.error("Invalid data object received:", data); | |
throw new Error("Invalid or missing data object"); | |
} | |
// Show the results section | |
const resultsSection = document.getElementById('results'); | |
if (resultsSection) { | |
resultsSection.style.display = 'block'; | |
} else { | |
console.error("Results section not found with ID 'results'"); | |
} | |
// Ensure we have the necessary data structures to prevent errors | |
data.domain_info = data.domain_info || {}; | |
data.suspicious_patterns = data.suspicious_patterns || []; | |
data.html_security = data.html_security || {}; | |
data.section_totals = data.section_totals || {}; | |
data.feature_table = data.feature_table || []; | |
data.feature_contributions = data.feature_contributions || []; | |
// Check for unresolvable IP before displaying results | |
const domainInfo = data.domain_info; | |
const ipNotResolved = !domainInfo.ip_address || | |
domainInfo.ip_address === 'Unknown' || | |
domainInfo.ip_address === 'Could not resolve'; | |
if (ipNotResolved) { | |
try { | |
increaseIPResolutionRisk(data); | |
} catch (error) { | |
console.error("Error adjusting IP resolution risk:", error); | |
} | |
} | |
// Calculate section scores and update the overall score first | |
try { | |
displaySectionRiskMeters(data); | |
} catch (error) { | |
console.error("Error displaying section risk meters:", error); | |
} | |
// Display all components with error handling for each section | |
try { | |
displayRiskFactors(data); | |
} catch (error) { | |
console.error("Error displaying risk factors:", error); | |
} | |
try { | |
displayDomainInfo(data); | |
} catch (error) { | |
console.error("Error displaying domain info:", error); | |
} | |
try { | |
displaySuspiciousPatterns(data); | |
} catch (error) { | |
console.error("Error displaying suspicious patterns:", error); | |
} | |
try { | |
displayFeatureDetails(data); | |
} catch (error) { | |
console.error("Error displaying feature details:", error); | |
} | |
try { | |
displayHtmlSecurity(data); | |
} catch (error) { | |
console.error("Error displaying HTML security:", error); | |
} | |
// Update the result URL | |
const resultUrlElement = document.getElementById('resultUrl'); | |
if (resultUrlElement && data.url) { | |
resultUrlElement.textContent = data.url; | |
} | |
// Scroll to results | |
if (resultsSection) { | |
resultsSection.scrollIntoView({ behavior: 'smooth' }); | |
} | |
} | |
/** | |
* Displays section risk meters based on section totals | |
* @param {Object} data - The analysis data from the API | |
*/ | |
function displaySectionRiskMeters(data) { | |
// Get section totals from the data | |
const sectionTotals = data.section_totals || {}; | |
// Get section scores | |
const keyRiskScore = sectionTotals["Key Risk Factors (AI Predict)"] || sectionTotals["Key Risk Factors"] || 0; | |
const domainInfoScore = sectionTotals["Domain Information"] || 0; | |
// Check if there are any suspicious patterns | |
const hasPatterns = data.suspicious_patterns && data.suspicious_patterns.length > 0; | |
// Force suspicious patterns score to 0 if no patterns were detected | |
const suspiciousPatternsScore = hasPatterns ? | |
(sectionTotals["Suspicious Patterns"] || 0) : 0; | |
// Calculate the total risk from all sections | |
const totalRisk = keyRiskScore + domainInfoScore + suspiciousPatternsScore; | |
// Update the overall fraud risk score to be the sum of the three sections | |
data.score = totalRisk; | |
console.log("Section scores:", { | |
keyRiskScore, | |
domainInfoScore, | |
suspiciousPatternsScore, | |
hasPatterns, | |
totalRisk | |
}); | |
// Display each section's risk contribution | |
updateSectionRiskMeter('keyRiskFactors', keyRiskScore, totalRisk); | |
updateSectionRiskMeter('domainInfo', domainInfoScore, totalRisk); | |
updateSectionRiskMeter('suspiciousPatterns', suspiciousPatternsScore, totalRisk); | |
// Update the overall score display with the new calculated total | |
const riskScoreElement = document.getElementById('riskScore'); | |
if (riskScoreElement) { | |
riskScoreElement.textContent = Math.round(totalRisk) + '%'; | |
// Update risk level | |
const riskTagElement = document.getElementById('riskTag'); | |
let riskClass, riskText; | |
if (totalRisk < 30) { | |
riskClass = 'risk-low'; | |
riskText = 'Low Risk'; | |
} else if (totalRisk < 60) { | |
riskClass = 'risk-medium'; | |
riskText = 'Medium Risk'; | |
} else { | |
riskClass = 'risk-high'; | |
riskText = 'High Risk'; | |
} | |
if (riskTagElement) { | |
riskTagElement.textContent = riskText; | |
riskTagElement.className = `risk-tag ${riskClass}`; | |
} | |
// Update score display container | |
const scoreDisplayContainer = document.getElementById('scoreDisplayContainer'); | |
if (scoreDisplayContainer) { | |
scoreDisplayContainer.className = `score-display ${riskClass}`; | |
} | |
// Update meter | |
const scoreMeter = document.getElementById('scoreMeter'); | |
const meterLabel = document.getElementById('meterLabel'); | |
if (scoreMeter) { | |
scoreMeter.style.width = `${totalRisk}%`; | |
scoreMeter.className = `meter-fill ${totalRisk < 30 ? 'low' : totalRisk < 60 ? 'medium' : 'high'}`; | |
} | |
if (meterLabel) { | |
meterLabel.textContent = `${Math.round(totalRisk)}%`; | |
} | |
} | |
// Add an explanation about the risk score breakdown | |
const resultsSummary = document.querySelector('.results-summary'); | |
if (resultsSummary) { | |
// Remove any existing risk breakdown explanation | |
const existingExplanation = resultsSummary.querySelector('.risk-breakdown-explanation'); | |
if (existingExplanation) { | |
resultsSummary.removeChild(existingExplanation); | |
} | |
const breakdownExplanation = document.createElement('div'); | |
breakdownExplanation.className = 'risk-breakdown-explanation'; | |
breakdownExplanation.style.fontSize = '0.9rem'; | |
breakdownExplanation.style.backgroundColor = 'rgba(15, 23, 42, 0.3)'; | |
breakdownExplanation.style.borderRadius = '8px'; | |
breakdownExplanation.style.padding = '12px 16px'; | |
breakdownExplanation.style.marginTop = '20px'; | |
breakdownExplanation.style.color = '#cbd5e1'; | |
breakdownExplanation.style.lineHeight = '1.6'; | |
// Create table-like display for the calculation | |
breakdownExplanation.innerHTML = ` | |
<div style="display: flex; align-items: center; margin-bottom: 10px;"> | |
<i class="fas fa-calculator" style="color: #60a5fa; margin-right: 8px; font-size: 14px;"></i> | |
<strong style="color: #f8fafc; font-size: 0.95rem;">Fraud Risk Score Calculation</strong> | |
</div> | |
<div style="display: flex; flex-direction: column; gap: 4px;"> | |
<div style="display: flex; justify-content: space-between;"> | |
<span style="color: #60a5fa;">URL Features (Key Risk Factors):</span> | |
<span style="font-weight: 500;">${keyRiskScore.toFixed(1)}%</span> | |
</div> | |
<div style="display: flex; justify-content: space-between;"> | |
<span style="color: #10b981;">Domain Information:</span> | |
<span style="font-weight: 500;">${domainInfoScore.toFixed(1)}%</span> | |
</div> | |
<div style="display: flex; justify-content: space-between;"> | |
<span style="color: #f59e0b;">Suspicious Patterns:</span> | |
<span style="font-weight: 500;">${suspiciousPatternsScore.toFixed(1)}%</span> | |
</div> | |
</div> | |
`; | |
// Add a divider and total | |
const totalSection = document.createElement('div'); | |
totalSection.style.borderTop = '1px solid rgba(100, 116, 139, 0.3)'; | |
totalSection.style.marginTop = '10px'; | |
totalSection.style.paddingTop = '10px'; | |
totalSection.style.display = 'flex'; | |
totalSection.style.justifyContent = 'space-between'; | |
totalSection.style.fontWeight = '600'; | |
totalSection.innerHTML = ` | |
<span>Total Fraud Risk Score:</span> | |
<span style="color: #f8fafc;">${totalRisk.toFixed(1)}%</span> | |
`; | |
breakdownExplanation.appendChild(totalSection); | |
// Find any existing explanation and replace it | |
resultsSummary.appendChild(breakdownExplanation); | |
} | |
} | |
/** | |
* Updates a section risk meter with the provided score | |
* @param {string} sectionId - Base ID of the section | |
* @param {number} sectionScore - Score for the section | |
* @param {number} totalScore - Total risk score | |
*/ | |
function updateSectionRiskMeter(sectionId, sectionScore, totalScore) { | |
const scoreElement = document.getElementById(`${sectionId}Score`); | |
const meterElement = document.getElementById(`${sectionId}Meter`); | |
if (scoreElement && meterElement) { | |
// Round to one decimal place | |
const roundedScore = Math.round(sectionScore * 10) / 10; | |
// Update the score text | |
scoreElement.textContent = `${roundedScore}%`; | |
// Update the meter width | |
meterElement.style.width = `${roundedScore}%`; | |
// Determine and set color based on contribution to total risk | |
let colorClass = 'low'; | |
// Calculate relative contribution | |
const contribution = totalScore > 0 ? (sectionScore / totalScore) * 100 : 0; | |
if (contribution >= 50) { | |
colorClass = 'high'; | |
} else if (contribution >= 25) { | |
colorClass = 'medium'; | |
} | |
// Set color class | |
meterElement.className = `section-meter-fill ${colorClass}`; | |
} | |
} | |
// Helper functions | |
function formatFeatureName(name) { | |
if (!name) return 'Unknown Feature'; | |
// Special case for Https Present | |
if (name === 'https_present' || name === 'Https Present') { | |
return 'Security Weights'; | |
} | |
return name | |
.replace(/_/g, ' ') | |
.replace(/\b\w/g, l => l.toUpperCase()); | |
} | |
function getSeverityClass(severity) { | |
const severityLower = String(severity).toLowerCase(); | |
if (severityLower === 'high') return 'high-severity'; | |
if (severityLower === 'medium') return 'medium-severity'; | |
return 'low-severity'; | |
} | |
function capitalizeFirstLetter(string) { | |
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); | |
} | |
/** | |
* Gets risk level text based on a numeric score | |
* @param {number} score - Risk score (0-100) | |
* @returns {string} - Risk level text (low, medium, high) | |
*/ | |
function get_risk_level(score) { | |
const numericScore = parseFloat(score); | |
if (isNaN(numericScore)) return "unknown"; | |
if (numericScore < 30) { | |
return "low"; | |
} else if (numericScore < 60) { | |
return "medium"; | |
} else { | |
return "high"; | |
} | |
} | |
function extractDomainFromData(data) { | |
// Try to get domain from various possible locations | |
if (data.domain) return data.domain; | |
if (data.url) { | |
try { | |
const url = new URL(data.url); | |
return url.hostname; | |
} catch (e) { | |
return data.url; | |
} | |
} | |
if (data.domain_info && data.domain_info.name) return data.domain_info.name; | |
return 'Unknown Domain'; | |
} | |
function extractProtocolFromData(data) { | |
// Try to get protocol from URL | |
if (data.url) { | |
try { | |
const url = new URL(data.url); | |
return url.protocol.replace(':', ''); | |
} catch (e) { | |
return data.url.startsWith('https') ? 'https' : 'http'; | |
} | |
} | |
// Default to unknown | |
return 'Unknown'; | |
} | |
function updateScoreDisplay(score) { | |
const scoreElement = document.getElementById('risk-score'); | |
if (!scoreElement) { | |
console.error("Risk score element not found!"); | |
return; | |
} | |
// Ensure score is a number and within valid range | |
const numericScore = parseFloat(score); | |
if (isNaN(numericScore)) { | |
scoreElement.textContent = 'N/A'; | |
return; | |
} | |
// Normalize to 0-100 if it's a decimal | |
const normalizedScore = numericScore <= 1 | |
? Math.round(numericScore * 100) | |
: Math.round(numericScore); | |
// Update score text | |
scoreElement.textContent = normalizedScore + '%'; | |
// Update color and risk level | |
let riskClass, riskText; | |
if (normalizedScore < 30) { | |
riskClass = 'risk-low'; | |
riskText = 'Low Risk'; | |
} else if (normalizedScore < 60) { | |
riskClass = 'risk-medium'; | |
riskText = 'Medium Risk'; | |
} else { | |
riskClass = 'risk-high'; | |
riskText = 'High Risk'; | |
} | |
// Update score element class | |
scoreElement.className = `risk-score ${riskClass}`; | |
// Update risk level text | |
const riskLevelElement = document.getElementById('risk-level'); | |
if (riskLevelElement) { | |
riskLevelElement.textContent = riskText; | |
riskLevelElement.className = `risk-label ${riskClass}`; | |
} | |
// Update score display container | |
const scoreDisplayElement = document.getElementById('score-display'); | |
if (scoreDisplayElement) { | |
scoreDisplayElement.className = `score-display ${riskClass}`; | |
} | |
// Update progress bar if it exists | |
const progressBarElement = document.getElementById('score-progress-bar'); | |
if (progressBarElement) { | |
progressBarElement.style.width = `${normalizedScore}%`; | |
// Update color class | |
if (normalizedScore < 30) { | |
progressBarElement.style.background = 'var(--success-color)'; | |
} else if (normalizedScore < 60) { | |
progressBarElement.style.background = 'linear-gradient(90deg, var(--warning-color), #f97316)'; | |
} else { | |
progressBarElement.style.background = 'var(--danger-color)'; | |
} | |
} | |
// Update progress label | |
const progressLabelElement = document.getElementById('score-progress-label'); | |
if (progressLabelElement) { | |
progressLabelElement.textContent = `${normalizedScore}%`; | |
} | |
} | |
/** | |
* Creates a severity badge for suspicious patterns | |
* @param {number} count - Number of patterns with this severity | |
* @param {string} severity - Severity level ('high', 'medium', 'low') | |
* @param {string} label - Display label | |
* @returns {HTMLElement} - Badge element | |
*/ | |
function createSeverityBadge(count, severity, label) { | |
const badge = document.createElement('div'); | |
badge.className = `severity-badge ${severity}`; | |
badge.style.padding = '5px 10px'; | |
badge.style.borderRadius = '12px'; | |
badge.style.marginRight = '8px'; | |
badge.style.display = 'flex'; | |
badge.style.alignItems = 'center'; | |
badge.style.justifyContent = 'center'; | |
badge.style.fontSize = '0.85rem'; | |
badge.style.fontWeight = 'bold'; | |
const colors = { | |
high: { | |
bg: 'rgba(239, 68, 68, 0.15)', | |
text: '#f87171', | |
icon: 'fa-exclamation-circle' | |
}, | |
medium: { | |
bg: 'rgba(245, 158, 11, 0.15)', | |
text: '#fbbf24', | |
icon: 'fa-exclamation-triangle' | |
}, | |
low: { | |
bg: 'rgba(16, 185, 129, 0.15)', | |
text: '#34d399', | |
icon: 'fa-info-circle' | |
} | |
}; | |
badge.style.backgroundColor = colors[severity].bg; | |
badge.style.color = colors[severity].text; | |
// Add icon instead of any text that could be interpreted as an X | |
const icon = document.createElement('i'); | |
icon.className = `fas ${colors[severity].icon}`; | |
icon.style.marginRight = '5px'; | |
icon.style.fontSize = '0.9rem'; | |
const countSpan = document.createElement('span'); | |
countSpan.textContent = `${count} ${label}`; | |
badge.appendChild(icon); | |
badge.appendChild(countSpan); | |
return badge; | |
} | |
// Public API | |
return { | |
displayAllResults, | |
displayRiskFactors, | |
displayDomainInfo, | |
displaySuspiciousPatterns, | |
displayFeatureDetails, | |
displayHtmlSecurity, | |
increaseIPResolutionRisk | |
}; | |
})(); | |
// Make display functions globally available | |
window.displayResults = displayResults; |