Compare commits
5 Commits
b6d42beff8
...
41704c7ba2
| Author | SHA1 | Date | |
|---|---|---|---|
| 41704c7ba2 | |||
| 21b23b738b | |||
| 9d011d993d | |||
| 8ea4d37806 | |||
| edf8567419 |
12
README.md
12
README.md
@ -1,2 +1,14 @@
|
||||
# MetaGuardSOT
|
||||
|
||||
Как запустить на Windows:
|
||||
|
||||
1. **https://winpython.github.io/** → WinPython portable → распаковать
|
||||
2. **https://git.vibe42.kz/sot/MetaGuardSOT** → Download ZIP → распаковать
|
||||
3. В папке проекта (cmd):
|
||||
```
|
||||
WinPython\python.exe -m pip install -r requirements.txt
|
||||
WinPython\python.exe app.py
|
||||
```
|
||||
4. Браузер: http://127.0.0.1:5000
|
||||
|
||||
Если установлен Python — просто запустить `run.bat`.
|
||||
|
||||
BIN
__pycache__/core.cpython-312.pyc
Normal file
BIN
__pycache__/core.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/reporters.cpython-312.pyc
Normal file
BIN
__pycache__/reporters.cpython-312.pyc
Normal file
Binary file not shown.
67
app.py
67
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)
|
||||
|
||||
11
core.py
11
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
|
||||
))
|
||||
|
||||
42
run.bat
Normal file
42
run.bat
Normal file
@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title MetaGuardSOT
|
||||
|
||||
echo ============================================
|
||||
echo MetaGuardSOT - Metadata Risk Scanner
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
where python >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ОШИБКА] Python не найден!
|
||||
echo.
|
||||
echo Скачай и установи Python с сайта:
|
||||
echo https://www.python.org/downloads/
|
||||
echo.
|
||||
echo ВАЖНО: при установке поставь галочку "Add Python to PATH"
|
||||
echo.
|
||||
echo После установки перезапусти этот файл.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/3] Устанавливаю зависимости...
|
||||
python -m pip install -r requirements.txt
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ОШИБКА] Не удалось установить зависимости.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo OK
|
||||
|
||||
echo [2/3] Запускаю сервер...
|
||||
echo.
|
||||
echo Открой в браузере: http://127.0.0.1:5000
|
||||
echo Закрой это окно, чтобы остановить сервер.
|
||||
echo.
|
||||
start http://127.0.0.1:5000
|
||||
python app.py
|
||||
|
||||
pause
|
||||
118
static/script.js
118
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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
0
uploads/.gitkeep
Normal file
0
uploads/.gitkeep
Normal file
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
venv/bin/python3.12
Symbolic link
1
venv/bin/python3.12
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
venv/lib64
Symbolic link
1
venv/lib64
Symbolic link
@ -0,0 +1 @@
|
||||
lib
|
||||
5
venv/pyvenv.cfg
Normal file
5
venv/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /srv/opencode/workspaces/users/sot/MetaGuardSOT/venv
|
||||
Loading…
Reference in New Issue
Block a user