progress bar + table sort

This commit is contained in:
Dauren777 2026-06-19 06:34:52 +00:00
parent 8ea4d37806
commit 9d011d993d
5 changed files with 235 additions and 37 deletions

67
app.py
View File

@ -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)

11
core.py
View File

@ -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
))

View File

@ -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;

View File

@ -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);

View File

@ -40,17 +40,24 @@
</div>
</section>
<div class="progress-bar-container" id="progressBar" style="display:none">
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progressFill"></div>
</div>
<span class="progress-text" id="progressText">0 / 0</span>
</div>
<div class="results-container">
<section class="table-section">
<div class="table-wrapper">
<table id="resultsTable">
<thead>
<tr>
<th>Level</th>
<th>Score</th>
<th>Type</th>
<th>Path</th>
<th>Author/User</th>
<th data-sort="level" class="sortable">Level</th>
<th data-sort="score" class="sortable">Score</th>
<th data-sort="type" class="sortable">Type</th>
<th data-sort="path" class="sortable">Path</th>
<th data-sort="author" class="sortable">Author/User</th>
</tr>
</thead>
<tbody id="resultsBody">