MetaGuardSOT/static/script.js

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}