docs

アーキテクチャ

3 プロセス分離と SQLite データバスによる疎結合構成

全体像

   ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
   │  ursus-sensor   │    │ ursus-detector  │    │    ursus-ui     │
   │                 │    │                 │    │                 │
   │ ┌─────────────┐ │    │ ┌─────────────┐ │    │  FastAPI        │
   │ │ process     │ │    │ │ rule loader │ │    │  Jinja2 TPL     │
   │ │ (netlink)   │ │    │ └─────────────┘ │    │  127.0.0.1:8080 │
   │ └─────────────┘ │    │ ┌─────────────┐ │    │                 │
   │ ┌─────────────┐ │    │ │ engine      │ │    │  /              │
   │ │ file        │ │    │ │ ├─ eval     │ │    │  /events        │
   │ │ (inotify)   │ │    │ │ └─ alert!   │ │    │  /alerts/<id>   │
   │ └─────────────┘ │    │ └─────────────┘ │    │  /process-tree  │
   │ ┌─────────────┐ │    │ ┌─────────────┐ │    │                 │
   │ │ network     │ │    │ │ response    │ │    │                 │
   │ │ (psutil)    │ │    │ │ handler     │ │    │                 │
   │ └─────────────┘ │    │ └─────────────┘ │    │                 │
   │ ┌─────────────┐ │    │                 │    │                 │
   │ │ auth        │ │    │                 │    │                 │
   │ │ (journalctl)│ │    │                 │    │                 │
   │ └─────────────┘ │    │                 │    │                 │
   └────────┬────────┘    └────────┬────────┘    └────────┬────────┘
            │                      │                      │
            │ INSERT events        │ SELECT events > ckpt │ SELECT
            │                      │ INSERT alerts        │ events / alerts
            │                      │ INSERT response_log  │
            └──────────────────────┴──────────────────────┘
                              ▼
                    ┌────────────────────┐
                    │  data/edr.db       │
                    │  SQLite (WAL mode) │
                    │  ─ events          │
                    │  ─ alerts          │
                    │  ─ response_log    │
                    │  ─ detector_state  │
                    └────────────────────┘

3 つのプロセスは互いに直接通信せず、SQLite ファイルを データバスとして 使います。Sensor はイベント監視と SQLite への記録のみを行い、Detector は SQLite から読み出したイベントを Koguma で評価し、アラートを SQLite に記録します。UI は記録されたイベントとアラートを SQLite より抽出し、画面へ表示します。WAL モード有効化により、複数プロセスからの 同時アクセスでもブロックしません。

データフロー

  カーネル                Sensor                    DB                   Detector
  ────────                ──────                    ──                   ────────

  exec(2) ──netlink──> process_collector ──INSERT──> events
                                                       │
                                                       │ poll loop
                                                       │ (1s)
                                                       ▼
                                              SELECT * FROM events
                                              WHERE id > checkpoint
                                                       │
                                                       ▼
                                              eval_condition(rule, event)
                                                       │
                                                       │ if match
                                                       ▼
                                              INSERT alerts ────────────> UI
                                              INSERT response_log

プロセス起動を例にしたエンドツーエンドのフロー

手順を分解

  1. カーネル → Sensor: プロセス起動なら cn_proc netlink、ファイル変更なら inotify、 認証ログなら journald が、ユーザ空間に push 通知します。
  2. Sensor → DB: 通知を受け取った Sensor は events 表に 1 行 INSERT。 raw_json には Linux API から取れた生データ全てを格納し、検知に使う重要 フィールドだけを非正規化カラム(pid, process_name, file_path 等)に 展開します。
  3. Detector → DB: Detector は 1 秒間隔のポーリングで「checkpoint より新しい events」を SELECT。 各イベントについて、event_type が一致するルール群を順に評価します。
  4. マッチ → アラート: ルールの condition が true になったら alerts 表に INSERT。 続いて ResponseHandler が response_log に対応アクションを記録 (dry_run なら実行はしない)。
  5. UI: ブラウザは FastAPI に GET し、events / alerts を SELECT して Jinja2 でレンダリング。ダッシュボード、検索、アラート詳細、プロセスツリー。

DB スキーマ

URSUS の全データモデルは SQLite の 4 テーブルに収まります。

テーブル役割書き手読み手
events Sensor が観測した生イベント。raw_json + 非正規化カラム Sensor Detector / UI
alerts Detector がルールマッチで生成した検知結果 Detector UI
response_log 各アラートに紐づくレスポンス実行ログ Detector (ResponseHandler) UI
detector_state 評価チェックポイント (last_evaluated_event_id) Detector Detector

events 表の構造

CREATE TABLE events (
  -- 共通
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  timestamp     REAL NOT NULL,
  event_type    TEXT NOT NULL CHECK(event_type IN ('process','file','network','auth')),
  hostname      TEXT NOT NULL,
  raw_json      TEXT NOT NULL,            -- センサが取れた全データ

  -- 検索・検知用の非正規化カラム
  pid                  INTEGER,
  ppid                 INTEGER,
  user                 TEXT,
  process_name         TEXT,
  parent_process_name  TEXT,
  cmdline              TEXT,
  exe_path             TEXT,
  file_path            TEXT,
  file_op              TEXT,             -- create/modify/delete/move
  remote_addr          TEXT,
  remote_port          INTEGER,
  local_port           INTEGER,
  conn_state           TEXT,             -- LISTEN / ESTABLISHED 等
  auth_user            TEXT,
  auth_result          TEXT,             -- success / failure / sudo
  source_ip            TEXT
);

非正規化カラムとは、本来 raw_json に入っているフィールドを「検索効率のため」 別カラムにコピーしたものです。これにより:

  • SQL で WHERE process_name = 'bash' のようなインデックス付き検索ができる
  • UI が raw_json をパースせずに表形式で表示できる
  • Detector のルール評価時に JSON パースを毎回しなくて済む

ルール DSL の field には、この非正規化カラム名か raw.<key> 形式 (raw_json 内の任意フィールド)のどちらでも指定できます。

プロセスとスレッド

プロセススレッド構成備考
ursus-sensor main + 4 collector スレッド + watchdog Observer の emitter スレッド群 各 collector は独自の DB 接続を保持。SIGTERM で stop_event を共有して終了。
ursus-detector main + engine スレッド (実質 1 スレッド) シンプルなポーリングループ。再入や排他は不要。
ursus-ui uvicorn + FastAPI ワーカー 各リクエストごとに DB 接続を開いて閉じる。

なぜ SQLite か

データバスとして PostgreSQL や Redis を使うこともできますが、URSUS は学習可能性を 優先して SQLite を選びました。

  • 外部依存ゼロ: Python 標準ライブラリだけで動き、別プロセスのデーモンが不要
  • 1 ファイル = 1 セッション: data/edr.db をコピー / 削除すれば状態がリセット
  • WAL モードで多プロセス読み取り可: 1 writer + N readers の構成と相性が良い
  • 外部ツールでの直接検査: sqlite3 data/edr.db から生クエリを投げて状態を直接観察できる
SQL を直接叩いて遊ぶ
$ sqlite3 data/edr.db
sqlite> .schema events
sqlite> SELECT event_type, COUNT(*) FROM events GROUP BY event_type;
sqlite> SELECT process_name, cmdline FROM events
   ...> WHERE event_type = 'process' AND timestamp > strftime('%s','now') - 60
   ...> ORDER BY timestamp DESC LIMIT 20;

関連ドキュメント