Ruby で ClamAV のコントロール
いま急激に温泉欲が高まっている suzuki です。
みなさん、Clam Anti Virus はご存知でしょうか?
オープンソースで開発が続けられている、ウイルス対策ソフトウェアです。
「オープンソースのウイルス対策ソフトウェアなんて大丈夫なの?」と思う方もいらっしゃるかも知れませんが、アップルの Mac OS X Server にも採用されている信頼のおけるステキなソフトウェアです。
今回は、この Clam Anti Virus(以下 ClamAV)を Ruby でコントロールしてみます。
ClamAV には、単体で動く clamscan コマンドと、デーモンとして clamd を起動しておき、そのデーモンと通信しつつウイルススキャンを行なう clamdscan コマンドが存在します。ここでは、この clamdscan に代替する部分を 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 ユーザではなく、一般ユーザでも終了できちゃいます。個人的には、ちょっと気持ち悪いんですが、何か回避方法ってあるのでしょうか?


ディスカッション
コメント一覧
まだ、コメントがありません