Koguma ルールの定義
YAML で記述する検知条件の文法と評価セマンティクスについて解説します
概要
URSUS の検知ロジックは、rules/ ディレクトリ配下に R<NNN>_<name>.yml
という形式で配置される、Koguma と呼ばれる YAML ファイルによって定義され、
1 つのファイルが 1 つのルールに対応しています。
Detector は起動時に rules/*.yml をすべて読み込み、バリデーション(必須フィールドの有無、ID 形式、未知の演算子、ツリーの深さなど)を通過した Koguma のみを評価対象としてロードします。
Koguma を追加する際の一般的なワークフローは以下の通りです:
rules/ディレクトリに YAML ファイルを配置しますursus-detectorプロセスを再起動します- 条件に合致するイベントが発生すると、
alertsテーブルにレコードが追加されます
Koguma のスキーマ
id: R001 # 必須。一意である必要があります(形式: ^R\d{3}$)
title: "短い人間可読タイトル" # 必須。ルールの内容を簡潔に示します
description: | # 任意。複数行での記述が可能です
ルールの意図、参考リンク、誤検知の傾向などを記載します
severity: high # 必須。low / medium / high / critical から選択します
event_type: process # 必須。評価対象のイベント種別を指定します
condition: # 必須。検知条件(後述)
all:
- field: process_name
op: in
value: [bash, sh]
- field: parent_process_name
op: in
value: [nginx, apache2]
response: # 必須。マッチ時に実行するアクションを指定します
- alert
mitre: # 任意。MITRE ATT&CK のテクニック ID です
- T1059.004
enabled: true # 任意。既定値は true です
フィールド詳細
| フィールド | 型 | 必須 | 備考 |
|---|---|---|---|
id |
string | ◯ | 一意な ID です。^R\d{3}$ 形式でない場合はロード時に拒否されます |
title |
string | ◯ | UI のアラート一覧などで表示されるサマリです |
description |
string | — | アラートの詳細画面で表示されます。複数行は | を用いて記述できます |
severity |
enum | ◯ | 重要度です。low / medium / high / critical から選びます |
event_type |
enum | ◯ | process / file / network / auth。Detector はこの種別に沿って評価を行います |
condition |
tree | ◯ | 検知条件の評価木です。最大 10 段までの深度をサポートしています |
response |
list<string> | ◯ | マッチ時に実行されるアクションの名前です。Responses を参照してください |
mitre |
list<string> | — | MITRE ATT&CK のテクニック ID です。UI で関連ページへのリンクが表示されます |
enabled |
bool | — | false を設定すると、この Koguma は評価対象から除外されます |
ロード時のバリデーション
Koguma の読み込み処理は、src/ursus/detector/rule_loader.py の load_rules() が担当しています。
読み込み中にエラーが検出された場合は、該当する 1 件のみをスキップしてログに記録し、他のルールのロードは継続して行われます。
以下のケースでは Koguma が拒否されます:
- YAML の構文エラーがある場合
- 必須フィールド(
id,title,severity,event_type,condition,response)が欠けている場合 idが^R\d{3}$の形式に一致しない場合idが他のルールと重複している場合severityまたはevent_typeが定義外の値である場合responseが空リストである場合conditionツリーの深さが 10 段を超えている場合- リーフノードに
fieldまたはopが存在しない場合 opに未知の演算子が指定されている場合
condition の記述方法
condition は再帰的な評価構造(ツリー)となっており、各ノードは「枝ノード」または「リーフノード」のいずれかとして定義されます。
枝ノード
論理演算(AND, OR, NOT)を表現するノードです。
| キー | 意味 | 子の形式 | 評価ロジック |
|---|---|---|---|
all |
AND | 1つ以上のリスト | すべての子要素が「真」であれば「真」となります(短絡評価)。 |
any |
OR | 1つ以上のリスト | いずれかの子要素が「真」であれば「真」となります(短絡評価)。 |
not |
NOT | 単一ノード | 子要素の評価結果を反転させます。 |
# AND の例
condition:
all:
- field: process_name
op: eq
value: bash
- field: parent_process_name
op: eq
value: nginx
# OR + NOT のネスト
condition:
all:
- field: file_op
op: eq
value: modify
- any:
- field: file_path
op: startswith
value: /etc/cron.d/
- field: file_path
op: startswith
value: /etc/systemd/system/
- not:
field: process_name
op: eq
value: dpkg
リーフノード
実際に値を比較する末端のノードです。
field: process_name # 必須。比較対象のフィールドを指定します
op: regex # 必須。演算子を指定します
value: "^/tmp/" # 指定した op に応じた値を記述します
field の解決方法
field には以下の 2 種類の参照方法があります。
非正規化カラム名による参照
events テーブルの非正規化カラムを直接参照します。データベースのインデックスが利用されるため高速に動作し、ほとんどの Koguma はこの方法で記述できます。
| field | 由来 | 主に使う event_type |
|---|---|---|
pid | プロセス ID | process / network / auth |
ppid | 親プロセス ID | process |
user | 実行ユーザー名 | process |
process_name | プロセス名 (comm) | process / network |
parent_process_name | 親プロセス名 | process |
cmdline | コマンドライン (空白区切り 1 文字列) | process / auth (sudo) |
exe_path | 実行ファイルの絶対パス | process |
file_path | 変更対象のファイルパス | file |
file_op | create / modify / delete / move | file |
local_port | ローカル側ポート | network |
remote_addr | リモート IP | network |
remote_port | リモートポート | network |
conn_state | LISTEN / ESTABLISHED | network |
auth_user | 認証対象ユーザー | auth |
auth_result | success / failure / sudo | auth |
source_ip | SSH 等の接続元 IP | auth |
raw_json へのドット記法アクセス
非正規化カラムに含まれていない情報は、raw.<key>[.<key>...] という形式で参照できます。
ただし、評価のたびに raw_json をデコードするため、非正規化カラムを参照する場合に比べて処理速度は低下します。
# 例: process イベントの raw_json 内の cmdline 配列の最初の要素
condition:
- field: raw.cmdline.0
op: eq
value: bash
# 例: network イベントの raw.family
condition:
all:
- field: raw.family
op: eq
value: AF_INET6
指定したキーが存在しない場合は None として扱われ、ほとんどの演算子において「偽」と判定されます。
演算子
すべての演算子は (event_value, rule_value) -> bool という形式の純粋関数として、src/ursus/detector/operators.py に実装されています。
| op | 真になる条件 | value の型 | None 時の挙動 |
|---|---|---|---|
eq |
event == value |
scalar | value が None の場合のみ真 |
neq |
event != value |
scalar | value が None でなければ真 |
in |
event in value |
list | list に None を含まなければ偽 |
not_in |
event not in value |
list | list に None を含まなければ真 |
regex |
re.search(value, str(event)) がマッチ |
string (正規表現) | 常に偽 |
contains |
value in str(event) |
string | 常に偽 |
startswith |
str(event).startswith(value) |
string | 常に偽 |
endswith |
str(event).endswith(value) |
string | 常に偽 |
gt |
float(event) > float(value) |
number | 変換失敗で偽 |
lt |
float(event) < float(value) |
number | 変換失敗で偽 |
exists |
event is not None |
(無視) | 偽 |
regex のコンパイルキャッシュ
regex 演算子で使用されるパターンは、パフォーマンス向上のため functools.lru_cache(maxsize=256) を用いてキャッシュされます。
これは、同じパターンが大量のイベントに対して繰り返し評価される運用を想定した最適化です。
@lru_cache(maxsize=256)
def _compile(pattern):
return re.compile(pattern)
YAML 内の正規表現とエスケープ
YAML のダブルクォート(")内では、バックスラッシュ(\)を 2 重(\\)に書く必要があります。
シングルクォート(')を使用すると、エスケープなしでそのまま記述できます。
# どちらも同じ正規表現 (curl|wget).+\|\s*(sh|bash) を表現
value: "(curl|wget).+\\|\\s*(sh|bash)" # ダブルクォート
value: '(curl|wget).+\|\s*(sh|bash)' # シングルクォート
具体例:Koguma の評価フロー
R001(Web サーバー配下からのシェル起動)を例に、実際の評価プロセスを追ってみましょう。
id: R001
event_type: process
condition:
all:
- field: process_name
op: in
value: [bash, sh, dash, zsh, ash]
- field: parent_process_name
op: in
value: [nginx, apache2, httpd, php-fpm]
入力となるイベント(events テーブルの 1 レコード)の例:
event_type: process
process_name: bash
parent_process_name: nginx
cmdline: "bash -c 'id'"
pid: 4567
ppid: 1000
user: www-data
評価プロセス:
event_typeがprocessなので、Detector はこのイベントを process 型の Koguma 群で評価します。- R001 の
condition.allノードを評価します。すべての子要素が「真」である必要があります。 - 子要素 1:
field=process_nameは"bash"です。op=inで[bash, sh, dash, zsh, ash]と比較し、「真」となります。 - 子要素 2:
field=parent_process_nameは"nginx"です。op=inで[nginx, apache2, httpd, php-fpm]と比較し、「真」となります。 - すべての子要素が真であるため、
allの評価結果は「真」となり、R001 が発火します。 responseに指定されたalertアクションが実行され、alertsテーブルにレコードが追加されます。
イベント種別ごとの Koguma の例
process
# R009: SUID ビット付与
id: R009
event_type: process
condition:
all:
- field: process_name
op: eq
value: chmod
- field: cmdline
op: regex
value: "(u\\+s|\\b4[0-7]{3}\\b)"
response:
- alert
mitre:
- T1548.001
file
# /etc/passwd / /etc/shadow への変更を検知
id: R002
event_type: file
condition:
all:
- field: file_op
op: in
value: [create, modify]
- field: file_path
op: in
value: ["/etc/passwd", "/etc/shadow"]
response:
- alert
network
# 高ポートでの新規 LISTEN を検知
id: Rxxx
event_type: network
condition:
all:
- field: conn_state
op: eq
value: LISTEN
- field: local_port
op: gt
value: 10000
response:
- alert
auth
# R011: SSH での root ユーザログイン試行
id: R011
event_type: auth
condition:
all:
- field: auth_user
op: eq
value: root
- field: source_ip
op: exists
response:
- alert
mitre:
- T1110
同梱されているデフォルトルールの一覧
| ID | タイトル | event_type | severity | MITRE |
|---|---|---|---|---|
R001 | Webサーバー配下からのシェル起動 | process | high | T1059.004 |
R002 | スクリプト言語によるインタラクティブシェルの起動 (pty.spawn) | process | high | T1059.006 |
R003 | Netcatによるリバースシェル/バインドシェルの実行 | process | high | T1059 |
R004 | /tmp 配下からのバイナリ実行 | process | medium | — |
R005 | 機密ファイルの変更 (/etc/shadow 等) | file | high | T1003.008 |
R006 | SSH公開鍵の不正追加・変更 | file | high | T1098.004 |
R007 | /var/tmp 配下への不審なバイナリ・ツールのコピー | file | high | T1105 |
R008 | 不審なポートへの外部通信 (C2通信の疑い) | network | high | T1043 |
R009 | SSH での root ユーザログイン試行 | auth | high | T1110, T1078.003 |
R010 | sudo 経由での悪意あるコマンド実行 | auth | medium | T1548.003, T1136.001 |
重複発火の抑止について
Detector は、(rule_id, triggered_event_id) の組み合わせに基づいて、すでに alerts テーブルに該当するレコードが存在するかを _fire_alert 処理の中で事前にチェックします。これにより、バックフィル処理や再起動後のチェックポイントからの再評価などで同一イベントを再度処理した場合でも、重複してアラートが生成されることはありません。
existing = conn.execute(
"SELECT id FROM alerts WHERE rule_id = ? AND triggered_event_id = ?",
(rule.id, ev["id"]),
).fetchone()
if existing:
return
よくある間違いと注意点
not の子要素はリストではありません
all や any は子要素としてリストを受け取りますが、not は単一のノードのみを受け取ります。
# 誤り: not の値がリスト
condition:
not:
- field: process_name
op: eq
value: bash
# 正しい: not の値は単一ノード
condition:
not:
field: process_name
op: eq
value: bash
event_type の指定ミス
Detector は event_type が一致する Koguma のみを評価対象とします。
例えば、認証失敗に関する検知条件を event_type: process として記述しても、評価対象から外れるため検知は行われません。
cmdline は空白区切りの単一文字列です
process イベントにおける非正規化カラムの cmdline は、コマンドライン引数を空白で連結した 1 つの文字列です。
引数を個別の要素として扱いたい場合は、ドット記法を用いて raw.cmdline を参照してください。
network イベントには cmdline は含まれません
network イベントの非正規化カラムには process_name は存在しますが、cmdline は含まれていません。
プロセス名に基づいてフィルタリングを行う場合は、process_name フィールドを使用してください。
関連ドキュメント
- Detector の仕組み — チェックポイント駆動の評価ループについて解説します
- レスポンス動作 — マッチ時に実行されるアクションの仕様について解説します