source: server.py@ e4c61dd

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

Prototype 1.1

  • Property mode set to 100644
File size: 54.3 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
[e4c61dd]49from dotenv import load_dotenv
50from pathlib import Path
51
52BASE_DIR = Path(__file__).resolve().parent
53load_dotenv(BASE_DIR / ".env")
54
55print("GRAFANA_API_TOKEN =", os.environ.get("GRAFANA_API_TOKEN"))
[640ed89]56
[505f39a]57
58# ----------------------------
59# Config
60# ----------------------------
61DB_FILE = os.environ.get("DB_FILE", "lan_logs_sysmon.db")
62LOG_DIR = os.environ.get("LOG_DIR", "received_data_sysmon")
63
64OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
65GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
66JWT_SECRET = os.environ.get("JWT_SECRET", "dev-change-me")
67JWT_ISSUER = os.environ.get("JWT_ISSUER", "netintel")
68COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "0") == "1" # set 1 only on HTTPS
[e4c61dd]69GRAFANA_TOKEN = os.environ.get("GRAFANA_TOKEN", "")
[505f39a]70
71# CORS origins (add your LAN origins if needed)
72EXTRA_ORIGINS = [o.strip() for o in os.environ.get("CORS_ORIGINS", "").split(",") if o.strip()]
73DEFAULT_ORIGINS = [
74 "http://localhost:5173",
75 "http://127.0.0.1:5173",
76]
77ALLOWED_ORIGINS = list(dict.fromkeys(DEFAULT_ORIGINS + EXTRA_ORIGINS))
78
79client = OpenAI(api_key=OPENAI_API_KEY, timeout=15) if (OPENAI_API_KEY and OpenAI) else None
80
81# ----------------------------
82# App
83# ----------------------------
[640ed89]84app = Flask(__name__)
[505f39a]85os.makedirs(LOG_DIR, exist_ok=True)
86
87CORS(
88 app,
89 supports_credentials=True,
90 origins=ALLOWED_ORIGINS,
91 allow_headers=[
92 "Content-Type",
93 "X-Admin-Session",
94 "X-Env",
95 "X-Env-Token",
96 ],
97 methods=["GET", "POST", "OPTIONS"],
98)
99
100
101# ----------------------------
102# DB helpers
103# ----------------------------
104def db():
105 conn = sqlite3.connect(DB_FILE)
106 conn.row_factory = sqlite3.Row
107 return conn
[640ed89]108
109
[505f39a]110def init_db():
111 """Create tables if missing + light migrations."""
112 conn = db()
113 c = conn.cursor()
[2058e5c]114
[505f39a]115 c.execute("PRAGMA foreign_keys = ON;")
[2058e5c]116
[505f39a]117 # Tenants / users / memberships
118 c.execute("""
119 CREATE TABLE IF NOT EXISTS tenants(
120 id INTEGER PRIMARY KEY AUTOINCREMENT,
121 name TEXT NOT NULL,
122 owner_email TEXT NOT NULL,
123 created_at TEXT
124 )
125 """)
[2058e5c]126
[505f39a]127 c.execute("""
128 CREATE TABLE IF NOT EXISTS users(
129 id INTEGER PRIMARY KEY AUTOINCREMENT,
130 email TEXT UNIQUE NOT NULL,
131 name TEXT,
132 picture TEXT,
133 created_at TEXT
134 )
135 """)
[2058e5c]136
[505f39a]137 c.execute("""
138 CREATE TABLE IF NOT EXISTS memberships(
139 user_id INTEGER NOT NULL,
140 tenant_id INTEGER NOT NULL,
141 role TEXT DEFAULT 'admin',
142 created_at TEXT,
143 PRIMARY KEY(user_id, tenant_id),
144 FOREIGN KEY(user_id) REFERENCES users(id),
145 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
146 )
147 """)
[2058e5c]148
[505f39a]149 # Environments (unique per tenant)
150 c.execute("""
151 CREATE TABLE IF NOT EXISTS environments(
152 id INTEGER PRIMARY KEY AUTOINCREMENT,
153 tenant_id INTEGER NOT NULL,
154 name TEXT NOT NULL,
155 created_at TEXT,
156 UNIQUE(tenant_id, name),
157 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
158 )
159 """)
[2058e5c]160
[505f39a]161 # Env tokens (agent tokens)
162 c.execute("""
163 CREATE TABLE IF NOT EXISTS env_tokens(
164 id INTEGER PRIMARY KEY AUTOINCREMENT,
165 tenant_id INTEGER NOT NULL,
166 env_name TEXT NOT NULL,
167 token TEXT UNIQUE NOT NULL,
168 created_at TEXT,
169 expires_at TEXT,
170 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
171 )
172 """)
[2058e5c]173
[505f39a]174 # Computers
175 c.execute("""
176 CREATE TABLE IF NOT EXISTS computers(
177 id INTEGER PRIMARY KEY AUTOINCREMENT,
178 tenant_id INTEGER NOT NULL,
179 env_name TEXT DEFAULT 'default',
180 name TEXT NOT NULL,
181 user TEXT,
182 ip TEXT,
183 os TEXT,
184 first_seen TEXT,
185 last_seen TEXT,
186 sysmon_available INTEGER DEFAULT 0,
187 UNIQUE(tenant_id, name)
188 )
189 """)
[2058e5c]190
[505f39a]191 # History
[2058e5c]192 c.execute("""
[505f39a]193 CREATE TABLE IF NOT EXISTS computer_history(
194 id INTEGER PRIMARY KEY AUTOINCREMENT,
195 computer_id INTEGER,
196 cpu_usage REAL,
197 ram_usage REAL,
198 disk_usage REAL,
199 network_sent_mb REAL,
200 network_recv_mb REAL,
201 timestamp TEXT,
202 FOREIGN KEY(computer_id) REFERENCES computers(id)
203 )
204 """)
[2058e5c]205
[505f39a]206 # Processes
207 c.execute("""
208 CREATE TABLE IF NOT EXISTS computer_processes(
209 id INTEGER PRIMARY KEY AUTOINCREMENT,
210 computer_id INTEGER,
211 pid INTEGER,
212 name TEXT,
213 cpu_percent REAL,
214 memory_mb REAL,
215 username TEXT,
216 cmdline TEXT,
217 timestamp TEXT,
218 FOREIGN KEY(computer_id) REFERENCES computers(id)
219 )
220 """)
[640ed89]221
[505f39a]222 # Sysmon events
223 c.execute("""
224 CREATE TABLE IF NOT EXISTS sysmon_events(
225 id INTEGER PRIMARY KEY AUTOINCREMENT,
226 computer_id INTEGER,
227 event_id INTEGER,
228 event_type TEXT,
229 message TEXT,
230 timestamp TEXT,
231 details TEXT,
232 FOREIGN KEY(computer_id) REFERENCES computers(id)
233 )
234 """)
[640ed89]235
[505f39a]236 # Network connections
237 c.execute("""
238 CREATE TABLE IF NOT EXISTS network_connections(
239 id INTEGER PRIMARY KEY AUTOINCREMENT,
240 computer_id INTEGER,
241 pid INTEGER,
242 local_address TEXT,
243 remote_address TEXT,
244 status TEXT,
245 process_name TEXT,
246 timestamp TEXT,
247 FOREIGN KEY(computer_id) REFERENCES computers(id)
248 )
249 """)
[640ed89]250
[505f39a]251 # Security alerts
252 c.execute("""
253 CREATE TABLE IF NOT EXISTS security_alerts(
254 id INTEGER PRIMARY KEY AUTOINCREMENT,
255 computer_id INTEGER,
256 alert_type TEXT,
257 severity TEXT,
258 description TEXT,
259 timestamp TEXT,
260 resolved INTEGER DEFAULT 0,
261 FOREIGN KEY(computer_id) REFERENCES computers(id)
262 )
263 """)
[640ed89]264
[d42aac3]265 # Environment settings (switch-ови по env)
266 c.execute("""
267 CREATE TABLE IF NOT EXISTS env_settings(
268 tenant_id INTEGER NOT NULL,
269 env_name TEXT NOT NULL,
270 save_process_history INTEGER DEFAULT 0,
271 created_at TEXT,
272 updated_at TEXT,
273 PRIMARY KEY(tenant_id, env_name),
274 FOREIGN KEY(tenant_id) REFERENCES tenants(id)
275 )
276 """)
277
278 # Latest processes (only current snapshot)
279 c.execute("""
280 CREATE TABLE IF NOT EXISTS computer_processes_current(
281 id INTEGER PRIMARY KEY AUTOINCREMENT,
282 computer_id INTEGER NOT NULL,
283 pid INTEGER,
284 name TEXT,
285 cpu_percent REAL,
286 memory_mb REAL,
287 username TEXT,
288 cmdline TEXT,
289 timestamp TEXT,
290 FOREIGN KEY(computer_id) REFERENCES computers(id)
291 )
292 """)
293
294 # History processes (optional, controlled by switch)
295 c.execute("""
296 CREATE TABLE IF NOT EXISTS computer_processes_history(
297 id INTEGER PRIMARY KEY AUTOINCREMENT,
298 computer_id INTEGER NOT NULL,
299 pid INTEGER,
300 name TEXT,
301 cpu_percent REAL,
302 memory_mb REAL,
303 username TEXT,
304 cmdline TEXT,
305 timestamp TEXT,
306 FOREIGN KEY(computer_id) REFERENCES computers(id)
307 )
308 """)
309
310
[640ed89]311 conn.commit()
312 conn.close()
[505f39a]313 print(f"✅ DB ready: {DB_FILE}")
[640ed89]314
315
[505f39a]316# ----------------------------
317# JWT helpers
318# ----------------------------
319def make_jwt(payload: dict, minutes=60 * 24):
320 exp = datetime.utcnow() + timedelta(minutes=minutes)
321 data = {**payload, "iss": JWT_ISSUER, "exp": exp}
322 return jwt.encode(data, JWT_SECRET, algorithm="HS256")
[640ed89]323
324
[505f39a]325def read_jwt(token: str):
326 return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
[640ed89]327
328
[505f39a]329def require_user():
330 def deco(fn):
331 @wraps(fn)
332 def wrap(*args, **kwargs):
333 tok = request.cookies.get("session") or ""
334 if not tok:
335 return jsonify({"error": "Unauthorized"}), 401
336 try:
337 claims = read_jwt(tok)
338 except Exception:
339 return jsonify({"error": "Unauthorized"}), 401
340 request.user = claims
341 return fn(*args, **kwargs)
[640ed89]342
[505f39a]343 return wrap
[2058e5c]344
[505f39a]345 return deco
[d42aac3]346def is_process_history_enabled(tenant_id: int, env_name: str) -> bool:
347 conn = db()
348 c = conn.cursor()
349 c.execute("""
350 SELECT save_process_history
351 FROM env_settings
352 WHERE tenant_id=? AND env_name=?
353 LIMIT 1
354 """, (tenant_id, env_name))
355 row = c.fetchone()
356 conn.close()
357
358 # default: OFF ако нема запис
359 return bool(row["save_process_history"]) if row else False
[640ed89]360
[505f39a]361
362def require_tenant_admin():
363 def deco(fn):
364 @wraps(fn)
365 def wrap(*args, **kwargs):
366 tok = request.cookies.get("session") or ""
367 if not tok:
368 return jsonify({"error": "Unauthorized"}), 401
[640ed89]369 try:
[505f39a]370 claims = read_jwt(tok)
371 except Exception:
372 return jsonify({"error": "Unauthorized"}), 401
373 if claims.get("role") != "admin":
374 return jsonify({"error": "Forbidden"}), 403
375 request.user = claims
376 return fn(*args, **kwargs)
[640ed89]377
[505f39a]378 return wrap
[640ed89]379
[505f39a]380 return deco
[640ed89]381
382
[505f39a]383# ----------------------------
384# Env-token helpers (agents)
385# ----------------------------
386def get_env_from_token(req):
387 tok = req.headers.get("X-Env-Token", "")
388 if not tok:
389 return (None, None)
[640ed89]390
[505f39a]391 conn = db()
392 c = conn.cursor()
393 c.execute("""
394 SELECT env_name, tenant_id
395 FROM env_tokens
396 WHERE token = ?
397 AND (expires_at IS NULL OR expires_at > datetime('now'))
398 LIMIT 1
399 """, (tok,))
400 row = c.fetchone()
401 conn.close()
402
403 if not row:
404 return (None, None)
405 return (row["env_name"], row["tenant_id"])
[640ed89]406
407
[505f39a]408# ----------------------------
409# Utility
410# ----------------------------
[640ed89]411def save_json_file(data):
[505f39a]412 info = data.get("info") or {}
413 computer_name = info.get("computer_name", "unknown")
[640ed89]414 date_str = datetime.now().strftime("%Y%m%d_%H")
415
416 computer_dir = os.path.join(LOG_DIR, computer_name)
417 os.makedirs(computer_dir, exist_ok=True)
418 filepath = os.path.join(computer_dir, f"{date_str}.json")
419
420 if os.path.exists(filepath):
[505f39a]421 try:
422 with open(filepath, "r", encoding="utf-8") as f:
423 existing_data = json.load(f)
424 except Exception:
425 existing_data = []
[640ed89]426 else:
427 existing_data = []
428
429 existing_data.append({
[505f39a]430 "timestamp": info.get("timestamp"),
[640ed89]431 "info": info,
[505f39a]432 "processes_count": len(data.get("processes", [])),
433 "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
[640ed89]434 })
435
[505f39a]436 with open(filepath, "w", encoding="utf-8") as f:
[640ed89]437 json.dump(existing_data, f, indent=2, ensure_ascii=False)
438
439
[505f39a]440def cleanup_old_data():
441 """Deletes old rows (30 days) every hour."""
442 while True:
443 time.sleep(3600)
[640ed89]444 try:
[505f39a]445 conn = db()
446 c = conn.cursor()
447 cutoff = (datetime.now() - timedelta(days=30)).isoformat()
[640ed89]448
[505f39a]449 c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff,))
450 c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff,))
451 c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff,))
[d42aac3]452 c.execute("DELETE FROM computer_processes_history WHERE timestamp < ?", (cutoff,))
[640ed89]453
[505f39a]454 conn.commit()
455 conn.close()
456 print(f"[Cleanup] Deleted entries older than {cutoff}")
457 except Exception as e:
458 print("[Cleanup] Error:", e)
[640ed89]459
460
[505f39a]461# ----------------------------
462# Auth endpoints
463# ----------------------------
464@app.route("/api/auth/google", methods=["POST"])
465def auth_google():
466 data = request.get_json(force=True, silent=True) or {}
467 credential = (data.get("credential") or "").strip()
468 if not credential:
469 return jsonify({"error": "Missing credential"}), 400
470 if not GOOGLE_CLIENT_ID:
471 return jsonify({"error": "Server missing GOOGLE_CLIENT_ID"}), 500
[640ed89]472
[505f39a]473 try:
474 idinfo = id_token.verify_oauth2_token(
475 credential,
476 grequests.Request(),
477 GOOGLE_CLIENT_ID,
478 )
479 email = idinfo.get("email")
480 name = idinfo.get("name") or ""
481 picture = idinfo.get("picture") or ""
[640ed89]482
[505f39a]483 if not email:
484 return jsonify({"error": "No email in token"}), 400
[640ed89]485
[505f39a]486 conn = db()
487 c = conn.cursor()
[640ed89]488
[505f39a]489 # upsert user
490 c.execute("SELECT id FROM users WHERE email=?", (email,))
491 row = c.fetchone()
492 if row:
493 user_id = row["id"]
494 c.execute("UPDATE users SET name=?, picture=? WHERE id=?", (name, picture, user_id))
495 else:
496 c.execute(
497 "INSERT INTO users(email, name, picture, created_at) VALUES(?,?,?, datetime('now'))",
498 (email, name, picture),
499 )
500 user_id = c.lastrowid
[640ed89]501
[505f39a]502 # membership -> tenant (create tenant if first time)
503 c.execute("SELECT tenant_id, role FROM memberships WHERE user_id=? LIMIT 1", (user_id,))
504 m = c.fetchone()
[640ed89]505
[505f39a]506 if not m:
507 tenant_name = email.split("@")[0]
508 c.execute(
509 "INSERT INTO tenants(name, owner_email, created_at) VALUES(?,?, datetime('now'))",
510 (tenant_name, email),
511 )
512 tenant_id = c.lastrowid
513 role = "admin"
[640ed89]514
[505f39a]515 c.execute(
516 "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
517 (user_id, tenant_id),
518 )
[640ed89]519
[505f39a]520 # default env for this tenant
521 c.execute(
522 "INSERT OR IGNORE INTO environments(tenant_id, name, created_at) VALUES(?, 'default', datetime('now'))",
523 (tenant_id,),
524 )
525 else:
526 tenant_id = m["tenant_id"]
527 role = m["role"] or "admin"
[640ed89]528
[505f39a]529 conn.commit()
530 conn.close()
[640ed89]531
[505f39a]532 session = make_jwt(
533 {
534 "uid": user_id,
535 "email": email,
536 "name": name,
537 "role": role,
538 "tenant_id": tenant_id,
539 },
540 minutes=60 * 24,
[640ed89]541 )
542
[505f39a]543 resp = jsonify({"ok": True, "user": {"email": email, "name": name, "role": role, "tenant_id": tenant_id}})
544 resp.set_cookie(
545 "session",
546 session,
547 httponly=True,
548 samesite="Lax",
549 secure=COOKIE_SECURE,
550 max_age=60 * 60 * 24,
[640ed89]551 )
[505f39a]552 return resp
[640ed89]553
[505f39a]554 except Exception as e:
555 return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
[640ed89]556
557
[505f39a]558@app.route("/api/me", methods=["GET"])
559@require_user()
560def api_me():
561 return jsonify({"user": request.user})
[640ed89]562
563
[505f39a]564@app.route("/api/auth/logout", methods=["POST"])
565def auth_logout():
566 resp = jsonify({"ok": True})
567 resp.set_cookie("session", "", expires=0)
568 return resp
[640ed89]569
570
[505f39a]571# ----------------------------
572# Admin endpoints
573# ----------------------------
574@app.route("/api/admin/environments", methods=["GET"])
575@require_tenant_admin()
576def admin_list_envs():
577 tenant_id = request.user["tenant_id"]
578 conn = db()
579 c = conn.cursor()
580 c.execute("SELECT name FROM environments WHERE tenant_id=? ORDER BY name", (tenant_id,))
581 envs = [r["name"] for r in c.fetchall()]
582 conn.close()
583 if not envs:
584 envs = ["default"]
585 return jsonify({"environments": envs})
[640ed89]586
587
[505f39a]588@app.route("/api/admin/environments", methods=["POST"])
589@require_tenant_admin()
590def admin_create_env():
591 tenant_id = request.user["tenant_id"]
592 data = request.get_json(force=True, silent=True) or {}
593 name = (data.get("name") or "").strip()
594 if not name:
595 return jsonify({"error": "Missing env name"}), 400
[640ed89]596
[505f39a]597 conn = db()
598 c = conn.cursor()
[640ed89]599 try:
[505f39a]600 c.execute(
601 "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
602 (tenant_id, name),
[640ed89]603 )
[505f39a]604 conn.commit()
[640ed89]605 except Exception as e:
[505f39a]606 conn.close()
607 return jsonify({"error": str(e)}), 400
608 conn.close()
609 return jsonify({"ok": True, "name": name})
[640ed89]610
611
[505f39a]612@app.route("/api/admin/tokens", methods=["POST"])
613@require_tenant_admin()
614def admin_generate_token():
615 tenant_id = request.user["tenant_id"]
616 data = request.get_json(force=True, silent=True) or {}
617 env = (data.get("env") or "").strip()
618 if not env:
619 return jsonify({"error": "Missing env"}), 400
[640ed89]620
[505f39a]621 conn = db()
[640ed89]622 c = conn.cursor()
[505f39a]623 c.execute("SELECT 1 FROM environments WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, env))
624 if not c.fetchone():
625 conn.close()
626 return jsonify({"error": "Environment not found"}), 404
[640ed89]627
[505f39a]628 token = secrets.token_urlsafe(32)
629 c.execute("""
630 INSERT INTO env_tokens(tenant_id, env_name, token, created_at, expires_at)
631 VALUES(?, ?, ?, datetime('now'), datetime('now', '+90 days'))
632 """, (tenant_id, env, token))
633 conn.commit()
634 conn.close()
[640ed89]635
[505f39a]636 return jsonify({"ok": True, "env": env, "token": token})
[640ed89]637
[d42aac3]638@app.route("/api/admin/env-settings/<env_name>", methods=["GET"])
639@require_tenant_admin()
640def admin_get_env_settings(env_name):
641 tenant_id = request.user["tenant_id"]
642
643 conn = db()
644 c = conn.cursor()
645 c.execute("""
646 SELECT save_process_history
647 FROM env_settings
648 WHERE tenant_id=? AND env_name=?
649 LIMIT 1
650 """, (tenant_id, env_name))
651 row = c.fetchone()
652 conn.close()
653
654 return jsonify({
655 "env": env_name,
656 "save_process_history": bool(row["save_process_history"]) if row else False
657 })
658
659@app.route("/api/admin/env-settings/<env_name>", methods=["POST"])
660@require_tenant_admin()
661def admin_set_env_settings(env_name):
662 tenant_id = request.user["tenant_id"]
663 data = request.get_json(force=True, silent=True) or {}
664 enabled = 1 if bool(data.get("save_process_history")) else 0
665
666 conn = db()
667 c = conn.cursor()
668 c.execute("""
669 INSERT INTO env_settings(tenant_id, env_name, save_process_history, created_at, updated_at)
670 VALUES(?, ?, ?, datetime('now'), datetime('now'))
671 ON CONFLICT(tenant_id, env_name) DO UPDATE SET
672 save_process_history=excluded.save_process_history,
673 updated_at=datetime('now')
674 """, (tenant_id, env_name, enabled))
675 conn.commit()
676 conn.close()
677
678 return jsonify({"ok": True, "env": env_name, "save_process_history": bool(enabled)})
[640ed89]679
[505f39a]680# ----------------------------
681# Agent endpoint
682# ----------------------------
683@app.route("/receive", methods=["POST"])
684def receive_data():
685 try:
686 data = request.get_json(force=True, silent=True) or {}
687 if not data:
688 return jsonify({"error": "No data"}), 400
[640ed89]689
[505f39a]690 env_name, tenant_id = get_env_from_token(request)
691 if not env_name or not tenant_id:
692 return jsonify({"error": "Missing or invalid X-Env-Token"}), 401
[640ed89]693
[505f39a]694 info = data.get("info") or {}
695 computer_name = info.get("computer_name")
696 if not computer_name:
697 return jsonify({"error": "Missing info.computer_name"}), 400
[640ed89]698
[505f39a]699 now_iso = datetime.now().isoformat()
700 security_data = data.get("security_data") or {}
[640ed89]701
[505f39a]702 conn = db()
703 c = conn.cursor()
[640ed89]704
[505f39a]705 # Find by (tenant_id + name)
[d42aac3]706 c.execute(
707 "SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1",
708 (tenant_id, computer_name),
709 )
[505f39a]710 row = c.fetchone()
711
712 if row:
713 computer_id = row["id"]
714 c.execute("""
[d42aac3]715 UPDATE computers
716 SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
717 WHERE id=? AND tenant_id=?
718 """, (
[505f39a]719 info.get("user"),
720 info.get("ip_address"),
721 info.get("os"),
722 now_iso,
723 int(info.get("is_sysmon_available", 0) or 0),
724 env_name,
725 computer_id,
726 tenant_id,
727 ))
728 else:
729 c.execute("""
[d42aac3]730 INSERT INTO computers(
731 tenant_id, env_name, name, user, ip, os,
732 first_seen, last_seen, sysmon_available
733 )
734 VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
735 """, (
[505f39a]736 tenant_id,
737 env_name,
738 computer_name,
739 info.get("user"),
740 info.get("ip_address"),
741 info.get("os"),
742 now_iso,
743 now_iso,
744 int(info.get("is_sysmon_available", 0) or 0),
745 ))
746 computer_id = c.lastrowid
[640ed89]747
[505f39a]748 # history row
749 c.execute("""
[d42aac3]750 INSERT INTO computer_history(
751 computer_id, cpu_usage, ram_usage, disk_usage,
752 network_sent_mb, network_recv_mb, timestamp
753 )
754 VALUES(?, ?, ?, ?, ?, ?, ?)
755 """, (
[505f39a]756 computer_id,
757 float(info.get("cpu_usage") or 0),
758 float(info.get("ram_usage") or 0),
759 float(info.get("disk_usage") or 0),
760 float(info.get("network_sent_mb") or 0),
761 float(info.get("network_recv_mb") or 0),
762 info.get("timestamp") or now_iso,
763 ))
764
[d42aac3]765 # ----------------------------
766 # processes: CURRENT (always)
767 # ----------------------------
768 # Clear old current snapshot for this computer
769 c.execute("DELETE FROM computer_processes_current WHERE computer_id=?", (computer_id,))
770
771 # Insert new snapshot
[505f39a]772 for proc in (data.get("processes") or []):
773 c.execute("""
[d42aac3]774 INSERT INTO computer_processes_current(
775 computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp
776 )
777 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
778 """, (
[505f39a]779 computer_id,
780 proc.get("pid"),
781 proc.get("name"),
782 float(proc.get("cpu_percent") or 0),
783 float(proc.get("memory_mb") or 0),
784 proc.get("user") or proc.get("username"),
785 proc.get("cmdline"),
786 info.get("timestamp") or now_iso,
787 ))
788
[d42aac3]789 # ----------------------------
790 # processes: HISTORY (optional)
791 # ----------------------------
792 if is_process_history_enabled(tenant_id, env_name):
793 for proc in (data.get("processes") or []):
794 c.execute("""
795 INSERT INTO computer_processes_history(
796 computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp
797 )
798 VALUES(?, ?, ?, ?, ?, ?, ?, ?)
799 """, (
800 computer_id,
801 proc.get("pid"),
802 proc.get("name"),
803 float(proc.get("cpu_percent") or 0),
804 float(proc.get("memory_mb") or 0),
805 proc.get("user") or proc.get("username"),
806 proc.get("cmdline"),
807 info.get("timestamp") or now_iso,
808 ))
809
[505f39a]810 # sysmon events
811 sysmon_count = 0
812 for ev in (security_data.get("sysmon_events") or []):
813 c.execute("""
[d42aac3]814 INSERT INTO sysmon_events(
815 computer_id, event_id, event_type, message, timestamp, details
816 )
817 VALUES(?, ?, ?, ?, ?, ?)
818 """, (
[505f39a]819 computer_id,
820 ev.get("event_id"),
821 ev.get("event_type", "Unknown"),
822 ev.get("message", ""),
823 ev.get("timestamp") or (info.get("timestamp") or now_iso),
824 json.dumps(ev, ensure_ascii=False),
825 ))
826 sysmon_count += 1
827
828 # network connections
829 for nc in (security_data.get("network_connections") or []):
830 c.execute("""
[d42aac3]831 INSERT INTO network_connections(
832 computer_id, pid, local_address, remote_address, status, process_name, timestamp
833 )
834 VALUES(?, ?, ?, ?, ?, ?, ?)
835 """, (
[505f39a]836 computer_id,
837 nc.get("pid"),
838 nc.get("local_address"),
839 nc.get("remote_address"),
840 nc.get("status"),
841 nc.get("process_name"),
842 info.get("timestamp") or now_iso,
843 ))
[640ed89]844
[505f39a]845 conn.commit()
846 conn.close()
[640ed89]847
[505f39a]848 # optional JSON archive
[640ed89]849 try:
[505f39a]850 save_json_file(data)
851 except Exception:
852 pass
[640ed89]853
[505f39a]854 return jsonify({
855 "ok": True,
856 "computer_id": computer_id,
857 "env": env_name,
858 "tenant_id": tenant_id,
859 "sysmon_events_saved": sysmon_count,
860 "server_time": now_iso,
[d42aac3]861 "process_history_enabled": bool(is_process_history_enabled(tenant_id, env_name)),
[505f39a]862 })
[640ed89]863
[505f39a]864 except Exception as e:
865 traceback.print_exc()
866 return jsonify({"error": str(e)}), 500
[640ed89]867
868
[505f39a]869# ----------------------------
870# Dashboard API (tenant scoped)
871# ----------------------------
872@app.route("/api/computers", methods=["GET"])
873@require_user()
874def api_computers():
875 tenant_id = request.user["tenant_id"]
876 env = request.headers.get("X-Env", "default")
[640ed89]877
[505f39a]878 conn = db()
879 c = conn.cursor()
880 c.execute("""
881 SELECT id, name, user, ip, os, first_seen, last_seen, sysmon_available
882 FROM computers
883 WHERE tenant_id=? AND env_name=?
884 ORDER BY last_seen DESC
885 """, (tenant_id, env))
886 rows = c.fetchall()
[640ed89]887
[505f39a]888 computers = []
889 for r in rows:
890 cid = r["id"]
891
892 c.execute("""
893 SELECT COUNT(*) AS n
894 FROM computer_history
895 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
896 """, (cid,))
897 recent_logs = int(c.fetchone()["n"] or 0)
898
899 c.execute("""
900 SELECT COUNT(*) AS n
901 FROM sysmon_events
902 WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
903 """, (cid,))
904 recent_sysmon = int(c.fetchone()["n"] or 0)
905
906 c.execute("""
907 SELECT cpu_usage, ram_usage, timestamp
908 FROM computer_history
909 WHERE computer_id=?
910 ORDER BY timestamp DESC
911 LIMIT 5
912 """, (cid,))
913 metrics = c.fetchall()
914 avg_cpu = round(sum(m["cpu_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
915 avg_ram = round(sum(m["ram_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
916
917 status = "offline"
918 status_color = "gray"
919 try:
920 last_seen = r["last_seen"]
921 if last_seen:
922 dt = datetime.fromisoformat(
923 last_seen.replace("Z", "+00:00")) if "T" in last_seen else datetime.fromisoformat(last_seen)
924 diff = (datetime.now() - dt).total_seconds()
925 if diff < 60:
926 status, status_color = "online", "green"
927 elif diff < 300:
928 status, status_color = "idle", "orange"
929 except Exception:
930 pass
[640ed89]931
[505f39a]932 computers.append({
933 "name": r["name"],
934 "user": r["user"],
935 "ip": r["ip"],
936 "os": r["os"],
937 "first_seen": r["first_seen"],
938 "last_seen": r["last_seen"],
939 "sysmon_available": bool(r["sysmon_available"]),
940 "recent_logs": recent_logs,
941 "recent_sysmon": recent_sysmon,
942 "avg_cpu": avg_cpu,
943 "avg_ram": avg_ram,
944 "status": status,
945 "status_color": status_color,
946 })
[640ed89]947
[505f39a]948 conn.close()
949 return jsonify(computers)
[640ed89]950
951
[505f39a]952@app.route("/api/stats", methods=["GET"])
953@require_user()
954def api_stats():
955 tenant_id = request.user["tenant_id"]
[640ed89]956
[505f39a]957 conn = db()
[640ed89]958 c = conn.cursor()
959
[505f39a]960 c.execute("""
961 SELECT COUNT(*) AS n
962 FROM sysmon_events s
963 JOIN computers c2 ON c2.id = s.computer_id
964 WHERE s.timestamp > datetime('now','-24 hours')
965 AND c2.tenant_id = ?
966 """, (tenant_id,))
967 total_sysmon = int(c.fetchone()["n"] or 0)
[640ed89]968
[505f39a]969 c.execute("""
970 SELECT COUNT(*) AS n
971 FROM network_connections n
972 JOIN computers c2 ON c2.id = n.computer_id
973 WHERE n.timestamp > datetime('now','-24 hours')
974 AND c2.tenant_id = ?
975 """, (tenant_id,))
976 total_net = int(c.fetchone()["n"] or 0)
[640ed89]977
978 conn.close()
979 return jsonify({
[505f39a]980 "total_sysmon_events": total_sysmon,
981 "total_network_connections": total_net,
982 "server_time": datetime.now().isoformat(),
[640ed89]983 })
984
985
[505f39a]986@app.route("/api/computer/<computer_name>", methods=["GET"])
987@require_user()
988def api_computer_details(computer_name):
989 tenant_id = request.user["tenant_id"]
[640ed89]990
[505f39a]991 conn = db()
992 c = conn.cursor()
993 c.execute("""
994 SELECT id, name, user, ip, os, first_seen, last_seen, env_name
995 FROM computers
996 WHERE tenant_id=? AND name=?
997 LIMIT 1
998 """, (tenant_id, computer_name))
999 comp = c.fetchone()
1000 if not comp:
[640ed89]1001 conn.close()
1002 return jsonify({"error": "Computer not found"}), 404
1003
[505f39a]1004 computer_id = comp["id"]
1005
1006 c.execute("SELECT COUNT(*) AS n FROM computer_history WHERE computer_id=?", (computer_id,))
1007 total_logs = int(c.fetchone()["n"] or 0)
1008
1009 c.execute("""
1010 SELECT cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp
1011 FROM computer_history
1012 WHERE computer_id=?
1013 ORDER BY timestamp DESC
1014 LIMIT 100
1015 """, (computer_id,))
1016 history = [dict(r) for r in c.fetchall()]
1017 current_metrics = history[0] if history else {}
1018
1019 c.execute("""
[d42aac3]1020 SELECT pid, name, username, cpu_percent, memory_mb, cmdline, timestamp
1021 FROM computer_processes_current
1022 WHERE computer_id=?
1023 ORDER BY timestamp DESC
1024 LIMIT 200
1025 """, (computer_id,))
[505f39a]1026 processes = [dict(r) for r in c.fetchall()]
1027
1028 c.execute("""
1029 SELECT event_id, event_type, message, timestamp, details
1030 FROM sysmon_events
1031 WHERE computer_id=?
1032 ORDER BY timestamp DESC
1033 LIMIT 200
1034 """, (computer_id,))
1035 sysmon = []
1036 for r in c.fetchall():
1037 d = {}
1038 try:
1039 d = json.loads(r["details"]) if r["details"] else {}
1040 except Exception:
1041 d = {}
1042 sysmon.append({
1043 "event_id": r["event_id"],
1044 "event_type": r["event_type"],
1045 "message": r["message"],
1046 "timestamp": r["timestamp"],
1047 "details": d,
[640ed89]1048 })
1049
[505f39a]1050 c.execute("""
1051 SELECT pid, process_name, local_address, remote_address, status, timestamp
1052 FROM network_connections
1053 WHERE computer_id=?
1054 ORDER BY timestamp DESC
1055 LIMIT 100
1056 """, (computer_id,))
1057 net = [dict(r) for r in c.fetchall()]
1058
[640ed89]1059 conn.close()
1060
1061 return jsonify({
[505f39a]1062 "computer": {
1063 "id": comp["id"],
1064 "name": comp["name"],
1065 "user": comp["user"],
1066 "ip": comp["ip"],
1067 "os": comp["os"],
1068 "first_seen": comp["first_seen"],
1069 "last_seen": comp["last_seen"],
1070 "env_name": comp["env_name"],
1071 "total_logs": total_logs,
1072 },
1073 "history": history,
1074 "current_metrics": current_metrics,
1075 "recent_processes": processes,
1076 "sysmon_events": sysmon,
1077 "network_connections": net,
[640ed89]1078 })
1079
1080
[505f39a]1081@app.route("/api/export/<computer_name>", methods=["GET"])
1082@require_user()
1083def api_export(computer_name):
1084 tenant_id = request.user["tenant_id"]
[640ed89]1085
[505f39a]1086 conn = db()
1087 c = conn.cursor()
1088 c.execute("""
1089 SELECT *
1090 FROM computers
1091 WHERE tenant_id=? AND name=?
1092 LIMIT 1
1093 """, (tenant_id, computer_name))
1094 comp = c.fetchone()
1095 if not comp:
[640ed89]1096 conn.close()
1097 return jsonify({"error": "Computer not found"}), 404
1098
[505f39a]1099 computer_id = comp["id"]
1100 out = {"computer": dict(comp)}
[640ed89]1101
[505f39a]1102 c.execute("SELECT * FROM computer_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1103 out["history"] = [dict(r) for r in c.fetchall()]
[640ed89]1104
[505f39a]1105 c.execute("SELECT * FROM sysmon_events WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1106 out["sysmon_events"] = [dict(r) for r in c.fetchall()]
[640ed89]1107
[d42aac3]1108 c.execute("SELECT * FROM computer_processes_current WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1109 out["processes_current"] = [dict(r) for r in c.fetchall()]
1110
1111 c.execute("SELECT * FROM computer_processes_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1112 out["processes_history"] = [dict(r) for r in c.fetchall()]
[640ed89]1113
[505f39a]1114 c.execute("SELECT * FROM network_connections WHERE computer_id=? ORDER BY timestamp", (computer_id,))
1115 out["network_connections"] = [dict(r) for r in c.fetchall()]
[640ed89]1116
1117 conn.close()
1118
[505f39a]1119 return Response(
1120 json.dumps(out, indent=2, default=str, ensure_ascii=False),
1121 mimetype="application/json",
1122 headers={"Content-Disposition": f"attachment; filename={computer_name}_export.json"},
1123 )
[2058e5c]1124
1125
[505f39a]1126# ----------------------------
1127# Simple RAG chat
1128# ----------------------------
[e4c61dd]1129# def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10):
1130# conn = db()
1131# c = conn.cursor()
1132#
1133# params = [tenant_id]
1134# comp_clause = ""
1135# if computer_name:
1136# comp_clause = "AND c2.name = ?"
1137# params.append(computer_name)
1138#
1139# # sysmon
1140# c.execute(f"""
1141# SELECT s.timestamp, s.event_id, s.event_type, s.message
1142# FROM sysmon_events s
1143# JOIN computers c2 ON c2.id = s.computer_id
1144# WHERE c2.tenant_id = ? {comp_clause}
1145# ORDER BY s.timestamp DESC
1146# LIMIT ?
1147# """, (*params, limit_per_table))
1148# sysmon_rows = c.fetchall()
1149#
1150# # perf
1151# c.execute(f"""
1152# SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb
1153# FROM computer_history h
1154# JOIN computers c2 ON c2.id = h.computer_id
1155# WHERE c2.tenant_id = ? {comp_clause}
1156# ORDER BY h.timestamp DESC
1157# LIMIT ?
1158# """, (*params, limit_per_table))
1159# perf_rows = c.fetchall()
1160#
1161# # proc
1162# c.execute(f"""
1163# SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb
1164# FROM computer_processes_current p
1165# JOIN computers c2 ON c2.id = p.computer_id
1166# WHERE c2.tenant_id = ? {comp_clause}
1167# ORDER BY p.timestamp DESC
1168# LIMIT ?
1169# """, (*params, limit_per_table))
1170# proc_rows = c.fetchall()
1171#
1172# conn.close()
1173#
1174# lines = []
1175# for r in sysmon_rows:
1176# lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}")
1177# for r in perf_rows:
1178# lines.append(
1179# f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | "
1180# f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB"
1181# )
1182# for r in proc_rows:
1183# lines.append(
1184# f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB"
1185# )
1186#
1187# return "\n".join(lines)
1188def ask_llm_with_context(question, context):
1189 if not client:
1190 return "Немаш поставено OPENAI_API_KEY на серверот."
1191
1192 prompt = f"""Ти си асистент за безбедност и систем мониторинг.
1193Одговарај базирано ИСКЛУЧИВО на дадените логови.
1194Ако нема доволно податоци, кажи: 'Нема доволно информации во логовите.'.
1195
1196Прашање: {question}
1197
1198Логови:
1199{context or 'Нема логови.'}
1200"""
1201
1202 resp = client.responses.create(
1203 model="gpt-4.1-mini",
1204 input=[{"role": "user", "content": [{"type": "input_text", "text": prompt}]}],
1205 )
1206
1207 text = getattr(resp, "output_text", "") or ""
1208 return text.strip() or "Нема доволно информации во логовите."
1209
1210def require_grafana():
1211 def deco(fn):
1212 @wraps(fn)
1213 def wrap(*args, **kwargs):
1214 tok = request.headers.get("X-Grafana-Token", "")
1215 if not GRAFANA_TOKEN or tok != GRAFANA_TOKEN:
1216 return jsonify({"error": "Unauthorized"}), 401
1217 return fn(*args, **kwargs)
1218 return wrap
1219 return deco
1220@app.route("/api/public/stats", methods=["GET"])
1221@require_grafana()
1222def grafana_stats():
1223 # ако сакаш само една околина:
1224 env = request.args.get("env", "default")
1225
[505f39a]1226 conn = db()
1227 c = conn.cursor()
[2058e5c]1228
[e4c61dd]1229 # вкупно sysmon во 24h (за сите tenants, или ако сакаш само 1 tenant додади филтер)
1230 c.execute("""
1231 SELECT COUNT(*) AS n
1232 FROM sysmon_events s
1233 JOIN computers c2 ON c2.id = s.computer_id
1234 WHERE s.timestamp > datetime('now','-24 hours')
1235 AND c2.env_name = ?
1236 """, (env,))
1237 total_sysmon = int(c.fetchone()["n"] or 0)
1238
1239 c.execute("""
1240 SELECT COUNT(*) AS n
1241 FROM network_connections n
1242 JOIN computers c2 ON c2.id = n.computer_id
1243 WHERE n.timestamp > datetime('now','-24 hours')
1244 AND c2.env_name = ?
1245 """, (env,))
1246 total_net = int(c.fetchone()["n"] or 0)
1247
1248 conn.close()
1249 return jsonify({
1250 "total_sysmon_events": total_sysmon,
1251 "total_network_connections": total_net,
1252 "server_time": datetime.now().isoformat(),
1253 "env": env,
1254 })
1255
1256def require_grafana_token(fn):
1257 @wraps(fn)
1258 def wrapper(*args, **kwargs):
1259 expected = os.environ.get("GRAFANA_API_TOKEN") or ""
1260
1261 # 1) пробај Authorization: Bearer <token>
1262 auth = request.headers.get("Authorization", "")
1263 token = ""
1264 if auth.startswith("Bearer "):
1265 token = auth.replace("Bearer ", "").strip()
1266
1267 # 2) ако нема header, пробај query param ?token=
1268 if not token:
1269 token = (request.args.get("token") or "").strip()
1270
1271 if not expected or token != expected:
1272 return jsonify({"error": "Invalid Grafana token"}), 401
1273
1274 return fn(*args, **kwargs)
1275 return wrapper
1276
1277@app.route("/api/public/ips", methods=["GET"])
1278@require_grafana_token
1279def api_public_ips():
1280 tenant_id = request.args.get("tenant_id", type=int)
1281 env = request.args.get("env", "default")
1282 hours = int(request.args.get("hours", "24"))
1283 limit = int(request.args.get("limit", "200"))
1284
1285 if not tenant_id:
1286 return jsonify({"error": "Missing tenant_id"}), 400
1287
1288 conn = db()
1289 c = conn.cursor()
1290
1291 c.execute("""
1292 SELECT DISTINCT
1293 substr(nc.remote_address, 1,
1294 CASE
1295 WHEN instr(nc.remote_address, ':') > 0
1296 THEN instr(nc.remote_address, ':') - 1
1297 ELSE length(nc.remote_address)
1298 END
1299 ) AS ip
1300 FROM network_connections nc
1301 JOIN computers c2 ON c2.id = nc.computer_id
1302 WHERE c2.tenant_id = ?
1303 AND c2.env_name = ?
1304 AND nc.timestamp > datetime('now', ?)
1305 AND nc.remote_address IS NOT NULL
1306 AND nc.remote_address != ''
1307 ORDER BY ip
1308 LIMIT ?
1309 """, (tenant_id, env, f"-{hours} hours", limit))
1310
1311 ips = [r["ip"] for r in c.fetchall()]
1312 conn.close()
1313
1314 return jsonify({"ips": ips})
1315
1316
1317
1318@app.route("/api/public/sankey", methods=["GET"])
1319@require_grafana_token
1320def api_public_sankey():
1321 tenant_id = int(request.args.get("tenant_id", "1"))
1322 env = request.args.get("env", "office1")
1323 hours = int(request.args.get("hours", "24"))
1324 limit = int(request.args.get("limit", "20"))
1325
1326 computers = (request.args.get("computers") or "all").strip()
1327 ips = (request.args.get("ips") or "all").strip()
1328
1329 # normalize grafana All values
1330 if computers in ("", "__all"):
1331 computers = "all"
1332 if ips in ("", "__all"):
1333 ips = "all"
1334
1335 comp_list = [c.strip() for c in computers.split(",") if c.strip()] if computers != "all" else []
1336 ip_list = [i.strip() for i in ips.split(",") if i.strip()] if ips != "all" else []
1337
[505f39a]1338 comp_clause = ""
[e4c61dd]1339 ip_clause = ""
1340 params = [tenant_id, env, f"-{hours} hours"]
1341
1342 # ✅ target без port за IPv4: "1.2.3.4:443" -> "1.2.3.4"
1343 # ⚠️ за IPv6 ќе го остави како што е (за да не го расипеме со split по ':')
1344 target_expr = """
1345 CASE
1346 WHEN nc.remote_address LIKE '%:%' AND nc.remote_address NOT LIKE '%:%:%'
1347 THEN substr(nc.remote_address, 1, instr(nc.remote_address, ':') - 1)
1348 ELSE nc.remote_address
1349 END
1350 """
1351
1352 if comp_list:
1353 comp_clause = "AND comp.name IN (" + ",".join(["?"] * len(comp_list)) + ")"
1354 params.extend(comp_list)
1355
1356 # ✅ Филтер по IP без порт (истиот expr)
1357 if ip_list:
1358 ip_clause = f"AND ({target_expr}) IN (" + ",".join(["?"] * len(ip_list)) + ")"
1359 params.extend(ip_list)
1360
1361 # LIMIT на крај
1362 params.append(limit)
1363
1364 conn = db()
1365 c = conn.cursor()
[505f39a]1366
1367 c.execute(f"""
[e4c61dd]1368 SELECT
1369 comp.name AS source,
1370 ({target_expr}) AS target,
1371 COUNT(*) AS value
1372 FROM network_connections nc
1373 JOIN computers comp ON comp.id = nc.computer_id
1374 WHERE comp.tenant_id = ?
1375 AND comp.env_name = ?
1376 AND nc.timestamp > datetime('now', ?)
1377 AND nc.remote_address IS NOT NULL
1378 AND nc.remote_address != ''
1379
1380 -- ✅ тргни loopback spam (овие прават да изгледа како да нема линии)
1381 AND nc.remote_address NOT LIKE '127.0.0.1%'
1382 AND nc.remote_address NOT LIKE '::1%'
1383
1384 {comp_clause}
1385 {ip_clause}
1386 GROUP BY comp.name, target
1387 ORDER BY value DESC
1388 LIMIT ?
1389 """, params)
1390
1391 rows = [dict(r) for r in c.fetchall()]
1392 conn.close()
1393 return jsonify(rows)
1394
1395from urllib.parse import urlparse
1396
1397def _ip_only(addr: str):
1398 # addr може да е "1.2.3.4:443" или "hostname:port"
1399 if not addr:
1400 return None
1401 # ако има повеќе ':' (ipv6), ќе го оставиме целото; за ipv4 split е ок
1402 if addr.count(":") == 1:
1403 return addr.split(":")[0]
1404 return addr
1405
1406@app.route("/api/graph/net", methods=["GET"])
1407@require_user()
1408def api_graph_net():
1409 tenant_id = request.user["tenant_id"]
1410 env = request.args.get("env") or request.headers.get("X-Env", "default")
1411 computer = request.args.get("computer") # optional
1412 since_hours = int(request.args.get("since_hours") or 24)
1413
1414 conn = db()
1415 c = conn.cursor()
[505f39a]1416
[e4c61dd]1417 params = [tenant_id, env, since_hours]
1418 comp_clause = ""
1419 if computer:
1420 comp_clause = "AND c2.name = ?"
1421 params.append(computer)
1422
1423 # Вади top remote IP по број на конекции
[505f39a]1424 c.execute(f"""
[e4c61dd]1425 SELECT
1426 c2.name AS computer_name,
1427 n.remote_address AS remote_address,
1428 COUNT(*) AS cnt
1429 FROM network_connections n
1430 JOIN computers c2 ON c2.id = n.computer_id
1431 WHERE c2.tenant_id = ?
1432 AND c2.env_name = ?
1433 AND n.timestamp > datetime('now', '-' || ? || ' hours')
1434 {comp_clause}
1435 AND n.remote_address IS NOT NULL
1436 AND n.remote_address <> ''
1437 GROUP BY c2.name, n.remote_address
1438 ORDER BY cnt DESC
1439 LIMIT 200
1440 """, params)
1441
1442 rows = c.fetchall()
1443 conn.close()
1444
1445 # Build NodeGraph-like structure
1446 nodes = {}
1447 edges = []
1448
1449 def add_node(node_id, title, ntype):
1450 if node_id not in nodes:
1451 nodes[node_id] = {"id": node_id, "title": title, "subTitle": ntype}
1452
1453 for r in rows:
1454 comp_name = r["computer_name"]
1455 remote = _ip_only(r["remote_address"]) or r["remote_address"]
1456 cnt = int(r["cnt"] or 0)
1457
1458 comp_id = f"comp:{comp_name}"
1459 ip_id = f"ip:{remote}"
1460
1461 add_node(comp_id, comp_name, "computer")
1462 add_node(ip_id, remote, "remote_ip")
1463
1464 edges.append({
1465 "id": f"{comp_id}->{ip_id}",
1466 "source": comp_id,
1467 "target": ip_id,
1468 "mainStat": str(cnt),
1469 })
1470
1471 return jsonify({
1472 "nodes": list(nodes.values()),
1473 "edges": edges,
1474 "meta": {"env": env, "computer": computer, "since_hours": since_hours}
1475 })
1476
1477@app.route("/api/public/netgraph", methods=["GET"])
1478@require_grafana_token
1479def api_public_netgraph():
1480 tenant_id = int(request.args.get("tenant_id", "1"))
1481 env = request.args.get("env", "default")
1482 hours = int(request.args.get("hours", "6"))
1483 limit_edges = int(request.args.get("limit", "200"))
1484 computer = request.args.get("computer") # <-- NEW (optional)
1485
1486 conn = db()
1487 c = conn.cursor()
1488
1489 comp_clause = ""
1490 params = [tenant_id, env, f"-{hours} hours"]
1491
1492 if computer and computer != "all":
1493 comp_clause = "AND comp.name = ?"
1494 params.append(computer)
1495
1496 params.append(limit_edges)
1497
[505f39a]1498 c.execute(f"""
[e4c61dd]1499 SELECT
1500 comp.name AS src,
1501 nc.remote_address AS remote,
1502 COUNT(*) AS cnt
1503 FROM network_connections nc
1504 JOIN computers comp ON comp.id = nc.computer_id
1505 WHERE comp.tenant_id = ?
1506 AND comp.env_name = ?
1507 AND nc.timestamp > datetime('now', ?)
1508 {comp_clause}
1509 AND nc.remote_address IS NOT NULL
1510 AND nc.remote_address != ''
1511 GROUP BY comp.name, nc.remote_address
1512 ORDER BY cnt DESC
1513 LIMIT ?
1514 """, params)
[2058e5c]1515
[e4c61dd]1516 rows = c.fetchall()
[2058e5c]1517 conn.close()
1518
[e4c61dd]1519 def ip_only(addr: str) -> str:
1520 if not addr:
1521 return ""
1522 if addr.startswith("[") and "]" in addr:
1523 return addr[1:addr.index("]")]
1524 if ":" in addr:
1525 return addr.split(":")[0]
1526 return addr
[2058e5c]1527
[e4c61dd]1528 nodes = {}
1529 edges = []
[2058e5c]1530
[e4c61dd]1531 for r in rows:
1532 src = r["src"]
1533 remote_ip = ip_only(r["remote"])
1534 cnt = int(r["cnt"] or 0)
1535 if not remote_ip:
1536 continue
1537
1538 nodes[src] = {"id": src, "title": src, "subTitle": env, "mainStat": "PC"}
1539 ip_id = f"IP:{remote_ip}"
1540 if ip_id not in nodes:
1541 nodes[ip_id] = {"id": ip_id, "title": remote_ip, "subTitle": "remote", "mainStat": "IP"}
1542
1543 edges.append({
1544 "id": f"{src}->{ip_id}",
1545 "source": src,
1546 "target": ip_id,
1547 "mainStat": cnt
1548 })
[2058e5c]1549
[e4c61dd]1550 return jsonify({"nodes": list(nodes.values()), "edges": edges})
1551@app.route("/api/public/computers", methods=["GET"])
1552@require_grafana_token
1553def api_public_computers():
1554 tenant_id = int(request.args.get("tenant_id", "1"))
1555 env = request.args.get("env", "default")
[2058e5c]1556
[e4c61dd]1557 conn = db()
1558 c = conn.cursor()
1559 c.execute("""
1560 SELECT name
1561 FROM computers
1562 WHERE tenant_id = ? AND env_name = ?
1563 ORDER BY name
1564 """, (tenant_id, env))
1565 names = [r["name"] for r in c.fetchall()]
1566 conn.close()
1567
1568 return jsonify({"computers": names})
1569
1570@app.route("/api/public/stats", methods=["GET"])
1571@require_grafana_token
1572def public_stats():
1573 # ⚠️ ако сакаш tenant по tenant, ќе мора да додадеш tenant_id како query param,
1574 # или да имаш 1 tenant во проект за сега.
1575 tenant_id = request.args.get("tenant_id", type=int)
1576 if not tenant_id:
1577 return jsonify({"error": "Missing tenant_id"}), 400
1578
1579 conn = db()
1580 c = conn.cursor()
1581
1582 c.execute("""
1583 SELECT COUNT(*) AS n
1584 FROM sysmon_events s
1585 JOIN computers c2 ON c2.id = s.computer_id
1586 WHERE s.timestamp > datetime('now','-24 hours')
1587 AND c2.tenant_id = ?
1588 """, (tenant_id,))
1589 total_sysmon = int(c.fetchone()["n"] or 0)
1590
1591 c.execute("""
1592 SELECT COUNT(*) AS n
1593 FROM network_connections n
1594 JOIN computers c2 ON c2.id = n.computer_id
1595 WHERE n.timestamp > datetime('now','-24 hours')
1596 AND c2.tenant_id = ?
1597 """, (tenant_id,))
1598 total_net = int(c.fetchone()["n"] or 0)
1599
1600 conn.close()
1601
1602 return jsonify({
1603 "total_sysmon_events": total_sysmon,
1604 "total_network_connections": total_net,
1605 "server_time": datetime.now().isoformat(),
1606 })
1607
1608
1609def ask_llm_sql_generator(question: str, tenant_id: int, env: str, computer_name: str | None):
1610 """
1611 Returns dict: { "queries": [...], "notes": "...", "scope": {...} }
1612 Only SELECT/WITH queries, no modifications.
1613 """
1614 if not client:
1615 return {
1616 "queries": [],
1617 "notes": "Немаш поставено OPENAI_API_KEY на серверот.",
1618 "scope": {"tenant_id": tenant_id, "env": env, "computer_name": computer_name},
1619 }
1620
1621 schema = """
1622SQLite schema (важни табели):
1623- computers(id, tenant_id, env_name, name, user, ip, os, first_seen, last_seen, sysmon_available)
1624- computer_history(computer_id, cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp)
1625- computer_processes_current(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp)
1626- computer_processes_history(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp)
1627- sysmon_events(computer_id, event_id, event_type, message, timestamp, details)
1628- network_connections(computer_id, pid, local_address, remote_address, status, process_name, timestamp)
1629
1630Правила:
1631- Врати САМО валиден JSON.
1632- JSON формат: {"queries":[...], "notes":"..."}.
1633- queries мора да бидат само SELECT или WITH (никако INSERT/UPDATE/DELETE/ALTER/DROP).
1634- максимум 3 queries.
1635- користи placeholders: :tenant_id, :env, :computer_name (ако е дадено).
1636- ако нема доволно инфо -> queries празно и notes објаснува.
1637""".strip()
1638
1639 user_scope = {
1640 "tenant_id": tenant_id,
1641 "env": env,
1642 "computer_name": computer_name,
1643 }
1644
1645 user_msg = f"""
1646Прашање од корисник: {question}
1647
1648Scope (вметни во WHERE):
1649- tenant_id = :tenant_id
1650- env = :env
1651- computer_name = :computer_name (ако не е null)
1652
1653Врати 1-3 SELECT/WITH queries за да се добијат потребните податоци.
1654""".strip()
[2058e5c]1655
[505f39a]1656 completion = client.chat.completions.create(
1657 model="gpt-4.1-mini",
1658 messages=[
[e4c61dd]1659 {"role": "system", "content": schema},
1660 {"role": "user", "content": user_msg},
[505f39a]1661 ],
[e4c61dd]1662 temperature=0.1,
[505f39a]1663 )
[e4c61dd]1664
1665 raw = completion.choices[0].message.content or ""
1666
1667 # Safe parse JSON
1668 try:
1669 out = json.loads(raw)
1670 if not isinstance(out, dict):
1671 raise ValueError("Not an object")
1672 queries = out.get("queries") or []
1673 notes = out.get("notes") or ""
1674
1675 # hard safety filter: keep only SELECT/WITH
1676 safe_queries = []
1677 for q in queries:
1678 if not isinstance(q, str):
1679 continue
1680 qs = q.strip().lower()
1681 if qs.startswith("select") or qs.startswith("with"):
1682 # block obvious multi-statement
1683 if ";" in q.strip().rstrip(";"):
1684 continue
1685 safe_queries.append(q.strip())
1686
1687 return {"queries": safe_queries[:3], "notes": notes, "scope": user_scope}
1688
1689 except Exception:
1690 # fallback ако AI не врати JSON
1691 return {
1692 "queries": [],
1693 "notes": "AI не врати валиден JSON. Обиди се повторно (или намали прашање).",
1694 "scope": user_scope,
1695 "raw": raw[:1200],
1696 }
[2058e5c]1697
1698
[505f39a]1699@app.route("/api/chat", methods=["POST"])
1700@require_user()
1701def api_chat():
1702 try:
1703 tenant_id = request.user["tenant_id"]
[e4c61dd]1704 env = request.headers.get("X-Env", "default") # важно!
[505f39a]1705 data = request.get_json(force=True, silent=True) or {}
[e4c61dd]1706
[505f39a]1707 question = (data.get("question") or "").strip()
1708 computer_name = data.get("computer_name") or None
[2058e5c]1709
[505f39a]1710 if not question:
1711 return jsonify({"error": "No question provided"}), 400
[2058e5c]1712
[e4c61dd]1713 out = ask_llm_sql_generator(
1714 question=question,
1715 tenant_id=tenant_id,
1716 env=env,
1717 computer_name=computer_name,
1718 )
1719
1720 # Тука не извршуваме SQL, само враќаме query за копирање.
1721 return jsonify({
1722 "ok": True,
1723 "queries": out.get("queries", []),
1724 "notes": out.get("notes", ""),
1725 "scope": out.get("scope", {}),
1726 # "raw": out.get("raw") # ако сакаш debug
1727 }), 200
[2058e5c]1728
[505f39a]1729 except Exception as e:
1730 traceback.print_exc()
1731 return jsonify({"error": str(e)}), 500
[2058e5c]1732
1733
[e4c61dd]1734
[505f39a]1735# ----------------------------
1736# Misc
1737# ----------------------------
1738@app.route("/ping")
1739def ping():
1740 return jsonify({
1741 "status": "alive",
1742 "server_time": datetime.now().isoformat(),
1743 "database": DB_FILE,
1744 "cors_origins": ALLOWED_ORIGINS,
1745 })
[2058e5c]1746
1747
[505f39a]1748@app.route("/")
1749def root():
1750 return jsonify({
1751 "ok": True,
1752 "service": "netIntel server",
1753 "hint": "frontend should call /api/* endpoints",
1754 })
1755
[2058e5c]1756
[505f39a]1757if __name__ == "__main__":
1758 init_db()
[640ed89]1759
1760 cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
1761 cleanup_thread.start()
1762
[d42aac3]1763 print(" netIntel server running on 0.0.0.0:5555")
[505f39a]1764 # debug=False recommended; if you need debug, set True temporarily
1765 app.run(host="0.0.0.0", port=5555, debug=False, threaded=True)
Note: See TracBrowser for help on using the repository browser.