Logwatchの結果をチェックして「見慣れない」ログがあったらALERTするスクリプトのメモ

自分が使ってるサーバ(FC5, CentOS4)では、logwatchのバージョンがぜんぜん違う

  • FC5だと logwatch-7.x
  • CentOS4だと logwatch-5.x

そして、ログの種類によって、出力されるメッセージのスタイル、段落の切れ目などもけっこうバラバラなので、うまくparseできる文法をyacc(bison)のスタイルで書くのはかなりしんどそう。
せめて空行でパラグラフを区切るぐらいはできるかと思ったら、ところどころに /^\s$/ みたいな「空白のみを含む行」まで存在する始末。
そこで、考えを改めて、既知の「ALERTを出すほどではない」情報のパラグラフのスタイルを定義してみようと考えた。
たとえば、ssh関係のログのsummaryはこんなパラグラフになることが多い。空行の数が微妙なところにご注目(笑

 --------------------- SSHD Begin ------------------------


 Failed logins from:
    111.22.33.44 (foo.servername): 3 times

 Users logging in through sshd:
    me:
       11.55.66.77 (ok.servername): 5 times


 Received disconnect:
    2: disconnected by server request : 2 Time(s)

 **Unmatched Entries**
 error: channel_setup_fwd_listener: cannot listen to port: 3000 : 2 time(s)
 error: bind: Address already in use : 2 time(s)

 ---------------------- SSHD End -------------------------

初めが ----- SSHD Begin ---- で、
終わりが ---- SSHD End ---- にマッチすればいい。ぐらいのことはすぐできそうだ。
このパターンを一般化して、 ---- <サービスのタイトル> (Begin|End) ---- というのをマーカーにできそうだと考える。あとは、その中身をどう分類するか、なのだが、これがぜんぜん統一感が無い。
いくつかの出力結果を見比べて、分かったことは

  • ログのグループ分けは空行で行われるらしい
  • 各グループの最初の行は、ログのグループの説明か、そのグループで典型的なログでいきなり始まる
  • パラグラフの最初のマーカーの直後に空行があるとは限らない
  • パラグラフの最後のマーカーの前に空行があるとは限らない

実際、頼れるのはこれだけのようだった。

そこで、結局次のようなストラテジー(pseudo codeをRubyで書いてみた)で処理することになった

while thisline = gets do
  next if blank_line?(thisline)
  regexpsForServices.each do |servicename, regexps|
    found = false
    if regexps["start_marker"] =~ thisline then
      if (skipuntil(regexps["end_marker"], regexps["subparagraph_titles_array"])) then
        found = true
        break
      end
    end
  end
  raise "#{thisline} is illegal" unless found
end

とりあえず、このスタイルで「見知ったログのパラグラフとそのサブパラグラフ」はだいたい認識できるようになった。

あとはregepsのHashをどうやって構成するかだが、JSONで設定ファイルを作って、読み込んでみることにしてみた。
たとえば、上記のSSHDの場合は、こんな感じ↓ところで、マーカーの構造が常に同じなら、start_markerとend_markerの項目は冗長なのだが、実はそうではなかったりするところがミソである。たとえば、ディスク容量の項目は、end_markerが存在しなくて、Logwatch全体のエンドマーカーである"### LogWatch End #######"を使うことになったりした。

{
    "SSHD" :
    {
        "start_marker" : "---------- SSHD Begin --------",
        "subparagraph_titles_array" :
            [
            "Illegal users from these:",
            "Postponed authentication:",
            "Users logging in through sshd:"
            ],
         "end_marker" : "---------- SSHD End --------"
    },
    "httpd" :
    {
        "start_marker" : "---------- httpd Begin --------",
    (以下略)
}

これを読み込むコードは、Rubyだとこんな感じか。

require 'jsonp.rb'

fd = open(config_by_json_file).read
regexpsForServices = JsonParser.new.parse(fd)

p regexpsForServices["SSHD"]["start_marker"] => ---------- SSHD Begin --------

JSONのparserは、Ruby で JSON パーサーを書いてみました - WebOS Goodiesで紹介されていたものを利用させていただきました。

で、ここまでが自分としては前フリである(笑)本当にやりたいのは、上記のJSONのルール(white list)を、OKだったlog summary(メールで送られてくる)を入力として、自動生成することなのだ。
すでに、HashからJSONのファイルを作成するJsonBuilderクラスはjsonp.rbに用意されているので、あとは

  • start/end_markerの例外処理の検出
  • subparagraph_titles_arrayの配列の採取

ができれば良い。
特に後者は、SElinuxのlog summaryのように
/\*\*Unmatched Entries\*\* \(Only first \\d+ out of \\d+ are printed\)/
とかいうパラメトリックな形式のものがあるので、自動判別はかなり難しい。たくさんの入力データから得られたパターンを見比べて、最長一致をとるとかするしかないかもしれない。
そんな苦労をするくらいなら、ベイジアンフィルタを考えようよ、というのが、数日前の呻きだったのだ。