source: server.py@ 505f39a

Last change on this file since 505f39a was 505f39a, checked in by istevanoska <ilinastevanoska@…>, 6 months ago

Added OAuth/prototype

  • Property mode set to 100644
File size: 33.7 KB
Line 
1#!/usr/bin/env python3
2"""
3netIntel Flask Server (multi-tenant + Google login + JWT cookie session + env tokens)
4
5Auth:
6- POST /api/auth/google -> sets HttpOnly cookie "session"
7- GET /api/me -> returns current user claims
8- POST /api/auth/logout -> clears cookie
9
10Tenant admin (JWT cookie; admin role):
11- GET/POST /api/admin/environments
12- POST /api/admin/tokens
13
14Agent (X-Env-Token):
15- POST /receive
16
17Tenant-scoped dashboard (JWT cookie):
18- GET /api/stats
19- GET /api/computers
20- GET /api/computer/<name>
21- GET /api/export/<name>
22- POST /api/chat
23"""
24
25import os
26import json
27import time
28import sqlite3
29import traceback
30import threading
31import secrets
32from datetime import datetime, timedelta
33from functools import wraps
34
35from flask import Flask, request, jsonify, Response
36from dotenv import load_dotenv
37
38import jwt
39from google.oauth2 import id_token
40from google.auth.transport import requests as grequests
41
42# Optional OpenAI
43try:
44 from openai import OpenAI
45except Exception:
46 OpenAI = None
47
48from flask_cors import CORS
49
50load_dotenv()
51
52# ----------------------------
53# Config
54# ----------------------------
55DB_FILE = os.environ.get("DB_FILE", "lan_logs_sysmon.db")
56LOG_DIR = os.environ.get("LOG_DIR", "received_data_sysmon")
57
58OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
59GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
60JWT_SECRET = os.environ.get("JWT_SECRET", "dev-change-me")
61JWT_ISSUER = os.environ.get("JWT_ISSUER", "netintel")
62COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "0") == "1" # set 1 only on HTTPS
63
64# CORS origins (add your LAN origins if needed)
65EXTRA_ORIGINS = [o.strip() for o in os.environ.get("CORS_ORIGINS", "").split(",") if o.strip()]
66DEFAULT_ORIGINS = [
67 "http://localhost:5173",
68 "http://127.0.0.1:5173",
69]
70ALLOWED_ORIGINS = list(dict.fromkeys(DEFAULT_ORIGINS + EXTRA_ORIGINS))
71
72client = OpenAI(api_key=OPENAI_API_KEY, timeout=15) if (OPENAI_API_KEY and OpenAI) else None
73
74# ----------------------------
75# App
76# ----------------------------
77app = Flask(__name__)
78os.makedirs(LOG_DIR, exist_ok=True)
79
80CORS(
81 app,
82 supports_credentials=True,
83 origins=ALLOWED_ORIGINS,
84 allow_headers=[
85 "Content-Type",
86 "X-Admin-Session",
87 "X-Env",
88 "X-Env-Token",
89 ],
90 methods=["GET", "POST", "OPTIONS"],
91)
92
93
94# ----------------------------
95# DB helpers
96# ----------------------------
97def db():
98 conn = sqlite3.connect(DB_FILE)
99 conn.row_factory = sqlite3.Row
100 return conn
101
102
103def init_db():
104 """Create tables if missing + light migrations."""
105 conn = db()
106 c = conn.cursor()
107
108 c.execute("PRAGMA foreign_keys = ON;")
109
110 # Tenants / users / memberships
111 c.execute("""
112 CREATE TABLE IF NOT EXISTS tenants(
113 id INTEGER PRIMARY KEY AUTOINCREMENT,
114 name TEXT NOT NULL,
115 owner_email TEXT NOT NULL,
116 created_at TEXT
117 )
118 """)
119
120 c.execute("""
121 CREATE TABLE IF NOT EXISTS users(
122 id INTEGER PRIMARY KEY AUTOINCREMENT,
123 email TEXT UNIQUE NOT NULL,
124 name TEXT,
125 picture TEXT,
126 created_at TEXT
127 )
128 """)
129
130 c.execute("""
131 CREATE TABLE IF NOT EXISTS memberships(
132 user_id INTEGER NOT NULL,
133 tenant_id INTEGER NOT NULL,
134 role TEXT DEFAULT 'admin',
135 created_at TEXT,
136 PRIMARY KEY(user_id, tenant_id),
137 FOREIGN KEY(user_id) REFERENCES users(id),
138 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
139 )
140 """)
141
142 # Environments (unique per tenant)
143 c.execute("""
144 CREATE TABLE IF NOT EXISTS environments(
145 id INTEGER PRIMARY KEY AUTOINCREMENT,
146 tenant_id INTEGER NOT NULL,
147 name TEXT NOT NULL,
148 created_at TEXT,
149 UNIQUE(tenant_id, name),
150 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
151 )
152 """)
153
154 # Env tokens (agent tokens)
155 c.execute("""
156 CREATE TABLE IF NOT EXISTS env_tokens(
157 id INTEGER PRIMARY KEY AUTOINCREMENT,
158 tenant_id INTEGER NOT NULL,
159 env_name TEXT NOT NULL,
160 token TEXT UNIQUE NOT NULL,
161 created_at TEXT,
162 expires_at TEXT,
163 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
164 )
165 """)
166
167 # Computers
168 c.execute("""
169 CREATE TABLE IF NOT EXISTS computers(
170 id INTEGER PRIMARY KEY AUTOINCREMENT,
171 tenant_id INTEGER NOT NULL,
172 env_name TEXT DEFAULT 'default',
173 name TEXT NOT NULL,
174 user TEXT,
175 ip TEXT,
176 os TEXT,
177 first_seen TEXT,
178 last_seen TEXT,
179 sysmon_available INTEGER DEFAULT 0,
180 UNIQUE(tenant_id, name)
181 )
182 """)
183
184 # History
185 c.execute("""
186 CREATE TABLE IF NOT EXISTS computer_history(
187 id INTEGER PRIMARY KEY AUTOINCREMENT,
188 computer_id INTEGER,
189 cpu_usage REAL,
190 ram_usage REAL,
191 disk_usage REAL,
192 network_sent_mb REAL,
193 network_recv_mb REAL,
194 timestamp TEXT,
195 FOREIGN KEY(computer_id) REFERENCES computers(id)
196 )
197 """)
198
199 # Processes
200 c.execute("""
201 CREATE TABLE IF NOT EXISTS computer_processes(
202 id INTEGER PRIMARY KEY AUTOINCREMENT,
203 computer_id INTEGER,
204 pid INTEGER,
205 name TEXT,
206 cpu_percent REAL,
207 memory_mb REAL,
208 username TEXT,
209 cmdline TEXT,
210 timestamp TEXT,
211 FOREIGN KEY(computer_id) REFERENCES computers(id)
212 )
213 """)
214
215 # Sysmon events
216 c.execute("""
217 CREATE TABLE IF NOT EXISTS sysmon_events(
218 id INTEGER PRIMARY KEY AUTOINCREMENT,
219 computer_id INTEGER,
220 event_id INTEGER,
221 event_type TEXT,
222 message TEXT,
223 timestamp TEXT,
224 details TEXT,
225 FOREIGN KEY(computer_id) REFERENCES computers(id)
226 )
227 """)
228
229 # Network connections
230 c.execute("""
231 CREATE TABLE IF NOT EXISTS network_connections(
232 id INTEGER PRIMARY KEY AUTOINCREMENT,
233 computer_id INTEGER,
234 pid INTEGER,
235 local_address TEXT,
236 remote_address TEXT,
237 status TEXT,
238 process_name TEXT,
239 timestamp TEXT,
240 FOREIGN KEY(computer_id) REFERENCES computers(id)
241 )
242 """)
243
244 # Security alerts
245 c.execute("""
246 CREATE TABLE IF NOT EXISTS security_alerts(
247 id INTEGER PRIMARY KEY AUTOINCREMENT,
248 computer_id INTEGER,
249 alert_type TEXT,
250 severity TEXT,
251 description TEXT,
252 timestamp TEXT,
253 resolved INTEGER DEFAULT 0,
254 FOREIGN KEY(computer_id) REFERENCES computers(id)
255 )
256 """)
257
258 conn.commit()
259 conn.close()
260 print(f"✅ DB ready: {DB_FILE}")
261
262
263# ----------------------------
264# JWT helpers
265# ----------------------------
266def make_jwt(payload: dict, minutes=60 * 24):
267 exp = datetime.utcnow() + timedelta(minutes=minutes)
268 data = {**payload, "iss": JWT_ISSUER, "exp": exp}
269 return jwt.encode(data, JWT_SECRET, algorithm="HS256")
270
271
272def read_jwt(token: str):
273 return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
274
275
276def require_user():
277 def deco(fn):
278 @wraps(fn)
279 def wrap(*args, **kwargs):
280 tok = request.cookies.get("session") or ""
281 if not tok:
282 return jsonify({"error": "Unauthorized"}), 401
283 try:
284 claims = read_jwt(tok)
285 except Exception:
286 return jsonify({"error": "Unauthorized"}), 401
287 request.user = claims
288 return fn(*args, **kwargs)
289
290 return wrap
291
292 return deco
293
294
295def require_tenant_admin():
296 def deco(fn):
297 @wraps(fn)
298 def wrap(*args, **kwargs):
299 tok = request.cookies.get("session") or ""
300 if not tok:
301 return jsonify({"error": "Unauthorized"}), 401
302 try:
303 claims = read_jwt(tok)
304 except Exception:
305 return jsonify({"error": "Unauthorized"}), 401
306 if claims.get("role") != "admin":
307 return jsonify({"error": "Forbidden"}), 403
308 request.user = claims
309 return fn(*args, **kwargs)
310
311 return wrap
312
313 return deco
314
315
316# ----------------------------
317# Env-token helpers (agents)
318# ----------------------------
319def get_env_from_token(req):
320 tok = req.headers.get("X-Env-Token", "")
321 if not tok:
322 return (None, None)
323
324 conn = db()
325 c = conn.cursor()
326 c.execute("""
327 SELECT env_name, tenant_id
328 FROM env_tokens
329 WHERE token = ?
330 AND (expires_at IS NULL OR expires_at > datetime('now'))
331 LIMIT 1
332 """, (tok,))
333 row = c.fetchone()
334 conn.close()
335
336 if not row:
337 return (None, None)
338 return (row["env_name"], row["tenant_id"])
339
340
341# ----------------------------
342# Utility
343# ----------------------------
344def save_json_file(data):
345 info = data.get("info") or {}
346 computer_name = info.get("computer_name", "unknown")
347 date_str = datetime.now().strftime("%Y%m%d_%H")
348
349 computer_dir = os.path.join(LOG_DIR, computer_name)
350 os.makedirs(computer_dir, exist_ok=True)
351 filepath = os.path.join(computer_dir, f"{date_str}.json")
352
353 if os.path.exists(filepath):
354 try:
355 with open(filepath, "r", encoding="utf-8") as f:
356 existing_data = json.load(f)
357 except Exception:
358 existing_data = []
359 else:
360 existing_data = []
361
362 existing_data.append({
363 "timestamp": info.get("timestamp"),
364 "info": info,
365 "processes_count": len(data.get("processes", [])),
366 "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
367 })
368
369 with open(filepath, "w", encoding="utf-8") as f:
370 json.dump(existing_data, f, indent=2, ensure_ascii=False)
371
372
373def cleanup_old_data():
374 """Deletes old rows (30 days) every hour."""
375 while True:
376 time.sleep(3600)
377 try:
378 conn = db()
379 c = conn.cursor()
380 cutoff = (datetime.now() - timedelta(days=30)).isoformat()
381
382 c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff,))
383 c.execute("DELETE FROM computer_processes WHERE timestamp < ?", (cutoff,))
384 c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff,))
385 c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff,))
386
387 conn.commit()
388 conn.close()
389 print(f"[Cleanup] Deleted entries older than {cutoff}")
390 except Exception as e:
391 print("[Cleanup] Error:", e)
392
393
394# ----------------------------
395# Auth endpoints
396# ----------------------------
397@app.route("/api/auth/google", methods=["POST"])
398def auth_google():
399 data = request.get_json(force=True, silent=True) or {}
400 credential = (data.get("credential") or "").strip()
401 if not credential:
402 return jsonify({"error": "Missing credential"}), 400
403 if not GOOGLE_CLIENT_ID:
404 return jsonify({"error": "Server missing GOOGLE_CLIENT_ID"}), 500
405
406 try:
407 idinfo = id_token.verify_oauth2_token(
408 credential,
409 grequests.Request(),
410 GOOGLE_CLIENT_ID,
411 )
412 email = idinfo.get("email")
413 name = idinfo.get("name") or ""
414 picture = idinfo.get("picture") or ""
415
416 if not email:
417 return jsonify({"error": "No email in token"}), 400
418
419 conn = db()
420 c = conn.cursor()
421
422 # upsert user
423 c.execute("SELECT id FROM users WHERE email=?", (email,))
424 row = c.fetchone()
425 if row:
426 user_id = row["id"]
427 c.execute("UPDATE users SET name=?, picture=? WHERE id=?", (name, picture, user_id))
428 else:
429 c.execute(
430 "INSERT INTO users(email, name, picture, created_at) VALUES(?,?,?, datetime('now'))",
431 (email, name, picture),
432 )
433 user_id = c.lastrowid
434
435 # membership -> tenant (create tenant if first time)
436 c.execute("SELECT tenant_id, role FROM memberships WHERE user_id=? LIMIT 1", (user_id,))
437 m = c.fetchone()
438
439 if not m:
440 tenant_name = email.split("@")[0]
441 c.execute(
442 "INSERT INTO tenants(name, owner_email, created_at) VALUES(?,?, datetime('now'))",
443 (tenant_name, email),
444 )
445 tenant_id = c.lastrowid
446 role = "admin"
447
448 c.execute(
449 "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
450 (user_id, tenant_id),
451 )
452
453 # default env for this tenant
454 c.execute(
455 "INSERT OR IGNORE INTO environments(tenant_id, name, created_at) VALUES(?, 'default', datetime('now'))",
456 (tenant_id,),
457 )
458 else:
459 tenant_id = m["tenant_id"]
460 role = m["role"] or "admin"
461
462 conn.commit()
463 conn.close()
464
465 session = make_jwt(
466 {
467 "uid": user_id,
468 "email": email,
469 "name": name,
470 "role": role,
471 "tenant_id": tenant_id,
472 },
473 minutes=60 * 24,
474 )
475
476 resp = jsonify({"ok": True, "user": {"email": email, "name": name, "role": role, "tenant_id": tenant_id}})
477 resp.set_cookie(
478 "session",
479 session,
480 httponly=True,
481 samesite="Lax",
482 secure=COOKIE_SECURE,
483 max_age=60 * 60 * 24,
484 )
485 return resp
486
487 except Exception as e:
488 return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
489
490
491@app.route("/api/me", methods=["GET"])
492@require_user()
493def api_me():
494 return jsonify({"user": request.user})
495
496
497@app.route("/api/auth/logout", methods=["POST"])
498def auth_logout():
499 resp = jsonify({"ok": True})
500 resp.set_cookie("session", "", expires=0)
501 return resp
502
503
504# ----------------------------
505# Admin endpoints
506# ----------------------------
507@app.route("/api/admin/environments", methods=["GET"])
508@require_tenant_admin()
509def admin_list_envs():
510 tenant_id = request.user["tenant_id"]
511 conn = db()
512 c = conn.cursor()
513 c.execute("SELECT name FROM environments WHERE tenant_id=? ORDER BY name", (tenant_id,))
514 envs = [r["name"] for r in c.fetchall()]
515 conn.close()
516 if not envs:
517 envs = ["default"]
518 return jsonify({"environments": envs})
519
520
521@app.route("/api/admin/environments", methods=["POST"])
522@require_tenant_admin()
523def admin_create_env():
524 tenant_id = request.user["tenant_id"]
525 data = request.get_json(force=True, silent=True) or {}
526 name = (data.get("name") or "").strip()
527 if not name:
528 return jsonify({"error": "Missing env name"}), 400
529
530 conn = db()
531 c = conn.cursor()
532 try:
533 c.execute(
534 "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
535 (tenant_id, name),
536 )
537 conn.commit()
538 except Exception as e:
539 conn.close()
540 return jsonify({"error": str(e)}), 400
541 conn.close()
542 return jsonify({"ok": True, "name": name})
543
544
545@app.route("/api/admin/tokens", methods=["POST"])
546@require_tenant_admin()
547def admin_generate_token():
548 tenant_id = request.user["tenant_id"]
549 data = request.get_json(force=True, silent=True) or {}
550 env = (data.get("env") or "").strip()
551 if not env:
552 return jsonify({"error": "Missing env"}), 400
553
554 conn = db()
555 c = conn.cursor()
556 c.execute("SELECT 1 FROM environments WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, env))
557 if not c.fetchone():
558 conn.close()
559 return jsonify({"error": "Environment not found"}), 404
560
561 token = secrets.token_urlsafe(32)
562 c.execute("""
563 INSERT INTO env_tokens(tenant_id, env_name, token, created_at, expires_at)
564 VALUES(?, ?, ?, datetime('now'), datetime('now', '+90 days'))
565 """, (tenant_id, env, token))
566 conn.commit()
567 conn.close()
568
569 return jsonify({"ok": True, "env": env, "token": token})
570
571
572# ----------------------------
573# Agent endpoint
574# ----------------------------
575@app.route("/receive", methods=["POST"])
576def receive_data():
577 try:
578 data = request.get_json(force=True, silent=True) or {}
579 if not data:
580 return jsonify({"error": "No data"}), 400
581
582 env_name, tenant_id = get_env_from_token(request)
583 if not env_name or not tenant_id:
584 return jsonify({"error": "Missing or invalid X-Env-Token"}), 401
585
586 info = data.get("info") or {}
587 computer_name = info.get("computer_name")
588 if not computer_name:
589 return jsonify({"error": "Missing info.computer_name"}), 400
590
591 now_iso = datetime.now().isoformat()
592 security_data = data.get("security_data") or {}
593
594 conn = db()
595 c = conn.cursor()
596
597 # Find by (tenant_id + name)
598 c.execute("SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, computer_name))
599 row = c.fetchone()
600
601 if row:
602 computer_id = row["id"]
603 c.execute("""
604 UPDATE computers
605 SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
606 WHERE id=? AND tenant_id=?
607 """, (
608 info.get("user"),
609 info.get("ip_address"),
610 info.get("os"),
611 now_iso,
612 int(info.get("is_sysmon_available", 0) or 0),
613 env_name,
614 computer_id,
615 tenant_id,
616 ))
617 else:
618 c.execute("""
619 INSERT INTO computers(tenant_id, env_name, name, user, ip, os, first_seen, last_seen, sysmon_available)
620 VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
621 """, (
622 tenant_id,
623 env_name,
624 computer_name,
625 info.get("user"),
626 info.get("ip_address"),
627 info.get("os"),
628 now_iso,
629 now_iso,
630 int(info.get("is_sysmon_available", 0) or 0),
631 ))
632 computer_id = c.lastrowid
633
634 # history row
635 c.execute("""
636 INSERT INTO computer_history(computer_id, cpu_usage, ram_usage, disk_usage,
637 network_sent_mb, network_recv_mb, timestamp)
638 VALUES(?, ?, ?, ?, ?, ?, ?)
639 """, (
640 computer_id,
641 float(info.get("cpu_usage") or 0),
642 float(info.get("ram_usage") or 0),
643 float(info.get("disk_usage") or 0),
644 float(info.get("network_sent_mb") or 0),
645 float(info.get("network_recv_mb") or 0),
646 info.get("timestamp") or now_iso,
647 ))
648
649 # processes
650 for proc in (data.get("processes") or []):
651 c.execute("""
652 INSERT INTO computer_processes(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp)
653 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
654 """, (
655 computer_id,
656 proc.get("pid"),
657 proc.get("name"),
658 float(proc.get("cpu_percent") or 0),
659 float(proc.get("memory_mb") or 0),
660 proc.get("user") or proc.get("username"),
661 proc.get("cmdline"),
662 info.get("timestamp") or now_iso,
663 ))
664
665 # sysmon events
666 sysmon_count = 0
667 for ev in (security_data.get("sysmon_events") or []):
668 c.execute("""
669 INSERT INTO sysmon_events(computer_id, event_id, event_type, message, timestamp, details)
670 VALUES(?, ?, ?, ?, ?, ?)
671 """, (
672 computer_id,
673 ev.get("event_id"),
674 ev.get("event_type", "Unknown"),
675 ev.get("message", ""),
676 ev.get("timestamp") or (info.get("timestamp") or now_iso),
677 json.dumps(ev, ensure_ascii=False),
678 ))
679 sysmon_count += 1
680
681 # network connections
682 for nc in (security_data.get("network_connections") or []):
683 c.execute("""
684 INSERT INTO network_connections(computer_id, pid, local_address, remote_address, status, process_name, timestamp)
685 VALUES(?, ?, ?, ?, ?, ?, ?)
686 """, (
687 computer_id,
688 nc.get("pid"),
689 nc.get("local_address"),
690 nc.get("remote_address"),
691 nc.get("status"),
692 nc.get("process_name"),
693 info.get("timestamp") or now_iso,
694 ))
695
696 conn.commit()
697 conn.close()
698
699 # optional JSON archive
700 try:
701 save_json_file(data)
702 except Exception:
703 pass
704
705 return jsonify({
706 "ok": True,
707 "computer_id": computer_id,
708 "env": env_name,
709 "tenant_id": tenant_id,
710 "sysmon_events_saved": sysmon_count,
711 "server_time": now_iso,
712 })
713
714 except Exception as e:
715 traceback.print_exc()
716 return jsonify({"error": str(e)}), 500
717
718
719# ----------------------------
720# Dashboard API (tenant scoped)
721# ----------------------------
722@app.route("/api/computers", methods=["GET"])
723@require_user()
724def api_computers():
725 tenant_id = request.user["tenant_id"]
726 env = request.headers.get("X-Env", "default")
727
728 conn = db()
729 c = conn.cursor()
730 c.execute("""
731 SELECT id, name, user, ip, os, first_seen, last_seen, sysmon_available
732 FROM computers
733 WHERE tenant_id=? AND env_name=?
734 ORDER BY last_seen DESC
735 """, (tenant_id, env))
736 rows = c.fetchall()
737
738 computers = []
739 for r in rows:
740 cid = r["id"]
741
742 c.execute("""
743 SELECT COUNT(*) AS n
744 FROM computer_history
745 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
746 """, (cid,))
747 recent_logs = int(c.fetchone()["n"] or 0)
748
749 c.execute("""
750 SELECT COUNT(*) AS n
751 FROM sysmon_events
752 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
753 """, (cid,))
754 recent_sysmon = int(c.fetchone()["n"] or 0)
755
756 c.execute("""
757 SELECT cpu_usage, ram_usage, timestamp
758 FROM computer_history
759 WHERE computer_id=?
760 ORDER BY timestamp DESC
761 LIMIT 5
762 """, (cid,))
763 metrics = c.fetchall()
764 avg_cpu = round(sum(m["cpu_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
765 avg_ram = round(sum(m["ram_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
766
767 status = "offline"
768 status_color = "gray"
769 try:
770 last_seen = r["last_seen"]
771 if last_seen:
772 dt = datetime.fromisoformat(
773 last_seen.replace("Z", "+00:00")) if "T" in last_seen else datetime.fromisoformat(last_seen)
774 diff = (datetime.now() - dt).total_seconds()
775 if diff < 60:
776 status, status_color = "online", "green"
777 elif diff < 300:
778 status, status_color = "idle", "orange"
779 except Exception:
780 pass
781
782 computers.append({
783 "name": r["name"],
784 "user": r["user"],
785 "ip": r["ip"],
786 "os": r["os"],
787 "first_seen": r["first_seen"],
788 "last_seen": r["last_seen"],
789 "sysmon_available": bool(r["sysmon_available"]),
790 "recent_logs": recent_logs,
791 "recent_sysmon": recent_sysmon,
792 "avg_cpu": avg_cpu,
793 "avg_ram": avg_ram,
794 "status": status,
795 "status_color": status_color,
796 })
797
798 conn.close()
799 return jsonify(computers)
800
801
802@app.route("/api/stats", methods=["GET"])
803@require_user()
804def api_stats():
805 tenant_id = request.user["tenant_id"]
806
807 conn = db()
808 c = conn.cursor()
809
810 c.execute("""
811 SELECT COUNT(*) AS n
812 FROM sysmon_events s
813 JOIN computers c2 ON c2.id = s.computer_id
814 WHERE s.timestamp > datetime('now','-24 hours')
815 AND c2.tenant_id = ?
816 """, (tenant_id,))
817 total_sysmon = int(c.fetchone()["n"] or 0)
818
819 c.execute("""
820 SELECT COUNT(*) AS n
821 FROM network_connections n
822 JOIN computers c2 ON c2.id = n.computer_id
823 WHERE n.timestamp > datetime('now','-24 hours')
824 AND c2.tenant_id = ?
825 """, (tenant_id,))
826 total_net = int(c.fetchone()["n"] or 0)
827
828 conn.close()
829 return jsonify({
830 "total_sysmon_events": total_sysmon,
831 "total_network_connections": total_net,
832 "server_time": datetime.now().isoformat(),
833 })
834
835
836@app.route("/api/computer/<computer_name>", methods=["GET"])
837@require_user()
838def api_computer_details(computer_name):
839 tenant_id = request.user["tenant_id"]
840
841 conn = db()
842 c = conn.cursor()
843 c.execute("""
844 SELECT id, name, user, ip, os, first_seen, last_seen, env_name
845 FROM computers
846 WHERE tenant_id=? AND name=?
847 LIMIT 1
848 """, (tenant_id, computer_name))
849 comp = c.fetchone()
850 if not comp:
851 conn.close()
852 return jsonify({"error": "Computer not found"}), 404
853
854 computer_id = comp["id"]
855
856 c.execute("SELECT COUNT(*) AS n FROM computer_history WHERE computer_id=?", (computer_id,))
857 total_logs = int(c.fetchone()["n"] or 0)
858
859 c.execute("""
860 SELECT cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp
861 FROM computer_history
862 WHERE computer_id=?
863 ORDER BY timestamp DESC
864 LIMIT 100
865 """, (computer_id,))
866 history = [dict(r) for r in c.fetchall()]
867 current_metrics = history[0] if history else {}
868
869 c.execute("""
870 SELECT pid, name, username, cpu_percent, memory_mb, cmdline, timestamp
871 FROM computer_processes
872 WHERE computer_id=?
873 ORDER BY timestamp DESC
874 LIMIT 100
875 """, (computer_id,))
876 processes = [dict(r) for r in c.fetchall()]
877
878 c.execute("""
879 SELECT event_id, event_type, message, timestamp, details
880 FROM sysmon_events
881 WHERE computer_id=?
882 ORDER BY timestamp DESC
883 LIMIT 200
884 """, (computer_id,))
885 sysmon = []
886 for r in c.fetchall():
887 d = {}
888 try:
889 d = json.loads(r["details"]) if r["details"] else {}
890 except Exception:
891 d = {}
892 sysmon.append({
893 "event_id": r["event_id"],
894 "event_type": r["event_type"],
895 "message": r["message"],
896 "timestamp": r["timestamp"],
897 "details": d,
898 })
899
900 c.execute("""
901 SELECT pid, process_name, local_address, remote_address, status, timestamp
902 FROM network_connections
903 WHERE computer_id=?
904 ORDER BY timestamp DESC
905 LIMIT 100
906 """, (computer_id,))
907 net = [dict(r) for r in c.fetchall()]
908
909 conn.close()
910
911 return jsonify({
912 "computer": {
913 "id": comp["id"],
914 "name": comp["name"],
915 "user": comp["user"],
916 "ip": comp["ip"],
917 "os": comp["os"],
918 "first_seen": comp["first_seen"],
919 "last_seen": comp["last_seen"],
920 "env_name": comp["env_name"],
921 "total_logs": total_logs,
922 },
923 "history": history,
924 "current_metrics": current_metrics,
925 "recent_processes": processes,
926 "sysmon_events": sysmon,
927 "network_connections": net,
928 })
929
930
931@app.route("/api/export/<computer_name>", methods=["GET"])
932@require_user()
933def api_export(computer_name):
934 tenant_id = request.user["tenant_id"]
935
936 conn = db()
937 c = conn.cursor()
938 c.execute("""
939 SELECT *
940 FROM computers
941 WHERE tenant_id=? AND name=?
942 LIMIT 1
943 """, (tenant_id, computer_name))
944 comp = c.fetchone()
945 if not comp:
946 conn.close()
947 return jsonify({"error": "Computer not found"}), 404
948
949 computer_id = comp["id"]
950 out = {"computer": dict(comp)}
951
952 c.execute("SELECT * FROM computer_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
953 out["history"] = [dict(r) for r in c.fetchall()]
954
955 c.execute("SELECT * FROM sysmon_events WHERE computer_id=? ORDER BY timestamp", (computer_id,))
956 out["sysmon_events"] = [dict(r) for r in c.fetchall()]
957
958 c.execute("SELECT * FROM computer_processes WHERE computer_id=? ORDER BY timestamp", (computer_id,))
959 out["processes"] = [dict(r) for r in c.fetchall()]
960
961 c.execute("SELECT * FROM network_connections WHERE computer_id=? ORDER BY timestamp", (computer_id,))
962 out["network_connections"] = [dict(r) for r in c.fetchall()]
963
964 conn.close()
965
966 return Response(
967 json.dumps(out, indent=2, default=str, ensure_ascii=False),
968 mimetype="application/json",
969 headers={"Content-Disposition": f"attachment; filename={computer_name}_export.json"},
970 )
971
972
973# ----------------------------
974# Simple RAG chat
975# ----------------------------
976def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10):
977 conn = db()
978 c = conn.cursor()
979
980 params = [tenant_id]
981 comp_clause = ""
982 if computer_name:
983 comp_clause = "AND c2.name = ?"
984 params.append(computer_name)
985
986 # sysmon
987 c.execute(f"""
988 SELECT s.timestamp, s.event_id, s.event_type, s.message
989 FROM sysmon_events s
990 JOIN computers c2 ON c2.id = s.computer_id
991 WHERE c2.tenant_id = ? {comp_clause}
992 ORDER BY s.timestamp DESC
993 LIMIT ?
994 """, (*params, limit_per_table))
995 sysmon_rows = c.fetchall()
996
997 # perf
998 c.execute(f"""
999 SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb
1000 FROM computer_history h
1001 JOIN computers c2 ON c2.id = h.computer_id
1002 WHERE c2.tenant_id = ? {comp_clause}
1003 ORDER BY h.timestamp DESC
1004 LIMIT ?
1005 """, (*params, limit_per_table))
1006 perf_rows = c.fetchall()
1007
1008 # proc
1009 c.execute(f"""
1010 SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb
1011 FROM computer_processes p
1012 JOIN computers c2 ON c2.id = p.computer_id
1013 WHERE c2.tenant_id = ? {comp_clause}
1014 ORDER BY p.timestamp DESC
1015 LIMIT ?
1016 """, (*params, limit_per_table))
1017 proc_rows = c.fetchall()
1018
1019 conn.close()
1020
1021 lines = []
1022 for r in sysmon_rows:
1023 lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}")
1024 for r in perf_rows:
1025 lines.append(
1026 f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | "
1027 f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB"
1028 )
1029 for r in proc_rows:
1030 lines.append(
1031 f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB"
1032 )
1033
1034 return "\n".join(lines)
1035
1036
1037def ask_llm_with_context(question, context):
1038 if not client:
1039 return "Немаш поставено OPENAI_API_KEY на серверот."
1040
1041 system_msg = (
1042 "Ти си асистент за безбедност и систем мониторинг. "
1043 "Одговараш базирано ИСКЛУЧИВО на дадените логови. "
1044 "Ако нема доволно податоци, кажи дека нема доволно информации. "
1045 "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите."
1046 )
1047
1048 completion = client.chat.completions.create(
1049 model="gpt-4.1-mini",
1050 messages=[
1051 {"role": "system", "content": system_msg},
1052 {"role": "user", "content": f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"},
1053 ],
1054 temperature=0.2,
1055 )
1056 return completion.choices[0].message.content
1057
1058
1059@app.route("/api/chat", methods=["POST"])
1060@require_user()
1061def api_chat():
1062 try:
1063 tenant_id = request.user["tenant_id"]
1064 data = request.get_json(force=True, silent=True) or {}
1065 question = (data.get("question") or "").strip()
1066 computer_name = data.get("computer_name") or None
1067
1068 if not question:
1069 return jsonify({"error": "No question provided"}), 400
1070
1071 ctx = build_rag_context(tenant_id, question, computer_name=computer_name, limit_per_table=10)
1072 ans = ask_llm_with_context(question, ctx)
1073 return jsonify({"answer": ans or ""}), 200
1074
1075 except Exception as e:
1076 traceback.print_exc()
1077 return jsonify({"error": str(e)}), 500
1078
1079
1080# ----------------------------
1081# Misc
1082# ----------------------------
1083@app.route("/ping")
1084def ping():
1085 return jsonify({
1086 "status": "alive",
1087 "server_time": datetime.now().isoformat(),
1088 "database": DB_FILE,
1089 "cors_origins": ALLOWED_ORIGINS,
1090 })
1091
1092
1093@app.route("/")
1094def root():
1095 return jsonify({
1096 "ok": True,
1097 "service": "netIntel server",
1098 "hint": "frontend should call /api/* endpoints",
1099 })
1100
1101
1102if __name__ == "__main__":
1103 init_db()
1104
1105 cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
1106 cleanup_thread.start()
1107
1108 print("🚀 netIntel server running on 0.0.0.0:5555")
1109 # debug=False recommended; if you need debug, set True temporarily
1110 app.run(host="0.0.0.0", port=5555, debug=False, threaded=True)
Note: See TracBrowser for help on using the repository browser.