[Node.js] ClamAVでウイルスチェック

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のサンプルを利用したらこちらはエラーが発生せず、正常に動作しました。何でしょうね?

参考ページ