Node.jsでウイルスチェックを行います。ユーザーがアップロードしたファイルを公開するようなサービスでは事前にチェックを行わないと、簡単にウイルスを撒き散らすスーパースプレッダーと化してしまいます。
今回は無料で使えるオープンソースのアンチウイルスソフト「ClamAV」を利用します。
最終的なコード
GitHubへアップしました。全体像が見たい方はこちらをどうぞ。 github.com
インストール
ClamAV
まずはClamAV本体を入れます。Amazon Linux2の場合は以下をご覧くださいませ。
macOSの場合はこちらをどうぞ。
インストールされたコマンドの場所や、ウイルスの定義がされたデータベースの場所をメモしておいてください。またデーモンを利用する場合には通信先のIPアドレス、ポート、socketファイルなどの情報が必要になります。
プロジェクトの準備
Node.jsをインストールしたら、npmなどでpackage.jsonを適当に用意します。今回はNode.jsからClamAVを利用できるclamscanモジュールをインストールします。
$ mkdir clamscan && cd clamscan $ npm init $ npm install clamscan
サンプルコード
ファイル単体をスキャン
NodeClam().init()
で初期化、clamscan.is_infected(file)
の引数にチェックしたいファイルのパスを渡すだけです。子プロセスが生成されclamscanコマンドが実行されます。
const path = require('path') const NodeClam = require('clamscan') // 初期化 const ClamScan = new NodeClam().init({ clamscan: { path: '/usr/local/bin/clamscan', // clamscanコマンドの絶対パス db: '/usr/local/var/lib/clamav' // 定義ファイル用ディレクトリの絶対パス } }) // ウイルススキャン ClamScan .then(async clamscan => { try { const target = path.resolve('sample/eicar.com') const {is_infected, file, viruses} = await clamscan.is_infected(target) // 感染チェック if (is_infected){ console.log(`${file} は ${viruses} に感染しています`) } else{ console.log(`${file} は健康です`) } } catch (err) { console.eror(`[ERROR1] ${err}`) } }) .catch(err => { console.error(`[ERROR2] ${err}`) });
ディレクトリをまるごとスキャン
ファイルをチェックするのとそれほど変わりません。NodeClam().init()
で初期化、clamscan.scan_dir(dir, callback)
でチェックを行います。こちらも子プロセスが生成されclamscanコマンドが実行されます。
const path = require('path') const NodeClam = require('clamscan') // 初期化 const ClamScan = new NodeClam().init({ clamscan: { path: '/usr/local/bin/clamscan', // clamscanコマンドの絶対パス db: '/usr/local/var/lib/clamav' // 定義ファイル用ディレクトリの絶対パス } }) // ウイルススキャン ClamScan .then( clamscan => { try { const target = path.resolve('sample/') clamscan.scan_dir(target, (err, good_files, bad_files, viruses) => { // 実行時エラー if (err){ console.log(err) } // 感染が見つかった else if (bad_files.length > 0) { console.log(`${viruses.join(',')}に感染しています`) console.log(bad_files.join('\n')) } // 感染なし else{ console.log('健康です') } }) } catch(err) { console.log(`[ERROR1] ${err}`) } }) .catch(err => { console.log(`[ERROR2] ${err}`) })
デーモンを利用する
ここまでは通常のコマンドでしたが、デーモンを起動した状態でNodeClam().init()
に通信先の情報をセットすることでデーモンを利用することができます。以下ではデーモンとTCPによる通信を行っています。
// 初期化 const ClamScan = new NodeClam().init({ clamdscan: { socket: '/tmp/clamd.socket', host: '127.0.0.1', port: 3310, path: '/bin/clamdscan', config_file: '/usr/local/etc/clamav/clamd.conf' } })
もちろんこちらの方が高速に動作します。頻繁に実行する場合はデーモンを利用したいところですが、メモリを1Gほど消費するので環境によっては注意が必要です。
その他
scan_dirをasync/awaitで利用するとエラーが発生する
当初はドキュメントに掲載されていた以下のコードを元に作成していたのですが、
const {path, is_infected, good_files, bad_files, viruses} = await clamscan.scan_dir('/some/path/to/scan');
以下のようなエラーが発生してしまいます。
$ node clamscan_dir.js (node:76405) UnhandledPromiseRejectionWarning: Error: Error: spawn /usr/local/bin/clamscan --no-summary --stdout --remove=no --database=/usr/local/var/lib/clamav --scan-archive=yes -r sample sample/arupaka.png sample/eicar.com ENOENT
コードを追うのが面倒で原因がすぐには分からなかったので試しにCallbackのサンプルを利用したらこちらはエラーが発生せず、正常に動作しました。何でしょうね?