348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
let selectedFiles = [];
|
|
let scanResults = [];
|
|
let selectedIndex = -1;
|
|
let sortField = null;
|
|
let sortDir = 'asc';
|
|
|
|
const fileInput = document.getElementById('fileInput');
|
|
const uploadInfo = document.getElementById('uploadInfo');
|
|
const resultsBody = document.getElementById('resultsBody');
|
|
const detailsContent = document.getElementById('detailsContent');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const btnScan = document.getElementById('btnScan');
|
|
const btnSanitize = document.getElementById('btnSanitize');
|
|
const statusText = document.getElementById('statusText');
|
|
const dropZone = document.getElementById('dropZone');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressFill = document.getElementById('progressFill');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
fileInput.addEventListener('change', handleFileSelect);
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
if (e.dataTransfer.files.length) {
|
|
fileInput.files = e.dataTransfer.files;
|
|
handleFileSelect();
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('#resultsTable th.sortable').forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const field = th.dataset.sort;
|
|
if (sortField === field) {
|
|
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortField = field;
|
|
sortDir = 'asc';
|
|
}
|
|
document.querySelectorAll('#resultsTable th.sortable').forEach(h => {
|
|
h.classList.remove('asc', 'desc');
|
|
});
|
|
th.classList.add(sortDir);
|
|
renderTable();
|
|
});
|
|
});
|
|
|
|
function handleFileSelect() {
|
|
selectedFiles = Array.from(fileInput.files);
|
|
if (selectedFiles.length === 0) {
|
|
uploadInfo.textContent = 'No files selected';
|
|
} else if (selectedFiles.length === 1) {
|
|
uploadInfo.textContent = selectedFiles[0].name;
|
|
} else {
|
|
uploadInfo.textContent = `${selectedFiles.length} files selected`;
|
|
}
|
|
}
|
|
|
|
async function uploadFiles() {
|
|
if (!selectedFiles.length) return false;
|
|
|
|
setStatus('Uploading files...');
|
|
const formData = new FormData();
|
|
for (const file of selectedFiles) {
|
|
formData.append('files', file);
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
setStatus('Upload failed: ' + data.error);
|
|
return false;
|
|
}
|
|
setStatus(`Uploaded ${data.count} file(s)`);
|
|
return true;
|
|
} catch (e) {
|
|
setStatus('Upload failed: ' + e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function startScan() {
|
|
const context = document.getElementById('context').value;
|
|
const recursive = document.getElementById('recursive').checked;
|
|
|
|
const uploaded = await uploadFiles();
|
|
if (!uploaded && !selectedFiles.length) {
|
|
setStatus('No files to scan. Select files first.');
|
|
return;
|
|
}
|
|
|
|
btnScan.disabled = true;
|
|
btnScan.textContent = 'Scanning...';
|
|
progressBar.style.display = 'flex';
|
|
progressFill.style.width = '0%';
|
|
progressText.textContent = 'Starting...';
|
|
|
|
try {
|
|
const resp = await fetch('/api/scan', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ target: '', recursive, context })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
setStatus('Scan failed: ' + data.error);
|
|
btnScan.disabled = false;
|
|
btnScan.textContent = 'Scan';
|
|
progressBar.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
await pollProgress();
|
|
await fetchResults();
|
|
} catch (e) {
|
|
setStatus('Scan failed: ' + e.message);
|
|
btnScan.disabled = false;
|
|
btnScan.textContent = 'Scan';
|
|
progressBar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function pollProgress() {
|
|
let done = false;
|
|
while (!done) {
|
|
await new Promise(r => setTimeout(r, 300));
|
|
try {
|
|
const resp = await fetch('/api/scan/progress');
|
|
const p = await resp.json();
|
|
const pct = p.total > 0 ? Math.round((p.current / p.total) * 100) : 0;
|
|
progressFill.style.width = Math.min(pct, 100) + '%';
|
|
const name = p.current_file ? p.current_file.split('/').pop().split('\\').pop() : '';
|
|
progressText.textContent = name
|
|
? `${p.current} / ${p.total} - ${name}`
|
|
: `${p.current} / ${p.total}`;
|
|
|
|
if (p.status === 'completed' || p.status === 'error') {
|
|
progressFill.style.width = p.status === 'completed' ? '100%' : progressFill.style.width;
|
|
progressText.textContent = p.status === 'completed'
|
|
? `Complete - ${p.total} file(s)`
|
|
: 'Error: ' + (p.current_file || 'unknown');
|
|
done = true;
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
} catch (e) {
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fetchResults() {
|
|
try {
|
|
const resp = await fetch('/api/results');
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
setStatus('Failed to load results: ' + data.error);
|
|
} else {
|
|
scanResults = data.results;
|
|
selectedIndex = -1;
|
|
sortField = null;
|
|
sortDir = 'asc';
|
|
document.querySelectorAll('#resultsTable th.sortable').forEach(h => h.classList.remove('asc', 'desc'));
|
|
renderTable();
|
|
setStatus(`Scan complete: ${data.count} file(s) analyzed`);
|
|
}
|
|
} catch (e) {
|
|
setStatus('Failed to load results: ' + e.message);
|
|
} finally {
|
|
btnScan.disabled = false;
|
|
btnScan.textContent = 'Scan';
|
|
setTimeout(() => { progressBar.style.display = 'none'; }, 1000);
|
|
}
|
|
}
|
|
|
|
function getSortValue(r, field) {
|
|
switch (field) {
|
|
case 'level': {
|
|
const order = { 'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3 };
|
|
return order[r.level] ?? 99;
|
|
}
|
|
case 'score': return r.total_risk;
|
|
case 'type': return r.file_type || '';
|
|
case 'path': return r.path || '';
|
|
case 'author': return (r.metadata.author || r.metadata.last_modified_by || '').toLowerCase();
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
function renderTable() {
|
|
resultsBody.innerHTML = '';
|
|
emptyState.style.display = scanResults.length ? 'none' : 'block';
|
|
|
|
let items = scanResults.map((r, i) => ({ r, i }));
|
|
|
|
if (sortField) {
|
|
items.sort((a, b) => {
|
|
const va = getSortValue(a.r, sortField);
|
|
const vb = getSortValue(b.r, sortField);
|
|
if (typeof va === 'number' && typeof vb === 'number') {
|
|
return sortDir === 'asc' ? va - vb : vb - va;
|
|
}
|
|
const cmp = String(va).localeCompare(String(vb));
|
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
}
|
|
|
|
items.forEach(({ r, i }) => {
|
|
const tr = document.createElement('tr');
|
|
tr.dataset.index = i;
|
|
|
|
const author = r.metadata.author || r.metadata.last_modified_by || '';
|
|
|
|
tr.innerHTML = `
|
|
<td class="level-${r.level}">${r.level}</td>
|
|
<td class="level-${r.level}">${r.total_risk}</td>
|
|
<td>${r.file_type}</td>
|
|
<td class="path-cell" title="${escapeHtml(r.path)}">${escapeHtml(r.path)}</td>
|
|
<td>${escapeHtml(author)}</td>
|
|
`;
|
|
|
|
tr.addEventListener('click', () => selectRow(i));
|
|
resultsBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function selectRow(index) {
|
|
selectedIndex = index;
|
|
const r = scanResults[index];
|
|
|
|
document.querySelectorAll('#resultsBody tr').forEach(tr => {
|
|
tr.classList.toggle('selected', parseInt(tr.dataset.index) === index);
|
|
});
|
|
|
|
btnSanitize.disabled = r.file_type !== 'image';
|
|
|
|
let html = '';
|
|
html += `<span class="level-${r.level}"><b>${r.level}</b> (Score: ${r.total_risk})</span>\n\n`;
|
|
html += `<b>Path:</b> ${escapeHtml(r.path)}\n`;
|
|
html += `<b>Type:</b> ${r.file_type}\n`;
|
|
html += `<b>Size:</b> ${formatSize(r.size_bytes)}\n`;
|
|
html += `<b>SHA-256:</b> ${r.sha256}\n`;
|
|
html += `<b>Context:</b> ${r.context}\n\n`;
|
|
|
|
html += `<b>Metadata:</b>\n`;
|
|
for (const [k, v] of Object.entries(r.metadata)) {
|
|
if (k === 'all_exif_tags') continue;
|
|
if (typeof v === 'object' && v !== null) {
|
|
html += ` ${k}: ${JSON.stringify(v)}\n`;
|
|
} else {
|
|
html += ` ${k}: ${v}\n`;
|
|
}
|
|
}
|
|
|
|
if (r.findings && r.findings.length) {
|
|
html += `\n<b>Findings:</b>\n`;
|
|
r.findings.forEach(f => {
|
|
html += ` <span class="finding-${f.severity}">${f.severity}</span> | ${f.name} | ${f.evidence}\n`;
|
|
});
|
|
}
|
|
|
|
detailsContent.innerHTML = html;
|
|
}
|
|
|
|
async function exportReport(fmt) {
|
|
if (!scanResults.length) {
|
|
setStatus('No results to export. Run scan first.');
|
|
return;
|
|
}
|
|
|
|
setStatus(`Exporting ${fmt.toUpperCase()}...`);
|
|
|
|
try {
|
|
const resp = await fetch(`/api/export/${fmt}`);
|
|
if (!resp.ok) {
|
|
const data = await resp.json();
|
|
setStatus('Export failed: ' + (data.error || resp.statusText));
|
|
return;
|
|
}
|
|
|
|
const blob = await resp.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `report.${fmt}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
setStatus(`Exported ${fmt.toUpperCase()} successfully`);
|
|
} catch (e) {
|
|
setStatus('Export failed: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function sanitizeFile() {
|
|
if (selectedIndex < 0) {
|
|
setStatus('Select a file first.');
|
|
return;
|
|
}
|
|
|
|
const r = scanResults[selectedIndex];
|
|
if (r.file_type !== 'image') {
|
|
setStatus('Sanitization supported only for image files.');
|
|
return;
|
|
}
|
|
|
|
setStatus('Sanitizing...');
|
|
|
|
try {
|
|
const resp = await fetch(`/api/sanitize/${selectedIndex}`, { method: 'POST' });
|
|
const data = await resp.json();
|
|
|
|
if (data.error) {
|
|
setStatus('Sanitization failed: ' + data.error);
|
|
} else {
|
|
setStatus(`Sanitized copy created: ${data.sanitized_path}`);
|
|
}
|
|
} catch (e) {
|
|
setStatus('Sanitization failed: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function setStatus(msg) {
|
|
statusText.textContent = msg;
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
if (!s) return '';
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|