docs

Koguma ルールの定義

YAML で記述する検知条件の文法と評価セマンティクスについて解説します

概要

URSUS の検知ロジックは、rules/ ディレクトリ配下に R<NNN>_<name>.yml という形式で配置される、Koguma と呼ばれる YAML ファイルによって定義され、 1 つのファイルが 1 つのルールに対応しています。 Detector は起動時に rules/*.yml をすべて読み込み、バリデーション(必須フィールドの有無、ID 形式、未知の演算子、ツリーの深さなど)を通過した Koguma のみを評価対象としてロードします。

Koguma を追加する際の一般的なワークフローは以下の通りです:

  1. rules/ ディレクトリに YAML ファイルを配置します
  2. ursus-detector プロセスを再起動します
  3. 条件に合致するイベントが発生すると、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.pyload_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プロセス IDprocess / network / auth
ppid親プロセス IDprocess
user実行ユーザー名process
process_nameプロセス名 (comm)process / network
parent_process_name親プロセス名process
cmdlineコマンドライン (空白区切り 1 文字列)process / auth (sudo)
exe_path実行ファイルの絶対パスprocess
file_path変更対象のファイルパスfile
file_opcreate / modify / delete / movefile
local_portローカル側ポートnetwork
remote_addrリモート IPnetwork
remote_portリモートポートnetwork
conn_stateLISTEN / ESTABLISHEDnetwork
auth_user認証対象ユーザーauth
auth_resultsuccess / failure / sudoauth
source_ipSSH 等の接続元 IPauth

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

評価プロセス:

  1. event_typeprocess なので、Detector はこのイベントを process 型の Koguma 群で評価します。
  2. R001 の condition.all ノードを評価します。すべての子要素が「真」である必要があります。
  3. 子要素 1: field=process_name"bash" です。op=in[bash, sh, dash, zsh, ash] と比較し、「真」となります。
  4. 子要素 2: field=parent_process_name"nginx" です。op=in[nginx, apache2, httpd, php-fpm] と比較し、「真」となります。
  5. すべての子要素が真であるため、all の評価結果は「真」となり、R001 が発火します。
  6. 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_typeseverityMITRE
R001Webサーバー配下からのシェル起動processhighT1059.004
R002スクリプト言語によるインタラクティブシェルの起動 (pty.spawn)processhighT1059.006
R003Netcatによるリバースシェル/バインドシェルの実行processhighT1059
R004/tmp 配下からのバイナリ実行processmedium
R005機密ファイルの変更 (/etc/shadow 等)filehighT1003.008
R006SSH公開鍵の不正追加・変更filehighT1098.004
R007/var/tmp 配下への不審なバイナリ・ツールのコピーfilehighT1105
R008不審なポートへの外部通信 (C2通信の疑い)networkhighT1043
R009SSH での root ユーザログイン試行authhighT1110, T1078.003
R010sudo 経由での悪意あるコマンド実行authmediumT1548.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 の子要素はリストではありません

allany は子要素としてリストを受け取りますが、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 の仕組み — チェックポイント駆動の評価ループについて解説します
  • レスポンス動作 — マッチ時に実行されるアクションの仕様について解説します