VirusTotalでのファイルスキャン結果をRaspberry Piに喋ってもらう話

概要

ふと、VirusTotalでAPIが提供されていたことを思い出し、なんとなく叩こうという気持ちが高まったのがきっかけです。しかしただ触るだけでは面白く無いので、今回は帰ってきた結果を喋らせてみようかなと独り言を放ちました。すると、昔々某企業様から某イベントの某賞で頂いたRaspberry Pi(Model B)が仲間になりたそうな目でこちらを見てきたので、喋ってもらうことにしました。なお、プログラミング言語にはPythonを、音声合成には非モノローグ音声合成を用いました。

VirusTotal APIについて

VirusTotalにファイルを投げると、50個くらいのウイルス対策ソフトでファイルをスキャンしてくれます。そのAPIを叩いていくということになりますが、提供されている機能は

  1. ファイルを投げる(スキャンキューに追加、スキャン実施。制限は4個/分。)
  2. スキャン結果レポートの受信
  3. 過去にスキャンしたことのあるファイルの再スキャン

と言った感じ(他にもあるので興味のある方は公式ドキュメントを読むと良いと思います)。レスポンスはJSON形式でくれるので、良い感じにパースしてやれば良いです。今回は1, 2を使ってファイルを投げる→結果をパースして喋らせることをやります。
基本的にはドキュメントに書いてあるとおりに通りにやったので、ハマりどころはほとんどありませんでした。

ステップ1.ファイルを投げる

1
https://www.virustotal.com/vtapi/v2/file/scan

にファイルを送ります。ドキュメントにも書いてありますが、multipart/form-dataを転送するコードを先人が用意してくださっているので、利用します。なお、httpsでリクエストするように先人のコードに手を加えておかないと、「Bad Request」と怒られてしまうのでここだけ気をつけます。
今回は先人のコードをクラス化してimportして利用しました。それ以外はほぼドキュメント通りです。

1
2
3
4
5
6
7
8
9
10
import postfile
def post_bin(target):
pf = postfile.postfile()
host = "www.virustotal.com"
selector = "https://www.virustotal.com/vtapi/v2/file/scan"
fields = [("apikey", APIKEY)]
file_to_send = open(target, "rb").read()
files = [("file", target, file_to_send)]
sent = pf.post_multipart(host, selector, fields, files)
return sent

sentにはファイル送信に対するレスポンスが入ります。レスポンスは

1
2
3
4
5
6
7
8
9
10
{
"scan_id": "SCAN\_HASH",
"sha1": "SHA1_HASH",
"resource": "RESOURCE_HASH",
"response_code": 1,
"sha256": "SHA256_HASH",
"permalink": "PERMALINK",
"md5": "MD5_HASH",
"verbose_msg": "Scan request successfully queued, come back later for the report"
}

という形式で、この中の“scan_id”キーに対応する値を使って結果レポートを取得していきます。帰ってきているJSONレスポンスのキー順序が若干ドキュメントに示されているものと前後しているのは気にしない方針でやっていきます(特に困らないので)。

ステップ2.結果レポートの受信

1
https://www.virustotal.com/vtapi/v2/file/report

に取得したスキャンIDを添えてリクエストを投げます。といっても送信後すぐにレポートが見られるわけではなくて、VirusTotalでのファイルスキャンにはある程度の時間(2~5分くらい?)を要します。ので、スキャンが完了してからレポートをパースするような設計にすれば良いです。スキャンが終了しているかどうかは、レスポンスJSON内の“response_code”で識別できます。このキーに対応する値が-2の時はまだスキャン中なので、その間は大人しく待ちます。今回は1待ち間隔を30秒にしましたが、本家では確か5秒に一回更新していたので、もう少し短くても良いかもわかりません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def read_report(sent):
r = json.loads(sent)
while True:
url = "https://www.virustotal.com/vtapi/v2/file/report"
parameters = {
"resource": r["scan_id"], # ここでscan_idを指定する
"apikey": APIKEY
}
data = urllib.urlencode(parameters)
req = urllib2.Request(url, data)
res = urllib2.urlopen(req)
j = json.loads(res.read())
# レスポンスコードが-2の間は気長に待つ心が大事です
if j["response_code"] != -2:
break
else:
print "still work..."
time.sleep(30) # 30秒後にもう一回リクエストを投げる(連投防止)
return j

ステップ3. 結果をパースして音声合成する

ここまでで検知結果のJSONが取得できているはずなので、結果を元に音声合成をします。音声合成の部分もサンプルコードとして提示されているものをそのまま使えば良いです(今回ほとんどコード書いてない!w)。base64エンコードされたwav音声が手に入ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def voice(message):
tts_url ="http://rospeex.ucri.jgn-x.jp/nauth_json/jsServices/VoiceTraSS"
tts_command = {
"method":"speak",
"params": [
"1.1",
{
"language": "ja",
"text": message,
"voiceType": "*",
"audioType": "audio/x-wav"}]}
obj_command = json.dumps(tts_command)
req = urllib2.Request(tts_url, obj_command)
received = urllib2.urlopen(req).read()
obj_received = json.loads(received)
tmp = obj_received['result']['audio']
speech = base64.decodestring(tmp.encode('utf-8'))
f = open ("out.wav",'wb')
f.write(speech)
f.close

引数のmessageは

1
2
3
4
5
6
7
8
# "positives"の値 => ウイルス対策ソフトによる陽性反応の数
# "total"の値 => ファイルスキャンを実施したウイルス対策ソフトの合計数
if result["positives"] > 0:
voice("%d個中%d個のウイルス対策ソフトがマルウェアと判定しました!" % (result["total"], result["positives"]))
print "malware detected."
else:
voice("危険なソフトウェアではない可能性が高いと判定されました。")
print "maybe it's benign."

こんな感じで決めてやります。

ステップ4. 喋らせる

やっと喋らせるターンです。ステップ3でwavファイルが手に入っているので、雑ですが

1
2
import os
os.system("aplay out.wav")

で再生してしまいます。aplayはCUI上で音声再生を行えるものです。やるだけ感が半端ないのですが、今回ラズパイが久々の起動だったからかrpi-update, apt-get update/upgrade周りが失敗したらしく、aplayやamixer, alsamixerといったツール群がすべて使用不能になり、やむなくraspbian OSの再インストール・設定を行いました(ここで一番時間溶かした)。OS再インストール後は普通に再生ができました。

おわりに

今回はRaspberr Piを長き封印から解き放ち、言葉を喋らせることをやりました。VirusTotal本家でファイルを投げてブラウザの画面が忙しく更新され、無機質なレポート(失礼)が表示されるのを待つよりも、待っている間のわくわく感が高まった感じがあります(※個人差が有ります)。ラズパイを採択した理由は本当になんとなくでしたが、今回使用したNICTの音声合成エンジンが元々ROS向けに開発されたもの(らしい)なので、せっかくだしPCではなくこれで、と言った理由がないわけでもないです。まあしかしラズパイの特徴を活かせているかと言われるとう~んという感じなので、個人的には今度はロボットとか作ってみたいです。

参考資料