Ruby で ClamAV のコントロール

suzukiClamAV, clamdscan, Ruby, ウイルス対策

いま急激に温泉欲が高まっている suzuki です。

みなさん、Clam Anti Virus はご存知でしょうか?
オープンソースで開発が続けられている、ウイルス対策ソフトウェアです。

「オープンソースのウイルス対策ソフトウェアなんて大丈夫なの?」と思う方もいらっしゃるかも知れませんが、アップルの Mac OS X Server にも採用されている信頼のおけるステキなソフトウェアです。

今回は、この Clam Anti Virus(以下 ClamAV)を Ruby でコントロールしてみます。

ClamAV には、単体で動く clamscan コマンドと、デーモンとして clamd を起動しておき、そのデーモンと通信しつつウイルススキャンを行なう clamdscan コマンドが存在します。ここでは、この clamdscan に代替する部分を Ruby で作成してみました。

clamd + ruby
clamd + ruby

プログラムは、clamd デーモンとの通信を行なうライブラリ部分と、ファイルを読み込んでウイルスを発見した時の処理を行なうフロント部分とに分けてみました。

まずはライブラリ部分です。ちょっと手を抜いていて、clamd デーモンが localhost のデフォルトポート(3310)で動いていることを期待しています。なお、もし、ここに書いたサンプルを実行してみる場合には「clamd.rb」の名前で保存してください。

#!/bin/env ruby
#
# Clam Anti Virus Daemon 'clamd' control class
#

class Clamd

   require 'socket'

   ERROR_NOT_CONNECT = "Can not connect clamd daemon. perhaps, clamd is down ?"

   def initialize(host='127.0.0.1',port=3310)
      @host = host
      @port = port

      # check clamd is active ?
      begin
         result = ping
         if result != 'PONG'
            raise IOError.new(ERROR_NOT_CONNECT)
         end
      end
   end

   # Check the daemon's state (should reply with "PONG")
   def ping()
      result = execute('PING')
      result.chomp
   end

   # Print program and database versions.
   def version()
      result = execute('VERSION')
      result.chomp
   end

   # Reload the databases.
   def reload()
      result = execute('RELOAD')
      result.chomp!
      if result == "RELOADING"
         true
      else
         false
      end
   end

   # Perform a clean exit.
   def shutdown()
      result = execute('SHUTDOWN')
      result.chomp!
   end

   # Scan file or directory (recursively) with archive support enabled
   # (a full path is required).
   def scan(path)
      found = {}
      result = execute("SCAN #{path}")
      if result
         found = parse_result(result)
      end
   end

   # Scan file or directory (recursively) with archive and special
   # file support disabled (a full path is required).
   def rawscan(path)
      found = {}
      result = execute("RAWSCAN #{path}")
      
      found = parse_result(result)
   end

   # Scan file or directory (recursively) with archive support enabled
   # and don't stop the scanning when a virus is found.
   def contscan(path)
      found = {}
      result = execute("CONTSCAN #{path}")
      if result
         found = parse_result(result)
      end
   end

   # Scan file in a standard way or scan directory (recursively) using
   # multiple threads (to make the scanning faster on SMP machines).
   def multiscan(path)
      result = execute("MULTISCAN #{path}")
      if result
         found = parse_result(result)
      end
   end

   # Scan stream: clamd will return a new port number you should
   # connect to and send data to scan.
   def stream()
      execute("STREAM")
   end

   # Start/end a clamd session - you can do multiple commands per TCP
   # session (WARNING: due to the clamd implementation the RELOAD
   # command will break the session).
   def session()
      execute("SESSION")
   end
   def end()
      execute("END")
   end


   ##############################################################
   # private method
   ##############################################################
   private

   def execute(command)
      begin
         s = TCPSocket.open(@host,@port)
         if ! s
            raise IOError.new(ERROR_NOT_CONNECT)
         end
      end

      s.write(command)
      ret = ''
      while s.gets
         ret += $_
      end
      s.close

      return ret
   end

   def parse_result(result)
      found = {}
      tmp = result.split("\n")
      while t = tmp.shift
         if /(.+):\s+([^:]+)\s+FOUND$/ =~ t
            file = $1
            virus = $2
            found[file] = virus
         end
      end
      found
   end
end

やっていることは簡単で、socket を使って clamd デーモンと通信しているだけです。通信内容は、ClamAV のプロトコルに沿っています。

実は Ruby には、ClamAV を使う為の clamavr というライブラリが既に存在しますが、今回は前述のプロトコルを試してみたくて、自作しました。

次にフロント部分です。こちらはもっと手抜きで、スキャンするディレクトリは SCAN_DIR に指定したところ固定です。また、ウイルスを発見しても標準出力へ表示するだけです。

#!/bin/env ruby

require 'clamd.rb'

SCAN_DIR = '/usr/share/doc/clamav-0.94.2/test'

c = Clamd.new()

result = c.multiscan(SCAN_DIR)

if result.size > 0
   print 'WARNING: Virus Found !!!' + "\n"
   result.each do |file, type|
      print file + ':' + type + "\n"
   end

else 
   print 'OK: No Virus Found' + "\n"
end

実際に上記の clamd.rb を使う時には、ウイルスの有無を syslog へ書き出したり、メールを出したりと言った処理を付け加えるのが良いと思います。

「ウイルスに感染したファイルをサーバ上に置かせない」為の対策を取るのが、まず第一だとは思いますが、「置かれていないことを確認する」手段を念のため用意するのも良いのではないでしょうか。

ところで、このライブラリを作っていた時に気がついたのですが、clamd デーモンは、クライアントプログラム側から SHUTDOWN リクエストを送ると、見事に停止してくれます。クライアントプログラムとは言わず、telnet コマンドで次のように打ち込んでも停止してくれます。

$ telnet 127.0.0.1 3310
Trying 127.0.0.1...
Connected to localhost (127.0.0.1).
Escape character is '^]'.
SHUTDOWN
Connection closed by foreign host.

これって root ユーザではなく、一般ユーザでも終了できちゃいます。個人的には、ちょっと気持ち悪いんですが、何か回避方法ってあるのでしょうか?

suzukiClamAV, clamdscan, Ruby, ウイルス対策

Posted by suzuki