Hello World|Labs
Published on

Wazuhを使ったアプリケーションの脅威検知とインシデントの管理

本記事ではファイル改竄やマルウェア感染、不正アクセスなどWebアプリケーションに対する様々な脅威を検知し管理する仕組みをWazuhで構築します。

システム概要

今回構築するシステムで検知する脅威は以下の通りです。

  • サーバーおよびアプリケーション内におけるファイルの追加・変更・削除
  • サーバーのマルウェア感染
  • 不審なネットワークトラフィック
  • アプリケーションに対する攻撃

上記で検知した脅威はレベルに応じて管理ツールに自動登録します。

システム構成

今回のシステム構成は下図のようになります。 WebサーバーおよびアプリケーションサーバーにWazuhエージェントをインストールし脅威を検知します。 エージェントは検知した脅威情報を管理サーバーに送信し、管理サーバーはワークフローエンジンを介してインシデント管理ツールおよびチャットツールに脅威情報を送信します。

ResourceUsageHosting TypeLicensing Model
Wazuhサーバーに対する様々な脅威を検知・管理するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Catalystセキュリティに関するアラートやインシデントなどをチケット管理するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Caddy + CorazaWebサーバーおよびWAFSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
n8nワークフローを実行するSelf Hosting(Docker container on Hetzner Cloud)Freemium
Open Source
Mattermost脅威情報の通知先Self Hosting(Docker container on Hetzner Cloud)Freemium
Open Source

Wazuhの設定

Wazuhの設定はossec.confで行います。 設定ファイルはWazuhマネージャーがインストールされたWazuhサーバー、またはWazuhエージェントがインストールされたエンドポイントの/var/ossec/etc/ossec.confに配置されます。 設定可能な項目はマネージャー・エージェント両方で設定可、マネージャーのみで設定可、エージェントのみで設定可の3種類に分類されます。

設定項目一覧:https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/index.html

ファイル整合性監視

エンドポイントのファイルの作成、変更や削除を監視するための設定を行います。 エンドポイント側ossec.confsyscheckセクションに監視したいディレクトリや監視から除外したいディレクトリを追加します。

<!-- File integrity monitoring -->
<syscheck>
 
  [...]
 
  <!-- Frequency that syscheck is executed default every 12 hours -->
  <frequency>43200</frequency>
 
  [...]
 
  <!-- Directories to check  (perform all possible verifications) -->
  <directories>/etc,/usr/bin,/usr/sbin</directories>
  <directories>/bin,/sbin,/boot</directories>
 
  <!-- 監視したいアプリケーションのディレクトリ -->
  <directories realtime="yes">/var/www/html</directories>
 
  <!-- Files/directories to ignore -->
  <ignore>/etc/mtab</ignore>
  <ignore>/etc/hosts.deny</ignore>
  <ignore>/etc/mail/statistics</ignore>
  <ignore>/etc/random-seed</ignore>
  <ignore>/etc/random.seed</ignore>
  <ignore>/etc/adjtime</ignore>
  <ignore>/etc/httpd/logs</ignore>
  <ignore>/etc/utmpx</ignore>
  <ignore>/etc/wtmpx</ignore>
  <ignore>/etc/cups/certs</ignore>
  <ignore>/etc/dumpdates</ignore>
  <ignore>/etc/svc/volatile</ignore>
 
  <!-- 監視から除外したいアプリケーションのディレクトリ -->
  <ignore>/var/www/html/var/cache</ignore>
  <ignore>/var/www/html/var/sessions</ignore>
 
  <!-- File types to ignore -->
  <ignore type="sregex">.log$|.swp$</ignore>
 
  [...]
 
</syscheck>

Note

ファイルの整合性チェックはfrequencyに設定した秒数のサイクルで実行されますが、監視したいディレクトリにrealtimeオプションを付けることでリアルタイムの監視を行うことができます。

試しにアプリケーションのソースコードを変更してみると下図のように変更したファイルがダッシュボードのセキュリティイベントに表示されます。

マルウェアの検知

エンドポイント(アプリケーションサーバー)の設定

まずはエンドポイント側の設定を行います。 マルウェアの検知にはYARAを使用します。 YARAはテキストやバイナリデータのパターンなどを定義したルールをもとにマルウェアや潜在的な危険性のあるファイルの識別と分類を行うツールです。

公式ドキュメントのインストール手順を参考にしてエンドポイントにYARAをインストールします。

インストールが完了したらエンドポイントにYARAのルールをダウンロードします。 ルールは様々なコミュニティ・企業によって提供されています。 今回はNextron Systems社が提供しているYARAルールセット「VALHALLA」を使用します。 ルールセットはAPI経由でダウンロードします(APIキーはデモ用のキーを使用)。

sudo mkdir -p /tmp/yara/rules
sudo curl 'https://valhalla.nextron-systems.com/api/v1/get' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' \
--compressed \
-H 'Referer: https://valhalla.nextron-systems.com/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'DNT: 1' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' \
--data 'demo=demo&apikey=1111111111111111111111111111111111111111111111111111111111111111&format=text' \
-o /tmp/yara/rules/yara_rules.yar

ダウンロードが完了したらエンドポイントにアクティブ・レスポンス用のシェルスクリプトを/var/ossec/active-response/bin/yara.shに作成します。 アクティブ・レスポンスとはセキュリティイベントをトリガーにしてスクリプトを実行する機能です。 今回はファイルの作成や更新をトリガーにしてYARAでファイルのスキャンを行い、その結果をログファイルに出力します。 スクリプトは下記のようになります。

#!/bin/bash
 
read INPUT_JSON
 
# YARA実行ファイルのパス
YARA_PATH=$(echo $INPUT_JSON | jq -r .parameters.extra_args[1])
# YARAルールファイルのパス
YARA_RULES=$(echo $INPUT_JSON | jq -r .parameters.extra_args[3])
# セキュリティイベントのトリガーとなったファイル
FILENAME=$(echo $INPUT_JSON | jq -r .parameters.alert.syscheck.path)
# ファイルのmtime
MTIME=$(echo $INPUT_JSON | jq -r .parameters.alert.syscheck.mtime_after)
MTIME_EPOCH=$(date --date=${MTIME} +"%s")
 
LOG_FILE="logs/active-responses.log"
 
# ファイル書き込み中の場合は完了するまで待機
size=0
actual_size=$(stat -c %s ${FILENAME})
while [ ${size} -ne ${actual_size} ]; do
    sleep 1
    size=${actual_size}
    actual_size=$(stat -c %s ${FILENAME})
done
 
mkdir -p /tmp/lock
LOCK_FILE="/tmp/lock/$(sha256sum ${FILENAME} | awk '{ print $1 }').lock"
 
# ロックファイルを使用して重複起動を回避
if [[ -f $LOCK_FILE ]]; then
    OLD_MTIME_EPOCH=$(cat $LOCK_FILE)
    if [ $((MTIME_EPOCH - OLD_MTIME_EPOCH)) -gt 30 ]; then
        echo "$MTIME_EPOCH" > $LOCK_FILE
    else
        exit 1
    fi
else
    touch $LOCK_FILE
    echo "$MTIME_EPOCH" > $LOCK_FILE
fi
 
if [[ ! $YARA_PATH ]] || [[ ! $YARA_RULES ]]; then
    echo "wazuh-yara: ERROR - Yara active response error. Yara path and rules parameters are mandatory." >> ${LOG_FILE}
    exit 1
fi
 
# YARAスキャンを実行
yara_output="$("${YARA_PATH}"/yara -w -r "$YARA_RULES" "$FILENAME")"
 
if [[ $yara_output != "" ]]; then
    # マッチしたルールを一つずつログファイルに出力
    while read -r line; do
        echo "wazuh-yara: INFO - Scan result: $line" >> ${LOG_FILE}
    done <<< "$yara_output"
fi
 
exit 0

Note

ファイルのダウンロードなどファイルの書き込みが完了するまでに時間がかかるケースでは、リアルタイムのファイル整合性監視によって同じファイルが何度も検知され、セキュリティイベント(ファイル更新イベント)が複数回発生することがあります。 そのため、上記スクリプトではロックファイルを作成してファイルのタイムスタンプ情報を比較して、同じファイルに対してYARAスキャンが1回しか実行されないようにしています。

Wazuhサーバーの設定

まず、Wazuhサーバーにアプリケーションディレクトリのファイル作成・更新を検知するカスタムルールを作成します。 /var/ossec/etc/rules/local_rules.xmlに以下のルールを追加します。 if_sidに指定している550はファイルのチェックサムの変更を検知するルールで、554はファイルが作成されたことを検知するルールです。 ここではファイル作成・更新を検知する550と554の関連ルールとして、アプリケーションディレクトリ配下のファイル作成・変更を検知するルール100200と100201を新たに定義しています。

<group name="syscheck,">
  <rule id="100200" level="7">
    <if_sid>550</if_sid>
    <field name="file">/var/www/html/</field>
    <description>File modified in the app directory.</description>
  </rule>
  <rule id="100201" level="7">
    <if_sid>554</if_sid>
    <field name="file">/var/www/html/</field>
    <description>File added to the app directory.</description>
  </rule>
</group>

次にアクティブ・レスポンスの設定を行います。 Wazuhサーバーの/var/ossec/etc/ossec.confに以下の内容を追加します。 下記設定によりアプリケーションディレクトリ配下のファイル作成・変更をトリガーとしてYARAスキャンを実行するスクリプトを起動することができます。

<ossec_config>
 
  [...]
 
  <command>
    <name>yara_linux</name>
    <executable>yara.sh</executable>
    <extra_args>-yara_path /usr/local/bin -yara_rules /tmp/yara/rules/yara_rules.yar</extra_args>
    <timeout_allowed>no</timeout_allowed>
  </command>
 
  <active-response>
    <disabled>no</disabled>
    <!-- 実行するコマンド名 -->
    <command>yara_linux</command>
    <!-- コマンドを実行する場所(localはセキュリティイベントが発生したサーバーを表す) -->
    <location>local</location>
    <!-- 指定したルールIDに関わるセキュリティイベントの場合のみコマンドを実行する -->
    <rules_id>100200,100201</rules_id>
  </active-response>
 
  [...]
 
</ossec_config>

次にYARAスキャンの検知結果から情報を抽出するデコーダーとYARAスキャンの検知に対応するルールを作成します。 YARAスキャンの結果は/var/ossec/logs/active-responses.logwazuh-yara: INFO - Scan result: Blah Blah Blahという書式で出力しています。 出力したログは設定ファイルの以下の設定によってログコレクターに収集されます。 収集したログはデコーダーによって情報を抽出され、該当するルールによってセキュリティイベントが作成されます。

<ossec_config>
 
  [...]
 
  <!-- 下記ファイルをログ収集対象に追加 -->
  <localfile>
    <log_format>syslog</log_format>
    <location>/var/ossec/logs/active-responses.log</location>
  </localfile>
 
  [...]
 
</ossec_config>

/var/ossec/etc/decoders/local_decoder.xmlに以下のデコーダーを追加します。

<decoder name="yara_decoder">
  <prematch>wazuh-yara:</prematch>
</decoder>
 
<decoder name="yara_decoder1">
  <parent>yara_decoder</parent>
  <regex>wazuh-yara: (\S+) - Scan result: (\S+) (\S+)</regex>
  <!-- 上の正規表現のキャプチャグループと対になるラベル -->
  <order>log_type, yara_rule, yara_scanned_file</order>
</decoder>

そして/var/ossec/etc/rules/local_rules.xmlに以下のルールを追加します。

<group name="yara,">
  <rule id="108000" level="0">
    <decoded_as>yara_decoder</decoded_as>
    <description>Yara grouping rule</description>
  </rule>
  <rule id="108001" level="12">
    <if_sid>108000</if_sid>
    <match>wazuh-yara: INFO - Scan result: </match>
    <description>File "$(yara_scanned_file)" is a positive match. Yara rule: $(yara_rule)</description>
    <group>malware,incident,</group>
  </rule>
</group>

以上の設定は完了です。 YARスキャンで脅威を検知した場合に独自のセキュリティイベントを発生させることができるようになりました。 アプリケーションの任意のディレクトリにマルウェアのサンプルをダウンロードしてイベントが発生するかどうか試してみます。

curl https://wazuh-demo.s3-us-west-1.amazonaws.com/mirai --output /var/www/html/html/upload/mirai

上記コマンドでダウンロードすると下図のようにセキュリティイベントが発生していることが確認できます。

ソースコードにWebShellを埋め込んでみます。 下図のようにセキュリティイベントが発生していることが確認できます。

不審なネットワークトラフィックの検知

エンドポイント(アプリケーションサーバー)の設定

不審なネットワークトラフィックの検知にはSuricataを使用します。 SuricataはOISF(Open Information Security Foundation)によって開発されているオープンソースのネットワークIDS/IPSエンジンです。

エンドポイントにSuricataをインストールしたら新しい検知ルール群をダウンロードします。

cd /tmp/ && curl -LO https://rules.emergingthreats.net/open/suricata-6.0.8/emerging.rules.tar.gz
sudo tar -xvzf emerging.rules.tar.gz && sudo mkdir /etc/suricata/rules && sudo mv rules/*.rules /etc/suricata/rules/
sudo chmod 640 /etc/suricata/rules/*.rules

次に/etc/suricata/suricata.yamlを修正します。

vars:
  address-groups:
    HOME_NET: "<エンドポイントのIPアドレス>"
 
af-packet:
  - interface: "<監視したいネットワークインターフェース(例:eth0)>"
 
rule-files:
  - "*.rules"

/var/ossec/etc/ossec.confに以下の内容を追加して、イベントログをログ収集の対象に加えます。

<ossec_config>
 
  [...]
 
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/suricata/eve.json</location>
  </localfile>
 
  [...]
 
</ossec_config>

次に/etc/suricata/rules/ディレクトリ直下にルールファイルを追加して以下のオリジナルルールを追加し、エンドポイントから外部のネットワークに対して60秒間に30回を超えるアウトバウンドトラフィックを検知した場合にアラートを出すようにします。

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"Possible Malicious Outbound Activity"; flow:to_server; flags: S,12; threshold: type both, track by_src, count 30, seconds 60; classtype:misc-activity; sid:1000001; rev:1; metadata: severity high;)

Wazuhサーバーの設定

/var/ossec/etc/rules/local_rules.xmlに以下のルールを追加し、metadataのseverityにhighを指定しているアラートの場合にルールレベルが9のセキュリティイベントが発生するようにします。

<group name="ids,suricata,">
  <rule id="86611" level="9">
    <if_sid>86601</if_sid>
    <field name="alert.metadata.severity" type="pcre2">\["high"\]</field>
    <description>Suricata: Alert - $(alert.signature)</description>
    <group>alert,</group>
  </rule>
</group>

以上で設定は完了です。 試しにエンドポイントにnmapでポートスキャンを行ってみます。 結果は下図のようになりセキュリティイベントが発生していることが確認できます。

エンドポイントから外部のサーバーに対してリクエストを多数送信してみます。 結果は下図のようになりセキュリティイベントが発生していることが確認できます。

アプリケーションに対する攻撃の検知

エンドポイント(アプリケーションサーバー)の設定

今回はアプリケーション(EC-CUBE)のログイン認証に対する外部からのブルートフォース攻撃を検知する設定を行います。 まず、アプリケーションログのフォーマットをsyslog形式のヘッダーとボディで構成されるフォーマットにします。

eccube.log.formatter.line:
  class: Monolog\Formatter\LineFormatter
  arguments:
      - "%%datetime%% ECCUBE: %%channel%%.%%level_name%% [%%extra.ip%%] [%%extra.session_id%%] [%%extra.uid%%] [%%extra.user_id%%] [%%extra.class%%:%%extra.function%%:%%extra.line%%] - %%message%% [%%extra.http_method%%, %%extra.url%%, %%extra.referrer%%, %%extra.user_agent%%]\n"

以下はログイン認証に失敗した時のログのサンプルです。

2024-09-01T00:00:00.000000+09:00 ECCUBE: security.INFO [192.168.1.1] [d0254ab9] [5345b59] [N/A] [Symfony\Component\Security\Http\Authentication\AuthenticatorManager:handleAuthenticationFailure:251] - Authenticator failed.  [POST, /mypage/login, http://example.com/mypage/login, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36]

/var/ossec/etc/ossec.confに以下の内容を追加して、上記アプリケーションログをログ収集の対象に加えます。

<ossec_config>
 
  [...]
 
  <localfile>
    <log_format>syslog</log_format>
    <location>/var/www/html/var/log/prod/*.log</location>
  </localfile>
 
  [...]
 
</ossec_config>

Wazuhサーバーの設定

収集したアプリケーションログのデコーダーを/var/ossec/etc/decoders/local_decoder.xmlに追加します。

<decoder name="eccube-security-info">
  <type>syslog</type>
  <program_name>ECCUBE</program_name>
  <prematch>security.INFO </prematch>
  <regex offset="after_prematch" type="pcre2">\[(\S*)\] \[(\S*)\] \[(\S*)\] \[(\S*)\] (.*)</regex>
  <order>srcip,session_id,uid,user_id,message</order>
</decoder>

デコーダーで抽出したメッセージを条件にログイン認証成功と失敗のルールを/var/ossec/etc/rules/local_rules.xmlに追加します。

<group name="syslog,eccube,">
  <rule id="100400" level="3">
    <decoded_as>eccube-security-info</decoded_as>
    <description>EC-CUBE security information.</description>
  </rule>
 
  <rule id="100401" level="5">
    <if_sid>100400</if_sid>
    <field name="message">Authenticator failed</field>
    <description>eccube: Authentication failed.</description>
    <group>authentication_failed,</group>
  </rule>
 
  <rule id="100402" level="3">
    <if_sid>100400</if_sid>
    <field name="message">Authenticator successful</field>
    <description>eccube: Authentication succeeded.</description>
    <group>authentication_success,</group>
  </rule>
</group>

実際にログイン画面でログイン認証に失敗してみると該当のセキュリティイベントが発生していることが確認できます。

このままだとログイン認証失敗のアラートが大量にログ表示されてしまいますので同一IPアドレスからのログイン認証失敗の回数が閾値を超えた場合のみ表示するようにしたいと思います。以下のように既存のルールにno_logオプションを追加し、新しいルールを追加します。

<group name="syslog,eccube,">
 
  [...]
 
  <rule id="100401" level="5">
    [...]
    <options>no_log</options>
  </rule>
 
  <rule id="100402" level="3">
    [...]
    <options>no_log</options>
  </rule>
 
  <rule id="100403" level="7" frequency="10" timeframe="120">
    <if_matched_group>authentication_failed</if_matched_group>
    <same_srcip />
    <description>eccube: Too many login attempts from the same IP address</description>
    <group>authentication_failures,attack,alert,</group>
  </rule>
</group>

上記の修正により個々のログイン認証失敗はログ表示されなくなり、同一IPアドレスからのログイン認証失敗が120秒間に10回に達した場合にセキュリティイベントが発生するようになりました。 実際に試した結果は下図のようになります。

WAFによる攻撃の遮断

ログイン画面に対するブルートフォース攻撃を検知できるようになりましたので、検知したIPアドレスをWAFで自動的にブロックする簡易的な仕組みを追加します。

今回WebサーバーとしてCaddyを使用しており、CaddyにCorazaモジュールを使用してOWASP Coraza WAFの機能を組み込んでいます。以下はCaddyファイルのWAFの設定内容です。

coraza_waf {
  load_owasp_crs
  directives `
  Include @coraza.conf-recommended
  Include @crs-setup.conf.example
  Include @owasp_crs/*.conf
  SecDefaultAction "phase:3,log,auditlog,pass"
  SecDefaultAction "phase:4,log,auditlog,pass"
  SecDefaultAction "phase:5,log,auditlog,pass"
  SecRuleEngine On
  SecDebugLog /dev/stdout
  SecDebugLogLevel 9
  SecRule REMOTE_ADDR "@ipMatchFromFile /caddy/banned_ips" "id:101,phase:1,t:lowercase,deny,status:403"
  SecRule REQUEST_URI "@streq /admin" "id:102,phase:1,t:lowercase,deny,status:403"
  SecRule REQUEST_BODY "@rx maliciouspayload" "id:103,phase:2,t:lowercase,deny,status:403"
  SecRule RESPONSE_STATUS "@rx 406" "id:104,phase:3,t:lowercase,deny,status:403"
  SecResponseBodyAccess On
  SecResponseBodyMimeType application/json
  SecRule RESPONSE_BODY "@contains responsebodycode" "id:105,phase:4,t:lowercase,deny,status:403"
  `
}

上記設定の以下の箇所でファイルに列挙されたIPアドレスとアクセスIPアドレスが一致する場合はアクセスをブロックするようにしています。

SecRule REMOTE_ADDR "@ipMatchFromFile /caddy/banned_ips" "id:101,phase:1,t:lowercase,deny,status:403"

ログイン認証失敗が閾値を超えた場合に発生するセキュリティイベントをトリガーにしてこのIPリストファイルを更新し、攻撃者のIPアドレスのブロックを自動化します。

エンドポイント(Webサーバー)の設定

アクティブ・レスポンス用のシェルスクリプトを/var/ossec/active-response/bin/ip-block.shに作成します。 スクリプトは下記のようになります。

#!/bin/bash
 
read INPUT_JSON
 
BANNED_IPS_FILE="/caddy/banned_ips"
 
ip_address=$(echo $INPUT_JSON | sed 's/\\/\\\\/g' | jq -r .parameters.alert.data.srcip)
 
echo "$ip_address" >> "$BANNED_IPS_FILE"
 
# ファイルの更新を反映させるためにWebサーバーの再起動
rc-service caddy restart
 
exit 0;

Wazuhサーバーの設定

アクティブ・レスポンスの設定を行います。 /var/ossec/etc/ossec.confに以下の内容を追加します。

<ossec_config>
 
  [...]
 
  <command>
    <name>ip_block</name>
    <executable>ip-block.sh</executable>
    <extra_args></extra_args>
    <timeout_allowed>no</timeout_allowed>
  </command>
 
  <active-response>
    <disabled>no</disabled>
    <command>ip_block</command>
    <!-- コマンドを実行するエンドポイントを直接指定 -->
    <location>defined-agent</location>
    <agent_id><エンドポイント(Webサーバー)のエージェントID></agent_id>
    <!-- 最大試行回数超過のルールID -->
    <rules_id>100403</rules_id>
  </active-response>
 
  [...]
 
</ossec_config>

Note

今回は単一Webサーバーを想定してエージェントIDを直接指定しています。 複数台構成の場合はlocationをallに設定して全てのエージェントでコマンドが実行されるようにし、コマンドにCaddyプロセスが存在すればファイルの更新を行うなどの処理を追加してください。

実際にログイン画面で最大試行回数を超過するとアクセスがブロックされることが確認できます。

<試行回数超過後>

インシデント管理ツールへの登録および通知

発生したセキュリティイベントのインシデント管理ツールへの登録およびチャットツールへの通知を行うワークフローを作成します。

Wazuhサーバーの設定

まず、n8nにイベントデータを送信するためのスクリプトをWazuhサーバーの/var/ossec/integrations/custom-n8nに作成します。

#!/usr/bin/python3
 
import json
import logging
import sys
import os
 
try:
    import requests
except ModuleNotFoundError:
    print("No module 'requests' found. Install: pip install requests")
    sys.exit(1)
 
LOG_FILE = '/var/ossec/logs/integrations.log'
 
def main(args):
    alert_file_location = args[1]
    with open(alert_file_location) as alert_file:
        json_alert = json.load(alert_file)
 
    try:
        agent_id = json_alert['agent']['id']
        alert_level = json_alert['rule']['level']
        description = json_alert['rule']['description']
 
        api_key = args[2]
        hook_url = args[3]
 
        message: str = generate_message(json_alert)
 
        if not message:
            return
 
        send_message(message, api_key, hook_url)
    except json.JSONDecodeError as json_error:
        logging.error(f"Error decoding JSON: {str(json_error)}")
    except KeyError as key_error:
        logging.error(f"Key not found in JSON: {str(key_error)}")
    except FileNotFoundError:
        logging.error("Alerts file not found")
    except Exception as e:
        logging.error(f"Error processing alerts: {str(e)}")
 
 
def generate_message(alert: any) -> str:
    level = alert['rule']['level']
 
    if level <= 7:
        severity = 1
    elif level >= 8 and level <= 12:
        severity = 2
    else:
        severity = 3
 
    message = {
        'severity': severity,
        'title': alert['rule']['description'] if 'description' in alert['rule'] else 'N/A',
        'text': alert.get('full_log'),
        'rule_id': alert['rule']['id'],
        'timestamp': alert['timestamp'],
        'id': alert['id'],
        'all_fields': alert,
    }
 
    return json.dumps(message)
 
 
def send_message(message: str, api_key: str, hook_url: str) -> None:
    headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'X-ApiKey': api_key}
    res = requests.post(hook_url, data=message, headers=headers, timeout=10)
 
 
if __name__ == "__main__":
    try:
        logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
        main(sys.argv)
    except Exception as e:
        logging.error(str(e))
        raise

次にWazuhサーバーの/var/ossec/etc/ossec.confに以下の内容を追加します。

<ossec_config>
 
  [...]
 
  <integration>
    <name>custom-n8n</name>
    <hook_url><n8nのWebフックURL></hook_url>
    <!-- ルールレベルが7以上のイベントのみ送信 -->
    <level>7</level>
    <alert_format>json</alert_format>
    <api_key><n8nで設定したAPIキー></api_key>
    <!-- 下記グループのイベントのみ送信 -->
    <group>malware,ids,attack,attacks,</group>
  </integration> 
 
  [...]
 
</ossec_config>

これで該当するセキュリティイベントが発生した時にn8nのWebフックにイベントデータが送信されるようになります。

インシデント管理ツール(Catalyst)の設定

Catalystにデータを連携して自動でチケットを作成するためにはReactionというものを作成する必要があります。 Reactionはトリガーとアクションで構成され、トリガーがイベントをリッスンしアクションはトリガーがアクティブになった時に実行されます。

管理画面で作成画面を開き以下の内容を設定します。

■トリガー

NameValue
TypeHTTP / Webhook
Token認証用トークン
PathWebhookのパス

■アクション

NameValue
TypePython
requirements.txtpocketbase
Script下記スクリプトを入力
import sys
import json
import os
 
from pocketbase import PocketBase
 
event = json.loads(sys.argv[1])
body = json.loads(event["body"])
 
client = PocketBase('http://0.0.0.0:8090')
client.auth_store.save(token=os.environ["CATALYST_TOKEN"])
 
severity = "Medium" if body["severity"] == 1 else "High"
groups = body["groups"].split(",")
type = "incident" if "incident" in groups else "alert"
 
if type:
  client.collection("tickets").create({
    "name": body["name"],
    "description": body["description"],
    "state": { "severity": severity }, 
    "type": type,
    "open": True,
  })

ワークフローの作成

まずセキュリティイベントを受信するWebフックを作成します(ノードパネルからOn webhook callを選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
HTTP MethodPOST
Path任意のパスを設定
AuthenticationHeader Auth
Credential for Header AuthNameにX-ApiKey、ValueにWazuhサーバーのossec.confで指定したAPIキーを設定
RespondImmediately
Response Code200

次にHTTP Requestノードを追加してCatalystにチケットを登録します(ノードパネルのHelpersから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
MethodPOST
URLCatalystで作成したReactionのWebフックURL
AuthenticationGeneric Credential Type
Generic Auth TypeHeader Auth
Credential for Header AuthAuthorization Bearer <Reactionで設定したトークン>
Send HeadersON
Header Parameters (Name1)Content-Type
Header Parameters (Value1)application/json
Header Parameters (Name2)Accept
Header Parameters (Value2)application/json
Send BodyON
Body Content TypeJSON
Specify BodyUsing Fields Below
Body Parameters(Name1)name
Body Parameters(Value1){{ $json.body.title }}
Body Parameters(Name2)description
Body Parameters(Value2){{ $json.body.text }}
Body Parameters(Name3)severity
Body Parameters(Value3){{ $json.body.severity }}
Body Parameters(Name4)groups
Body Parameters(Value4){{ $json.body.all_fields.rule.groups.join() }}

次にCodeノードで通知用データの加工を行います(ノードパネルのData transformationから選択)。

ノードを追加して設定パネルのコード入力欄に以下のJavaScriptを入力します。

const notification = {}
const severity = ['Medium', 'High', 'Critical']
 
for (const item of $('Recieve security event from Wazuh').all()) {
  notification.severity = severity[parseInt(item.json.body.severity) - 1]
  notification.title = item.json.body.title
  // ログのデコーダーがjsonの場合はJSON形式のログにインデントを付けて見やすいように整形
  notification.log = item.json.body.all_fields.decoder.name === 'json' ? JSON.stringify(JSON.parse(item.json.body.text), null, 2) : item.json.body.text
  notification.isIncident = item.json.body.all_fields.rule.groups.some(g => g === 'incident')
}
 
return [notification]

最後にMattermostノードを追加してアラートを通知します。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Credential to connect withMattermostで発行したアクセストークンとMattermostのURLを設定
ResourceMessage
OperationPost
Channel Name or ID投稿したいチャネルのIDを設定
Message下記のメッセージを入力
** {{ $json.isIncident ? ':fire: ' : ':rotating_light:' }}[{{ $json.severity }}]{{ $json.title }} **

> {{ $json.log }}

以上でワークフローは完成です。動作確認を行ってみます。

マルウェアをダウンロードすると下図のようにインシデントが登録され通知が届きます。

アプリケーションから外部のサーバーに多数のリクエストを送信すると下図のようにアラートが登録され通知が届きます。

アプリケーションのログイン認証に対してブルートフォース攻撃を行うと下図のようにアラートが登録され通知が届きます。