CodebaseAi commited on
Commit
0f8fe33
·
0 Parent(s):

Major Project

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +18 -0
  2. backend/app.py +106 -0
  3. backend/capture/__init__.py +0 -0
  4. backend/capture/live_capture.py +620 -0
  5. backend/capture/live_manager.py +57 -0
  6. backend/extensions.py +4 -0
  7. backend/flow_builder.py +35 -0
  8. backend/generated_reports/traffic_logs.csv +8 -0
  9. backend/list_groq_models.py +13 -0
  10. backend/logs/bcc_logs.csv +0 -0
  11. backend/logs/cicids_logs.csv +0 -0
  12. backend/reporting/pdf_report.py +58 -0
  13. backend/requirements.txt +34 -0
  14. backend/retrain_requests.jsonl +1 -0
  15. backend/routes/__init__.py +0 -0
  16. backend/routes/ai_route.py +40 -0
  17. backend/routes/alerts_route.py +82 -0
  18. backend/routes/chat_route.py +26 -0
  19. backend/routes/geo_route.py +33 -0
  20. backend/routes/ip_lookup_route.py +148 -0
  21. backend/routes/live_route.py +53 -0
  22. backend/routes/logs_route.py +137 -0
  23. backend/routes/manual_predict_route.py +384 -0
  24. backend/routes/ml_route.py +297 -0
  25. backend/routes/ml_switch_route.py +105 -0
  26. backend/routes/offline_detection.py +139 -0
  27. backend/routes/predict_route.py +132 -0
  28. backend/routes/reports_route.py +172 -0
  29. backend/routes/system_info.py +211 -0
  30. backend/routes/traffic_routes.py +49 -0
  31. backend/sample/bcc_sample.csv +2 -0
  32. backend/sample/cicids_sample.csv +2 -0
  33. backend/socket_manager.py +80 -0
  34. backend/uploads/bcc_sample.csv +2 -0
  35. backend/uploads/cicids_sample.csv +2 -0
  36. backend/uploads/cicids_sample_1.csv +2 -0
  37. backend/uploads/iris.csv +151 -0
  38. backend/utils/ai_engine.py +164 -0
  39. backend/utils/geo_lookup.py +133 -0
  40. backend/utils/logger.py +273 -0
  41. backend/utils/model_selector.py +116 -0
  42. backend/utils/pcap_to_csv.py +23 -0
  43. backend/utils/risk_engine.py +77 -0
  44. frontend/.gitignore +24 -0
  45. frontend/README.md +16 -0
  46. frontend/components.json +22 -0
  47. frontend/eslint.config.js +29 -0
  48. frontend/index.html +17 -0
  49. frontend/jsconfig.json +8 -0
  50. 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