アーキテクチャ
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
プロセス起動を例にしたエンドツーエンドのフロー
手順を分解
- カーネル → Sensor:
プロセス起動なら
cn_procnetlink、ファイル変更なら inotify、 認証ログなら journald が、ユーザ空間に push 通知します。 - Sensor → DB:
通知を受け取った Sensor は
events表に 1 行 INSERT。 raw_json には Linux API から取れた生データ全てを格納し、検知に使う重要 フィールドだけを非正規化カラム(pid, process_name, file_path 等)に 展開します。 - Detector → DB: Detector は 1 秒間隔のポーリングで「checkpoint より新しい events」を SELECT。 各イベントについて、event_type が一致するルール群を順に評価します。
- マッチ → アラート:
ルールの condition が true になったら
alerts表に INSERT。 続いて ResponseHandler がresponse_logに対応アクションを記録 (dry_run なら実行はしない)。 - 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;
関連ドキュメント
- Sensor の仕組み — 各コレクタの技術詳細と実装方式
- Detector の仕組み — checkpoint 駆動の評価ループ
- 検知ルールの定義 — YAML で書ける検知条件の文法