Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions frontend/Assets/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
const API = '/api';

// ── UNC / host helpers ─────────────────────────────────────────────────────
function parseUncHost(path) {
return path.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
}

function parseHostPort(str) {
const ci = str.lastIndexOf(':');
if (ci > 0) {
const p = parseInt(str.slice(ci + 1), 10);
if (p > 0 && p < 65536) return { host: str.slice(0, ci), port: p };
}
return { host: str, port: 445 };
}

let findings = [];
let filteredFindings = []; // filtered + sorted subset rendered by virtual scroll
let activeFilter = 'ALL';
Expand All @@ -17,7 +31,7 @@
// Pre-fill host from current scanPath if it looks like a UNC path
const path = document.getElementById('scanPath').value.trim();
if ((path.startsWith('\\\\') || path.startsWith('//')) && !document.getElementById('smbHost').value) {
const host = path.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
const host = parseUncHost(path);
if (host) document.getElementById('smbHost').value = host;
}
}
Expand All @@ -32,7 +46,7 @@
const v = document.getElementById('scanPath').value;
const isUnc = v.startsWith('\\\\') || v.startsWith('//');
if (isUnc) {
const host = v.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
const host = parseUncHost(v);
const hostEl = document.getElementById('smbHost');
if (host && !hostEl.value) hostEl.value = host;
}
Expand All @@ -43,7 +57,7 @@
let raw = document.getElementById('smbHost').value.trim();
// If user pasted a full UNC path, extract just the host
if (raw.startsWith('\\\\') || raw.startsWith('//')) {
raw = raw.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
raw = parseUncHost(raw);
document.getElementById('smbHost').value = raw;
}
document.getElementById('sharesList').innerHTML = '';
Expand Down Expand Up @@ -76,99 +90,94 @@
}

// ── Start scan ─────────────────────────────────────────────────────────────
async function startScan() {
const scanPath = document.getElementById('scanPath').value.trim();
const maxFileSizeMB = parseInt(document.getElementById('maxFileSizeMB').value) || 10;
const workers = Math.max(1, Math.min(16, parseInt(document.getElementById('workerCount').value) || 8));
const resume = document.getElementById('resumeScan')?.checked || false;
const username = document.getElementById('smbUsername')?.value.trim() || '';
const password = document.getElementById('smbPassword')?.value || '';
const domain = document.getElementById('smbDomain')?.value.trim() || '';

// Parse port from the Server/Host field (e.g. "127.0.0.1:4445")
let smbPort = 445;
const rawHost = document.getElementById('smbHost')?.value.trim() || '';
const ci = rawHost.lastIndexOf(':');
if (ci > 0) {
const p = parseInt(rawHost.slice(ci + 1), 10);
if (p > 0 && p < 65536) smbPort = p;
}
const { port: smbPort } = parseHostPort(rawHost);

if (!scanPath) { alert('Please enter a file share path.'); return; }

// Reset
findings = [];
filteredFindings = [];
riskCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
document.getElementById('tableBody').innerHTML = '';
document.getElementById('vsTopSpacer').style.height = '0';
document.getElementById('vsBotSpacer').style.height = '0';
document.getElementById('logWrap').innerHTML = '';
document.getElementById('statScanned').textContent = '0';
document.getElementById('statFindings').textContent = '0';
document.getElementById('findingsBadge').style.display = 'none';
document.getElementById('emptyState').style.display = 'block';
document.getElementById('findingsTable').style.display = 'none';
updateRiskBars();
setStatus('scanning');

let response;
try {
response = await fetch(`${API}/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scanPath, maxFileSizeMB, workers, resume, username, password, domain, smbPort }),
});
} catch (e) {
alert('Cannot reach backend. Is it running?\n' + e.message);
setStatus('idle');
return;
}

if (!response.ok) {
try {
const err = await response.json();
alert(err.error || 'Scan failed.');
} catch (_) { alert('Scan failed.'); }
setStatus('idle');
return;
}

// Read the SSE stream from the POST response body
activeReader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

try {
while (true) {
const { done, value } = await activeReader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';

for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data: ')) continue;
try {
const data = JSON.parse(trimmed.slice(6));
handleStreamEvent(data);
} catch (_) {}
}
}
} catch (e) {
if (e.name !== 'AbortError') {
appendLog('Stream error: ' + e.message, true);
}
}

setStatus('done');
activeReader = null;
document.getElementById('progressFill').classList.remove('indeterminate');
document.getElementById('progressFill').style.width = '100%';
document.getElementById('progressText').textContent = 'Scan complete';
}

Check notice on line 180 in frontend/Assets/app.js

View check run for this annotation

codefactor.io / CodeFactor

frontend/Assets/app.js#L93-L180

Complex Method

// ── Handle individual SSE events ───────────────────────────────────────────
function handleStreamEvent(d) {
Expand Down Expand Up @@ -225,21 +234,13 @@
// ── SMB: Discover shares ───────────────────────────────────────────────────
async function discoverShares() {
let raw = document.getElementById('smbHost').value.trim()
|| (() => {
const p = document.getElementById('scanPath').value.trim();
return p.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
})();
|| parseUncHost(document.getElementById('scanPath').value.trim());
// Normalise if someone pasted a full UNC path into the host field
if (raw.startsWith('\\\\') || raw.startsWith('//')) {
raw = raw.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0];
raw = parseUncHost(raw);
}
// Parse optional :port suffix (e.g. 127.0.0.1:4445)
let host = raw, port = 445;
const colonIdx = raw.lastIndexOf(':');
if (colonIdx > 0) {
const maybePort = parseInt(raw.slice(colonIdx + 1), 10);
if (maybePort > 0 && maybePort < 65536) { port = maybePort; host = raw.slice(0, colonIdx); }
}
const { host, port } = parseHostPort(raw);
if (!host) { alert('Enter a server hostname or IP first.'); return; }

const btn = document.getElementById('discoverBtn');
Expand Down
82 changes: 15 additions & 67 deletions leaklens.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
import os
import queue
import re
import sqlite3
import threading

__version__ = "1.1.0"

from flask import Flask, Response, jsonify, request, send_from_directory, stream_with_context
from scanner.engine import scan_path
from scanner import db as _db

logging.basicConfig(
level=logging.WARNING,
Expand Down Expand Up @@ -72,14 +73,14 @@
return jsonify({"error": "A scan is already running."}), 409

data = request.get_json(silent=True) or {}
scan_path = data.get("scanPath", "").strip()
scan_path_val = data.get("scanPath", "").strip()
max_file_size_mb = int(data.get("maxFileSizeMB", 10))
workers = max(1, min(16, int(data.get("workers", 8))))
resume = bool(data.get("resume", False))
smb_port = max(1, min(65535, int(data.get("smbPort", 445))))
username, password, domain = _extract_smb_creds(data)

if not scan_path:
if not scan_path_val:
return jsonify({"error": "scanPath is required."}), 400

max_size = max_file_size_mb * 1024 * 1024
Expand All @@ -93,9 +94,8 @@

def run():
try:
from scanner.engine import scan_path as do_scan
for event in do_scan(
scan_path,
for event in scan_path(
scan_path_val,
max_size,
stop_event,
username=username,
Expand Down Expand Up @@ -194,24 +194,7 @@
@app.route("/api/scans", methods=["GET"])
def list_scans():
"""Return metadata for all completed/in-progress SQLite scan databases."""
if not os.path.isdir(REPORTS_DIR):
return jsonify([])
scans = []
for fname in os.listdir(REPORTS_DIR):
if not fname.endswith(".db"):
continue
db_path = os.path.join(REPORTS_DIR, fname)
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
row = conn.execute("SELECT * FROM scans LIMIT 1").fetchone()
if row:
scans.append(dict(row))
conn.close()
except Exception as e:
_log.debug("Could not read scan DB %s: %s", db_path, e)
scans.sort(key=lambda s: s.get("started_at", 0), reverse=True)
return jsonify(scans)
return jsonify(_db.get_all_scans(REPORTS_DIR))


# ─── GET /api/findings ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -248,34 +231,7 @@
return jsonify({"error": "Scan not found"}), 404

try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row

conditions = ["scan_id = ?"]
params: list = [scan_id]

if risk != "ALL":
conditions.append("risk_level = ?")
params.append(risk)

if search:
conditions.append("(file_name LIKE ? OR full_path LIKE ?)")
params.extend([f"%{search}%", f"%{search}%"])

where = " AND ".join(conditions)

total = conn.execute(
f"SELECT COUNT(*) FROM findings WHERE {where}", params
).fetchone()[0]

rows = conn.execute(
f"SELECT data FROM findings WHERE {where} ORDER BY id LIMIT ? OFFSET ?",
params + [per_page, page * per_page],
).fetchall()

findings = [json.loads(r["data"]) for r in rows]
conn.close()

total, findings = _db.query_findings(db_path, scan_id, page, per_page, risk, search)
return jsonify({
"scan_id": scan_id,
"total": total,
Expand All @@ -301,24 +257,16 @@
return jsonify({"error": "Scan not found"}), 404

try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
scan_row = conn.execute("SELECT * FROM scans WHERE id = ?", (scan_id,)).fetchone()
if not scan_row:
conn.close()
scan_meta = _db.get_scan_meta(db_path, scan_id)
if not scan_meta:
return jsonify({"error": "Scan not found"}), 404
rows = conn.execute(
"SELECT data FROM findings WHERE scan_id = ? ORDER BY id",
(scan_id,),
).fetchall()
findings = [json.loads(r["data"]) for r in rows]
conn.close()
findings = _db.get_all_findings(db_path, scan_id)
return jsonify({
"scan_id": scan_id,
"scan_path": scan_row["scan_path"],
"scan_date": scan_row["scan_date"],
"scanned": scan_row["scanned"],
"hits": scan_row["hits"],
"scan_path": scan_meta["scan_path"],
"scan_date": scan_meta["scan_date"],
"scanned": scan_meta["scanned"],

Check notice on line 268 in leaklens.py

View check run for this annotation

codefactor.io / CodeFactor

leaklens.py#L268

Multiple spaces after ':'. (E241)
"hits": scan_meta["hits"],

Check notice on line 269 in leaklens.py

View check run for this annotation

codefactor.io / CodeFactor

leaklens.py#L269

Multiple spaces after ':'. (E241)
"findings": findings,
})
except Exception:
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
flask==3.1.3
smbprotocol==1.16.0
PyYAML==6.0.2
Loading