From 9d011d993d2ee85bab64160d109aa6061b025449 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 19 Jun 2026 06:34:52 +0000 Subject: [PATCH] progress bar + table sort --- app.py | 67 ++++++++++++++++-------- core.py | 11 ++-- static/script.js | 118 ++++++++++++++++++++++++++++++++++++++++--- static/style.css | 59 ++++++++++++++++++++++ templates/index.html | 17 +++++-- 5 files changed, 235 insertions(+), 37 deletions(-) diff --git a/app.py b/app.py index 7b5d127..3af5fcf 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ import os import sys import shutil import tempfile +import threading from dataclasses import asdict from typing import List @@ -21,6 +22,14 @@ os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) results_store: List[ScanResult] = [] +scan_progress = { + "status": "idle", + "total": 0, + "current": 0, + "current_file": "", +} +scan_lock = threading.Lock() + @app.route("/") def index(): @@ -59,27 +68,43 @@ def scan(): else: return jsonify({"error": "No files to scan. Upload files first."}), 400 - try: - results = scan_target(target, recursive, context) - global results_store - results_store = results + with scan_lock: + scan_progress["status"] = "scanning" + scan_progress["total"] = 0 + scan_progress["current"] = 0 + scan_progress["current_file"] = "" - out = [] - for r in results: - out.append({ - "path": r.path, - "file_type": r.file_type, - "size_bytes": r.size_bytes, - "sha256": r.sha256, - "metadata": r.metadata, - "context": r.context, - "total_risk": r.total_risk, - "level": r.level, - "findings": [{"name": f.name, "severity": f.severity, "evidence": f.evidence} for f in r.findings], - }) - return jsonify({"results": out, "count": len(out)}) - except Exception as e: - return jsonify({"error": str(e)}), 500 + def progress_callback(current, total, filepath): + with scan_lock: + scan_progress["current"] = current + scan_progress["total"] = total + scan_progress["current_file"] = filepath + + def run_scan(): + global results_store + try: + results = scan_target(target, recursive, context, progress_callback=progress_callback) + results_store = results + with scan_lock: + scan_progress["status"] = "completed" + scan_progress["current_file"] = "" + except Exception as e: + with scan_lock: + scan_progress["status"] = "error" + scan_progress["current_file"] = str(e) + + thread = threading.Thread(target=run_scan) + thread.daemon = True + thread.start() + + return jsonify({"status": "started"}) + + +@app.route("/api/scan/progress") +def get_scan_progress(): + with scan_lock: + p = dict(scan_progress) + return jsonify(p) @app.route("/api/results") @@ -177,4 +202,4 @@ if __name__ == "__main__": port = int(os.environ.get("PORT", 5000)) debug = os.environ.get("DEBUG", "1") == "1" print(f"MetaGuardSOT starting on http://127.0.0.1:{port}") - app.run(host="0.0.0.0", port=port, debug=debug) + app.run(host="0.0.0.0", port=port, debug=debug, threaded=True) diff --git a/core.py b/core.py index 960cf98..79a2828 100644 --- a/core.py +++ b/core.py @@ -394,17 +394,20 @@ def iter_files(target: str, recursive: bool) -> List[str]: return res -def scan_target(target: str, recursive: bool, context: str) -> List[ScanResult]: +def scan_target(target: str, recursive: bool, context: str, progress_callback=None) -> List[ScanResult]: files = iter_files(target, recursive) results: List[ScanResult] = [] - for p in files: + total = len(files) + for i, p in enumerate(files): + if progress_callback: + progress_callback(i + 1, total, p) ftype = detect_file_type(p) size = os.path.getsize(p) if os.path.exists(p) else -1 sha = sha256_file(p) raw = extract_by_type(p, ftype) norm = normalize_metadata(ftype, raw) - total, level, findings = evaluate_risk(norm, context) + total_risk, level, findings = evaluate_risk(norm, context) results.append(ScanResult( path=p, @@ -413,7 +416,7 @@ def scan_target(target: str, recursive: bool, context: str) -> List[ScanResult]: sha256=sha, metadata=norm, context=context, - total_risk=total, + total_risk=total_risk, level=level, findings=findings )) diff --git a/static/script.js b/static/script.js index 7c5210a..2e5a9a2 100644 --- a/static/script.js +++ b/static/script.js @@ -1,6 +1,8 @@ let selectedFiles = []; let scanResults = []; let selectedIndex = -1; +let sortField = null; +let sortDir = 'asc'; const fileInput = document.getElementById('fileInput'); const uploadInfo = document.getElementById('uploadInfo'); @@ -11,6 +13,9 @@ 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); @@ -32,6 +37,23 @@ dropZone.addEventListener('drop', (e) => { } }); +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) { @@ -79,7 +101,9 @@ async function startScan() { btnScan.disabled = true; btnScan.textContent = 'Scanning...'; - setStatus('Scanning...'); + progressBar.style.display = 'flex'; + progressFill.style.width = '0%'; + progressText.textContent = 'Starting...'; try { const resp = await fetch('/api/scan', { @@ -88,21 +112,87 @@ async function startScan() { 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; } - scanResults = data.results; - selectedIndex = -1; - renderTable(); - setStatus(`Scan complete: ${data.count} file(s) analyzed`); + 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 ''; } } @@ -110,7 +200,21 @@ function renderTable() { resultsBody.innerHTML = ''; emptyState.style.display = scanResults.length ? 'none' : 'block'; - scanResults.forEach((r, i) => { + 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; diff --git a/static/style.css b/static/style.css index bf8d732..f9329fa 100644 --- a/static/style.css +++ b/static/style.css @@ -90,6 +90,40 @@ main { cursor: pointer; } +.progress-bar-container { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 16px; + display: flex; + align-items: center; + gap: 12px; +} + +.progress-bar-track { + flex: 1; + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + width: 0%; + background: var(--accent); + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-text { + color: var(--text-secondary); + font-size: 12px; + white-space: nowrap; + min-width: 100px; + text-align: right; +} + .checkbox-label input[type="checkbox"] { accent-color: var(--accent); } @@ -229,6 +263,31 @@ th { white-space: nowrap; } +th.sortable { + cursor: pointer; + user-select: none; +} + +th.sortable:hover { + background: #1f2630; +} + +th.sortable::after { + content: ' \25B4\25BE'; + font-size: 10px; + opacity: 0.3; +} + +th.sortable.asc::after { + content: ' \25B4'; + opacity: 0.8; +} + +th.sortable.desc::after { + content: ' \25BE'; + opacity: 0.8; +} + td { padding: 8px 12px; border-bottom: 1px solid var(--gridline); diff --git a/templates/index.html b/templates/index.html index cb059bc..3f7ed79 100644 --- a/templates/index.html +++ b/templates/index.html @@ -40,17 +40,24 @@ + +
- - - - - + + + + +
LevelScoreTypePathAuthor/UserLevelScoreTypePathAuthor/User