/** * MITM Certificate Search Interface * Provides search functionality for SSL MITM certificate data */ (function() { 'use strict'; // Theme toggle (shared with dashboard) var themes = ['dark', 'muted-dark', 'light']; function getTheme() { if (document.documentElement.classList.contains('light')) return 'light'; if (document.documentElement.classList.contains('muted-dark')) return 'muted-dark'; return 'dark'; } function setTheme(theme) { document.documentElement.classList.remove('light', 'muted-dark'); if (theme === 'light') document.documentElement.classList.add('light'); else if (theme === 'muted-dark') document.documentElement.classList.add('muted-dark'); try { localStorage.setItem('ppf-theme', theme); } catch(e) {} } function initTheme() { var saved = null; try { saved = localStorage.getItem('ppf-theme'); } catch(e) {} if (saved && themes.indexOf(saved) !== -1) { setTheme(saved); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { setTheme('light'); } var btn = document.getElementById('themeToggle'); if (btn) { btn.addEventListener('click', function() { var current = getTheme(); var idx = themes.indexOf(current); var next = themes[(idx + 1) % themes.length]; setTheme(next); }); } } document.addEventListener('DOMContentLoaded', initTheme); // DOM elements var searchInput = document.getElementById('searchInput'); var searchClear = document.getElementById('searchClear'); var suggestions = document.getElementById('suggestions'); var resultsContainer = document.getElementById('resultsContainer'); var resultsEmpty = document.getElementById('resultsEmpty'); var resultsLoading = document.getElementById('resultsLoading'); var resultsList = document.getElementById('resultsList'); var resultsContent = document.getElementById('resultsContent'); var resultsCount = document.getElementById('resultsCount'); var noDataState = document.getElementById('noDataState'); // Stats elements var totalDetections = document.getElementById('totalDetections'); var uniqueCerts = document.getElementById('uniqueCerts'); var uniqueOrgs = document.getElementById('uniqueOrgs'); var uniqueProxies = document.getElementById('uniqueProxies'); // State var searchTimeout = null; var currentData = null; var suggestionIndex = -1; // Search field definitions for autocomplete var searchFields = [ { prefix: 'org:', desc: 'organization name', icon: '🏢' }, { prefix: 'issuer:', desc: 'certificate issuer', icon: '📄' }, { prefix: 'cn:', desc: 'common name', icon: '🔗' }, { prefix: 'proxy:', desc: 'proxy IP address', icon: '🖥' }, { prefix: 'fp:', desc: 'fingerprint', icon: '🔑' }, { prefix: 'serial:', desc: 'serial number', icon: '🔢' }, { prefix: 'expires:', desc: 'expiration year', icon: '📅' }, { prefix: 'expired:', desc: 'yes/no', icon: '⚠' } ]; /** * Escape HTML to prevent XSS */ function escapeHtml(str) { if (str === null || str === undefined) return ''; var div = document.createElement('div'); div.textContent = String(str); return div.innerHTML; } /** * Sanitize search query - remove potentially dangerous characters */ function sanitizeQuery(query) { if (!query) return ''; // Allow alphanumeric, spaces, common punctuation for searches // Remove control characters and dangerous patterns return String(query) .replace(/[\x00-\x1f\x7f]/g, '') // Control characters .replace(/<[^>]*>/g, '') // HTML tags .trim() .substring(0, 200); // Limit length } /** * Format number with comma separators */ function formatNumber(n) { if (n === null || n === undefined || n === '-') return '-'; return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } /** * Format timestamp to readable date */ function formatDate(ts) { if (!ts) return '-'; // Handle ASN.1 time format (YYYYMMDDhhmmssZ) if (typeof ts === 'string' && ts.length >= 14) { var y = ts.substring(0, 4); var m = ts.substring(4, 6); var d = ts.substring(6, 8); return y + '-' + m + '-' + d; } // Handle Unix timestamp if (typeof ts === 'number') { var date = new Date(ts * 1000); return date.toISOString().split('T')[0]; } return String(ts); } /** * Check if certificate is expired */ function isExpired(notAfter) { if (!notAfter || notAfter.length < 14) return false; var expDate = new Date( parseInt(notAfter.substring(0, 4)), parseInt(notAfter.substring(4, 6)) - 1, parseInt(notAfter.substring(6, 8)) ); return expDate < new Date(); } /** * Load initial stats */ function loadStats() { fetch('/api/mitm') .then(function(r) { return r.json(); }) .then(function(data) { currentData = data; // Update summary stats totalDetections.textContent = formatNumber(data.total_detections || 0); uniqueCerts.textContent = formatNumber(data.unique_certs || 0); uniqueProxies.textContent = formatNumber(data.unique_proxies || 0); // Count unique orgs from top_organizations var orgCount = (data.top_organizations || []).length; uniqueOrgs.textContent = formatNumber(orgCount); // Show no data state if empty if (!data.certificates || data.certificates.length === 0) { document.getElementById('statsSummary').style.display = 'none'; document.querySelector('.search-box').style.display = 'none'; document.getElementById('helpBox').style.display = 'none'; resultsContainer.style.display = 'none'; noDataState.style.display = 'block'; } }) .catch(function(err) { console.error('Failed to load MITM stats:', err); }); } /** * Parse search query into structured filters */ function parseQuery(query) { var filters = { org: null, issuer: null, cn: null, proxy: null, fp: null, serial: null, expires: null, expired: null, text: [] }; if (!query) return filters; // Match field:value patterns and quoted strings var parts = query.match(/(\w+:"[^"]+"|"[^"]+"|[\w:.]+)/g) || []; for (var i = 0; i < parts.length; i++) { var part = parts[i]; var colonIdx = part.indexOf(':'); if (colonIdx > 0 && colonIdx < part.length - 1) { var field = part.substring(0, colonIdx).toLowerCase(); var value = part.substring(colonIdx + 1).replace(/^"|"$/g, ''); if (filters.hasOwnProperty(field)) { filters[field] = value; } else { filters.text.push(part); } } else { // Plain text search filters.text.push(part.replace(/^"|"$/g, '')); } } return filters; } /** * Check if certificate matches filters */ function matchesCert(cert, filters) { // Organization filter if (filters.org) { var org = (cert.subject_o || '').toLowerCase(); if (org.indexOf(filters.org.toLowerCase()) === -1) return false; } // Issuer filter if (filters.issuer) { var issuer = (cert.issuer_cn || cert.issuer_o || '').toLowerCase(); if (issuer.indexOf(filters.issuer.toLowerCase()) === -1) return false; } // Common name filter if (filters.cn) { var cn = (cert.subject_cn || '').toLowerCase(); if (cn.indexOf(filters.cn.toLowerCase()) === -1) return false; } // Proxy filter if (filters.proxy) { var proxies = cert.proxies || []; var found = false; for (var i = 0; i < proxies.length; i++) { if (proxies[i].indexOf(filters.proxy) !== -1) { found = true; break; } } if (!found) return false; } // Fingerprint filter if (filters.fp) { var fp = (cert.fingerprint || cert.fingerprint_full || '').toLowerCase(); if (fp.indexOf(filters.fp.toLowerCase()) === -1) return false; } // Serial filter if (filters.serial) { var serial = (cert.serial || '').toLowerCase(); if (serial.indexOf(filters.serial.toLowerCase()) === -1) return false; } // Expiration year filter if (filters.expires) { var notAfter = cert.not_after || ''; if (notAfter.indexOf(filters.expires) === -1) return false; } // Expired filter if (filters.expired) { var wantExpired = filters.expired.toLowerCase() === 'yes'; var certExpired = isExpired(cert.not_after); if (wantExpired !== certExpired) return false; } // Text search (searches all fields) if (filters.text.length > 0) { var searchable = [ cert.subject_cn || '', cert.subject_o || '', cert.issuer_cn || '', cert.issuer_o || '', cert.fingerprint || '', cert.serial || '' ].join(' ').toLowerCase(); for (var j = 0; j < filters.text.length; j++) { if (searchable.indexOf(filters.text[j].toLowerCase()) === -1) { return false; } } } return true; } /** * Perform search on current data */ function performSearch(query) { query = sanitizeQuery(query); if (!query) { showEmpty(); return; } if (!currentData || !currentData.certificates) { showEmpty(); return; } showLoading(); // Use setTimeout to not block UI setTimeout(function() { var filters = parseQuery(query); var results = []; var certs = currentData.certificates || []; for (var i = 0; i < certs.length; i++) { if (matchesCert(certs[i], filters)) { results.push(certs[i]); } } showResults(results, query); }, 50); } /** * Show empty state */ function showEmpty() { resultsEmpty.style.display = 'block'; resultsLoading.style.display = 'none'; resultsList.style.display = 'none'; } /** * Show loading state */ function showLoading() { resultsEmpty.style.display = 'none'; resultsLoading.style.display = 'block'; resultsList.style.display = 'none'; } /** * Render search results */ function showResults(results, query) { resultsEmpty.style.display = 'none'; resultsLoading.style.display = 'none'; resultsList.style.display = 'block'; resultsCount.textContent = formatNumber(results.length); if (results.length === 0) { resultsContent.innerHTML = '
' + '
🔍
' + '
No certificates match your search
' + '
Try adjusting your filters or search terms
' + '
'; return; } var html = ''; for (var i = 0; i < results.length; i++) { html += renderCertCard(results[i]); } resultsContent.innerHTML = html; } /** * Render a single certificate card */ function renderCertCard(cert) { var expired = isExpired(cert.not_after); var proxies = cert.proxies || []; var html = '
'; html += '
'; html += '
'; html += 'Certificate'; html += escapeHtml(cert.subject_cn || cert.subject_o || 'Unknown'); html += '
'; html += '
'; if (expired) { html += 'Expired · '; } html += 'Seen ' + escapeHtml(String(cert.count || 1)) + ' time(s)'; html += '
'; html += '
'; html += '
'; // Subject html += '
'; html += '
Subject (CN)
'; html += '
' + escapeHtml(cert.subject_cn || '-') + '
'; html += '
'; // Organization html += '
'; html += '
Organization
'; html += '
' + escapeHtml(cert.subject_o || '-') + '
'; html += '
'; // Issuer html += '
'; html += '
Issuer (CN)
'; html += '
' + escapeHtml(cert.issuer_cn || '-') + '
'; html += '
'; // Issuer Org html += '
'; html += '
Issuer Org
'; html += '
' + escapeHtml(cert.issuer_o || '-') + '
'; html += '
'; // Fingerprint html += '
'; html += '
Fingerprint
'; html += '
' + escapeHtml(cert.fingerprint || cert.fingerprint_full || '-') + '
'; html += '
'; // Serial html += '
'; html += '
Serial
'; html += '
' + escapeHtml(cert.serial || '-') + '
'; html += '
'; // Valid From html += '
'; html += '
Valid From
'; html += '
' + escapeHtml(formatDate(cert.not_before)) + '
'; html += '
'; // Valid Until html += '
'; html += '
Valid Until
'; html += '
' + escapeHtml(formatDate(cert.not_after)) + '
'; html += '
'; html += '
'; // Proxies list if (proxies.length > 0) { html += '
'; html += '
Proxies using this certificate (' + proxies.length + ')
'; html += '
'; for (var i = 0; i < Math.min(proxies.length, 10); i++) { html += '' + escapeHtml(proxies[i]) + ''; } if (proxies.length > 10) { html += '+' + (proxies.length - 10) + ' more'; } html += '
'; html += '
'; } html += '
'; return html; } /** * Show autocomplete suggestions */ function showSuggestions(query) { query = sanitizeQuery(query); if (!query) { hideSuggestions(); return; } var html = ''; // Check if user is typing a field prefix var lastWord = query.split(/\s+/).pop() || ''; var colonIdx = lastWord.indexOf(':'); if (colonIdx === -1) { // Show matching field suggestions var matchingFields = []; for (var i = 0; i < searchFields.length; i++) { if (searchFields[i].prefix.toLowerCase().indexOf(lastWord.toLowerCase()) === 0) { matchingFields.push(searchFields[i]); } } if (matchingFields.length > 0) { html += '
'; html += '
Field Filters
'; for (var j = 0; j < matchingFields.length; j++) { var field = matchingFields[j]; html += '
'; html += '' + field.icon + ''; html += '' + escapeHtml(field.prefix) + ''; html += '' + escapeHtml(field.desc) + ''; html += '
'; } html += '
'; } } // Add quick value suggestions from data if (currentData && colonIdx > 0) { var fieldName = lastWord.substring(0, colonIdx); var valuePart = lastWord.substring(colonIdx + 1).toLowerCase(); var valueSuggestions = getValueSuggestions(fieldName, valuePart); if (valueSuggestions.length > 0) { html += '
'; html += '
Matching Values
'; for (var k = 0; k < Math.min(valueSuggestions.length, 5); k++) { var val = valueSuggestions[k]; html += '
'; html += ''; html += '' + escapeHtml(val) + ''; html += '
'; } html += '
'; } } if (html) { suggestions.innerHTML = html; suggestions.classList.add('visible'); suggestionIndex = -1; } else { hideSuggestions(); } } /** * Get value suggestions for a field */ function getValueSuggestions(field, partial) { if (!currentData) return []; var values = []; field = field.toLowerCase(); if (field === 'org') { var orgs = currentData.top_organizations || []; for (var i = 0; i < orgs.length; i++) { var orgName = orgs[i].name || ''; if (partial === '' || orgName.toLowerCase().indexOf(partial) !== -1) { values.push(orgName); } } } else if (field === 'issuer') { var issuers = currentData.top_issuers || []; for (var j = 0; j < issuers.length; j++) { var issuerName = issuers[j].name || ''; if (partial === '' || issuerName.toLowerCase().indexOf(partial) !== -1) { values.push(issuerName); } } } else if (field === 'expired') { values = ['yes', 'no']; } return values; } /** * Hide suggestions dropdown */ function hideSuggestions() { suggestions.classList.remove('visible'); suggestionIndex = -1; } /** * Apply suggestion to search input */ function applySuggestion(value) { var query = searchInput.value; var parts = query.split(/\s+/); parts[parts.length - 1] = value; searchInput.value = parts.join(' '); searchInput.focus(); hideSuggestions(); // Trigger search if value is complete if (value.indexOf(':') !== -1) { handleSearch(); } } /** * Handle search input */ function handleSearch() { var query = searchInput.value; // Show/hide clear button if (query) { searchClear.classList.add('visible'); } else { searchClear.classList.remove('visible'); } // Debounce search if (searchTimeout) { clearTimeout(searchTimeout); } searchTimeout = setTimeout(function() { performSearch(query); }, 200); // Show suggestions immediately showSuggestions(query); } /** * Clear search */ function clearSearch() { searchInput.value = ''; searchClear.classList.remove('visible'); hideSuggestions(); showEmpty(); searchInput.focus(); } /** * Toggle help box */ window.toggleHelp = function() { var helpBox = document.getElementById('helpBox'); helpBox.classList.toggle('collapsed'); }; /** * Keyboard navigation for suggestions */ function handleKeydown(e) { var items = suggestions.querySelectorAll('.suggestion-item'); if (!items.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); suggestionIndex = Math.min(suggestionIndex + 1, items.length - 1); updateSuggestionHighlight(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); suggestionIndex = Math.max(suggestionIndex - 1, 0); updateSuggestionHighlight(items); } else if (e.key === 'Enter' && suggestionIndex >= 0) { e.preventDefault(); var value = items[suggestionIndex].getAttribute('data-value'); if (value) applySuggestion(value); } else if (e.key === 'Escape') { hideSuggestions(); } } /** * Update suggestion highlight */ function updateSuggestionHighlight(items) { for (var i = 0; i < items.length; i++) { if (i === suggestionIndex) { items[i].classList.add('active'); } else { items[i].classList.remove('active'); } } } // Event listeners searchInput.addEventListener('input', handleSearch); searchInput.addEventListener('keydown', handleKeydown); searchInput.addEventListener('focus', function() { if (searchInput.value) showSuggestions(searchInput.value); }); searchClear.addEventListener('click', clearSearch); // Click on suggestion suggestions.addEventListener('click', function(e) { var item = e.target.closest('.suggestion-item'); if (item) { var value = item.getAttribute('data-value'); if (value) applySuggestion(value); } }); // Click outside to hide suggestions document.addEventListener('click', function(e) { if (!e.target.closest('.search-box')) { hideSuggestions(); } }); // Initialize loadStats(); })();