Files
intrusion-detection/app/web/__init__.py
T
zwitschi 9f8ab0f61c feat: add WHOIS subnet querying and IP consolidation utility
- Implemented a new utility for querying WHOIS information and consolidating IP addresses by subnet.
- Added caching mechanism for WHOIS responses to improve performance.
- Introduced functions for loading and saving the WHOIS cache, querying subnets, and consolidating IPs based on a threshold.

feat: create Flask web UI for intrusion detection system

- Developed a Flask web application to manage the intrusion detection system.
- Implemented routes for scanning, reports, blocklist management, and firewall configuration.
- Added helper functions for reading files and handling requests.

feat: define routes for web UI

- Registered routes for various web pages and API endpoints in the Flask application.
- Implemented functionality for scanning, reporting, blocklist management, and firewall state retrieval.

feat: implement background scan runner for web UI

- Created a background scan runner to handle scan jobs in a separate thread.
- Managed an in-memory job store for tracking scan jobs and their statuses.
- Added persistence for scan jobs to maintain state across application restarts.

chore: update start script for Flask application

- Enhanced the PowerShell start script to check for the existence of the virtual environment.
- Added comments and structured the script for better readability and usability.
2026-08-08 14:33:51 +02:00

172 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Flask web UI for the intrusion-detection system.
A localhost-only web server (``127.0.0.1:<high port>``) for running scans,
browsing reports, and managing the MikroTik blocklist / firewall.
Run directly: ``python -m app.web``
Or via the entry point: ``python app.py --web`` (see ``serve()``).
"""
from __future__ import annotations
import json
import os
from flask import Flask, jsonify, request
from ..utils.constants import (
APP_REPORT_DATA_DIR,
APP_REPORT_DIR,
COPYPARTY_LOG_PATH,
COPYPARTY_REPORT_DIR,
DEFAULT_ERROR_RATIO,
DEFAULT_MIN_DENIED,
DEFAULT_MIN_ERRORS,
DEFAULT_TOP,
MIKROTIK_REPORT_DIR,
NPM_LOG_PATH,
NPM_REPORT_DIR,
REPORT_DIR,
WEB_HOST,
WEB_PORT,
)
from ..firewall import MikrotikFirewall
from ..scan import ScanParams
from . import web_scan
#: report sources exposed by the API -> report directory
REPORT_SOURCES = {
"copyparty": COPYPARTY_REPORT_DIR,
"npm": NPM_REPORT_DIR,
"app": APP_REPORT_DIR,
"mikrotik": MIKROTIK_REPORT_DIR,
}
SCAN_KINDS = ("copyparty", "npm", "consolidated")
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def _count_lines(path: str) -> int | None:
"""Return the line count of a file, or None if it cannot be read."""
try:
with open(path, "r", encoding="utf-8") as f:
return sum(1 for _ in f)
except OSError:
return None
def _read_lines(path: str) -> list[str]:
"""Return non-empty lines of a file, or [] if it cannot be read."""
try:
with open(path, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip()]
except OSError:
return []
def _read_json(path: str, default: list | dict | None = None) -> list | dict:
"""Read a JSON file, returning ``default`` (or []) on failure."""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (OSError, ValueError):
return default if default is not None else []
def _params_from_request() -> ScanParams:
"""Build ScanParams from the JSON request body, falling back to defaults."""
data = request.get_json(silent=True) or {}
def _pick(key: str, default):
value = data.get(key)
return default if value is None or value == "" else value
return ScanParams(
copyparty_log_path=str(
_pick("copyparty_log_path", COPYPARTY_LOG_PATH)),
copyparty_report_dir=str(
_pick("copyparty_report_dir", COPYPARTY_REPORT_DIR)),
min_denied=int(_pick("min_denied", DEFAULT_MIN_DENIED)),
npm_log_path=str(_pick("npm_log_path", NPM_LOG_PATH)),
npm_report_dir=str(_pick("npm_report_dir", NPM_REPORT_DIR)),
min_errors=int(_pick("min_errors", DEFAULT_MIN_ERRORS)),
error_ratio=float(_pick("error_ratio", DEFAULT_ERROR_RATIO)),
report_dir=str(_pick("report_dir", REPORT_DIR)),
top=int(_pick("top", DEFAULT_TOP)),
quiet=bool(data.get("quiet", False)),
full=bool(data.get("full", False)),
update_firewall=bool(data.get("update_firewall", False)),
dry_run=bool(data.get("dry_run", False)),
)
def _with_router(fn):
"""Connect to the router, run ``fn(fw)``, always disconnect.
Returns a JSON error response (502) if the router is unreachable.
"""
fw = MikrotikFirewall()
try:
fw.connect()
return fn(fw)
except Exception as e:
return jsonify({"error": str(e)}), 502
finally:
fw.disconnect()
def _sync_firewall(dry_run: bool):
"""Sync the latest consolidated blocklist to the router (preview or apply)."""
v4 = _read_lines(os.path.join(APP_REPORT_DATA_DIR, "blocklist_ipv4.txt"))
v6 = _read_lines(os.path.join(APP_REPORT_DATA_DIR, "blocklist_ipv6.txt"))
def _sync(fw):
changes = fw.sync_blocklist(v4, v6, dry_run=dry_run)
return jsonify({**changes, "dry_run": dry_run})
return _with_router(_sync)
# ---------------------------------------------------------------------------
# app factory
# ---------------------------------------------------------------------------
def create_app() -> Flask:
"""Build and configure the Flask application."""
_app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
app = Flask(__name__,
template_folder=os.path.join(_app_dir, "templates"),
static_folder=os.path.join(_app_dir, "static"))
# Seed the in-memory job store from disk so the dashboard keeps showing
# recent scans across server restarts.
web_scan.load_persisted()
from .routes import register_routes
helpers = {
"_count_lines": _count_lines,
"_read_lines": _read_lines,
"_read_json": _read_json,
"_params_from_request": _params_from_request,
"_with_router": _with_router,
"_sync_firewall": _sync_firewall,
}
register_routes(app, REPORT_SOURCES, SCAN_KINDS, web_scan, helpers)
return app
def serve(host: str = WEB_HOST, port: int = WEB_PORT) -> None:
"""Run the web server on localhost at the configured high port."""
app = create_app()
print(f"Intrusion Detection web UI running at http://{host}:{port}")
app.run(host=host, port=port, use_reloader=False)
if __name__ == "__main__":
serve()