244 lines
7.2 KiB
JavaScript
244 lines
7.2 KiB
JavaScript
let selectedFiles = [];
|
|
let scanResults = [];
|
|
let selectedIndex = -1;
|
|
|
|
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');
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
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...';
|
|
setStatus('Scanning...');
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
scanResults = data.results;
|
|
selectedIndex = -1;
|
|
renderTable();
|
|
setStatus(`Scan complete: ${data.count} file(s) analyzed`);
|
|
} catch (e) {
|
|
setStatus('Scan failed: ' + e.message);
|
|
} finally {
|
|
btnScan.disabled = false;
|
|
btnScan.textContent = 'Scan';
|
|
}
|
|
}
|
|
|
|
function renderTable() {
|
|
resultsBody.innerHTML = '';
|
|
emptyState.style.display = scanResults.length ? 'none' : 'block';
|
|
|
|
scanResults.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, '"');
|
|
}
|