Spaces:
Sleeping
Sleeping
Commit
·
0f8fe33
0
Parent(s):
Major Project
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +18 -0
- backend/app.py +106 -0
- backend/capture/__init__.py +0 -0
- backend/capture/live_capture.py +620 -0
- backend/capture/live_manager.py +57 -0
- backend/extensions.py +4 -0
- backend/flow_builder.py +35 -0
- backend/generated_reports/traffic_logs.csv +8 -0
- backend/list_groq_models.py +13 -0
- backend/logs/bcc_logs.csv +0 -0
- backend/logs/cicids_logs.csv +0 -0
- backend/reporting/pdf_report.py +58 -0
- backend/requirements.txt +34 -0
- backend/retrain_requests.jsonl +1 -0
- backend/routes/__init__.py +0 -0
- backend/routes/ai_route.py +40 -0
- backend/routes/alerts_route.py +82 -0
- backend/routes/chat_route.py +26 -0
- backend/routes/geo_route.py +33 -0
- backend/routes/ip_lookup_route.py +148 -0
- backend/routes/live_route.py +53 -0
- backend/routes/logs_route.py +137 -0
- backend/routes/manual_predict_route.py +384 -0
- backend/routes/ml_route.py +297 -0
- backend/routes/ml_switch_route.py +105 -0
- backend/routes/offline_detection.py +139 -0
- backend/routes/predict_route.py +132 -0
- backend/routes/reports_route.py +172 -0
- backend/routes/system_info.py +211 -0
- backend/routes/traffic_routes.py +49 -0
- backend/sample/bcc_sample.csv +2 -0
- backend/sample/cicids_sample.csv +2 -0
- backend/socket_manager.py +80 -0
- backend/uploads/bcc_sample.csv +2 -0
- backend/uploads/cicids_sample.csv +2 -0
- backend/uploads/cicids_sample_1.csv +2 -0
- backend/uploads/iris.csv +151 -0
- backend/utils/ai_engine.py +164 -0
- backend/utils/geo_lookup.py +133 -0
- backend/utils/logger.py +273 -0
- backend/utils/model_selector.py +116 -0
- backend/utils/pcap_to_csv.py +23 -0
- backend/utils/risk_engine.py +77 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/components.json +22 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +17 -0
- frontend/jsconfig.json +8 -0
- frontend/package-lock.json +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- SENSITIVE KEYS (Never Push) ---
|
| 2 |
+
.env
|
| 3 |
+
backend/.env
|
| 4 |
+
frontend/.env
|
| 5 |
+
*.env
|
| 6 |
+
|
| 7 |
+
# --- HEAVY FOLDERS (Never Push) ---
|
| 8 |
+
node_modules/
|
| 9 |
+
frontend/node_modules/
|
| 10 |
+
backend/env/
|
| 11 |
+
env/
|
| 12 |
+
__pycache__/
|
| 13 |
+
backend/__pycache__/
|
| 14 |
+
dist/
|
| 15 |
+
build/
|
| 16 |
+
|
| 17 |
+
# --- ML MODELS (Ignore entire folder over 100MB) ---
|
| 18 |
+
backend/ml_models/
|
backend/app.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================
|
| 2 |
+
# FILE: app.py
|
| 3 |
+
# Optimized Flask + SocketIO entry (threading mode, no debug)
|
| 4 |
+
# =============================================================
|
| 5 |
+
import logging
|
| 6 |
+
from flask import Flask, jsonify
|
| 7 |
+
from flask_cors import CORS
|
| 8 |
+
from flask_socketio import SocketIO
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# lightweight logging
|
| 12 |
+
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
| 13 |
+
logging.getLogger('socketio').setLevel(logging.ERROR)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
app = Flask(__name__)
|
| 17 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Use threading mode to avoid eventlet monkey-patch issues with Scapy/IO
|
| 21 |
+
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Mail initialization is left as-is but keep credentials out of source in production
|
| 25 |
+
try:
|
| 26 |
+
from extensions import mail
|
| 27 |
+
app.config.update(
|
| 28 |
+
MAIL_SERVER="smtp.gmail.com",
|
| 29 |
+
MAIL_PORT=587,
|
| 30 |
+
MAIL_USE_TLS=True,
|
| 31 |
+
MAIL_USERNAME="yishu2005.ju@gmail.com",
|
| 32 |
+
MAIL_PASSWORD="prko cejt awef zmmi",
|
| 33 |
+
MAIL_DEFAULT_SENDER=("Adaptive AI NIDS", "yishu2005.ju@gmail.com")
|
| 34 |
+
)
|
| 35 |
+
mail.init_app(app)
|
| 36 |
+
|
| 37 |
+
except Exception:
|
| 38 |
+
# If mail is not available in dev/test, continue gracefully
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# lazy import of sniffer so import side-effects are minimal
|
| 43 |
+
sniffer = None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _get_sniffer():
|
| 47 |
+
global sniffer
|
| 48 |
+
if sniffer is None:
|
| 49 |
+
from capture.live_manager import sniffer as _s
|
| 50 |
+
sniffer = _s
|
| 51 |
+
return sniffer
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# Register blueprints lazily to avoid heavy imports at startup
|
| 55 |
+
def register_blueprints(app):
|
| 56 |
+
from importlib import import_module
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
routes = [
|
| 60 |
+
("routes.live_route", "live_bp", "/api/live"),
|
| 61 |
+
("routes.logs_route", "logs_bp", "/api/logs"),
|
| 62 |
+
("routes.predict_route", "predict_bp", "/api/predict"),
|
| 63 |
+
("routes.reports_route", "reports_bp", "/api/reports"),
|
| 64 |
+
("routes.ip_lookup_route", "ip_lookup_bp", "/api/ip"),
|
| 65 |
+
("routes.geo_route", "geo_bp", "/api/geo"),
|
| 66 |
+
("routes.alerts_route", "alerts_bp", "/api"),
|
| 67 |
+
("routes.system_info", "system_bp", "/api"),
|
| 68 |
+
("routes.ml_route", "ml_bp", "/api"),
|
| 69 |
+
("routes.traffic_routes", "traffic_bp", "/api"),
|
| 70 |
+
("routes.ml_switch_route","ml_switch","/api/model"),
|
| 71 |
+
("routes.manual_predict_route","manual_predict","/api"),
|
| 72 |
+
("routes.ai_route","ai_bp","/api/ai"),
|
| 73 |
+
("routes.chat_route","chat_bp","/api"),
|
| 74 |
+
("routes.offline_detection","offline_bp","/api/offline")
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
for module_name, varname, prefix in routes:
|
| 78 |
+
try:
|
| 79 |
+
mod = import_module(module_name)
|
| 80 |
+
bp = getattr(mod, varname)
|
| 81 |
+
app.register_blueprint(bp, url_prefix=prefix)
|
| 82 |
+
print(f"✅ Registered route: {module_name} -> {prefix}")
|
| 83 |
+
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"⚠️ Skipping {module_name}: {e}")
|
| 86 |
+
|
| 87 |
+
register_blueprints(app)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@app.route("/")
|
| 91 |
+
def home():
|
| 92 |
+
s = _get_sniffer()
|
| 93 |
+
return jsonify({
|
| 94 |
+
"status": "✅ Backend Active",
|
| 95 |
+
"capture_running": s.is_running() if s else False,
|
| 96 |
+
"tip": "Use /api/live/start and /api/live/stop to control capture"
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
print("🚀 Starting Adaptive AI NIDS Backend (threading mode)...")
|
| 102 |
+
# Run without debug — debug spawns extra processes and uses more CPU
|
| 103 |
+
socketio.run(app, host="0.0.0.0", port=5000, debug=False)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
|
backend/capture/__init__.py
ADDED
|
File without changes
|
backend/capture/live_capture.py
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/capture/live_capture.py
|
| 2 |
+
# Flow-aware live capture supporting both BCC (per-packet) and CICIDS (flow-aggregated)
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
import threading
|
| 6 |
+
import queue
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from collections import defaultdict, deque
|
| 9 |
+
import numpy as np
|
| 10 |
+
from scapy.all import sniff, IP, TCP, UDP # keep scapy usage
|
| 11 |
+
import joblib
|
| 12 |
+
|
| 13 |
+
from utils.logger import push_event
|
| 14 |
+
from socket_manager import emit_new_event
|
| 15 |
+
from utils.model_selector import get_active_model, load_model
|
| 16 |
+
|
| 17 |
+
# -------------------------
|
| 18 |
+
# Tunables
|
| 19 |
+
# -------------------------
|
| 20 |
+
CAPTURE_QUEUE_MAX = 5000
|
| 21 |
+
PROCESS_BATCH_SIZE = 40
|
| 22 |
+
EMIT_INTERVAL = 0.5
|
| 23 |
+
BPF_FILTER = "tcp or udp"
|
| 24 |
+
SAMPLE_RATE = 0.45
|
| 25 |
+
THROTTLE_PER_PACKET = 0.02
|
| 26 |
+
|
| 27 |
+
# Flow builder tunables
|
| 28 |
+
FLOW_IDLE_TIMEOUT = 1.5 # seconds of inactivity -> expire flow
|
| 29 |
+
FLOW_PACKET_THRESHOLD = 50 # force flush if many packets
|
| 30 |
+
FLOW_MAX_TRACKED = 20000 # limit number of active flows tracked to avoid memory explosion
|
| 31 |
+
|
| 32 |
+
# -------------------------
|
| 33 |
+
# Internal state
|
| 34 |
+
# -------------------------
|
| 35 |
+
_packet_queue = queue.Queue(maxsize=CAPTURE_QUEUE_MAX)
|
| 36 |
+
_running = threading.Event()
|
| 37 |
+
_last_emit = 0.0
|
| 38 |
+
|
| 39 |
+
# Flow table and lock
|
| 40 |
+
_flows = dict() # flow_key -> Flow object
|
| 41 |
+
_flows_lock = threading.Lock()
|
| 42 |
+
|
| 43 |
+
# background threads
|
| 44 |
+
_processor_thr = None
|
| 45 |
+
_capture_thr = None
|
| 46 |
+
_expiry_thr = None
|
| 47 |
+
|
| 48 |
+
# -------------------------
|
| 49 |
+
# Flow data container
|
| 50 |
+
# -------------------------
|
| 51 |
+
class Flow:
|
| 52 |
+
def __init__(self, first_pkt, ts):
|
| 53 |
+
# 5-tuple key derived externally
|
| 54 |
+
self.first_seen = ts
|
| 55 |
+
self.last_seen = ts
|
| 56 |
+
self.packets_total = 0
|
| 57 |
+
self.packets_fwd = 0
|
| 58 |
+
self.packets_bwd = 0
|
| 59 |
+
self.bytes_fwd = 0
|
| 60 |
+
self.bytes_bwd = 0
|
| 61 |
+
self.fwd_lens = [] # for mean
|
| 62 |
+
self.bwd_lens = []
|
| 63 |
+
self.inter_arrivals = [] # global IATs across flow
|
| 64 |
+
self.last_pkt_ts = ts
|
| 65 |
+
self.fwd_psh = 0
|
| 66 |
+
self.fwd_urg = 0
|
| 67 |
+
self.protocol = 6 if first_pkt.haslayer(TCP) else (17 if first_pkt.haslayer(UDP) else 0)
|
| 68 |
+
# store client/server ip+port orientation based on first packet's src/dst
|
| 69 |
+
self.client_ip = first_pkt[IP].src
|
| 70 |
+
self.server_ip = first_pkt[IP].dst
|
| 71 |
+
self.client_port = first_pkt.sport if hasattr(first_pkt, 'sport') else 0
|
| 72 |
+
self.server_port = first_pkt.dport if hasattr(first_pkt, 'dport') else 0
|
| 73 |
+
|
| 74 |
+
def update(self, pkt, ts):
|
| 75 |
+
self.packets_total += 1
|
| 76 |
+
# Determine direction relative to initial client/server
|
| 77 |
+
try:
|
| 78 |
+
src = pkt[IP].src
|
| 79 |
+
sport = pkt.sport if hasattr(pkt, 'sport') else 0
|
| 80 |
+
payload = bytes(pkt.payload) if pkt.payload else b""
|
| 81 |
+
plen = len(payload)
|
| 82 |
+
except Exception:
|
| 83 |
+
src = None; sport = 0; plen = 0
|
| 84 |
+
|
| 85 |
+
# if src equals initial client, it's forward
|
| 86 |
+
if src == self.client_ip and sport == self.client_port:
|
| 87 |
+
dir_fwd = True
|
| 88 |
+
else:
|
| 89 |
+
dir_fwd = False
|
| 90 |
+
|
| 91 |
+
if dir_fwd:
|
| 92 |
+
self.packets_fwd += 1
|
| 93 |
+
self.bytes_fwd += plen
|
| 94 |
+
self.fwd_lens.append(plen)
|
| 95 |
+
# flags
|
| 96 |
+
if pkt.haslayer(TCP):
|
| 97 |
+
flags = pkt[TCP].flags
|
| 98 |
+
if flags & 0x08: # PSH
|
| 99 |
+
self.fwd_psh += 1
|
| 100 |
+
if flags & 0x20: # URG
|
| 101 |
+
self.fwd_urg += 1
|
| 102 |
+
else:
|
| 103 |
+
self.packets_bwd += 1
|
| 104 |
+
self.bytes_bwd += plen
|
| 105 |
+
self.bwd_lens.append(plen)
|
| 106 |
+
|
| 107 |
+
# inter-arrival
|
| 108 |
+
iat = ts - (self.last_pkt_ts or ts)
|
| 109 |
+
if iat > 0:
|
| 110 |
+
self.inter_arrivals.append(iat)
|
| 111 |
+
self.last_pkt_ts = ts
|
| 112 |
+
self.last_seen = ts
|
| 113 |
+
|
| 114 |
+
def is_idle(self, now, idle_timeout):
|
| 115 |
+
return (now - self.last_seen) >= idle_timeout
|
| 116 |
+
|
| 117 |
+
def build_cicids_features(self, dst_port_override=None):
|
| 118 |
+
"""
|
| 119 |
+
Build feature vector matching:
|
| 120 |
+
['Protocol', 'Dst Port', 'Flow Duration', 'Tot Fwd Pkts', 'Tot Bwd Pkts',
|
| 121 |
+
'TotLen Fwd Pkts', 'TotLen Bwd Pkts', 'Fwd Pkt Len Mean', 'Bwd Pkt Len Mean',
|
| 122 |
+
'Flow IAT Mean', 'Fwd PSH Flags', 'Fwd URG Flags', 'Fwd IAT Mean']
|
| 123 |
+
-> returns list of floats/ints
|
| 124 |
+
"""
|
| 125 |
+
duration = max(self.last_seen - self.first_seen, 0.000001)
|
| 126 |
+
tot_fwd = self.packets_fwd
|
| 127 |
+
tot_bwd = self.packets_bwd
|
| 128 |
+
totlen_fwd = float(self.bytes_fwd)
|
| 129 |
+
totlen_bwd = float(self.bytes_bwd)
|
| 130 |
+
fwd_mean = float(np.mean(self.fwd_lens)) if self.fwd_lens else 0.0
|
| 131 |
+
bwd_mean = float(np.mean(self.bwd_lens)) if self.bwd_lens else 0.0
|
| 132 |
+
flow_iat_mean = float(np.mean(self.inter_arrivals)) if self.inter_arrivals else 0.0
|
| 133 |
+
fwd_iat_mean = self._fwd_iat_mean()
|
| 134 |
+
proto = int(self.protocol)
|
| 135 |
+
# FIXED: respect explicit override even if zero
|
| 136 |
+
dst_port = self.server_port if dst_port_override is None else int(dst_port_override or 0)
|
| 137 |
+
|
| 138 |
+
return [
|
| 139 |
+
proto,
|
| 140 |
+
dst_port,
|
| 141 |
+
duration,
|
| 142 |
+
tot_fwd,
|
| 143 |
+
tot_bwd,
|
| 144 |
+
totlen_fwd,
|
| 145 |
+
totlen_bwd,
|
| 146 |
+
fwd_mean,
|
| 147 |
+
bwd_mean,
|
| 148 |
+
flow_iat_mean,
|
| 149 |
+
self.fwd_psh,
|
| 150 |
+
self.fwd_urg,
|
| 151 |
+
fwd_iat_mean
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
def _fwd_iat_mean(self):
|
| 155 |
+
# approximate forward-only IATs by splitting inter_arrivals roughly (coarse)
|
| 156 |
+
# If we had per-direction timestamps we would measure precisely;
|
| 157 |
+
# here we approximate as global mean when forward packets exist.
|
| 158 |
+
if self.inter_arrivals and self.packets_fwd > 0:
|
| 159 |
+
return float(np.mean(self.inter_arrivals))
|
| 160 |
+
return 0.0
|
| 161 |
+
|
| 162 |
+
# -------------------------
|
| 163 |
+
# helpers: flow key
|
| 164 |
+
# -------------------------
|
| 165 |
+
def make_flow_key(pkt):
|
| 166 |
+
try:
|
| 167 |
+
ip = pkt[IP]
|
| 168 |
+
proto = 6 if pkt.haslayer(TCP) else (17 if pkt.haslayer(UDP) else 0)
|
| 169 |
+
sport = pkt.sport if hasattr(pkt, 'sport') else 0
|
| 170 |
+
dport = pkt.dport if hasattr(pkt, 'dport') else 0
|
| 171 |
+
# canonicalize tuple order to consider direction
|
| 172 |
+
return (ip.src, ip.dst, sport, dport, proto)
|
| 173 |
+
except Exception:
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
# -------------------------
|
| 177 |
+
# queueing / sniff simple wrappers
|
| 178 |
+
# -------------------------
|
| 179 |
+
def _enqueue(pkt):
|
| 180 |
+
try:
|
| 181 |
+
_packet_queue.put_nowait((pkt, time.time()))
|
| 182 |
+
except queue.Full:
|
| 183 |
+
return
|
| 184 |
+
|
| 185 |
+
def _packet_capture_worker(iface=None):
|
| 186 |
+
sniff(iface=iface, prn=_enqueue, store=False, filter=BPF_FILTER)
|
| 187 |
+
|
| 188 |
+
# -------------------------
|
| 189 |
+
# Expiry thread: periodically expire idle flows
|
| 190 |
+
# -------------------------
|
| 191 |
+
def _expiry_worker():
|
| 192 |
+
while _running.is_set():
|
| 193 |
+
time.sleep(0.5)
|
| 194 |
+
now = time.time()
|
| 195 |
+
to_flush = []
|
| 196 |
+
with _flows_lock:
|
| 197 |
+
keys = list(_flows.keys())
|
| 198 |
+
for k in keys:
|
| 199 |
+
f = _flows.get(k)
|
| 200 |
+
if f is None:
|
| 201 |
+
continue
|
| 202 |
+
if f.is_idle(now, FLOW_IDLE_TIMEOUT) or f.packets_total >= FLOW_PACKET_THRESHOLD:
|
| 203 |
+
to_flush.append(k)
|
| 204 |
+
|
| 205 |
+
if to_flush:
|
| 206 |
+
_process_and_emit_flows(to_flush)
|
| 207 |
+
|
| 208 |
+
# -------------------------
|
| 209 |
+
# core: process queue, update flows, flush when needed
|
| 210 |
+
# -------------------------
|
| 211 |
+
def _processor_worker():
|
| 212 |
+
global _last_emit
|
| 213 |
+
# lazy load initial model bundle
|
| 214 |
+
active = get_active_model()
|
| 215 |
+
model_bundle = load_model(active)
|
| 216 |
+
processor_model = model_bundle.get("model")
|
| 217 |
+
processor_scaler = model_bundle.get("scaler") or (model_bundle.get("artifacts") and model_bundle["artifacts"].get("scaler"))
|
| 218 |
+
processor_encoder = model_bundle.get("encoder") or (model_bundle.get("artifacts") and model_bundle["artifacts"].get("label_encoder"))
|
| 219 |
+
|
| 220 |
+
batch = []
|
| 221 |
+
while _running.is_set():
|
| 222 |
+
# refresh model if switched
|
| 223 |
+
new_active = get_active_model()
|
| 224 |
+
if new_active != active:
|
| 225 |
+
active = new_active
|
| 226 |
+
model_bundle = load_model(active)
|
| 227 |
+
processor_model = model_bundle.get("model")
|
| 228 |
+
processor_scaler = model_bundle.get("scaler") or (model_bundle.get("artifacts") and model_bundle["artifacts"].get("scaler"))
|
| 229 |
+
processor_encoder = model_bundle.get("encoder") or (model_bundle.get("artifacts") and model_bundle["artifacts"].get("label_encoder"))
|
| 230 |
+
print(f"[live_capture] switched active model to {active}")
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
pkt, ts = _packet_queue.get(timeout=0.5)
|
| 234 |
+
except queue.Empty:
|
| 235 |
+
# flush small batches if exist (not required)
|
| 236 |
+
continue
|
| 237 |
+
|
| 238 |
+
# sampling, ignore some traffic
|
| 239 |
+
if np.random.rand() > SAMPLE_RATE:
|
| 240 |
+
continue
|
| 241 |
+
if not pkt.haslayer(IP):
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
# BCC path: still do per-packet predictions if active 'bcc'
|
| 245 |
+
if active == "bcc":
|
| 246 |
+
batch.append((pkt, ts))
|
| 247 |
+
if len(batch) >= PROCESS_BATCH_SIZE or _packet_queue.empty():
|
| 248 |
+
_process_bcc_batch(batch, processor_model, processor_scaler, processor_encoder)
|
| 249 |
+
batch.clear()
|
| 250 |
+
continue
|
| 251 |
+
|
| 252 |
+
# CICIDS path: update flow table
|
| 253 |
+
key = make_flow_key(pkt)
|
| 254 |
+
if key is None:
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
# Prevent runaway flows table
|
| 258 |
+
with _flows_lock:
|
| 259 |
+
if len(_flows) > FLOW_MAX_TRACKED:
|
| 260 |
+
# flush oldest flows (heuristic) to free space
|
| 261 |
+
# choose keys ordered by last_seen
|
| 262 |
+
items = list(_flows.items())
|
| 263 |
+
items.sort(key=lambda kv: kv[1].last_seen)
|
| 264 |
+
n_to_remove = int(len(items) * 0.1) or 100
|
| 265 |
+
keys_to_flush = [k for k, _ in items[:n_to_remove]]
|
| 266 |
+
# flush asynchronously
|
| 267 |
+
threading.Thread(target=_process_and_emit_flows, args=(keys_to_flush,), daemon=True).start()
|
| 268 |
+
|
| 269 |
+
flow = _flows.get(key)
|
| 270 |
+
if flow is None:
|
| 271 |
+
# new flow
|
| 272 |
+
flow = Flow(pkt, ts)
|
| 273 |
+
_flows[key] = flow
|
| 274 |
+
|
| 275 |
+
# update outside big lock (Flow.update is mostly per-flow)
|
| 276 |
+
flow.update(pkt, ts)
|
| 277 |
+
|
| 278 |
+
# flush immediately if surpass threshold
|
| 279 |
+
if flow.packets_total >= FLOW_PACKET_THRESHOLD:
|
| 280 |
+
_process_and_emit_flows([key])
|
| 281 |
+
|
| 282 |
+
# when stopped, flush all
|
| 283 |
+
with _flows_lock:
|
| 284 |
+
keys = list(_flows.keys())
|
| 285 |
+
if keys:
|
| 286 |
+
_process_and_emit_flows(keys)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# -------------------------
|
| 290 |
+
# Process BCC batch (existing behavior)
|
| 291 |
+
# -------------------------
|
| 292 |
+
def _process_bcc_batch(batch, model, scaler, encoder):
|
| 293 |
+
events = []
|
| 294 |
+
features_list = []
|
| 295 |
+
for pkt, ts in batch:
|
| 296 |
+
# reuse earlier extraction (simple)
|
| 297 |
+
features = _extract_bcc_vector(pkt)
|
| 298 |
+
features_list.append(features)
|
| 299 |
+
|
| 300 |
+
X = np.asarray(features_list, dtype=float)
|
| 301 |
+
if scaler is not None:
|
| 302 |
+
try:
|
| 303 |
+
Xs = scaler.transform(X)
|
| 304 |
+
except Exception:
|
| 305 |
+
Xs = X
|
| 306 |
+
else:
|
| 307 |
+
Xs = X
|
| 308 |
+
|
| 309 |
+
if model is not None:
|
| 310 |
+
try:
|
| 311 |
+
preds = model.predict(Xs)
|
| 312 |
+
probs = model.predict_proba(Xs) if hasattr(model, "predict_proba") else None
|
| 313 |
+
except Exception as e:
|
| 314 |
+
preds = [None] * len(Xs)
|
| 315 |
+
probs = None
|
| 316 |
+
print("[live_capture] BCC model predict failed:", e)
|
| 317 |
+
else:
|
| 318 |
+
preds = [None] * len(Xs)
|
| 319 |
+
probs = None
|
| 320 |
+
|
| 321 |
+
for i, (pkt, ts) in enumerate(batch):
|
| 322 |
+
pred = preds[i]
|
| 323 |
+
conf = float(np.max(probs[i])) if (probs is not None and len(probs) > i) else None
|
| 324 |
+
try:
|
| 325 |
+
decoded = encoder.inverse_transform([int(pred)])[0] if encoder else str(pred)
|
| 326 |
+
except Exception:
|
| 327 |
+
decoded = str(pred)
|
| 328 |
+
|
| 329 |
+
evt = {
|
| 330 |
+
"time": datetime.now().strftime("%H:%M:%S"),
|
| 331 |
+
"src_ip": pkt[IP].src,
|
| 332 |
+
"dst_ip": pkt[IP].dst,
|
| 333 |
+
"sport": (pkt.sport if (pkt.haslayer(TCP) or pkt.haslayer(UDP)) else 0),
|
| 334 |
+
"dport": (pkt.dport if (pkt.haslayer(TCP) or pkt.haslayer(UDP)) else 0),
|
| 335 |
+
"proto": "TCP" if pkt.haslayer(TCP) else ("UDP" if pkt.haslayer(UDP) else "OTHER"),
|
| 336 |
+
"prediction": decoded,
|
| 337 |
+
"confidence": conf if conf is None or isinstance(conf, float) else float(conf),
|
| 338 |
+
"packet_meta": extract_packet_metadata(pkt) # <-- NEW
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
try:
|
| 342 |
+
push_event(evt)
|
| 343 |
+
except Exception:
|
| 344 |
+
pass
|
| 345 |
+
events.append(evt)
|
| 346 |
+
|
| 347 |
+
# emit once per batch
|
| 348 |
+
if events:
|
| 349 |
+
try:
|
| 350 |
+
emit_new_event({"items": events, "count": len(events)})
|
| 351 |
+
except Exception:
|
| 352 |
+
pass
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def _extract_bcc_vector(pkt):
|
| 356 |
+
# this matches your old extract_bcc_features but kept minimal and robust
|
| 357 |
+
try:
|
| 358 |
+
proto = 6 if pkt.haslayer(TCP) else (17 if pkt.haslayer(UDP) else 1)
|
| 359 |
+
src_port = pkt.sport if pkt.haslayer(TCP) or pkt.haslayer(UDP) else 0
|
| 360 |
+
dst_port = pkt.dport if pkt.haslayer(TCP) or pkt.haslayer(UDP) else 0
|
| 361 |
+
|
| 362 |
+
payload = bytes(pkt.payload) if pkt.payload else b""
|
| 363 |
+
plen = len(payload)
|
| 364 |
+
header = max(len(pkt) - plen, 0)
|
| 365 |
+
|
| 366 |
+
syn = 1 if pkt.haslayer(TCP) and pkt[TCP].flags & 0x02 else 0
|
| 367 |
+
ack = 1 if pkt.haslayer(TCP) and pkt[TCP].flags & 0x10 else 0
|
| 368 |
+
rst = 1 if pkt.haslayer(TCP) and pkt[TCP].flags & 0x04 else 0
|
| 369 |
+
fin = 1 if pkt.haslayer(TCP) and pkt[TCP].flags & 0x01 else 0
|
| 370 |
+
|
| 371 |
+
return [
|
| 372 |
+
proto,
|
| 373 |
+
src_port,
|
| 374 |
+
dst_port,
|
| 375 |
+
0.001,
|
| 376 |
+
1,
|
| 377 |
+
1,
|
| 378 |
+
0,
|
| 379 |
+
plen,
|
| 380 |
+
header,
|
| 381 |
+
plen / 0.002 if 0.002 else plen,
|
| 382 |
+
1 / 0.002 if 0.002 else 1,
|
| 383 |
+
syn,
|
| 384 |
+
ack,
|
| 385 |
+
rst,
|
| 386 |
+
fin
|
| 387 |
+
]
|
| 388 |
+
except Exception:
|
| 389 |
+
return [0] * 15
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# -------------------------
|
| 393 |
+
# Packet-level metadata extractor
|
| 394 |
+
# -------------------------
|
| 395 |
+
def extract_packet_metadata(pkt):
|
| 396 |
+
"""Extract detailed packet-level metadata for frontend display."""
|
| 397 |
+
meta = {}
|
| 398 |
+
|
| 399 |
+
# IP-level metadata
|
| 400 |
+
try:
|
| 401 |
+
meta["ttl"] = pkt[IP].ttl if pkt.haslayer(IP) else None
|
| 402 |
+
meta["pkt_len"] = len(pkt)
|
| 403 |
+
except:
|
| 404 |
+
meta["ttl"] = None
|
| 405 |
+
meta["pkt_len"] = None
|
| 406 |
+
|
| 407 |
+
# TCP metadata
|
| 408 |
+
if pkt.haslayer(TCP):
|
| 409 |
+
tcp = pkt[TCP]
|
| 410 |
+
try:
|
| 411 |
+
meta["seq"] = int(tcp.seq)
|
| 412 |
+
meta["ack"] = int(tcp.ack)
|
| 413 |
+
meta["window"] = int(tcp.window)
|
| 414 |
+
meta["flags"] = str(tcp.flags)
|
| 415 |
+
meta["header_len"] = tcp.dataofs * 4 # Data offset (words)
|
| 416 |
+
except:
|
| 417 |
+
meta["seq"] = None
|
| 418 |
+
meta["ack"] = None
|
| 419 |
+
meta["window"] = None
|
| 420 |
+
meta["flags"] = None
|
| 421 |
+
meta["header_len"] = None
|
| 422 |
+
else:
|
| 423 |
+
meta["seq"] = None
|
| 424 |
+
meta["ack"] = None
|
| 425 |
+
meta["window"] = None
|
| 426 |
+
meta["flags"] = None
|
| 427 |
+
meta["header_len"] = None
|
| 428 |
+
|
| 429 |
+
# Payload length
|
| 430 |
+
try:
|
| 431 |
+
payload = bytes(pkt.payload)
|
| 432 |
+
meta["payload_len"] = len(payload)
|
| 433 |
+
except:
|
| 434 |
+
meta["payload_len"] = None
|
| 435 |
+
|
| 436 |
+
return meta
|
| 437 |
+
|
| 438 |
+
# -------------------------
|
| 439 |
+
# flush flows and emit/predict
|
| 440 |
+
# -------------------------
|
| 441 |
+
def _process_and_emit_flows(keys):
|
| 442 |
+
# keys: list of flow_keys to flush; safe to call from any thread
|
| 443 |
+
# collect features for predict, delete flows
|
| 444 |
+
to_predict = []
|
| 445 |
+
mapping = [] # keep (flow_key, flow_obj) for events
|
| 446 |
+
with _flows_lock:
|
| 447 |
+
for k in keys:
|
| 448 |
+
f = _flows.pop(k, None)
|
| 449 |
+
if f:
|
| 450 |
+
mapping.append((k, f))
|
| 451 |
+
|
| 452 |
+
if not mapping:
|
| 453 |
+
return
|
| 454 |
+
|
| 455 |
+
# create features list
|
| 456 |
+
for k, f in mapping:
|
| 457 |
+
feat = f.build_cicids_features()
|
| 458 |
+
to_predict.append((k, f, feat))
|
| 459 |
+
|
| 460 |
+
X = np.array([t[2] for t in to_predict], dtype=float)
|
| 461 |
+
# lazy load latest model bundle (in case switching)
|
| 462 |
+
active = get_active_model()
|
| 463 |
+
bundle = load_model(active)
|
| 464 |
+
model = bundle.get("model")
|
| 465 |
+
scaler = None
|
| 466 |
+
artifacts = bundle.get("artifacts")
|
| 467 |
+
|
| 468 |
+
# try to get scaler from bundle/artifacts
|
| 469 |
+
if bundle.get("scaler") is not None:
|
| 470 |
+
scaler = bundle.get("scaler")
|
| 471 |
+
elif artifacts and artifacts.get("scaler") is not None:
|
| 472 |
+
scaler = artifacts.get("scaler")
|
| 473 |
+
|
| 474 |
+
if scaler is not None:
|
| 475 |
+
try:
|
| 476 |
+
# If scaler expects dataframe shape, it should still accept ndarray
|
| 477 |
+
Xs = scaler.transform(X)
|
| 478 |
+
except Exception as e:
|
| 479 |
+
print("[live_capture] cicids scaler transform failed:", e)
|
| 480 |
+
Xs = X
|
| 481 |
+
else:
|
| 482 |
+
Xs = X
|
| 483 |
+
|
| 484 |
+
preds = []
|
| 485 |
+
probs = None
|
| 486 |
+
if model is not None:
|
| 487 |
+
try:
|
| 488 |
+
preds = model.predict(Xs)
|
| 489 |
+
if hasattr(model, "predict_proba"):
|
| 490 |
+
try:
|
| 491 |
+
probs = model.predict_proba(Xs)
|
| 492 |
+
except Exception:
|
| 493 |
+
probs = None
|
| 494 |
+
except Exception as e:
|
| 495 |
+
print("[live_capture] cicids model predict failed:", e)
|
| 496 |
+
preds = [None] * len(Xs)
|
| 497 |
+
probs = None
|
| 498 |
+
else:
|
| 499 |
+
preds = [None] * len(Xs)
|
| 500 |
+
|
| 501 |
+
# build events and emit/push
|
| 502 |
+
events = []
|
| 503 |
+
for i, (k, f, feat) in enumerate(to_predict):
|
| 504 |
+
pred = preds[i]
|
| 505 |
+
conf = float(np.max(probs[i])) if (probs is not None and len(probs) > i) else None
|
| 506 |
+
|
| 507 |
+
# -------------------------
|
| 508 |
+
# SIMPLIFIED LABEL DECODING
|
| 509 |
+
# -------------------------
|
| 510 |
+
# Your RF pipeline outputs string labels directly (e.g. 'DoS attacks-Hulk', 'BENIGN').
|
| 511 |
+
# So keep it simple and safe:
|
| 512 |
+
try:
|
| 513 |
+
label = str(pred)
|
| 514 |
+
except Exception:
|
| 515 |
+
label = repr(pred)
|
| 516 |
+
|
| 517 |
+
evt = {
|
| 518 |
+
"time": datetime.now().strftime("%H:%M:%S"),
|
| 519 |
+
"src_ip": f.client_ip,
|
| 520 |
+
"dst_ip": f.server_ip,
|
| 521 |
+
"sport": f.client_port,
|
| 522 |
+
"dport": f.server_port,
|
| 523 |
+
"proto": "TCP" if f.protocol == 6 else ("UDP" if f.protocol == 17 else "OTHER"),
|
| 524 |
+
"prediction": label,
|
| 525 |
+
"confidence": conf if conf is None or isinstance(conf, float) else float(conf),
|
| 526 |
+
"features": feat,
|
| 527 |
+
"flow_summary": {
|
| 528 |
+
"packets_fwd": f.packets_fwd,
|
| 529 |
+
"packets_bwd": f.packets_bwd,
|
| 530 |
+
"bytes_fwd": f.bytes_fwd,
|
| 531 |
+
"bytes_bwd": f.bytes_bwd,
|
| 532 |
+
"duration": f.last_seen - f.first_seen,
|
| 533 |
+
"fwd_mean_len": float(np.mean(f.fwd_lens)) if f.fwd_lens else 0.0
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
try:
|
| 538 |
+
push_event(evt)
|
| 539 |
+
except Exception:
|
| 540 |
+
pass
|
| 541 |
+
events.append(evt)
|
| 542 |
+
|
| 543 |
+
if events:
|
| 544 |
+
try:
|
| 545 |
+
emit_new_event({"items": events, "count": len(events)})
|
| 546 |
+
except Exception:
|
| 547 |
+
pass
|
| 548 |
+
|
| 549 |
+
# -------------------------
|
| 550 |
+
# start/stop API (keeps your old signatures)
|
| 551 |
+
# -------------------------
|
| 552 |
+
def start_live_capture_packet_mode(iface=None):
|
| 553 |
+
"""Start packet capture + processor + expiry threads."""
|
| 554 |
+
global _processor_thr, _capture_thr, _expiry_thr
|
| 555 |
+
if _running.is_set():
|
| 556 |
+
print("Already running")
|
| 557 |
+
return
|
| 558 |
+
_running.set()
|
| 559 |
+
_processor_thr = threading.Thread(target=_processor_worker, daemon=True)
|
| 560 |
+
_capture_thr = threading.Thread(target=_packet_capture_worker, kwargs={"iface": iface}, daemon=True)
|
| 561 |
+
_expiry_thr = threading.Thread(target=_expiry_worker, daemon=True)
|
| 562 |
+
_processor_thr.start()
|
| 563 |
+
_capture_thr.start()
|
| 564 |
+
_expiry_thr.start()
|
| 565 |
+
print("Live capture started (flow-aware)")
|
| 566 |
+
|
| 567 |
+
def stop_live_capture():
|
| 568 |
+
_running.clear()
|
| 569 |
+
time.sleep(0.2)
|
| 570 |
+
# flush all flows and stop
|
| 571 |
+
with _flows_lock:
|
| 572 |
+
keys = list(_flows.keys())
|
| 573 |
+
if keys:
|
| 574 |
+
_process_and_emit_flows(keys)
|
| 575 |
+
print("Stopping capture...")
|
| 576 |
+
|
| 577 |
+
def is_running():
|
| 578 |
+
return _running.is_set()
|
| 579 |
+
|
| 580 |
+
# -------------------------
|
| 581 |
+
# Small test helpers (simulate simple flow packets)
|
| 582 |
+
# -------------------------
|
| 583 |
+
def _make_fake_pkt(src, dst, sport, dport, proto='TCP', payload_len=100, flags=0x18):
|
| 584 |
+
"""Return a tiny object resembling scapy packet for testing without scapy."""
|
| 585 |
+
# If scapy present prefer to build actual IP/TCP
|
| 586 |
+
try:
|
| 587 |
+
if proto.upper() == 'TCP':
|
| 588 |
+
from scapy.all import IP, TCP
|
| 589 |
+
pkt = IP(src=src, dst=dst)/TCP(sport=sport, dport=dport, flags=flags)/("X"*payload_len)
|
| 590 |
+
return pkt
|
| 591 |
+
elif proto.upper() == 'UDP':
|
| 592 |
+
from scapy.all import IP, UDP
|
| 593 |
+
pkt = IP(src=src, dst=dst)/UDP(sport=sport, dport=dport)/("X"*payload_len)
|
| 594 |
+
return pkt
|
| 595 |
+
except Exception:
|
| 596 |
+
# fallback plain namespace
|
| 597 |
+
class SimplePkt:
|
| 598 |
+
def __init__(self):
|
| 599 |
+
self.payload = b"X"*payload_len
|
| 600 |
+
self.len = payload_len + 40
|
| 601 |
+
def haslayer(self, cls):
|
| 602 |
+
return False
|
| 603 |
+
return SimplePkt()
|
| 604 |
+
|
| 605 |
+
def simulate_flow(src="10.0.0.1", dst="10.0.0.2", sport=1234, dport=80, count=6, interval=0.1):
|
| 606 |
+
"""Quick local simulator: pushes `count` fake packets for a flow into the queue."""
|
| 607 |
+
for i in range(count):
|
| 608 |
+
pkt = _make_fake_pkt(src, dst, sport, dport, proto='TCP', payload_len=100, flags=0x18)
|
| 609 |
+
_enqueue((pkt, time.time())) if False else _packet_queue.put_nowait((pkt, time.time()))
|
| 610 |
+
time.sleep(interval)
|
| 611 |
+
|
| 612 |
+
# ----------------------------------------------------------------------------
|
| 613 |
+
# If you want to test this module interactively:
|
| 614 |
+
# 1) from backend.capture import live_capture
|
| 615 |
+
# 2) live_capture.start_live_capture_packet_mode()
|
| 616 |
+
# 3) call live_capture.simulate_flow(...) or send real packets
|
| 617 |
+
# 4) view server logs, or GET /api/live/recent to see events (existing route)
|
| 618 |
+
# ----------------------------------------------------------------------------
|
| 619 |
+
|
| 620 |
+
|
backend/capture/live_manager.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# live_manager.py (Optimized)
|
| 2 |
+
# -------------------------------------------------------------
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from .live_capture import start_live_capture_packet_mode, stop_live_capture, is_running
|
| 7 |
+
from utils.logger import get_recent_events, get_model_stats, get_active_model
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class LiveSniffer:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self._thr: Optional[threading.Thread] = None
|
| 13 |
+
self._lock = threading.Lock()
|
| 14 |
+
self._iface = None
|
| 15 |
+
self._last_start_time = None
|
| 16 |
+
|
| 17 |
+
def start(self, iface=None, packet_limit=0):
|
| 18 |
+
with self._lock:
|
| 19 |
+
if is_running():
|
| 20 |
+
print("Already running.")
|
| 21 |
+
return
|
| 22 |
+
self._iface = iface
|
| 23 |
+
self._last_start_time = time.strftime("%H:%M:%S")
|
| 24 |
+
|
| 25 |
+
def _worker():
|
| 26 |
+
print(f"LiveSniffer started on interface={iface or 'default'}")
|
| 27 |
+
try:
|
| 28 |
+
# FIX: start_live_capture_packet_mode signature accepts iface only
|
| 29 |
+
start_live_capture_packet_mode(iface=self._iface)
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print("Sniffer error:", e)
|
| 32 |
+
print("LiveSniffer thread exit.")
|
| 33 |
+
|
| 34 |
+
self._thr = threading.Thread(target=_worker, daemon=True)
|
| 35 |
+
self._thr.start()
|
| 36 |
+
|
| 37 |
+
def stop(self):
|
| 38 |
+
with self._lock:
|
| 39 |
+
if not is_running():
|
| 40 |
+
print("Already stopped.")
|
| 41 |
+
return
|
| 42 |
+
stop_live_capture()
|
| 43 |
+
|
| 44 |
+
if self._thr and self._thr.is_alive():
|
| 45 |
+
self._thr.join(timeout=3)
|
| 46 |
+
print("Sniffer fully stopped.")
|
| 47 |
+
|
| 48 |
+
def is_running(self) -> bool:
|
| 49 |
+
return is_running()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def recent(self, n=200):
|
| 53 |
+
return get_recent_events(get_active_model(), n)
|
| 54 |
+
def stats(self):
|
| 55 |
+
return get_model_stats(get_active_model())
|
| 56 |
+
|
| 57 |
+
sniffer = LiveSniffer()
|
backend/extensions.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/extensions.py
|
| 2 |
+
from flask_mail import Mail
|
| 3 |
+
|
| 4 |
+
mail = Mail()
|
backend/flow_builder.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# flow_builder.py
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
|
| 4 |
+
def build_flows(events):
|
| 5 |
+
flows = defaultdict(lambda: {
|
| 6 |
+
"src_ip": "",
|
| 7 |
+
"dst_ip": "",
|
| 8 |
+
"sport": "",
|
| 9 |
+
"dport": "",
|
| 10 |
+
"proto": "",
|
| 11 |
+
"packets": 0,
|
| 12 |
+
"bytes": 0,
|
| 13 |
+
"first_seen": "",
|
| 14 |
+
"last_seen": "",
|
| 15 |
+
})
|
| 16 |
+
|
| 17 |
+
for e in events:
|
| 18 |
+
key = (e["src_ip"], e["dst_ip"], e["sport"], e["dport"], e["proto"])
|
| 19 |
+
f = flows[key]
|
| 20 |
+
|
| 21 |
+
f["src_ip"] = e["src_ip"]
|
| 22 |
+
f["dst_ip"] = e["dst_ip"]
|
| 23 |
+
f["sport"] = e["sport"]
|
| 24 |
+
f["dport"] = e["dport"]
|
| 25 |
+
f["proto"] = e["proto"]
|
| 26 |
+
|
| 27 |
+
f["packets"] += 1
|
| 28 |
+
f["bytes"] += 1500 # approximation (or use real payload length if available)
|
| 29 |
+
|
| 30 |
+
# Update timestamps
|
| 31 |
+
if not f["first_seen"]:
|
| 32 |
+
f["first_seen"] = e.get("time")
|
| 33 |
+
f["last_seen"] = e.get("time")
|
| 34 |
+
|
| 35 |
+
return list(flows.values())
|
backend/generated_reports/traffic_logs.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
date,VPN,TOR,I2P,DDoS
|
| 2 |
+
2025-11-06,3,1,0,5
|
| 3 |
+
2025-11-07,4,3,1,7
|
| 4 |
+
2025-11-08,7,2,0,6
|
| 5 |
+
2025-11-09,5,4,2,8
|
| 6 |
+
2025-11-10,2,3,1,4
|
| 7 |
+
2025-11-11,6,2,1,6
|
| 8 |
+
2025-11-12,3,1,0,5
|
backend/list_groq_models.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
|
| 4 |
+
API_KEY = os.getenv("GROQ_API_KEY")
|
| 5 |
+
if not API_KEY:
|
| 6 |
+
raise RuntimeError("Set env GROQ_API_KEY")
|
| 7 |
+
|
| 8 |
+
resp = requests.get(
|
| 9 |
+
"https://api.groq.com/v1/models",
|
| 10 |
+
headers={"Authorization": f"Bearer {API_KEY}"}
|
| 11 |
+
)
|
| 12 |
+
print("Status:", resp.status_code)
|
| 13 |
+
print("Response:", resp.text)
|
backend/logs/bcc_logs.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/logs/cicids_logs.csv
ADDED
|
File without changes
|
backend/reporting/pdf_report.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fpdf import FPDF
|
| 2 |
+
import pandas as pd, os
|
| 3 |
+
from utils.logger import log_path
|
| 4 |
+
|
| 5 |
+
class NIDSReportPDF(FPDF):
|
| 6 |
+
def header(self):
|
| 7 |
+
self.set_font("Helvetica", "B", 18)
|
| 8 |
+
self.cell(0, 10, "NIDS - Network Intrusion Detection Report", ln=True, align="C")
|
| 9 |
+
self.ln(5)
|
| 10 |
+
|
| 11 |
+
def footer(self):
|
| 12 |
+
self.set_y(-15)
|
| 13 |
+
self.set_font("Helvetica", "I", 9)
|
| 14 |
+
self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")
|
| 15 |
+
|
| 16 |
+
def generate_pdf_bytes(n=300):
|
| 17 |
+
"""Generate PDF summary of recent events."""
|
| 18 |
+
df = pd.read_csv(log_path) if os.path.exists(log_path) else pd.DataFrame()
|
| 19 |
+
|
| 20 |
+
pdf = NIDSReportPDF()
|
| 21 |
+
pdf.alias_nb_pages()
|
| 22 |
+
pdf.add_page()
|
| 23 |
+
pdf.set_font("Helvetica", "", 12)
|
| 24 |
+
|
| 25 |
+
pdf.cell(0, 10, f"Last {n} Events Summary", ln=True)
|
| 26 |
+
pdf.ln(5)
|
| 27 |
+
|
| 28 |
+
if len(df) == 0:
|
| 29 |
+
pdf.cell(0, 10, "No data available.", ln=True)
|
| 30 |
+
else:
|
| 31 |
+
df = df.tail(n)
|
| 32 |
+
counts = df["prediction"].value_counts().to_dict() if "prediction" in df.columns else {}
|
| 33 |
+
|
| 34 |
+
pdf.cell(0, 10, "Prediction Distribution:", ln=True)
|
| 35 |
+
pdf.ln(4)
|
| 36 |
+
for label, count in counts.items():
|
| 37 |
+
pdf.cell(0, 10, f"{label}: {count}", ln=True)
|
| 38 |
+
|
| 39 |
+
pdf.ln(8)
|
| 40 |
+
pdf.cell(0, 10, "Sample Events:", ln=True)
|
| 41 |
+
pdf.ln(4)
|
| 42 |
+
|
| 43 |
+
# limit to 10 sample rows
|
| 44 |
+
cols = ["time", "src", "dst", "proto", "prediction"]
|
| 45 |
+
cols = [c for c in cols if c in df.columns]
|
| 46 |
+
for _, row in df.tail(10).iterrows():
|
| 47 |
+
line = " | ".join(str(row[c]) for c in cols)
|
| 48 |
+
if len(line) > 150:
|
| 49 |
+
line = line[:147] + "..."
|
| 50 |
+
pdf.multi_cell(0, 8, line)
|
| 51 |
+
|
| 52 |
+
# return as bytes
|
| 53 |
+
output = pdf.output(dest="S")
|
| 54 |
+
if isinstance(output, (bytes, bytearray)):
|
| 55 |
+
return bytes(output)
|
| 56 |
+
else:
|
| 57 |
+
return bytes(output.encode("latin1", "ignore"))
|
| 58 |
+
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
appdirs==1.4.4
|
| 2 |
+
blinker==1.9.0
|
| 3 |
+
click==8.3.0
|
| 4 |
+
colorama==0.4.6
|
| 5 |
+
contourpy==1.3.3
|
| 6 |
+
cycler==0.12.1
|
| 7 |
+
Flask==3.1.2
|
| 8 |
+
flask-cors==6.0.1
|
| 9 |
+
fonttools==4.60.1
|
| 10 |
+
itsdangerous==2.2.0
|
| 11 |
+
Jinja2==3.1.6
|
| 12 |
+
joblib==1.5.2
|
| 13 |
+
kiwisolver==1.4.9
|
| 14 |
+
lightgbm==4.6.0
|
| 15 |
+
lxml==6.0.2
|
| 16 |
+
MarkupSafe==3.0.3
|
| 17 |
+
matplotlib==3.10.7
|
| 18 |
+
numpy==2.3.4
|
| 19 |
+
packaging==25.0
|
| 20 |
+
pandas==2.3.3
|
| 21 |
+
pillow==12.0.0
|
| 22 |
+
pyparsing==3.2.5
|
| 23 |
+
pyshark==0.6
|
| 24 |
+
python-dateutil==2.9.0.post0
|
| 25 |
+
pytz==2025.2
|
| 26 |
+
scapy==2.6.1
|
| 27 |
+
scikit-learn==1.7.2
|
| 28 |
+
scipy==1.16.3
|
| 29 |
+
seaborn==0.13.2
|
| 30 |
+
six==1.17.0
|
| 31 |
+
termcolor==3.2.0
|
| 32 |
+
threadpoolctl==3.6.0
|
| 33 |
+
tzdata==2025.2
|
| 34 |
+
Werkzeug==3.1.3
|
backend/retrain_requests.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"model": "cicids", "expected": "Brute Force -XSS", "predicted": "DoS attacks-Slowloris", "values": [6, 80, 5000000, 2, 2, 120, 120, 60, 60, 2000000, 0, 0, 2000000], "note": "Model is wrong"}
|
backend/routes/__init__.py
ADDED
|
File without changes
|
backend/routes/ai_route.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# routes/ai_route.py
|
| 2 |
+
# --------------------------------------
|
| 3 |
+
from flask import Blueprint, request, jsonify
|
| 4 |
+
from utils.ai_engine import explain_threat, summarize_events
|
| 5 |
+
from utils.logger import get_recent_events, get_active_model
|
| 6 |
+
|
| 7 |
+
ai_bp = Blueprint("ai_bp", __name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@ai_bp.route("/explain", methods=["POST"])
|
| 11 |
+
def ai_explain():
|
| 12 |
+
"""
|
| 13 |
+
Body: JSON event (one row from table)
|
| 14 |
+
Returns: {"explanation": "..."}
|
| 15 |
+
"""
|
| 16 |
+
data = request.get_json() or {}
|
| 17 |
+
try:
|
| 18 |
+
text = explain_threat(data)
|
| 19 |
+
return jsonify({"ok": True, "explanation": text})
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print("AI explain error:", e)
|
| 22 |
+
return jsonify({"ok": False, "error": str(e)}), 500
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@ai_bp.route("/summary", methods=["GET"])
|
| 26 |
+
def ai_summary():
|
| 27 |
+
"""
|
| 28 |
+
Query: ?model=bcc&n=200
|
| 29 |
+
Returns: {"ok": True, "summary": "..."}
|
| 30 |
+
"""
|
| 31 |
+
model = request.args.get("model", get_active_model())
|
| 32 |
+
n = int(request.args.get("n", 200))
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
events = get_recent_events(model, n)
|
| 36 |
+
text = summarize_events(events, model=model)
|
| 37 |
+
return jsonify({"ok": True, "summary": text, "count": len(events), "model": model})
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print("AI summary error:", e)
|
| 40 |
+
return jsonify({"ok": False, "error": str(e)}), 500
|
backend/routes/alerts_route.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, jsonify
|
| 2 |
+
from flask_cors import cross_origin
|
| 3 |
+
from utils.logger import get_recent_events
|
| 4 |
+
from utils.risk_engine import compute_risk_score
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
alerts_bp = Blueprint("alerts", __name__)
|
| 8 |
+
|
| 9 |
+
# ---------------------------------------------------------
|
| 10 |
+
# Deduce risk based on prediction (simple + stable)
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
def classify_risk(prediction):
|
| 13 |
+
if prediction in ["TOR", "I2P", "ZERONET", "FREENET"]:
|
| 14 |
+
return "High"
|
| 15 |
+
if prediction in ["VPN"]:
|
| 16 |
+
return "Medium"
|
| 17 |
+
return "Low"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@alerts_bp.route("/alerts", methods=["GET"])
|
| 21 |
+
@cross_origin()
|
| 22 |
+
def get_alerts():
|
| 23 |
+
"""
|
| 24 |
+
Returns ONLY real alerts (Medium + High)
|
| 25 |
+
with stable risk scoring and time sorting.
|
| 26 |
+
Fully compatible with optimized logger.
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
raw_events = get_recent_events()
|
| 30 |
+
alerts = []
|
| 31 |
+
|
| 32 |
+
for e in raw_events:
|
| 33 |
+
pred = e.get("prediction", "Unknown")
|
| 34 |
+
|
| 35 |
+
# -------------------------------
|
| 36 |
+
# Recompute Risk
|
| 37 |
+
# -------------------------------
|
| 38 |
+
risk = classify_risk(pred)
|
| 39 |
+
|
| 40 |
+
if risk == "Low":
|
| 41 |
+
continue # do NOT include normal traffic
|
| 42 |
+
|
| 43 |
+
# -------------------------------
|
| 44 |
+
# Stable risk score (0-100)
|
| 45 |
+
# -------------------------------
|
| 46 |
+
try:
|
| 47 |
+
risk_score = compute_risk_score(e)
|
| 48 |
+
except:
|
| 49 |
+
# fallback scoring
|
| 50 |
+
risk_score = 90 if risk == "High" else 60
|
| 51 |
+
|
| 52 |
+
# -------------------------------
|
| 53 |
+
# Build alert payload
|
| 54 |
+
# -------------------------------
|
| 55 |
+
alerts.append({
|
| 56 |
+
"timestamp": datetime.now().strftime("%H:%M:%S"),
|
| 57 |
+
"time": e.get("time"),
|
| 58 |
+
"src_ip": e.get("src_ip"),
|
| 59 |
+
"dst_ip": e.get("dst_ip"),
|
| 60 |
+
"sport": e.get("sport", "—"),
|
| 61 |
+
"dport": e.get("dport", "—"),
|
| 62 |
+
"proto": e.get("proto", "-"),
|
| 63 |
+
"prediction": pred,
|
| 64 |
+
"risk_level": risk,
|
| 65 |
+
"risk_score": risk_score,
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
# ------------------------------------------------
|
| 69 |
+
# Sort newest first (based on event time)
|
| 70 |
+
# ------------------------------------------------
|
| 71 |
+
alerts = sorted(alerts, key=lambda x: x["time"], reverse=True)
|
| 72 |
+
|
| 73 |
+
return jsonify({
|
| 74 |
+
"count": len(alerts),
|
| 75 |
+
"alerts": alerts[:150], # limit for UI performance
|
| 76 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
except Exception as err:
|
| 80 |
+
print("❌ Alerts API error:", err)
|
| 81 |
+
return jsonify({"error": str(err)}), 500
|
| 82 |
+
|
backend/routes/chat_route.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from groq import Groq
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
chat_bp = Blueprint("chat_bp", __name__)
|
| 6 |
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 7 |
+
|
| 8 |
+
@chat_bp.route("/chat", methods=["POST"])
|
| 9 |
+
def chat():
|
| 10 |
+
try:
|
| 11 |
+
data = request.get_json()
|
| 12 |
+
msg = data.get("message", "")
|
| 13 |
+
|
| 14 |
+
result = client.chat.completions.create(
|
| 15 |
+
model="llama-3.1-8b-instant",
|
| 16 |
+
messages=[{"role": "user", "content": msg}]
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
reply = result.choices[0].message.content
|
| 20 |
+
|
| 21 |
+
return jsonify({"reply": reply})
|
| 22 |
+
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print("Chat error:", e)
|
| 25 |
+
return jsonify({"error": str(e)}), 500
|
| 26 |
+
|
backend/routes/geo_route.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================
|
| 2 |
+
# 🌍 GEO ROUTE — Adaptive AI NIDS
|
| 3 |
+
# ------------------------------------------
|
| 4 |
+
# ✅ /api/geo/resolve?ip=<ip>
|
| 5 |
+
# ✅ /api/geo/recent
|
| 6 |
+
# ==========================================
|
| 7 |
+
|
| 8 |
+
from flask import Blueprint, jsonify, request
|
| 9 |
+
from utils.geo_lookup import get_geo_info, enrich_event_with_geo
|
| 10 |
+
from utils.logger import get_recent_events
|
| 11 |
+
|
| 12 |
+
geo_bp = Blueprint("geo", __name__)
|
| 13 |
+
|
| 14 |
+
# 🔹 Resolve a single IP (for IPInfoModal)
|
| 15 |
+
@geo_bp.route("/resolve")
|
| 16 |
+
def resolve_ip():
|
| 17 |
+
ip = request.args.get("ip")
|
| 18 |
+
if not ip:
|
| 19 |
+
return jsonify({"error": "Missing IP parameter"}), 400
|
| 20 |
+
info = get_geo_info(ip)
|
| 21 |
+
return jsonify(info), 200
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# 🔹 Return recent events enriched with geo (for map)
|
| 25 |
+
@geo_bp.route("/recent")
|
| 26 |
+
def geo_recent():
|
| 27 |
+
try:
|
| 28 |
+
events = get_recent_events()
|
| 29 |
+
geo_events = [enrich_event_with_geo(e) for e in events[-200:]]
|
| 30 |
+
return jsonify(geo_events), 200
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print("⚠️ Geo recent error:", e)
|
| 33 |
+
return jsonify({"error": str(e)}), 500
|
backend/routes/ip_lookup_route.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import ipaddress
|
| 3 |
+
from flask import Blueprint, jsonify, request
|
| 4 |
+
from flask_cors import cross_origin
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
ip_lookup_bp = Blueprint("ip_lookup", __name__)
|
| 8 |
+
|
| 9 |
+
# 🔹 Cache lookups to reduce API load
|
| 10 |
+
_ip_cache = {}
|
| 11 |
+
|
| 12 |
+
# ======================================
|
| 13 |
+
# 🚨 RISK CLASSIFIER
|
| 14 |
+
# ======================================
|
| 15 |
+
def _guess_risk(org_name: str):
|
| 16 |
+
org = (org_name or "").lower()
|
| 17 |
+
if any(k in org for k in ["tor", "anonym", "i2p"]):
|
| 18 |
+
return {"level": "High", "score": 95, "reason": "Anonymizing service (TOR/I2P detected)"}
|
| 19 |
+
if any(k in org for k in ["vpn", "proxy", "tunnel"]):
|
| 20 |
+
return {"level": "Medium", "score": 80, "reason": "VPN or proxy-based routing"}
|
| 21 |
+
if any(k in org for k in ["aws", "gcp", "digitalocean", "azure", "oracle"]):
|
| 22 |
+
return {"level": "Medium", "score": 70, "reason": "Cloud-hosted server (possible C2 or proxy)"}
|
| 23 |
+
return {"level": "Low", "score": 40, "reason": "Likely clean residential or enterprise IP"}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ======================================
|
| 27 |
+
# ⚙️ IP DATA NORMALIZATION
|
| 28 |
+
# ======================================
|
| 29 |
+
def _normalize_data(ip, d: dict, api_source: str):
|
| 30 |
+
"""Unify structure across ipapi.co and ipwho.is"""
|
| 31 |
+
if not d:
|
| 32 |
+
return {"error": "No data"}
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
if api_source == "ipapi":
|
| 36 |
+
org = d.get("org", "")
|
| 37 |
+
return {
|
| 38 |
+
"ip": ip,
|
| 39 |
+
"city": d.get("city"),
|
| 40 |
+
"region": d.get("region"),
|
| 41 |
+
"country_name": d.get("country_name"),
|
| 42 |
+
"continent_code": d.get("continent_code"),
|
| 43 |
+
"org": org,
|
| 44 |
+
"asn": d.get("asn"),
|
| 45 |
+
"version": d.get("version", "IPv4"),
|
| 46 |
+
"latitude": float(d.get("latitude", 0)),
|
| 47 |
+
"longitude": float(d.get("longitude", 0)),
|
| 48 |
+
"timezone": d.get("timezone"),
|
| 49 |
+
"risk": _guess_risk(org),
|
| 50 |
+
"flag": f"https://flagsapi.com/{d.get('country_code','US')}/flat/32.png"
|
| 51 |
+
}
|
| 52 |
+
elif api_source == "ipwhois":
|
| 53 |
+
org = d.get("connection", {}).get("isp", "")
|
| 54 |
+
return {
|
| 55 |
+
"ip": ip,
|
| 56 |
+
"city": d.get("city"),
|
| 57 |
+
"region": d.get("region"),
|
| 58 |
+
"country_name": d.get("country"),
|
| 59 |
+
"continent_code": d.get("continent"),
|
| 60 |
+
"org": org,
|
| 61 |
+
"asn": d.get("connection", {}).get("asn"),
|
| 62 |
+
"version": d.get("type", "IPv4"),
|
| 63 |
+
"latitude": float(d.get("latitude", 0)),
|
| 64 |
+
"longitude": float(d.get("longitude", 0)),
|
| 65 |
+
"timezone": d.get("timezone"),
|
| 66 |
+
"risk": _guess_risk(org),
|
| 67 |
+
"flag": f"https://flagsapi.com/{d.get('country_code','US')}/flat/32.png"
|
| 68 |
+
}
|
| 69 |
+
except Exception:
|
| 70 |
+
pass
|
| 71 |
+
|
| 72 |
+
return {"error": "Normalization failed"}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ======================================
|
| 76 |
+
# 🔍 LOOKUP (PRIVATE or PUBLIC)
|
| 77 |
+
# ======================================
|
| 78 |
+
def lookup_ip_data(ip: str):
|
| 79 |
+
"""Internal helper for backend components (non-JSON)."""
|
| 80 |
+
try:
|
| 81 |
+
if not ip:
|
| 82 |
+
return {"error": "Empty IP"}
|
| 83 |
+
|
| 84 |
+
# Check cache first
|
| 85 |
+
if ip in _ip_cache:
|
| 86 |
+
return _ip_cache[ip]
|
| 87 |
+
|
| 88 |
+
# Handle local/private IPs
|
| 89 |
+
if ipaddress.ip_address(ip).is_private:
|
| 90 |
+
info = {
|
| 91 |
+
"ip": ip,
|
| 92 |
+
"city": "Bengaluru",
|
| 93 |
+
"region": "Private Range",
|
| 94 |
+
"country_name": "India",
|
| 95 |
+
"org": "Local Device",
|
| 96 |
+
"asn": "LAN",
|
| 97 |
+
"version": "IPv4",
|
| 98 |
+
"latitude": 12.9716,
|
| 99 |
+
"longitude": 77.5946,
|
| 100 |
+
"risk": {"level": "Low", "score": 20, "reason": "Private/local IP"},
|
| 101 |
+
"flag": "https://flagsapi.com/IN/flat/32.png"
|
| 102 |
+
}
|
| 103 |
+
_ip_cache[ip] = info
|
| 104 |
+
return info
|
| 105 |
+
|
| 106 |
+
# === Try ipapi.co ===
|
| 107 |
+
try:
|
| 108 |
+
r = requests.get(f"https://ipapi.co/{ip}/json/", timeout=4)
|
| 109 |
+
if r.ok:
|
| 110 |
+
d = r.json()
|
| 111 |
+
if not d.get("error"):
|
| 112 |
+
info = _normalize_data(ip, d, "ipapi")
|
| 113 |
+
_ip_cache[ip] = info
|
| 114 |
+
return info
|
| 115 |
+
except Exception:
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
# === Fallback: ipwho.is ===
|
| 119 |
+
try:
|
| 120 |
+
r = requests.get(f"https://ipwho.is/{ip}", timeout=4)
|
| 121 |
+
d = r.json()
|
| 122 |
+
if d.get("success"):
|
| 123 |
+
info = _normalize_data(ip, d, "ipwhois")
|
| 124 |
+
_ip_cache[ip] = info
|
| 125 |
+
return info
|
| 126 |
+
except Exception:
|
| 127 |
+
pass
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
return {"error": str(e)}
|
| 131 |
+
|
| 132 |
+
return {"error": "Could not fetch IP info"}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ======================================
|
| 136 |
+
# 🌍 EXTERNAL API ENDPOINT
|
| 137 |
+
# ======================================
|
| 138 |
+
@ip_lookup_bp.route("/lookup/<ip>", methods=["GET"])
|
| 139 |
+
@cross_origin()
|
| 140 |
+
def lookup_ip(ip):
|
| 141 |
+
"""Public API: Look up an IP's geolocation + threat risk."""
|
| 142 |
+
data = lookup_ip_data(ip)
|
| 143 |
+
if "error" in data:
|
| 144 |
+
return jsonify(data), 404
|
| 145 |
+
|
| 146 |
+
data["lookup_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 147 |
+
return jsonify(data)
|
| 148 |
+
|
backend/routes/live_route.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==============================================================
|
| 2 |
+
# live_route.py — Flask routes for controlling live capture
|
| 3 |
+
# ==============================================================
|
| 4 |
+
|
| 5 |
+
from flask import Blueprint, jsonify, request
|
| 6 |
+
from capture.live_manager import sniffer
|
| 7 |
+
import numpy as np
|
| 8 |
+
import math
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
live_bp = Blueprint("live_bp", __name__)
|
| 12 |
+
|
| 13 |
+
@live_bp.route("/start")
|
| 14 |
+
def start_live():
|
| 15 |
+
iface = request.args.get("iface")
|
| 16 |
+
sniffer.start(iface=iface)
|
| 17 |
+
return jsonify({"status": "started", "running": sniffer.is_running()})
|
| 18 |
+
|
| 19 |
+
@live_bp.route("/stop")
|
| 20 |
+
def stop_live():
|
| 21 |
+
sniffer.stop()
|
| 22 |
+
return jsonify({"status": "stopped", "running": sniffer.is_running()})
|
| 23 |
+
|
| 24 |
+
@live_bp.route("/status")
|
| 25 |
+
def status():
|
| 26 |
+
return jsonify({"running": sniffer.is_running()})
|
| 27 |
+
|
| 28 |
+
@live_bp.route("/recent")
|
| 29 |
+
def recent():
|
| 30 |
+
events = sniffer.recent()
|
| 31 |
+
|
| 32 |
+
safe_events = []
|
| 33 |
+
for e in events:
|
| 34 |
+
safe = {}
|
| 35 |
+
for k, v in e.items():
|
| 36 |
+
|
| 37 |
+
# convert numpy ints/floats to python native
|
| 38 |
+
if isinstance(v, (np.generic,)):
|
| 39 |
+
v = v.item()
|
| 40 |
+
|
| 41 |
+
# replace None / NaN with string
|
| 42 |
+
if v is None or (isinstance(v, float) and math.isnan(v)):
|
| 43 |
+
v = "Unknown"
|
| 44 |
+
|
| 45 |
+
safe[str(k)] = v
|
| 46 |
+
|
| 47 |
+
safe_events.append(safe)
|
| 48 |
+
|
| 49 |
+
return jsonify({"events": safe_events}), 200
|
| 50 |
+
|
| 51 |
+
@live_bp.route("/stats")
|
| 52 |
+
def stats():
|
| 53 |
+
return jsonify(sniffer.stats())
|
backend/routes/logs_route.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, send_file, jsonify, request
|
| 2 |
+
import os
|
| 3 |
+
from utils.logger import (
|
| 4 |
+
BCC_LOG_FILE,
|
| 5 |
+
CICIDS_LOG_FILE,
|
| 6 |
+
LOG_FILE,
|
| 7 |
+
get_recent_events,
|
| 8 |
+
get_model_stats,
|
| 9 |
+
clear_last_events,
|
| 10 |
+
delete_by_prediction,
|
| 11 |
+
delete_by_index,
|
| 12 |
+
get_active_model
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
logs_bp = Blueprint("logs", __name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# -------------------------------
|
| 19 |
+
# DOWNLOAD CSV LOG FILE (global)
|
| 20 |
+
# -------------------------------
|
| 21 |
+
@logs_bp.route("/download", methods=["GET"])
|
| 22 |
+
def download_logs():
|
| 23 |
+
model = request.args.get("model")
|
| 24 |
+
|
| 25 |
+
# MODEL-SPECIFIC CSVs
|
| 26 |
+
if model == "bcc":
|
| 27 |
+
path = BCC_LOG_FILE
|
| 28 |
+
elif model == "cicids":
|
| 29 |
+
path = CICIDS_LOG_FILE
|
| 30 |
+
else:
|
| 31 |
+
# fallback — global CSV
|
| 32 |
+
path = LOG_FILE
|
| 33 |
+
|
| 34 |
+
if not os.path.exists(path):
|
| 35 |
+
return jsonify({"error": "Log file not found"}), 404
|
| 36 |
+
|
| 37 |
+
return send_file(
|
| 38 |
+
path,
|
| 39 |
+
as_attachment=True,
|
| 40 |
+
download_name=f"{model}_logs.csv" if model else "traffic_logs.csv",
|
| 41 |
+
mimetype="text/csv",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# -------------------------------
|
| 47 |
+
# DOWNLOAD MODEL-SPECIFIC JSON
|
| 48 |
+
# -------------------------------
|
| 49 |
+
@logs_bp.route("/download/json", methods=["GET"])
|
| 50 |
+
def download_json_logs():
|
| 51 |
+
try:
|
| 52 |
+
model = request.args.get("model", get_active_model())
|
| 53 |
+
events = get_recent_events(model)
|
| 54 |
+
return jsonify({"model": model, "count": len(events), "events": events})
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print("❌ JSON log fetch error:", e)
|
| 57 |
+
return jsonify({"error": "Failed to fetch logs"}), 500
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# -------------------------------
|
| 61 |
+
# CLEAR MODEL-WISE LAST N EVENTS
|
| 62 |
+
# -------------------------------
|
| 63 |
+
@logs_bp.route("/clear", methods=["POST"])
|
| 64 |
+
def clear_logs():
|
| 65 |
+
try:
|
| 66 |
+
model = request.args.get("model", get_active_model())
|
| 67 |
+
n = int(request.args.get("n", 50))
|
| 68 |
+
|
| 69 |
+
clear_last_events(model, n)
|
| 70 |
+
|
| 71 |
+
print(f"🧹 Cleared last {n} events for model={model}")
|
| 72 |
+
return jsonify({"status": "ok", "deleted": n, "model": model})
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print("❌ Clear logs error:", e)
|
| 75 |
+
return jsonify({"error": str(e)}), 500
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# -------------------------------
|
| 79 |
+
# CLEAR MODEL-WISE BY PREDICTION
|
| 80 |
+
# -------------------------------
|
| 81 |
+
@logs_bp.route("/clear_pred", methods=["POST"])
|
| 82 |
+
def clear_pred():
|
| 83 |
+
pred = request.args.get("pred")
|
| 84 |
+
model = request.args.get("model", get_active_model())
|
| 85 |
+
|
| 86 |
+
if not pred:
|
| 87 |
+
return jsonify({"error": "Missing 'pred' parameter"}), 400
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
delete_by_prediction(model, pred)
|
| 91 |
+
print(f"🧹 Deleted all events for prediction={pred} in model={model}")
|
| 92 |
+
return jsonify({"status": "ok", "deleted_pred": pred, "model": model})
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print("❌ Clear prediction error:", e)
|
| 95 |
+
return jsonify({"error": str(e)}), 500
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# -------------------------------
|
| 99 |
+
# DELETE ONE ROW MODEL-WISE
|
| 100 |
+
# -------------------------------
|
| 101 |
+
@logs_bp.route("/delete_one", methods=["POST"])
|
| 102 |
+
def delete_one():
|
| 103 |
+
try:
|
| 104 |
+
model = request.args.get("model", get_active_model())
|
| 105 |
+
idx = int(request.args.get("index", -1))
|
| 106 |
+
|
| 107 |
+
ok = delete_by_index(model, idx)
|
| 108 |
+
|
| 109 |
+
if ok:
|
| 110 |
+
print(f"🗑️ Deleted row index={idx} from model={model}")
|
| 111 |
+
return jsonify({"status": "ok", "index": idx, "model": model})
|
| 112 |
+
else:
|
| 113 |
+
return jsonify({"status": "invalid index", "index": idx}), 400
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print("❌ Delete row error:", e)
|
| 116 |
+
return jsonify({"error": str(e)}), 500
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# -------------------------------
|
| 120 |
+
# MODEL-WISE LOG STATUS
|
| 121 |
+
# -------------------------------
|
| 122 |
+
@logs_bp.route("/status", methods=["GET"])
|
| 123 |
+
def log_status():
|
| 124 |
+
try:
|
| 125 |
+
model = request.args.get("model", get_active_model())
|
| 126 |
+
counts = get_model_stats(model)
|
| 127 |
+
total = sum(counts.values())
|
| 128 |
+
|
| 129 |
+
return jsonify({
|
| 130 |
+
"model": model,
|
| 131 |
+
"total_events": total,
|
| 132 |
+
"by_class": counts
|
| 133 |
+
})
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print("❌ Log status error:", e)
|
| 136 |
+
return jsonify({"error": str(e)}), 500
|
| 137 |
+
|
backend/routes/manual_predict_route.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify
|
| 2 |
+
from utils.model_selector import get_active_model, load_model
|
| 3 |
+
import numpy as np
|
| 4 |
+
import traceback
|
| 5 |
+
import math
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
manual_predict = Blueprint("manual_predict", __name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _reliability_score_from_count(count):
|
| 12 |
+
# simple monotonic score: log-scale so diminishing returns for many samples
|
| 13 |
+
# returns 0-100
|
| 14 |
+
if count is None:
|
| 15 |
+
return None
|
| 16 |
+
try:
|
| 17 |
+
c = float(count)
|
| 18 |
+
score = 20 + min(75, math.log10(c + 1) * 18) # tuned curve
|
| 19 |
+
return round(min(100, score), 1)
|
| 20 |
+
except Exception:
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@manual_predict.route("/predict_manual", methods=["POST"])
|
| 25 |
+
def predict_manual():
|
| 26 |
+
data = request.get_json(force=True, silent=True) or {}
|
| 27 |
+
|
| 28 |
+
model_name = data.get("model")
|
| 29 |
+
values = data.get("values") # expecting a LIST (array)
|
| 30 |
+
if not model_name or not isinstance(values, list):
|
| 31 |
+
return jsonify({
|
| 32 |
+
"error": "Expect JSON: { model: 'cicids'|'bcc', values: [v1, v2, ...] }"
|
| 33 |
+
}), 400
|
| 34 |
+
|
| 35 |
+
bundle = load_model(model_name)
|
| 36 |
+
model = bundle.get("model")
|
| 37 |
+
artifacts = bundle.get("artifacts") or {}
|
| 38 |
+
|
| 39 |
+
if model is None:
|
| 40 |
+
return jsonify({"error": "Model not loaded"}), 500
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
# Common metadata
|
| 44 |
+
model_info = {
|
| 45 |
+
"model_name": model_name,
|
| 46 |
+
"features": artifacts.get("features") or artifacts.get("feature_list") or bundle.get("features") or None,
|
| 47 |
+
"classes": None,
|
| 48 |
+
"train_counts": artifacts.get("train_counts") or artifacts.get("class_counts") or None,
|
| 49 |
+
"scaler_present": bool(artifacts.get("scaler")) or bool(bundle.get("scaler")),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
# helper to decode label_map / encoder (checks artifacts first, then bundle)
|
| 53 |
+
def decode_label(raw):
|
| 54 |
+
try:
|
| 55 |
+
# artifacts label_map (mapping value->name)
|
| 56 |
+
if artifacts.get("label_map"):
|
| 57 |
+
inv = {v: k for k, v in artifacts["label_map"].items()}
|
| 58 |
+
return inv.get(int(raw), str(raw))
|
| 59 |
+
|
| 60 |
+
# artifacts label_encoder
|
| 61 |
+
if artifacts.get("label_encoder"):
|
| 62 |
+
return artifacts["label_encoder"].inverse_transform([int(raw)])[0]
|
| 63 |
+
|
| 64 |
+
# bundle-level encoder (e.g. realtime_encoder.pkl loaded in bundle)
|
| 65 |
+
if bundle.get("encoder"):
|
| 66 |
+
return bundle["encoder"].inverse_transform([int(raw)])[0]
|
| 67 |
+
except Exception as e:
|
| 68 |
+
# decoding failed; log and fallback to str
|
| 69 |
+
print("[decode_label] ERROR:", e)
|
| 70 |
+
# fallback: if raw already a string return it, else stringified raw
|
| 71 |
+
return str(raw)
|
| 72 |
+
|
| 73 |
+
# CICIDS (13 features expected)
|
| 74 |
+
if model_name == "cicids":
|
| 75 |
+
feature_list = model_info["features"]
|
| 76 |
+
if not feature_list:
|
| 77 |
+
return jsonify({"error": "CICIDS artifacts missing 'features' list"}), 500
|
| 78 |
+
|
| 79 |
+
if len(values) != len(feature_list):
|
| 80 |
+
return jsonify({
|
| 81 |
+
"error": f"CICIDS needs {len(feature_list)} features, received {len(values)}"
|
| 82 |
+
}), 400
|
| 83 |
+
|
| 84 |
+
X = np.array([[float(x) for x in values]], dtype=float)
|
| 85 |
+
|
| 86 |
+
# apply scaler if present in artifacts or bundle
|
| 87 |
+
scaler = artifacts.get("scaler") or bundle.get("scaler")
|
| 88 |
+
scaled_row = None
|
| 89 |
+
try:
|
| 90 |
+
if scaler is not None:
|
| 91 |
+
scaled = scaler.transform(X)
|
| 92 |
+
scaled_row = np.array(scaled).tolist()
|
| 93 |
+
Xs = scaled
|
| 94 |
+
else:
|
| 95 |
+
Xs = X
|
| 96 |
+
except Exception as e:
|
| 97 |
+
# fallback to raw X if scaler fails
|
| 98 |
+
print("[predict_manual][CICIDS] scaler error:", e)
|
| 99 |
+
Xs = X
|
| 100 |
+
|
| 101 |
+
# predict
|
| 102 |
+
pred_raw = model.predict(Xs)[0]
|
| 103 |
+
pred_label = decode_label(pred_raw)
|
| 104 |
+
|
| 105 |
+
# probabilities
|
| 106 |
+
proba_max = None
|
| 107 |
+
probs = None
|
| 108 |
+
try:
|
| 109 |
+
if hasattr(model, "predict_proba"):
|
| 110 |
+
p = model.predict_proba(Xs)[0]
|
| 111 |
+
probs = [float(x) for x in p]
|
| 112 |
+
proba_max = float(max(p))
|
| 113 |
+
except Exception:
|
| 114 |
+
pass
|
| 115 |
+
|
| 116 |
+
# fill model_info.classes if possible (prefer encoder classes)
|
| 117 |
+
try:
|
| 118 |
+
if artifacts.get("label_encoder"):
|
| 119 |
+
model_info["classes"] = list(artifacts["label_encoder"].classes_)
|
| 120 |
+
elif bundle.get("encoder"):
|
| 121 |
+
model_info["classes"] = list(bundle["encoder"].classes_)
|
| 122 |
+
elif hasattr(model, "classes_"):
|
| 123 |
+
model_info["classes"] = [str(c) for c in model.classes_]
|
| 124 |
+
except Exception:
|
| 125 |
+
pass
|
| 126 |
+
|
| 127 |
+
# compute reliability
|
| 128 |
+
train_counts = model_info.get("train_counts")
|
| 129 |
+
reliability = None
|
| 130 |
+
if train_counts and isinstance(train_counts, dict):
|
| 131 |
+
# try to get count for the predicted label (string keys)
|
| 132 |
+
reliability = _reliability_score_from_count(
|
| 133 |
+
train_counts.get(str(pred_label)) or train_counts.get(pred_raw)
|
| 134 |
+
)
|
| 135 |
+
elif train_counts and isinstance(train_counts, list):
|
| 136 |
+
reliability = _reliability_score_from_count(sum(train_counts) / len(train_counts))
|
| 137 |
+
else:
|
| 138 |
+
reliability = None
|
| 139 |
+
|
| 140 |
+
resp = {
|
| 141 |
+
"prediction": pred_label,
|
| 142 |
+
"pred_raw": str(pred_raw),
|
| 143 |
+
"confidence": proba_max,
|
| 144 |
+
"proba_max": proba_max,
|
| 145 |
+
"probs": probs,
|
| 146 |
+
"raw_row": X.tolist()[0],
|
| 147 |
+
"scaled_row": scaled_row,
|
| 148 |
+
"model_info": model_info,
|
| 149 |
+
"reliability": reliability
|
| 150 |
+
}
|
| 151 |
+
return jsonify(resp)
|
| 152 |
+
|
| 153 |
+
# BCC (15 features expected)
|
| 154 |
+
elif model_name == "bcc":
|
| 155 |
+
EXPECTED = 15
|
| 156 |
+
if len(values) != EXPECTED:
|
| 157 |
+
return jsonify({
|
| 158 |
+
"error": f"BCC needs {EXPECTED} features, received {len(values)}"
|
| 159 |
+
}), 400
|
| 160 |
+
|
| 161 |
+
X = np.array([[float(x) for x in values]], dtype=float)
|
| 162 |
+
|
| 163 |
+
scaler = bundle.get("scaler") or artifacts.get("scaler")
|
| 164 |
+
scaled_row = None
|
| 165 |
+
try:
|
| 166 |
+
if scaler is not None:
|
| 167 |
+
scaled = scaler.transform(X)
|
| 168 |
+
scaled_row = np.array(scaled).tolist()
|
| 169 |
+
Xs = scaled
|
| 170 |
+
else:
|
| 171 |
+
Xs = X
|
| 172 |
+
except Exception as e:
|
| 173 |
+
print("[predict_manual][BCC] scaler error:", e)
|
| 174 |
+
Xs = X
|
| 175 |
+
|
| 176 |
+
pred_raw = model.predict(Xs)[0]
|
| 177 |
+
pred_label = decode_label(pred_raw)
|
| 178 |
+
|
| 179 |
+
proba_max = None
|
| 180 |
+
probs = None
|
| 181 |
+
try:
|
| 182 |
+
if hasattr(model, "predict_proba"):
|
| 183 |
+
p = model.predict_proba(Xs)[0]
|
| 184 |
+
probs = [float(x) for x in p]
|
| 185 |
+
proba_max = float(max(p))
|
| 186 |
+
except Exception:
|
| 187 |
+
pass
|
| 188 |
+
|
| 189 |
+
# model_info classes: prefer encoder classes if present
|
| 190 |
+
try:
|
| 191 |
+
encoder = bundle.get("encoder") or artifacts.get("label_encoder")
|
| 192 |
+
if encoder is not None:
|
| 193 |
+
model_info["classes"] = list(encoder.classes_)
|
| 194 |
+
elif hasattr(model, "classes_"):
|
| 195 |
+
# fallback - often these are numeric indices
|
| 196 |
+
model_info["classes"] = [str(c) for c in model.classes_]
|
| 197 |
+
except Exception:
|
| 198 |
+
pass
|
| 199 |
+
|
| 200 |
+
train_counts = model_info.get("train_counts")
|
| 201 |
+
reliability = None
|
| 202 |
+
if train_counts and isinstance(train_counts, dict):
|
| 203 |
+
reliability = _reliability_score_from_count(
|
| 204 |
+
train_counts.get(str(pred_label)) or train_counts.get(pred_raw)
|
| 205 |
+
)
|
| 206 |
+
elif train_counts:
|
| 207 |
+
reliability = _reliability_score_from_count(sum(train_counts) / len(train_counts))
|
| 208 |
+
|
| 209 |
+
resp = {
|
| 210 |
+
"prediction": pred_label,
|
| 211 |
+
"pred_raw": str(pred_raw),
|
| 212 |
+
"confidence": proba_max,
|
| 213 |
+
"proba_max": proba_max,
|
| 214 |
+
"probs": probs,
|
| 215 |
+
"raw_row": X.tolist()[0],
|
| 216 |
+
"scaled_row": scaled_row,
|
| 217 |
+
"model_info": model_info,
|
| 218 |
+
"reliability": reliability
|
| 219 |
+
}
|
| 220 |
+
return jsonify(resp)
|
| 221 |
+
|
| 222 |
+
else:
|
| 223 |
+
return jsonify({"error": "unsupported model"}), 400
|
| 224 |
+
|
| 225 |
+
except Exception as e:
|
| 226 |
+
print("[predict_manual] Exception:", e)
|
| 227 |
+
return jsonify({"error": str(e)}), 500
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@manual_predict.route("/predict_debug", methods=["POST"])
|
| 231 |
+
def predict_debug():
|
| 232 |
+
"""
|
| 233 |
+
Debug endpoint: returns raw ordered vector, scaled vector (if scaler),
|
| 234 |
+
model classes, prediction, predict_proba (if available), and artifacts info.
|
| 235 |
+
Use this to compare what you *intend* to send vs what model receives.
|
| 236 |
+
"""
|
| 237 |
+
try:
|
| 238 |
+
data = request.get_json(force=True, silent=True) or {}
|
| 239 |
+
model_name = data.get("model")
|
| 240 |
+
feats = data.get("features")
|
| 241 |
+
if not model_name or not isinstance(feats, dict):
|
| 242 |
+
return jsonify({"error": "Provide JSON {model: 'cicids'|'bcc', features: {...}}"}), 400
|
| 243 |
+
|
| 244 |
+
bundle = load_model(model_name)
|
| 245 |
+
model = bundle.get("model")
|
| 246 |
+
artifacts = bundle.get("artifacts") or {}
|
| 247 |
+
|
| 248 |
+
if model is None:
|
| 249 |
+
return jsonify({"error": "Model not loaded"}), 500
|
| 250 |
+
|
| 251 |
+
debug = {"model_name": model_name}
|
| 252 |
+
|
| 253 |
+
if model_name == "cicids":
|
| 254 |
+
feature_list = artifacts.get("features")
|
| 255 |
+
debug["artifact_features"] = feature_list
|
| 256 |
+
# Build ordered row (float)
|
| 257 |
+
row = [float(feats.get(f, 0.0)) for f in (feature_list or [])]
|
| 258 |
+
debug["raw_row"] = row
|
| 259 |
+
|
| 260 |
+
X = np.array([row], dtype=float)
|
| 261 |
+
|
| 262 |
+
scaler = artifacts.get("scaler") or bundle.get("scaler")
|
| 263 |
+
if scaler is not None:
|
| 264 |
+
try:
|
| 265 |
+
Xs = scaler.transform(X)
|
| 266 |
+
debug["scaled_row"] = np.array(Xs).tolist()
|
| 267 |
+
except Exception as e:
|
| 268 |
+
debug["scaler_error"] = str(e)
|
| 269 |
+
Xs = X
|
| 270 |
+
else:
|
| 271 |
+
Xs = X
|
| 272 |
+
debug["scaled_row"] = None
|
| 273 |
+
|
| 274 |
+
# predict
|
| 275 |
+
try:
|
| 276 |
+
pred_raw = model.predict(Xs)[0]
|
| 277 |
+
debug["pred_raw"] = repr(pred_raw)
|
| 278 |
+
# classes
|
| 279 |
+
try:
|
| 280 |
+
debug["model_classes"] = [str(c) for c in getattr(model, "classes_", [])]
|
| 281 |
+
except Exception:
|
| 282 |
+
debug["model_classes"] = None
|
| 283 |
+
# proba
|
| 284 |
+
if hasattr(model, "predict_proba"):
|
| 285 |
+
try:
|
| 286 |
+
probs = model.predict_proba(Xs)[0].tolist()
|
| 287 |
+
debug["probs"] = probs
|
| 288 |
+
debug["proba_max"] = max(probs)
|
| 289 |
+
except Exception as e:
|
| 290 |
+
debug["proba_error"] = str(e)
|
| 291 |
+
# decode label
|
| 292 |
+
label = str(pred_raw)
|
| 293 |
+
try:
|
| 294 |
+
if artifacts.get("label_map"):
|
| 295 |
+
inv = {v: k for k, v in artifacts["label_map"].items()}
|
| 296 |
+
label = inv.get(int(pred_raw), str(pred_raw))
|
| 297 |
+
elif artifacts.get("label_encoder"):
|
| 298 |
+
label = artifacts["label_encoder"].inverse_transform([int(pred_raw)])[0]
|
| 299 |
+
elif bundle.get("encoder"):
|
| 300 |
+
label = bundle["encoder"].inverse_transform([int(pred_raw)])[0]
|
| 301 |
+
except Exception as e:
|
| 302 |
+
debug["label_decode_error"] = str(e)
|
| 303 |
+
|
| 304 |
+
debug["label"] = label
|
| 305 |
+
except Exception as e:
|
| 306 |
+
debug["predict_error"] = str(e)
|
| 307 |
+
debug["predict_tb"] = traceback.format_exc()
|
| 308 |
+
|
| 309 |
+
return jsonify(debug)
|
| 310 |
+
|
| 311 |
+
elif model_name == "bcc":
|
| 312 |
+
# BCC: we will attempt to build 15-element row from expected keys or values
|
| 313 |
+
BCC_FEATURES = [
|
| 314 |
+
"proto", "src_port", "dst_port", "flow_duration", "total_fwd_pkts",
|
| 315 |
+
"total_bwd_pkts", "flags_numeric", "payload_len", "header_len",
|
| 316 |
+
"rate", "iat", "syn", "ack", "rst", "fin"
|
| 317 |
+
]
|
| 318 |
+
debug["expected_bcc_features"] = BCC_FEATURES
|
| 319 |
+
if all(k in feats for k in BCC_FEATURES):
|
| 320 |
+
row = [float(feats.get(k, 0.0)) for k in BCC_FEATURES]
|
| 321 |
+
else:
|
| 322 |
+
vals = list(feats.values())
|
| 323 |
+
vals = [float(v) if (v is not None and str(v).strip() != "") else 0.0 for v in vals]
|
| 324 |
+
if len(vals) < 15:
|
| 325 |
+
vals = vals + [0.0] * (15 - len(vals))
|
| 326 |
+
row = vals[:15]
|
| 327 |
+
debug["raw_row"] = row
|
| 328 |
+
X = np.array([row], dtype=float)
|
| 329 |
+
|
| 330 |
+
# try scaler from bundle or artifacts
|
| 331 |
+
scaler = bundle.get("scaler") or artifacts.get("scaler")
|
| 332 |
+
if scaler is not None:
|
| 333 |
+
try:
|
| 334 |
+
Xs = scaler.transform(X)
|
| 335 |
+
debug["scaled_row"] = np.array(Xs).tolist()
|
| 336 |
+
except Exception as e:
|
| 337 |
+
debug["scaler_error"] = str(e)
|
| 338 |
+
Xs = X
|
| 339 |
+
else:
|
| 340 |
+
Xs = X
|
| 341 |
+
debug["scaled_row"] = None
|
| 342 |
+
|
| 343 |
+
try:
|
| 344 |
+
pred_raw = model.predict(Xs)[0]
|
| 345 |
+
debug["pred_raw"] = repr(pred_raw)
|
| 346 |
+
# model raw classes (may be numeric)
|
| 347 |
+
debug["model_classes"] = [str(c) for c in getattr(model, "classes_", [])]
|
| 348 |
+
if hasattr(model, "predict_proba"):
|
| 349 |
+
try:
|
| 350 |
+
probs = model.predict_proba(Xs)[0].tolist()
|
| 351 |
+
debug["probs"] = probs
|
| 352 |
+
debug["proba_max"] = max(probs)
|
| 353 |
+
except Exception as e:
|
| 354 |
+
debug["proba_error"] = str(e)
|
| 355 |
+
# decode using encoder if present (bundle or artifacts)
|
| 356 |
+
label = str(pred_raw)
|
| 357 |
+
try:
|
| 358 |
+
encoder = bundle.get("encoder") or artifacts.get("label_encoder")
|
| 359 |
+
if encoder:
|
| 360 |
+
label = encoder.inverse_transform([int(pred_raw)])[0]
|
| 361 |
+
except Exception as e:
|
| 362 |
+
debug["label_decode_error"] = str(e)
|
| 363 |
+
debug["label"] = label
|
| 364 |
+
except Exception as e:
|
| 365 |
+
debug["predict_error"] = str(e)
|
| 366 |
+
debug["predict_tb"] = traceback.format_exc()
|
| 367 |
+
|
| 368 |
+
return jsonify(debug)
|
| 369 |
+
|
| 370 |
+
else:
|
| 371 |
+
return jsonify({"error": "unsupported model"}), 400
|
| 372 |
+
|
| 373 |
+
except Exception as e:
|
| 374 |
+
return jsonify({"error": str(e), "tb": traceback.format_exc()}), 500
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
@manual_predict.route("/retrain_request", methods=["POST"])
|
| 378 |
+
def retrain_request():
|
| 379 |
+
data = request.get_json() or {}
|
| 380 |
+
# Save retrain request to a file for later processing
|
| 381 |
+
with open("retrain_requests.jsonl", "a") as f:
|
| 382 |
+
f.write(json.dumps(data) + "\n")
|
| 383 |
+
|
| 384 |
+
return jsonify({"status": "saved", "msg": "Retrain request recorded"})
|
backend/routes/ml_route.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==========================================================
|
| 2 |
+
# backend/routes/ml_route.py
|
| 3 |
+
# Adaptive AI Framework - ML Route for NIDS Intelligence
|
| 4 |
+
# ==========================================================
|
| 5 |
+
|
| 6 |
+
from flask import Blueprint, request, jsonify
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
import os
|
| 10 |
+
import joblib
|
| 11 |
+
import random
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from flask_cors import cross_origin
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
ml_bp = Blueprint("ml_bp", __name__)
|
| 17 |
+
|
| 18 |
+
ML_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ml_models"))
|
| 19 |
+
|
| 20 |
+
# In-memory global stores
|
| 21 |
+
MODELS = {}
|
| 22 |
+
RETRAIN_STATUS = {"running": False, "progress": 0, "message": "", "last_result": None}
|
| 23 |
+
METRICS_CACHE = {}
|
| 24 |
+
|
| 25 |
+
# Your NIDS feature list (15)
|
| 26 |
+
FEATURE_NAMES = [
|
| 27 |
+
"protocol", "src_port", "dst_port", "duration", "packets_count",
|
| 28 |
+
"fwd_packets_count", "bwd_packets_count", "total_payload_bytes",
|
| 29 |
+
"total_header_bytes", "bytes_rate", "packets_rate",
|
| 30 |
+
"syn_flag_counts", "ack_flag_counts", "rst_flag_counts", "fin_flag_counts"
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
# ==========================================================
|
| 34 |
+
# 🧠 Model Management
|
| 35 |
+
# ==========================================================
|
| 36 |
+
|
| 37 |
+
def try_load_models():
|
| 38 |
+
"""Load models from disk (if available)."""
|
| 39 |
+
global MODELS
|
| 40 |
+
MODELS = {}
|
| 41 |
+
try:
|
| 42 |
+
files = os.listdir(ML_DIR)
|
| 43 |
+
except Exception:
|
| 44 |
+
files = []
|
| 45 |
+
|
| 46 |
+
for fname in files:
|
| 47 |
+
if fname.endswith(".pkl"):
|
| 48 |
+
name = os.path.splitext(fname)[0]
|
| 49 |
+
try:
|
| 50 |
+
m = joblib.load(os.path.join(ML_DIR, fname))
|
| 51 |
+
MODELS[name] = {"obj": m, "name": name, "path": os.path.join(ML_DIR, fname)}
|
| 52 |
+
except Exception as e:
|
| 53 |
+
MODELS[name] = {"obj": None, "name": name, "path": os.path.join(ML_DIR, fname), "load_error": str(e)}
|
| 54 |
+
|
| 55 |
+
try_load_models()
|
| 56 |
+
|
| 57 |
+
def model_summary(name, entry):
|
| 58 |
+
obj = entry.get("obj")
|
| 59 |
+
info = {
|
| 60 |
+
"id": name,
|
| 61 |
+
"name": name,
|
| 62 |
+
"type": type(obj).__name__ if obj is not None else "Unknown",
|
| 63 |
+
"accuracy": None,
|
| 64 |
+
"f1_score": None,
|
| 65 |
+
"dataset": "unknown",
|
| 66 |
+
"status": "Active" if obj is not None else "Unavailable",
|
| 67 |
+
"last_trained": None,
|
| 68 |
+
}
|
| 69 |
+
meta = getattr(obj, "metadata", None)
|
| 70 |
+
if isinstance(meta, dict):
|
| 71 |
+
info.update({
|
| 72 |
+
"accuracy": meta.get("accuracy"),
|
| 73 |
+
"f1_score": meta.get("f1_score"),
|
| 74 |
+
"dataset": meta.get("dataset"),
|
| 75 |
+
"last_trained": meta.get("last_trained"),
|
| 76 |
+
})
|
| 77 |
+
return info
|
| 78 |
+
|
| 79 |
+
# ==========================================================
|
| 80 |
+
# 📦 ROUTES
|
| 81 |
+
# ==========================================================
|
| 82 |
+
|
| 83 |
+
@ml_bp.route("/ml/models", methods=["GET"])
|
| 84 |
+
def list_models():
|
| 85 |
+
"""Return all available models."""
|
| 86 |
+
try_load_models()
|
| 87 |
+
out = [model_summary(name, entry) for name, entry in MODELS.items()]
|
| 88 |
+
|
| 89 |
+
if not out:
|
| 90 |
+
out = [{
|
| 91 |
+
"id": "placeholder_model",
|
| 92 |
+
"name": "Placeholder Detector",
|
| 93 |
+
"type": "Simulated",
|
| 94 |
+
"accuracy": 92.1,
|
| 95 |
+
"f1_score": 0.90,
|
| 96 |
+
"dataset": "Simulated-NIDS",
|
| 97 |
+
"status": "Active",
|
| 98 |
+
"last_trained": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 99 |
+
}]
|
| 100 |
+
return jsonify(out)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@ml_bp.route("/ml/metrics", methods=["GET"])
|
| 104 |
+
def get_metrics():
|
| 105 |
+
"""Return metrics like accuracy history & class distribution."""
|
| 106 |
+
if METRICS_CACHE:
|
| 107 |
+
return jsonify(METRICS_CACHE)
|
| 108 |
+
|
| 109 |
+
accuracy_history = [
|
| 110 |
+
{"epoch": i + 1, "accuracy": round(0.8 + i * 0.04 + random.random() * 0.01, 3)}
|
| 111 |
+
for i in range(10)
|
| 112 |
+
]
|
| 113 |
+
class_distribution = {
|
| 114 |
+
"Normal": 1500,
|
| 115 |
+
"DDoS": 420,
|
| 116 |
+
"PortScan": 260,
|
| 117 |
+
"Botnet": 140,
|
| 118 |
+
"VPN": 120,
|
| 119 |
+
"TOR": 90
|
| 120 |
+
}
|
| 121 |
+
METRICS_CACHE.update({
|
| 122 |
+
"accuracy_history": accuracy_history,
|
| 123 |
+
"class_distribution": class_distribution
|
| 124 |
+
})
|
| 125 |
+
return jsonify(METRICS_CACHE)
|
| 126 |
+
|
| 127 |
+
# ==========================================================
|
| 128 |
+
# 🔮 PREDICTION ENDPOINT
|
| 129 |
+
# ==========================================================
|
| 130 |
+
|
| 131 |
+
def safe_predict_with_model(model_entry, features):
|
| 132 |
+
"""Try to predict with a loaded model."""
|
| 133 |
+
obj = model_entry.get("obj")
|
| 134 |
+
if obj is None:
|
| 135 |
+
return None
|
| 136 |
+
try:
|
| 137 |
+
if isinstance(features, dict):
|
| 138 |
+
features_list = [features.get(k, 0) for k in FEATURE_NAMES]
|
| 139 |
+
else:
|
| 140 |
+
features_list = features
|
| 141 |
+
X = [features_list]
|
| 142 |
+
|
| 143 |
+
if hasattr(obj, "predict_proba"):
|
| 144 |
+
probs = obj.predict_proba(X)[0]
|
| 145 |
+
pred_idx = int(np.argmax(probs))
|
| 146 |
+
pred_label = obj.classes_[pred_idx] if hasattr(obj, "classes_") else str(pred_idx)
|
| 147 |
+
return {"prediction": str(pred_label), "confidence": float(round(probs[pred_idx], 4))}
|
| 148 |
+
else:
|
| 149 |
+
pred = obj.predict(X)[0]
|
| 150 |
+
return {"prediction": str(pred), "confidence": 1.0}
|
| 151 |
+
except Exception:
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@ml_bp.route("/ml/predict-test", methods=["POST"])
|
| 156 |
+
def predict_test():
|
| 157 |
+
"""Accept feature dict and return prediction result."""
|
| 158 |
+
data = request.get_json() or {}
|
| 159 |
+
features = data.get("features") or data.get("sample")
|
| 160 |
+
model_name = data.get("model")
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
if not features:
|
| 164 |
+
return jsonify({"error": "No features provided"}), 400
|
| 165 |
+
|
| 166 |
+
try_load_models()
|
| 167 |
+
chosen = None
|
| 168 |
+
if model_name and model_name in MODELS:
|
| 169 |
+
chosen = MODELS[model_name]
|
| 170 |
+
else:
|
| 171 |
+
for k, e in MODELS.items():
|
| 172 |
+
if e.get("obj") is not None:
|
| 173 |
+
chosen = e
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
if chosen:
|
| 177 |
+
res = safe_predict_with_model(chosen, features)
|
| 178 |
+
if res:
|
| 179 |
+
res.update({"model_used": chosen.get("name")})
|
| 180 |
+
return jsonify(res)
|
| 181 |
+
|
| 182 |
+
# Fallback simulated prediction
|
| 183 |
+
classes = ["Normal", "DDoS", "PortScan", "Botnet", "VPN", "TOR"]
|
| 184 |
+
pred = random.choice(classes)
|
| 185 |
+
confidence = round(random.uniform(0.7, 0.99), 3)
|
| 186 |
+
return jsonify({
|
| 187 |
+
"prediction": pred,
|
| 188 |
+
"confidence": confidence,
|
| 189 |
+
"model_used": "SimulatedDetector",
|
| 190 |
+
"used_features": FEATURE_NAMES
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
return jsonify({"error": str(e)}), 500
|
| 195 |
+
|
| 196 |
+
# ==========================================================
|
| 197 |
+
# ⚙️ RETRAIN SIMULATION
|
| 198 |
+
# ==========================================================
|
| 199 |
+
|
| 200 |
+
def _retrain_job(model_id=None, epochs=6):
|
| 201 |
+
RETRAIN_STATUS["running"] = True
|
| 202 |
+
RETRAIN_STATUS["progress"] = 0
|
| 203 |
+
RETRAIN_STATUS["message"] = "Starting retrain..."
|
| 204 |
+
best_acc = 0.0
|
| 205 |
+
try:
|
| 206 |
+
for e in range(1, epochs + 1):
|
| 207 |
+
RETRAIN_STATUS["message"] = f"Epoch {e}/{epochs}..."
|
| 208 |
+
for p in range(5):
|
| 209 |
+
time.sleep(0.45)
|
| 210 |
+
RETRAIN_STATUS["progress"] = int(((e - 1) * 100 / epochs) + (p + 1) * (100 / (epochs * 5)))
|
| 211 |
+
best_acc = round(0.85 + (e * 0.02) + random.random() * 0.01, 4)
|
| 212 |
+
RETRAIN_STATUS["message"] = f"Epoch {e} finished. acc: {best_acc}"
|
| 213 |
+
RETRAIN_STATUS["message"] = "Finalizing..."
|
| 214 |
+
time.sleep(0.6)
|
| 215 |
+
RETRAIN_STATUS["last_result"] = {
|
| 216 |
+
"accuracy": best_acc,
|
| 217 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 218 |
+
}
|
| 219 |
+
METRICS_CACHE.setdefault("accuracy_history", []).append({
|
| 220 |
+
"epoch": len(METRICS_CACHE.get("accuracy_history", [])) + 1,
|
| 221 |
+
"accuracy": best_acc
|
| 222 |
+
})
|
| 223 |
+
except Exception as e:
|
| 224 |
+
RETRAIN_STATUS["message"] = f"Error: {e}"
|
| 225 |
+
finally:
|
| 226 |
+
RETRAIN_STATUS["running"] = False
|
| 227 |
+
RETRAIN_STATUS["progress"] = 100
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@ml_bp.route("/ml/retrain", methods=["POST"])
|
| 231 |
+
def retrain():
|
| 232 |
+
"""Start retraining in background thread."""
|
| 233 |
+
if RETRAIN_STATUS.get("running"):
|
| 234 |
+
return jsonify({"error": "Retrain already in progress"}), 409
|
| 235 |
+
payload = request.get_json() or {}
|
| 236 |
+
model_id = payload.get("model")
|
| 237 |
+
epochs = int(payload.get("epochs", 6))
|
| 238 |
+
t = threading.Thread(target=_retrain_job, args=(model_id, epochs), daemon=True)
|
| 239 |
+
t.start()
|
| 240 |
+
return jsonify({"message": "Retrain started", "epochs": epochs})
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@ml_bp.route("/ml/retrain/status", methods=["GET"])
|
| 244 |
+
def retrain_status():
|
| 245 |
+
"""Get retrain progress."""
|
| 246 |
+
return jsonify(RETRAIN_STATUS)
|
| 247 |
+
|
| 248 |
+
# ==========================================================
|
| 249 |
+
# 🧩 FEATURE IMPORTANCE
|
| 250 |
+
# ==========================================================
|
| 251 |
+
|
| 252 |
+
@ml_bp.route("/feature-importance/<model_id>", methods=["GET"])
|
| 253 |
+
@cross_origin()
|
| 254 |
+
def feature_importance(model_id):
|
| 255 |
+
"""Return actual or simulated feature importances."""
|
| 256 |
+
try:
|
| 257 |
+
try_load_models()
|
| 258 |
+
entry = MODELS.get(model_id)
|
| 259 |
+
mdl = entry.get("obj") if entry else None
|
| 260 |
+
|
| 261 |
+
fi = []
|
| 262 |
+
if mdl is not None and hasattr(mdl, "feature_importances_"):
|
| 263 |
+
arr = np.array(getattr(mdl, "feature_importances_")).flatten()
|
| 264 |
+
arr = arr[:len(FEATURE_NAMES)]
|
| 265 |
+
for i, v in enumerate(arr):
|
| 266 |
+
fi.append({"feature": FEATURE_NAMES[i], "importance": float(v)})
|
| 267 |
+
elif mdl is not None and hasattr(mdl, "coef_"):
|
| 268 |
+
arr = np.abs(np.array(getattr(mdl, "coef_")).flatten())
|
| 269 |
+
arr = arr[:len(FEATURE_NAMES)]
|
| 270 |
+
total = float(np.sum(arr)) or 1.0
|
| 271 |
+
for i, v in enumerate(arr):
|
| 272 |
+
fi.append({"feature": FEATURE_NAMES[i], "importance": float(v / total * 100.0)})
|
| 273 |
+
else:
|
| 274 |
+
simulated = {
|
| 275 |
+
"protocol": 8.2,
|
| 276 |
+
"src_port": 7.1,
|
| 277 |
+
"dst_port": 6.4,
|
| 278 |
+
"duration": 10.5,
|
| 279 |
+
"packets_count": 7.8,
|
| 280 |
+
"fwd_packets_count": 6.9,
|
| 281 |
+
"bwd_packets_count": 6.5,
|
| 282 |
+
"total_payload_bytes": 9.8,
|
| 283 |
+
"total_header_bytes": 8.6,
|
| 284 |
+
"bytes_rate": 9.9,
|
| 285 |
+
"packets_rate": 9.1,
|
| 286 |
+
"syn_flag_counts": 5.3,
|
| 287 |
+
"ack_flag_counts": 4.9,
|
| 288 |
+
"rst_flag_counts": 3.8,
|
| 289 |
+
"fin_flag_counts": 3.2
|
| 290 |
+
}
|
| 291 |
+
fi = [{"feature": f, "importance": float(v)} for f, v in simulated.items()]
|
| 292 |
+
|
| 293 |
+
return jsonify({"model_id": model_id, "feature_importance": fi})
|
| 294 |
+
|
| 295 |
+
except Exception as e:
|
| 296 |
+
return jsonify({"error": str(e)}), 500
|
| 297 |
+
|
backend/routes/ml_switch_route.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routes/ml_switch_route.py
|
| 2 |
+
from flask import Blueprint, request, jsonify
|
| 3 |
+
from utils.model_selector import set_active_model, get_active_model, load_model
|
| 4 |
+
|
| 5 |
+
ml_switch = Blueprint("ml_switch", __name__)
|
| 6 |
+
|
| 7 |
+
@ml_switch.route("/active", methods=["GET"])
|
| 8 |
+
def active():
|
| 9 |
+
return jsonify({"active_model": get_active_model()})
|
| 10 |
+
|
| 11 |
+
@ml_switch.route("/select", methods=["POST"])
|
| 12 |
+
def select():
|
| 13 |
+
data = request.get_json(force=True, silent=True) or {}
|
| 14 |
+
model = data.get("model")
|
| 15 |
+
if model not in ("bcc", "cicids"):
|
| 16 |
+
return jsonify({"error": "model must be 'bcc' or 'cicids'"}), 400
|
| 17 |
+
try:
|
| 18 |
+
set_active_model(model)
|
| 19 |
+
# attempt load to give quick feedback
|
| 20 |
+
info = load_model(model)
|
| 21 |
+
return jsonify({"message": f"Active model set to {model}", "loaded": bool(info)})
|
| 22 |
+
|
| 23 |
+
except Exception as e:
|
| 24 |
+
return jsonify({"error": str(e)}), 500
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@ml_switch.route("/health", methods=["GET"])
|
| 28 |
+
def health():
|
| 29 |
+
import numpy as np
|
| 30 |
+
import pandas as pd
|
| 31 |
+
|
| 32 |
+
active = get_active_model()
|
| 33 |
+
bundle = load_model(active)
|
| 34 |
+
|
| 35 |
+
model = bundle.get("model")
|
| 36 |
+
artifacts = bundle.get("artifacts")
|
| 37 |
+
|
| 38 |
+
# Default responses
|
| 39 |
+
artifact_keys = list(artifacts.keys()) if artifacts else []
|
| 40 |
+
features = None
|
| 41 |
+
feature_count = 0
|
| 42 |
+
test_prediction = "N/A"
|
| 43 |
+
|
| 44 |
+
# ------------------------------------------
|
| 45 |
+
# CICIDS HEALTH CHECK
|
| 46 |
+
# ------------------------------------------
|
| 47 |
+
if active == "cicids":
|
| 48 |
+
if artifacts and "features" in artifacts:
|
| 49 |
+
features = artifacts["features"]
|
| 50 |
+
feature_count = len(features)
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
# generate a zero vector
|
| 54 |
+
X = np.zeros((1, feature_count))
|
| 55 |
+
scaler = artifacts.get("scaler")
|
| 56 |
+
if scaler:
|
| 57 |
+
X = scaler.transform(X)
|
| 58 |
+
|
| 59 |
+
pred = model.predict(X)[0]
|
| 60 |
+
test_prediction = str(pred)
|
| 61 |
+
|
| 62 |
+
except Exception as e:
|
| 63 |
+
test_prediction = f"Error: {str(e)}"
|
| 64 |
+
|
| 65 |
+
# ------------------------------------------
|
| 66 |
+
# BCC HEALTH CHECK
|
| 67 |
+
# ------------------------------------------
|
| 68 |
+
elif active == "bcc":
|
| 69 |
+
try:
|
| 70 |
+
# Create minimal fake BCC packet feature vector: 15 values
|
| 71 |
+
X = np.zeros((1, 15))
|
| 72 |
+
|
| 73 |
+
scaler = bundle.get("scaler")
|
| 74 |
+
encoder = bundle.get("encoder")
|
| 75 |
+
|
| 76 |
+
if scaler:
|
| 77 |
+
Xs = scaler.transform(X)
|
| 78 |
+
else:
|
| 79 |
+
Xs = X
|
| 80 |
+
|
| 81 |
+
pred_raw = model.predict(Xs)[0]
|
| 82 |
+
|
| 83 |
+
if encoder:
|
| 84 |
+
pred = encoder.inverse_transform([int(pred_raw)])[0]
|
| 85 |
+
else:
|
| 86 |
+
pred = str(pred_raw)
|
| 87 |
+
|
| 88 |
+
test_prediction = f"OK: {pred}"
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
test_prediction = f"Error: {str(e)}"
|
| 92 |
+
|
| 93 |
+
# ------------------------------------------
|
| 94 |
+
# Build response
|
| 95 |
+
# ------------------------------------------
|
| 96 |
+
return {
|
| 97 |
+
"active_model": active,
|
| 98 |
+
"model_loaded": model is not None,
|
| 99 |
+
"artifact_keys": artifact_keys,
|
| 100 |
+
"feature_count": feature_count,
|
| 101 |
+
"features": features,
|
| 102 |
+
"test_prediction": test_prediction
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
backend/routes/offline_detection.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from flask import Blueprint, request, jsonify, send_file
|
| 4 |
+
from werkzeug.utils import secure_filename
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import joblib
|
| 7 |
+
from fpdf import FPDF
|
| 8 |
+
from utils.pcap_to_csv import convert_pcap_to_csv
|
| 9 |
+
|
| 10 |
+
offline_bp = Blueprint("offline_bp", __name__)
|
| 11 |
+
|
| 12 |
+
UPLOAD_DIR = "uploads"
|
| 13 |
+
SAMPLE_DIR = "sample"
|
| 14 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 15 |
+
os.makedirs(SAMPLE_DIR, exist_ok=True)
|
| 16 |
+
|
| 17 |
+
ALLOWED_EXT = {"csv", "pcap"}
|
| 18 |
+
|
| 19 |
+
# Features
|
| 20 |
+
BCC_FEATURES = [
|
| 21 |
+
"proto","src_port","dst_port","flow_duration","total_fwd_pkts","total_bwd_pkts",
|
| 22 |
+
"flags_numeric","payload_len","header_len","rate","iat","syn","ack","rst","fin"
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
CICIDS_FEATURES = [
|
| 26 |
+
"Protocol","Dst Port","Flow Duration","Tot Fwd Pkts","Tot Bwd Pkts",
|
| 27 |
+
"TotLen Fwd Pkts","TotLen Bwd Pkts","Fwd Pkt Len Mean","Bwd Pkt Len Mean",
|
| 28 |
+
"Flow IAT Mean","Fwd PSH Flags","Fwd URG Flags","Fwd IAT Mean"
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
# Models
|
| 32 |
+
bcc_model = joblib.load("ml_models/realtime_model.pkl")
|
| 33 |
+
bcc_encoder = joblib.load("ml_models/realtime_encoder.pkl")
|
| 34 |
+
bcc_scaler = joblib.load("ml_models/realtime_scaler.pkl")
|
| 35 |
+
|
| 36 |
+
cicids_model = joblib.load("ml_models/rf_pipeline.joblib")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def allowed(filename):
|
| 40 |
+
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXT
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# 📌 Sample CSV Download
|
| 44 |
+
@offline_bp.route("/sample/<model>", methods=["GET"])
|
| 45 |
+
def download_sample(model):
|
| 46 |
+
file_path = None
|
| 47 |
+
if model == "bcc":
|
| 48 |
+
file_path = os.path.join(SAMPLE_DIR, "bcc_sample.csv")
|
| 49 |
+
elif model == "cicids":
|
| 50 |
+
file_path = os.path.join(SAMPLE_DIR, "cicids_sample.csv")
|
| 51 |
+
else:
|
| 52 |
+
return jsonify(success=False, message="Invalid model"), 400
|
| 53 |
+
|
| 54 |
+
if not os.path.exists(file_path):
|
| 55 |
+
return jsonify(success=False, message="Sample file missing"), 404
|
| 56 |
+
|
| 57 |
+
return send_file(file_path, as_attachment=True)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# 📌 Prediction API
|
| 61 |
+
@offline_bp.route("/predict", methods=["POST"])
|
| 62 |
+
def offline_predict():
|
| 63 |
+
if "file" not in request.files:
|
| 64 |
+
return jsonify(success=False, message="No file uploaded"), 400
|
| 65 |
+
|
| 66 |
+
file = request.files["file"]
|
| 67 |
+
model_type = request.form.get("model", "bcc")
|
| 68 |
+
|
| 69 |
+
if not allowed(file.filename):
|
| 70 |
+
return jsonify(success=False, message="Unsupported file type"), 400
|
| 71 |
+
|
| 72 |
+
filename = secure_filename(file.filename)
|
| 73 |
+
saved_path = os.path.join(UPLOAD_DIR, filename)
|
| 74 |
+
file.save(saved_path)
|
| 75 |
+
|
| 76 |
+
# PCAP Conversion
|
| 77 |
+
if filename.lower().endswith(".pcap"):
|
| 78 |
+
saved_path = convert_pcap_to_csv(saved_path)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
df = pd.read_csv(saved_path)
|
| 82 |
+
# Prevent empty CSV prediction
|
| 83 |
+
if df.shape[0] == 0:
|
| 84 |
+
return jsonify(success=False, message="CSV has no data rows to analyze!"), 400
|
| 85 |
+
|
| 86 |
+
expected = BCC_FEATURES if model_type == "bcc" else CICIDS_FEATURES
|
| 87 |
+
|
| 88 |
+
missing = [c for c in expected if c not in df.columns]
|
| 89 |
+
if missing:
|
| 90 |
+
return jsonify(success=False, message=f"Missing features: {missing}")
|
| 91 |
+
|
| 92 |
+
df = df[expected]
|
| 93 |
+
|
| 94 |
+
if model_type == "bcc":
|
| 95 |
+
scaled = bcc_scaler.transform(df)
|
| 96 |
+
preds = bcc_model.predict(scaled)
|
| 97 |
+
labels = bcc_encoder.inverse_transform(preds)
|
| 98 |
+
else:
|
| 99 |
+
labels = cicids_model.predict(df)
|
| 100 |
+
|
| 101 |
+
df["prediction"] = labels
|
| 102 |
+
class_counts = df["prediction"].value_counts().to_dict()
|
| 103 |
+
|
| 104 |
+
results = [{"index": i, "class": lbl} for i, lbl in enumerate(labels)]
|
| 105 |
+
|
| 106 |
+
result_file = os.path.join(UPLOAD_DIR, "last_results.csv")
|
| 107 |
+
df.to_csv(result_file, index=False)
|
| 108 |
+
|
| 109 |
+
return jsonify(success=True, classCounts=class_counts, results=results)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# 📌 PDF Report Generation
|
| 113 |
+
@offline_bp.route("/report", methods=["GET"])
|
| 114 |
+
def offline_report():
|
| 115 |
+
result_file = os.path.join(UPLOAD_DIR, "last_results.csv")
|
| 116 |
+
if not os.path.exists(result_file):
|
| 117 |
+
return jsonify(success=False, message="Run prediction first"), 400
|
| 118 |
+
|
| 119 |
+
df = pd.read_csv(result_file)
|
| 120 |
+
class_counts = df["prediction"].value_counts().to_dict()
|
| 121 |
+
|
| 122 |
+
pdf_path = os.path.join(UPLOAD_DIR, "offline_report.pdf")
|
| 123 |
+
|
| 124 |
+
pdf = FPDF()
|
| 125 |
+
pdf.add_page()
|
| 126 |
+
pdf.set_font("Arial", "B", 16)
|
| 127 |
+
pdf.cell(0, 10, "AI-NIDS Offline Threat Analysis Report", ln=True)
|
| 128 |
+
|
| 129 |
+
pdf.set_font("Arial", size=12)
|
| 130 |
+
pdf.cell(0, 10, f"Generated: {datetime.now()}", ln=True)
|
| 131 |
+
pdf.ln(5)
|
| 132 |
+
|
| 133 |
+
for c, v in class_counts.items():
|
| 134 |
+
pdf.cell(0, 8, f"{c}: {v}", ln=True)
|
| 135 |
+
|
| 136 |
+
pdf.output(pdf_path)
|
| 137 |
+
return send_file(pdf_path, as_attachment=True)
|
| 138 |
+
|
| 139 |
+
|
backend/routes/predict_route.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routes/predict_route.py
|
| 2 |
+
from flask import Blueprint, request, jsonify
|
| 3 |
+
import time
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from utils.model_selector import load_model, get_active_model
|
| 7 |
+
from utils.logger import classify_risk
|
| 8 |
+
|
| 9 |
+
predict_bp = Blueprint("predict", __name__)
|
| 10 |
+
|
| 11 |
+
@predict_bp.route("/", methods=["GET"])
|
| 12 |
+
def info():
|
| 13 |
+
active = get_active_model()
|
| 14 |
+
return jsonify({
|
| 15 |
+
"message": "POST JSON to /api/predict/ to get model prediction.",
|
| 16 |
+
"active_model": active,
|
| 17 |
+
"note": "For 'bcc' model send ordered features or dict; for 'cicids' send named features matching artifacts['features']."
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
@predict_bp.route("/", methods=["POST"])
|
| 21 |
+
def predict():
|
| 22 |
+
active = get_active_model()
|
| 23 |
+
mdl = load_model(active)
|
| 24 |
+
|
| 25 |
+
if active == "bcc":
|
| 26 |
+
model = mdl.get("model")
|
| 27 |
+
scaler = mdl.get("scaler")
|
| 28 |
+
encoder = mdl.get("encoder")
|
| 29 |
+
|
| 30 |
+
if model is None or scaler is None or encoder is None:
|
| 31 |
+
return jsonify({"error": "BCC model/scaler/encoder not loaded on server."}), 500
|
| 32 |
+
|
| 33 |
+
data = request.get_json(force=True, silent=True)
|
| 34 |
+
if data is None:
|
| 35 |
+
return jsonify({"error": "No JSON body provided"}), 400
|
| 36 |
+
|
| 37 |
+
# Accept either list/array or dict of features
|
| 38 |
+
# You must keep the same feature order as used in training (15 values)
|
| 39 |
+
if isinstance(data, dict):
|
| 40 |
+
# if the client provides named keys, try to coerce to ordered list
|
| 41 |
+
# fallback: take values in insertion order
|
| 42 |
+
vals = list(data.values())
|
| 43 |
+
else:
|
| 44 |
+
vals = list(data)
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
X = np.array([float(v) for v in vals], dtype=float).reshape(1, -1)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return jsonify({"error": f"Failed to coerce input to numeric vector: {e}"}), 400
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
Xs = scaler.transform(X)
|
| 53 |
+
except Exception:
|
| 54 |
+
# fallback: try prediction without scaler
|
| 55 |
+
Xs = X
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
pred_idx = model.predict(Xs)[0]
|
| 59 |
+
conf = None
|
| 60 |
+
if hasattr(model, "predict_proba"):
|
| 61 |
+
conf = float(np.max(model.predict_proba(Xs))) * 100.0
|
| 62 |
+
label = encoder.inverse_transform([int(pred_idx)])[0]
|
| 63 |
+
risk = classify_risk(label)
|
| 64 |
+
return jsonify({
|
| 65 |
+
"prediction": str(label),
|
| 66 |
+
"confidence": round(conf, 2) if conf is not None else None,
|
| 67 |
+
"risk_level": risk
|
| 68 |
+
})
|
| 69 |
+
except Exception as e:
|
| 70 |
+
return jsonify({"error": f"Model predict failed: {str(e)}"}), 500
|
| 71 |
+
|
| 72 |
+
elif active == "cicids":
|
| 73 |
+
obj = mdl.get("artifacts", None)
|
| 74 |
+
model = mdl.get("model", None)
|
| 75 |
+
if model is None or obj is None:
|
| 76 |
+
return jsonify({"error": "CICIDS model or artifacts not available on server."}), 500
|
| 77 |
+
|
| 78 |
+
# artifacts expected to have 'features' and 'scaler'
|
| 79 |
+
features = obj.get("features") or obj.get("features_used") or obj.get("feature_list")
|
| 80 |
+
scaler = obj.get("scaler") or obj.get("scaler_object")
|
| 81 |
+
|
| 82 |
+
if not features or scaler is None:
|
| 83 |
+
return jsonify({"error": "CICIDS artifacts missing features or scaler."}), 500
|
| 84 |
+
|
| 85 |
+
data = request.get_json(force=True, silent=True)
|
| 86 |
+
if data is None:
|
| 87 |
+
return jsonify({"error": "No JSON body provided"}), 400
|
| 88 |
+
|
| 89 |
+
# Accept dict of named features or list
|
| 90 |
+
if isinstance(data, dict):
|
| 91 |
+
# build row using artifacts feature order (missing -> 0)
|
| 92 |
+
row = [float(data.get(f, 0)) for f in features]
|
| 93 |
+
else:
|
| 94 |
+
# list or array
|
| 95 |
+
try:
|
| 96 |
+
row = [float(x) for x in data]
|
| 97 |
+
except Exception as e:
|
| 98 |
+
return jsonify({"error": "Provided input must be array or dict of numbers."}), 400
|
| 99 |
+
|
| 100 |
+
if len(row) != len(features):
|
| 101 |
+
return jsonify({"error": f"Expecting {len(features)} features for cicids: {features}"}), 400
|
| 102 |
+
|
| 103 |
+
X_df = pd.DataFrame([row], columns=features)
|
| 104 |
+
try:
|
| 105 |
+
Xs = scaler.transform(X_df)
|
| 106 |
+
except Exception:
|
| 107 |
+
Xs = X_df.values
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
pred = model.predict(Xs)[0]
|
| 111 |
+
conf = None
|
| 112 |
+
if hasattr(model, "predict_proba"):
|
| 113 |
+
conf = float(np.max(model.predict_proba(Xs))) * 100.0
|
| 114 |
+
# label may already be string; try safe conversion
|
| 115 |
+
try:
|
| 116 |
+
label = str(pred)
|
| 117 |
+
except Exception:
|
| 118 |
+
label = repr(pred)
|
| 119 |
+
|
| 120 |
+
risk = classify_risk(label)
|
| 121 |
+
return jsonify({
|
| 122 |
+
"prediction": label,
|
| 123 |
+
"confidence": round(conf, 2) if conf else None,
|
| 124 |
+
"risk_level": risk
|
| 125 |
+
})
|
| 126 |
+
except Exception as e:
|
| 127 |
+
return jsonify({"error": f"CICIDS predict failed: {str(e)}"}), 500
|
| 128 |
+
|
| 129 |
+
else:
|
| 130 |
+
return jsonify({"error": "Unknown active model"}), 500
|
| 131 |
+
|
| 132 |
+
|
backend/routes/reports_route.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routes/reports_route.py
|
| 2 |
+
from flask import Blueprint, jsonify, request, send_file
|
| 3 |
+
from fpdf import FPDF
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from flask_mail import Message
|
| 7 |
+
from extensions import mail
|
| 8 |
+
import random
|
| 9 |
+
|
| 10 |
+
reports_bp = Blueprint("reports_bp", __name__)
|
| 11 |
+
|
| 12 |
+
# Fake (but structured) historical attack data
|
| 13 |
+
CLASSES = ["DDoS", "VPN", "TOR", "I2P", "SQL Injection", "Malware"]
|
| 14 |
+
|
| 15 |
+
def generate_fake_data(days=7):
|
| 16 |
+
today = datetime.now()
|
| 17 |
+
data = []
|
| 18 |
+
for i in range(days):
|
| 19 |
+
date = (today - timedelta(days=i)).strftime("%Y-%m-%d")
|
| 20 |
+
day_data = {cls: random.randint(20, 200) for cls in CLASSES}
|
| 21 |
+
day_data["date"] = date
|
| 22 |
+
data.append(day_data)
|
| 23 |
+
return list(reversed(data))
|
| 24 |
+
|
| 25 |
+
ATTACK_DATA = generate_fake_data(14)
|
| 26 |
+
|
| 27 |
+
# --------------------------------------------------------
|
| 28 |
+
@reports_bp.route("/", methods=["GET"])
|
| 29 |
+
def reports_overview():
|
| 30 |
+
total_attacks = sum(sum(v for k, v in day.items() if k != "date") for day in ATTACK_DATA)
|
| 31 |
+
recent = ATTACK_DATA[-1]
|
| 32 |
+
return jsonify({
|
| 33 |
+
"total_attacks": total_attacks,
|
| 34 |
+
"last_day": recent["date"],
|
| 35 |
+
"last_day_total": sum(v for k, v in recent.items() if k != "date"),
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
# --------------------------------------------------------
|
| 39 |
+
@reports_bp.route("/trend", methods=["GET"])
|
| 40 |
+
def attack_trend():
|
| 41 |
+
trend = [{"date": d["date"], "attacks": sum(v for k, v in d.items() if k != "date")} for d in ATTACK_DATA]
|
| 42 |
+
return jsonify(trend)
|
| 43 |
+
|
| 44 |
+
# --------------------------------------------------------
|
| 45 |
+
@reports_bp.route("/distribution", methods=["GET"])
|
| 46 |
+
def attack_distribution():
|
| 47 |
+
total = {cls: sum(day.get(cls, 0) for day in ATTACK_DATA) for cls in CLASSES}
|
| 48 |
+
return jsonify(total)
|
| 49 |
+
|
| 50 |
+
# --------------------------------------------------------
|
| 51 |
+
@reports_bp.route("/generate", methods=["GET"])
|
| 52 |
+
def generate_report_pdf():
|
| 53 |
+
pdf = FPDF()
|
| 54 |
+
pdf.add_page()
|
| 55 |
+
pdf.set_font("Helvetica", "B", 16)
|
| 56 |
+
pdf.cell(0, 10, "Adaptive AI NIDS - Attack Report", ln=True, align="C")
|
| 57 |
+
|
| 58 |
+
pdf.set_font("Helvetica", "", 12)
|
| 59 |
+
pdf.ln(8)
|
| 60 |
+
pdf.cell(0, 10, f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ln=True)
|
| 61 |
+
pdf.ln(6)
|
| 62 |
+
|
| 63 |
+
pdf.set_font("Helvetica", "B", 13)
|
| 64 |
+
pdf.cell(0, 10, "Summary:", ln=True)
|
| 65 |
+
pdf.set_font("Helvetica", "", 11)
|
| 66 |
+
total = {cls: sum(day.get(cls, 0) for day in ATTACK_DATA) for cls in CLASSES}
|
| 67 |
+
for cls, val in total.items():
|
| 68 |
+
pdf.cell(0, 8, f" • {cls}: {val} attacks", ln=True)
|
| 69 |
+
|
| 70 |
+
pdf.ln(8)
|
| 71 |
+
pdf.set_font("Helvetica", "I", 10)
|
| 72 |
+
pdf.multi_cell(0, 8,
|
| 73 |
+
"This report summarizes attack activity captured by the Adaptive AI NIDS system. "
|
| 74 |
+
"It includes class-wise distribution and historical trend for the past two weeks.")
|
| 75 |
+
|
| 76 |
+
# Output to memory
|
| 77 |
+
buffer = BytesIO()
|
| 78 |
+
pdf.output(buffer)
|
| 79 |
+
buffer.seek(0)
|
| 80 |
+
filename = f"NIDS_Report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 81 |
+
return send_file(buffer, as_attachment=True, download_name=filename, mimetype="application/pdf")
|
| 82 |
+
|
| 83 |
+
# --------------------------------------------------------
|
| 84 |
+
@reports_bp.route("/email", methods=["POST"])
|
| 85 |
+
def send_report_email():
|
| 86 |
+
data = request.get_json()
|
| 87 |
+
recipient = data.get("email")
|
| 88 |
+
if not recipient:
|
| 89 |
+
return jsonify({"error": "No recipient email provided"}), 400
|
| 90 |
+
|
| 91 |
+
# Generate detailed PDF
|
| 92 |
+
pdf_buffer = BytesIO()
|
| 93 |
+
pdf = FPDF()
|
| 94 |
+
pdf.add_page()
|
| 95 |
+
pdf.set_font("Helvetica", "B", 16)
|
| 96 |
+
pdf.cell(0, 10, "Adaptive AI NIDS - Full System Report", ln=True, align="C")
|
| 97 |
+
pdf.ln(8)
|
| 98 |
+
pdf.set_font("Helvetica", "", 12)
|
| 99 |
+
pdf.cell(0, 10, "Summary of recent network activity:", ln=True)
|
| 100 |
+
pdf.ln(5)
|
| 101 |
+
|
| 102 |
+
pdf.set_font("Helvetica", "B", 12)
|
| 103 |
+
pdf.cell(0, 8, "Attack Distribution:", ln=True)
|
| 104 |
+
total = {cls: sum(day.get(cls, 0) for day in ATTACK_DATA) for cls in CLASSES}
|
| 105 |
+
pdf.set_font("Helvetica", "", 11)
|
| 106 |
+
for cls, val in total.items():
|
| 107 |
+
pdf.cell(0, 8, f" - {cls}: {val} attacks", ln=True)
|
| 108 |
+
|
| 109 |
+
pdf.ln(6)
|
| 110 |
+
pdf.set_font("Helvetica", "B", 12)
|
| 111 |
+
pdf.cell(0, 8, "Recent Trend (last 7 days):", ln=True)
|
| 112 |
+
pdf.set_font("Helvetica", "", 11)
|
| 113 |
+
for d in ATTACK_DATA[-7:]:
|
| 114 |
+
pdf.cell(0, 8, f"{d['date']}: {sum(v for k, v in d.items() if k != 'date')} total", ln=True)
|
| 115 |
+
|
| 116 |
+
pdf.ln(10)
|
| 117 |
+
pdf.set_font("Helvetica", "I", 10)
|
| 118 |
+
pdf.multi_cell(0, 8, "This automated report is generated by Adaptive AI NIDS. "
|
| 119 |
+
"It summarizes live detections, system diagnostics, and "
|
| 120 |
+
"AI-identified attack classes.")
|
| 121 |
+
|
| 122 |
+
pdf.output(pdf_buffer)
|
| 123 |
+
pdf_buffer.seek(0)
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
msg = Message(
|
| 127 |
+
subject="Adaptive AI NIDS - Full Report",
|
| 128 |
+
recipients=[recipient],
|
| 129 |
+
body="Attached is your Adaptive AI NIDS summary report with recent attack data.",
|
| 130 |
+
)
|
| 131 |
+
msg.attach("Adaptive_NIDS_Report.pdf", "application/pdf", pdf_buffer.read())
|
| 132 |
+
mail.send(msg)
|
| 133 |
+
return jsonify({"success": True, "message": f"Email sent to {recipient}"})
|
| 134 |
+
except Exception as e:
|
| 135 |
+
return jsonify({"error": str(e)}), 500
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@reports_bp.route("/list", methods=["GET"])
|
| 140 |
+
def list_reports():
|
| 141 |
+
reports = [
|
| 142 |
+
{
|
| 143 |
+
"id": 1,
|
| 144 |
+
"name": "System Health Summary",
|
| 145 |
+
"type": "System Health",
|
| 146 |
+
"size": "420 KB",
|
| 147 |
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 148 |
+
"endpoint": "/api/reports/generate"
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"id": 2,
|
| 152 |
+
"name": "Network Attack Analysis",
|
| 153 |
+
"type": "Network Analysis",
|
| 154 |
+
"size": "1.2 MB",
|
| 155 |
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 156 |
+
"endpoint": "/api/reports/generate"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"id": 3,
|
| 160 |
+
"name": "Threat Intelligence Summary",
|
| 161 |
+
"type": "Threat Intelligence",
|
| 162 |
+
"size": "620 KB",
|
| 163 |
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 164 |
+
"endpoint": "/api/reports/generate"
|
| 165 |
+
},
|
| 166 |
+
]
|
| 167 |
+
return jsonify(reports)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
|
backend/routes/system_info.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, jsonify
|
| 2 |
+
import psutil
|
| 3 |
+
import platform
|
| 4 |
+
import socket
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import random
|
| 7 |
+
import time
|
| 8 |
+
import random
|
| 9 |
+
import io
|
| 10 |
+
from fpdf import FPDF
|
| 11 |
+
from flask import send_file
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
system_bp = Blueprint("system", __name__)
|
| 17 |
+
|
| 18 |
+
@system_bp.route("/system/status", methods=["GET"])
|
| 19 |
+
def system_status():
|
| 20 |
+
try:
|
| 21 |
+
hostname = socket.gethostname()
|
| 22 |
+
ip_address = socket.gethostbyname(hostname)
|
| 23 |
+
os_info = platform.platform()
|
| 24 |
+
cpu_name = platform.processor()
|
| 25 |
+
|
| 26 |
+
# --- Metrics ---
|
| 27 |
+
cpu_percent = psutil.cpu_percent(interval=0.5)
|
| 28 |
+
ram = psutil.virtual_memory()
|
| 29 |
+
disk = psutil.disk_usage('/')
|
| 30 |
+
net_io = psutil.net_io_counters()
|
| 31 |
+
|
| 32 |
+
# --- Temperature ---
|
| 33 |
+
try:
|
| 34 |
+
temps = psutil.sensors_temperatures()
|
| 35 |
+
cpu_temp = (
|
| 36 |
+
temps.get("coretemp")[0].current
|
| 37 |
+
if "coretemp" in temps
|
| 38 |
+
else random.uniform(45.0, 75.0) # fallback
|
| 39 |
+
)
|
| 40 |
+
except Exception:
|
| 41 |
+
cpu_temp = random.uniform(45.0, 75.0)
|
| 42 |
+
|
| 43 |
+
# --- AI Health Score ---
|
| 44 |
+
# Weighted average (higher = better)
|
| 45 |
+
usage = (cpu_percent * 0.4 + ram.percent * 0.3 + disk.percent * 0.3)
|
| 46 |
+
health_score = max(0, 100 - usage)
|
| 47 |
+
|
| 48 |
+
data = {
|
| 49 |
+
"hostname": hostname,
|
| 50 |
+
"ip_address": ip_address,
|
| 51 |
+
"os": os_info,
|
| 52 |
+
"cpu_name": cpu_name,
|
| 53 |
+
"cpu_usage": round(cpu_percent, 2),
|
| 54 |
+
"ram_usage": round(ram.percent, 2),
|
| 55 |
+
"disk_usage": round(disk.percent, 2),
|
| 56 |
+
"ram_total": round(ram.total / (1024 ** 3), 2),
|
| 57 |
+
"disk_total": round(disk.total / (1024 ** 3), 2),
|
| 58 |
+
"network_sent": round(net_io.bytes_sent / (1024 ** 2), 2),
|
| 59 |
+
"network_recv": round(net_io.bytes_recv / (1024 ** 2), 2),
|
| 60 |
+
"cpu_temp": round(cpu_temp, 2),
|
| 61 |
+
"health_score": round(health_score, 2),
|
| 62 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 63 |
+
}
|
| 64 |
+
return jsonify(data)
|
| 65 |
+
except Exception as e:
|
| 66 |
+
return jsonify({"error": str(e)}), 500
|
| 67 |
+
|
| 68 |
+
@system_bp.route("/system/diagnostic", methods=["GET"])
|
| 69 |
+
def run_diagnostic():
|
| 70 |
+
"""Simulate a full AI-powered system stability diagnostic."""
|
| 71 |
+
try:
|
| 72 |
+
# Simulated stress test (CPU, memory response)
|
| 73 |
+
cpu_load = random.uniform(60, 98)
|
| 74 |
+
ram_stress = random.uniform(50, 95)
|
| 75 |
+
disk_io = random.uniform(40, 90)
|
| 76 |
+
latency = random.uniform(15, 100)
|
| 77 |
+
|
| 78 |
+
# AI stability score (100 = perfect)
|
| 79 |
+
stability = 100 - ((cpu_load * 0.3) + (ram_stress * 0.3) + (disk_io * 0.2) + (latency * 0.2)) / 2
|
| 80 |
+
stability = round(max(0, min(100, stability)), 2)
|
| 81 |
+
|
| 82 |
+
# Fake attack summary data
|
| 83 |
+
attacks = {
|
| 84 |
+
"total_attacks": random.randint(1200, 4200),
|
| 85 |
+
"blocked": random.randint(1100, 4000),
|
| 86 |
+
"missed": random.randint(5, 20),
|
| 87 |
+
"recent_threats": [
|
| 88 |
+
{"type": "DDoS Flood", "risk": "High", "ip": "45.77.23.9"},
|
| 89 |
+
{"type": "SQL Injection", "risk": "Medium", "ip": "103.54.66.120"},
|
| 90 |
+
{"type": "VPN Evasion", "risk": "Low", "ip": "198.168.12.45"},
|
| 91 |
+
],
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
diagnostic = {
|
| 95 |
+
"cpu_load": round(cpu_load, 2),
|
| 96 |
+
"ram_stress": round(ram_stress, 2),
|
| 97 |
+
"disk_io": round(disk_io, 2),
|
| 98 |
+
"latency": round(latency, 2),
|
| 99 |
+
"stability_score": stability,
|
| 100 |
+
"attacks": attacks,
|
| 101 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 102 |
+
}
|
| 103 |
+
return jsonify(diagnostic)
|
| 104 |
+
except Exception as e:
|
| 105 |
+
return jsonify({"error": str(e)}), 500
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@system_bp.route("/system/report", methods=["GET"])
|
| 110 |
+
def generate_system_report():
|
| 111 |
+
"""Generate a downloadable PDF system report."""
|
| 112 |
+
try:
|
| 113 |
+
# --- Simulated data or pull from live sources ---
|
| 114 |
+
system_status = {
|
| 115 |
+
"OS": "Windows 10 Pro",
|
| 116 |
+
"CPU": "Intel i5-12700H",
|
| 117 |
+
"Memory": "16 GB",
|
| 118 |
+
"Disk": "512 GB SSD",
|
| 119 |
+
"IP": "127.0.0.1",
|
| 120 |
+
"Health Score": "89%",
|
| 121 |
+
"Last Diagnostic": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# --- Create PDF report ---
|
| 125 |
+
pdf = FPDF()
|
| 126 |
+
pdf.add_page()
|
| 127 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
| 128 |
+
|
| 129 |
+
# Title
|
| 130 |
+
pdf.set_font("Helvetica", "B", 18)
|
| 131 |
+
pdf.cell(0, 10, "Adaptive AI NIDS - System Report", ln=True, align="C")
|
| 132 |
+
pdf.ln(10)
|
| 133 |
+
|
| 134 |
+
# Subtitle
|
| 135 |
+
pdf.set_font("Helvetica", "", 12)
|
| 136 |
+
pdf.cell(0, 10, f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ln=True)
|
| 137 |
+
pdf.ln(8)
|
| 138 |
+
|
| 139 |
+
# Section: System Status
|
| 140 |
+
pdf.set_font("Helvetica", "B", 14)
|
| 141 |
+
pdf.cell(0, 10, "System Information", ln=True)
|
| 142 |
+
pdf.set_font("Helvetica", "", 12)
|
| 143 |
+
pdf.ln(5)
|
| 144 |
+
|
| 145 |
+
for key, value in system_status.items():
|
| 146 |
+
pdf.cell(0, 8, f"{key}: {value}", ln=True)
|
| 147 |
+
|
| 148 |
+
pdf.ln(10)
|
| 149 |
+
pdf.set_font("Helvetica", "B", 14)
|
| 150 |
+
pdf.cell(0, 10, "Attack Summary (Last 24h)", ln=True)
|
| 151 |
+
pdf.set_font("Helvetica", "", 12)
|
| 152 |
+
pdf.ln(5)
|
| 153 |
+
|
| 154 |
+
pdf.cell(0, 8, "Total Attacks Detected: 3471", ln=True)
|
| 155 |
+
pdf.cell(0, 8, "High Risk: 512", ln=True)
|
| 156 |
+
pdf.cell(0, 8, "Medium Risk: 948", ln=True)
|
| 157 |
+
pdf.cell(0, 8, "Low Risk: 2011", ln=True)
|
| 158 |
+
|
| 159 |
+
pdf.ln(10)
|
| 160 |
+
pdf.set_font("Helvetica", "I", 10)
|
| 161 |
+
pdf.cell(0, 8, "This report is automatically generated by Adaptive AI NIDS.", ln=True, align="C")
|
| 162 |
+
|
| 163 |
+
# Save to memory
|
| 164 |
+
buffer = io.BytesIO()
|
| 165 |
+
pdf.output(buffer)
|
| 166 |
+
buffer.seek(0)
|
| 167 |
+
|
| 168 |
+
filename = f"System_Report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 169 |
+
return send_file(buffer, as_attachment=True, download_name=filename, mimetype="application/pdf")
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
return jsonify({"error": str(e)}), 500
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@system_bp.route("/system/processes")
|
| 176 |
+
def system_processes():
|
| 177 |
+
try:
|
| 178 |
+
processes = []
|
| 179 |
+
for proc in psutil.process_iter(['name', 'cpu_percent', 'memory_percent', 'status']):
|
| 180 |
+
info = proc.info
|
| 181 |
+
processes.append({
|
| 182 |
+
"name": info.get("name", "Unknown"),
|
| 183 |
+
"cpu": round(info.get("cpu_percent", 0), 2),
|
| 184 |
+
"mem": round(info.get("memory_percent", 0), 2),
|
| 185 |
+
"status": info.get("status", "N/A"),
|
| 186 |
+
})
|
| 187 |
+
# ✅ Sort by CPU usage and keep top 6
|
| 188 |
+
top_processes = sorted(processes, key=lambda p: p["cpu"], reverse=True)[:6]
|
| 189 |
+
return jsonify(top_processes)
|
| 190 |
+
except Exception as e:
|
| 191 |
+
return jsonify({"error": str(e)}), 500
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@system_bp.route("/system/connections")
|
| 196 |
+
def system_connections():
|
| 197 |
+
try:
|
| 198 |
+
conns = []
|
| 199 |
+
for c in psutil.net_connections(kind='inet'):
|
| 200 |
+
if c.laddr:
|
| 201 |
+
conns.append({
|
| 202 |
+
"ip": c.laddr.ip,
|
| 203 |
+
"port": c.laddr.port,
|
| 204 |
+
"proto": "TCP" if c.type == socket.SOCK_STREAM else "UDP",
|
| 205 |
+
"state": c.status,
|
| 206 |
+
})
|
| 207 |
+
# ✅ Only top 6 most recent/active connections
|
| 208 |
+
top_conns = conns[:6]
|
| 209 |
+
return jsonify(top_conns)
|
| 210 |
+
except Exception as e:
|
| 211 |
+
return jsonify({"error": str(e)}), 500
|
backend/routes/traffic_routes.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# traffic_routes.py
|
| 2 |
+
from flask import Blueprint, jsonify
|
| 3 |
+
from utils.logger import get_recent_events, summarize_counts
|
| 4 |
+
from flow_builder import build_flows
|
| 5 |
+
|
| 6 |
+
traffic_bp = Blueprint("traffic_bp", __name__)
|
| 7 |
+
|
| 8 |
+
@traffic_bp.route("traffic/flows")
|
| 9 |
+
def flows():
|
| 10 |
+
"""Return aggregated flows from recent network events."""
|
| 11 |
+
events = get_recent_events(2000)
|
| 12 |
+
flows = build_flows(events)
|
| 13 |
+
return jsonify({"flows": flows})
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@traffic_bp.route("traffic/protocols")
|
| 17 |
+
def protocols():
|
| 18 |
+
"""Return protocol distribution."""
|
| 19 |
+
events = get_recent_events(2000)
|
| 20 |
+
counts = {"TCP": 0, "UDP": 0, "Other": 0}
|
| 21 |
+
|
| 22 |
+
for e in events:
|
| 23 |
+
proto = e.get("proto", "").upper()
|
| 24 |
+
if proto == "TCP":
|
| 25 |
+
counts["TCP"] += 1
|
| 26 |
+
elif proto == "UDP":
|
| 27 |
+
counts["UDP"] += 1
|
| 28 |
+
else:
|
| 29 |
+
counts["Other"] += 1
|
| 30 |
+
|
| 31 |
+
return jsonify(counts)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@traffic_bp.route("traffic/bandwidth")
|
| 35 |
+
def bandwidth():
|
| 36 |
+
"""
|
| 37 |
+
Returns packet count per second for the last ~30 records.
|
| 38 |
+
Used for bandwidth line chart.
|
| 39 |
+
"""
|
| 40 |
+
events = get_recent_events(200)
|
| 41 |
+
timeline = {}
|
| 42 |
+
|
| 43 |
+
for e in events:
|
| 44 |
+
t = e.get("time")
|
| 45 |
+
timeline[t] = timeline.get(t, 0) + 1
|
| 46 |
+
|
| 47 |
+
graph = [{"time": k, "value": v} for k, v in timeline.items()]
|
| 48 |
+
|
| 49 |
+
return jsonify(graph)
|
backend/sample/bcc_sample.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
proto,src_port,dst_port,flow_duration,total_fwd_pkts,total_bwd_pkts,flags_numeric,payload_len,header_len,rate,iat,syn,ack,rst,fin
|
| 2 |
+
6,12345,443,100000,20,5,2,5000,800,50,20000,1,1,0,0
|
backend/sample/cicids_sample.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Protocol,Dst Port,Flow Duration,Tot Fwd Pkts,Tot Bwd Pkts,TotLen Fwd Pkts,TotLen Bwd Pkts,Fwd Pkt Len Mean,Bwd Pkt Len Mean,Flow IAT Mean,Fwd PSH Flags,Fwd URG Flags,Fwd IAT Mean
|
| 2 |
+
6,443,120000,12,2,4000,1500,350,700,60000,1,0,30000
|
backend/socket_manager.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# socket_manager.py (Optimized)
|
| 2 |
+
# - Non-blocking emit queue with background worker
|
| 3 |
+
# - Rate-limited batching for frequent events
|
| 4 |
+
# - Backwards-compatible init_socketio & emit_new_event API
|
| 5 |
+
|
| 6 |
+
import threading
|
| 7 |
+
import time
|
| 8 |
+
import queue
|
| 9 |
+
|
| 10 |
+
_emit_q = queue.Queue(maxsize=2000)
|
| 11 |
+
_socketio = None
|
| 12 |
+
_emit_lock = threading.Lock()
|
| 13 |
+
_worker_thr = None
|
| 14 |
+
_stop_worker = threading.Event()
|
| 15 |
+
|
| 16 |
+
# batch/rate config
|
| 17 |
+
_BATCH_INTERVAL = 0.5 # seconds between worker sends
|
| 18 |
+
_BATCH_MAX = 10 # max events to bundle per emit
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def init_socketio(socketio):
|
| 22 |
+
"""Initialize global socketio and start background emit worker."""
|
| 23 |
+
global _socketio, _worker_thr
|
| 24 |
+
_socketio = socketio
|
| 25 |
+
print("✅ SocketIO initialized (thread-safe)")
|
| 26 |
+
if _worker_thr is None or not _worker_thr.is_alive():
|
| 27 |
+
_worker_thr = threading.Thread(target=_emit_worker, daemon=True)
|
| 28 |
+
_worker_thr.start()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _emit_worker():
|
| 32 |
+
"""Background worker: drains _emit_q and emits aggregated payloads at intervals."""
|
| 33 |
+
last_send = 0.0
|
| 34 |
+
buffer = []
|
| 35 |
+
while not _stop_worker.is_set():
|
| 36 |
+
try:
|
| 37 |
+
evt = _emit_q.get(timeout=_BATCH_INTERVAL)
|
| 38 |
+
buffer.append(evt)
|
| 39 |
+
except Exception:
|
| 40 |
+
# timeout, flush if buffer exists
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
now = time.time()
|
| 44 |
+
if buffer and (now - last_send >= _BATCH_INTERVAL or len(buffer) >= _BATCH_MAX):
|
| 45 |
+
payload = {"count": len(buffer), "items": buffer[:_BATCH_MAX]}
|
| 46 |
+
try:
|
| 47 |
+
if _socketio:
|
| 48 |
+
# emit in background so worker isn't blocked on network
|
| 49 |
+
_socketio.start_background_task(lambda: _socketio.emit("new_event", payload, namespace="/"))
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print("⚠️ emit worker error:", e)
|
| 52 |
+
buffer.clear()
|
| 53 |
+
last_send = now
|
| 54 |
+
|
| 55 |
+
# final flush on shutdown
|
| 56 |
+
if buffer and _socketio:
|
| 57 |
+
try:
|
| 58 |
+
_socketio.start_background_task(lambda: _socketio.emit("new_event", {"count": len(buffer), "items": buffer}, namespace="/"))
|
| 59 |
+
except Exception:
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def emit_new_event(evt):
|
| 64 |
+
"""Enqueue event for background emit. Non-blocking.
|
| 65 |
+
|
| 66 |
+
Compatible with previous API: callers can pass full event dicts.
|
| 67 |
+
"""
|
| 68 |
+
try:
|
| 69 |
+
_emit_q.put_nowait(evt)
|
| 70 |
+
except queue.Full:
|
| 71 |
+
# drop silently (prefer availability over backlog)
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def shutdown_socket_manager(timeout=2):
|
| 76 |
+
"""Stop background worker gracefully."""
|
| 77 |
+
_stop_worker.set()
|
| 78 |
+
if _worker_thr and _worker_thr.is_alive():
|
| 79 |
+
_worker_thr.join(timeout=timeout)
|
| 80 |
+
|
backend/uploads/bcc_sample.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
proto,src_port,dst_port,flow_duration,total_fwd_pkts,total_bwd_pkts,flags_numeric,payload_len,header_len,rate,iat,syn,ack,rst,fin
|
| 2 |
+
6,12345,443,100000,20,5,2,5000,800,50,20000,1,1,0,0
|
backend/uploads/cicids_sample.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Protocol,Dst Port,Flow Duration,Tot Fwd Pkts,Tot Bwd Pkts,TotLen Fwd Pkts,TotLen Bwd Pkts,Fwd Pkt Len Mean,Bwd Pkt Len Mean,Flow IAT Mean,Fwd PSH Flags,Fwd URG Flags,Fwd IAT Mean
|
| 2 |
+
|
backend/uploads/cicids_sample_1.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Protocol,Dst Port,Flow Duration,Tot Fwd Pkts,Tot Bwd Pkts,TotLen Fwd Pkts,TotLen Bwd Pkts,Fwd Pkt Len Mean,Bwd Pkt Len Mean,Flow IAT Mean,Fwd PSH Flags,Fwd URG Flags,Fwd IAT Mean
|
| 2 |
+
6,443,120000,12,2,4000,1500,350,700,60000,1,0,30000
|
backend/uploads/iris.csv
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
150,4,setosa,versicolor,virginica
|
| 2 |
+
5.1,3.5,1.4,0.2,0
|
| 3 |
+
4.9,3.0,1.4,0.2,0
|
| 4 |
+
4.7,3.2,1.3,0.2,0
|
| 5 |
+
4.6,3.1,1.5,0.2,0
|
| 6 |
+
5.0,3.6,1.4,0.2,0
|
| 7 |
+
5.4,3.9,1.7,0.4,0
|
| 8 |
+
4.6,3.4,1.4,0.3,0
|
| 9 |
+
5.0,3.4,1.5,0.2,0
|
| 10 |
+
4.4,2.9,1.4,0.2,0
|
| 11 |
+
4.9,3.1,1.5,0.1,0
|
| 12 |
+
5.4,3.7,1.5,0.2,0
|
| 13 |
+
4.8,3.4,1.6,0.2,0
|
| 14 |
+
4.8,3.0,1.4,0.1,0
|
| 15 |
+
4.3,3.0,1.1,0.1,0
|
| 16 |
+
5.8,4.0,1.2,0.2,0
|
| 17 |
+
5.7,4.4,1.5,0.4,0
|
| 18 |
+
5.4,3.9,1.3,0.4,0
|
| 19 |
+
5.1,3.5,1.4,0.3,0
|
| 20 |
+
5.7,3.8,1.7,0.3,0
|
| 21 |
+
5.1,3.8,1.5,0.3,0
|
| 22 |
+
5.4,3.4,1.7,0.2,0
|
| 23 |
+
5.1,3.7,1.5,0.4,0
|
| 24 |
+
4.6,3.6,1.0,0.2,0
|
| 25 |
+
5.1,3.3,1.7,0.5,0
|
| 26 |
+
4.8,3.4,1.9,0.2,0
|
| 27 |
+
5.0,3.0,1.6,0.2,0
|
| 28 |
+
5.0,3.4,1.6,0.4,0
|
| 29 |
+
5.2,3.5,1.5,0.2,0
|
| 30 |
+
5.2,3.4,1.4,0.2,0
|
| 31 |
+
4.7,3.2,1.6,0.2,0
|
| 32 |
+
4.8,3.1,1.6,0.2,0
|
| 33 |
+
5.4,3.4,1.5,0.4,0
|
| 34 |
+
5.2,4.1,1.5,0.1,0
|
| 35 |
+
5.5,4.2,1.4,0.2,0
|
| 36 |
+
4.9,3.1,1.5,0.2,0
|
| 37 |
+
5.0,3.2,1.2,0.2,0
|
| 38 |
+
5.5,3.5,1.3,0.2,0
|
| 39 |
+
4.9,3.6,1.4,0.1,0
|
| 40 |
+
4.4,3.0,1.3,0.2,0
|
| 41 |
+
5.1,3.4,1.5,0.2,0
|
| 42 |
+
5.0,3.5,1.3,0.3,0
|
| 43 |
+
4.5,2.3,1.3,0.3,0
|
| 44 |
+
4.4,3.2,1.3,0.2,0
|
| 45 |
+
5.0,3.5,1.6,0.6,0
|
| 46 |
+
5.1,3.8,1.9,0.4,0
|
| 47 |
+
4.8,3.0,1.4,0.3,0
|
| 48 |
+
5.1,3.8,1.6,0.2,0
|
| 49 |
+
4.6,3.2,1.4,0.2,0
|
| 50 |
+
5.3,3.7,1.5,0.2,0
|
| 51 |
+
5.0,3.3,1.4,0.2,0
|
| 52 |
+
7.0,3.2,4.7,1.4,1
|
| 53 |
+
6.4,3.2,4.5,1.5,1
|
| 54 |
+
6.9,3.1,4.9,1.5,1
|
| 55 |
+
5.5,2.3,4.0,1.3,1
|
| 56 |
+
6.5,2.8,4.6,1.5,1
|
| 57 |
+
5.7,2.8,4.5,1.3,1
|
| 58 |
+
6.3,3.3,4.7,1.6,1
|
| 59 |
+
4.9,2.4,3.3,1.0,1
|
| 60 |
+
6.6,2.9,4.6,1.3,1
|
| 61 |
+
5.2,2.7,3.9,1.4,1
|
| 62 |
+
5.0,2.0,3.5,1.0,1
|
| 63 |
+
5.9,3.0,4.2,1.5,1
|
| 64 |
+
6.0,2.2,4.0,1.0,1
|
| 65 |
+
6.1,2.9,4.7,1.4,1
|
| 66 |
+
5.6,2.9,3.6,1.3,1
|
| 67 |
+
6.7,3.1,4.4,1.4,1
|
| 68 |
+
5.6,3.0,4.5,1.5,1
|
| 69 |
+
5.8,2.7,4.1,1.0,1
|
| 70 |
+
6.2,2.2,4.5,1.5,1
|
| 71 |
+
5.6,2.5,3.9,1.1,1
|
| 72 |
+
5.9,3.2,4.8,1.8,1
|
| 73 |
+
6.1,2.8,4.0,1.3,1
|
| 74 |
+
6.3,2.5,4.9,1.5,1
|
| 75 |
+
6.1,2.8,4.7,1.2,1
|
| 76 |
+
6.4,2.9,4.3,1.3,1
|
| 77 |
+
6.6,3.0,4.4,1.4,1
|
| 78 |
+
6.8,2.8,4.8,1.4,1
|
| 79 |
+
6.7,3.0,5.0,1.7,1
|
| 80 |
+
6.0,2.9,4.5,1.5,1
|
| 81 |
+
5.7,2.6,3.5,1.0,1
|
| 82 |
+
5.5,2.4,3.8,1.1,1
|
| 83 |
+
5.5,2.4,3.7,1.0,1
|
| 84 |
+
5.8,2.7,3.9,1.2,1
|
| 85 |
+
6.0,2.7,5.1,1.6,1
|
| 86 |
+
5.4,3.0,4.5,1.5,1
|
| 87 |
+
6.0,3.4,4.5,1.6,1
|
| 88 |
+
6.7,3.1,4.7,1.5,1
|
| 89 |
+
6.3,2.3,4.4,1.3,1
|
| 90 |
+
5.6,3.0,4.1,1.3,1
|
| 91 |
+
5.5,2.5,4.0,1.3,1
|
| 92 |
+
5.5,2.6,4.4,1.2,1
|
| 93 |
+
6.1,3.0,4.6,1.4,1
|
| 94 |
+
5.8,2.6,4.0,1.2,1
|
| 95 |
+
5.0,2.3,3.3,1.0,1
|
| 96 |
+
5.6,2.7,4.2,1.3,1
|
| 97 |
+
5.7,3.0,4.2,1.2,1
|
| 98 |
+
5.7,2.9,4.2,1.3,1
|
| 99 |
+
6.2,2.9,4.3,1.3,1
|
| 100 |
+
5.1,2.5,3.0,1.1,1
|
| 101 |
+
5.7,2.8,4.1,1.3,1
|
| 102 |
+
6.3,3.3,6.0,2.5,2
|
| 103 |
+
5.8,2.7,5.1,1.9,2
|
| 104 |
+
7.1,3.0,5.9,2.1,2
|
| 105 |
+
6.3,2.9,5.6,1.8,2
|
| 106 |
+
6.5,3.0,5.8,2.2,2
|
| 107 |
+
7.6,3.0,6.6,2.1,2
|
| 108 |
+
4.9,2.5,4.5,1.7,2
|
| 109 |
+
7.3,2.9,6.3,1.8,2
|
| 110 |
+
6.7,2.5,5.8,1.8,2
|
| 111 |
+
7.2,3.6,6.1,2.5,2
|
| 112 |
+
6.5,3.2,5.1,2.0,2
|
| 113 |
+
6.4,2.7,5.3,1.9,2
|
| 114 |
+
6.8,3.0,5.5,2.1,2
|
| 115 |
+
5.7,2.5,5.0,2.0,2
|
| 116 |
+
5.8,2.8,5.1,2.4,2
|
| 117 |
+
6.4,3.2,5.3,2.3,2
|
| 118 |
+
6.5,3.0,5.5,1.8,2
|
| 119 |
+
7.7,3.8,6.7,2.2,2
|
| 120 |
+
7.7,2.6,6.9,2.3,2
|
| 121 |
+
6.0,2.2,5.0,1.5,2
|
| 122 |
+
6.9,3.2,5.7,2.3,2
|
| 123 |
+
5.6,2.8,4.9,2.0,2
|
| 124 |
+
7.7,2.8,6.7,2.0,2
|
| 125 |
+
6.3,2.7,4.9,1.8,2
|
| 126 |
+
6.7,3.3,5.7,2.1,2
|
| 127 |
+
7.2,3.2,6.0,1.8,2
|
| 128 |
+
6.2,2.8,4.8,1.8,2
|
| 129 |
+
6.1,3.0,4.9,1.8,2
|
| 130 |
+
6.4,2.8,5.6,2.1,2
|
| 131 |
+
7.2,3.0,5.8,1.6,2
|
| 132 |
+
7.4,2.8,6.1,1.9,2
|
| 133 |
+
7.9,3.8,6.4,2.0,2
|
| 134 |
+
6.4,2.8,5.6,2.2,2
|
| 135 |
+
6.3,2.8,5.1,1.5,2
|
| 136 |
+
6.1,2.6,5.6,1.4,2
|
| 137 |
+
7.7,3.0,6.1,2.3,2
|
| 138 |
+
6.3,3.4,5.6,2.4,2
|
| 139 |
+
6.4,3.1,5.5,1.8,2
|
| 140 |
+
6.0,3.0,4.8,1.8,2
|
| 141 |
+
6.9,3.1,5.4,2.1,2
|
| 142 |
+
6.7,3.1,5.6,2.4,2
|
| 143 |
+
6.9,3.1,5.1,2.3,2
|
| 144 |
+
5.8,2.7,5.1,1.9,2
|
| 145 |
+
6.8,3.2,5.9,2.3,2
|
| 146 |
+
6.7,3.3,5.7,2.5,2
|
| 147 |
+
6.7,3.0,5.2,2.3,2
|
| 148 |
+
6.3,2.5,5.0,1.9,2
|
| 149 |
+
6.5,3.0,5.2,2.0,2
|
| 150 |
+
6.2,3.4,5.4,2.3,2
|
| 151 |
+
5.9,3.0,5.1,1.8,2
|
backend/utils/ai_engine.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/ai_engine.py
|
| 2 |
+
# -----------------------------------------
|
| 3 |
+
# Lightweight "AI" engine using rules + templates
|
| 4 |
+
# No heavy ML model – safe for your laptop 🙂
|
| 5 |
+
|
| 6 |
+
from collections import Counter
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _normalize_label(label: str) -> str:
|
| 11 |
+
return str(label or "Unknown").strip().upper()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# 1️⃣ Explain a single threat/event
|
| 15 |
+
def explain_threat(event: dict) -> str:
|
| 16 |
+
"""
|
| 17 |
+
Takes a single event dict (from logger / recent())
|
| 18 |
+
and returns a human-readable explanation.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
label = _normalize_label(event.get("prediction"))
|
| 22 |
+
risk_level = str(event.get("risk_level", "Low")).title()
|
| 23 |
+
src_ip = event.get("src_ip") or event.get("src") or "Unknown source"
|
| 24 |
+
dst_ip = event.get("dst_ip") or event.get("dst") or "Unknown destination"
|
| 25 |
+
proto = event.get("proto", "Unknown")
|
| 26 |
+
sport = event.get("sport") or event.get("src_port") or "?"
|
| 27 |
+
dport = event.get("dport") or event.get("dst_port") or "?"
|
| 28 |
+
|
| 29 |
+
# Simple knowledge base
|
| 30 |
+
explanations = {
|
| 31 |
+
"VPN": (
|
| 32 |
+
"Traffic from {src} to {dst} over {proto} looks like VPN usage. "
|
| 33 |
+
"VPN tunnels encrypt traffic and can hide the real origin of an attacker. "
|
| 34 |
+
"Review if this VPN endpoint is expected for this host."
|
| 35 |
+
),
|
| 36 |
+
"TOR": (
|
| 37 |
+
"Traffic appears to be routed through the Tor anonymity network. "
|
| 38 |
+
"Tor is commonly used to hide attacker identity. "
|
| 39 |
+
"Investigate the host at {src} and check if Tor usage is allowed."
|
| 40 |
+
),
|
| 41 |
+
"I2P": (
|
| 42 |
+
"Detected I2P (Invisible Internet Project) style traffic. "
|
| 43 |
+
"I2P is an anonymity network similar to Tor and can be abused for C2 channels."
|
| 44 |
+
),
|
| 45 |
+
"FREENET": (
|
| 46 |
+
"Traffic resembles Freenet P2P anonymity network. "
|
| 47 |
+
"Such networks can be used to exchange illegal or malicious content."
|
| 48 |
+
),
|
| 49 |
+
"ZERONET": (
|
| 50 |
+
"ZeroNet-like traffic detected. ZeroNet hosts sites over a P2P network. "
|
| 51 |
+
"This may bypass normal web filtering and logging."
|
| 52 |
+
),
|
| 53 |
+
# CICIDS-style examples – extend as you like
|
| 54 |
+
"DOS HULK": (
|
| 55 |
+
"High-rate HTTP traffic typical of DoS-Hulk attack was detected. "
|
| 56 |
+
"This can exhaust web server resources and cause service disruption."
|
| 57 |
+
),
|
| 58 |
+
"DOS SLOWLORIS": (
|
| 59 |
+
"Slowloris-style DoS traffic detected. It keeps many HTTP connections open "
|
| 60 |
+
"to slowly exhaust server connection limits."
|
| 61 |
+
),
|
| 62 |
+
"BOT": (
|
| 63 |
+
"Behavior suggests the host may be part of a botnet. "
|
| 64 |
+
"Correlate with outbound connections and run malware scans on {src}."
|
| 65 |
+
),
|
| 66 |
+
"BENIGN": (
|
| 67 |
+
"This flow is classified as BENIGN. No immediate malicious pattern detected, "
|
| 68 |
+
"but you should still monitor for anomalies over time."
|
| 69 |
+
),
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Pick best match (exact or substring)
|
| 73 |
+
text = None
|
| 74 |
+
if label in explanations:
|
| 75 |
+
text = explanations[label]
|
| 76 |
+
else:
|
| 77 |
+
for k, v in explanations.items():
|
| 78 |
+
if k in label:
|
| 79 |
+
text = v
|
| 80 |
+
break
|
| 81 |
+
|
| 82 |
+
if text is None:
|
| 83 |
+
text = (
|
| 84 |
+
"The traffic is classified as '{label}' with a risk level of {risk}. "
|
| 85 |
+
"Review source {src} → destination {dst}, protocol {proto}, "
|
| 86 |
+
"and ports {sport} → {dport} for suspicious patterns."
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
return text.format(
|
| 90 |
+
label=label,
|
| 91 |
+
risk=risk_level,
|
| 92 |
+
src=src_ip,
|
| 93 |
+
dst=dst_ip,
|
| 94 |
+
proto=proto,
|
| 95 |
+
sport=sport,
|
| 96 |
+
dport=dport,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# 2️⃣ Summarize multiple events (for report)
|
| 101 |
+
def summarize_events(events, model: str = "bcc") -> str:
|
| 102 |
+
"""
|
| 103 |
+
Takes a list of events and returns a high-level English summary.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
if not events:
|
| 107 |
+
return "No recent events available for summary."
|
| 108 |
+
|
| 109 |
+
labels = [_normalize_label(e.get("prediction")) for e in events]
|
| 110 |
+
counts = Counter(labels)
|
| 111 |
+
total = len(events)
|
| 112 |
+
|
| 113 |
+
high_risk_keywords = [
|
| 114 |
+
"DDOS", "DOS", "BRUTE", "SQL", "BOT", "INFILTRATION", "HULK",
|
| 115 |
+
"SLOWLORIS", "SLOWHTTPTEST"
|
| 116 |
+
]
|
| 117 |
+
high_risk = sum(
|
| 118 |
+
c for lbl, c in counts.items()
|
| 119 |
+
if any(k in lbl for k in high_risk_keywords)
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
tor_like = sum(
|
| 123 |
+
counts.get(lbl, 0) for lbl in ["TOR", "I2P", "ZERONET", "FREENET", "VPN"]
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 127 |
+
|
| 128 |
+
# Build readable summary
|
| 129 |
+
parts = [
|
| 130 |
+
f"AI Summary generated at {ts} for model '{model.upper()}'.",
|
| 131 |
+
f"Total analysed events: {total}.",
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
if high_risk:
|
| 135 |
+
parts.append(
|
| 136 |
+
f"High-risk attacks detected: {high_risk} events "
|
| 137 |
+
f"({', '.join(k for k in counts.keys() if any(x in k for x in high_risk_keywords))})."
|
| 138 |
+
)
|
| 139 |
+
else:
|
| 140 |
+
parts.append("No high-risk attack pattern strongly detected in this window.")
|
| 141 |
+
|
| 142 |
+
if tor_like:
|
| 143 |
+
parts.append(
|
| 144 |
+
f"Anonymity or tunneling traffic (VPN/TOR/I2P/etc.) observed in {tor_like} events. "
|
| 145 |
+
"Verify if this usage is expected and authorized."
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# top 3 labels
|
| 149 |
+
top3 = counts.most_common(3)
|
| 150 |
+
label_str = ", ".join(f"{lbl}: {cnt}" for lbl, cnt in top3)
|
| 151 |
+
parts.append(f"Top traffic classes: {label_str}.")
|
| 152 |
+
|
| 153 |
+
if model == "bcc":
|
| 154 |
+
parts.append(
|
| 155 |
+
"BCC model focuses on live packet patterns; consider correlating with host logs "
|
| 156 |
+
"for deeper forensic analysis."
|
| 157 |
+
)
|
| 158 |
+
else:
|
| 159 |
+
parts.append(
|
| 160 |
+
"CICIDS model analyses flow-level statistics; consider exporting flows for "
|
| 161 |
+
"offline investigation if anomalies increase."
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return " ".join(parts)
|
backend/utils/geo_lookup.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/geo_lookup.py
|
| 2 |
+
# ==========================================
|
| 3 |
+
# 🌍 GEO LOOKUP UTILITY — Robust version
|
| 4 |
+
# - Uses ipwho.is
|
| 5 |
+
# - Validates inputs
|
| 6 |
+
# - Caches results
|
| 7 |
+
# - Graceful fallback for bad/ private IPs
|
| 8 |
+
# ==========================================
|
| 9 |
+
|
| 10 |
+
import requests
|
| 11 |
+
from functools import lru_cache
|
| 12 |
+
import re
|
| 13 |
+
import time
|
| 14 |
+
|
| 15 |
+
# Public API (no API key)
|
| 16 |
+
GEO_API = "https://ipwho.is/{ip}"
|
| 17 |
+
|
| 18 |
+
# Regex for private/reserved IPv4 blocks + simple IPv4/IPv6 check
|
| 19 |
+
_IPV4_RE = re.compile(r"^(?:\d{1,3}\.){3}\d{1,3}$")
|
| 20 |
+
_IPV6_RE = re.compile(r"^[0-9a-fA-F:]+$")
|
| 21 |
+
|
| 22 |
+
PRIVATE_IP_RANGES = [
|
| 23 |
+
re.compile(r"^127\."), # localhost
|
| 24 |
+
re.compile(r"^10\."), # private
|
| 25 |
+
re.compile(r"^192\.168\."), # private
|
| 26 |
+
re.compile(r"^172\.(1[6-9]|2[0-9]|3[0-1])\."), # private block
|
| 27 |
+
re.compile(r"^0\."), # invalid
|
| 28 |
+
re.compile(r"^255\."), # broadcast/reserved
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
# Cache size tuned to common usage (increase if you have many distinct IPs)
|
| 32 |
+
@lru_cache(maxsize=2000)
|
| 33 |
+
def get_geo_info(ip: str) -> dict:
|
| 34 |
+
"""Return geolocation info for an IP address (string-safe, cached, fallback)."""
|
| 35 |
+
# Normalize
|
| 36 |
+
try:
|
| 37 |
+
ip_raw = ip
|
| 38 |
+
if ip is None:
|
| 39 |
+
return _default_geo(ip, "Empty IP")
|
| 40 |
+
ip = str(ip).strip()
|
| 41 |
+
except Exception:
|
| 42 |
+
return _default_geo(ip, "Invalid IP")
|
| 43 |
+
|
| 44 |
+
# Quick checks
|
| 45 |
+
if ip == "" or ip.lower() in ("unknown", "n/a", "na", "local", "localhost"):
|
| 46 |
+
return _default_geo(ip, "Unknown")
|
| 47 |
+
|
| 48 |
+
# If it's clearly not an IPv4/IPv6 string, avoid calling external API
|
| 49 |
+
if not (_IPV4_RE.match(ip) or _IPV6_RE.match(ip)):
|
| 50 |
+
return _default_geo(ip, "Not an IP")
|
| 51 |
+
|
| 52 |
+
# Private/reserved check
|
| 53 |
+
if any(r.match(ip) for r in PRIVATE_IP_RANGES):
|
| 54 |
+
return {
|
| 55 |
+
"ip": ip,
|
| 56 |
+
"country": "Local",
|
| 57 |
+
"city": "Private Network",
|
| 58 |
+
"lat": 0.0,
|
| 59 |
+
"lon": 0.0,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Query remote API (with timeout + basic retry)
|
| 63 |
+
try:
|
| 64 |
+
# simple single attempt with timeout; if you need reliability add a tiny backoff/retry
|
| 65 |
+
res = requests.get(GEO_API.format(ip=ip), timeout=4)
|
| 66 |
+
if res.status_code == 200:
|
| 67 |
+
data = res.json()
|
| 68 |
+
# ipwho.is returns {"success": false, "message": "..."} for invalid
|
| 69 |
+
if data.get("success", True) is False:
|
| 70 |
+
return _default_geo(ip, data.get("message", "Invalid IP"))
|
| 71 |
+
return {
|
| 72 |
+
"ip": ip,
|
| 73 |
+
"country": data.get("country", "Unknown"),
|
| 74 |
+
"city": data.get("city", "Unknown"),
|
| 75 |
+
"lat": float(data.get("latitude") or 0.0),
|
| 76 |
+
"lon": float(data.get("longitude") or 0.0),
|
| 77 |
+
}
|
| 78 |
+
# non-200 -> fallback
|
| 79 |
+
print(f"⚠️ Geo lookup failed for {ip} (status {res.status_code})")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
# network errors, DNS issues, etc.
|
| 82 |
+
print(f"⚠️ Geo lookup error for {ip}: {e}")
|
| 83 |
+
|
| 84 |
+
return _default_geo(ip, "Unknown")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _default_geo(ip: str, reason="Unknown"):
|
| 88 |
+
"""Return default location info when lookup fails."""
|
| 89 |
+
return {
|
| 90 |
+
"ip": ip,
|
| 91 |
+
"country": reason,
|
| 92 |
+
"city": "Unknown",
|
| 93 |
+
"lat": 0.0,
|
| 94 |
+
"lon": 0.0,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def enrich_event_with_geo(evt: dict) -> dict:
|
| 99 |
+
"""
|
| 100 |
+
Given an event dict that contains 'src_ip' and 'dst_ip' (or similar keys),
|
| 101 |
+
attach src/dst city, country, lat, lon fields.
|
| 102 |
+
This function is safe to call synchronously, but consider async enrichment
|
| 103 |
+
when running on a hot packet-processing loop (see optional snippet below).
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
# Accept multiple possible keys (compatibility)
|
| 107 |
+
src_ip = evt.get("src_ip") or evt.get("src") or evt.get("srcIP") or ""
|
| 108 |
+
dst_ip = evt.get("dst_ip") or evt.get("dst") or evt.get("dstIP") or ""
|
| 109 |
+
|
| 110 |
+
# Normalize to string before calling get_geo_info
|
| 111 |
+
src_ip = str(src_ip).strip() if src_ip is not None else ""
|
| 112 |
+
dst_ip = str(dst_ip).strip() if dst_ip is not None else ""
|
| 113 |
+
|
| 114 |
+
# Get geo info (cached)
|
| 115 |
+
src_info = get_geo_info(src_ip)
|
| 116 |
+
dst_info = get_geo_info(dst_ip)
|
| 117 |
+
|
| 118 |
+
evt.update({
|
| 119 |
+
"src_country": src_info["country"],
|
| 120 |
+
"dst_country": dst_info["country"],
|
| 121 |
+
"src_city": src_info["city"],
|
| 122 |
+
"dst_city": dst_info["city"],
|
| 123 |
+
"src_lat": src_info["lat"],
|
| 124 |
+
"src_lon": src_info["lon"],
|
| 125 |
+
"dst_lat": dst_info["lat"],
|
| 126 |
+
"dst_lon": dst_info["lon"],
|
| 127 |
+
})
|
| 128 |
+
except Exception as e:
|
| 129 |
+
# Keep it quiet but informative
|
| 130 |
+
print(f"⚠️ Geo enrichment failed for event: {e}")
|
| 131 |
+
|
| 132 |
+
return evt
|
| 133 |
+
|
backend/utils/logger.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# logger.py (Model-separated, non-blocking logger, per-model CSVs)
|
| 2 |
+
# -------------------------------------------------------------
|
| 3 |
+
import os
|
| 4 |
+
import csv
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
LOG_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "logs"))
|
| 11 |
+
os.makedirs(LOG_DIR, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
BCC_LOG_FILE = os.path.join(LOG_DIR, "bcc_logs.csv")
|
| 14 |
+
CICIDS_LOG_FILE = os.path.join(LOG_DIR, "cicids_logs.csv")
|
| 15 |
+
|
| 16 |
+
_MAX_RECENT = 500
|
| 17 |
+
_FLUSH_INTERVAL = 2.0
|
| 18 |
+
_FLUSH_BATCH = 50
|
| 19 |
+
|
| 20 |
+
_headers = [
|
| 21 |
+
"time", "src_ip", "sport", "dst_ip", "dport", "proto",
|
| 22 |
+
"prediction", "risk_level", "risk_score",
|
| 23 |
+
"src_country", "src_city", "src_lat", "src_lon",
|
| 24 |
+
"dst_country", "dst_city", "dst_lat", "dst_lon"
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
# In-memory per-model buffers & stats
|
| 28 |
+
_model_events = {
|
| 29 |
+
"bcc": [], # list of dicts
|
| 30 |
+
"cicids": []
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
_model_stats = {
|
| 34 |
+
"bcc": {},
|
| 35 |
+
"cicids": {}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# active model (default)
|
| 39 |
+
_active_model_lock = threading.Lock()
|
| 40 |
+
_active_model = "bcc"
|
| 41 |
+
|
| 42 |
+
# writer buffers and locks
|
| 43 |
+
_write_buffer = [] # list of dicts, each item must include "model" key
|
| 44 |
+
_buffer_lock = threading.Lock()
|
| 45 |
+
_events_lock = threading.Lock()
|
| 46 |
+
|
| 47 |
+
_stop_writer = threading.Event()
|
| 48 |
+
|
| 49 |
+
# -------------------------
|
| 50 |
+
# Helpers: file name for model
|
| 51 |
+
# -------------------------
|
| 52 |
+
def _file_for_model(model):
|
| 53 |
+
if model == "cicids":
|
| 54 |
+
return CICIDS_LOG_FILE
|
| 55 |
+
return BCC_LOG_FILE
|
| 56 |
+
|
| 57 |
+
# -------------------------
|
| 58 |
+
# Full overwrite for a model CSV
|
| 59 |
+
# -------------------------
|
| 60 |
+
def _flush_full_overwrite_model(model):
|
| 61 |
+
"""Rewrite the entire CSV for a specific model from its in-memory buffer."""
|
| 62 |
+
fname = _file_for_model(model)
|
| 63 |
+
try:
|
| 64 |
+
with _events_lock:
|
| 65 |
+
rows = list(_model_events.get(model, []))
|
| 66 |
+
with open(fname, "w", newline="", encoding="utf-8") as f:
|
| 67 |
+
writer = csv.DictWriter(f, fieldnames=_headers)
|
| 68 |
+
writer.writeheader()
|
| 69 |
+
for row in rows:
|
| 70 |
+
writer.writerow({k: row.get(k, "") for k in _headers})
|
| 71 |
+
# optional debug print
|
| 72 |
+
# print(f"[logger] {model} CSV fully rewritten: {len(rows)} rows -> {fname}")
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print("[logger] Full overwrite failed:", e)
|
| 75 |
+
|
| 76 |
+
# -------------------------
|
| 77 |
+
# Flush small batches to disk (append)
|
| 78 |
+
# -------------------------
|
| 79 |
+
def _flush_to_disk():
|
| 80 |
+
global _write_buffer
|
| 81 |
+
with _buffer_lock:
|
| 82 |
+
if not _write_buffer:
|
| 83 |
+
return
|
| 84 |
+
batch = _write_buffer[:_FLUSH_BATCH]
|
| 85 |
+
_write_buffer = _write_buffer[len(batch):]
|
| 86 |
+
|
| 87 |
+
# group by model for efficient writes
|
| 88 |
+
groups = {}
|
| 89 |
+
for row in batch:
|
| 90 |
+
m = row.get("model", "bcc")
|
| 91 |
+
groups.setdefault(m, []).append(row)
|
| 92 |
+
|
| 93 |
+
for model, rows in groups.items():
|
| 94 |
+
fname = _file_for_model(model)
|
| 95 |
+
try:
|
| 96 |
+
file_empty = not os.path.exists(fname) or os.stat(fname).st_size == 0
|
| 97 |
+
with open(fname, "a", newline="", encoding="utf-8") as f:
|
| 98 |
+
writer = csv.DictWriter(f, fieldnames=_headers)
|
| 99 |
+
if file_empty:
|
| 100 |
+
writer.writeheader()
|
| 101 |
+
for r in rows:
|
| 102 |
+
# write only header keys (ignore extra)
|
| 103 |
+
writer.writerow({k: r.get(k, "") for k in _headers})
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print("[logger] Append write error for", model, ":", e)
|
| 106 |
+
|
| 107 |
+
# -------------------------
|
| 108 |
+
# Background writer thread
|
| 109 |
+
# -------------------------
|
| 110 |
+
def _writer_thread():
|
| 111 |
+
while not _stop_writer.is_set():
|
| 112 |
+
time.sleep(_FLUSH_INTERVAL)
|
| 113 |
+
_flush_to_disk()
|
| 114 |
+
# flush remaining on shutdown
|
| 115 |
+
_flush_to_disk()
|
| 116 |
+
|
| 117 |
+
_writer_thr = threading.Thread(target=_writer_thread, daemon=True)
|
| 118 |
+
_writer_thr.start()
|
| 119 |
+
|
| 120 |
+
# -------------------------
|
| 121 |
+
# Load existing CSVs into _model_events on startup (keep last _MAX_RECENT)
|
| 122 |
+
# -------------------------
|
| 123 |
+
def _load_recent_model(model):
|
| 124 |
+
fname = _file_for_model(model)
|
| 125 |
+
if not os.path.exists(fname):
|
| 126 |
+
return []
|
| 127 |
+
try:
|
| 128 |
+
with open(fname, "r", encoding="utf-8") as f:
|
| 129 |
+
reader = list(csv.DictReader(f))
|
| 130 |
+
return reader[-_MAX_RECENT:]
|
| 131 |
+
except Exception:
|
| 132 |
+
return []
|
| 133 |
+
|
| 134 |
+
def _load_all_recent():
|
| 135 |
+
global _model_events
|
| 136 |
+
with _events_lock:
|
| 137 |
+
_model_events["bcc"] = _load_recent_model("bcc")
|
| 138 |
+
_model_events["cicids"] = _load_recent_model("cicids")
|
| 139 |
+
|
| 140 |
+
_load_all_recent()
|
| 141 |
+
|
| 142 |
+
# ===============================
|
| 143 |
+
# Public API: push_event
|
| 144 |
+
# ===============================
|
| 145 |
+
def push_event(evt):
|
| 146 |
+
"""
|
| 147 |
+
evt: dict containing event fields expected (prediction, src_ip, dst_ip, etc.)
|
| 148 |
+
Uses current active model to store event.
|
| 149 |
+
Also enqueues to write buffer for background flush.
|
| 150 |
+
"""
|
| 151 |
+
global _write_buffer
|
| 152 |
+
|
| 153 |
+
# attach model at time of push
|
| 154 |
+
with _active_model_lock:
|
| 155 |
+
model = _active_model
|
| 156 |
+
|
| 157 |
+
e = dict(evt)
|
| 158 |
+
e.setdefault("time", datetime.now().strftime("%H:%M:%S"))
|
| 159 |
+
e.setdefault("risk_level", "Low")
|
| 160 |
+
e.setdefault("risk_score", 0)
|
| 161 |
+
|
| 162 |
+
# add to in-memory buffer for model
|
| 163 |
+
with _events_lock:
|
| 164 |
+
_model_events.setdefault(model, [])
|
| 165 |
+
_model_events[model].append(e)
|
| 166 |
+
if len(_model_events[model]) > _MAX_RECENT:
|
| 167 |
+
_model_events[model] = _model_events[model][-_MAX_RECENT:]
|
| 168 |
+
|
| 169 |
+
# update stats
|
| 170 |
+
pred = str(e.get("prediction", "Unknown"))
|
| 171 |
+
_model_stats.setdefault(model, {})
|
| 172 |
+
_model_stats[model][pred] = _model_stats[model].get(pred, 0) + 1
|
| 173 |
+
|
| 174 |
+
# add to write buffer with model tag for background writer
|
| 175 |
+
item = dict(e)
|
| 176 |
+
item["model"] = model
|
| 177 |
+
with _buffer_lock:
|
| 178 |
+
_write_buffer.append(item)
|
| 179 |
+
# if buffer grows big, flush asynchronously
|
| 180 |
+
if len(_write_buffer) > (_FLUSH_BATCH * 4):
|
| 181 |
+
threading.Thread(target=_flush_to_disk, daemon=True).start()
|
| 182 |
+
|
| 183 |
+
# ===============================
|
| 184 |
+
# Public API: get recent & stats
|
| 185 |
+
# ===============================
|
| 186 |
+
def get_recent_events(model="bcc", n=None):
|
| 187 |
+
with _events_lock:
|
| 188 |
+
data = list(_model_events.get(model, []))
|
| 189 |
+
if n:
|
| 190 |
+
return data[-n:]
|
| 191 |
+
return data
|
| 192 |
+
|
| 193 |
+
def get_model_stats(model="bcc"):
|
| 194 |
+
with _events_lock:
|
| 195 |
+
# return a shallow copy to avoid external mutation
|
| 196 |
+
return dict(_model_stats.get(model, {}))
|
| 197 |
+
|
| 198 |
+
# -------------------------
|
| 199 |
+
# Convenience: summary across active model (legacy)
|
| 200 |
+
# -------------------------
|
| 201 |
+
def summarize_counts():
|
| 202 |
+
with _active_model_lock:
|
| 203 |
+
model = _active_model
|
| 204 |
+
return get_model_stats(model)
|
| 205 |
+
|
| 206 |
+
# ===============================
|
| 207 |
+
# Model selection API
|
| 208 |
+
# ===============================
|
| 209 |
+
def set_active_model(model):
|
| 210 |
+
if model not in ("bcc", "cicids"):
|
| 211 |
+
raise ValueError("invalid model")
|
| 212 |
+
with _active_model_lock:
|
| 213 |
+
global _active_model
|
| 214 |
+
_active_model = model
|
| 215 |
+
# no immediate clearing — in-memory buffers persist per model
|
| 216 |
+
return _active_model
|
| 217 |
+
|
| 218 |
+
def get_active_model():
|
| 219 |
+
with _active_model_lock:
|
| 220 |
+
return _active_model
|
| 221 |
+
|
| 222 |
+
# ===============================
|
| 223 |
+
# CLEAR / DELETE (model-wise)
|
| 224 |
+
# ===============================
|
| 225 |
+
def clear_last_events(model="bcc", n=99999):
|
| 226 |
+
with _events_lock:
|
| 227 |
+
ev = _model_events.get(model, [])
|
| 228 |
+
if n >= len(ev):
|
| 229 |
+
_model_events[model] = []
|
| 230 |
+
else:
|
| 231 |
+
_model_events[model] = ev[:-n]
|
| 232 |
+
# reset stats for this model
|
| 233 |
+
_model_stats[model] = {}
|
| 234 |
+
# rewrite model CSV fully
|
| 235 |
+
_flush_full_overwrite_model(model)
|
| 236 |
+
return True
|
| 237 |
+
|
| 238 |
+
def delete_by_index(model="bcc", idx=0):
|
| 239 |
+
with _events_lock:
|
| 240 |
+
ev = _model_events.get(model, [])
|
| 241 |
+
if 0 <= idx < len(ev):
|
| 242 |
+
ev.pop(idx)
|
| 243 |
+
_model_events[model] = ev
|
| 244 |
+
# recompute stats (simple recompute)
|
| 245 |
+
_model_stats[model] = {}
|
| 246 |
+
for e in ev:
|
| 247 |
+
pred = str(e.get("prediction", "Unknown"))
|
| 248 |
+
_model_stats[model][pred] = _model_stats[model].get(pred, 0) + 1
|
| 249 |
+
_flush_full_overwrite_model(model)
|
| 250 |
+
return True
|
| 251 |
+
return False
|
| 252 |
+
|
| 253 |
+
def delete_by_prediction(model="bcc", pred=None):
|
| 254 |
+
if pred is None:
|
| 255 |
+
return False
|
| 256 |
+
with _events_lock:
|
| 257 |
+
ev = _model_events.get(model, [])
|
| 258 |
+
_model_events[model] = [e for e in ev if e.get("prediction") != pred]
|
| 259 |
+
# recompute stats
|
| 260 |
+
_model_stats[model] = {}
|
| 261 |
+
for e in _model_events[model]:
|
| 262 |
+
p = str(e.get("prediction", "Unknown"))
|
| 263 |
+
_model_stats[model][p] = _model_stats[model].get(p, 0) + 1
|
| 264 |
+
_flush_full_overwrite_model(model)
|
| 265 |
+
return True
|
| 266 |
+
|
| 267 |
+
# ===============================
|
| 268 |
+
# Shutdown
|
| 269 |
+
# ===============================
|
| 270 |
+
def shutdown_logger():
|
| 271 |
+
_stop_writer.set()
|
| 272 |
+
_writer_thr.join(timeout=3)
|
| 273 |
+
|
backend/utils/model_selector.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import joblib
|
| 3 |
+
import threading
|
| 4 |
+
import traceback
|
| 5 |
+
|
| 6 |
+
# Global active model (default = bcc so your current flow remains unchanged)
|
| 7 |
+
ACTIVE_MODEL = "bcc"
|
| 8 |
+
_ACTIVE_LOCK = threading.Lock()
|
| 9 |
+
|
| 10 |
+
# Cache loaded models to avoid repeated disk loads
|
| 11 |
+
_MODEL_CACHE = {}
|
| 12 |
+
ML_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ml_models"))
|
| 13 |
+
print("[model_selector] ML_DIR =", ML_DIR)
|
| 14 |
+
try:
|
| 15 |
+
print("[model_selector] ML_DIR files:", os.listdir(ML_DIR))
|
| 16 |
+
except Exception as e:
|
| 17 |
+
print("[model_selector] Could not list ML_DIR:", e)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _try_load(path):
|
| 21 |
+
"""Try to joblib.load(path). On failure return None but print full traceback."""
|
| 22 |
+
if not os.path.exists(path):
|
| 23 |
+
print(f"[model_selector] SKIP (not found): {path}")
|
| 24 |
+
return None
|
| 25 |
+
try:
|
| 26 |
+
print(f"[model_selector] Attempting to load: {path}")
|
| 27 |
+
obj = joblib.load(path)
|
| 28 |
+
print(f"[model_selector] Successfully loaded: {os.path.basename(path)}")
|
| 29 |
+
return obj
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"[model_selector] FAILED to load {path}: {e}")
|
| 32 |
+
traceback.print_exc()
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
def load_model(model_key):
|
| 36 |
+
"""Return a dict with keys depending on model. Caches result."""
|
| 37 |
+
if model_key in _MODEL_CACHE:
|
| 38 |
+
return _MODEL_CACHE[model_key]
|
| 39 |
+
|
| 40 |
+
if model_key == "bcc":
|
| 41 |
+
# original BCC artifact names (your working files)
|
| 42 |
+
model_path = os.path.join(ML_DIR, "realtime_model.pkl")
|
| 43 |
+
scaler_path = os.path.join(ML_DIR, "realtime_scaler.pkl")
|
| 44 |
+
encoder_path = os.path.join(ML_DIR, "realtime_encoder.pkl")
|
| 45 |
+
|
| 46 |
+
model = _try_load(model_path)
|
| 47 |
+
scaler = _try_load(scaler_path)
|
| 48 |
+
encoder = _try_load(encoder_path)
|
| 49 |
+
|
| 50 |
+
if model is None:
|
| 51 |
+
print(f"[model_selector] WARNING: bcc model not found at {model_path}")
|
| 52 |
+
_MODEL_CACHE["bcc"] = {"model": model, "scaler": scaler, "encoder": encoder}
|
| 53 |
+
return _MODEL_CACHE["bcc"]
|
| 54 |
+
|
| 55 |
+
if model_key == "cicids":
|
| 56 |
+
# Prefer the RF pipeline you requested; try common names in preferred order
|
| 57 |
+
candidate_models = [
|
| 58 |
+
"rf_pipeline.joblib", # preferred - your RF pipeline
|
| 59 |
+
"cicids_rf.joblib",
|
| 60 |
+
"rf_pipeline.pkl",
|
| 61 |
+
"cicids_model.joblib",
|
| 62 |
+
"lgb_pipeline.joblib",
|
| 63 |
+
"cicids_rf.pkl",
|
| 64 |
+
]
|
| 65 |
+
# prefer 'training_artifacts' or 'cicids_artifacts'
|
| 66 |
+
candidate_artifacts = [
|
| 67 |
+
"training_artifacts.joblib",
|
| 68 |
+
"training_artifacts.pkl",
|
| 69 |
+
"cicids_artifacts.joblib",
|
| 70 |
+
"cicids_artifacts.pkl",
|
| 71 |
+
"artifacts.joblib",
|
| 72 |
+
"artifacts.pkl"
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
model = None
|
| 76 |
+
artifacts = None
|
| 77 |
+
for fn in candidate_models:
|
| 78 |
+
p = os.path.join(ML_DIR, fn)
|
| 79 |
+
model = _try_load(p)
|
| 80 |
+
if model is not None:
|
| 81 |
+
print(f"[model_selector] Loaded cicids model from {p}")
|
| 82 |
+
break
|
| 83 |
+
|
| 84 |
+
for fn in candidate_artifacts:
|
| 85 |
+
p = os.path.join(ML_DIR, fn)
|
| 86 |
+
artifacts = _try_load(p)
|
| 87 |
+
if artifacts is not None:
|
| 88 |
+
print(f"[model_selector] Loaded cicids artifacts from {p}")
|
| 89 |
+
break
|
| 90 |
+
|
| 91 |
+
if model is None:
|
| 92 |
+
print("[model_selector] WARNING: No cicids model found in ml_models.")
|
| 93 |
+
if artifacts is None:
|
| 94 |
+
print("[model_selector] WARNING: No cicids artifacts found in ml_models.")
|
| 95 |
+
|
| 96 |
+
# artifacts expected to include: 'scaler' and 'features' at minimum
|
| 97 |
+
_MODEL_CACHE["cicids"] = {
|
| 98 |
+
"model": model,
|
| 99 |
+
"artifacts": artifacts
|
| 100 |
+
}
|
| 101 |
+
return _MODEL_CACHE["cicids"]
|
| 102 |
+
|
| 103 |
+
raise ValueError("Unknown model_key")
|
| 104 |
+
|
| 105 |
+
def set_active_model(key: str):
|
| 106 |
+
global ACTIVE_MODEL
|
| 107 |
+
if key not in ("bcc", "cicids"):
|
| 108 |
+
raise ValueError("Active model must be 'bcc' or 'cicids'")
|
| 109 |
+
with _ACTIVE_LOCK:
|
| 110 |
+
ACTIVE_MODEL = key
|
| 111 |
+
print(f"[model_selector] ACTIVE_MODEL set to: {ACTIVE_MODEL}")
|
| 112 |
+
|
| 113 |
+
def get_active_model():
|
| 114 |
+
return ACTIVE_MODEL
|
| 115 |
+
|
| 116 |
+
|
backend/utils/pcap_to_csv.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from scapy.all import rdpcap
|
| 3 |
+
|
| 4 |
+
def convert_pcap_to_csv(input_pcap):
|
| 5 |
+
packets = rdpcap(input_pcap)
|
| 6 |
+
data = []
|
| 7 |
+
|
| 8 |
+
for pkt in packets:
|
| 9 |
+
try:
|
| 10 |
+
row = {
|
| 11 |
+
"src_port": pkt.sport if hasattr(pkt, "sport") else 0,
|
| 12 |
+
"dst_port": pkt.dport if hasattr(pkt, "dport") else 0,
|
| 13 |
+
"proto": pkt.proto if hasattr(pkt, "proto") else 0,
|
| 14 |
+
"payload_len": len(pkt.payload)
|
| 15 |
+
}
|
| 16 |
+
data.append(row)
|
| 17 |
+
except:
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
df = pd.DataFrame(data)
|
| 21 |
+
out_csv = input_pcap + ".csv"
|
| 22 |
+
df.to_csv(out_csv, index=False)
|
| 23 |
+
return out_csv
|
backend/utils/risk_engine.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# risk_engine.py (Optimized)
|
| 2 |
+
# - Accepts optional `recent_events` to avoid repeated disk/IO calls
|
| 3 |
+
# - Uses light-weight counters and caching for frequency checks
|
| 4 |
+
# - Returns (level, score) as before
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
import time
|
| 8 |
+
from utils.logger import get_recent_events
|
| 9 |
+
|
| 10 |
+
# small in-memory cache for source counts to avoid repeated scans
|
| 11 |
+
_SRC_CACHE = {
|
| 12 |
+
"ts": 0,
|
| 13 |
+
"counts": {},
|
| 14 |
+
"ttl": 2.0 # seconds
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _build_source_cache(recent_events):
|
| 19 |
+
counts = {}
|
| 20 |
+
for e in recent_events:
|
| 21 |
+
s = e.get("src_ip")
|
| 22 |
+
if s:
|
| 23 |
+
counts[s] = counts.get(s, 0) + 1
|
| 24 |
+
return counts
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def compute_risk_score(evt, recent_events=None):
|
| 28 |
+
"""Compute adaptive risk score (0–100).
|
| 29 |
+
|
| 30 |
+
If `recent_events` is provided, it is used directly. Otherwise `get_recent_events()`
|
| 31 |
+
is called once (limited inside the function).
|
| 32 |
+
"""
|
| 33 |
+
label = (evt.get("prediction") or "").upper()
|
| 34 |
+
src_ip = evt.get("src_ip") or ""
|
| 35 |
+
|
| 36 |
+
base_map = {
|
| 37 |
+
"TOR": 90,
|
| 38 |
+
"I2P": 85,
|
| 39 |
+
"ZERONET": 70,
|
| 40 |
+
"VPN": 55,
|
| 41 |
+
"FREENET": 60,
|
| 42 |
+
"HTTP": 30,
|
| 43 |
+
"DNS": 25,
|
| 44 |
+
}
|
| 45 |
+
base = base_map.get(label, 35)
|
| 46 |
+
|
| 47 |
+
# get recent events once if not provided
|
| 48 |
+
if recent_events is None:
|
| 49 |
+
recent_events = get_recent_events()
|
| 50 |
+
|
| 51 |
+
# try cached counts for short TTL
|
| 52 |
+
now = time.time()
|
| 53 |
+
if now - _SRC_CACHE.get("ts", 0) > _SRC_CACHE.get("ttl", 2.0) or not _SRC_CACHE.get("counts"):
|
| 54 |
+
_SRC_CACHE["counts"] = _build_source_cache(recent_events)
|
| 55 |
+
_SRC_CACHE["ts"] = now
|
| 56 |
+
|
| 57 |
+
freq = _SRC_CACHE["counts"].get(src_ip, 0)
|
| 58 |
+
|
| 59 |
+
freq_boost = 0
|
| 60 |
+
if freq >= 3:
|
| 61 |
+
freq_boost = 5
|
| 62 |
+
if freq >= 6:
|
| 63 |
+
freq_boost = 15
|
| 64 |
+
|
| 65 |
+
noise = random.randint(-3, 3)
|
| 66 |
+
|
| 67 |
+
score = min(100, max(0, base + freq_boost + noise))
|
| 68 |
+
|
| 69 |
+
if score >= 80:
|
| 70 |
+
level = "High"
|
| 71 |
+
elif score >= 50:
|
| 72 |
+
level = "Medium"
|
| 73 |
+
else:
|
| 74 |
+
level = "Low"
|
| 75 |
+
|
| 76 |
+
return level, score
|
| 77 |
+
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/components.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "default",
|
| 4 |
+
"rsc": false,
|
| 5 |
+
"tsx": false,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "tailwind.config.js",
|
| 8 |
+
"css": "src/index.css",
|
| 9 |
+
"baseColor": "slate",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"iconLibrary": "lucide",
|
| 14 |
+
"aliases": {
|
| 15 |
+
"components": "@/components",
|
| 16 |
+
"utils": "@/lib/utils",
|
| 17 |
+
"ui": "@/components/ui",
|
| 18 |
+
"lib": "@/lib",
|
| 19 |
+
"hooks": "@/hooks"
|
| 20 |
+
},
|
| 21 |
+
"registries": {}
|
| 22 |
+
}
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 6 |
+
|
| 7 |
+
export default defineConfig([
|
| 8 |
+
globalIgnores(['dist']),
|
| 9 |
+
{
|
| 10 |
+
files: ['**/*.{js,jsx}'],
|
| 11 |
+
extends: [
|
| 12 |
+
js.configs.recommended,
|
| 13 |
+
reactHooks.configs['recommended-latest'],
|
| 14 |
+
reactRefresh.configs.vite,
|
| 15 |
+
],
|
| 16 |
+
languageOptions: {
|
| 17 |
+
ecmaVersion: 2020,
|
| 18 |
+
globals: globals.browser,
|
| 19 |
+
parserOptions: {
|
| 20 |
+
ecmaVersion: 'latest',
|
| 21 |
+
ecmaFeatures: { jsx: true },
|
| 22 |
+
sourceType: 'module',
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
rules: {
|
| 26 |
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<link
|
| 7 |
+
rel="stylesheet"
|
| 8 |
+
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
| 9 |
+
/>
|
| 10 |
+
<link rel="shortcut icon" href="/images.ico" />
|
| 11 |
+
<title>NIDS Cyber Security</title>
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<div id="root"></div>
|
| 15 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 16 |
+
</body>
|
| 17 |
+
</html>
|
frontend/jsconfig.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"baseUrl": "./src",
|
| 4 |
+
"paths": {
|
| 5 |
+
"@/*": ["*"]
|
| 6 |
+
}
|
| 7 |
+
}
|
| 8 |
+
}
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|