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
RevLine 
[640ed89]1#!/usr/bin/env python3
2"""
[505f39a]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
[640ed89]23"""
24
[505f39a]25import os
[640ed89]26import json
27import time
[505f39a]28import sqlite3
29import traceback
[640ed89]30import threading
[505f39a]31import secrets
32from datetime import datetime, timedelta
33from functools import wraps
34
35from flask import Flask, request, jsonify, Response
[2058e5c]36from dotenv import load_dotenv
[640ed89]37
[505f39a]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
[640ed89]47
[505f39a]48from flask_cors import CORS
[640ed89]49
[505f39a]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# ----------------------------
[640ed89]77app = Flask(__name__)
[505f39a]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
[640ed89]101
102
[505f39a]103def init_db():
104 """Create tables if missing + light migrations."""
105 conn = db()
106 c = conn.cursor()
[2058e5c]107
[505f39a]108 c.execute("PRAGMA foreign_keys = ON;")
[2058e5c]109
[505f39a]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 """)
[2058e5c]119
[505f39a]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 """)
[2058e5c]129
[505f39a]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 """)
[2058e5c]141
[505f39a]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 """)
[2058e5c]153
[505f39a]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 """)
[2058e5c]166
[505f39a]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 """)
[2058e5c]183
[505f39a]184 # History
[2058e5c]185 c.execute("""
[505f39a]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 """)
[2058e5c]198
[505f39a]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 """)
[640ed89]214
[505f39a]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 """)
[640ed89]228
[505f39a]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 """)
[640ed89]243
[505f39a]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 """)
[640ed89]257
258 conn.commit()
259 conn.close()
[505f39a]260 print(f"✅ DB ready: {DB_FILE}")
[640ed89]261
262
[505f39a]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")
[640ed89]270
271
[505f39a]272def read_jwt(token: str):
273 return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
[640ed89]274
275
[505f39a]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)
[640ed89]289
[505f39a]290 return wrap
[2058e5c]291
[505f39a]292 return deco
[640ed89]293
[505f39a]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
[640ed89]302 try:
[505f39a]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)
[640ed89]310
[505f39a]311 return wrap
[640ed89]312
[505f39a]313 return deco
[640ed89]314
315
[505f39a]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)
[640ed89]323
[505f39a]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"])
[640ed89]339
340
[505f39a]341# ----------------------------
342# Utility
343# ----------------------------
[640ed89]344def save_json_file(data):
[505f39a]345 info = data.get("info") or {}
346 computer_name = info.get("computer_name", "unknown")
[640ed89]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):
[505f39a]354 try:
355 with open(filepath, "r", encoding="utf-8") as f:
356 existing_data = json.load(f)
357 except Exception:
358 existing_data = []
[640ed89]359 else:
360 existing_data = []
361
362 existing_data.append({
[505f39a]363 "timestamp": info.get("timestamp"),
[640ed89]364 "info": info,
[505f39a]365 "processes_count": len(data.get("processes", [])),
366 "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
[640ed89]367 })
368
[505f39a]369 with open(filepath, "w", encoding="utf-8") as f:
[640ed89]370 json.dump(existing_data, f, indent=2, ensure_ascii=False)
371
372
[505f39a]373def cleanup_old_data():
374 """Deletes old rows (30 days) every hour."""
375 while True:
376 time.sleep(3600)
[640ed89]377 try:
[505f39a]378 conn = db()
379 c = conn.cursor()
380 cutoff = (datetime.now() - timedelta(days=30)).isoformat()
[640ed89]381
[505f39a]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,))
[640ed89]386
[505f39a]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)
[640ed89]392
393
[505f39a]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
[640ed89]405
[505f39a]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 ""
[640ed89]415
[505f39a]416 if not email:
417 return jsonify({"error": "No email in token"}), 400
[640ed89]418
[505f39a]419 conn = db()
420 c = conn.cursor()
[640ed89]421
[505f39a]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
[640ed89]434
[505f39a]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()
[640ed89]438
[505f39a]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"
[640ed89]447
[505f39a]448 c.execute(
449 "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
450 (user_id, tenant_id),
451 )
[640ed89]452
[505f39a]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"
[640ed89]461
[505f39a]462 conn.commit()
463 conn.close()
[640ed89]464
[505f39a]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,
[640ed89]474 )
475
[505f39a]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,
[640ed89]484 )
[505f39a]485 return resp
[640ed89]486
[505f39a]487 except Exception as e:
488 return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
[640ed89]489
490
[505f39a]491@app.route("/api/me", methods=["GET"])
492@require_user()
493def api_me():
494 return jsonify({"user": request.user})
[640ed89]495
496
[505f39a]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
[640ed89]502
503
[505f39a]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})
[640ed89]519
520
[505f39a]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
[640ed89]529
[505f39a]530 conn = db()
531 c = conn.cursor()
[640ed89]532 try:
[505f39a]533 c.execute(
534 "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
535 (tenant_id, name),
[640ed89]536 )
[505f39a]537 conn.commit()
[640ed89]538 except Exception as e:
[505f39a]539 conn.close()
540 return jsonify({"error": str(e)}), 400
541 conn.close()
542 return jsonify({"ok": True, "name": name})
[640ed89]543
544
[505f39a]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
[640ed89]553
[505f39a]554 conn = db()
[640ed89]555 c = conn.cursor()
[505f39a]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
[640ed89]560
[505f39a]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()
[640ed89]568
[505f39a]569 return jsonify({"ok": True, "env": env, "token": token})
[640ed89]570
571
[505f39a]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
[640ed89]581
[505f39a]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
[640ed89]585
[505f39a]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
[640ed89]590
[505f39a]591 now_iso = datetime.now().isoformat()
592 security_data = data.get("security_data") or {}
[640ed89]593
[505f39a]594 conn = db()
595 c = conn.cursor()
[640ed89]596
[505f39a]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
[640ed89]633
[505f39a]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 ))
[640ed89]695
[505f39a]696 conn.commit()
697 conn.close()
[640ed89]698
[505f39a]699 # optional JSON archive
[640ed89]700 try:
[505f39a]701 save_json_file(data)
702 except Exception:
703 pass
[640ed89]704
[505f39a]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 })
[640ed89]713
[505f39a]714 except Exception as e:
715 traceback.print_exc()
716 return jsonify({"error": str(e)}), 500
[640ed89]717
718
[505f39a]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")
[640ed89]727
[505f39a]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()
[640ed89]737
[505f39a]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
[640ed89]781
[505f39a]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 })
[640ed89]797
[505f39a]798 conn.close()
799 return jsonify(computers)
[640ed89]800
801
[505f39a]802@app.route("/api/stats", methods=["GET"])
803@require_user()
804def api_stats():
805 tenant_id = request.user["tenant_id"]
[640ed89]806
[505f39a]807 conn = db()
[640ed89]808 c = conn.cursor()
809
[505f39a]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)
[640ed89]818
[505f39a]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)
[640ed89]827
828 conn.close()
829 return jsonify({
[505f39a]830 "total_sysmon_events": total_sysmon,
831 "total_network_connections": total_net,
832 "server_time": datetime.now().isoformat(),
[640ed89]833 })
834
835
[505f39a]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"]
[640ed89]840
[505f39a]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:
[640ed89]851 conn.close()
852 return jsonify({"error": "Computer not found"}), 404
853
[505f39a]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,
[640ed89]898 })
899
[505f39a]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
[640ed89]909 conn.close()
910
911 return jsonify({
[505f39a]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,
[640ed89]928 })
929
930
[505f39a]931@app.route("/api/export/<computer_name>", methods=["GET"])
932@require_user()
933def api_export(computer_name):
934 tenant_id = request.user["tenant_id"]
[640ed89]935
[505f39a]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:
[640ed89]946 conn.close()
947 return jsonify({"error": "Computer not found"}), 404
948
[505f39a]949 computer_id = comp["id"]
950 out = {"computer": dict(comp)}
[640ed89]951
[505f39a]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()]
[640ed89]954
[505f39a]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()]
[640ed89]957
[505f39a]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()]
[640ed89]960
[505f39a]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()]
[640ed89]963
964 conn.close()
965
[505f39a]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 )
[2058e5c]971
972
[505f39a]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()
[2058e5c]979
[505f39a]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()
[2058e5c]1018
1019 conn.close()
1020
[505f39a]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 )
[2058e5c]1033
[505f39a]1034 return "\n".join(lines)
[2058e5c]1035
1036
[505f39a]1037def ask_llm_with_context(question, context):
1038 if not client:
1039 return "Немаш поставено OPENAI_API_KEY на серверот."
[2058e5c]1040
[505f39a]1041 system_msg = (
1042 "Ти си асистент за безбедност и систем мониторинг. "
1043 "Одговараш базирано ИСКЛУЧИВО на дадените логови. "
1044 "Ако нема доволно податоци, кажи дека нема доволно информации. "
1045 "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите."
1046 )
[2058e5c]1047
[505f39a]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
[2058e5c]1057
1058
[505f39a]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
[2058e5c]1067
[505f39a]1068 if not question:
1069 return jsonify({"error": "No question provided"}), 400
[2058e5c]1070
[505f39a]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
[2058e5c]1074
[505f39a]1075 except Exception as e:
1076 traceback.print_exc()
1077 return jsonify({"error": str(e)}), 500
[2058e5c]1078
1079
[505f39a]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 })
[2058e5c]1091
1092
[505f39a]1093@app.route("/")
1094def root():
1095 return jsonify({
1096 "ok": True,
1097 "service": "netIntel server",
1098 "hint": "frontend should call /api/* endpoints",
1099 })
1100
[2058e5c]1101
[505f39a]1102if __name__ == "__main__":
1103 init_db()
[640ed89]1104
1105 cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
1106 cleanup_thread.start()
1107
[505f39a]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.