Files
litterbox/app/static/js/fuzzy.js
T
2025-08-19 09:40:05 -07:00

717 lines
31 KiB
JavaScript

// app/static/js/fuzzy.js
class FuzzyAnalyzer {
constructor() {
// Only initialize if we're in fuzzy mode
if (window.analysisType !== 'fuzzy') return;
// Initialize state
this.currentView = null;
// Initialize components
this.bindElements();
this.bindEvents();
this.initialize();
}
// DOM Element Bindings
bindElements() {
// Core elements
this.elements = {
fuzzyContent: document.getElementById('fuzzyContent'),
results: document.getElementById('fuzzyAnalysisResults'),
resultsTitle: document.getElementById('fuzzyResultsTitle'),
createDbForm: document.getElementById('createDbForm'),
analyzeForm: document.getElementById('analyzeForm'),
dbStats: document.getElementById('databaseStats'),
folderPath: document.getElementById('folderPath'),
tabs: {
createDb: document.getElementById('createDbView'),
analyze: document.getElementById('analyzeView')
}
};
// Exit if not in fuzzy view
if (!this.elements.fuzzyContent) return;
}
// Event Bindings
bindEvents() {
if (!this.elements.fuzzyContent) return;
// DOM Ready event
document.addEventListener('DOMContentLoaded', () => this.handleDOMReady());
// Input events
this.elements.folderPath?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.createDb();
});
}
handleDOMReady() {
if (!this.elements.fuzzyContent) return;
const currentView = document.querySelector('.nav-tab.active')?.id.replace('View', '');
if (currentView === 'analyze') {
this.loadAvailableFiles();
}
}
// Initialization
initialize() {
if (!this.elements.fuzzyContent) return;
this.currentView = document.querySelector('.nav-tab.active')?.id.replace('View', '') || 'create_db';
if (this.currentView === 'analyze') {
this.loadAvailableFiles();
}
this.loadDatabaseStats();
}
// View Management
switchView(view) {
if (!this.elements.fuzzyContent) return;
// Update tab states
this.updateTabStates(view);
// Update form visibility
this.updateFormVisibility(view);
// Update content
this.updateContent(view);
// Store current view
this.currentView = view;
}
updateTabStates(view) {
if (!this.elements.tabs) return;
// Reset all tabs
const inactiveClasses = 'nav-tab px-4 py-2 -mb-px border-b-2 text-gray-400 border-transparent';
const activeClasses = 'nav-tab px-4 py-2 -mb-px border-b-2 active border-blue-500 text-blue-500';
Object.values(this.elements.tabs).forEach(tab => {
if (tab) tab.className = inactiveClasses;
});
// Set active tab
const activeTab = this.elements.tabs[view === 'create_db' ? 'createDb' : 'analyze'];
if (activeTab) {
activeTab.className = activeClasses;
}
}
updateFormVisibility(view) {
if (!this.elements.createDbForm || !this.elements.analyzeForm) return;
this.elements.createDbForm.classList.toggle('hidden', view !== 'create_db');
this.elements.analyzeForm.classList.toggle('hidden', view !== 'analyze');
}
updateContent(view) {
if (!this.elements.resultsTitle || !this.elements.results) return;
// Update title
const titles = {
'create_db': 'Database Creation Results',
'analyze': 'Available Files for Analysis'
};
this.elements.resultsTitle.textContent = titles[view] || 'Results';
// Load appropriate content
if (view === 'analyze') {
this.loadAvailableFiles();
} else {
this.clearResults();
}
}
updateFormVisibility(view) {
this.elements.createDbForm.classList.toggle('hidden', view !== 'create_db');
this.elements.analyzeForm.classList.toggle('hidden', view !== 'analyze');
}
updateContent(view) {
// Update title
const titles = {
'create_db': 'Database Creation Results',
'analyze': 'Available Files for Analysis'
};
this.elements.resultsTitle.textContent = titles[view] || 'Results';
// Load appropriate content
if (view === 'analyze') {
this.loadAvailableFiles();
} else {
this.clearResults();
}
}
// Database Statistics
loadDatabaseStats() {
if (!this.elements.dbStats) return;
if (!this.validateDatabaseStats()) {
this.showDatabaseError();
return;
}
this.renderDatabaseStats();
}
validateDatabaseStats() {
return window.dbStats && window.dbStats.total_files !== 0;
}
showDatabaseError() {
if (!this.elements.dbStats) return;
this.elements.dbStats.innerHTML = this.templates.databaseError();
}
renderDatabaseStats() {
if (!this.elements.dbStats) return;
this.elements.dbStats.innerHTML = this.templates.databaseStats(window.dbStats);
}
// File Analysis Operations
async loadAvailableFiles() {
if (!this.elements.results) return;
this.showLoading('Loading available files...');
try {
const response = await this.api.getFiles();
if (response.status === 'success') {
this.displayAvailableFiles(response.payload_based.payloads);
} else {
this.showError('Failed to load files');
}
} catch (error) {
console.error('Error:', error);
this.showError('Failed to load available files');
}
}
displayAvailableFiles(files) {
if (!this.elements.results) return;
if (!files || Object.keys(files).length === 0) {
this.elements.results.innerHTML = this.templates.noFiles();
return;
}
this.elements.results.innerHTML = this.templates.filesList(files);
}
async analyzeFile(hash) {
if (!this.elements.results) return;
this.showLoading('Analyzing file...');
try {
const response = await this.api.analyzeFile(hash);
this.displayResults(this.templates.analysisResults(response));
} catch (error) {
console.error('Analysis error:', error);
this.showError(error.message);
}
}
// Database Operations
async createDb() {
if (!this.elements.folderPath || !this.elements.results) return;
const folderPath = this.elements.folderPath.value.trim();
if (!folderPath) {
this.showError('Folder path is required');
return;
}
this.showLoading('Creating database...');
try {
const response = await this.api.createDatabase(folderPath);
if (response.status === 'success') {
// Update the global dbStats with the new stats
window.dbStats = {
total_files: response.stats.processed,
db_size_human: 'Updated',
last_updated: new Date().toLocaleString(),
total_size_human: 'Calculating...',
sources: response.stats.sources.reduce((acc, src) => {
acc[src] = { count: 1 };
return acc;
}, {})
};
// Display creation results
this.displayResults(this.templates.databaseResults(response));
// Clear any error message in the stats section
if (this.elements.dbStats) {
this.elements.dbStats.innerHTML = this.templates.databaseStats(window.dbStats);
}
// Optionally, fetch fresh stats
try {
const statsResponse = await this.api.getDatabaseStats();
if (statsResponse.status === 'success') {
window.dbStats = statsResponse.stats;
if (this.elements.dbStats) {
this.elements.dbStats.innerHTML = this.templates.databaseStats(window.dbStats);
}
}
} catch (statsError) {
console.error('Error fetching updated stats:', statsError);
}
} else {
this.showError(response.error || 'Failed to create database');
}
} catch (error) {
console.error('Create DB error:', error);
this.showError(error.message || 'Failed to create database');
}
}
// UI Helpers
showLoading(message = 'Processing...') {
if (!this.elements.results) return;
this.elements.results.innerHTML = this.templates.loading(message);
}
showError(message) {
if (!this.elements.results) return;
this.elements.results.innerHTML = this.templates.error(message);
}
clearResults() {
if (!this.elements.results) return;
this.elements.results.innerHTML = '';
}
displayResults(html) {
if (!this.elements.results) return;
this.elements.results.innerHTML = html;
}
// Utility Methods
formatHexView(data) {
if (!data || !data.length) return 'No printable characters found';
const lines = data.map(block => {
return block.ascii_repr.split('\n')
.filter(line => !line.split('').every(char => char === '.'))
.map(line => line.trim())
.filter(Boolean);
}).flat();
return lines.length ? lines.join('\n') : 'No printable characters found';
}
getSimilarityColor(similarity) {
if (similarity >= 90) return 'text-green-400';
if (similarity >= 70) return 'text-yellow-400';
if (similarity >= 50) return 'text-blue-400';
return 'text-gray-400';
}
// API Methods
api = {
createDatabase: async (folderPath, extensions) => {
return await fetch('/doppelganger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'fuzzy',
operation: 'create_db',
folder_path: folderPath,
extensions: extensions
})
}).then(r => r.json());
},
getFiles: async () => {
return await fetch('/files').then(r => r.json());
},
analyzeFile: async (hash) => {
return await fetch('/doppelganger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'fuzzy',
operation: 'analyze',
hash: hash,
threshold: 1
})
}).then(r => r.json());
}
};
toggleSources(event) {
const button = event.currentTarget;
const sourcesList = button.parentElement.nextElementSibling;
const expandIcon = button.querySelector('.expand-icon');
const expandText = button.querySelector('.expand-text');
if (sourcesList.style.maxHeight === '100px') {
sourcesList.style.maxHeight = sourcesList.scrollHeight + 'px';
expandIcon.classList.add('rotate-180');
expandText.textContent = 'Show Less';
} else {
sourcesList.style.maxHeight = '100px';
expandIcon.classList.remove('rotate-180');
expandText.textContent = 'Show More';
}
}
toggleRegionContent(event) {
const regionContainer = event.currentTarget.closest('.p-4');
const contentBlocks = regionContainer.querySelectorAll('.content-block');
const expandIcon = event.currentTarget.querySelector('.expand-icon');
const expandText = event.currentTarget.querySelector('.expand-text');
if (contentBlocks[0].style.maxHeight === '100px') {
contentBlocks.forEach(block => {
block.style.maxHeight = block.scrollHeight + 'px';
});
expandIcon.classList.add('rotate-180');
expandText.textContent = 'Show Less';
} else {
contentBlocks.forEach(block => {
block.style.maxHeight = '100px';
});
expandIcon.classList.remove('rotate-180');
expandText.textContent = 'Show More';
}
}
// Templates
templates = {
loading: (message) => `
<div class="flex items-center justify-center p-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-red-500"></div>
<span class="ml-2 text-gray-400">${message}</span>
</div>
`,
error: (message) => `
<div class="bg-red-900/20 border border-red-900/50 text-red-400 px-4 py-3 rounded">
Error: ${message}
</div>
`,
databaseError: () => `
<h2 class="text-lg font-medium text-gray-100 mb-4">Database Statistics</h2>
<div class="bg-red-900/20 border border-red-900/50 text-red-400 px-4 py-3 rounded">
Error: No database found. Create a database to get started.
</div>
`,
databaseStats: (stats) => `
<h2 class="text-lg font-medium text-gray-100 mb-4">Database Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-gray-700/50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-300 mb-2">Database Info</h3>
<div class="space-y-2 text-sm">
<div>
<span class="text-gray-400">Database Size:</span>
<span class="text-gray-100 ml-2">${stats.db_size_human}</span>
</div>
<div>
<span class="text-gray-400">Last Updated:</span>
<span class="text-gray-100 ml-2">${stats.last_updated}</span>
</div>
</div>
</div>
<div class="bg-gray-700/50 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-300 mb-2">Content Info</h3>
<div class="space-y-2 text-sm">
<div>
<span class="text-gray-400">Total Files:</span>
<span class="text-gray-100 ml-2">${stats.total_files}</span>
</div>
<div>
<span class="text-gray-400">Total Files Size:</span>
<span class="text-gray-100 ml-2">${stats.total_size_human}</span>
</div>
</div>
</div>
<div class="bg-gray-700/50 rounded-lg p-4">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center gap-2">
<h3 class="text-sm font-medium text-gray-300">Sources</h3>
<span class="text-xs text-gray-400">(${Object.keys(stats.sources).length} total)</span>
</div>
<button onclick="fuzzyAnalyzer.toggleSources(event)"
class="text-blue-400 text-sm hover:text-blue-300 flex items-center gap-1">
<span class="expand-text">Show More</span>
<svg class="w-4 h-4 transform transition-transform expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
</div>
<div class="space-y-2 text-sm sources-list" style="max-height: 100px; overflow: hidden; transition: max-height 0.3s ease-in-out">
${Object.entries(stats.sources).map(([source, data]) => `
<div>
<span class="text-yellow-400">${source}</span>
</div>
`).join('')}
</div>
</div>
</div>
`,
noFiles: () => `
<div class="flex flex-col items-center justify-center py-12 px-4 bg-gray-800/20 rounded-lg">
<div class="mb-6">
<svg class="w-16 h-16 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M4 5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V19a2 2 0 01-2 2H6a2 2 0 01-2-2V5z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 15v.01M12 12v-4"/>
</svg>
</div>
<h3 class="text-xl font-medium text-gray-300 mb-3">
No Files Available
</h3>
<div class="space-y-2 text-center max-w-md">
<p class="text-gray-400">
There are currently no files available for analysis in the system.
</p>
<p class="text-gray-500 text-sm">
Upload files through the interface to begin the analysis process.
</p>
</div>
<div class="mt-6 flex gap-4">
<a href="/" class="px-4 py-2 bg-pink-500/10 text-green-400 rounded-lg border border-green-500/20 hover:bg-green-500/20 transition-colors">
Upload Files
</a>
</div>
</div>
`,
filesList: (files) => `
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-800 text-left text-xs font-medium text-gray-300 tracking-wider">File name</th>
<th class="px-6 py-3 bg-gray-800 text-left text-xs font-medium text-gray-300 tracking-wider">Hash</th>
<th class="px-6 py-3 bg-gray-800 text-left text-xs font-medium text-gray-300 tracking-wider">Upload date</th>
<th class="px-6 py-3 bg-gray-800 text-left text-xs font-medium text-gray-300 tracking-wider">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
${Object.entries(files).map(([id, file], index) => `
<tr class="${index % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-800/30'}">
<td class="px-6 py-4">
<span class="text-gray-200">${file.filename}</span>
</td>
<td class="px-6 py-4">
<span class="text-sm text-gray-500 font-mono">${file.md5}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
${file.upload_time}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<button onclick="fuzzyAnalyzer.analyzeFile('${file.md5}')"
class="px-3 py-1 text-blue-400 border border-blue-900/20 rounded-lg hover:bg-blue-500/10 transition-colors">
Compare
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`,
databaseResults: (data) => `
<div class="space-y-4">
<div class="p-4 bg-gray-700/50 rounded-lg">
<h3 class="text-lg font-medium text-gray-100 mb-2">Database Creation Complete</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-400">Processed Files:</span>
<span class="text-gray-100 ml-2">${data.stats.processed}</span>
</div>
<div>
<span class="text-gray-400">Skipped Files:</span>
<span class="text-gray-100 ml-2">${data.stats.skipped}</span>
</div>
<div>
<span class="text-gray-400">Total Files:</span>
<span class="text-gray-100 ml-2">${data.stats.total}</span>
</div>
<div>
<span class="text-gray-400">Sources:</span>
<span class="text-gray-100 ml-2">${data.stats.sources.join(', ') || 'None'}</span>
</div>
</div>
</div>
</div>
`,
analysisResults: (data) => {
if (!data.results || !data.results.length) {
return `
<div class="text-center text-gray-400 py-4">
<p>No matches found in the database</p>
</div>
`;
}
return data.results.map(result => `
<div class="p-4 bg-gray-700/50 rounded-lg mb-4">
<h3 class="text-lg font-medium text-gray-100 mb-2">Results for: ${result.file}</h3>
<div class="space-y-2 text-sm">
<div>
<span class="text-gray-400">MD5:</span>
<span class="text-gray-100 ml-2 font-mono">${result.md5}</span>
</div>
<div>
<span class="text-gray-400">File Size:</span>
<span class="text-gray-100 ml-2">${result.file_size} bytes</span>
</div>
<div>
<span class="text-gray-400">Total Blocks in File:</span>
<span class="text-gray-100 ml-2">${result.total_blocks}</span>
</div>
<div>
<span class="text-gray-400">Matches Found:</span>
<span class="text-gray-100 ml-2">${result.total_matches} files</span>
</div>
</div>
${this.templates.matches(result.matches, result.total_blocks)}
</div>
`).join('');
},
matches: (matches, totalBlocks) => {
if (!matches || !matches.length) return '';
return `
<div class="mt-4 space-y-4">
${matches.map(match => `
<div class="bg-gray-800/50 rounded-lg overflow-hidden border border-gray-700">
<!-- Match Header -->
<div class="p-4 border-b border-gray-700">
<div class="flex justify-between items-start mb-2">
<div>
<div class="text-gray-100">${match.file}</div>
<div class="text-sm text-gray-400">Source: ${match.source}</div>
</div>
<div class="text-right">
<div class="text-2xl font-bold ${this.getSimilarityColor(match.overall_similarity)}">
${match.overall_similarity.toFixed(1)}%
</div>
<div class="text-sm text-gray-400">Similar</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-400">Target Size:</span>
<span class="text-gray-100 ml-2">${match.target_size} bytes</span>
</div>
<div>
<span class="text-gray-400">MD5:</span>
<span class="text-gray-100 ml-2 font-mono">${match.md5}</span>
</div>
<div>
<span class="text-gray-400">Added:</span>
<span class="text-gray-100 ml-2">${match.date_added}</span>
</div>
<div>
<span class="text-gray-400">Matching Regions:</span>
<span class="text-gray-100 ml-2">${match.total_regions} out of ${totalBlocks}</span>
</div>
</div>
</div>
<!-- Matching Regions -->
${this.templates.matchingRegions(match.matching_regions)}
</div>
`).join('')}
</div>
`;
},
matchingRegions: (regions) => {
return regions.map((region, index) => `
<div class="p-4 border-t border-gray-700">
<!-- Region Header -->
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<h4 class="text-lg font-medium text-gray-100">Region ${index + 1}</h4>
<div class="text-sm flex items-center gap-4">
<span class="text-blue-400 font-medium">${region.avg_similarity.toFixed(1)}% Similar</span>
<span class="text-gray-400">(${region.length} bytes)</span>
<button onclick="fuzzyAnalyzer.toggleRegionContent(event)"
class="text-blue-400 text-sm hover:text-blue-300 flex items-center gap-1">
<span class="expand-text">Show More</span>
<svg class="w-4 h-4 transform transition-transform expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
</div>
</div>
<!-- Memory Ranges -->
<div class="grid grid-cols-2 gap-4 text-sm bg-gray-800/50 p-3 rounded-lg">
<div>
<div class="text-gray-400">Memory Range in DB File:</div>
<div class="font-mono text-gray-100">
${region.target_start.toString(16).padStart(8, '0')} - ${(region.target_start + region.length).toString(16).padStart(8, '0')}
</div>
</div>
<div>
<div class="text-gray-400">Memory Range in Payload File:</div>
<div class="font-mono text-gray-100">
${region.source_start.toString(16).padStart(8, '0')} - ${(region.source_start + region.length).toString(16).padStart(8, '0')}
</div>
</div>
</div>
</div>
<!-- Data Comparison -->
<div class="grid grid-cols-2 gap-6">
<!-- Database File Data -->
<div class="space-y-2">
<div class="flex items-center bg-purple-900/20 text-purple-400 px-3 py-2 rounded-t-lg border-b border-purple-900/50">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Database File Contents
</div>
<div class="overflow-x-auto bg-gray-900/50 rounded-b-lg p-3">
<pre class="font-mono text-xs leading-relaxed whitespace-pre text-gray-300 content-block" style="max-height: 100px; overflow: hidden; transition: max-height 0.3s ease-in-out">${this.formatHexView(region.target_data)}</pre>
</div>
</div>
<!-- Payload File Data -->
<div class="space-y-2">
<div class="flex items-center bg-blue-900/20 text-blue-400 px-3 py-2 rounded-t-lg border-b border-blue-900/50">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Payload File Contents (Being Analyzed)
</div>
<div class="overflow-x-auto bg-gray-900/50 rounded-b-lg p-3">
<pre class="font-mono text-xs leading-relaxed whitespace-pre text-gray-300 content-block" style="max-height: 100px; overflow: hidden; transition: max-height 0.3s ease-in-out">${this.formatHexView(region.source_data)}</pre>
</div>
</div>
</div>
</div>
`).join('');
}
};
}
if (window.analysisType === 'fuzzy') {
window.fuzzyAnalyzer = new FuzzyAnalyzer();
}