/* ================================================================ 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 = `