Changeset 505f39a for server.py


Ignore:
Timestamp:
01/21/26 13:35:44 (6 months ago)
Author:
istevanoska <ilinastevanoska@…>
Branches:
master
Children:
d42aac3
Parents:
2058e5c
Message:

Added OAuth/prototype

File:
1 edited

Legend:

Unmodified
Added
Removed
  • server.py

    r2058e5c r505f39a  
    11#!/usr/bin/env python3
    22"""
    3 POPRAVEN SERVER со Sysmon обработка
     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
    423"""
     24
     25import os
     26import json
     27import time
     28import sqlite3
    529import traceback
    6 
    7 from flask import Flask, request, jsonify, render_template_string
    8 import json
    9 import sqlite3
     30import threading
     31import secrets
    1032from datetime import datetime, timedelta
    11 import time
    12 import os
    13 import socket
    14 from collections import defaultdict
    15 import threading
    16 import requests
    17 from openai import OpenAI
    18 from openai import OpenAI
     33from functools import wraps
     34
     35from flask import Flask, request, jsonify, Response
    1936from dotenv import load_dotenv
     37
     38import jwt
     39from google.oauth2 import id_token
     40from google.auth.transport import requests as grequests
     41
     42# Optional OpenAI
     43try:
     44    from openai import OpenAI
     45except Exception:
     46    OpenAI = None
     47
     48from flask_cors import CORS
     49
    2050load_dotenv()
    2151
    22 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
    23 client = OpenAI(
    24     api_key=OPENAI_API_KEY,
    25     timeout=15  # максимум 15 секунди да чека
    26 ) if OPENAI_API_KEY else None
    27 
    28 
     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# ----------------------------
    2977app = Flask(__name__)
    30 
    31 DB_FILE = "lan_logs_sysmon.db"
    32 LOG_DIR = "received_data_sysmon"
    33 
    34 import secrets
    35 
    36 ADMIN_KEY = os.environ.get("ADMIN_KEY", "")
    37 ADMIN_KEY = os.environ.get("ADMIN_KEY", "")
    38 
    39 
    40 def init_admin_tables():
     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():
    4198    conn = sqlite3.connect(DB_FILE)
     99    conn.row_factory = sqlite3.Row
     100    return conn
     101
     102
     103def init_db():
     104    """Create tables if missing + light migrations."""
     105    conn = db()
    42106    c = conn.cursor()
    43107
    44     c.execute('''CREATE TABLE IF NOT EXISTS environments(
    45         id INTEGER PRIMARY KEY AUTOINCREMENT,
    46         name TEXT UNIQUE,
    47         created_at TEXT
    48     )''')
    49 
    50     c.execute('''CREATE TABLE IF NOT EXISTS env_tokens(
    51         id INTEGER PRIMARY KEY AUTOINCREMENT,
    52         env_name TEXT,
    53         token TEXT UNIQUE,
    54         created_at TEXT,
    55         expires_at TEXT
    56     )''')
    57 
    58     # migration ако табелата веќе постои
    59     c.execute("PRAGMA table_info(env_tokens)")
    60     cols = [r[1] for r in c.fetchall()]
    61     if "expires_at" not in cols:
    62         c.execute("ALTER TABLE env_tokens ADD COLUMN expires_at TEXT")
    63 
    64     c.execute('''CREATE TABLE IF NOT EXISTS admin_sessions(
    65         id INTEGER PRIMARY KEY AUTOINCREMENT,
    66         token TEXT UNIQUE,
    67         created_at TEXT
    68     )''')
    69 
    70     # default env
    71     c.execute("INSERT OR IGNORE INTO environments(name, created_at) VALUES('default', datetime('now'))")
     108    c.execute("PRAGMA foreign_keys = ON;")
     109
     110    # Tenants / users / memberships
     111    c.execute("""
     112        CREATE TABLE IF NOT EXISTS tenants(
     113            id INTEGER PRIMARY KEY AUTOINCREMENT,
     114            name TEXT NOT NULL,
     115            owner_email TEXT NOT NULL,
     116            created_at TEXT
     117        )
     118        """)
     119
     120    c.execute("""
     121        CREATE TABLE IF NOT EXISTS users(
     122            id INTEGER PRIMARY KEY AUTOINCREMENT,
     123            email TEXT UNIQUE NOT NULL,
     124            name TEXT,
     125            picture TEXT,
     126            created_at TEXT
     127        )
     128        """)
     129
     130    c.execute("""
     131        CREATE TABLE IF NOT EXISTS memberships(
     132            user_id INTEGER NOT NULL,
     133            tenant_id INTEGER NOT NULL,
     134            role TEXT DEFAULT 'admin',
     135            created_at TEXT,
     136            PRIMARY KEY(user_id, tenant_id),
     137            FOREIGN KEY(user_id) REFERENCES users(id),
     138            FOREIGN KEY(tenant_id) REFERENCES tenants(id)
     139        )
     140        """)
     141
     142    # Environments (unique per tenant)
     143    c.execute("""
     144        CREATE TABLE IF NOT EXISTS environments(
     145            id INTEGER PRIMARY KEY AUTOINCREMENT,
     146            tenant_id INTEGER NOT NULL,
     147            name TEXT NOT NULL,
     148            created_at TEXT,
     149            UNIQUE(tenant_id, name),
     150            FOREIGN KEY(tenant_id) REFERENCES tenants(id)
     151        )
     152        """)
     153
     154    # Env tokens (agent tokens)
     155    c.execute("""
     156        CREATE TABLE IF NOT EXISTS env_tokens(
     157            id INTEGER PRIMARY KEY AUTOINCREMENT,
     158            tenant_id INTEGER NOT NULL,
     159            env_name TEXT NOT NULL,
     160            token TEXT UNIQUE NOT NULL,
     161            created_at TEXT,
     162            expires_at TEXT,
     163            FOREIGN KEY(tenant_id) REFERENCES tenants(id)
     164        )
     165        """)
     166
     167    # Computers
     168    c.execute("""
     169        CREATE TABLE IF NOT EXISTS computers(
     170            id INTEGER PRIMARY KEY AUTOINCREMENT,
     171            tenant_id INTEGER NOT NULL,
     172            env_name TEXT DEFAULT 'default',
     173            name TEXT NOT NULL,
     174            user TEXT,
     175            ip TEXT,
     176            os TEXT,
     177            first_seen TEXT,
     178            last_seen TEXT,
     179            sysmon_available INTEGER DEFAULT 0,
     180            UNIQUE(tenant_id, name)
     181        )
     182        """)
     183
     184    # History
     185    c.execute("""
     186        CREATE TABLE IF NOT EXISTS computer_history(
     187            id INTEGER PRIMARY KEY AUTOINCREMENT,
     188            computer_id INTEGER,
     189            cpu_usage REAL,
     190            ram_usage REAL,
     191            disk_usage REAL,
     192            network_sent_mb REAL,
     193            network_recv_mb REAL,
     194            timestamp TEXT,
     195            FOREIGN KEY(computer_id) REFERENCES computers(id)
     196        )
     197        """)
     198
     199    # Processes
     200    c.execute("""
     201        CREATE TABLE IF NOT EXISTS computer_processes(
     202            id INTEGER PRIMARY KEY AUTOINCREMENT,
     203            computer_id INTEGER,
     204            pid INTEGER,
     205            name TEXT,
     206            cpu_percent REAL,
     207            memory_mb REAL,
     208            username TEXT,
     209            cmdline TEXT,
     210            timestamp TEXT,
     211            FOREIGN KEY(computer_id) REFERENCES computers(id)
     212        )
     213        """)
     214
     215    # Sysmon events
     216    c.execute("""
     217        CREATE TABLE IF NOT EXISTS sysmon_events(
     218            id INTEGER PRIMARY KEY AUTOINCREMENT,
     219            computer_id INTEGER,
     220            event_id INTEGER,
     221            event_type TEXT,
     222            message TEXT,
     223            timestamp TEXT,
     224            details TEXT,
     225            FOREIGN KEY(computer_id) REFERENCES computers(id)
     226        )
     227        """)
     228
     229    # Network connections
     230    c.execute("""
     231        CREATE TABLE IF NOT EXISTS network_connections(
     232            id INTEGER PRIMARY KEY AUTOINCREMENT,
     233            computer_id INTEGER,
     234            pid INTEGER,
     235            local_address TEXT,
     236            remote_address TEXT,
     237            status TEXT,
     238            process_name TEXT,
     239            timestamp TEXT,
     240            FOREIGN KEY(computer_id) REFERENCES computers(id)
     241        )
     242        """)
     243
     244    # Security alerts
     245    c.execute("""
     246        CREATE TABLE IF NOT EXISTS security_alerts(
     247            id INTEGER PRIMARY KEY AUTOINCREMENT,
     248            computer_id INTEGER,
     249            alert_type TEXT,
     250            severity TEXT,
     251            description TEXT,
     252            timestamp TEXT,
     253            resolved INTEGER DEFAULT 0,
     254            FOREIGN KEY(computer_id) REFERENCES computers(id)
     255        )
     256        """)
    72257
    73258    conn.commit()
    74259    conn.close()
    75 
    76 def require_admin_session():
    77     tok = request.headers.get("X-Admin-Session", "")
    78     if not tok:
    79         return False
    80     conn = sqlite3.connect(DB_FILE)
    81     c = conn.cursor()
    82     c.execute("SELECT 1 FROM admin_sessions WHERE token = ?", (tok,))
    83     ok = c.fetchone() is not None
    84     conn.close()
    85     return ok
    86 
     260    print(f"✅ DB ready: {DB_FILE}")
     261
     262
     263# ----------------------------
     264# JWT helpers
     265# ----------------------------
     266def make_jwt(payload: dict, minutes=60 * 24):
     267    exp = datetime.utcnow() + timedelta(minutes=minutes)
     268    data = {**payload, "iss": JWT_ISSUER, "exp": exp}
     269    return jwt.encode(data, JWT_SECRET, algorithm="HS256")
     270
     271
     272def read_jwt(token: str):
     273    return jwt.decode(token, JWT_SECRET, algorithms=["HS256"], issuer=JWT_ISSUER)
     274
     275
     276def require_user():
     277    def deco(fn):
     278        @wraps(fn)
     279        def wrap(*args, **kwargs):
     280            tok = request.cookies.get("session") or ""
     281            if not tok:
     282                return jsonify({"error": "Unauthorized"}), 401
     283            try:
     284                claims = read_jwt(tok)
     285            except Exception:
     286                return jsonify({"error": "Unauthorized"}), 401
     287            request.user = claims
     288            return fn(*args, **kwargs)
     289
     290        return wrap
     291
     292    return deco
     293
     294
     295def require_tenant_admin():
     296    def deco(fn):
     297        @wraps(fn)
     298        def wrap(*args, **kwargs):
     299            tok = request.cookies.get("session") or ""
     300            if not tok:
     301                return jsonify({"error": "Unauthorized"}), 401
     302            try:
     303                claims = read_jwt(tok)
     304            except Exception:
     305                return jsonify({"error": "Unauthorized"}), 401
     306            if claims.get("role") != "admin":
     307                return jsonify({"error": "Forbidden"}), 403
     308            request.user = claims
     309            return fn(*args, **kwargs)
     310
     311        return wrap
     312
     313    return deco
     314
     315
     316# ----------------------------
     317# Env-token helpers (agents)
     318# ----------------------------
    87319def get_env_from_token(req):
    88320    tok = req.headers.get("X-Env-Token", "")
    89321    if not tok:
    90         return None
    91 
    92     conn = sqlite3.connect(DB_FILE)
     322        return (None, None)
     323
     324    conn = db()
    93325    c = conn.cursor()
    94326    c.execute("""
    95         SELECT env_name
    96         FROM env_tokens
    97         WHERE token = ?
    98           AND (expires_at IS NULL OR expires_at > datetime('now'))
    99     """, (tok,))
     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,))
    100333    row = c.fetchone()
    101334    conn.close()
    102     return row[0] if row else None
    103 
    104 
    105 def init_database():
    106     """Креирај подобрена база со Sysmon табели"""
    107     os.makedirs(LOG_DIR, exist_ok=True)
    108 
    109     conn = sqlite3.connect(DB_FILE)
    110     c = conn.cursor()
    111 
    112     # Табела за компјутери
    113     c.execute('''CREATE TABLE IF NOT EXISTS computers
    114                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    115                   name TEXT UNIQUE,
    116                   user TEXT,
    117                   ip TEXT,
    118                   os TEXT,
    119                   first_seen TIMESTAMP,
    120                   last_seen TIMESTAMP,
    121                   sysmon_available BOOLEAN DEFAULT 0)''')
    122     # --- MIGRATION: add env_name column if missing ---
    123     c.execute("PRAGMA table_info(computers)")
    124     cols = [r[1] for r in c.fetchall()]
    125     if "env_name" not in cols:
    126         c.execute("ALTER TABLE computers ADD COLUMN env_name TEXT DEFAULT 'default'")
    127     # --- END MIGRATION ---
    128 
    129     # Табела за историја
    130     c.execute('''CREATE TABLE IF NOT EXISTS computer_history
    131                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    132                   computer_id INTEGER,
    133                   cpu_usage REAL,
    134                   ram_usage REAL,
    135                   disk_usage REAL,
    136                   network_sent_mb REAL,
    137                   network_recv_mb REAL,
    138                   timestamp TIMESTAMP,
    139                   FOREIGN KEY(computer_id) REFERENCES computers(id))''')
    140 
    141     # Табела за процеси
    142     c.execute('''CREATE TABLE IF NOT EXISTS computer_processes
    143                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    144                   computer_id INTEGER,
    145                   pid INTEGER,
    146                   name TEXT,
    147                   cpu_percent REAL,
    148                   memory_mb REAL,
    149                   username TEXT,
    150                   cmdline TEXT,
    151                   timestamp TIMESTAMP,
    152                   FOREIGN KEY(computer_id) REFERENCES computers(id))''')
    153 
    154     # НОВА: Табела за Sysmon настани
    155     c.execute('''CREATE TABLE IF NOT EXISTS sysmon_events
    156                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    157                   computer_id INTEGER,
    158                   event_id INTEGER,
    159                   event_type TEXT,
    160                   message TEXT,
    161                   timestamp TIMESTAMP,
    162                   details TEXT,
    163                   FOREIGN KEY(computer_id) REFERENCES computers(id))''')
    164 
    165     # НОВА: Табела за детални мрежни конекции
    166     c.execute('''CREATE TABLE IF NOT EXISTS network_connections
    167                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    168                   computer_id INTEGER,
    169                   pid INTEGER,
    170                   local_address TEXT,
    171                   remote_address TEXT,
    172                   status TEXT,
    173                   process_name TEXT,
    174                   timestamp TIMESTAMP,
    175                   FOREIGN KEY(computer_id) REFERENCES computers(id))''')
    176 
    177     # НОВА: Табела за безбедносни настани
    178     c.execute('''CREATE TABLE IF NOT EXISTS security_alerts
    179                  (id INTEGER PRIMARY KEY AUTOINCREMENT,
    180                   computer_id INTEGER,
    181                   alert_type TEXT,
    182                   severity TEXT,
    183                   description TEXT,
    184                   timestamp TIMESTAMP,
    185                   resolved BOOLEAN DEFAULT 0,
    186                   FOREIGN KEY(computer_id) REFERENCES computers(id))''')
    187 
    188     conn.commit()
    189     conn.close()
    190 
    191     print(f"✅ Database created: {DB_FILE}")
    192 
    193 
    194 @app.route('/receive', methods=['POST'])
    195 def receive_data():
     335
     336    if not row:
     337        return (None, None)
     338    return (row["env_name"], row["tenant_id"])
     339
     340
     341# ----------------------------
     342# Utility
     343# ----------------------------
     344def save_json_file(data):
     345    info = data.get("info") or {}
     346    computer_name = info.get("computer_name", "unknown")
     347    date_str = datetime.now().strftime("%Y%m%d_%H")
     348
     349    computer_dir = os.path.join(LOG_DIR, computer_name)
     350    os.makedirs(computer_dir, exist_ok=True)
     351    filepath = os.path.join(computer_dir, f"{date_str}.json")
     352
     353    if os.path.exists(filepath):
     354        try:
     355            with open(filepath, "r", encoding="utf-8") as f:
     356                existing_data = json.load(f)
     357        except Exception:
     358            existing_data = []
     359    else:
     360        existing_data = []
     361
     362    existing_data.append({
     363        "timestamp": info.get("timestamp"),
     364        "info": info,
     365        "processes_count": len(data.get("processes", [])),
     366        "sysmon_events_count": len((data.get("security_data") or {}).get("sysmon_events", [])),
     367    })
     368
     369    with open(filepath, "w", encoding="utf-8") as f:
     370        json.dump(existing_data, f, indent=2, ensure_ascii=False)
     371
     372
     373def cleanup_old_data():
     374    """Deletes old rows (30 days) every hour."""
     375    while True:
     376        time.sleep(3600)
     377        try:
     378            conn = db()
     379            c = conn.cursor()
     380            cutoff = (datetime.now() - timedelta(days=30)).isoformat()
     381
     382            c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff,))
     383            c.execute("DELETE FROM computer_processes WHERE timestamp < ?", (cutoff,))
     384            c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff,))
     385            c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff,))
     386
     387            conn.commit()
     388            conn.close()
     389            print(f"[Cleanup] Deleted entries older than {cutoff}")
     390        except Exception as e:
     391            print("[Cleanup] Error:", e)
     392
     393
     394# ----------------------------
     395# Auth endpoints
     396# ----------------------------
     397@app.route("/api/auth/google", methods=["POST"])
     398def auth_google():
     399    data = request.get_json(force=True, silent=True) or {}
     400    credential = (data.get("credential") or "").strip()
     401    if not credential:
     402        return jsonify({"error": "Missing credential"}), 400
     403    if not GOOGLE_CLIENT_ID:
     404        return jsonify({"error": "Server missing GOOGLE_CLIENT_ID"}), 500
     405
    196406    try:
    197         data = request.json
    198 
    199         if not data:
    200             return jsonify({"error": "No data"}), 400
    201         env_name = get_env_from_token(request)
    202         if not env_name:
    203             return jsonify({"error": "Missing or invalid X-Env-Token"}), 401
    204 
    205         info = data['info']
    206         computer_name = info['computer_name']
    207         timestamp = datetime.now()
    208         security_data = data.get('security_data', {})
    209 
    210         conn = sqlite3.connect(DB_FILE)
     407        idinfo = id_token.verify_oauth2_token(
     408            credential,
     409            grequests.Request(),
     410            GOOGLE_CLIENT_ID,
     411        )
     412        email = idinfo.get("email")
     413        name = idinfo.get("name") or ""
     414        picture = idinfo.get("picture") or ""
     415
     416        if not email:
     417            return jsonify({"error": "No email in token"}), 400
     418
     419        conn = db()
    211420        c = conn.cursor()
    212421
    213         # Провери дали компјутерот веќе постои
    214         c.execute("SELECT id FROM computers WHERE name = ?", (computer_name,))
    215         result = c.fetchone()
    216 
    217         if result:
    218             # Компјутерот постои - ажурирај го
    219             computer_id = result[0]
    220             c.execute('''UPDATE computers
    221                         SET user = ?, ip = ?, os = ?, last_seen = ?, sysmon_available = ?, env_name = ?
    222                         WHERE id = ?''',
    223                       (info['user'], info['ip_address'], info['os'],
    224                        timestamp.isoformat(), info.get('is_sysmon_available', 0),
    225                        env_name, computer_id))
     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))
    226428        else:
    227             # Нов компјутер - додади го
    228             c.execute('''INSERT INTO computers
    229                         (name, user, ip, os, first_seen, last_seen, sysmon_available, env_name)
    230                         VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
    231                       (computer_name, info['user'], info['ip_address'],
    232                        info['os'], timestamp.isoformat(), timestamp.isoformat(),
    233                        info.get('is_sysmon_available', 0), env_name))
    234 
    235             computer_id = c.lastrowid
    236 
    237         # Додади во историја
    238         c.execute('''INSERT INTO computer_history
    239                     (computer_id, cpu_usage, ram_usage, disk_usage,
    240                      network_sent_mb, network_recv_mb, timestamp)
    241                     VALUES (?, ?, ?, ?, ?, ?, ?)''',
    242                   (computer_id, info['cpu_usage'], info['ram_usage'],
    243                    info['disk_usage'], info.get('network_sent_mb', 0),
    244                    info.get('network_recv_mb', 0), info['timestamp']))
    245 
    246         # Зачувај ги процесите
    247         for proc in data.get('processes', []):
    248             c.execute('''INSERT INTO computer_processes
    249                         (computer_id, pid, name, cpu_percent, memory_mb,
    250                          username, cmdline, timestamp)
    251                         VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
    252                       (computer_id, proc.get('pid'), proc.get('name'),
    253                        proc.get('cpu_percent', 0), proc.get('memory_mb', 0),
    254                        proc.get('user'), proc.get('cmdline'), info['timestamp']))
    255 
    256         # ЗАЧУВАЈ SYSMON НАСТАНИ
    257         sysmon_events_count = 0
    258         for event in security_data.get('sysmon_events', []):
    259             try:
    260                 c.execute('''INSERT INTO sysmon_events
    261                             (computer_id, event_id, event_type, message,
    262                              timestamp, details)
    263                             VALUES (?, ?, ?, ?, ?, ?)''',
    264                           (computer_id, event.get('event_id'),
    265                            event.get('event_type', 'Unknown'),
    266                            event.get('message', ''),
    267                            event.get('timestamp', info['timestamp']),
    268                            json.dumps(event)))
    269                 sysmon_events_count += 1
    270             except Exception as e:
    271                 print(f"Error saving Sysmon event: {e}")
    272 
    273         # ЗАЧУВАЈ МРЕЖНИ КОНЕКЦИИ
    274         for conn_data in security_data.get('network_connections', []):
    275             try:
    276                 c.execute('''INSERT INTO network_connections
    277                             (computer_id, pid, local_address, remote_address,
    278                              status, process_name, timestamp)
    279                             VALUES (?, ?, ?, ?, ?, ?, ?)''',
    280                           (computer_id, conn_data.get('pid'),
    281                            conn_data.get('local_address'),
    282                            conn_data.get('remote_address'),
    283                            conn_data.get('status'),
    284                            conn_data.get('process_name'),
    285                            info['timestamp']))
    286             except Exception as e:
    287                 print(f"Error saving network connection: {e}")
     429            c.execute(
     430                "INSERT INTO users(email, name, picture, created_at) VALUES(?,?,?, datetime('now'))",
     431                (email, name, picture),
     432            )
     433            user_id = c.lastrowid
     434
     435        # membership -> tenant (create tenant if first time)
     436        c.execute("SELECT tenant_id, role FROM memberships WHERE user_id=? LIMIT 1", (user_id,))
     437        m = c.fetchone()
     438
     439        if not m:
     440            tenant_name = email.split("@")[0]
     441            c.execute(
     442                "INSERT INTO tenants(name, owner_email, created_at) VALUES(?,?, datetime('now'))",
     443                (tenant_name, email),
     444            )
     445            tenant_id = c.lastrowid
     446            role = "admin"
     447
     448            c.execute(
     449                "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?, 'admin', datetime('now'))",
     450                (user_id, tenant_id),
     451            )
     452
     453            # default env for this tenant
     454            c.execute(
     455                "INSERT OR IGNORE INTO environments(tenant_id, name, created_at) VALUES(?, 'default', datetime('now'))",
     456                (tenant_id,),
     457            )
     458        else:
     459            tenant_id = m["tenant_id"]
     460            role = m["role"] or "admin"
    288461
    289462        conn.commit()
    290463        conn.close()
    291464
    292         # Зачувај во JSON
    293         save_json_file(data)
    294 
    295         print(f"[{timestamp.strftime('%H:%M:%S')}] 📥 {computer_name}: "
    296               f"CPU {info['cpu_usage']}% | Sysmon: {sysmon_events_count} events")
    297 
    298         return jsonify({
    299             "status": "success",
    300             "message": f"Data saved for {computer_name}",
    301             "computer_id": computer_id,
    302             "event_count": sysmon_events_count,
    303             "timestamp": timestamp.isoformat()
    304         })
     465        session = make_jwt(
     466            {
     467                "uid": user_id,
     468                "email": email,
     469                "name": name,
     470                "role": role,
     471                "tenant_id": tenant_id,
     472            },
     473            minutes=60 * 24,
     474        )
     475
     476        resp = jsonify({"ok": True, "user": {"email": email, "name": name, "role": role, "tenant_id": tenant_id}})
     477        resp.set_cookie(
     478            "session",
     479            session,
     480            httponly=True,
     481            samesite="Lax",
     482            secure=COOKIE_SECURE,
     483            max_age=60 * 60 * 24,
     484        )
     485        return resp
    305486
    306487    except Exception as e:
    307         print(f"[-] Грешка: {e}")
    308         return jsonify({"error": str(e)}), 500
    309 
    310 
    311 def save_json_file(data):
    312     """Зачувај во JSON"""
    313     info = data['info']
    314     computer_name = info['computer_name']
    315     date_str = datetime.now().strftime("%Y%m%d_%H")
    316 
    317     # Креирај folder за компјутерот
    318     computer_dir = os.path.join(LOG_DIR, computer_name)
    319     os.makedirs(computer_dir, exist_ok=True)
    320 
    321     # Фајл за секој час (за подобро организирање)
    322     filepath = os.path.join(computer_dir, f"{date_str}.json")
    323 
    324     # Додади во постоечки фајл или креирај нов
    325     if os.path.exists(filepath):
    326         with open(filepath, 'r', encoding='utf-8') as f:
    327             existing_data = json.load(f)
    328     else:
    329         existing_data = []
    330 
    331     # Додади нов запис
    332     existing_data.append({
    333         "timestamp": info['timestamp'],
    334         "info": info,
    335         "processes_count": len(data.get('processes', [])),
    336         "sysmon_events_count": len(data.get('security_data', {}).get('sysmon_events', []))
    337     })
    338 
    339     # Зачувај
    340     with open(filepath, 'w', encoding='utf-8') as f:
    341         json.dump(existing_data, f, indent=2, ensure_ascii=False)
    342 
    343 
    344 @app.route('/')
    345 def dashboard():
    346     """Главен dashboard"""
    347     conn = sqlite3.connect(DB_FILE)
     488        return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
     489
     490
     491@app.route("/api/me", methods=["GET"])
     492@require_user()
     493def api_me():
     494    return jsonify({"user": request.user})
     495
     496
     497@app.route("/api/auth/logout", methods=["POST"])
     498def auth_logout():
     499    resp = jsonify({"ok": True})
     500    resp.set_cookie("session", "", expires=0)
     501    return resp
     502
     503
     504# ----------------------------
     505# Admin endpoints
     506# ----------------------------
     507@app.route("/api/admin/environments", methods=["GET"])
     508@require_tenant_admin()
     509def admin_list_envs():
     510    tenant_id = request.user["tenant_id"]
     511    conn = db()
    348512    c = conn.cursor()
    349 
    350     # Земaj ги сите компјутери
    351     c.execute('''SELECT name, user, ip, os, first_seen, last_seen, sysmon_available, env_name
    352                  FROM computers
    353                  ORDER BY last_seen DESC''')
    354 
    355     computers = []
    356     for row in c.fetchall():
    357         computer_name = row[0]
    358 
    359         # Пресметaj колку записи има во последните 24 часа
    360         c.execute('''SELECT COUNT(*) FROM computer_history
    361                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    362                      AND timestamp > datetime('now', '-24 hours')''',
    363                   (computer_name,))
    364         recent_logs = c.fetchone()[0]
    365 
    366         # Земaj ги последните 5 записи за CPU/RAM
    367         c.execute('''SELECT cpu_usage, ram_usage, timestamp
    368                      FROM computer_history
    369                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    370                      ORDER BY timestamp DESC LIMIT 5''',
    371                   (computer_name,))
    372         recent_metrics = c.fetchall()
    373 
    374         # Sysmon настани за последните 24 часа
    375         c.execute('''SELECT COUNT(*) FROM sysmon_events
    376                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    377                      AND timestamp > datetime('now', '-24 hours')''',
    378                   (computer_name,))
    379         recent_sysmon = c.fetchone()[0]
    380 
    381         # Пресметaj просек
    382         avg_cpu = sum(m[0] for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
    383         avg_ram = sum(m[1] for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
    384 
    385         # Статус
    386         last_seen = datetime.fromisoformat(row[5])
    387         time_diff = (datetime.now() - last_seen).seconds
    388 
    389         if time_diff < 60:
    390             status = 'online'
    391             status_color = 'green'
    392         elif time_diff < 300:
    393             status = 'idle'
    394             status_color = 'orange'
    395         else:
    396             status = 'offline'
    397             status_color = 'gray'
    398 
    399         computers.append({
    400             'name': row[0],
    401             'user': row[1],
    402             'ip': row[2],
    403             'os': row[3],
    404             'first_seen': row[4],
    405             'last_seen': row[5],
    406             'sysmon_available': bool(row[6]),
    407             'recent_logs': recent_logs,
    408             'recent_sysmon': recent_sysmon,
    409             'avg_cpu': round(avg_cpu, 1),
    410             'avg_ram': round(avg_ram, 1),
    411             'status': status,
    412             'status_color': status_color,
    413             'env_name': row[7],
    414         })
    415 
     513    c.execute("SELECT name FROM environments WHERE tenant_id=? ORDER BY name", (tenant_id,))
     514    envs = [r["name"] for r in c.fetchall()]
    416515    conn.close()
    417 
    418     # Генерирај HTML
    419     return render_template_string('''
    420     <!DOCTYPE html>
    421     <html>
    422     <head>
    423         <title>LAN Log Server - Sysmon Dashboard</title>
    424         <meta charset="utf-8">
    425         <meta name="viewport" content="width=device-width, initial-scale=1">
    426         <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    427         <style>
    428             :root {
    429                 --primary-color: #2c3e50;
    430                 --secondary-color: #3498db;
    431                 --danger-color: #e74c3c;
    432                 --warning-color: #f39c12;
    433                 --success-color: #27ae60;
    434             }
    435 
    436             body {
    437                 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    438                 margin: 0;
    439                 padding: 20px;
    440                 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    441                 min-height: 100vh;
    442             }
    443 
    444             .container {
    445                 max-width: 1400px;
    446                 margin: 0 auto;
    447                 background: white;
    448                 border-radius: 15px;
    449                 box-shadow: 0 20px 40px rgba(0,0,0,0.1);
    450                 overflow: hidden;
    451             }
    452 
    453             .header {
    454                 background: linear-gradient(to right, var(--primary-color), #1a2530);
    455                 color: white;
    456                 padding: 30px 40px;
    457                 border-radius: 15px 15px 0 0;
    458             }
    459 
    460             .header h1 {
    461                 margin: 0;
    462                 font-size: 2.5em;
    463                 display: flex;
    464                 align-items: center;
    465                 gap: 15px;
    466             }
    467 
    468             .stats-grid {
    469                 display: grid;
    470                 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    471                 gap: 20px;
    472                 padding: 30px;
    473             }
    474 
    475             .stat-card {
    476                 background: white;
    477                 padding: 25px;
    478                 border-radius: 10px;
    479                 box-shadow: 0 5px 15px rgba(0,0,0,0.08);
    480                 border-left: 5px solid var(--secondary-color);
    481                 transition: transform 0.3s ease;
    482             }
    483 
    484             .stat-card:hover {
    485                 transform: translateY(-5px);
    486             }
    487 
    488             .stat-card.sysmon {
    489                 border-left-color: var(--danger-color);
    490             }
    491 
    492             .stat-card.network {
    493                 border-left-color: var(--success-color);
    494             }
    495 
    496             .stat-value {
    497                 font-size: 2.8em;
    498                 font-weight: bold;
    499                 margin: 10px 0;
    500             }
    501 
    502             .stat-label {
    503                 color: #666;
    504                 font-size: 0.9em;
    505                 text-transform: uppercase;
    506                 letter-spacing: 1px;
    507             }
    508 
    509             .computers-grid {
    510                 display: grid;
    511                 grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
    512                 gap: 25px;
    513                 padding: 30px;
    514             }
    515 
    516             .computer-card {
    517                 background: white;
    518                 border-radius: 12px;
    519                 padding: 25px;
    520                 box-shadow: 0 8px 20px rgba(0,0,0,0.1);
    521                 cursor: pointer;
    522                 transition: all 0.3s ease;
    523                 border: 2px solid transparent;
    524                 position: relative;
    525                 overflow: hidden;
    526             }
    527 
    528             .computer-card:hover {
    529                 transform: translateY(-8px);
    530                 box-shadow: 0 15px 30px rgba(0,0,0,0.15);
    531                 border-color: var(--secondary-color);
    532             }
    533 
    534             .computer-card::before {
    535                 content: '';
    536                 position: absolute;
    537                 top: 0;
    538                 left: 0;
    539                 right: 0;
    540                 height: 4px;
    541                 background: linear-gradient(to right, #667eea, #764ba2);
    542             }
    543 
    544             .computer-name {
    545                 font-size: 1.4em;
    546                 font-weight: bold;
    547                 color: var(--primary-color);
    548                 margin-bottom: 10px;
    549                 display: flex;
    550                 justify-content: space-between;
    551                 align-items: center;
    552             }
    553 
    554             .status-indicator {
    555                 display: inline-block;
    556                 width: 12px;
    557                 height: 12px;
    558                 border-radius: 50%;
    559                 margin-right: 8px;
    560             }
    561 
    562             .computer-info {
    563                 color: #555;
    564                 margin: 8px 0;
    565                 font-size: 0.95em;
    566                 display: flex;
    567                 align-items: center;
    568                 gap: 10px;
    569             }
    570 
    571             .computer-info i {
    572                 width: 20px;
    573                 color: #777;
    574             }
    575 
    576             .stats-row {
    577                 display: flex;
    578                 justify-content: space-between;
    579                 margin-top: 20px;
    580                 padding-top: 20px;
    581                 border-top: 1px solid #eee;
    582             }
    583 
    584             .stat-item {
    585                 text-align: center;
    586             }
    587 
    588             .stat-number {
    589                 font-size: 1.6em;
    590                 font-weight: bold;
    591                 color: var(--primary-color);
    592             }
    593 
    594             .stat-text {
    595                 font-size: 0.8em;
    596                 color: #777;
    597                 margin-top: 5px;
    598             }
    599 
    600             .sysmon-badge {
    601                 background: var(--danger-color);
    602                 color: white;
    603                 padding: 3px 10px;
    604                 border-radius: 20px;
    605                 font-size: 0.8em;
    606                 display: inline-block;
    607                 margin-left: 10px;
    608             }
    609 
    610             /* Модал */
    611             .modal {
    612                 display: none;
    613                 position: fixed;
    614                 top: 0;
    615                 left: 0;
    616                 right: 0;
    617                 bottom: 0;
    618                 background: rgba(0,0,0,0.7);
    619                 z-index: 1000;
    620                 overflow-y: auto;
    621             }
    622 
    623             .modal-content {
    624                 background: white;
    625                 margin: 50px auto;
    626                 width: 90%;
    627                 max-width: 1200px;
    628                 border-radius: 15px;
    629                 animation: modalSlideIn 0.3s ease;
    630             }
    631 
    632             @keyframes modalSlideIn {
    633                 from {
    634                     transform: translateY(-50px);
    635                     opacity: 0;
    636                 }
    637                 to {
    638                     transform: translateY(0);
    639                     opacity: 1;
    640                 }
    641             }
    642 
    643             .modal-header {
    644                 padding: 25px 30px;
    645                 background: var(--primary-color);
    646                 color: white;
    647                 border-radius: 15px 15px 0 0;
    648                 display: flex;
    649                 justify-content: space-between;
    650                 align-items: center;
    651             }
    652 
    653             .close-btn {
    654                 background: none;
    655                 border: none;
    656                 color: white;
    657                 font-size: 1.8em;
    658                 cursor: pointer;
    659                 width: 40px;
    660                 height: 40px;
    661                 border-radius: 50%;
    662                 display: flex;
    663                 align-items: center;
    664                 justify-content: center;
    665                 transition: background 0.2s;
    666             }
    667 
    668             .close-btn:hover {
    669                 background: rgba(255,255,255,0.1);
    670             }
    671 
    672             .tabs {
    673                 display: flex;
    674                 border-bottom: 1px solid #ddd;
    675                 padding: 0 30px;
    676                 background: #f8f9fa;
    677             }
    678 
    679             .tab-btn {
    680                 padding: 15px 25px;
    681                 background: none;
    682                 border: none;
    683                 cursor: pointer;
    684                 font-size: 1em;
    685                 color: #666;
    686                 position: relative;
    687                 transition: all 0.3s;
    688             }
    689 
    690             .tab-btn:hover {
    691                 color: var(--primary-color);
    692             }
    693 
    694             .tab-btn.active {
    695                 color: var(--primary-color);
    696                 font-weight: bold;
    697             }
    698 
    699             .tab-btn.active::after {
    700                 content: '';
    701                 position: absolute;
    702                 bottom: -1px;
    703                 left: 0;
    704                 right: 0;
    705                 height: 3px;
    706                 background: var(--secondary-color);
    707             }
    708 
    709             .tab-content {
    710                 padding: 30px;
    711                 display: none;
    712             }
    713 
    714             .tab-content.active {
    715                 display: block;
    716             }
    717 
    718             .events-table {
    719                 width: 100%;
    720                 border-collapse: collapse;
    721                 margin-top: 20px;
    722             }
    723 
    724             .events-table th {
    725                 background: #f8f9fa;
    726                 padding: 15px;
    727                 text-align: left;
    728                 border-bottom: 2px solid #dee2e6;
    729                 color: var(--primary-color);
    730             }
    731 
    732             .events-table td {
    733                 padding: 12px 15px;
    734                 border-bottom: 1px solid #eee;
    735             }
    736 
    737             .events-table tr:hover {
    738                 background: #f8f9fa;
    739             }
    740 
    741             .severity-high {
    742                 background: #ffe6e6;
    743                 color: var(--danger-color);
    744                 padding: 3px 10px;
    745                 border-radius: 3px;
    746                 font-weight: bold;
    747             }
    748 
    749             .severity-medium {
    750                 background: #fff3cd;
    751                 color: var(--warning-color);
    752                 padding: 3px 10px;
    753                 border-radius: 3px;
    754             }
    755 
    756             .event-details {
    757                 max-width: 500px;
    758                 word-wrap: break-word;
    759                 font-family: monospace;
    760                 font-size: 0.9em;
    761             }
    762 
    763             .filter-bar {
    764                 padding: 20px 30px;
    765                 background: #f8f9fa;
    766                 border-bottom: 1px solid #ddd;
    767             }
    768 
    769             .search-box {
    770                 padding: 10px 15px;
    771                 border: 1px solid #ddd;
    772                 border-radius: 5px;
    773                 width: 300px;
    774             }
    775 
    776             .btn {
    777                 padding: 10px 20px;
    778                 background: var(--secondary-color);
    779                 color: white;
    780                 border: none;
    781                 border-radius: 5px;
    782                 cursor: pointer;
    783                 transition: background 0.3s;
    784             }
    785 
    786             .btn:hover {
    787                 background: #2980b9;
    788             }
    789 
    790             .chart-container {
    791                 height: 300px;
    792                 margin: 20px 0;
    793             }
    794 
    795             @media (max-width: 768px) {
    796                 .computers-grid {
    797                     grid-template-columns: 1fr;
    798                 }
    799 
    800                 .modal-content {
    801                     width: 95%;
    802                     margin: 20px auto;
    803                 }
    804             }
    805         </style>
    806     </head>
    807     <body>
    808         <div class="container">
    809             <div class="header">
    810                 <h1>
    811                     <span>🛡️ LAN Security Monitor</span>
    812                     <small style="font-size: 0.5em; opacity: 0.8;">Sysmon Edition</small>
    813                 </h1>
    814                 <p>Server: {{ socket.gethostbyname(socket.gethostname()) }}:5555 | Total Computers: {{ computers|length }}</p>
    815             </div>
    816 
    817             <div class="stats-grid">
    818                 <div class="stat-card">
    819                     <div class="stat-label">Total Computers</div>
    820                     <div class="stat-value">{{ computers|length }}</div>
    821                     <div class="stat-text">Monitored Systems</div>
    822                 </div>
    823 
    824                 <div class="stat-card sysmon">
    825                     <div class="stat-label">Sysmon Events (24h)</div>
    826                     <div class="stat-value" id="total-sysmon">0</div>
    827                     <div class="stat-text">Security Events</div>
    828                 </div>
    829 
    830                 <div class="stat-card network">
    831                     <div class="stat-label">Network Connections</div>
    832                     <div class="stat-value" id="total-network">0</div>
    833                     <div class="stat-text">Active Connections</div>
    834                 </div>
    835             </div>
    836 
    837             <div style="padding: 0 30px;">
    838                 <h2>Connected Computers</h2>
    839                 <div class="computers-grid" id="computers-grid">
    840                     {% for comp in computers %}
    841                     <div class="computer-card" onclick="showComputerDetails('{{ comp.name }}')">
    842                         <div class="computer-name">
    843                             {{ comp.name }}
    844                             <span class="status-indicator" style="background: {{ comp.status_color }}"></span>
    845                         </div>
    846 
    847                         <div class="computer-info">
    848                             <i>👤</i> {{ comp.user }}
    849                         </div>
    850 
    851                         <div class="computer-info">
    852                             <i>🌐</i> {{ comp.ip }}
    853                         </div>
    854                         <div class="computer-info">
    855                         <i>🏷️</i> Env: {{ comp.env_name }}</div>
    856 
    857 
    858                         <div class="computer-info">
    859                             <i>💻</i> {{ comp.os[:40] }}
    860                         </div>
    861 
    862                         <div class="computer-info">
    863                             <i>⏰</i> Last seen: {{ comp.last_seen[11:19] }}
    864                         </div>
    865 
    866                         {% if comp.sysmon_available %}
    867                         <div class="computer-info">
    868                             <i>🛡️</i> Sysmon Available
    869                             <span class="sysmon-badge">{{ comp.recent_sysmon }} events</span>
    870                         </div>
    871                         {% endif %}
    872 
    873                         <div class="stats-row">
    874                             <div class="stat-item">
    875                                 <div class="stat-number">{{ comp.avg_cpu }}%</div>
    876                                 <div class="stat-text">CPU</div>
    877                             </div>
    878                             <div class="stat-item">
    879                                 <div class="stat-number">{{ comp.avg_ram }}%</div>
    880                                 <div class="stat-text">RAM</div>
    881                             </div>
    882                             <div class="stat-item">
    883                                 <div class="stat-number">{{ comp.recent_logs }}</div>
    884                                 <div class="stat-text">Logs (24h)</div>
    885                             </div>
    886                         </div>
    887                     </div>
    888                     {% endfor %}
    889                 </div>
    890             </div>
    891         </div>
    892 
    893         <!-- Детален модал -->
    894         <div class="modal" id="computer-modal">
    895             <div class="modal-content">
    896                 <div class="modal-header">
    897                     <h2 id="modal-title">Computer Details</h2>
    898                     <button class="close-btn" onclick="closeModal()">×</button>
    899                 </div>
    900 
    901                 <div class="filter-bar">
    902                     <input type="text" id="event-search" class="search-box" placeholder="Search events..."
    903                            onkeyup="filterEvents()">
    904                     <button class="btn" onclick="exportData()">Export Data</button>
    905                 </div>
    906 
    907                 <div class="tabs">
    908                     <button class="tab-btn active" onclick="switchTab('overview')">Overview</button>
    909                     <button class="tab-btn" onclick="switchTab('sysmon')">Sysmon Events</button>
    910                     <button class="tab-btn" onclick="switchTab('processes')">Processes</button>
    911                     <button class="tab-btn" onclick="switchTab('network')">Network</button>
    912                     <button class="tab-btn" onclick="switchTab('charts')">Charts</button>
    913                 </div>
    914 
    915                 <!-- Overview Tab -->
    916                 <div class="tab-content active" id="overview-tab">
    917                     <h3>System Overview</h3>
    918                     <div id="overview-content"></div>
    919                 </div>
    920 
    921                 <!-- Sysmon Events Tab -->
    922                 <div class="tab-content" id="sysmon-tab">
    923                     <h3>Security Events</h3>
    924                     <table class="events-table" id="sysmon-table">
    925                         <thead>
    926                             <tr>
    927                                 <th>Time</th>
    928                                 <th>Event ID</th>
    929                                 <th>Type</th>
    930                                 <th>Severity</th>
    931                                 <th>Message</th>
    932                                 <th>Actions</th>
    933                             </tr>
    934                         </thead>
    935                         <tbody id="sysmon-events">
    936                             <!-- Events will be populated here -->
    937                         </tbody>
    938                     </table>
    939                 </div>
    940 
    941                 <!-- Processes Tab -->
    942                 <div class="tab-content" id="processes-tab">
    943                     <h3>Running Processes</h3>
    944                     <table class="events-table">
    945                         <thead>
    946                             <tr>
    947                                 <th>PID</th>
    948                                 <th>Name</th>
    949                                 <th>User</th>
    950                                 <th>CPU %</th>
    951                                 <th>Memory</th>
    952                                 <th>Command Line</th>
    953                             </tr>
    954                         </thead>
    955                         <tbody id="processes-list">
    956                             <!-- Processes will be populated here -->
    957                         </tbody>
    958                     </table>
    959                 </div>
    960 
    961                 <!-- Network Tab -->
    962                 <div class="tab-content" id="network-tab">
    963                     <h3>Network Connections</h3>
    964                     <table class="events-table">
    965                         <thead>
    966                             <tr>
    967                                 <th>PID</th>
    968                                 <th>Process</th>
    969                                 <th>Local Address</th>
    970                                 <th>Remote Address</th>
    971                                 <th>Status</th>
    972                                 <th>Timestamp</th>
    973                             </tr>
    974                         </thead>
    975                         <tbody id="network-connections">
    976                             <!-- Connections will be populated here -->
    977                         </tbody>
    978                     </table>
    979                 </div>
    980 
    981                 <!-- Charts Tab -->
    982                 <div class="tab-content" id="charts-tab">
    983                     <h3>Performance Analytics</h3>
    984                     <div class="chart-container">
    985                         <canvas id="performance-chart"></canvas>
    986                     </div>
    987                 </div>
    988             </div>
    989         </div>
    990 
    991         <!-- Event Details Modal -->
    992         <div class="modal" id="event-modal" style="display: none;">
    993             <div class="modal-content" style="max-width: 800px;">
    994                 <div class="modal-header">
    995                     <h2>Event Details</h2>
    996                     <button class="close-btn" onclick="closeEventModal()">×</button>
    997                 </div>
    998                 <div style="padding: 30px;">
    999                     <pre id="event-details-content" style="background: #f5f5f5; padding: 20px; border-radius: 5px; overflow: auto; max-height: 500px;"></pre>
    1000                 </div>
    1001             </div>
    1002         </div>
    1003 
    1004         <script>
    1005             let currentComputer = null;
    1006             let allSysmonEvents = [];
    1007 
    1008             function updateStats() {
    1009                 // Ажурирај ги вкупните статистики
    1010                 fetch('/api/stats')
    1011                     .then(r => r.json())
    1012                     .then(data => {
    1013                         document.getElementById('total-sysmon').textContent = data.total_sysmon_events;
    1014                         document.getElementById('total-network').textContent = data.total_network_connections;
    1015                     });
    1016             }
    1017 
    1018             function showComputerDetails(computerName) {
    1019                 currentComputer = computerName;
    1020 
    1021                 // Покажи модал
    1022                 document.getElementById('computer-modal').style.display = 'block';
    1023                 document.getElementById('modal-title').textContent = computerName;
    1024 
    1025                 // Вчитувај податоци
    1026                 loadComputerDetails(computerName);
    1027             }
    1028 
    1029             function loadComputerDetails(computerName) {
    1030                 // Вчитувај целосни податоци за компјутерот
    1031                 fetch(`/api/computer/${encodeURIComponent(computerName)}`)
    1032                     .then(response => response.json())
    1033                     .then(data => {
    1034                         populateOverview(data);
    1035                         populateSysmonEvents(data);
    1036                         populateProcesses(data);
    1037                         populateNetwork(data);
    1038                         updateCharts(data);
    1039                     })
    1040                     .catch(error => {
    1041                         console.error('Error loading details:', error);
    1042                     });
    1043             }
    1044 
    1045             function populateOverview(data) {
    1046                 const overview = document.getElementById('overview-content');
    1047                 overview.innerHTML = `
    1048                     <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px;">
    1049                         <div>
    1050                             <h4>System Information</h4>
    1051                             <p><strong>User:</strong> ${data.computer.user}</p>
    1052                             <p><strong>IP:</strong> ${data.computer.ip}</p>
    1053                             <p><strong>OS:</strong> ${data.computer.os}</p>
    1054                             <p><strong>First Seen:</strong> ${new Date(data.computer.first_seen).toLocaleString()}</p>
    1055                             <p><strong>Last Seen:</strong> ${new Date(data.computer.last_seen).toLocaleString()}</p>
    1056                             <p><strong>Total Logs:</strong> ${data.computer.total_logs}</p>
    1057                         </div>
    1058                         <div>
    1059                             <h4>Current Status</h4>
    1060                             <p><strong>CPU Usage:</strong> ${data.current_metrics?.cpu_usage || 0}%</p>
    1061                             <p><strong>RAM Usage:</strong> ${data.current_metrics?.ram_usage || 0}%</p>
    1062                             <p><strong>Disk Usage:</strong> ${data.current_metrics?.disk_usage || 0}%</p>
    1063                             <p><strong>Network Sent:</strong> ${data.current_metrics?.network_sent_mb || 0} MB</p>
    1064                             <p><strong>Network Received:</strong> ${data.current_metrics?.network_recv_mb || 0} MB</p>
    1065                         </div>
    1066                     </div>
    1067                 `;
    1068             }
    1069 
    1070             function populateSysmonEvents(data) {
    1071                 const tableBody = document.getElementById('sysmon-events');
    1072                 tableBody.innerHTML = '';
    1073                 allSysmonEvents = data.sysmon_events || [];
    1074 
    1075                 allSysmonEvents.forEach(event => {
    1076                     const row = tableBody.insertRow();
    1077                     const time = new Date(event.timestamp).toLocaleTimeString();
    1078                     const severity = getSeverityForEvent(event.event_id);
    1079 
    1080                     row.innerHTML = `
    1081                         <td>${time}</td>
    1082                         <td>${event.event_id}</td>
    1083                         <td>${event.event_type}</td>
    1084                         <td><span class="severity-${severity}">${severity.toUpperCase()}</span></td>
    1085                         <td class="event-details">${event.message.substring(0, 100)}...</td>
    1086                         <td>
    1087                             <button class="btn" style="padding: 5px 10px; font-size: 0.9em;"
    1088                                     onclick="showEventDetails(${JSON.stringify(event).replace(/"/g, '&quot;')})">
    1089                                 View Details
    1090                             </button>
    1091                         </td>
    1092                     `;
    1093                 });
    1094             }
    1095 
    1096             function populateProcesses(data) {
    1097                 const tableBody = document.getElementById('processes-list');
    1098                 tableBody.innerHTML = '';
    1099 
    1100                 (data.recent_processes || []).forEach(proc => {
    1101                     const row = tableBody.insertRow();
    1102                     row.innerHTML = `
    1103                         <td>${proc.pid}</td>
    1104                         <td>${proc.name}</td>
    1105                         <td>${proc.username}</td>
    1106                         <td>${proc.cpu_percent || 0}</td>
    1107                         <td>${proc.memory_mb || 0} MB</td>
    1108                         <td class="event-details">${proc.cmdline || ''}</td>
    1109                     `;
    1110                 });
    1111             }
    1112 
    1113             function populateNetwork(data) {
    1114                 const tableBody = document.getElementById('network-connections');
    1115                 tableBody.innerHTML = '';
    1116 
    1117                 (data.network_connections || []).forEach(conn => {
    1118                     const row = tableBody.insertRow();
    1119                     const time = new Date(conn.timestamp).toLocaleTimeString();
    1120                     row.innerHTML = `
    1121                         <td>${conn.pid}</td>
    1122                         <td>${conn.process_name || 'N/A'}</td>
    1123                         <td>${conn.local_address || 'N/A'}</td>
    1124                         <td>${conn.remote_address || 'N/A'}</td>
    1125                         <td>${conn.status}</td>
    1126                         <td>${time}</td>
    1127                     `;
    1128                 });
    1129             }
    1130 
    1131             function updateCharts(data) {
    1132                 const ctx = document.getElementById('performance-chart').getContext('2d');
    1133                 const history = data.history || [];
    1134 
    1135                 const labels = history.slice(-20).map(h =>
    1136                     new Date(h.timestamp).toLocaleTimeString()
    1137                 );
    1138                 const cpuData = history.slice(-20).map(h => h.cpu_usage);
    1139                 const ramData = history.slice(-20).map(h => h.ram_usage);
    1140 
    1141                 if(window.performanceChart) {
    1142                     window.performanceChart.destroy();
    1143                 }
    1144 
    1145                 window.performanceChart = new Chart(ctx, {
    1146                     type: 'line',
    1147                     data: {
    1148                         labels: labels,
    1149                         datasets: [
    1150                             {
    1151                                 label: 'CPU Usage %',
    1152                                 data: cpuData,
    1153                                 borderColor: 'rgb(255, 99, 132)',
    1154                                 backgroundColor: 'rgba(255, 99, 132, 0.2)',
    1155                                 tension: 0.4
    1156                             },
    1157                             {
    1158                                 label: 'RAM Usage %',
    1159                                 data: ramData,
    1160                                 borderColor: 'rgb(54, 162, 235)',
    1161                                 backgroundColor: 'rgba(54, 162, 235, 0.2)',
    1162                                 tension: 0.4
    1163                             }
    1164                         ]
    1165                     },
    1166                     options: {
    1167                         responsive: true,
    1168                         maintainAspectRatio: false,
    1169                         scales: {
    1170                             y: {
    1171                                 beginAtZero: true,
    1172                                 max: 100
    1173                             }
    1174                         }
    1175                     }
    1176                 });
    1177             }
    1178 
    1179             function getSeverityForEvent(eventId) {
    1180                 const highSeverity = [1, 3, 8, 10]; // Process creation, network, remote thread, process access
    1181                 const mediumSeverity = [5, 6, 7, 11, 22]; // Process termination, driver/image load, file create, DNS
    1182 
    1183                 if (highSeverity.includes(parseInt(eventId))) return 'high';
    1184                 if (mediumSeverity.includes(parseInt(eventId))) return 'medium';
    1185                 return 'low';
    1186             }
    1187 
    1188             function showEventDetails(event) {
    1189                 document.getElementById('event-details-content').textContent =
    1190                     JSON.stringify(event, null, 2);
    1191                 document.getElementById('event-modal').style.display = 'block';
    1192             }
    1193 
    1194             function closeEventModal() {
    1195                 document.getElementById('event-modal').style.display = 'none';
    1196             }
    1197 
    1198             function filterEvents() {
    1199                 const searchTerm = document.getElementById('event-search').value.toLowerCase();
    1200                 const tableBody = document.getElementById('sysmon-events');
    1201                 tableBody.innerHTML = '';
    1202 
    1203                 allSysmonEvents
    1204                     .filter(event =>
    1205                         event.message.toLowerCase().includes(searchTerm) ||
    1206                         event.event_type.toLowerCase().includes(searchTerm) ||
    1207                         event.event_id.toString().includes(searchTerm)
    1208                     )
    1209                     .forEach(event => {
    1210                         const row = tableBody.insertRow();
    1211                         const time = new Date(event.timestamp).toLocaleTimeString();
    1212                         const severity = getSeverityForEvent(event.event_id);
    1213 
    1214                         row.innerHTML = `
    1215                             <td>${time}</td>
    1216                             <td>${event.event_id}</td>
    1217                             <td>${event.event_type}</td>
    1218                             <td><span class="severity-${severity}">${severity.toUpperCase()}</span></td>
    1219                             <td class="event-details">${event.message.substring(0, 100)}...</td>
    1220                             <td>
    1221                                 <button class="btn" style="padding: 5px 10px; font-size: 0.9em;"
    1222                                         onclick="showEventDetails(${JSON.stringify(event).replace(/"/g, '&quot;')})">
    1223                                     View Details
    1224                                 </button>
    1225                             </td>
    1226                         `;
    1227                     });
    1228             }
    1229 
    1230             function switchTab(tabName) {
    1231                 // Деактивирај сите tab-ови
    1232                 document.querySelectorAll('.tab-btn').forEach(btn => {
    1233                     btn.classList.remove('active');
    1234                 });
    1235                 document.querySelectorAll('.tab-content').forEach(tab => {
    1236                     tab.classList.remove('active');
    1237                 });
    1238 
    1239                 // Активирај избраниот tab
    1240                 document.querySelector(`[onclick="switchTab('${tabName}')"]`).classList.add('active');
    1241                 document.getElementById(`${tabName}-tab`).classList.add('active');
    1242             }
    1243 
    1244             function closeModal() {
    1245                 document.getElementById('computer-modal').style.display = 'none';
    1246                 currentComputer = null;
    1247                 allSysmonEvents = [];
    1248             }
    1249 
    1250             function exportData() {
    1251                 if (!currentComputer) return;
    1252 
    1253                 fetch(`/api/export/${encodeURIComponent(currentComputer)}`)
    1254                     .then(response => response.blob())
    1255                     .then(blob => {
    1256                         const url = window.URL.createObjectURL(blob);
    1257                         const a = document.createElement('a');
    1258                         a.href = url;
    1259                         a.download = `${currentComputer}_export_${new Date().toISOString().slice(0,10)}.json`;
    1260                         document.body.appendChild(a);
    1261                         a.click();
    1262                         document.body.removeChild(a);
    1263                     });
    1264             }
    1265 
    1266             // Auto-refresh
    1267             setInterval(() => {
    1268                 updateStats();
    1269 
    1270                 // Ажурирај детали ако модал е отворен
    1271                 if (currentComputer && document.getElementById('computer-modal').style.display === 'block') {
    1272                     loadComputerDetails(currentComputer);
    1273                 }
    1274             }, 30000);
    1275 
    1276             // Затвори модал со Escape
    1277             document.addEventListener('keydown', (e) => {
    1278                 if (e.key === 'Escape') {
    1279                     closeModal();
    1280                     closeEventModal();
    1281                 }
    1282             });
    1283 
    1284             // Иницијализирај статистики при старт
    1285             updateStats();
    1286         </script>
    1287     </body>
    1288     </html>
    1289     ''', computers=computers, socket=socket, datetime=datetime)
    1290 
    1291 
    1292 @app.route('/api/computer/<computer_name>')
    1293 def get_computer_details(computer_name):
    1294     """Добиј сите детални информации за еден компјутер"""
    1295     conn = sqlite3.connect(DB_FILE)
    1296     c = conn.cursor()
    1297 
    1298     # Основни информации
    1299     c.execute('''SELECT id, name, user, ip, os, first_seen, last_seen
    1300                  FROM computers WHERE name = ?''', (computer_name,))
    1301     computer_data = c.fetchone()
    1302 
    1303     if not computer_data:
    1304         conn.close()
    1305         return jsonify({"error": "Computer not found"}), 404
    1306 
    1307     computer_id = computer_data[0]
    1308 
    1309     # Број на сите записи
    1310     c.execute('SELECT COUNT(*) FROM computer_history WHERE computer_id = ?', (computer_id,))
    1311     total_logs = c.fetchone()[0]
    1312 
    1313     # Сите history записи
    1314     c.execute('''SELECT cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp
    1315                  FROM computer_history
    1316                  WHERE computer_id = ?
    1317                  ORDER BY timestamp DESC LIMIT 100''', (computer_id,))
    1318     history = []
    1319     for row in c.fetchall():
    1320         history.append({
    1321             'cpu_usage': row[0],
    1322             'ram_usage': row[1],
    1323             'disk_usage': row[2],
    1324             'network_sent_mb': row[3],
    1325             'network_recv_mb': row[4],
    1326             'timestamp': row[5]
    1327         })
    1328 
    1329     # Последни метрики
    1330     current_metrics = history[0] if history else {}
    1331 
    1332     # Последни процеси
    1333     c.execute('''SELECT pid, name, username, cpu_percent, memory_mb, cmdline, timestamp
    1334                  FROM computer_processes
    1335                  WHERE computer_id = ?
    1336                  ORDER BY timestamp DESC LIMIT 100''', (computer_id,))
    1337     recent_processes = []
    1338     for row in c.fetchall():
    1339         recent_processes.append({
    1340             'pid': row[0],
    1341             'name': row[1],
    1342             'username': row[2],
    1343             'cpu_percent': row[3],
    1344             'memory_mb': row[4],
    1345             'cmdline': row[5],
    1346             'timestamp': row[6]
    1347         })
    1348 
    1349     # SYSMON НАСТАНИ
    1350     c.execute('''SELECT event_id, event_type, message, timestamp, details
    1351                  FROM sysmon_events
    1352                  WHERE computer_id = ?
    1353                  ORDER BY timestamp DESC LIMIT 200''', (computer_id,))
    1354     sysmon_events = []
    1355     for row in c.fetchall():
    1356         try:
    1357             details = json.loads(row[4]) if row[4] else {}
    1358         except:
    1359             details = {}
    1360 
    1361         sysmon_events.append({
    1362             'event_id': row[0],
    1363             'event_type': row[1],
    1364             'message': row[2],
    1365             'timestamp': row[3],
    1366             'details': details
    1367         })
    1368 
    1369     # Мрежни конекции
    1370     c.execute('''SELECT pid, process_name, local_address, remote_address, status, timestamp
    1371                  FROM network_connections
    1372                  WHERE computer_id = ?
    1373                  ORDER BY timestamp DESC LIMIT 100''', (computer_id,))
    1374     network_connections = []
    1375     for row in c.fetchall():
    1376         network_connections.append({
    1377             'pid': row[0],
    1378             'process_name': row[1],
    1379             'local_address': row[2],
    1380             'remote_address': row[3],
    1381             'status': row[4],
    1382             'timestamp': row[5]
    1383         })
    1384 
    1385     conn.close()
    1386 
    1387     return jsonify({
    1388         'computer': {
    1389             'id': computer_data[0],
    1390             'name': computer_data[1],
    1391             'user': computer_data[2],
    1392             'ip': computer_data[3],
    1393             'os': computer_data[4],
    1394             'first_seen': computer_data[5],
    1395             'last_seen': computer_data[6],
    1396             'total_logs': total_logs
    1397         },
    1398         'history': history,
    1399         'current_metrics': current_metrics,
    1400         'recent_processes': recent_processes,
    1401         'sysmon_events': sysmon_events,
    1402         'network_connections': network_connections
    1403     })
    1404 
    1405 @app.route('/api/computers')
    1406 def get_computers():
    1407     conn = sqlite3.connect(DB_FILE)
    1408     c = conn.cursor()
    1409     env = request.headers.get("X-Env", "default")
    1410 
    1411     c.execute('''SELECT name, user, ip, os, first_seen, last_seen, sysmon_available
    1412                  FROM computers
    1413                  WHERE env_name = ?
    1414                  ORDER BY last_seen DESC''', (env,))
    1415 
    1416     computers = []
    1417     for row in c.fetchall():
    1418         computer_name = row[0]
    1419 
    1420         # recent logs
    1421         c.execute('''SELECT COUNT(*) FROM computer_history
    1422                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    1423                      AND timestamp > datetime('now', '-24 hours')''',
    1424                   (computer_name,))
    1425         recent_logs = c.fetchone()[0]
    1426 
    1427         # last 5 metrics
    1428         c.execute('''SELECT cpu_usage, ram_usage, timestamp
    1429                      FROM computer_history
    1430                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    1431                      ORDER BY timestamp DESC LIMIT 5''',
    1432                   (computer_name,))
    1433         recent_metrics = c.fetchall()
    1434 
    1435         c.execute('''SELECT COUNT(*) FROM sysmon_events
    1436                      WHERE computer_id = (SELECT id FROM computers WHERE name = ?)
    1437                      AND timestamp > datetime('now', '-24 hours')''',
    1438                   (computer_name,))
    1439         recent_sysmon = c.fetchone()[0]
    1440 
    1441         avg_cpu = sum(m[0] for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
    1442         avg_ram = sum(m[1] for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
    1443 
    1444         last_seen = datetime.fromisoformat(row[5])
    1445         time_diff = (datetime.now() - last_seen).seconds
    1446 
    1447         if time_diff < 60:
    1448             status = 'online'
    1449             status_color = 'green'
    1450         elif time_diff < 300:
    1451             status = 'idle'
    1452             status_color = 'orange'
    1453         else:
    1454             status = 'offline'
    1455             status_color = 'gray'
    1456 
    1457         computers.append({
    1458             'name': row[0],
    1459             'user': row[1],
    1460             'ip': row[2],
    1461             'os': row[3],
    1462             'first_seen': row[4],
    1463             'last_seen': row[5],
    1464             'sysmon_available': bool(row[6]),
    1465             'recent_logs': recent_logs,
    1466             'recent_sysmon': recent_sysmon,
    1467             'avg_cpu': round(avg_cpu, 1),
    1468             'avg_ram': round(avg_ram, 1),
    1469             'status': status,
    1470             'status_color': status_color
    1471         })
    1472 
    1473     conn.close()
    1474     return jsonify(computers)
    1475 
    1476 @app.route('/api/stats')
    1477 def get_stats():
    1478     """Добиј глобални статистики"""
    1479     conn = sqlite3.connect(DB_FILE)
    1480     c = conn.cursor()
    1481 
    1482     # Вкупно Sysmon настани за последните 24 часа
    1483     c.execute('''SELECT COUNT(*) FROM sysmon_events
    1484                  WHERE timestamp > datetime('now', '-24 hours')''')
    1485     total_sysmon_events = c.fetchone()[0]
    1486 
    1487     # Вкупно мрежни конекции за последните 24 часа
    1488     c.execute('''SELECT COUNT(*) FROM network_connections
    1489                  WHERE timestamp > datetime('now', '-24 hours')''')
    1490     total_network_connections = c.fetchone()[0]
    1491 
    1492     conn.close()
    1493 
    1494     return jsonify({
    1495         'total_sysmon_events': total_sysmon_events,
    1496         'total_network_connections': total_network_connections,
    1497         'server_time': datetime.now().isoformat()
    1498     })
    1499 
    1500 def build_rag_context(question, computer_name=None, limit_per_table=10):
    1501     try:
    1502         conn = sqlite3.connect(DB_FILE)
    1503         c = conn.cursor()
    1504 
    1505         params = []
    1506         comp_filter = ""
    1507         if computer_name:
    1508             comp_filter = "AND computer_id = (SELECT id FROM computers WHERE name = ?)"
    1509             params.append(computer_name)
    1510 
    1511         # Sysmon настани
    1512         c.execute(
    1513             f"""
    1514             SELECT timestamp, event_id, event_type, message
    1515             FROM sysmon_events
    1516             WHERE 1=1 {comp_filter}
    1517             ORDER BY timestamp DESC
    1518             LIMIT ?
    1519             """,
    1520             params + [limit_per_table],
    1521         )
    1522         sysmon_rows = c.fetchall()
    1523 
    1524         # Перформанс историја
    1525         c.execute(
    1526             f"""
    1527             SELECT timestamp, cpu_usage, ram_usage, disk_usage,
    1528                    network_sent_mb, network_recv_mb
    1529             FROM computer_history
    1530             WHERE 1=1 {comp_filter}
    1531             ORDER BY timestamp DESC
    1532             LIMIT ?
    1533             """,
    1534             params + [limit_per_table],
    1535         )
    1536         perf_rows = c.fetchall()
    1537 
    1538         # Процеси
    1539         c.execute(
    1540             f"""
    1541             SELECT timestamp, pid, name, cpu_percent, memory_mb
    1542             FROM computer_processes
    1543             WHERE 1=1 {comp_filter}
    1544             ORDER BY timestamp DESC
    1545             LIMIT ?
    1546             """,
    1547             params + [limit_per_table],
    1548         )
    1549         proc_rows = c.fetchall()
    1550 
    1551         conn.close()
    1552 
    1553         print("[RAG] sysmon:", len(sysmon_rows),
    1554               "| perf:", len(perf_rows),
    1555               "| proc:", len(proc_rows))
    1556 
    1557         lines = []
    1558 
    1559         for ts, eid, etype, msg in sysmon_rows:
    1560             lines.append(f"[SYSMON] {ts} | Event {eid} ({etype}): {msg}")
    1561 
    1562         for ts, cpu, ram, disk, nsent, nrecv in perf_rows:
    1563             lines.append(
    1564                 f"[PERF] {ts} | CPU {cpu}% | RAM {ram}% | Disk {disk}% | "
    1565                 f"Net sent {nsent}MB / recv {nrecv}MB"
    1566             )
    1567 
    1568         for ts, pid, name, cpu, mem in proc_rows:
    1569             lines.append(f"[PROC] {ts} | PID {pid} | {name} | CPU {cpu}% | RAM {mem}MB")
    1570 
    1571         context = "\n".join(lines)
    1572         print("[RAG] sample context:\n", "\n".join(lines[:5]))
    1573         return context
    1574 
    1575     except Exception as e:
    1576         traceback.print_exc()
    1577         return ""
    1578 
    1579 # def ask_llm_with_context(question, context, language="mk"):
    1580 #     if not client:
    1581 #         return "Немаш поставено OPENAI_API_KEY на серверот."
    1582 #
    1583 #     system_msg = (
    1584 #         "Ти си асистент за безбедност и систем мониторинг. "
    1585 #         "Одговараш базирано ИСКЛУЧИВО на дадените логови што ти се дадени. "
    1586 #         "Ако нема доволно информации, кажи дека нема доволно податоци. "
    1587 #         "Објаснувај кратко и јасно."
    1588 #     )
    1589 #
    1590 #     user_msg = f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"
    1591 #
    1592 #     try:
    1593 #         completion = client.chat.completions.create(
    1594 #             model="gpt-4.1-mini",
    1595 #             messages=[
    1596 #                 {"role": "system", "content": system_msg},
    1597 #                 {"role": "user", "content": user_msg},
    1598 #             ],
    1599 #             temperature=0.2,
    1600 #         )
    1601 #         return completion.choices[0].message.content
    1602 #     except Exception as e:
    1603 #         # Може попрецизно: openai.APITimeoutError, openai.APIConnectionError итн.
    1604 #         return f"Грешка при повик кон LLM: {e}"
    1605 
    1606 def ask_llm_with_context(question, context, language="mk"):
    1607     """
    1608     Вика OpenAI модел и враќа одговор базиран на контекстот од логови.
    1609     """
    1610     if not client:
    1611         return "Немаш поставено OPENAI_API_KEY на серверот."
    1612 
    1613     system_msg = (
    1614         "Ти си асистент за безбедност и систем мониторинг. "
    1615         "Одговараш базирано ИСКЛУЧИВО на дадените логови што ти се дадени. "
    1616         "Типови редови во логовите:\n"
    1617         "- [SYSMON] ... Sysmon безбедносни настани.\n"
    1618         "- [PERF] ... Општи перформанси (CPU, RAM, диск, мрежа).\n"
    1619         "- [PROC] ... Информации за поединечни процеси: PID, име, CPU%, RAM MB.\n"
    1620         "Кога корисникот прашува за процеси и нивна потрошувачка, "
    1621         "СЕКОГАШ користи ги [PROC] редовите. "
    1622         "Никогаш не кажувај дека 'нема информации за процеси' ако постои барем еден [PROC] ред. "
    1623         "Ако навистина НЕМА [PROC] редови, тогаш можеш да кажеш дека нема доволно податоци. "
    1624         "Објаснувај кратко и јасно."
    1625     )
    1626 
    1627     user_msg = f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"
    1628 
    1629     try:
    1630         print("[LLM] Calling OpenAI…")
    1631         completion = client.chat.completions.create(
    1632             model="gpt-4.1-mini",
    1633             messages=[
    1634                 {"role": "system", "content": system_msg},
    1635                 {"role": "user", "content": user_msg},
    1636             ],
    1637             temperature=0.2,
    1638         )
    1639         print("[LLM] Got response")
    1640         return completion.choices[0].message.content
    1641 
    1642     except Exception as e:
    1643         print("[LLM] ERROR:", e)
    1644         return f"Грешка при повик кон LLM: {e}"
    1645 
    1646 
    1647 
    1648 #
    1649 # def ask_llm_with_context(question, context, language="mk"):
    1650 #     payload = {
    1651 #         "model": "deepseek-r1:1.5b",
    1652 #         "prompt": f"Прашање: {question}\n\nКонтекст:\n{context}",
    1653 #         "stream": False
    1654 #     }
    1655 #
    1656 #     try:
    1657 #         r = requests.post("http://localhost:11434/api/generate", json=payload)
    1658 #         r.raise_for_status()
    1659 #         data = r.json()
    1660 #         return data.get("response", "")
    1661 #     except Exception as e:
    1662 #         return f"Грешка при повик кон Ollama: {e}"
    1663 
    1664 
    1665 @app.route('/api/export/<computer_name>')
    1666 def export_computer_data(computer_name):
    1667     """Експортирај ги сите податоци за еден компјутер"""
    1668     data = {}
    1669 
    1670     conn = sqlite3.connect(DB_FILE)
    1671     c = conn.cursor()
    1672 
    1673     # Основни информации за компјутерот
    1674     c.execute('''SELECT * FROM computers WHERE name = ?''', (computer_name,))
    1675     columns = [description[0] for description in c.description]
    1676     computer_data = c.fetchone()
    1677 
    1678     if computer_data:
    1679         data['computer'] = dict(zip(columns, computer_data))
    1680 
    1681         # Историја
    1682         computer_id = computer_data[0]
    1683         c.execute('''SELECT * FROM computer_history WHERE computer_id = ? ORDER BY timestamp''', (computer_id,))
    1684         data['history'] = [dict(zip([d[0] for d in c.description], row)) for row in c.fetchall()]
    1685 
    1686         # Sysmon настани
    1687         c.execute('''SELECT * FROM sysmon_events WHERE computer_id = ? ORDER BY timestamp''', (computer_id,))
    1688         data['sysmon_events'] = [dict(zip([d[0] for d in c.description], row)) for row in c.fetchall()]
    1689 
    1690         # Процеси
    1691         c.execute('''SELECT * FROM computer_processes WHERE computer_id = ? ORDER BY timestamp''', (computer_id,))
    1692         data['processes'] = [dict(zip([d[0] for d in c.description], row)) for row in c.fetchall()]
    1693 
    1694         # Мрежни конекции
    1695         c.execute('''SELECT * FROM network_connections WHERE computer_id = ? ORDER BY timestamp''', (computer_id,))
    1696         data['network_connections'] = [dict(zip([d[0] for d in c.description], row)) for row in c.fetchall()]
    1697 
    1698     conn.close()
    1699 
    1700     # Креирај JSON одговор
    1701     from flask import Response
    1702     response = Response(
    1703         json.dumps(data, indent=2, default=str),
    1704         mimetype='application/json',
    1705         headers={'Content-Disposition': f'attachment; filename={computer_name}_export.json'}
    1706     )
    1707 
    1708     return response
    1709 
    1710 
    1711 @app.route('/ping')
    1712 def ping():
    1713     return jsonify({
    1714         'status': 'alive',
    1715         'server_time': datetime.now().isoformat(),
    1716         'version': '2.0_sysmon',
    1717         'database': DB_FILE
    1718     })
    1719 
    1720 
    1721 def get_local_ip():
    1722     try:
    1723         s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    1724         s.connect(("8.8.8.8", 80))
    1725         local_ip = s.getsockname()[0]
    1726         s.close()
    1727         return local_ip
    1728     except:
    1729         return "127.0.0.1"
    1730 
    1731 
    1732 def cleanup_old_data():
    1733     """Чисти стари податоци од базата (стари од 30 дена)"""
    1734     while True:
    1735         time.sleep(3600)  # Чисти на секој час
    1736         try:
    1737             conn = sqlite3.connect(DB_FILE)
    1738             c = conn.cursor()
    1739 
    1740             cutoff_date = (datetime.now() - timedelta(days=30)).isoformat()
    1741 
    1742             # Избриши стари записи
    1743             c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff_date,))
    1744             c.execute("DELETE FROM computer_processes WHERE timestamp < ?", (cutoff_date,))
    1745             c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff_date,))
    1746             c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff_date,))
    1747 
    1748             conn.commit()
    1749             conn.close()
    1750 
    1751             print(f"[Cleanup] Избришани стари записи пред {cutoff_date}")
    1752 
    1753         except Exception as e:
    1754             print(f"[Cleanup] Грешка: {e}")
    1755 
    1756 import time
    1757 @app.route("/api/chat", methods=["POST"])
    1758 def api_chat():
    1759     try:
    1760         t0 = time.time()
    1761         data = request.get_json(force=True) or {}
    1762         question = data.get("question", "").strip()
    1763         computer_name = data.get("computer_name") or None
    1764 
    1765         if not question:
    1766             return jsonify({"error": "No question provided"}), 400
    1767 
    1768         context = build_rag_context(question, computer_name=computer_name)
    1769         t1 = time.time()
    1770 
    1771         answer = ask_llm_with_context(question, context)
    1772         t2 = time.time()
    1773 
    1774         print(f"[CHAT] build_rag_context: {t1 - t0:.2f}s, LLM: {t2 - t1:.2f}s")
    1775 
    1776         return jsonify({"answer": answer})
    1777     except Exception as e:
    1778         traceback.print_exc()
    1779         return jsonify({"error": str(e)}), 500
    1780 
    1781 
    1782 @app.route('/api/computer/<computer_name>/sysmon')
    1783 def get_sysmon_processes(computer_name):
    1784     """Добиј Sysmon процеси за компјутер"""
    1785     conn = sqlite3.connect(DB_FILE)
    1786     c = conn.cursor()
    1787 
    1788     # Прво најди го computer_id
    1789     c.execute('SELECT id FROM computers WHERE name = ?', (computer_name,))
    1790     result = c.fetchone()
    1791 
    1792     if not result:
    1793         conn.close()
    1794         return jsonify({"error": "Computer not found"}), 404
    1795 
    1796     computer_id = result[0]
    1797 
    1798     # Земaj ги Sysmon процесите (последни 100)
    1799     c.execute('''SELECT
    1800                  event_id,
    1801                  event_type,
    1802                  message,
    1803                  timestamp,
    1804                  details
    1805                  FROM sysmon_events
    1806                  WHERE computer_id = ?
    1807                  ORDER BY timestamp DESC LIMIT 100''', (computer_id,))
    1808 
    1809     processes = []
    1810     for row in c.fetchall():
    1811         try:
    1812             details = json.loads(row[4]) if row[4] else {}
    1813         except:
    1814             details = {}
    1815 
    1816         processes.append({
    1817             'process_id': row[0],  # Event ID како PID
    1818             'process_name': row[1],  # Event type како име
    1819             'timestamp': row[3],
    1820             'message': row[2],
    1821             'details': details
    1822         })
    1823 
    1824     conn.close()
    1825 
    1826     return jsonify({
    1827         'computer_name': computer_name,
    1828         'processes': processes,
    1829         'count': len(processes)
    1830     })
    1831 
    1832 
    1833 @app.route('/api/computer/<computer_name>/network')
    1834 def get_network_connections(computer_name):
    1835     """Добиј мрежни конекции за компјутер"""
    1836     conn = sqlite3.connect(DB_FILE)
    1837     c = conn.cursor()
    1838 
    1839     # Прво најди го computer_id
    1840     c.execute('SELECT id FROM computers WHERE name = ?', (computer_name,))
    1841     result = c.fetchone()
    1842 
    1843     if not result:
    1844         conn.close()
    1845         return jsonify({"error": "Computer not found"}), 404
    1846 
    1847     computer_id = result[0]
    1848 
    1849     # Земaj ги мрежните конекции (последни 100)
    1850     c.execute('''SELECT
    1851                  pid,
    1852                  process_name,
    1853                  local_address,
    1854                  remote_address,
    1855                  status,
    1856                  timestamp
    1857                  FROM network_connections
    1858                  WHERE computer_id = ?
    1859                  ORDER BY timestamp DESC LIMIT 100''', (computer_id,))
    1860 
    1861     connections = []
    1862     for row in c.fetchall():
    1863         # Парсирај IP и порта
    1864         local = row[2] or "N/A"
    1865         remote = row[3] or "N/A"
    1866 
    1867         connections.append({
    1868             'pid': row[0],
    1869             'process_name': row[1] or 'Unknown',
    1870             'local_address': local,
    1871             'remote_address': remote,
    1872             'local_port': local.split(':')[-1] if ':' in local else '',
    1873             'remote_port': remote.split(':')[-1] if ':' in remote else '',
    1874             'protocol': 'TCP' if ':443' in local or ':80' in local else 'Unknown',
    1875             'state': row[4] or 'ESTABLISHED',
    1876             'timestamp': row[5]
    1877         })
    1878 
    1879     conn.close()
    1880 
    1881     return jsonify({
    1882         'computer_name': computer_name,
    1883         'connections': connections,
    1884         'count': len(connections)
    1885     })
    1886 
    1887 
    1888 @app.route('/api/computer/<computer_name>/detailed-sysmon')
    1889 def get_detailed_sysmon(computer_name):
    1890     """Добиј детални Sysmon процеси со повеќе информации"""
    1891     conn = sqlite3.connect(DB_FILE)
    1892     c = conn.cursor()
    1893 
    1894     c.execute('SELECT id FROM computers WHERE name = ?', (computer_name,))
    1895     result = c.fetchone()
    1896 
    1897     if not result:
    1898         conn.close()
    1899         return jsonify({"error": "Computer not found"}), 404
    1900 
    1901     computer_id = result[0]
    1902 
    1903     c.execute('''SELECT event_id, event_type, message, timestamp, details
    1904                  FROM sysmon_events
    1905                  WHERE computer_id = ?
    1906                  ORDER BY timestamp DESC LIMIT 50''', (computer_id,))
    1907 
    1908     detailed_processes = []
    1909 
    1910     for row in c.fetchall():
    1911         event_id = row[0]
    1912         event_type = row[1]
    1913         message = row[2]
    1914         timestamp = row[3]
    1915         details_str = row[4]
    1916 
    1917         # Пробај да го парсираш details JSON
    1918         try:
    1919             details = json.loads(details_str) if details_str else {}
    1920         except:
    1921             details = {}
    1922 
    1923         # Екстрактирај информации од details
    1924         process_info = {
    1925             'event_id': event_id,
    1926             'event_type': event_type,
    1927             'timestamp': timestamp,
    1928             'message': message,
    1929             'process_name': details.get('ProcessName', 'Unknown'),
    1930             'process_id': details.get('ProcessId', event_id),
    1931             'command_line': details.get('CommandLine', ''),
    1932             'parent_process': details.get('ParentProcessName', ''),
    1933             'parent_pid': details.get('ParentProcessId', ''),
    1934             'user': details.get('User', ''),
    1935             'details': details
    1936         }
    1937 
    1938         # Додади дополнителни полиња за различни типови на настани
    1939         if event_id in [3, 22]:  # Network connection or DNS
    1940             process_info['source_ip'] = details.get('SourceIp', '')
    1941             process_info['dest_ip'] = details.get('DestinationIp', '')
    1942             process_info['dest_port'] = details.get('DestinationPort', '')
    1943 
    1944         detailed_processes.append(process_info)
    1945 
    1946     conn.close()
    1947 
    1948     return jsonify({
    1949         'computer_name': computer_name,
    1950         'processes': detailed_processes,
    1951         'count': len(detailed_processes)
    1952     })
    1953 @app.route("/api/admin/login", methods=["POST"])
    1954 def admin_login():
    1955     data = request.get_json(force=True) or {}
    1956     key = (data.get("admin_key") or "").strip()
    1957     if not ADMIN_KEY or key != ADMIN_KEY:
    1958         return jsonify({"error": "Unauthorized"}), 401
    1959 
    1960     session_token = secrets.token_urlsafe(32)
    1961     conn = sqlite3.connect(DB_FILE)
    1962     c = conn.cursor()
    1963     c.execute("INSERT INTO admin_sessions(token, created_at) VALUES(?, datetime('now'))", (session_token,))
    1964     conn.commit()
    1965     conn.close()
    1966 
    1967     return jsonify({"ok": True, "session": session_token})
    1968 
    1969 
    1970 @app.route("/api/admin/environments", methods=["GET"])
    1971 def admin_list_envs():
    1972     if not require_admin_session():
    1973         return jsonify({"error": "Unauthorized"}), 401
    1974 
    1975     conn = sqlite3.connect(DB_FILE)
    1976     c = conn.cursor()
    1977     c.execute("SELECT name FROM environments ORDER BY name")
    1978     envs = [r[0] for r in c.fetchall()]
    1979     conn.close()
    1980 
     516    if not envs:
     517        envs = ["default"]
    1981518    return jsonify({"environments": envs})
    1982519
    1983520
    1984521@app.route("/api/admin/environments", methods=["POST"])
     522@require_tenant_admin()
    1985523def admin_create_env():
    1986     if not require_admin_session():
    1987         return jsonify({"error": "Unauthorized"}), 401
    1988 
    1989     data = request.get_json(force=True) or {}
     524    tenant_id = request.user["tenant_id"]
     525    data = request.get_json(force=True, silent=True) or {}
    1990526    name = (data.get("name") or "").strip()
    1991527    if not name:
    1992528        return jsonify({"error": "Missing env name"}), 400
    1993529
    1994     conn = sqlite3.connect(DB_FILE)
     530    conn = db()
    1995531    c = conn.cursor()
    1996532    try:
    1997         c.execute("INSERT INTO environments(name, created_at) VALUES(?, datetime('now'))", (name,))
     533        c.execute(
     534            "INSERT INTO environments(tenant_id, name, created_at) VALUES(?, ?, datetime('now'))",
     535            (tenant_id, name),
     536        )
    1998537        conn.commit()
    1999538    except Exception as e:
     
    2001540        return jsonify({"error": str(e)}), 400
    2002541    conn.close()
    2003 
    2004542    return jsonify({"ok": True, "name": name})
    2005543
    2006544
    2007545@app.route("/api/admin/tokens", methods=["POST"])
     546@require_tenant_admin()
    2008547def admin_generate_token():
    2009     if not require_admin_session():
    2010         return jsonify({"error": "Unauthorized"}), 401
    2011 
    2012     data = request.get_json(force=True) or {}
     548    tenant_id = request.user["tenant_id"]
     549    data = request.get_json(force=True, silent=True) or {}
    2013550    env = (data.get("env") or "").strip()
    2014551    if not env:
    2015552        return jsonify({"error": "Missing env"}), 400
    2016553
    2017     conn = sqlite3.connect(DB_FILE)
     554    conn = db()
    2018555    c = conn.cursor()
    2019 
    2020     c.execute("SELECT 1 FROM environments WHERE name = ?", (env,))
     556    c.execute("SELECT 1 FROM environments WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, env))
    2021557    if not c.fetchone():
    2022558        conn.close()
     
    2025561    token = secrets.token_urlsafe(32)
    2026562    c.execute("""
    2027         INSERT INTO env_tokens(env_name, token, created_at, expires_at)
    2028         VALUES(?, ?, datetime('now'), datetime('now', '+15 minutes'))
    2029     """, (env, token))
    2030 
     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))
    2031566    conn.commit()
    2032567    conn.close()
     
    2034569    return jsonify({"ok": True, "env": env, "token": token})
    2035570
    2036 if __name__ == '__main__':
    2037     init_database()
    2038     init_admin_tables()
    2039 
    2040 
    2041     # Стартувај cleanup thread
     571
     572# ----------------------------
     573# Agent endpoint
     574# ----------------------------
     575@app.route("/receive", methods=["POST"])
     576def receive_data():
     577    try:
     578        data = request.get_json(force=True, silent=True) or {}
     579        if not data:
     580            return jsonify({"error": "No data"}), 400
     581
     582        env_name, tenant_id = get_env_from_token(request)
     583        if not env_name or not tenant_id:
     584            return jsonify({"error": "Missing or invalid X-Env-Token"}), 401
     585
     586        info = data.get("info") or {}
     587        computer_name = info.get("computer_name")
     588        if not computer_name:
     589            return jsonify({"error": "Missing info.computer_name"}), 400
     590
     591        now_iso = datetime.now().isoformat()
     592        security_data = data.get("security_data") or {}
     593
     594        conn = db()
     595        c = conn.cursor()
     596
     597        # Find by (tenant_id + name)
     598        c.execute("SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, computer_name))
     599        row = c.fetchone()
     600
     601        if row:
     602            computer_id = row["id"]
     603            c.execute("""
     604                    UPDATE computers
     605                    SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
     606                    WHERE id=? AND tenant_id=?
     607                """, (
     608                info.get("user"),
     609                info.get("ip_address"),
     610                info.get("os"),
     611                now_iso,
     612                int(info.get("is_sysmon_available", 0) or 0),
     613                env_name,
     614                computer_id,
     615                tenant_id,
     616            ))
     617        else:
     618            c.execute("""
     619                    INSERT INTO computers(tenant_id, env_name, name, user, ip, os, first_seen, last_seen, sysmon_available)
     620                    VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
     621                """, (
     622                tenant_id,
     623                env_name,
     624                computer_name,
     625                info.get("user"),
     626                info.get("ip_address"),
     627                info.get("os"),
     628                now_iso,
     629                now_iso,
     630                int(info.get("is_sysmon_available", 0) or 0),
     631            ))
     632            computer_id = c.lastrowid
     633
     634        # history row
     635        c.execute("""
     636                INSERT INTO computer_history(computer_id, cpu_usage, ram_usage, disk_usage,
     637                                             network_sent_mb, network_recv_mb, timestamp)
     638                VALUES(?, ?, ?, ?, ?, ?, ?)
     639            """, (
     640            computer_id,
     641            float(info.get("cpu_usage") or 0),
     642            float(info.get("ram_usage") or 0),
     643            float(info.get("disk_usage") or 0),
     644            float(info.get("network_sent_mb") or 0),
     645            float(info.get("network_recv_mb") or 0),
     646            info.get("timestamp") or now_iso,
     647        ))
     648
     649        # processes
     650        for proc in (data.get("processes") or []):
     651            c.execute("""
     652                    INSERT INTO computer_processes(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp)
     653                    VALUES(?, ?, ?, ?, ?, ?, ?, ?)
     654                """, (
     655                computer_id,
     656                proc.get("pid"),
     657                proc.get("name"),
     658                float(proc.get("cpu_percent") or 0),
     659                float(proc.get("memory_mb") or 0),
     660                proc.get("user") or proc.get("username"),
     661                proc.get("cmdline"),
     662                info.get("timestamp") or now_iso,
     663            ))
     664
     665        # sysmon events
     666        sysmon_count = 0
     667        for ev in (security_data.get("sysmon_events") or []):
     668            c.execute("""
     669                    INSERT INTO sysmon_events(computer_id, event_id, event_type, message, timestamp, details)
     670                    VALUES(?, ?, ?, ?, ?, ?)
     671                """, (
     672                computer_id,
     673                ev.get("event_id"),
     674                ev.get("event_type", "Unknown"),
     675                ev.get("message", ""),
     676                ev.get("timestamp") or (info.get("timestamp") or now_iso),
     677                json.dumps(ev, ensure_ascii=False),
     678            ))
     679            sysmon_count += 1
     680
     681        # network connections
     682        for nc in (security_data.get("network_connections") or []):
     683            c.execute("""
     684                    INSERT INTO network_connections(computer_id, pid, local_address, remote_address, status, process_name, timestamp)
     685                    VALUES(?, ?, ?, ?, ?, ?, ?)
     686                """, (
     687                computer_id,
     688                nc.get("pid"),
     689                nc.get("local_address"),
     690                nc.get("remote_address"),
     691                nc.get("status"),
     692                nc.get("process_name"),
     693                info.get("timestamp") or now_iso,
     694            ))
     695
     696        conn.commit()
     697        conn.close()
     698
     699        # optional JSON archive
     700        try:
     701            save_json_file(data)
     702        except Exception:
     703            pass
     704
     705        return jsonify({
     706            "ok": True,
     707            "computer_id": computer_id,
     708            "env": env_name,
     709            "tenant_id": tenant_id,
     710            "sysmon_events_saved": sysmon_count,
     711            "server_time": now_iso,
     712        })
     713
     714    except Exception as e:
     715        traceback.print_exc()
     716        return jsonify({"error": str(e)}), 500
     717
     718
     719# ----------------------------
     720# Dashboard API (tenant scoped)
     721# ----------------------------
     722@app.route("/api/computers", methods=["GET"])
     723@require_user()
     724def api_computers():
     725    tenant_id = request.user["tenant_id"]
     726    env = request.headers.get("X-Env", "default")
     727
     728    conn = db()
     729    c = conn.cursor()
     730    c.execute("""
     731            SELECT id, name, user, ip, os, first_seen, last_seen, sysmon_available
     732            FROM computers
     733            WHERE tenant_id=? AND env_name=?
     734            ORDER BY last_seen DESC
     735        """, (tenant_id, env))
     736    rows = c.fetchall()
     737
     738    computers = []
     739    for r in rows:
     740        cid = r["id"]
     741
     742        c.execute("""
     743                SELECT COUNT(*) AS n
     744                FROM computer_history
     745                WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
     746            """, (cid,))
     747        recent_logs = int(c.fetchone()["n"] or 0)
     748
     749        c.execute("""
     750                SELECT COUNT(*) AS n
     751                FROM sysmon_events
     752                WHERE computer_id=? AND timestamp > datetime('now','-24 hours')
     753            """, (cid,))
     754        recent_sysmon = int(c.fetchone()["n"] or 0)
     755
     756        c.execute("""
     757                SELECT cpu_usage, ram_usage, timestamp
     758                FROM computer_history
     759                WHERE computer_id=?
     760                ORDER BY timestamp DESC
     761                LIMIT 5
     762            """, (cid,))
     763        metrics = c.fetchall()
     764        avg_cpu = round(sum(m["cpu_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
     765        avg_ram = round(sum(m["ram_usage"] for m in metrics) / len(metrics), 1) if metrics else 0.0
     766
     767        status = "offline"
     768        status_color = "gray"
     769        try:
     770            last_seen = r["last_seen"]
     771            if last_seen:
     772                dt = datetime.fromisoformat(
     773                    last_seen.replace("Z", "+00:00")) if "T" in last_seen else datetime.fromisoformat(last_seen)
     774                diff = (datetime.now() - dt).total_seconds()
     775                if diff < 60:
     776                    status, status_color = "online", "green"
     777                elif diff < 300:
     778                    status, status_color = "idle", "orange"
     779        except Exception:
     780            pass
     781
     782        computers.append({
     783            "name": r["name"],
     784            "user": r["user"],
     785            "ip": r["ip"],
     786            "os": r["os"],
     787            "first_seen": r["first_seen"],
     788            "last_seen": r["last_seen"],
     789            "sysmon_available": bool(r["sysmon_available"]),
     790            "recent_logs": recent_logs,
     791            "recent_sysmon": recent_sysmon,
     792            "avg_cpu": avg_cpu,
     793            "avg_ram": avg_ram,
     794            "status": status,
     795            "status_color": status_color,
     796        })
     797
     798    conn.close()
     799    return jsonify(computers)
     800
     801
     802@app.route("/api/stats", methods=["GET"])
     803@require_user()
     804def api_stats():
     805    tenant_id = request.user["tenant_id"]
     806
     807    conn = db()
     808    c = conn.cursor()
     809
     810    c.execute("""
     811            SELECT COUNT(*) AS n
     812            FROM sysmon_events s
     813            JOIN computers c2 ON c2.id = s.computer_id
     814            WHERE s.timestamp > datetime('now','-24 hours')
     815              AND c2.tenant_id = ?
     816        """, (tenant_id,))
     817    total_sysmon = int(c.fetchone()["n"] or 0)
     818
     819    c.execute("""
     820            SELECT COUNT(*) AS n
     821            FROM network_connections n
     822            JOIN computers c2 ON c2.id = n.computer_id
     823            WHERE n.timestamp > datetime('now','-24 hours')
     824              AND c2.tenant_id = ?
     825        """, (tenant_id,))
     826    total_net = int(c.fetchone()["n"] or 0)
     827
     828    conn.close()
     829    return jsonify({
     830        "total_sysmon_events": total_sysmon,
     831        "total_network_connections": total_net,
     832        "server_time": datetime.now().isoformat(),
     833    })
     834
     835
     836@app.route("/api/computer/<computer_name>", methods=["GET"])
     837@require_user()
     838def api_computer_details(computer_name):
     839    tenant_id = request.user["tenant_id"]
     840
     841    conn = db()
     842    c = conn.cursor()
     843    c.execute("""
     844            SELECT id, name, user, ip, os, first_seen, last_seen, env_name
     845            FROM computers
     846            WHERE tenant_id=? AND name=?
     847            LIMIT 1
     848        """, (tenant_id, computer_name))
     849    comp = c.fetchone()
     850    if not comp:
     851        conn.close()
     852        return jsonify({"error": "Computer not found"}), 404
     853
     854    computer_id = comp["id"]
     855
     856    c.execute("SELECT COUNT(*) AS n FROM computer_history WHERE computer_id=?", (computer_id,))
     857    total_logs = int(c.fetchone()["n"] or 0)
     858
     859    c.execute("""
     860            SELECT cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp
     861            FROM computer_history
     862            WHERE computer_id=?
     863            ORDER BY timestamp DESC
     864            LIMIT 100
     865        """, (computer_id,))
     866    history = [dict(r) for r in c.fetchall()]
     867    current_metrics = history[0] if history else {}
     868
     869    c.execute("""
     870            SELECT pid, name, username, cpu_percent, memory_mb, cmdline, timestamp
     871            FROM computer_processes
     872            WHERE computer_id=?
     873            ORDER BY timestamp DESC
     874            LIMIT 100
     875        """, (computer_id,))
     876    processes = [dict(r) for r in c.fetchall()]
     877
     878    c.execute("""
     879            SELECT event_id, event_type, message, timestamp, details
     880            FROM sysmon_events
     881            WHERE computer_id=?
     882            ORDER BY timestamp DESC
     883            LIMIT 200
     884        """, (computer_id,))
     885    sysmon = []
     886    for r in c.fetchall():
     887        d = {}
     888        try:
     889            d = json.loads(r["details"]) if r["details"] else {}
     890        except Exception:
     891            d = {}
     892        sysmon.append({
     893            "event_id": r["event_id"],
     894            "event_type": r["event_type"],
     895            "message": r["message"],
     896            "timestamp": r["timestamp"],
     897            "details": d,
     898        })
     899
     900    c.execute("""
     901            SELECT pid, process_name, local_address, remote_address, status, timestamp
     902            FROM network_connections
     903            WHERE computer_id=?
     904            ORDER BY timestamp DESC
     905            LIMIT 100
     906        """, (computer_id,))
     907    net = [dict(r) for r in c.fetchall()]
     908
     909    conn.close()
     910
     911    return jsonify({
     912        "computer": {
     913            "id": comp["id"],
     914            "name": comp["name"],
     915            "user": comp["user"],
     916            "ip": comp["ip"],
     917            "os": comp["os"],
     918            "first_seen": comp["first_seen"],
     919            "last_seen": comp["last_seen"],
     920            "env_name": comp["env_name"],
     921            "total_logs": total_logs,
     922        },
     923        "history": history,
     924        "current_metrics": current_metrics,
     925        "recent_processes": processes,
     926        "sysmon_events": sysmon,
     927        "network_connections": net,
     928    })
     929
     930
     931@app.route("/api/export/<computer_name>", methods=["GET"])
     932@require_user()
     933def api_export(computer_name):
     934    tenant_id = request.user["tenant_id"]
     935
     936    conn = db()
     937    c = conn.cursor()
     938    c.execute("""
     939            SELECT *
     940            FROM computers
     941            WHERE tenant_id=? AND name=?
     942            LIMIT 1
     943        """, (tenant_id, computer_name))
     944    comp = c.fetchone()
     945    if not comp:
     946        conn.close()
     947        return jsonify({"error": "Computer not found"}), 404
     948
     949    computer_id = comp["id"]
     950    out = {"computer": dict(comp)}
     951
     952    c.execute("SELECT * FROM computer_history WHERE computer_id=? ORDER BY timestamp", (computer_id,))
     953    out["history"] = [dict(r) for r in c.fetchall()]
     954
     955    c.execute("SELECT * FROM sysmon_events WHERE computer_id=? ORDER BY timestamp", (computer_id,))
     956    out["sysmon_events"] = [dict(r) for r in c.fetchall()]
     957
     958    c.execute("SELECT * FROM computer_processes WHERE computer_id=? ORDER BY timestamp", (computer_id,))
     959    out["processes"] = [dict(r) for r in c.fetchall()]
     960
     961    c.execute("SELECT * FROM network_connections WHERE computer_id=? ORDER BY timestamp", (computer_id,))
     962    out["network_connections"] = [dict(r) for r in c.fetchall()]
     963
     964    conn.close()
     965
     966    return Response(
     967        json.dumps(out, indent=2, default=str, ensure_ascii=False),
     968        mimetype="application/json",
     969        headers={"Content-Disposition": f"attachment; filename={computer_name}_export.json"},
     970    )
     971
     972
     973# ----------------------------
     974# Simple RAG chat
     975# ----------------------------
     976def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10):
     977    conn = db()
     978    c = conn.cursor()
     979
     980    params = [tenant_id]
     981    comp_clause = ""
     982    if computer_name:
     983        comp_clause = "AND c2.name = ?"
     984        params.append(computer_name)
     985
     986    # sysmon
     987    c.execute(f"""
     988            SELECT s.timestamp, s.event_id, s.event_type, s.message
     989            FROM sysmon_events s
     990            JOIN computers c2 ON c2.id = s.computer_id
     991            WHERE c2.tenant_id = ? {comp_clause}
     992            ORDER BY s.timestamp DESC
     993            LIMIT ?
     994        """, (*params, limit_per_table))
     995    sysmon_rows = c.fetchall()
     996
     997    # perf
     998    c.execute(f"""
     999            SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb
     1000            FROM computer_history h
     1001            JOIN computers c2 ON c2.id = h.computer_id
     1002            WHERE c2.tenant_id = ? {comp_clause}
     1003            ORDER BY h.timestamp DESC
     1004            LIMIT ?
     1005        """, (*params, limit_per_table))
     1006    perf_rows = c.fetchall()
     1007
     1008    # proc
     1009    c.execute(f"""
     1010            SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb
     1011            FROM computer_processes p
     1012            JOIN computers c2 ON c2.id = p.computer_id
     1013            WHERE c2.tenant_id = ? {comp_clause}
     1014            ORDER BY p.timestamp DESC
     1015            LIMIT ?
     1016        """, (*params, limit_per_table))
     1017    proc_rows = c.fetchall()
     1018
     1019    conn.close()
     1020
     1021    lines = []
     1022    for r in sysmon_rows:
     1023        lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}")
     1024    for r in perf_rows:
     1025        lines.append(
     1026            f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | "
     1027            f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB"
     1028        )
     1029    for r in proc_rows:
     1030        lines.append(
     1031            f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB"
     1032        )
     1033
     1034    return "\n".join(lines)
     1035
     1036
     1037def ask_llm_with_context(question, context):
     1038    if not client:
     1039        return "Немаш поставено OPENAI_API_KEY на серверот."
     1040
     1041    system_msg = (
     1042        "Ти си асистент за безбедност и систем мониторинг. "
     1043        "Одговараш базирано ИСКЛУЧИВО на дадените логови. "
     1044        "Ако нема доволно податоци, кажи дека нема доволно информации. "
     1045        "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите."
     1046    )
     1047
     1048    completion = client.chat.completions.create(
     1049        model="gpt-4.1-mini",
     1050        messages=[
     1051            {"role": "system", "content": system_msg},
     1052            {"role": "user", "content": f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"},
     1053        ],
     1054        temperature=0.2,
     1055    )
     1056    return completion.choices[0].message.content
     1057
     1058
     1059@app.route("/api/chat", methods=["POST"])
     1060@require_user()
     1061def api_chat():
     1062    try:
     1063        tenant_id = request.user["tenant_id"]
     1064        data = request.get_json(force=True, silent=True) or {}
     1065        question = (data.get("question") or "").strip()
     1066        computer_name = data.get("computer_name") or None
     1067
     1068        if not question:
     1069            return jsonify({"error": "No question provided"}), 400
     1070
     1071        ctx = build_rag_context(tenant_id, question, computer_name=computer_name, limit_per_table=10)
     1072        ans = ask_llm_with_context(question, ctx)
     1073        return jsonify({"answer": ans or ""}), 200
     1074
     1075    except Exception as e:
     1076        traceback.print_exc()
     1077        return jsonify({"error": str(e)}), 500
     1078
     1079
     1080# ----------------------------
     1081# Misc
     1082# ----------------------------
     1083@app.route("/ping")
     1084def ping():
     1085    return jsonify({
     1086        "status": "alive",
     1087        "server_time": datetime.now().isoformat(),
     1088        "database": DB_FILE,
     1089        "cors_origins": ALLOWED_ORIGINS,
     1090    })
     1091
     1092
     1093@app.route("/")
     1094def root():
     1095    return jsonify({
     1096        "ok": True,
     1097        "service": "netIntel server",
     1098        "hint": "frontend should call /api/* endpoints",
     1099    })
     1100
     1101
     1102if __name__ == "__main__":
     1103    init_db()
     1104
    20421105    cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
    20431106    cleanup_thread.start()
    20441107
    2045     local_ip = get_local_ip()
    2046 
    2047     print("\n" + "=" * 70)
    2048     print("🚀 ENHANCED LAN LOG SERVER со Sysmon")
    2049     print("=" * 70)
    2050     print(f"🌐 Server IP: {local_ip}:5555")
    2051     print(f"📊 Dashboard: http://{local_ip}:5555/")
    2052     print(f"📊 Alternative: http://localhost:5555/")
    2053     print(f"🏥 Health check: http://{local_ip}:5555/ping")
    2054     print("\n🛡️  Sysmon monitoring е активирано")
    2055     print("✅ Кликни на секој компјутер за детални информации")
    2056     print("✅ Сите Sysmon настани се прикажани во табла")
    2057     print("✅ Кликни на секој настан за детали")
    2058     print("=" * 70 + "\n")
    2059 
    2060     app.run(host='0.0.0.0', port=5555, debug=False, threaded=True)
     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 TracChangeset for help on using the changeset viewer.