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
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
[d42aac3]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
[640ed89]304 conn.commit()
305 conn.close()
[505f39a]306 print(f"✅ DB ready: {DB_FILE}")
[640ed89]307
308
[505f39a]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")
[640ed89]316
317
[505f39a]318def read_jwt(token: str):
319 return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
[640ed89]320
321
[505f39a]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)
[640ed89]335
[505f39a]336 return wrap
[2058e5c]337
[505f39a]338 return deco
[d42aac3]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
[640ed89]353
[505f39a]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
[640ed89]362 try:
[505f39a]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)
[640ed89]370
[505f39a]371 return wrap
[640ed89]372
[505f39a]373 return deco
[640ed89]374
375
[505f39a]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)
[640ed89]383
[505f39a]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"])
[640ed89]399
400
[505f39a]401# ----------------------------
402# Utility
403# ----------------------------
[640ed89]404def save_json_file(data):
[505f39a]405 info = data.get("info") or {}
406 computer_name = info.get("computer_name", "unknown")
[640ed89]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):
[505f39a]414 try:
415 with open(filepath, "r", encoding="utf-8") as f:
416 existing_data = json.load(f)
417 except Exception:
418 existing_data = []
[640ed89]419 else:
420 existing_data = []
421
422 existing_data.append({
[505f39a]423 "timestamp": info.get("timestamp"),
[640ed89]424 "info": info,
[505f39a]425 "processes_count": len(data.get("processes", [])),
426 "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
[640ed89]427 })
428
[505f39a]429 with open(filepath, "w", encoding="utf-8") as f:
[640ed89]430 json.dump(existing_data, f, indent=2, ensure_ascii=False)
431
432
[505f39a]433def cleanup_old_data():
434 """Deletes old rows (30 days) every hour."""
435 while True:
436 time.sleep(3600)
[640ed89]437 try:
[505f39a]438 conn = db()
439 c = conn.cursor()
440 cutoff = (datetime.now() - timedelta(days=30)).isoformat()
[640ed89]441
[505f39a]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,))
[d42aac3]445 c.execute("DELETE FROM computer_processes_history WHERE timestamp < ?", (cutoff,))
[640ed89]446
[505f39a]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)
[640ed89]452
453
[505f39a]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
[640ed89]465
[505f39a]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 ""
[640ed89]475
[505f39a]476 if not email:
477 return jsonify({"error": "No email in token"}), 400
[640ed89]478
[505f39a]479 conn = db()
480 c = conn.cursor()
[640ed89]481
[505f39a]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
[640ed89]494
[505f39a]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()
[640ed89]498
[505f39a]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"
[640ed89]507
[505f39a]508 c.execute(
509 "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
510 (user_id, tenant_id),
511 )
[640ed89]512
[505f39a]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"
[640ed89]521
[505f39a]522 conn.commit()
523 conn.close()
[640ed89]524
[505f39a]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,
[640ed89]534 )
535
[505f39a]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,
[640ed89]544 )
[505f39a]545 return resp
[640ed89]546
[505f39a]547 except Exception as e:
548 return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
[640ed89]549
550
[505f39a]551@app.route("/api/me", methods=["GET"])
552@require_user()
553def api_me():
554 return jsonify({"user": request.user})
[640ed89]555
556
[505f39a]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
[640ed89]562
563
[505f39a]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})
[640ed89]579
580
[505f39a]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
[640ed89]589
[505f39a]590 conn = db()
591 c = conn.cursor()
[640ed89]592 try:
[505f39a]593 c.execute(
594 "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
595 (tenant_id, name),
[640ed89]596 )
[505f39a]597 conn.commit()
[640ed89]598 except Exception as e:
[505f39a]599 conn.close()
600 return jsonify({"error": str(e)}), 400
601 conn.close()
602 return jsonify({"ok": True, "name": name})
[640ed89]603
604
[505f39a]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
[640ed89]613
[505f39a]614 conn = db()
[640ed89]615 c = conn.cursor()
[505f39a]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
[640ed89]620
[505f39a]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()
[640ed89]628
[505f39a]629 return jsonify({"ok": True, "env": env, "token": token})
[640ed89]630
[d42aac3]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)})
[640ed89]672
[505f39a]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
[640ed89]682
[505f39a]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
[640ed89]686
[505f39a]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
[640ed89]691
[505f39a]692 now_iso = datetime.now().isoformat()
693 security_data = data.get("security_data") or {}
[640ed89]694
[505f39a]695 conn = db()
696 c = conn.cursor()
[640ed89]697
[505f39a]698 # Find by (tenant_id + name)
[d42aac3]699 c.execute(
700 "SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1",
701 (tenant_id, computer_name),
702 )
[505f39a]703 row = c.fetchone()
704
705 if row:
706 computer_id = row["id"]
707 c.execute("""
[d42aac3]708 UPDATE computers
709 SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
710 WHERE id=? AND tenant_id=?
711 """, (
[505f39a]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("""
[d42aac3]723 INSERT INTO computers(
724 tenant_id, env_name, name, user, ip, os,
725 first_seen, last_seen, sysmon_available
726 )
727 VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
728 """, (
[505f39a]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
[640ed89]740
[505f39a]741 # history row
742 c.execute("""
[d42aac3]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 """, (
[505f39a]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
[d42aac3]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
[505f39a]765 for proc in (data.get("processes") or []):
766 c.execute("""
[d42aac3]767 INSERT INTO computer_processes_current(
768 computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp
769 )
770 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
771 """, (
[505f39a]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
[d42aac3]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
[505f39a]803 # sysmon events
804 sysmon_count = 0
805 for ev in (security_data.get("sysmon_events") or []):
806 c.execute("""
[d42aac3]807 INSERT INTO sysmon_events(
808 computer_id, event_id, event_type, message, timestamp, details
809 )
810 VALUES(?, ?, ?, ?, ?, ?)
811 """, (
[505f39a]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("""
[d42aac3]824 INSERT INTO network_connections(
825 computer_id, pid, local_address, remote_address, status, process_name, timestamp
826 )
827 VALUES(?, ?, ?, ?, ?, ?, ?)
828 """, (
[505f39a]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 ))
[640ed89]837
[505f39a]838 conn.commit()
839 conn.close()
[640ed89]840
[505f39a]841 # optional JSON archive
[640ed89]842 try:
[505f39a]843 save_json_file(data)
844 except Exception:
845 pass
[640ed89]846
[505f39a]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,
[d42aac3]854 "process_history_enabled": bool(is_process_history_enabled(tenant_id, env_name)),
[505f39a]855 })
[640ed89]856
[505f39a]857 except Exception as e:
858 traceback.print_exc()
859 return jsonify({"error": str(e)}), 500
[640ed89]860
861
[505f39a]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")
[640ed89]870
[505f39a]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()
[640ed89]880
[505f39a]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
[640ed89]924
[505f39a]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 })
[640ed89]940
[505f39a]941 conn.close()
942 return jsonify(computers)
[640ed89]943
944
[505f39a]945@app.route("/api/stats", methods=["GET"])
946@require_user()
947def api_stats():
948 tenant_id = request.user["tenant_id"]
[640ed89]949
[505f39a]950 conn = db()
[640ed89]951 c = conn.cursor()
952
[505f39a]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)
[640ed89]961
[505f39a]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)
[640ed89]970
971 conn.close()
972 return jsonify({
[505f39a]973 "total_sysmon_events": total_sysmon,
974 "total_network_connections": total_net,
975 "server_time": datetime.now().isoformat(),
[640ed89]976 })
977
978
[505f39a]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"]
[640ed89]983
[505f39a]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:
[640ed89]994 conn.close()
995 return jsonify({"error": "Computer not found"}), 404
996
[505f39a]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("""
[d42aac3]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,))
[505f39a]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,
[640ed89]1041 })
1042
[505f39a]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
[640ed89]1052 conn.close()
1053
1054 return jsonify({
[505f39a]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,
[640ed89]1071 })
1072
1073
[505f39a]1074@app.route("/api/export/<computer_name>", methods=["GET"])
1075@require_user()
1076def api_export(computer_name):
1077 tenant_id = request.user["tenant_id"]
[640ed89]1078
[505f39a]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:
[640ed89]1089 conn.close()
1090 return jsonify({"error": "Computer not found"}), 404
1091
[505f39a]1092 computer_id = comp["id"]
1093 out = {"computer": dict(comp)}
[640ed89]1094
[505f39a]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()]
[640ed89]1097
[505f39a]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()]
[640ed89]1100
[d42aac3]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()]
[640ed89]1106
[505f39a]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()]
[640ed89]1109
1110 conn.close()
1111
[505f39a]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 )
[2058e5c]1117
1118
[505f39a]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()
[2058e5c]1125
[505f39a]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
[d42aac3]1157 FROM computer_processes_current p
[505f39a]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()
[2058e5c]1164
1165 conn.close()
1166
[505f39a]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 )
[2058e5c]1179
[505f39a]1180 return "\n".join(lines)
[2058e5c]1181
1182
[505f39a]1183def ask_llm_with_context(question, context):
1184 if not client:
1185 return "Немаш поставено OPENAI_API_KEY на серверот."
[2058e5c]1186
[505f39a]1187 system_msg = (
1188 "Ти си асистент за безбедност и систем мониторинг. "
1189 "Одговараш базирано ИСКЛУЧИВО на дадените логови. "
1190 "Ако нема доволно податоци, кажи дека нема доволно информации. "
1191 "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите."
1192 )
[2058e5c]1193
[505f39a]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
[2058e5c]1203
1204
[505f39a]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
[2058e5c]1213
[505f39a]1214 if not question:
1215 return jsonify({"error": "No question provided"}), 400
[2058e5c]1216
[505f39a]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
[2058e5c]1220
[505f39a]1221 except Exception as e:
1222 traceback.print_exc()
1223 return jsonify({"error": str(e)}), 500
[2058e5c]1224
1225
[505f39a]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 })
[2058e5c]1237
1238
[505f39a]1239@app.route("/")
1240def root():
1241 return jsonify({
1242 "ok": True,
1243 "service": "netIntel server",
1244 "hint": "frontend should call /api/* endpoints",
1245 })
1246
[2058e5c]1247
[505f39a]1248if __name__ == "__main__":
1249 init_db()
[640ed89]1250
1251 cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
1252 cleanup_thread.start()
1253
[d42aac3]1254 print(" netIntel server running on 0.0.0.0:5555")
[505f39a]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.