Linuxのウィルス対策がそんなに難しいわけがない

# Linuxのウィルス対策は難しいのか？
結論から言うと難しくはないのですが、思った以上に手こずりました。
マニュアルどおりに設定しても想定どおりに動作しない原因を特定したり、正しい設定を調べるのにそれなりの手間がかかりました。

そこで定時スキャンだけの簡易なウィルス対策に機能を絞って、セットアップを自動化するためのシェルスクリプトを自作しました。

リアルタイムスキャンをしない代わりにメモリに常駐しないので、他のプロセスへの影響が心配なサーバや非力なマシンにも向いていると思います。

ウィルス対策ソフトは、フリーの [ClamAV](http://www.clamav.net/) を使用します。

## 特徴
* 最低限のパッケージのインストール、定時スキャンのスケジュール、感染ファイルの隔離とロギングの設定を行う
* スケジュールや除外ディレクトリ/ファイルを簡単に設定できる
* プロキシが設定された環境の場合、自動的にプロキシ設定を行う
* SELinuxが有効な場合、自動的にウィルススキャンの許可設定を行う
* セットアップの冪等性（何度実行しても同じ状態になる）
* ClamAV 以外のソフトウェアを使用しない

## 環境
CentOS 7.6 Minimal で確認

# ダウンロード
以下のリポジトリからダウンロードしてご利用いただけます。
[https://github.com/i3244/easy-clamav-setup](https://github.com/i3244/easy-clamav-setup)

# コードの説明
以下、説明用に簡略化したコードを使って説明します。

## 設定ファイル
必要に応じて以下の設定を変更してから、次に紹介する`easyclamav_setup.sh`を実行すればセットアップが完了します。

```Bash:easyclamav.conf
# 定時実行スクリプト、スキャン除外リスト およびこのファイルを格納するディレクトリ
EASYCLAMAV_HOME="${HOME}/.easyclamav"

# スキャン除外パスの配列
# 各パスの先頭は'/'で始まり、ディレクトリは'/'で終わらなければならない
# $MOVE_DIRECTORYは自動的に追加される
EXCLUDE_PATHS=(
  /sys/
  /proc/
  /dev/
)

# スキャン対象ファイルの最大サイズ (最大: < 4 GB)
MAX_FILESIZE=100M

# 圧縮ファイル展開後の最大スキャンサイズ (最大: < 4 GB)
MAX_SCANSIZE=200M

# 感染ファイルの移動先
MOVE_DIRECTORY=/var/tmp/infected_files

# スキャン対象ディレクトリ
SCAN_DIRECTORY=/

# 定時スキャンのスケジュール
# 以下の例は毎日12:01に実行する
#              .---------------- minute (0 - 59)
#              |  .------------- hour (0 - 23)
#              |  |  .---------- day of month (1 - 31)
#              |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
#              |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7)
#              |  |  |  |  |     OR sun,mon,tue,wed,thu,fri,sat
#              *  *  *  *  *
SCAN_SCHEDULE="1 12  *  *  *"
```

## セットアップスクリプト
以下の1.～5.のソースを連結するとセットアップ用のスクリプトになります。

### 1. RPMパッケージのインストール

まず必要なRPMパッケージをインストールします。

```Bash:easyclamav_setup.sh
#!/bin/bash

cd $(dirname "${0}")
source easyclamav.conf || exit ${?}

# EPELリポジトリが未インストールならインストールする
yum --disablerepo=* list installed epel-release 2>/dev/null ||
    yum -y --enablerepo=extras install epel-release || exit ${?}

# ClamAVの非常駐実行に必要な最低限のパッケージをインストールする
yum --disablerepo=* list installed clamav 2>/dev/null ||
    yum -y --enablerepo=epel install clamav || exit ${?}
```

### 2. 日本のウィルスデータベースを優先的に参照する設定を追加
タイムゾーンが日本の場合、ウィルスデータベースを更新する時に日本のサイトを優先して参照するように、freshclam設定ファイルのデフォルトURLの上に日本のURLを挿入します。

```Bash:easyclamav_setup.sh

conf_file="/etc/freshclam.conf" # freshclamの設定ファイル

if timedatectl status | grep -q "^\s*Time zone:\s*Asia/Tokyo"; then
  new_db_url_line="DatabaseMirror db.jp.clamav.net" # 日本のデータベースのURL

  # 設定ファイルの既存URLの行を「日本のURL＋改行＋既存URL」で置換する
  grep -q "^${new_db_url_line}" "${conf_file}" ||
      sed -i "s/^\(DatabaseMirror .*\)$/${new_db_url_line}\n\1/" "${conf_file}"
fi
```
変更前
`DatabaseMirror database.clamav.net`

変更後
`DatabaseMirror db.jp.clamav.net`
`DatabaseMirror database.clamav.net`

### 3. プロキシ環境の場合に設定を追加
環境変数`http_proxy`が設定されている場合のみ、設定ファイルにプロキシ設定を追加します。

プロキシの値は以下のパターンに対応します。
* `http[s]://`の有無
* `/`の有無
* `user:pass@server:port`
* `server:port`

```Bash:easyclamav_setup.sh
if [[ "${http_proxy}" != "" ]]; then
  proxy_full=${HTTP_PROXY/http:\/\//}   # 'http://'を除去
  proxy_full=${HTTP_PROXY/https:\/\//}  # 'https://'を除去
  proxy_full=${proxy_full////}          # '/'を全部除去

  proxy_port=${proxy_full##*:}      # 最後の':'より右（ポート）を取得
  proxy_full=${proxy_full%:*}       # 最後の':'以降を除去

  if [[ ${proxy_full} =~ @ ]]; then
    # '@'がある場合、サーバ、ユーザ名、パスワードを取得
    proxy_server=$(echo ${proxy_full} | cut -d@ -f2)
    proxy_user=$(echo ${proxy_full} | cut -d@ -f1 | cut -d: -f1)
    proxy_pass=$(echo ${proxy_full} | cut -d@ -f1 | cut -d: -f2)
  else
    # '@'がない場合、サーバを取得
    proxy_server=${proxy_full}
    proxy_user=
    proxy_pass=
  fi

  # 設定ファイルにサーバとポートを設定
  sed -i "s/^#*HTTPProxyServer .*$/HTTPProxyServer ${proxy_server}/"  "${conf_file}"
  sed -i "s/^#*HTTPProxyPort .*$/HTTPProxyPort ${proxy_port}/"        "${conf_file}"

  # 設定ファイルにユーザ名とパスワードを設定
  if [[ "${proxy_user}" != "" ]]; then
    sed -i "s/^#*HTTPProxyUsername .*$/HTTPProxyUsername ${proxy_user}/" "${conf_file}"
    sed -i "s/^#*HTTPProxyPassword .*$/HTTPProxyPassword ${proxy_pass}/" "${conf_file}"
  fi
fi
```
### 4. SELinuxが有効な場合に許可設定を追加

```Bash:easyclamav_setup.sh
# SELinuxの許可設定を追加
if [[ $(getenforce) == "Enforcing" ]]; then
  setsebool -P antivirus_can_scan_system on
  setsebool -P antivirus_use_jit         on
fi
```
### 5. ファイルのコピーと定時実行スケジュールの設定

```Bash:easyclamav_setup.sh
# 同じディレクトリにある定時実行スクリプトをコピーする
mkdir -p "${EASYCLAMAV_HOME}"
cp -f easyclamav      "${EASYCLAMAV_HOME}" || exit ${?}
cp -f easyclamav.conf "${EASYCLAMAV_HOME}" || exit ${?}
chmod +x "${EASYCLAMAV_HOME}/easyclamav"

# スキャン除外リストを生成する
exclude_list="${EASYCLAMAV_HOME}/exclude_list"
: >"${exclude_list}"
for exclude_path in "${EXCLUDE_PATHS[@]}"
do
  echo "${exclude_path}" >>"${exclude_list}"
done
echo "${MOVE_DIRECTORY}/" >>"${exclude_list}"

# 定時実行のスケジュールを作成する
schedule_file="/etc/cron.d/easyclamav"
echo "${SCAN_SCHEDULE} root '${EASYCLAMAV_HOME}/easyclamav'" \
    >"${schedule_file}" || exit ${?}

echo "Completed."
exit 0
```

## 定時実行スクリプト
実際のウィルススキャンと感染ファイルの移動を行うスクリプトです。

処理内容：

1. ClamAVのパッケージを更新する（失敗しても続行）
2. ウィルスデータベースを更新する（失敗しても続行）
3. スキャン除外パスのリストを展開して、clamscanコマンドに指定するオプションを生成する
4. 指定のパス以下をスキャンする
5. 感染ファイルが見つかったら指定のディレクトリに移動する
6. タグ名＝`'easyclamav'`でsyslogに結果を出力する

```Bash:easyclamav
#!/bin/bash

readonly HOME_DIR=$(dirname "${0}") # このスクリプトのディレクトリ
readonly LOG_TAG=$(basename "${0}") # このスクリプトの名前
info_log=$(mktemp)
err_log=$(mktemp)

source "${HOME_DIR}/easyclamav.conf"

# ClamAVパッケージを更新する
/bin/logger -p cron.info -t ${LOG_TAG} "Updating clamav packages."
if yum -y --enablerepo=epel update clamav* 1>"${info_log}" 2>"${err_log}"; then
  /bin/logger -p cron.info -t ${LOG_TAG} -f "${info_log}"
  /bin/logger -p cron.info -t ${LOG_TAG} "Finished updating clamav packages."
else
  /bin/logger -p cron.err -t ${LOG_TAG} -f "${err_log}"
  /bin/logger -p cron.err -t ${LOG_TAG} "Failed to update clamav packages."
fi

# ウィルスデータベースを更新する
/bin/logger -p cron.info -t ${LOG_TAG} "Updating virus database."
if /bin/freshclam 1>"${info_log}" 2>"${err_log}"; then
  /bin/logger -p cron.info -t ${LOG_TAG} -f "${info_log}"
  /bin/logger -p cron.info -t ${LOG_TAG} "Finished updating virus database."
else
  /bin/logger -p cron.err -t ${LOG_TAG} -f "${err_log}"
  /bin/logger -p cron.err -t ${LOG_TAG} "Failed to update virus database."
fi

# スキャン除外リストを展開する
exclude_list="${HOME_DIR}/exclude_list"
if [[ -e "${exclude_list}" ]]; then
  exclude_opt=$(grep   "^/.*/$"    "${exclude_list}" | sed -e 's/^\(.*\)$/ --exclude-dir=\1/g' | tr -d "\n")
  exclude_opt+=$(grep  "^/.*[^/]$" "${exclude_list}" | sed -e 's/^\(.*\)$/ --exclude=\1/g'     | tr -d "\n")
  /bin/logger -p cron.info -t ${LOG_TAG} "exclude_opt = '${exclude_opt}'"
else
  /bin/logger -p cron.err -t ${LOG_TAG} "'${exclude_list}' not found."
fi

# スキャン実行
mkdir -p "${MOVE_DIRECTORY}"
/bin/logger -p cron.info -t ${LOG_TAG} "Scanning virus."
/bin/clamscan \
    --max-filesize=${MAX_FILESIZE} \
    --max-scansize=${MAX_SCANSIZE} \
    --move="${MOVE_DIRECTORY}" --infected --recursive ${exclude_opt} \
    "${SCAN_DIRECTORY}" 1>"${info_log}" 2>"${err_log}"
return_code=${?}

# 標準出力をログに記録
/bin/logger -p cron.info -t ${LOG_TAG} -f "${info_log}"

if ((return_code == 0)); then
  # 正常終了時のログ
  /bin/logger -p cron.info  -t ${LOG_TAG} "Completed, no virus found."
elif ((return_code == 1)); then
  # ウィルス検出時のログ
  /bin/logger -p cron.err   -t ${LOG_TAG} -f "${err_log}"
  /bin/logger -p cron.err   -t ${LOG_TAG} "Viruses found."
else
  # スキャン失敗時のログ
  /bin/logger -p cron.err   -t ${LOG_TAG} -f "${err_log}"
  /bin/logger -p cron.err   -t ${LOG_TAG} "Return code: ${return_code}"
  /bin/logger -p cron.err   -t ${LOG_TAG} "Failed to scan."
fi

exit 0
```

# スキャン結果の確認
以下のコマンドでログを確認できます。
`journalctl -e -t easyclamav`

感染ファイルが検出された場合は、`$MOVE_DIRECTORY`で設定されたディレクトリ（デフォルト：`/var/tmp/infected_files`）に移動されます。

# あとがき
御覧のとおり技術的に高度なことは何もないのですが、実際にやってみると面倒で手間がかかる作業だったので自動化せざるを得ませんでした。

参考: [Clam AntiVirus User Manual](https://www.clamav.net/documents/)