Changes between Initial Version and Version 1 of AdvancedApplicationDevelopment


Ignore:
Timestamp:
07/07/26 18:51:44 (6 hours ago)
Author:
231118
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedApplicationDevelopment

    v1 v1  
     1= Advanced Application Development =
     2
     3== Transactions ==
     4
     5Трансакциите во системот се имплементирани на апликациско ниво преку SQLite конекции. Секоја операција која вклучува повеќе табели се извршува атомарно — доколку некоја од операциите не успее, се повикува `conn.rollback()` и ниедна промена не се зачувува.
     6
     7----
     8
     9=== Прием на податоци од агент (`/receive`) ===
     10
     11Најкритичната трансакција во системот се извршува при прием на податоци од агентите преку POST `/receive`. Во рамките на една конекција се извршуваат следните операции:
     12
     13 * Пронаоѓање или креирање на компјутер во `computers`
     14 * Ажурирање на `last_seen`, `ip`, `os`, `user`, `sysmon_available`
     15 * Внесување на системски метрики во `computer_history`
     16 * Бришење и повторно внесување на тековните процеси во `computer_processes_current`
     17 * Условно внесување на историски процеси во `computer_processes_history` (доколку е овозможено во `env_settings`)
     18 * Внесување на Sysmon настани во `sysmon_events`
     19 * Внесување на мрежни конекции во `network_connections`
     20
     21Сите операции се завршуваат со `conn.commit()`. Доколку се случи грешка во кој било чекор, се извршува `conn.rollback()` и ниедна промена не е трајно зачувана:
     22
     23{{{
     24@app.route("/receive", methods=["POST"])
     25def receive_data():
     26    conn = None
     27    try:
     28        data = request.get_json(force=True, silent=True) or {}
     29        env_name, tenant_id = get_env_from_token(request)
     30
     31        conn = db()
     32        c = conn.cursor()
     33
     34        # Чекор 1: Пронаоѓање или креирање на компјутер
     35        c.execute(
     36            "SELECT id FROM computers WHERE tenant_id=? AND name=? LIMIT 1",
     37            (tenant_id, computer_name),
     38        )
     39        row = c.fetchone()
     40
     41        if row:
     42            computer_id = row["id"]
     43            c.execute("""
     44                UPDATE computers
     45                SET user=?, ip=?, os=?, last_seen=?, sysmon_available=?, env_name=?
     46                WHERE id=? AND tenant_id=?
     47            """, (info.get("user"), info.get("ip_address"), info.get("os"),
     48                  now_iso, int(info.get("is_sysmon_available", 0) or 0),
     49                  env_name, computer_id, tenant_id))
     50        else:
     51            c.execute("""
     52                INSERT INTO computers(tenant_id, env_name, name, user, ip, os,
     53                                      first_seen, last_seen, sysmon_available)
     54                VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
     55            """, (tenant_id, env_name, computer_name, info.get("user"),
     56                  info.get("ip_address"), info.get("os"), now_iso, now_iso,
     57                  int(info.get("is_sysmon_available", 0) or 0)))
     58            computer_id = c.lastrowid
     59
     60        # Чекор 2: Системски метрики
     61        c.execute("""
     62            INSERT INTO computer_history(
     63                computer_id, cpu_usage, ram_usage, disk_usage,
     64                network_sent_mb, network_recv_mb, timestamp)
     65            VALUES(?, ?, ?, ?, ?, ?, ?)
     66        """, (computer_id, float(info.get("cpu_usage") or 0),
     67              float(info.get("ram_usage") or 0), float(info.get("disk_usage") or 0),
     68              float(info.get("network_sent_mb") or 0),
     69              float(info.get("network_recv_mb") or 0),
     70              info.get("timestamp") or now_iso))
     71
     72        # Чекор 3: Тековни процеси (бриши и внеси повторно)
     73        c.execute("DELETE FROM computer_processes_current WHERE computer_id=?", (computer_id,))
     74        for proc in (data.get("processes") or []):
     75            c.execute("""
     76                INSERT INTO computer_processes_current(
     77                    computer_id, pid, name, cpu_percent,
     78                    memory_mb, username, cmdline, timestamp)
     79                VALUES(?, ?, ?, ?, ?, ?, ?, ?)
     80            """, (computer_id, proc.get("pid"), proc.get("name"),
     81                  float(proc.get("cpu_percent") or 0),
     82                  float(proc.get("memory_mb") or 0),
     83                  proc.get("user") or proc.get("username"),
     84                  proc.get("cmdline"), info.get("timestamp") or now_iso))
     85
     86        # Чекор 4: Историски процеси (само ако е овозможено во env_settings)
     87        if is_process_history_enabled(tenant_id, env_name):
     88            for proc in (data.get("processes") or []):
     89                c.execute("""
     90                    INSERT INTO computer_processes_history(
     91                        computer_id, pid, name, cpu_percent,
     92                        memory_mb, username, cmdline, timestamp)
     93                    VALUES(?, ?, ?, ?, ?, ?, ?, ?)
     94                """, (computer_id, proc.get("pid"), proc.get("name"),
     95                      float(proc.get("cpu_percent") or 0),
     96                      float(proc.get("memory_mb") or 0),
     97                      proc.get("user") or proc.get("username"),
     98                      proc.get("cmdline"), info.get("timestamp") or now_iso))
     99
     100        # Чекор 5: Sysmon настани
     101        for ev in (security_data.get("sysmon_events") or []):
     102            c.execute("""
     103                INSERT INTO sysmon_events(
     104                    computer_id, event_id, event_type, message, timestamp, details)
     105                VALUES(?, ?, ?, ?, ?, ?)
     106            """, (computer_id, ev.get("event_id"), ev.get("event_type", "Unknown"),
     107                  ev.get("message", ""),
     108                  ev.get("timestamp") or (info.get("timestamp") or now_iso),
     109                  json.dumps(ev, ensure_ascii=False)))
     110
     111        # Чекор 6: Мрежни конекции
     112        for nc in (security_data.get("network_connections") or []):
     113            c.execute("""
     114                INSERT INTO network_connections(
     115                    computer_id, pid, local_address,
     116                    remote_address, status, process_name, timestamp)
     117                VALUES(?, ?, ?, ?, ?, ?, ?)
     118            """, (computer_id, nc.get("pid"), nc.get("local_address"),
     119                  nc.get("remote_address"), nc.get("status"),
     120                  nc.get("process_name"), info.get("timestamp") or now_iso))
     121
     122        # Сите операции успешни — зачувај
     123        conn.commit()
     124        return jsonify({"ok": True, "computer_id": computer_id, ...})
     125
     126    except Exception as e:
     127        if conn:
     128            conn.rollback()   # Врати ги сите промени
     129        return jsonify({"error": str(e)}), 500
     130
     131    finally:
     132        if conn:
     133            conn.close()
     134}}}
     135
     136Клучни карактеристики на оваа трансакција:
     137 * `conn.rollback()` се повикува во `except` блокот — ниедна табела нема делумни податоци
     138 * `conn.close()` е во `finally` блокот — конекцијата секогаш се затвора
     139 * Условното снимање на процесна историја (`is_process_history_enabled`) е дел од истата трансакција
     140
     141----
     142
     143=== Google автентикација и автоматско креирање на tenant (`/api/auth/google`) ===
     144
     145При прва најава на нов корисник, во рамките на една конекција атомарно се извршуваат:
     146
     147 * Внесување или ажурирање на корисникот во `users`
     148 * Проверка за постојно членство во `memberships`
     149 * Доколку нов корисник: креирање на `tenant`, `membership` и default `environment`
     150
     151{{{
     152@app.route("/api/auth/google", methods=["POST"])
     153def auth_google():
     154    try:
     155        idinfo = id_token.verify_oauth2_token(credential, grequests.Request(), GOOGLE_CLIENT_ID)
     156        email = idinfo.get("email")
     157
     158        conn = db()
     159        c = conn.cursor()
     160
     161        # Чекор 1: Upsert корисник
     162        c.execute("SELECT id FROM users WHERE email=?", (email,))
     163        row = c.fetchone()
     164        if row:
     165            user_id = row["id"]
     166            c.execute("UPDATE users SET name=?, picture=? WHERE id=?", (name, picture, user_id))
     167        else:
     168            c.execute(
     169                "INSERT INTO users(email, name, picture, created_at) VALUES(?,?,?,datetime('now'))",
     170                (email, name, picture))
     171            user_id = c.lastrowid
     172
     173        # Чекор 2: Проверка за membership
     174        c.execute("SELECT tenant_id, role FROM memberships WHERE user_id=? LIMIT 1", (user_id,))
     175        m = c.fetchone()
     176
     177        if not m:
     178            # Чекор 3: Нов корисник — креирај tenant, membership и default environment атомарно
     179            c.execute(
     180                "INSERT INTO tenants(name, owner_email, created_at) VALUES(?,?,datetime('now'))",
     181                (tenant_name, email))
     182            tenant_id = c.lastrowid
     183
     184            c.execute(
     185                "INSERT INTO memberships(user_id, tenant_id, role, created_at) VALUES(?,?,'admin',datetime('now'))",
     186                (user_id, tenant_id))
     187
     188            c.execute(
     189                "INSERT OR IGNORE INTO environments(tenant_id, name, created_at) VALUES(?,'default',datetime('now'))",
     190                (tenant_id,))
     191        else:
     192            tenant_id = m["tenant_id"]
     193            role = m["role"] or "admin"
     194
     195        conn.commit()
     196        conn.close()
     197
     198        session = make_jwt({"uid": user_id, "email": email, "role": role, "tenant_id": tenant_id})
     199        ...
     200
     201    except Exception as e:
     202        return jsonify({"error": "Invalid Google token", "details": str(e)}), 401
     203}}}
     204
     205Доколку некоја од INSERT операциите не успее, целата автентикација се откажува и корисникот добива грешка.
     206
     207----
     208
     209=== Генерирање на environment токен (`/api/admin/tokens`) ===
     210
     211При генерирање на нов токен за агент, системот прво верификува дека environment постои, а потоа внесува токен. Токенот се генерира со `secrets.token_urlsafe(32)` и е валиден 90 дена:
     212
     213{{{
     214@app.route("/api/admin/tokens", methods=["POST"])
     215@require_tenant_admin()
     216def admin_generate_token():
     217    tenant_id = request.user["tenant_id"]
     218    env = (data.get("env") or "").strip()
     219
     220    conn = db()
     221    c = conn.cursor()
     222
     223    # Верификација дека environment постои
     224    c.execute("SELECT 1 FROM environments WHERE tenant_id=? AND name=? LIMIT 1", (tenant_id, env))
     225    if not c.fetchone():
     226        conn.close()
     227        return jsonify({"error": "Environment not found"}), 404
     228
     229    # Генерирање и зачувување на токенот
     230    token = secrets.token_urlsafe(32)
     231    c.execute("""
     232        INSERT INTO env_tokens(tenant_id, env_name, token, created_at, expires_at)
     233        VALUES(?, ?, ?, datetime('now'), datetime('now', '+90 days'))
     234    """, (tenant_id, env, token))
     235    conn.commit()
     236    conn.close()
     237
     238    return jsonify({"ok": True, "env": env, "token": token})
     239}}}
     240
     241Токенот се проверува при секој повик на агентот преку `get_env_from_token()`:
     242
     243{{{
     244def get_env_from_token(req):
     245    tok = req.headers.get("X-Env-Token", "")
     246    if not tok:
     247        return (None, None)
     248
     249    conn = db()
     250    c = conn.cursor()
     251    c.execute("""
     252        SELECT env_name, tenant_id
     253        FROM env_tokens
     254        WHERE token = ?
     255          AND (expires_at IS NULL OR expires_at > datetime('now'))
     256        LIMIT 1
     257    """, (tok,))
     258    row = c.fetchone()
     259    conn.close()
     260
     261    if not row:
     262        return (None, None)
     263    return (row["env_name"], row["tenant_id"])
     264}}}
     265
     266----
     267
     268=== Ажурирање на поставки за околина (`/api/admin/env-settings/<env_name>`) ===
     269
     270Поставките за околина се запишуваат атомарно преку `INSERT OR ... ON CONFLICT DO UPDATE` (upsert), со цел да се избегне race condition при истовремено читање и пишување:
     271
     272{{{
     273@app.route("/api/admin/env-settings/<env_name>", methods=["POST"])
     274@require_tenant_admin()
     275def admin_set_env_settings(env_name):
     276    tenant_id = request.user["tenant_id"]
     277    enabled = 1 if bool(data.get("save_process_history")) else 0
     278
     279    conn = db()
     280    c = conn.cursor()
     281    c.execute("""
     282        INSERT INTO env_settings(tenant_id, env_name, save_process_history, created_at, updated_at)
     283        VALUES(?, ?, ?, datetime('now'), datetime('now'))
     284        ON CONFLICT(tenant_id, env_name) DO UPDATE SET
     285            save_process_history=excluded.save_process_history,
     286            updated_at=datetime('now')
     287    """, (tenant_id, env_name, enabled))
     288    conn.commit()
     289    conn.close()
     290
     291    return jsonify({"ok": True, "env": env_name, "save_process_history": bool(enabled)})
     292}}}
     293
     294----
     295
     296== Database Connection Pooling ==
     297
     298Во моменталната имплементација системот користи SQLite база на податоци преку `sqlite3` библиотеката. Конекцијата кон базата се воспоставува преку помошната функција `db()`:
     299
     300{{{
     301DB_FILE = os.environ.get("DB_FILE", "lan_logs_sysmon.db")
     302
     303def db():
     304    conn = sqlite3.connect(DB_FILE)
     305    conn.row_factory = sqlite3.Row
     306    return conn
     307}}}
     308
     309`conn.row_factory = sqlite3.Row` овозможува пристап до колоните по име (на пр. `row["email"]`) наместо по индекс. При секое барање се отвора нова конекција која се затвора по завршување на операцијата — секогаш во `finally` блокот:
     310
     311{{{
     312conn = db()
     313try:
     314    c = conn.cursor()
     315    c.execute("SELECT ...")
     316    result = c.fetchall()
     317    conn.commit()
     318finally:
     319    conn.close()
     320}}}
     321
     322Поради природата на SQLite (file-based база со writer lock), Connection Pooling не е применливо во тековната верзија.
     323
     324----
     325
     326=== Позадинска задача за автоматско чистење (`cleanup_old_data`) ===
     327
     328Системот стартува посебна daemon нишка која на секои 3600 секунди (1 час) автоматски ги брише записите постари од 30 дена. Нишката ја користи истата `db()` функција и работи со своја независна конекција:
     329
     330{{{
     331def cleanup_old_data():
     332    """Deletes old rows (30 days) every hour."""
     333    while True:
     334        time.sleep(3600)
     335        try:
     336            conn = db()
     337            c = conn.cursor()
     338            cutoff = (datetime.now() - timedelta(days=30)).isoformat()
     339
     340            c.execute("DELETE FROM computer_history WHERE timestamp < ?", (cutoff,))
     341            c.execute("DELETE FROM sysmon_events WHERE timestamp < ?", (cutoff,))
     342            c.execute("DELETE FROM network_connections WHERE timestamp < ?", (cutoff,))
     343            c.execute("DELETE FROM computer_processes_history WHERE timestamp < ?", (cutoff,))
     344
     345            conn.commit()
     346            conn.close()
     347            print(f"[Cleanup] Deleted entries older than {cutoff}")
     348        except Exception as e:
     349            print("[Cleanup] Error:", e)
     350
     351# Стартување при иницијализација на серверот
     352cleanup_thread = threading.Thread(target=cleanup_old_data, daemon=True)
     353cleanup_thread.start()
     354}}}
     355
     356Нишката е `daemon=True` — автоматски се гасне кога се гасне главниот Flask процес.
     357
     358----
     359
     360=== Планирана миграција кон PostgreSQL со Connection Pooling ===
     361
     362По миграција кон PostgreSQL, `db()` функцијата би се заменила со SQLAlchemy engine со connection pool:
     363
     364{{{
     365from sqlalchemy import create_engine
     366from sqlalchemy.orm import sessionmaker
     367from contextlib import contextmanager
     368
     369DATABASE_URL = "postgresql://app_user:password@localhost:5432/netintel_db"
     370
     371engine = create_engine(
     372    DATABASE_URL,
     373    pool_size=10,        # Број на постојни конекции во pool-от
     374    max_overflow=20,     # Дополнителни конекции при врв на оптоварување
     375    pool_timeout=30,     # Максимално чекање за слободна конекција (секунди)
     376    pool_recycle=1800,   # Освежување на конекции на секои 30 минути
     377    pool_pre_ping=True   # Проверка дали конекцијата е активна пред употреба
     378)
     379
     380SessionLocal = sessionmaker(bind=engine)
     381
     382@contextmanager
     383def db():
     384    session = SessionLocal()
     385    try:
     386        yield session
     387        session.commit()
     388    except Exception:
     389        session.rollback()
     390        raise
     391    finally:
     392        session.close()  # Конекцијата се враќа во pool-от, не се затвора физички
     393}}}
     394
     395Пример на употреба во `/receive` endpoint со новиот pool:
     396
     397{{{
     398@app.route("/receive", methods=["POST"])
     399def receive_data():
     400    with db() as session:
     401        result = session.execute(
     402            text("SELECT id FROM computers WHERE tenant_id=:tid AND name=:name LIMIT 1"),
     403            {"tid": tenant_id, "name": computer_name}
     404        ).fetchone()
     405        # Конекцијата автоматски се враќа во pool-от по излегување од with блокот
     406}}}
     407
     408Предности на оваа миграција:
     409 * Повторно користење на активни конекции наместо отворање нова при секое барање
     410 * Намалување на латенцијата — воспоставувањето на нова PostgreSQL конекција трае ~50-100ms
     411 * `pool_pre_ping=True` автоматски ги отстранува прекинатите конекции од pool-от
     412 * Подобри перформанси при голем број истовремени агенти кои праќаат податоци паралелно
     413 * `threaded=True` е веќе поставено на Flask серверот (`app.run(..., threaded=True)`), па pooling директно го подобрува паралелизмот