Files
litterbox/app/static/js/upload/core.js
T
BlackSnufkin 96ced766e6 Add 'All' pipeline + EDR saved-view + Whiskers chunked XOR
- New 'All' analysis mode: client-side coordinator runs Static + every EDR
  profile in parallel; Dynamic waits only for Static (EDR is on a remote
  VM, no local resource contention). New /analyze/all/<target> route +
  analyze_all.html + analyze-all/core.js. Done banner exposes per-result
  jump links; rows linkify to their saved detail views (no auto-redirect).

- /results/edr/<profile>/<target>: saved-view route that loads the saved
  JSON and renders via the same tools/edr.js module the live page uses,
  so the saved view shows MITRE chips, call stack, triggering API,
  memory region, expandable per-alert detail and raw _source. New
  edr_info.html embeds findings as window.__edrSavedResults; new
  edr-saved.js bootstraps the renderer.

- Whiskers: chunked XOR write (64 KiB working buffer) replaces the
  byte-by-byte loop that was timing out the orchestrator on multi-MB
  payloads. AgentClient gains a separate exec_timeout (180s) for the
  multipart upload + agent-side decode path.

- file_info.html: Dynamic button uses lb-btn-primary to match Static and
  EDR (drops the orphan yellow styling).
2026-04-30 02:52:55 -07:00

696 lines
32 KiB
JavaScript

// app/static/js/upload/core.js
// DOMContentLoaded entry for the upload page (/).
// Drag-drop, validation, POST /upload, file metadata rendering,
// and analysis-button wiring.
import {
createLnkInfoSection,
renderLnkInfo,
calculateLnkRisk,
getRiskLevelClass,
getTargetCommandBorderClass,
getTargetCommandTextClass,
} from './lnk.js';
// app/static/js/upload_updated.js
// Check if serverConfig exists and use a default if it doesn't
const maxFileSize = (window.serverConfig && window.serverConfig.maxFileSize) || 100 * 1024 * 1024;
const maxFileSizeMB = (window.serverConfig && window.serverConfig.maxFileSizeMB) || 100;
// Upload configurations
const UPLOAD_CONFIG = {
maxFileSize: maxFileSize,
toastDuration: 3000,
transitionDelay: 300,
fadeDelay: 50
};
// Global variables
let currentFileHash = null;
let currentFileExtension = null;
let isDriverFile = false;
document.addEventListener('DOMContentLoaded', function() {
const elements = {
dropZone: document.getElementById('dropZone'),
fileInput: document.getElementById('fileInput'),
uploadStatus: document.getElementById('uploadStatus'),
uploadArea: document.getElementById('uploadArea'),
fileAnalysisArea: document.getElementById('fileAnalysisArea'),
fileName: document.getElementById('fileName'),
fileSize: document.getElementById('fileSize'),
fileType: document.getElementById('fileType'),
fileFormat: document.getElementById('fileFormat'),
fileCategory: document.getElementById('fileCategory'),
fileEntropy: document.getElementById('fileEntropy'),
uploadTime: document.getElementById('uploadTime'),
md5Hash: document.getElementById('md5Hash'),
sha256Hash: document.getElementById('sha256Hash'),
fileSpecificInfo: document.getElementById('fileSpecificInfo'),
step1Circle: document.getElementById('step1Circle'),
step1Text: document.getElementById('step1Text'),
step2Circle: document.getElementById('step2Circle'),
step2Text: document.getElementById('step2Text'),
progressLine: document.getElementById('progressLine'),
toastContainer: document.getElementById('toastContainer'),
entropyBar: document.getElementById('entropyBar'),
entropyNotes: document.getElementById('entropyNotes'),
detectionRisk: document.getElementById('detectionRisk'),
peInfo: document.getElementById('peInfo'),
sectionsList: document.getElementById('sectionsList'),
detectionNotes: document.getElementById('detectionNotes'),
officeInfo: document.getElementById('officeInfo'),
macroStatus: document.getElementById('macroStatus'),
macroDetectionNotes: document.getElementById('macroDetectionNotes'),
checksumInfo: document.getElementById('checksumInfo'),
checksumStatus: document.getElementById('checksumStatus'),
storedChecksum: document.getElementById('storedChecksum'),
calculatedChecksum: document.getElementById('calculatedChecksum'),
checksumNotes: document.getElementById('checksumNotes'),
suspiciousImports: document.getElementById('suspiciousImports'),
suspiciousImportsList: document.getElementById('suspiciousImportsList'),
suspiciousImportsCount: document.getElementById('suspiciousImportsCount'),
suspiciousImportsSummary: document.getElementById('suspiciousImportsSummary'),
suspiciousImportsTitle: document.getElementById('suspiciousImportsTitle'),
};
let dragCounter = 0;
// Event Listeners
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
elements.dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
elements.dropZone.addEventListener('dragenter', () => {
dragCounter++;
if (dragCounter === 1) highlight();
});
elements.dropZone.addEventListener('dragleave', () => {
dragCounter--;
if (dragCounter === 0) unhighlight();
});
elements.dropZone.addEventListener('drop', (e) => {
dragCounter = 0;
unhighlight();
handleDrop(e);
});
elements.fileInput.addEventListener('change', handleFiles);
// Utility Functions
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight() {
const label = elements.dropZone.querySelector('.lb-upload-zone');
if (label) label.classList.add('lb-drag-over');
}
function unhighlight() {
const label = elements.dropZone.querySelector('.lb-upload-zone');
if (label) label.classList.remove('lb-drag-over');
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
const colors = {
success: 'border-green-500/25 bg-green-500/10 text-green-300',
error: 'border-red-500/25 bg-red-500/10 text-red-300',
info: 'border-blue-500/25 bg-blue-500/10 text-blue-300'
};
toast.className = `flex items-center space-x-2 p-4 rounded-lg border ${colors[type]} transform translate-y-2 opacity-0 transition-all duration-300 text-base`;
toast.innerHTML = `
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="${type === 'success' ? 'M5 13l4 4L19 7' : 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'}"/>
</svg>
<span>${message}</span>
`;
elements.toastContainer.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.remove('translate-y-2', 'opacity-0');
});
setTimeout(() => {
toast.classList.add('translate-y-2', 'opacity-0');
setTimeout(() => toast.remove(), UPLOAD_CONFIG.fadeDelay);
}, UPLOAD_CONFIG.toastDuration);
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function updateProgress(step, completed = false) {
const stepCircle = step === 1 ? elements.step1Circle : elements.step2Circle;
const stepText = step === 1 ? elements.step1Text : elements.step2Text;
if (completed && step === 1) {
stepCircle.classList.remove('bg-red-500/10', 'border-red-500', 'bg-red-500/8', 'border-red-500/40', 'bg-black/50', 'border-gray-700');
stepCircle.classList.add('bg-green-500/8', 'border-green-500/40');
stepText.innerHTML = `
<svg class="w-5 h-5 text-green-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
`;
elements.progressLine.classList.remove('to-gray-800');
elements.progressLine.classList.add('to-red-500/15');
elements.step2Circle.classList.remove('bg-black/50', 'border-gray-700');
elements.step2Circle.classList.add('bg-red-500/8', 'border-red-500/40');
elements.step2Text.classList.remove('text-gray-500');
elements.step2Text.classList.add('text-red-300');
}
}
// File type detection and UI updates.
//
// The analysis-mode selector is a single segmented control with one tab
// per mode (Static / Dynamic / each EDR profile / HolyGrail). Each tab
// is tagged data-family="regular" or "driver"; we only show the family
// matching the uploaded file. The first visible tab becomes active.
function updateAnalysisOptions(fileExtension) {
isDriverFile = fileExtension.toLowerCase() === 'sys';
const family = isDriverFile ? 'driver' : 'regular';
const tabs = document.querySelectorAll('#modeTabs .lb-tab');
const bodies = document.querySelectorAll('.lb-mode-body');
// Show only tabs for this file family; active state moves to first.
let firstVisible = null;
tabs.forEach(t => {
const matches = t.dataset.family === family;
t.classList.toggle('hidden', !matches);
t.classList.remove('active');
if (matches && !firstVisible) firstVisible = t;
});
if (firstVisible) {
firstVisible.classList.add('active');
// Hide all bodies, then show the one that matches.
bodies.forEach(b => b.classList.add('hidden'));
const target = document.querySelector(`.lb-mode-body[data-mode="${firstVisible.dataset.mode}"]`);
if (target) target.classList.remove('hidden');
}
}
// Wire tab clicks. Activating a tab swaps the active class and reveals
// the matching mode body.
document.getElementById('modeTabs').addEventListener('click', (e) => {
const tab = e.target.closest('.lb-tab');
if (!tab || tab.classList.contains('hidden')) return;
document.querySelectorAll('#modeTabs .lb-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.lb-mode-body').forEach(b => b.classList.add('hidden'));
const target = document.querySelector(`.lb-mode-body[data-mode="${tab.dataset.mode}"]`);
if (target) target.classList.remove('hidden');
});
function getDetectionRiskColor(risk) {
const colors = {
'High': 'bg-red-500/10 text-red-300 border border-red-500/25',
'Medium': 'bg-yellow-500/10 text-yellow-300 border border-yellow-500/25',
'Low': 'bg-green-500/10 text-green-300 border border-green-500/25'
};
return colors[risk] || colors['Low'];
}
// File Info Functions (all the existing functions remain the same)
function renderFileTypeSpecificInfo(fileInfo) {
elements.peInfo.classList.add('hidden');
elements.officeInfo.classList.add('hidden');
elements.suspiciousImports.classList.add('hidden');
if (fileInfo.entropy_analysis) {
const entropyPercentage = (fileInfo.entropy / 8) * 100;
elements.entropyBar.style.width = `${entropyPercentage}%`;
elements.entropyBar.className = `absolute h-full transition-all duration-300 ${
fileInfo.entropy_analysis.detection_risk === 'High' ? 'bg-red-400' :
fileInfo.entropy_analysis.detection_risk === 'Medium' ? 'bg-yellow-400' : 'bg-green-400'
}`;
elements.detectionRisk.className = `px-3 py-1 text-sm rounded-full ${
getDetectionRiskColor(fileInfo.entropy_analysis.detection_risk)
}`;
elements.detectionRisk.textContent = `${fileInfo.entropy_analysis.detection_risk} Detection Risk`;
elements.entropyNotes.innerHTML = fileInfo.entropy_analysis.notes.map(note => `
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>${note}</span>
</div>
`).join('');
}
if (fileInfo.pe_info) {
elements.peInfo.classList.remove('hidden');
const pe = fileInfo.pe_info;
if (pe.suspicious_imports && pe.suspicious_imports.length > 0) {
elements.suspiciousImports.classList.remove('hidden');
const buildWith = pe.build_with || null;
const runtimeConfig = getRuntimeConfig(buildWith);
const runtimeImports = pe.suspicious_imports.filter(imp => imp.is_runtime_import);
const genuinelySuspicious = pe.suspicious_imports.filter(imp => !imp.is_runtime_import);
elements.suspiciousImportsTitle.textContent = runtimeConfig.title;
elements.suspiciousImportsCount.className = `px-3 py-1 text-sm ${runtimeConfig.countClass} rounded-full`;
elements.suspiciousImportsCount.textContent = `${pe.suspicious_imports.length} Found${runtimeConfig.badge ? ` (${runtimeConfig.badge})` : ''}`;
elements.suspiciousImportsList.innerHTML = pe.suspicious_imports.map(imp => {
const isRuntimeImport = imp.is_runtime_import || false;
const config = isRuntimeImport ? runtimeConfig : getRuntimeConfig(null);
return `
<div class="border-b border-gray-800 last:border-b-0 pb-3">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="${config.dllColor} font-mono">${imp.dll}</span>
<span class="text-gray-400">→</span>
<span class="text-gray-300 font-mono">${imp.function}</span>
<span class="ml-2 px-2 py-0.5 text-xs ${config.categoryBg} ${config.categoryText} rounded-full">[${imp.category || 'Unknown'}]</span>
${isRuntimeImport ? `<span class="ml-2 px-2 py-0.5 text-xs ${config.badgeBg} ${config.badgeText} rounded-full">${runtimeConfig.badgeLabel}</span>` : ''}
</div>
${imp.hint !== null && imp.hint !== undefined ? `<span class="text-xs text-gray-500" title="Import hint: suggested index in DLL export table">Hint: ${imp.hint}</span>` : ''}
</div>
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 ${config.iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span class="text-sm text-gray-400">${imp.note}</span>
</div>
</div>
`;
}).join('');
updateImportsSummary(buildWith, runtimeImports.length, genuinelySuspicious.length);
}
if (pe.checksum_info) {
elements.checksumInfo.classList.remove('hidden');
elements.storedChecksum.textContent = pe.checksum_info.stored_checksum;
elements.calculatedChecksum.textContent = pe.checksum_info.calculated_checksum;
const buildWith = pe.checksum_info.build_with;
const isValid = pe.checksum_info.is_valid;
if (isValid) {
elements.checksumStatus.className = 'px-3 py-1 text-sm rounded-full bg-green-500/8 text-green-300 border border-green-500/22';
elements.checksumStatus.textContent = 'Valid';
} else if (buildWith) {
const runtimeConfig = getRuntimeConfig(buildWith);
elements.checksumStatus.className = `px-3 py-1 text-sm rounded-full ${runtimeConfig.checksumClass}`;
elements.checksumStatus.textContent = `${buildWith.charAt(0).toUpperCase() + buildWith.slice(1)} Binary`;
} else {
elements.checksumStatus.className = 'px-3 py-1 text-sm rounded-full bg-red-500/8 text-red-300 border border-red-500/22';
elements.checksumStatus.textContent = 'Invalid';
}
if (!pe.checksum_info.is_valid) {
const buildWith = pe.checksum_info.build_with;
const runtimeConfig = getRuntimeConfig(buildWith);
elements.checksumNotes.innerHTML = `
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 ${runtimeConfig.iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span>${runtimeConfig.checksumNote}</span>
</div>
`;
}
} else {
elements.checksumInfo.classList.add('hidden');
}
renderPEBasicInfo(pe);
}
else if (fileInfo.office_info) {
elements.officeInfo.classList.remove('hidden');
const office = fileInfo.office_info;
elements.macroStatus.className = `px-3 py-1 text-sm rounded-full ${
office.has_macros ? 'bg-red-500/8 text-red-300 border border-red-500/22' : 'bg-green-500/8 text-green-300 border border-green-500/22'
}`;
elements.macroStatus.textContent = office.has_macros ? 'Macros Present' : 'No Macros';
if (office.detection_notes && office.detection_notes.length > 0) {
elements.macroDetectionNotes.innerHTML = office.detection_notes.map(note => `
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 text-yellow-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span>${note}</span>
</div>
`).join('');
}
}
else if (fileInfo.lnk_info) {
// Show LNK-specific information section
const lnkInfoSection = document.getElementById('lnkInfo') || createLnkInfoSection();
lnkInfoSection.classList.remove('hidden');
renderLnkInfo(fileInfo.lnk_info);
}
}
function getRuntimeConfig(buildWith) {
const configs = {
'go': {
title: 'API Imports Analysis (Go Runtime)',
badge: 'Go Runtime',
badgeLabel: 'Go Runtime',
countClass: 'bg-blue-500/5 text-blue-300',
dllColor: 'text-blue-300',
categoryBg: 'bg-blue-500/10',
categoryText: 'text-blue-300',
badgeBg: 'bg-gray-500/10',
badgeText: 'text-gray-400',
iconColor: 'text-blue-400',
checksumClass: 'bg-blue-500/5 text-blue-300',
checksumNote: 'Go binaries typically have non-standard PE checksums - This is normal behavior'
},
'rust': {
title: 'API Imports Analysis (Rust Runtime)',
badge: 'Rust Runtime',
badgeLabel: 'Rust Runtime',
countClass: 'bg-purple-500/5 text-purple-300',
dllColor: 'text-purple-300',
categoryBg: 'bg-purple-500/10',
categoryText: 'text-purple-300',
badgeBg: 'bg-gray-500/10',
badgeText: 'text-gray-400',
iconColor: 'text-purple-400',
checksumClass: 'bg-purple-500/5 text-purple-300',
checksumNote: 'Rust binaries may have non-standard PE checksums - This is normal behavior'
}
};
const defaultConfig = {
title: 'Sensitive Imports Analysis',
badge: null,
badgeLabel: 'Sensitive',
countClass: 'bg-red-500/5 text-red-300',
dllColor: 'text-red-300',
categoryBg: 'bg-red-500/10',
categoryText: 'text-red-300',
badgeBg: 'bg-red-500/10',
badgeText: 'text-red-300',
iconColor: 'text-yellow-400',
checksumClass: 'bg-red-500/5 text-red-300',
checksumNote: 'Invalid checksum - Common in packed/modified payloads'
};
return configs[buildWith] || defaultConfig;
}
function renderPEBasicInfo(pe) {
const html = `
<div class="space-y-4">
<div class="flex items-center justify-between">
<h6 class="text-base font-medium text-gray-300">PE File Information</h6>
<span class="text-sm text-gray-400">File Type: ${pe.file_type}</span>
<span class="text-sm text-gray-400">Compile Time: ${pe.compile_time}</span>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<div class="text-base text-gray-400 mb-1">Machine Type</div>
<div class="text-base text-gray-300">${pe.machine_type}</div>
</div>
<div>
<div class="text-base text-gray-400 mb-1">Subsystem</div>
<div class="text-base text-gray-300">${pe.subsystem}</div>
</div>
<div>
<div class="text-base text-gray-400 mb-1">Entry Point</div>
<div class="text-base font-mono text-gray-300">${pe.entry_point}</div>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-base text-gray-400">PE Sections</span>
<span class="text-sm text-gray-400">${pe.sections.length} sections</span>
</div>
<div class="flex flex-wrap gap-2">
${pe.sections.map(section => {
const isStandardSection = ['.text', '.data', '.bss', '.rdata', '.edata', '.idata', '.pdata', '.reloc', '.rsrc', '.tls', '.debug'].includes(section.name);
return `
<span class="px-2 py-1 text-sm ${isStandardSection ? 'bg-gray-900/50 text-gray-400' : 'bg-red-500/8 text-red-300'} rounded-lg border ${isStandardSection ? 'border-gray-800' : 'border-red-500/22'}">
${section.name}
</span>
`;
}).join('')}
</div>
</div>
${pe.imports && pe.imports.length > 0 ? `
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-base text-gray-400">Imported DLLs</span>
<span class="text-sm text-gray-400">${pe.imports.length} imports</span>
</div>
<div class="flex flex-wrap gap-2">
${pe.imports.map(imp => `
<span class="px-2 py-1 text-sm bg-gray-900/50 rounded-lg border border-gray-800 text-gray-400">
${imp}
</span>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
elements.fileSpecificInfo.innerHTML = html;
}
function updateImportsSummary(buildWith, runtimeCount, suspiciousCount) {
let summaryText = '';
if (buildWith === 'go') {
if (suspiciousCount > 0) {
summaryText = `Go binary detected: ${runtimeCount} standard Go runtime imports and ${suspiciousCount} sensitive imports observed.`;
} else {
summaryText = `Go binary detected: ${runtimeCount} standard Go runtime imports — typically not user logic.`;
}
} else if (buildWith === 'rust') {
if (suspiciousCount > 0) {
summaryText = `Rust binary detected: ${runtimeCount} standard Rust runtime imports and ${suspiciousCount} sensitive imports observed.`;
} else {
summaryText = `Rust binary detected: ${runtimeCount} standard Rust runtime imports — typically not user logic.`;
}
} else {
const totalImports = runtimeCount + suspiciousCount;
summaryText = `${totalImports} sensitive imports observed — these are APIs commonly watched by AV/EDR.`;
}
elements.suspiciousImportsSummary.textContent = summaryText;
}
function updateFileInfo(fileInfo) {
currentFileHash = fileInfo.md5;
currentFileExtension = fileInfo.extension;
elements.fileName.textContent = fileInfo.original_name;
elements.fileSize.textContent = formatFileSize(fileInfo.size);
elements.fileType.textContent = fileInfo.extension.toUpperCase();
elements.fileFormat.textContent = fileInfo.mime_type;
elements.fileCategory.textContent = fileInfo.extension.toUpperCase();
elements.fileEntropy.textContent = fileInfo.entropy;
elements.uploadTime.textContent = formatTimestamp(fileInfo.upload_time);
elements.md5Hash.textContent = fileInfo.md5;
elements.sha256Hash.textContent = `${fileInfo.sha256.substring(0, 32)}...`;
document.getElementById('sha256HashFull').textContent = fileInfo.sha256;
localStorage.setItem('currentFileExtension', fileInfo.extension);
// Update analysis options based on file type
updateAnalysisOptions(fileInfo.extension);
renderFileTypeSpecificInfo(fileInfo);
elements.uploadArea.classList.add('opacity-0', 'scale-95');
setTimeout(() => {
elements.uploadArea.classList.add('hidden');
elements.fileAnalysisArea.classList.remove('hidden');
setTimeout(() => {
elements.fileAnalysisArea.classList.remove('opacity-0', 'scale-95');
}, UPLOAD_CONFIG.fadeDelay);
}, UPLOAD_CONFIG.transitionDelay);
updateProgress(1, true);
}
// File Handling Functions
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 1) {
showToast('Please upload only one file at a time', 'error');
return;
}
handleFiles({ target: { files } });
}
function handleFiles(e) {
const file = e.target.files[0];
if (!file) return;
const extension = file.name.split('.').pop().toLowerCase();
const allowedExtensions = (window.serverConfig && window.serverConfig.allowedExtensions) || [];
if (!allowedExtensions.includes(extension)) {
showToast(`Unsupported file type. Allowed types: ${allowedExtensions.map(e => '.' + e).join(', ')}`, 'error');
return;
}
if (file.size > UPLOAD_CONFIG.maxFileSize) {
showToast('File size exceeds limit', 'error');
return;
}
uploadFile(file);
}
function uploadFile(file) {
showToast('Uploading file...', 'info');
const formData = new FormData();
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) throw new Error(data.error);
showToast('File uploaded successfully', 'success');
if (data.file_info) updateFileInfo(data.file_info);
})
.catch(error => {
showToast(error.message, 'error');
});
}
// Global Functions
window.copyHash = function(elementId) {
const hashType = elementId === 'md5Hash' ? 'md5' : 'sha256';
const fullHash = document.getElementById(`${hashType}HashFull`).textContent;
navigator.clipboard.writeText(fullHash).then(() => {
showToast(`${hashType.toUpperCase()} hash copied to clipboard`, 'success');
});
}
window.selectAnalysisType = function(type) {
if (!currentFileHash) return;
updateProgress(2, true);
elements.fileAnalysisArea.classList.add('opacity-0', 'scale-95');
setTimeout(() => {
if (type === 'holygrail') {
// Show loading message
showToast('Starting HolyGrail BYOVD analysis...', 'info');
// Call HolyGrail analysis endpoint
fetch(`/holygrail?hash=${currentFileHash}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showToast('HolyGrail analysis completed successfully', 'success');
// Redirect to results page
setTimeout(() => {
window.location.href = `/results/byovd/${currentFileHash}`;
}, 1000);
} else {
// Handle error
showToast(`Analysis failed: ${data.error || data.message}`, 'error');
// Reset UI
elements.fileAnalysisArea.classList.remove('opacity-0', 'scale-95');
}
})
.catch(error => {
console.error('HolyGrail analysis error:', error);
showToast(`Analysis failed: ${error.message}`, 'error');
// Reset UI
elements.fileAnalysisArea.classList.remove('opacity-0', 'scale-95');
});
} else if (type === 'all') {
// "All" pipeline: static + (every registered EDR) in parallel,
// then dynamic. Args (if any) shared across dynamic + EDR.
const argsInput = document.getElementById('allAnalysisArgs');
const argsValue = argsInput ? argsInput.value : '';
const args = argsValue.split(' ').filter(arg => arg.trim() !== '');
localStorage.setItem('analysisArgs', JSON.stringify(args));
window.location.href = `/analyze/all/${currentFileHash}`;
} else if (type === 'dynamic') {
// Get user-specified arguments for dynamic analysis
const argsInput = document.getElementById('analysisArgs').value;
const args = argsInput.split(' ').filter(arg => arg.trim() !== '');
// Save arguments to localStorage
localStorage.setItem('analysisArgs', JSON.stringify(args));
// Navigate to dynamic analysis
window.location.href = `/analyze/${type}/${currentFileHash}`;
} else if (type.startsWith('edr:')) {
// EDR profile dispatch: type is "edr:<profile_name>".
// Each profile body has its own args input (id =
// edrArgs-<profile>); read it, persist to localStorage so
// the results page's POST forwards it to Whiskers.
const profile = type.slice(4);
const argsInput = document.getElementById(`edrArgs-${profile}`);
const argsValue = argsInput ? argsInput.value : '';
const args = argsValue.split(' ').filter(arg => arg.trim() !== '');
localStorage.setItem('analysisArgs', JSON.stringify(args));
window.location.href = `/analyze/edr/${encodeURIComponent(profile)}/${currentFileHash}`;
} else {
// Navigate to static analysis
window.location.href = `/analyze/${type}/${currentFileHash}`;
}
}, UPLOAD_CONFIG.transitionDelay);
};
});