rb1337's picture
Upload 50 files
2cc7f91 verified
/* ================================================================
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(); });
});