河野哲治のコラム「LuaでSyslogを監視してSlackに通知してみよう」

みなさんこんにちは。テックデザインの河野です。

1/23(木)にヤマハ銀座スタジオで行われた「ヤマハMeetup東京2020 ~Happy New Year~」のライトニングトークセッションに参加させていただき、ヤマハルーターのSyslogをLuaスクリプトで監視して、ルーターへのログイン/ログアウトを検出したらSlackへ通知するというデモを行いました。今回はそのSlack通知Luaスクリプトを紹介いたします。

本記事ではvRXを使って解説をしています。お手元にヤマハルーターがなくても、AWSアカウントをお持ちであれば vRX無償トライアルライセンスがなくても試すことが可能です。動作確認は vRX Rev.19.00.01 (Fri Sep 13 12:13:56 2019)とRTX830 Rev.15.02.14で行っています。

1. ヤマハルーター対応機種について

通知はSlackのIncoming WebhookにHTTPリクエストで送信しています。HTTPSを使用するため、ヤマハルーターに搭載されているLuaバージョンが1.08以上である必要があります。ヤマハルーターの現行機種では、NVR500を除く全ての機種対応しています。

[対応機種]
RTX5000/RTX3500/RTX1210/RTX830
NVR700W/NVR510
vRX

参考:Lua スクリプト機能バージョンの変更履歴
http://www.rtpro.yamaha.co.jp/RT/docs/lua/#version

2. 通知メッセージのフォーマット

通知メッセージの1行目は見出しとして太字にし、2行目は検知したSyslogメッセージとします。また、メッセージの重要度や緊急性が一目で分かるように赤・黄・緑の3色に色分けをして通知します。

図1 通知メッセージ例

3. ソースコード

> 下記のLuaスクリプトをダウンロードすることができます。(解凍してお使いください)

◆ヤマハルーターSlack通知Luaスクリプト
動作環境:LUAスクリプト機能バージョン 1.08以上
動作機種:RTX5000/RTX3500/RTX1210/RTX830/NVR700W/NVR510/vRX
--]]

----------------##  初期設定  ##-------------------

-- 検出したいSYSLOG文字列パターン
ptn = "Log%l%l%l* "

-- Slack Incoming Webhook URL
url = "(Webhook URL)"

-- Slackチャンネル名
-- 例:#general
channel = "(通知先チャンネル名)"

-- Slack通知ユーザー名
-- 例:vRX Syslog監視bot
botname = "(表示ユーザー名)"

------------##  初期設定ここまで  ##-----------------

--------------------------------------------------
-- 通知カラー名・Subject設定用関数
--------------------------------------------------
function make_arg(syslog_message)
  if string.find(syslog_message, "succeeded") ~= nil then
    color_name = "green"
    subject = "*情報:ログイン成功*"
  end
  if string.find(syslog_message, "failed") ~= nil then
    color_name = "yellow"
    subject = "*注意:ログイン失敗*"
  end
  if string.find(syslog_message, "Logout ") ~= nil then
    color_name = "green"
    subject = "*情報:ログアウト*"
  end
  if string.find(syslog_message, "restricted") ~= nil then
    color_name = "red"
    subject = "*警告:ログイン制限中*"
  end
    return color_name, subject
end

--------------------------------------------------
-- カラー名・RGB変換用関数
--------------------------------------------------
function set_rgb_color(color_name)
  if color_name == "green" then
    return "#01bc77"
  elseif color_name == "red" then
    return "#ec4301"
  elseif color_name == "yellow" then
    return "#ffc759"
  else
    -- gray
    return "#e3e4e6"
  end
end

--------------------------------------------------
-- Slackペイロード作成用関数
--------------------------------------------------
local function make_payload(channel, botname, color_name, subject, syslog_message)

  -- カラー名をRGBに変換
  local color_rgb = set_rgb_color(color_name)

  -- メッセージ部
  local text = subject .. "\\n" .. syslog_message

  -- ペイロードを作成
  local payload = "payload={\"channel\": \"" .. channel .. "\", \"username\": \"" .. botname .. "\", \"mrkdwn\": true, \"attachments\": [{\"color\": \"" .. color_rgb .. "\", \"text\": \"" .. text .. "\", \"mrkdwn_in\": [\"text\"]}]}"

  return payload
end

--------------------------------------------------
-- Slack通知用関数
--------------------------------------------------
function post_slack(url, payload)
  local req_t = {
    url = url,
    method = "POST",
    content_type = "application/x-www-form-urlencoded",
    post_text = payload
  }

  -- SlackにPOST
  local rsp_t = rt.httprequest(req_t)

  -- エラー処理
  if (rsp_t.code ~= 200) then
    -- デバッグ用
    print("Post failed.")
    print(rsp_t.rtn1, rsp_t.rtn2, rsp_t.err, rsp_t.code, rsp_t.header)
  end
end

--------------------------------------------------
-- メインルーチン
--------------------------------------------------
local rtn, str, color_name, subject, payload

while (true) do
  rtn, str = rt.syslogwatch(ptn, 1)
  if (rtn) and (str) then
    color_name, subject = make_arg(str[1])
    payload = make_payload(channel, botname, color_name, subject, str[1])
    post_slack(url, payload)
  end
end

4. 解説

初期設定

ユーザー環境に合わせて設定が必要になるのは初期設定の4項目のみです。

SYSLOG文字列の監視は rt.syslogwatch コマンドで行っています。監視する文字列にはLua正規表現パターンを利用することができますがカッコを用いたキャプチャ機能には対応していないため、残念ながらあまり複雑なパターンを作ることはできません。

下記の例では「Login 」と「Logout 」(末尾に半角スペース付き)を検知するため、共通部分「Lo」のあとに2〜3文字の任意の英小文字の文字列を表す「%l%l%l*」と半角スペースを付けています。
検知する文字列を変更したい場合は、このパターンを変更してください。

Luaの正規表現については下記に詳しく解説されています。
Lua言語のライブラリ関数
http://www.rtpro.yamaha.co.jp/RT/docs/lua/tutorial/library.html#pattern

また、本稿ではSlackのIncoming Webhook URLの取得方法については割愛します。
下記参考に設定し、取得してください。

Slack での Incoming Webhook の利用 | Slack
https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8

----------------##  設定値  ##--------------------

-- 検出したいSYSLOG文字列パターン
ptn = "Log%l%l%l* "

-- Slack Incoming Webhook URL
url = "(Webhook URL)"

-- Slackチャンネル名
-- 例:#general
channel = "(通知先チャンネル名)"

-- Slack通知ユーザー名
-- 例:vRX Syslog監視bot
botname = "(表示ユーザー名)"

------------##  設定値ここまで  ##----------------

通知カラー名・Subject設定用関数

ここでは通知メッセージのカラーと1行目の文字列を設定しています。
色指定は最終的にRGBで行いますが、追加や変更がしやすいように green や red など名前で指定しています。

また、検知したSYSLOGメッセージに「succeeded」「failed」「Logout 」「restricted」が含まれているかをチェックし、パターン毎に1行目の文字列を設定しています。

--------------------------------------------------
-- 通知カラー名・Subject設定用関数
--------------------------------------------------
function make_arg(syslog_message)
  if string.find(syslog_message, "succeeded") ~= nil then
    color_name = "green"
    subject = "*情報:ログイン成功*"
  end
  if string.find(syslog_message, "failed") ~= nil then
    color_name = "yellow"
    subject = "*注意:ログイン失敗*"
  end
  if string.find(syslog_message, "Logout ") ~= nil then
    color_name = "green"
    subject = "*情報:ログアウト*"
  end
  if string.find(syslog_message, "restricted") ~= nil then
    color_name = "red"
    subject = "*警告:ログイン制限中*"
  end
    return color_name, subject
end

カラー名・RGB変換用関数

green / red/ yellow の文字列を RGB 値に変換し、それ以外の文字列が与えられた場合にはグレーの RGB 値を返します。後述の「Slack ペイロード作成用関数」から呼び出して使っています。

--------------------------------------------------
-- カラー名・RGB変換用関数
--------------------------------------------------
function set_rgb_color(color_name)
  if color_name == "green" then
    return "#01bc77"
  elseif color_name == "red" then
    return "#ec4301"
  elseif color_name == "yellow" then
    return "#ffc759"
  else
    -- gray
    return "#e3e4e6"
  end
end

Slackペイロード作成用関数

Incoming Webhook API に送信する JSON 形式のペイロードを作成します。
Slack のペイロード仕様については下記で詳しく説明されています。

Reference: Message payloads | Slack
https://api.slack.com/reference/messaging/payload

--------------------------------------------------
-- Slackペイロード作成用関数
--------------------------------------------------
local function make_payload(channel, botname, color_name, subject, syslog_message)

  -- カラー名をRGBに変換
  local color_rgb = set_rgb_color(color_name)

  -- メッセージ部
  local text = subject .. "\\n" .. syslog_message

  -- ペイロードを作成
  local payload = "payload={\"channel\": \"" .. channel .. "\", \"username\": \"" .. botname .. "\", \"mrkdwn\": true, \"attachments\": [{\"color\": \"" .. color_rgb .. "\", \"text\": \"" .. text .. "\", \"mrkdwn_in\": [\"text\"]}]}"

  return payload
end

Slack通知用関数

rt.httprequest 関数を使い、Webhook URL に HTTP POSTメソッドで作成したペイロードを送信します。

--------------------------------------------------
-- Slack通知用関数
--------------------------------------------------
function post_slack(url, payload)
  local req_t = {
    url = url,
    method = "POST",
    content_type = "application/x-www-form-urlencoded",
    post_text = payload
  }

  -- SlackにPOST
  local rsp_t = rt.httprequest(req_t)

  -- エラー処理
  if (rsp_t.code ~= 200) then
    -- デバッグ用
    print("Post failed.")
    print(rsp_t.rtn1, rsp_t.rtn2, rsp_t.err, rsp_t.code, rsp_t.header)
  end
end

送信に失敗した場合は、デバッグ用としてコンソール上にHTTPリクエストの送信結果、エラーメッセージ、HTTPステータスコード、レスポンスデータのヘッダを出力するようにしています。
下記は誤った Webhook URL を設定した際の出力です。ステータスコード 403 がヒントになりますね。

vRX> Post failed.
true    nil nil 403 HTTP/1.1 403 Forbidden
Date: Sun, 01 Mar 2020 18:00:20 GMT
Server: Apache
Vary: Accept-Encoding
X-Slack-Backend: h
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Referrer-Policy: no-referrer
X-Frame-Options: SAMEORIGIN
Access-Control-Allow-Origin: *
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html
X-Via: haproxy-www-rtkx

メインルーチン

rt.syslogwatch 関数を while true のループで実行することでSYSLOGを監視し続けます。指定した文字列を検知すると、

1)make_arg 関数で検知内容に適合する通知カラーとSubjectを設定
2)make_payload 関数でペイロード作成
3)post_slack 関数で Incoming Webhook URL に作成したペイロードを送信

の順に実行します。

--------------------------------------------------
-- メインルーチン
--------------------------------------------------
local rtn, str, color_name, subject, payload

while (true) do
  rtn, str = rt.syslogwatch(ptn, 1)
  if (rtn) and (str) then
    color_name, subject = make_arg(str[1])
    payload = make_payload(channel, botname, color_name, subject, str[1])
    post_slack(url, payload)
  end
end

5. Luaスクリプトをヤマハルーターに転送する

初期設定が完了したら、tftp コマンドや sftp コマンドで slack.lua をルーターに転送します。

$ sftp -i ~/.ssh/vRX_key.pem XX.XX.XX.XX
Connected to XX.XX.XX.XX.
sftp> put slack.lua /slack.lua
Uploading slack.lua to /slack.lua
slack.lua               100%  503    23.3KB/s   00:00
sftp> quit

転送が完了したらヤマハルーターにSSHでログインして管理者ユーザーに切り替え、 schedule at コマンドでsyslog_watch.luaをルーター起動時に開始するように設定します。既にスケジュールが登録されている場合は、環境に合わせてスケジュール番号を適宜変更してください。

vRX# schedule at 1 startup * /slack.lua

スケジュールコマンドを登録したらルーターをいったん再起動して、Luaスクリプトが自動的に起動するか確認します。show status lua コマンドを実行して、[running] に slack.lua が表示されていれば成功です。

vRX> show status lua
Luaライブラリバージョン:        Lua 5.1.5
Luaスクリプト機能バージョン:    1.08

[running]
LuaタスクID (状態):  1  (WATCH)
走行トリガー:        スケジュールによる実行
コマンドライン:      lua /slack.lua
スクリプトファイル:  /slack.lua
監視文字列:          "Log%l%l%l* "
開始日時:            2020/03/02 00:34:40
経過時間:            38分51秒
(以下省略)
```

6. 動作確認

実際にログインやログアウトを行うことで動作確認をすることができますが、LTデモで行ったように管理者ユーザーでSYSLOGを直接書き込む方法でも動作確認をすることができます。

vRX# lua -e "rt.syslog(\"INFO\", \"2020/03/02 03:14:45: [TEST] Login succeeded : XX.XX.XX.XX  as administrator\")"

ヤマハのウェブサイトには既に多くの実用的な設定例が公開されていますが、「Google 認証システムの確認コード生成方法に準じたワンタイムパスワードを設定する」は「Luaでここまで機能拡張ができるのか!」と個人的に最も衝撃を受けた設定例です。外部サービスと連携するとヤマハルーターをより便利に面白く使うことができると思いますので、「私はこんな使い方をしているよ!」みたいな例があれば Twitter や Facebook で教えていただけると嬉しいです。