source: server.py@ d42aac3

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

Added history mmanage

  • Property mode set to 100644
File size: 38.4 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 # Environment settings (switch-ови по env)
259 c.execute("""
260 CREATE TABLE IF NOT EXISTS env_settings(
261 tenant_id INTEGER NOT NULL,
262 env_name TEXT NOT NULL,
263 save_process_history INTEGER DEFAULT 0,
264 created_at TEXT,
265 updated_at TEXT,
266 PRIMARY KEY(tenant_id, env_name),
267 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
268 )
269 """)
270
271 # Latest processes (only current snapshot)
272 c.execute("""
273 CREATE TABLE IF NOT EXISTS computer_processes_current(
274 id INTEGER PRIMARY KEY AUTOINCREMENT,
275 computer_id INTEGER NOT NULL,
276 pid INTEGER,
277 name TEXT,
278 cpu_percent REAL,
279 memory_mb REAL,
280 username TEXT,
281 cmdline TEXT,
282 timestamp TEXT,
283 FOREIGN KEY(computer_id) REFERENCES computers(id)
284 )
285 """)
286
287 # History processes (optional, controlled by switch)
288 c.execute("""
289 CREATE TABLE IF NOT EXISTS computer_processes_history(
290 id INTEGER PRIMARY KEY AUTOINCREMENT,
291 computer_id INTEGER NOT NULL,
292 pid INTEGER,
293 name TEXT,
294 cpu_percent REAL,
295 memory_mb REAL,
296 username TEXT,
297 cmdline TEXT,
298 timestamp TEXT,
299 FOREIGN KEY(computer_id) REFERENCES computers(id)
300 )
301 """)
302
303
304 conn.commit()
305 conn.close()
306 print(f"✅ DB ready: {DB_FILE}")
307
308
309# ----------------------------
310# JWT helpers
311# ----------------------------
312def make_jwt(payload: dict, minutes=60 * 24):
313 exp = datetime.utcnow() + timedelta(minutes=minutes)
314 data = {**payload, "iss": JWT_ISSUER, "exp": exp}
315 return jwt.encode(data, JWT_SECRET, algorithm="HS256")
316
317
318def read_jwt(token: str):
319 return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
320
321
322def require_user():
323 def deco(fn):
324 @wraps(fn)
325 def wrap(*args, **kwargs):
326 tok = request.cookies.get("session") or ""
327 if not tok:
328 return jsonify({"error": "Unauthorized"}), 401
329 try:
330 claims = read_jwt(tok)
331 except Exception:
332 return jsonify({"error": "Unauthorized"}), 401
333 request.user = claims
334 return fn(*args, **kwargs)
335
336 return wrap
337
338 return deco
339def is_process_history_enabled(tenant_id: int, env_name: str) -> bool:
340 conn = db()
341 c = conn.cursor()
342 c.execute("""
343 SELECT save_process_history
344 FROM env_settings
345 WHERE tenant_id=? AND env_name=?
346 LIMIT 1
347 """, (tenant_id, env_name))
348 row = c.fetchone()
349 conn.close()
350
351 # default: OFF ако нема запис
352 return bool(row["save_process_history"]) if row else False
353
354
355def require_tenant_admin():
356 def deco(fn):
357 @wraps(fn)
358 def wrap(*args, **kwargs):
359 tok = request.cookies.get("session") or ""
360 if not tok:
361 return jsonify({"error": "Unauthorized"}), 401
362 try:
363 claims = read_jwt(tok)
364 except Exception:
365 return jsonify({"error": "Unauthorized"}), 401
366 if claims.get("role") != "admin":
367 return jsonify({"error": "Forbidden"}), 403
368 request.user = claims
369 return fn(*args, **kwargs)
370
371 return wrap
372
373 return deco
374
375
376# ----------------------------
377# Env-token helpers (agents)
378# ----------------------------
379def get_env_from_token(req):
380 tok = req.headers.get("X-Env-Token", "")
381 if not tok:
382 return (None, None)
383
384 conn = db()
385 c = conn.cursor()
386 c.execute("""
387 SELECT env_name, tenant_id
388 FROM env_tokens
389 WHERE token = ?
390 AND (expires_at IS NULL OR expires_at > datetime('now'))
391 LIMIT 1
392 """, (tok,))
393 row = c.fetchone()
394 conn.close()
395
396 if not row:
397 return (None, None)
398 return (row["env_name"], row["tenant_id"])
399
400
401# ----------------------------
402# Utility
403# ----------------------------
404def save_json_file(data):
405 info = data.get("info") or {}
406 computer_name = info.get("computer_name", "unknown")
407 date_str = datetime.now().strftime("%Y%m%d_%H")
408
409 computer_dir = os.path.join(LOG_DIR, computer_name)
410 os.makedirs(computer_dir, exist_ok=True)
411 filepath = os.path.join(computer_dir, f"{date_str}.json")
412
413 if os.path.exists(filepath):
414 try:
415 with open(filepath, "r", encoding="utf-8") as f:
416 existing_data = json.load(f)
417 except Exception:
418 existing_data = []
419 else:
420 existing_data = []
421
422 existing_data.append({
423 "timestamp": info.get("timestamp"),
424 "info": info,
425 "processes_count": len(data.get("processes", [])),
426 "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
427 })
428
429 with open(filepath, "w", encoding="utf-8") as f:
430 json.dump(existing_data, f, indent=2, ensure_ascii=False)
431
432
433def cleanup_old_data():
434 """Deletes old rows (30 days) every hour."""
435 while True:
436 time.sleep(3600)
437 try:
438 conn = db()
439 c = conn.cursor()
440 cutoff = (datetime.now() - timedelta(days=30)).isoformat()
441
442 c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff,))
443 c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff,))
444 c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff,))
445 c.execute("DELETE FROM computer_processes_history WHERE timestamp < ?", (cutoff,))
446
447 conn.commit()
448 conn.close()
449 print(f"[Cleanup] Deleted entries older than {cutoff}")
450 except Exception as e:
451 print("[Cleanup] Error:", e)
452
453
454# ----------------------------
455# Auth endpoints
456# ----------------------------
457@app.route("/api/auth/google", methods=["POST"])
458def auth_google():
459 data = request.get_json(force=True, silent=True) or {}
460 credential = (data.get("credential") or "").strip()
461 if not credential:
462 return jsonify({"error": "Missing credential"}), 400
463 if not GOOGLE_CLIENT_ID:
464 return jsonify({"error": "Server missing GOOGLE_CLIENT_ID"}), 500
465
466 try:
467 idinfo = id_token.verify_oauth2_token(
468 credential,
469 grequests.Request(),
470 GOOGLE_CLIENT_ID,
471 )
472 email = idinfo.get("email")
473 name = idinfo.get("name") or ""
474 picture = idinfo.get("picture") or ""
475
476 if not email:
477 return jsonify({"error": "No email in token"}), 400
478
479 conn = db()
480 c = conn.cursor()
481
482 # upsert user
483 c.execute("SELECT id FROM users WHERE email=?", (email,))
484 row = c.fetchone()
485 if row:
486 user_id = row["id"]
487 c.execute("UPDATE users SET name=?, picture=? WHERE id=?", (name, picture, user_id))
488 else:
489 c.execute(
490 "INSERT INTO users(email, name, picture, created_at) VALUES(?,?,?, datetime('now'))",
491 (email, name, picture),
492 )
493 user_id = c.lastrowid
494
495 # membership -> tenant (create tenant if first time)
496 c.execute("SELECT tenant_id, role FROM memberships WHERE user_id=? LIMIT 1", (user_id,))
497 m = c.fetchone()
498
499 if not m:
500 tenant_name = email.split("@")[0]
501 c.execute(
502 "INSERT INTO tenants(name, owner_email, created_at) VALUES(?,?, datetime('now'))",
503 (tenant_name, email),
504 )
505 tenant_id = c.lastrowid
506 role = "admin"
507
508 c.execute(
509 "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
510 (user_id, tenant_id),
511 )
512
513 # default env for this tenant
514 c.execute(
515 "INSERT OR IGNORE INTO environments(tenant_id, name, created_at) VALUES(?, 'default', datetime('now'))",
516 (tenant_id,),
517 )
518 else:
519 tenant_id = m["tenant_id"]
520 role = m["role"] or "admin"
521
522 conn.commit()
523 conn.close()
524
525 session = make_jwt(
526 {
527 "uid": user_id,
528 "email": email,
529 "name": name,
530 "role": role,
531 "tenant_id": tenant_id,
532 },
533 minutes=60 * 24,
534 )
535
536 resp = jsonify({"ok": True, "user": {"email": email, "name": name, "role": role, "tenant_id": tenant_id}})
537 resp.set_cookie(
538 "session",
539 session,
540 httponly=True,
541 samesite="Lax",
542 secure=COOKIE_SECURE,
543 max_age=60 * 60 * 24,
544 )
545 return resp
546
547 except Exception as e:
548 return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
549
550
551@app.route("/api/me", methods=["GET"])
552@require_user()
553def api_me():
554 return jsonify({"user": request.user})
555
556
557@app.route("/api/auth/logout", methods=["POST"])
558def auth_logout():
559 resp = jsonify({"ok": True})
560 resp.set_cookie("session", "", expires=0)
561 return resp
562
563
564# ----------------------------
565# Admin endpoints
566# ----------------------------
567@app.route("/api/admin/environments", methods=["GET"])
568@require_tenant_admin()
569def admin_list_envs():
570 tenant_id = request.user["tenant_id"]
571 conn = db()
572 c = conn.cursor()
573 c.execute("SELECT name FROM environments WHERE tenant_id=? ORDER BY name", (tenant_id,))
574 envs = [r["name"] for r in c.fetchall()]
575 conn.close()
576 if not envs:
577 envs = ["default"]
578 return jsonify({"environments": envs})
579
580
581@app.route("/api/admin/environments", methods=["POST"])
582@require_tenant_admin()
583def admin_create_env():
584 tenant_id = request.user["tenant_id"]
585 data = request.get_json(force=True, silent=True) or {}
586 name = (data.get("name") or "").strip()
587 if not name:
588 return jsonify({"error": "Missing env name"}), 400
589
590 conn = db()
591 c = conn.cursor()
592 try:
593 c.execute(
594 "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
595 (tenant_id, name),
596 )
597 conn.commit()
598 except Exception as e:
599 conn.close()
600 return jsonify({"error": str(e)}), 400
601 conn.close()
602 return jsonify({"ok": True, "name": name})
603
604
605@app.route("/api/admin/tokens", methods=["POST"])
606@require_tenant_admin()
607def admin_generate_token():
608 tenant_id = request.user["tenant_id"]
609 data = request.get_json(force=True, silent=True) or {}
610 env = (data.get("env") or "").strip()
611 if not env:
612 return jsonify({"error": "Missing env"}), 400
613
614 conn = db()
615 c = conn.cursor()
616 c.execute("SELECT 1 FROM environments WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, env))
617 if not c.fetchone():
618 conn.close()
619 return jsonify({"error": "Environment not found"}), 404
620
621 token = secrets.token_urlsafe(32)
622 c.execute("""
623 INSERT INTO env_tokens(tenant_id, env_name, token, created_at, expires_at)
624 VALUES(?, ?, ?, datetime('now'), datetime('now', '+90 days'))
625 """, (tenant_id, env, token))
626 conn.commit()
627 conn.close()
628
629 return jsonify({"ok": True, "env": env, "token": token})
630
631@app.route("/api/admin/env-settings/<env_name>", methods=["GET"])
632@require_tenant_admin()
633def admin_get_env_settings(env_name):
634 tenant_id = request.user["tenant_id"]
635
636 conn = db()
637 c = conn.cursor()
638 c.execute("""
639 SELECT save_process_history
640 FROM env_settings
641 WHERE tenant_id=? AND env_name=?
642 LIMIT 1
643 """, (tenant_id, env_name))
644 row = c.fetchone()
645 conn.close()
646
647 return jsonify({
648 "env": env_name,
649 "save_process_history": bool(row["save_process_history"]) if row else False
650 })
651
652@app.route("/api/admin/env-settings/<env_name>", methods=["POST"])
653@require_tenant_admin()
654def admin_set_env_settings(env_name):
655 tenant_id = request.user["tenant_id"]
656 data = request.get_json(force=True, silent=True) or {}
657 enabled = 1 if bool(data.get("save_process_history")) else 0
658
659 conn = db()
660 c = conn.cursor()
661 c.execute("""
662 INSERT INTO env_settings(tenant_id, env_name, save_process_history, created_at, updated_at)
663 VALUES(?, ?, ?, datetime('now'), datetime('now'))
664 ON CONFLICT(tenant_id, env_name) DO UPDATE SET
665 save_process_history=excluded.save_process_history,
666 updated_at=datetime('now')
667 """, (tenant_id, env_name, enabled))
668 conn.commit()
669 conn.close()
670
671 return jsonify({"ok": True, "env": env_name, "save_process_history": bool(enabled)})
672
673# ----------------------------
674# Agent endpoint
675# ----------------------------
676@app.route("/receive", methods=["POST"])
677def receive_data():
678 try:
679 data = request.get_json(force=True, silent=True) or {}
680 if not data:
681 return jsonify({"error": "No data"}), 400
682
683 env_name, tenant_id = get_env_from_token(request)
684 if not env_name or not tenant_id:
685 return jsonify({"error": "Missing or invalid X-Env-Token"}), 401
686
687 info = data.get("info") or {}
688 computer_name = info.get("computer_name")
689 if not computer_name:
690 return jsonify({"error": "Missing info.computer_name"}), 400
691
692 now_iso = datetime.now().isoformat()
693 security_data = data.get("security_data") or {}
694
695 conn = db()
696 c = conn.cursor()
697
698 # Find by (tenant_id + name)
699 c.execute(
700 "SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1",
701 (tenant_id, computer_name),
702 )
703 row = c.fetchone()
704
705 if row:
706 computer_id = row["id"]
707 c.execute("""
708 UPDATE computers
709 SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
710 WHERE id=? AND tenant_id=?
711 """, (
712 info.get("user"),
713 info.get("ip_address"),
714 info.get("os"),
715 now_iso,
716 int(info.get("is_sysmon_available", 0) or 0),
717 env_name,
718 computer_id,
719 tenant_id,
720 ))
721 else:
722 c.execute("""
723 INSERT INTO computers(
724 tenant_id, env_name, name, user, ip, os,
725 first_seen, last_seen, sysmon_available
726 )
727 VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
728 """, (
729 tenant_id,
730 env_name,
731 computer_name,
732 info.get("user"),
733 info.get("ip_address"),
734 info.get("os"),
735 now_iso,
736 now_iso,
737 int(info.get("is_sysmon_available", 0) or 0),
738 ))
739 computer_id = c.lastrowid
740
741 # history row
742 c.execute("""
743 INSERT INTO computer_history(
744 computer_id, cpu_usage, ram_usage, disk_usage,
745 network_sent_mb, network_recv_mb, timestamp
746 )
747 VALUES(?, ?, ?, ?, ?, ?, ?)
748 """, (
749 computer_id,
750 float(info.get("cpu_usage") or 0),
751 float(info.get("ram_usage") or 0),
752 float(info.get("disk_usage") or 0),
753 float(info.get("network_sent_mb") or 0),
754 float(info.get("network_recv_mb") or 0),
755 info.get("timestamp") or now_iso,
756 ))
757
758 # ----------------------------
759 # processes: CURRENT (always)
760 # ----------------------------
761 # Clear old current snapshot for this computer
762 c.execute("DELETE FROM computer_processes_current WHERE computer_id=?", (computer_id,))
763
764 # Insert new snapshot
765 for proc in (data.get("processes") or []):
766 c.execute("""
767 INSERT INTO computer_processes_current(
768 computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp
769 )
770 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
771 """, (
772 computer_id,
773 proc.get("pid"),
774 proc.get("name"),
775 float(proc.get("cpu_percent") or 0),
776 float(proc.get("memory_mb") or 0),
777 proc.get("user") or proc.get("username"),
778 proc.get("cmdline"),
779 info.get("timestamp") or now_iso,
780 ))
781
782 # ----------------------------
783 # processes: HISTORY (optional)
784 # ----------------------------
785 if is_process_history_enabled(tenant_id, env_name):
786 for proc in (data.get("processes") or []):
787 c.execute("""
788 INSERT INTO computer_processes_history(
789 computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp
790 )
791 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
792 """, (
793 computer_id,
794 proc.get("pid"),
795 proc.get("name"),
796 float(proc.get("cpu_percent") or 0),
797 float(proc.get("memory_mb") or 0),
798 proc.get("user") or proc.get("username"),
799 proc.get("cmdline"),
800 info.get("timestamp") or now_iso,
801 ))
802
803 # sysmon events
804 sysmon_count = 0
805 for ev in (security_data.get("sysmon_events") or []):
806 c.execute("""
807 INSERT INTO sysmon_events(
808 computer_id, event_id, event_type, message, timestamp, details
809 )
810 VALUES(?, ?, ?, ?, ?, ?)
811 """, (
812 computer_id,
813 ev.get("event_id"),
814 ev.get("event_type", "Unknown"),
815 ev.get("message", ""),
816 ev.get("timestamp") or (info.get("timestamp") or now_iso),
817 json.dumps(ev, ensure_ascii=False),
818 ))
819 sysmon_count += 1
820
821 # network connections
822 for nc in (security_data.get("network_connections") or []):
823 c.execute("""
824 INSERT INTO network_connections(
825 computer_id, pid, local_address, remote_address, status, process_name, timestamp
826 )
827 VALUES(?, ?, ?, ?, ?, ?, ?)
828 """, (
829 computer_id,
830 nc.get("pid"),
831 nc.get("local_address"),
832 nc.get("remote_address"),
833 nc.get("status"),
834 nc.get("process_name"),
835 info.get("timestamp") or now_iso,
836 ))
837
838 conn.commit()
839 conn.close()
840
841 # optional JSON archive
842 try:
843 save_json_file(data)
844 except Exception:
845 pass
846
847 return jsonify({
848 "ok": True,
849 "computer_id": computer_id,
850 "env": env_name,
851 "tenant_id": tenant_id,
852 "sysmon_events_saved": sysmon_count,
853 "server_time": now_iso,
854 "process_history_enabled": bool(is_process_history_enabled(tenant_id, env_name)),
855 })
856
857 except Exception as e:
858 traceback.print_exc()
859 return jsonify({"error": str(e)}), 500
860
861
862# ----------------------------
863# Dashboard API (tenant scoped)
864# ----------------------------
865@app.route("/api/computers", methods=["GET"])
866@require_user()
867def api_computers():
868 tenant_id = request.user["tenant_id"]
869 env = request.headers.get("X-Env", "default")
870
871 conn = db()
872 c = conn.cursor()
873 c.execute("""
874 SELECT id, name, user, ip, os, first_seen, last_seen, sysmon_available
875 FROM computers
876 WHERE tenant_id=? AND env_name=?
877 ORDER BY last_seen DESC
878 """, (tenant_id, env))
879 rows = c.fetchall()
880
881 computers = []
882 for r in rows:
883 cid = r["id"]
884
885 c.execute("""
886 SELECT COUNT(*) AS n
887 FROM computer_history
888 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
889 """, (cid,))
890 recent_logs = int(c.fetchone()["n"] or 0)
891
892 c.execute("""
893 SELECT COUNT(*) AS n
894 FROM sysmon_events
895 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
896 """, (cid,))
897 recent_sysmon = int(c.fetchone()["n"] or 0)
898
899 c.execute("""
900 SELECT cpu_usage, ram_usage, timestamp
901 FROM computer_history
902 WHERE computer_id=?
903 ORDER BY timestamp DESC
904 LIMIT 5
905 """, (cid,))
906 metrics = c.fetchall()
907 avg_cpu = round(sum(m["cpu_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
908 avg_ram = round(sum(m["ram_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
909
910 status = "offline"
911 status_color = "gray"
912 try:
913 last_seen = r["last_seen"]
914 if last_seen:
915 dt = datetime.fromisoformat(
916 last_seen.replace("Z", "+00:00")) if "T" in last_seen else datetime.fromisoformat(last_seen)
917 diff = (datetime.now() - dt).total_seconds()
918 if diff < 60:
919 status, status_color = "online", "green"
920 elif diff < 300:
921 status, status_color = "idle", "orange"
922 except Exception:
923 pass
924
925 computers.append({
926 "name": r["name"],
927 "user": r["user"],
928 "ip": r["ip"],
929 "os": r["os"],
930 "first_seen": r["first_seen"],
931 "last_seen": r["last_seen"],
932 "sysmon_available": bool(r["sysmon_available"]),
933 "recent_logs": recent_logs,
934 "recent_sysmon": recent_sysmon,
935 "avg_cpu": avg_cpu,
936 "avg_ram": avg_ram,
937 "status": status,
938 "status_color": status_color,
939 })
940
941 conn.close()
942 return jsonify(computers)
943
944
945@app.route("/api/stats", methods=["GET"])
946@require_user()
947def api_stats():
948 tenant_id = request.user["tenant_id"]
949
950 conn = db()
951 c = conn.cursor()
952
953 c.execute("""
954 SELECT COUNT(*) AS n
955 FROM sysmon_events s
956 JOIN computers c2 ON c2.id = s.computer_id
957 WHERE s.timestamp > datetime('now','-24 hours')
958 AND c2.tenant_id = ?
959 """, (tenant_id,))
960 total_sysmon = int(c.fetchone()["n"] or 0)
961
962 c.execute("""
963 SELECT COUNT(*) AS n
964 FROM network_connections n
965 JOIN computers c2 ON c2.id = n.computer_id
966 WHERE n.timestamp > datetime('now','-24 hours')
967 AND c2.tenant_id = ?
968 """, (tenant_id,))
969 total_net = int(c.fetchone()["n"] or 0)
970
971 conn.close()
972 return jsonify({
973 "total_sysmon_events": total_sysmon,
974 "total_network_connections": total_net,
975 "server_time": datetime.now().isoformat(),
976 })
977
978
979@app.route("/api/computer/<computer_name>", methods=["GET"])
980@require_user()
981def api_computer_details(computer_name):
982 tenant_id = request.user["tenant_id"]
983
984 conn = db()
985 c = conn.cursor()
986 c.execute("""
987 SELECT id, name, user, ip, os, first_seen, last_seen, env_name
988 FROM computers
989 WHERE tenant_id=? AND name=?
990 LIMIT 1
991 """, (tenant_id, computer_name))
992 comp = c.fetchone()
993 if not comp:
994 conn.close()
995 return jsonify({"error": "Computer not found"}), 404
996
997 computer_id = comp["id"]
998
999 c.execute("SELECT COUNT(*) AS n FROM computer_history WHERE computer_id=?", (computer_id,))
1000 total_logs = int(c.fetchone()["n"] or 0)
1001
1002 c.execute("""
1003 SELECT cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp
1004 FROM computer_history
1005 WHERE computer_id=?
1006 ORDER BY timestamp DESC
1007 LIMIT 100
1008 """, (computer_id,))
1009 history = [dict(r) for r in c.fetchall()]
1010 current_metrics = history[0] if history else {}
1011
1012 c.execute("""
1013 SELECT pid, name, username, cpu_percent, memory_mb, cmdline, timestamp
1014 FROM computer_processes_current
1015 WHERE computer_id=?
1016 ORDER BY timestamp DESC
1017 LIMIT 200
1018 """, (computer_id,))
1019 processes = [dict(r) for r in c.fetchall()]
1020
1021 c.execute("""
1022 SELECT event_id, event_type, message, timestamp, details
1023 FROM sysmon_events
1024 WHERE computer_id=?
1025 ORDER BY timestamp DESC
1026 LIMIT 200
1027 """, (computer_id,))
1028 sysmon = []
1029 for r in c.fetchall():
1030 d = {}
1031 try:
1032 d = json.loads(r["details"]) if r["details"] else {}
1033 except Exception:
1034 d = {}
1035 sysmon.append({
1036 "event_id": r["event_id"],
1037 "event_type": r["event_type"],
1038 "message": r["message"],
1039 "timestamp": r["timestamp"],
1040 "details": d,
1041 })
1042
1043 c.execute("""
1044 SELECT pid, process_name, local_address, remote_address, status, timestamp
1045 FROM network_connections
1046 WHERE computer_id=?
1047 ORDER BY timestamp DESC
1048 LIMIT 100
1049 """, (computer_id,))
1050 net = [dict(r) for r in c.fetchall()]
1051
1052 conn.close()
1053
1054 return jsonify({
1055 "computer": {
1056 "id": comp["id"],
1057 "name": comp["name"],
1058 "user": comp["user"],
1059 "ip": comp["ip"],
1060 "os": comp["os"],
1061 "first_seen": comp["first_seen"],
1062 "last_seen": comp["last_seen"],
1063 "env_name": comp["env_name"],
1064 "total_logs": total_logs,
1065 },
1066 "history": history,
1067 "current_metrics": current_metrics,
1068 "recent_processes": processes,
1069 "sysmon_events": sysmon,
1070 "network_connections": net,
1071 })
1072
1073
1074@app.route("/api/export/<computer_name>", methods=["GET"])
1075@require_user()
1076def api_export(computer_name):
1077 tenant_id = request.user["tenant_id"]
1078
1079 conn = db()
1080 c = conn.cursor()
1081 c.execute("""
1082 SELECT *
1083 FROM computers
1084 WHERE tenant_id=? AND name=?
1085 LIMIT 1
1086 """, (tenant_id, computer_name))
1087 comp = c.fetchone()
1088 if not comp:
1089 conn.close()
1090 return jsonify({"error": "Computer not found"}), 404
1091
1092 computer_id = comp["id"]
1093 out = {"computer": dict(comp)}
1094
1095 c.execute("SELECT * FROM computer_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1096 out["history"] = [dict(r) for r in c.fetchall()]
1097
1098 c.execute("SELECT * FROM sysmon_events WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1099 out["sysmon_events"] = [dict(r) for r in c.fetchall()]
1100
1101 c.execute("SELECT * FROM computer_processes_current WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1102 out["processes_current"] = [dict(r) for r in c.fetchall()]
1103
1104 c.execute("SELECT * FROM computer_processes_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1105 out["processes_history"] = [dict(r) for r in c.fetchall()]
1106
1107 c.execute("SELECT * FROM network_connections WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1108 out["network_connections"] = [dict(r) for r in c.fetchall()]
1109
1110 conn.close()
1111
1112 return Response(
1113 json.dumps(out, indent=2, default=str, ensure_ascii=False),
1114 mimetype="application/json",
1115 headers={"Content-Disposition": f"attachment; filename={computer_name}_export.json"},
1116 )
1117
1118
1119# ----------------------------
1120# Simple RAG chat
1121# ----------------------------
1122def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10):
1123 conn = db()
1124 c = conn.cursor()
1125
1126 params = [tenant_id]
1127 comp_clause = ""
1128 if computer_name:
1129 comp_clause = "AND c2.name = ?"
1130 params.append(computer_name)
1131
1132 # sysmon
1133 c.execute(f"""
1134 SELECT s.timestamp, s.event_id, s.event_type, s.message
1135 FROM sysmon_events s
1136 JOIN computers c2 ON c2.id = s.computer_id
1137 WHERE c2.tenant_id = ? {comp_clause}
1138 ORDER BY s.timestamp DESC
1139 LIMIT ?
1140 """, (*params, limit_per_table))
1141 sysmon_rows = c.fetchall()
1142
1143 # perf
1144 c.execute(f"""
1145 SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb
1146 FROM computer_history h
1147 JOIN computers c2 ON c2.id = h.computer_id
1148 WHERE c2.tenant_id = ? {comp_clause}
1149 ORDER BY h.timestamp DESC
1150 LIMIT ?
1151 """, (*params, limit_per_table))
1152 perf_rows = c.fetchall()
1153
1154 # proc
1155 c.execute(f"""
1156 SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb
1157 FROM computer_processes_current p
1158 JOIN computers c2 ON c2.id = p.computer_id
1159 WHERE c2.tenant_id = ? {comp_clause}
1160 ORDER BY p.timestamp DESC
1161 LIMIT ?
1162 """, (*params, limit_per_table))
1163 proc_rows = c.fetchall()
1164
1165 conn.close()
1166
1167 lines = []
1168 for r in sysmon_rows:
1169 lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}")
1170 for r in perf_rows:
1171 lines.append(
1172 f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | "
1173 f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB"
1174 )
1175 for r in proc_rows:
1176 lines.append(
1177 f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB"
1178 )
1179
1180 return "\n".join(lines)
1181
1182
1183def ask_llm_with_context(question, context):
1184 if not client:
1185 return "Немаш поставено OPENAI_API_KEY на серверот."
1186
1187 system_msg = (
1188 "Ти си асистент за безбедност и систем мониторинг. "
1189 "Одговараш базирано ИСКЛУЧИВО на дадените логови. "
1190 "Ако нема доволно податоци, кажи дека нема доволно информации. "
1191 "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите."
1192 )
1193
1194 completion = client.chat.completions.create(
1195 model="gpt-4.1-mini",
1196 messages=[
1197 {"role": "system", "content": system_msg},
1198 {"role": "user", "content": f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"},
1199 ],
1200 temperature=0.2,
1201 )
1202 return completion.choices[0].message.content
1203
1204
1205@app.route("/api/chat", methods=["POST"])
1206@require_user()
1207def api_chat():
1208 try:
1209 tenant_id = request.user["tenant_id"]
1210 data = request.get_json(force=True, silent=True) or {}
1211 question = (data.get("question") or "").strip()
1212 computer_name = data.get("computer_name") or None
1213
1214 if not question:
1215 return jsonify({"error": "No question provided"}), 400
1216
1217 ctx = build_rag_context(tenant_id, question, computer_name=computer_name, limit_per_table=10)
1218 ans = ask_llm_with_context(question, ctx)
1219 return jsonify({"answer": ans or ""}), 200
1220
1221 except Exception as e:
1222 traceback.print_exc()
1223 return jsonify({"error": str(e)}), 500
1224
1225
1226# ----------------------------
1227# Misc
1228# ----------------------------
1229@app.route("/ping")
1230def ping():
1231 return jsonify({
1232 "status": "alive",
1233 "server_time": datetime.now().isoformat(),
1234 "database": DB_FILE,
1235 "cors_origins": ALLOWED_ORIGINS,
1236 })
1237
1238
1239@app.route("/")
1240def root():
1241 return jsonify({
1242 "ok": True,
1243 "service": "netIntel server",
1244 "hint": "frontend should call /api/* endpoints",
1245 })
1246
1247
1248if __name__ == "__main__":
1249 init_db()
1250
1251 cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
1252 cleanup_thread.start()
1253
1254 print(" netIntel server running on 0.0.0.0:5555")
1255 # debug=False recommended; if you need debug, set True temporarily
1256 app.run(host="0.0.0.0", port=5555, debug=False, threaded=True)
Note: See TracBrowser for help on using the repository browser.