Spaces:
Runtime error
Runtime error
| /* ================================================================ | |
| Phishing Detection – UI Controller (Unified All-Models) | |
| ================================================================ */ | |
| const API_BASE = window.location.origin; | |
| // ── DOM Refs ──────────────────────────────────────────────────── | |
| const $url = () => document.getElementById('urlInput'); | |
| const $loading = () => document.getElementById('loading'); | |
| const $results = () => document.getElementById('results'); | |
| // ── Feature-key catalogues ────────────────────────────────────── | |
| const TOP_URL_FEATURES = [ | |
| 'num_domain_parts', 'domain_dots', 'is_shortened', 'num_subdomains', | |
| 'domain_hyphens', 'is_free_platform', 'platform_subdomain_length', | |
| 'avg_domain_part_len', 'domain_length_category', 'path_digits', 'is_http', | |
| 'multiple_brands_in_url', 'brand_in_path', 'path_slashes', 'encoding_diff', | |
| 'symbol_ratio_domain', 'domain_length', 'has_at_symbol', 'tld_length', | |
| 'is_free_hosting', | |
| ]; | |
| const ALL_URL_FEATURES = [ | |
| 'url_length', 'domain_length', 'path_length', 'query_length', 'url_length_category', | |
| 'domain_length_category', 'num_dots', 'num_hyphens', 'num_underscores', 'num_slashes', | |
| 'num_question_marks', 'num_ampersands', 'num_equals', 'num_at', 'num_percent', | |
| 'num_digits_url', 'num_letters_url', 'domain_dots', 'domain_hyphens', 'domain_digits', | |
| 'path_slashes', 'path_dots', 'path_digits', 'digit_ratio_url', 'letter_ratio_url', | |
| 'special_char_ratio', 'digit_ratio_domain', 'symbol_ratio_domain', 'num_subdomains', | |
| 'num_domain_parts', 'tld_length', 'sld_length', 'longest_domain_part', 'avg_domain_part_len', | |
| 'longest_part_gt_20', 'longest_part_gt_30', 'longest_part_gt_40', 'has_suspicious_tld', | |
| 'has_trusted_tld', 'has_port', 'has_non_std_port', 'domain_randomness_score', | |
| 'sld_consonant_cluster_score', 'sld_keyboard_pattern', 'sld_has_dictionary_word', | |
| 'sld_pronounceability_score', 'domain_digit_position_suspicious', 'path_depth', | |
| 'max_path_segment_len', 'avg_path_segment_len', 'has_extension', 'extension_category', | |
| 'has_suspicious_extension', 'has_exe', 'has_double_slash', 'path_has_brand_not_domain', | |
| 'path_has_ip_pattern', 'suspicious_path_extension_combo', 'num_params', 'has_query', | |
| 'query_value_length', 'max_param_len', 'query_has_url', 'url_entropy', 'domain_entropy', | |
| 'path_entropy', 'max_consecutive_digits', 'max_consecutive_chars', 'max_consecutive_consonants', | |
| 'char_repeat_rate', 'unique_bigram_ratio', 'unique_trigram_ratio', 'sld_letter_diversity', | |
| 'domain_has_numbers_letters', 'url_complexity_score', 'has_ip_address', 'has_at_symbol', | |
| 'has_redirect', 'is_shortened', 'is_free_hosting', 'is_free_platform', | |
| 'platform_subdomain_length', 'has_uuid_subdomain', 'is_http', | |
| 'num_phishing_keywords', 'phishing_in_domain', 'phishing_in_path', 'num_brands', | |
| 'brand_in_domain', 'brand_in_path', 'brand_impersonation', 'has_login', 'has_account', | |
| 'has_verify', 'has_secure', 'has_update', 'has_bank', 'has_password', 'has_suspend', | |
| 'has_webscr', 'has_cmd', 'has_cgi', 'brand_in_subdomain_not_domain', 'multiple_brands_in_url', | |
| 'brand_with_hyphen', 'suspicious_brand_tld', 'brand_keyword_combo', 'has_url_encoding', | |
| 'encoding_count', 'encoding_diff', 'has_punycode', 'has_unicode', 'has_hex_string', | |
| 'has_base64', 'has_lookalike_chars', 'mixed_script_score', 'homograph_brand_risk', | |
| 'suspected_idn_homograph', 'double_encoding', 'encoding_in_domain', 'suspicious_unicode_category', | |
| ]; | |
| const TOP_HTML_FEATURES = [ | |
| 'has_login_form', 'num_password_fields', 'password_with_external_action', | |
| 'num_external_form_actions', 'num_empty_form_actions', 'num_hidden_fields', | |
| 'ratio_external_links', 'num_external_links', 'num_ip_based_links', | |
| 'num_suspicious_tld_links', 'has_eval', 'has_base64', 'has_atob', | |
| 'has_fromcharcode', 'has_document_write', 'has_right_click_disabled', | |
| 'has_status_bar_customization', 'has_meta_refresh', 'has_location_replace', | |
| 'num_hidden_iframes', | |
| ]; | |
| const ALL_HTML_FEATURES = [ | |
| 'html_length', 'num_tags', 'num_divs', 'num_spans', 'num_paragraphs', | |
| 'num_headings', 'num_lists', 'num_images', 'num_iframes', 'num_tables', | |
| 'has_title', 'dom_depth', | |
| 'num_forms', 'num_input_fields', 'num_password_fields', 'num_email_fields', | |
| 'num_text_fields', 'num_submit_buttons', 'num_hidden_fields', 'has_login_form', | |
| 'has_form', 'num_external_form_actions', 'num_empty_form_actions', | |
| 'num_links', 'num_external_links', 'num_internal_links', 'num_empty_links', | |
| 'num_mailto_links', 'num_javascript_links', 'ratio_external_links', | |
| 'num_ip_based_links', 'num_suspicious_tld_links', 'num_anchor_text_mismatch', | |
| 'num_scripts', 'num_inline_scripts', 'num_external_scripts', | |
| 'has_eval', 'has_unescape', 'has_escape', 'has_document_write', | |
| 'text_length', 'num_words', 'text_to_html_ratio', 'num_brand_mentions', | |
| 'num_urgency_keywords', 'has_copyright', 'has_phone_number', 'has_email_address', | |
| 'num_meta_tags', 'has_description', 'has_keywords', 'has_author', | |
| 'has_viewport', 'has_meta_refresh', | |
| 'num_css_files', 'num_external_css', 'num_external_images', | |
| 'num_data_uri_images', 'num_inline_styles', 'inline_css_length', 'has_favicon', | |
| 'password_with_external_action', 'has_base64', 'has_atob', 'has_fromcharcode', | |
| 'num_onload_events', 'num_onerror_events', 'num_onclick_events', | |
| 'num_unique_external_domains', 'num_forms_without_labels', | |
| 'has_display_none', 'has_visibility_hidden', 'has_window_open', | |
| 'has_location_replace', 'num_hidden_iframes', 'has_right_click_disabled', | |
| 'has_status_bar_customization', | |
| ]; | |
| // ── Highlight rules ───────────────────────────────────────────── | |
| const GOOD_INDICATORS = new Set([ | |
| 'has_trusted_tld', 'has_title', 'has_favicon', 'sld_has_dictionary_word', | |
| ]); | |
| const BAD_INDICATORS = new Set([ | |
| 'is_shortened', 'is_free_hosting', 'is_free_platform', | |
| 'has_ip_address', 'has_at_symbol', 'has_suspicious_tld', | |
| 'has_meta_refresh', 'has_popup_window', 'form_action_external', | |
| 'has_base64', 'brand_impersonation', 'has_punycode', | |
| 'has_unicode', 'has_hex_string', 'suspected_idn_homograph', | |
| 'is_http', 'multiple_brands_in_url', 'brand_in_path', | |
| ]); | |
| const DANGER_THRESHOLDS = { | |
| num_password_fields: [0, '>'], | |
| num_hidden_fields: [2, '>'], | |
| num_urgency_keywords: [0, '>'], | |
| num_phishing_keywords: [0, '>'], | |
| num_external_scripts: [10, '>'], | |
| platform_subdomain_length: [5, '>'], | |
| domain_dots: [3, '>'], | |
| num_subdomains: [3, '>'], | |
| domain_entropy: [4.5, '>'], | |
| symbol_ratio_domain: [0.3, '>'], | |
| max_consecutive_digits: [5, '>'], | |
| domain_hyphens: [1, '>'], | |
| path_digits: [5, '>'], | |
| encoding_diff: [0.5, '>'], | |
| }; | |
| const SAFE_THRESHOLDS = { | |
| domain_length: [15, '<'], | |
| domain_entropy: [3.5, '<'], | |
| num_brands: [1, '=='], | |
| num_domain_parts: [2, '=='], | |
| }; | |
| // ── API helpers ───────────────────────────────────────────────── | |
| function normalizeUrl(raw) { | |
| const trimmed = raw.trim(); | |
| if (!trimmed) return null; | |
| return trimmed.startsWith('http://') || trimmed.startsWith('https://') | |
| ? trimmed | |
| : 'https://' + trimmed; | |
| } | |
| async function fetchPrediction(endpoint, body) { | |
| const url = normalizeUrl($url().value); | |
| if (!url) { alert('Please enter a URL'); return; } | |
| showLoading(); | |
| try { | |
| const res = await fetch(`${API_BASE}${endpoint}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body(url)), | |
| }); | |
| if (!res.ok) throw new Error('Analysis failed'); | |
| return await res.json(); | |
| } catch (err) { | |
| alert('Error: ' + err.message); | |
| hideLoading(); | |
| return null; | |
| } | |
| } | |
| // ── Public actions ────────────────────────────────────────────── | |
| async function analyzeAll() { | |
| const data = await fetchPrediction('/api/predict/all', url => ({ url })); | |
| if (data) displayAllResults(data); | |
| } | |
| function clearResults() { | |
| const results = $results(); | |
| const input = $url(); | |
| if (results) results.style.display = 'none'; | |
| if (input) input.value = ''; | |
| } | |
| // ── Ensemble weights (F1-score based) ─────────────────────────── | |
| const MODEL_WEIGHTS = { | |
| 'Logistic Regression': 0.9359, | |
| 'Random Forest': 0.9768, | |
| 'XGBoost': 0.9805, | |
| 'Random Forest HTML': 0.8811, | |
| 'XGBoost HTML': 0.8809, | |
| 'Random Forest Combined': 0.9859, | |
| 'XGBoost Combined': 0.9901, | |
| 'CNN URL (Char-level)': 0.9837, | |
| 'CNN HTML (Char-level)': 0.9626, | |
| }; | |
| function computeEnsembleVerdict(data) { | |
| const allPredictions = []; | |
| const categories = ['url_models', 'html_models', 'combined_models', 'cnn_models']; | |
| categories.forEach(cat => { | |
| const section = data[cat]; | |
| if (section && section.predictions) { | |
| allPredictions.push(...section.predictions); | |
| } | |
| }); | |
| if (allPredictions.length === 0) { | |
| return { score: 0, isPhishing: false, totalModels: 0, phishingVotes: 0 }; | |
| } | |
| let weightedSum = 0; | |
| let totalWeight = 0; | |
| let phishingVotes = 0; | |
| allPredictions.forEach(p => { | |
| const w = MODEL_WEIGHTS[p.model_name] || 0.90; | |
| const phishProb = p.phishing_probability / 100; | |
| weightedSum += w * phishProb; | |
| totalWeight += w; | |
| if (p.prediction === 'PHISHING') phishingVotes++; | |
| }); | |
| const score = totalWeight > 0 ? (weightedSum / totalWeight) * 100 : 0; | |
| return { | |
| score: Math.round(score * 10) / 10, | |
| isPhishing: score >= 50, | |
| totalModels: allPredictions.length, | |
| phishingVotes, | |
| }; | |
| } | |
| // ── Loading UI ────────────────────────────────────────────────── | |
| function showLoading() { | |
| $loading().style.display = 'block'; | |
| $results().style.display = 'none'; | |
| } | |
| function hideLoading() { | |
| $loading().style.display = 'none'; | |
| } | |
| // UNIFIED RESULTS | |
| function displayAllResults(data) { | |
| hideLoading(); | |
| const el = $results(); | |
| el.style.display = 'block'; | |
| // Weighted ensemble verdict | |
| const verdict = computeEnsembleVerdict(data); | |
| const statusClass = verdict.isPhishing ? 'danger' : 'safe'; | |
| const statusText = verdict.isPhishing ? 'Phishing' : 'Legitimate'; | |
| const safeVotes = verdict.totalModels - verdict.phishingVotes; | |
| const banner = ` | |
| <div class="status-banner ${statusClass}"> | |
| <div class="status-headline"> | |
| <div> | |
| <div class="status-title">${statusText}</div> | |
| </div> | |
| </div> | |
| <div class="ensemble-score"> | |
| <div class="banner-score-value">${verdict.score.toFixed(1)}%</div> | |
| <div class="banner-score-label">Phishing risk</div> | |
| </div> | |
| <div class="ensemble-bar"> | |
| <div class="prob-fill ${statusClass}" style="width:${verdict.score}%"></div> | |
| </div> | |
| <div class="status-votes">${verdict.phishingVotes}/${verdict.totalModels} models flagged phishing \u00b7 ${safeVotes}/${verdict.totalModels} say legitimate</div> | |
| </div> | |
| <div class="url-display">${data.url}</div>`; | |
| // Build tabs | |
| const tabs = []; | |
| const tabContents = []; | |
| // Tab 1: URL Models | |
| if (data.url_models) { | |
| tabs.push({ id: 'tabUrl', label: 'URL Models', count: data.url_models.predictions?.length || 0 }); | |
| tabContents.push({ id: 'tabUrl', html: renderUrlModelsTab(data.url_models) }); | |
| } | |
| // Tab 2: HTML Models | |
| if (data.html_models) { | |
| tabs.push({ id: 'tabHtml', label: 'HTML Models', count: data.html_models.predictions?.length || 0 }); | |
| tabContents.push({ id: 'tabHtml', html: renderHtmlModelsTab(data.html_models) }); | |
| } else if (data.html_error) { | |
| tabs.push({ id: 'tabHtml', label: 'HTML Models', count: 0 }); | |
| tabContents.push({ id: 'tabHtml', html: `<div class="error-notice">HTML download failed: ${data.html_error}</div>` }); | |
| } | |
| // Tab 3: Combined Models | |
| if (data.combined_models) { | |
| tabs.push({ id: 'tabCombined', label: 'Combined Models', count: data.combined_models.predictions?.length || 0 }); | |
| tabContents.push({ id: 'tabCombined', html: renderCombinedModelsTab(data.combined_models) }); | |
| } | |
| // Tab 4: CNN Models | |
| if (data.cnn_models) { | |
| tabs.push({ id: 'tabCnn', label: 'CNN Models', count: data.cnn_models.predictions?.length || 0 }); | |
| tabContents.push({ id: 'tabCnn', html: renderCnnModelsTab(data.cnn_models) }); | |
| } | |
| const tabsHTML = tabs.map((t, i) => ` | |
| <button class="tab ${i === 0 ? 'active' : ''}" onclick="switchTab(event,'${t.id}')"> | |
| ${t.label} <span class="tab-count">${t.count}</span> | |
| </button> | |
| `).join(''); | |
| const contentsHTML = tabContents.map((t, i) => ` | |
| <div id="${t.id}" class="tab-content ${i === 0 ? 'active' : ''}">${t.html}</div> | |
| `).join(''); | |
| el.innerHTML = `${banner} | |
| <div class="tabs">${tabsHTML}</div> | |
| ${contentsHTML}`; | |
| } | |
| // TAB RENDERERS | |
| function renderUrlModelsTab(urlData) { | |
| const predictions = urlData.predictions || []; | |
| const features = urlData.features || {}; | |
| return ` | |
| <div class="section-title">Model Predictions</div> | |
| <div class="models-grid">${predictions.map(p => renderModelCard(p)).join('')}</div> | |
| ${renderFeatureSection(features, 'url')} | |
| `; | |
| } | |
| function renderHtmlModelsTab(htmlData) { | |
| const predictions = htmlData.predictions || []; | |
| const features = htmlData.features || {}; | |
| return ` | |
| <div class="section-title">Model Predictions</div> | |
| <div class="models-grid">${predictions.map(p => renderModelCard(p)).join('')}</div> | |
| ${renderFeatureSection(features, 'html')} | |
| `; | |
| } | |
| function renderCombinedModelsTab(combinedData) { | |
| const predictions = combinedData.predictions || []; | |
| const urlFeats = combinedData.url_features || {}; | |
| const htmlFeats = combinedData.html_features || {}; | |
| const hasHtmlF = Object.keys(htmlFeats).length > 0; | |
| return ` | |
| <div class="section-title">Model Predictions</div> | |
| <div class="models-grid">${predictions.map(p => renderModelCard(p)).join('')}</div> | |
| <div class="combined-features-tabs"> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchSubTab(event,'combinedUrlFeats')">URL Features</button> | |
| <button class="tab" onclick="switchSubTab(event,'combinedHtmlFeats')">HTML Features</button> | |
| </div> | |
| <div id="combinedUrlFeats" class="tab-content active"> | |
| ${renderFeatureSection(urlFeats, 'combined-url')} | |
| </div> | |
| <div id="combinedHtmlFeats" class="tab-content"> | |
| ${hasHtmlF | |
| ? renderFeatureSection(htmlFeats, 'combined-html') | |
| : `<div class="error-notice">HTML features unavailable${combinedData.html_error ? ': ' + combinedData.html_error : ''}</div>`} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderCnnModelsTab(cnnData) { | |
| const predictions = cnnData.predictions || []; | |
| return ` | |
| <div class="section-title">Model Predictions</div> | |
| <div class="models-grid">${predictions.map(p => renderModelCard(p)).join('')}</div> | |
| `; | |
| } | |
| // MODEL CARDS & INFO | |
| function renderModelCard(pred) { | |
| const isSafe = pred.prediction.toLowerCase() === 'legitimate'; | |
| const cls = isSafe ? 'safe' : 'danger'; | |
| return ` | |
| <div class="model-card ${cls}"> | |
| <div class="model-header"> | |
| <div class="model-name">${pred.model_name}</div> | |
| <div class="model-prediction ${cls}">${pred.prediction}</div> | |
| </div> | |
| <div class="model-confidence">${pred.confidence.toFixed(1)}%</div> | |
| <div class="model-confidence-label">Confidence</div> | |
| <div class="prob-container"> | |
| ${probRow('Safe', pred.legitimate_probability, 'safe')} | |
| ${probRow('Phishing', pred.phishing_probability, 'danger')} | |
| </div> | |
| </div>`; | |
| } | |
| function probRow(label, pct, cls) { | |
| return ` | |
| <div class="prob-row"> | |
| <span class="prob-label">${label}</span> | |
| <div class="prob-bar"><div class="prob-fill ${cls}" style="width:${pct}%"></div></div> | |
| <span class="prob-value">${pct.toFixed(0)}%</span> | |
| </div>`; | |
| } | |
| // FEATURE RENDERING | |
| function renderFeatureSection(features, tag) { | |
| if (!features || Object.keys(features).length === 0) return ''; | |
| const isHtml = 'num_forms' in features || 'html_length' in features; | |
| const topKeys = isHtml ? TOP_HTML_FEATURES : TOP_URL_FEATURES; | |
| const allKeys = isHtml ? ALL_HTML_FEATURES : ALL_URL_FEATURES; | |
| const remaining = allKeys.filter(k => !topKeys.includes(k)); | |
| const topHTML = renderFeatureList(topKeys, features); | |
| const remainingHTML = renderFeatureList(remaining, features); | |
| return ` | |
| <div class="section-title">Extracted Features (Top 20)</div> | |
| <div class="features-grid"> | |
| ${topHTML} | |
| <div id="hiddenFeatures-${tag}" class="features-hidden">${remainingHTML}</div> | |
| </div> | |
| <button class="show-more-btn" onclick="toggleAllFeatures('${tag}')" id="showMoreBtn-${tag}"> | |
| Show All Features (${Object.keys(features).length}) | |
| </button>`; | |
| } | |
| function renderFeatureList(keys, features) { | |
| return keys.filter(k => k in features).map(k => renderFeature(k, features[k])).join(''); | |
| } | |
| function renderFeature(key, value) { | |
| let itemClass = ''; | |
| let valueClass = ''; | |
| const isBool = typeof value === 'boolean' || value === 0 || value === 1; | |
| const boolVal = value === true || value === 1; | |
| if (isBool) { | |
| if (GOOD_INDICATORS.has(key)) { | |
| valueClass = boolVal ? 'true' : 'false'; | |
| itemClass = boolVal ? 'highlight-safe' : 'highlight-danger'; | |
| } else if (BAD_INDICATORS.has(key)) { | |
| valueClass = boolVal ? 'false' : 'true'; | |
| itemClass = boolVal ? 'highlight-danger' : 'highlight-safe'; | |
| } | |
| } | |
| if (key in DANGER_THRESHOLDS) { | |
| const [thr, op] = DANGER_THRESHOLDS[key]; | |
| if ((op === '>' && value > thr) || (op === '>=' && value >= thr)) { | |
| itemClass = 'highlight-danger'; | |
| } | |
| } | |
| if (key in SAFE_THRESHOLDS) { | |
| const [thr, op] = SAFE_THRESHOLDS[key]; | |
| if ((op === '<' && value < thr) || (op === '==' && value === thr)) { | |
| itemClass = 'highlight-safe'; | |
| } | |
| } | |
| return ` | |
| <div class="feature-item ${itemClass}"> | |
| <span class="feature-label">${formatName(key)}</span> | |
| <span class="feature-value ${valueClass}">${formatValue(value)}</span> | |
| </div>`; | |
| } | |
| function switchTab(event, tabId) { | |
| const parent = event.currentTarget.closest('.tabs')?.parentElement ?? document; | |
| parent.querySelectorAll('.tabs > .tab').forEach(t => t.classList.remove('active')); | |
| parent.querySelectorAll(':scope > .tab-content').forEach(c => c.classList.remove('active')); | |
| event.currentTarget.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| } | |
| function switchSubTab(event, tabId) { | |
| const parent = event.currentTarget.closest('.combined-features-tabs'); | |
| parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| parent.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| event.currentTarget.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| } | |
| function toggleFeatures(el) { | |
| const content = el.nextElementSibling; | |
| const icon = el.querySelector('.toggle-icon'); | |
| const isOpen = content.classList.toggle('open'); | |
| icon.textContent = isOpen ? '\u2212' : '+'; | |
| } | |
| function toggleAllFeatures(type) { | |
| const hidden = document.getElementById('hiddenFeatures-' + type); | |
| const btn = document.getElementById('showMoreBtn-' + type); | |
| if (hidden.classList.toggle('features-hidden')) { | |
| const total = hidden.closest('.features-grid')?.querySelectorAll('.feature-item').length ?? 0; | |
| btn.textContent = `Show All Features (${total})`; | |
| } else { | |
| btn.textContent = 'Show Less'; | |
| } | |
| } | |
| function formatName(name) { | |
| return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | |
| } | |
| function formatValue(value) { | |
| if (typeof value === 'boolean') return value ? 'Yes' : 'No'; | |
| if (value === 0 || value === 1) return value === 1 ? 'Yes' : 'No'; | |
| if (typeof value === 'number') return value % 1 === 0 ? value : value.toFixed(2); | |
| return value; | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const input = $url(); | |
| if (input) input.addEventListener('keypress', e => { if (e.key === 'Enter') analyzeAll(); }); | |
| }); | |