feat: Implement SQLite schema and migrations for intrusion detection system
- Added schema definition and migration logic in app/db/schema.py. - Created tables for log sources, requests, IP stats, scans, blocklist, whitelist, and firewall rules. - Introduced functions to apply migrations and ensure database integrity. feat: Enhance scan functionality with database integration - Added _sync_blocklist_to_db function to synchronize blocklist with SQLite. - Updated run_scan function to handle database connections and scan records. - Implemented error handling for database operations during scans. feat: Introduce log analysis page and API endpoints - Created analysis.html template for displaying log analysis results. - Added routes for fetching top IPs, IP request timelines, and cross-source IPs. - Integrated analysis data into the dashboard and status endpoints. fix: Ensure whitelists are synchronized with the database - Implemented sync_whitelists_to_db function to persist in-memory whitelists. - Updated constants.py to define the path for the SQLite database. refactor: Modularize analysis helpers for better maintainability - Created _analysis_helpers function to encapsulate SQLite query logic. - Updated routes.py to utilize new analysis helper functions for API responses.
This commit is contained in:
1 parent
0137264fe4
commit
6036fa8b1e
17 files changed
+1292
-5
No files matched your search
@@ -131,6 +131,41 @@ def _sync_firewall(dry_run: bool):
|
||||
return _with_router(_sync)
|
||||
|
||||
|
||||
def _analysis_helpers():
|
||||
"""SQLite-backed log-analysis query helpers (None when DB unavailable).
|
||||
|
||||
Functions take the shared DB connection each call so routes stay stateless
|
||||
across the background scan thread.
|
||||
"""
|
||||
try:
|
||||
from ..db import get_connection
|
||||
from ..db import repositories as repo
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def top_ips(since: str | None = None, limit: int = 50) -> list:
|
||||
return repo.top_ips(get_connection(), since=since, limit=limit)
|
||||
|
||||
def ip_timeline(ip: str) -> list:
|
||||
return repo.ip_timeline(get_connection(), ip=ip)
|
||||
|
||||
def cross_source(limit: int = 100) -> list:
|
||||
return repo.correlate_across_sources(get_connection(), limit=limit)
|
||||
|
||||
def conn():
|
||||
return get_connection()
|
||||
|
||||
def request_count(conn) -> int:
|
||||
return repo.count_requests(conn)
|
||||
|
||||
def scan_count(conn) -> int:
|
||||
return repo.count_scans(conn)
|
||||
|
||||
return {"top_ips": top_ips, "ip_timeline": ip_timeline,
|
||||
"cross_source": cross_source, "conn": conn,
|
||||
"request_count": request_count, "scan_count": scan_count}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# app factory
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -155,6 +190,7 @@ def create_app() -> Flask:
|
||||
"_params_from_request": _params_from_request,
|
||||
"_with_router": _with_router,
|
||||
"_sync_firewall": _sync_firewall,
|
||||
"_analysis": _analysis_helpers(),
|
||||
}
|
||||
register_routes(app, REPORT_SOURCES, SCAN_KINDS, web_scan, helpers)
|
||||
return app
|
||||
|
||||
@@ -32,6 +32,40 @@ def register_routes(app: Flask,
|
||||
_with_router = helpers["_with_router"]
|
||||
_sync_firewall = helpers["_sync_firewall"]
|
||||
|
||||
# --- log analysis (SQLite-backed) --------------------------------------
|
||||
|
||||
_analysis = helpers.get("_analysis")
|
||||
|
||||
@app.get("/analysis")
|
||||
def analysis_page():
|
||||
return render_template("analysis.html")
|
||||
|
||||
@app.get("/api/analysis/top-ips")
|
||||
def analysis_top_ips():
|
||||
if _analysis is None:
|
||||
return jsonify({"error": "analysis unavailable"}), 503
|
||||
since = (request.args.get("since") or "").strip() or None
|
||||
limit = request.args.get("limit", 50, type=int)
|
||||
return jsonify({"top_ips": _analysis["top_ips"](since=since, limit=limit)})
|
||||
|
||||
@app.get("/api/analysis/ip/<ip>")
|
||||
def analysis_ip(ip):
|
||||
if _analysis is None:
|
||||
return jsonify({"error": "analysis unavailable"}), 503
|
||||
return jsonify({"ip": ip,
|
||||
"timeline": _analysis["ip_timeline"](ip)})
|
||||
|
||||
@app.get("/api/analysis/cross-source")
|
||||
def analysis_cross_source():
|
||||
if _analysis is None:
|
||||
return jsonify({"error": "analysis unavailable"}), 503
|
||||
limit = request.args.get("limit", 100, type=int)
|
||||
return jsonify({"ips": _analysis["cross_source"](limit=limit)})
|
||||
|
||||
@app.get("/api/analysis/manifest")
|
||||
def analysis_manifest():
|
||||
return jsonify({"ok": _analysis is not None})
|
||||
|
||||
# --- pages -------------------------------------------------------------
|
||||
|
||||
@app.get("/")
|
||||
@@ -68,6 +102,17 @@ def register_routes(app: Flask,
|
||||
def status():
|
||||
v4_path = os.path.join(APP_REPORT_DATA_DIR, "blocklist_ipv4.txt")
|
||||
v6_path = os.path.join(APP_REPORT_DATA_DIR, "blocklist_ipv6.txt")
|
||||
analysis = {}
|
||||
if _analysis is not None:
|
||||
try:
|
||||
conn = _analysis["conn"]()
|
||||
analysis = {
|
||||
"available": True,
|
||||
"request_count": _analysis["request_count"](conn),
|
||||
"scan_count": _analysis["scan_count"](conn),
|
||||
}
|
||||
except Exception:
|
||||
analysis = {"available": False}
|
||||
return jsonify({
|
||||
"host": WEB_HOST,
|
||||
"port": WEB_PORT,
|
||||
@@ -79,6 +124,7 @@ def register_routes(app: Flask,
|
||||
"exists": os.path.isdir(MIKROTIK_DATA_DIR),
|
||||
"path": MIKROTIK_DATA_DIR,
|
||||
},
|
||||
"analysis": analysis,
|
||||
})
|
||||
|
||||
# --- scans -------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user