inotifywait based virus scanning

few months ago I’ve built my NAS and configured it so that my wife can store her files on it, too. Due to the fact that she uses Windows I thought it might be reasonable to scan files on the NAS for viruses. My first approach which I called „hashclamscan“ just searched all files, created checksums and scanned modified files. Here’s an inotifywait based solution, which uses clamscan (and if available sophos savscan) and makes use of CDB for the hashes…


inotify (inode notify) just reports changes to inodes to an application. inotifywait (part of inotify-tools) can be used for exactly this. A simple example looks like this:

while read file; do
  if [ -f "$file" ]; then
    do_something "$file"
done< <(inotifywait -mrq -e close_write --format %w%f /storage)

everytime a file is opened for writes and closed (that does not mean the file has been modified) do_something „$file“ would be called. The events I am interested in are:

event description
modify a file was changed
close_write a file was opened for writes and closed
move a file was moved
create a file was created

You should be aware of, that in some conditions inotifywait might miss modifications (check the manpage).

By default (at least in debian) the inotify watches limit is set to 8192 per user. You’ll for sure have more files than that. Hence this limit needs to be increased:

root@janice:~# find /storage/ -type f | wc -l

I’d just go with:

num=$(find /storage -type f | wc -l)
echo $limit > /proc/sys/fs/inotify/max_user_watches

Obviously you should restart the script every now and then if adding lots of files or directories. Or just define a high hardcoded limit.

constant database

I’ve always wanted to play around with cdb because it allows fast lookups. Primarily I’ve been interested into it because I wanted to use it for Lookups in Postfix. However, a long file containing file:hash pairs will become slow quite fast. So CDB might help me here, as well. For that purpose I did write the following methods for my bash script:

cdb_exists() {
  local key="$1"
  if cdbget $key < $CDB &>/dev/null; then
    return 0
  return 1
cdb_get() {
  local key="$1"
  cdbget $key < $CDB
  return $?
cdb_delete() {
  local key="$1"
  dump=$(cdbdump < $CDB | grep -v "$key")
  printf "$dump\n\n" | cdbmake $CDB $CDB.tmp
  return $?
cdb_put() {
  local key="$1"
  local keylen=${#key}
  local value="$2"
  local valuelen=${#value}
  local f="+$keylen,$valuelen:$key->$value"
  if cdb_exists "$key"; then
    cdb_delete "$key"
  dump=$(cdbdump < $CDB)
  printf "$f\n$dump\n\n" | cdbmake $CDB $CDB.tmp
  return $?

This simplifies the usage of CDB in my bash script.


clamscan is a simple tool which can be used to check files for viruses. clamscan is a bit slow and might not be aware of the newest viruses around. However, it is pretty useful and a cheap alternative to the commercial scanners.

# clam scan options, careful: if a parameter is wrong, you won't notice :)
CLAMOPTS="--no-summary --max-filesize=100M --max-scansize=1024M"
scan() {
  local file="$1"
  ret=$(clamscan $CLAMOPTS "$file" 2>/dev/null)
  if [[ $? == 1 ]]; then
    echo "found virus: $ret"
check() {
  local file="$1"
  currsum=$(sha1sum "$file" | awk '{ print $1 }')
  if cdb_exists "$file"; then
    oldsum=$(cdb_get "$file")
    if [ "$currsum" != "$oldsum" ]; then
      cdb_put "$file" "$currsum"
      scan "$file"
    cdb_put "$file" "$currsum"
    scan "$file"

Basically I am checking (in check()) if a given file does exist in the cdb database, if it does I am checking if the checksum matches. If it does not – I am adding a new checksum for the file and I am scanning the file. If there is no checksum I am just adding a checksum (and the file is scanned). The scan() method just calls clamscan and echo’s the result.


Sophos offers a free-linux version. I’ve just had to enter my name and mail address and accept the license terms to download it (Do not understand this as suggestion to use it or something like that. I was just searching for an addition to clamscan and found it with savscan). So, let’s ty it together in scan()

# careful: if a parameter is wrong, you won't notice :)
CLAMOPTS="--no-summary --max-filesize=100M --max-scansize=1024M"
SAVOPTS="-nb -f -all -ss -sc -archive --no-reset-atime"
scan() {
  local file="$1"
  local msg=""
  # assuming that clamscan is in $PATH
  if [ -x `which clamscan` ]; then
    ret=$(clamscan $CLAMOPTS "$file" 2>/dev/null)
    if [[ $? == 1 ]]; then
      msg=$msg"found virus (clamav): $ret\n"
  # assuming that savscan is in $PATH
  if [ -x `which savscan` ]; then
    ret=$(savscan $SAVOPTS "$file" 2>&1 | grep ">>")
    if [[ $? == 0 ]]; then
      msg=$msg"found virus (sophos): $ret\n"
  if [ ! -z "$msg" ]; then
    mailNotification "$msg"

If one or both detect a virus, the mailNotification method is called.

main processing loop

the main processing loop just calls check if a file was created, modified or moved. I’ve been thinking about whether I should implement a small delay of say 1 up to 2 seconds for short-living temporary files which might be gone when the virus scanner is reached, but thats why I do use 2>/dev/null above.

while read file; do
  if [ -f "$file" ] && [ -s "$file" ]; then
    check "$file"
done< <(inotifywait -mrq -e create,modify,move,close_write --format %w%f /storage/netboot/kodi)

Finally I am just adding the mailNotification method:

mailNotification() {
  local mailto="$ADMIN"
  local subject="$SUBJECT"
  local msg="$1"
  printf "$msg" | mail -s "$subject" "$mailto"

Now I just added a file containing the EICAR-testfile and got the following mail (just removed the > < stuff..):

From root@janice Sun Dec 20 23:48:15 2015
Return-path: root@janice
Envelope-to: root@janice
Delivery-date: Sun, 20 Dec 2015 23:48:15 +0100
Received: from root by janice with local (Exim 4.84)
        (envelope-from root@janice)
        id 1aAmlz-0000ZC-4H
        for root@janice; Sun, 20 Dec 2015 23:48:15 +0100
To: root@janice
Subject: inotifyscan detected a virus
Message-Id: E1aAmlz-0000ZC-4H@janice
From: root root@janice
Date: Sun, 20 Dec 2015 23:48:15 +0100
found virus (clamav): /storage/netboot/kodi/eicar-test: Eicar-Test-Signature FOUND
found virus (sophos): >>> Virus 'EICAR-AV-Test' found in file /storage/netboot/kodi/eicar-test

You can grab the full script here if you want to play around with it:

By the way. If you want to check if everything works as expected, you can either create an EICAR test file and check the mails or you can take a look at the CDB database, like so:

root@janice:~# cdbdump < /var/cdb/inotifyscan.cdb 

files with hashes around? good 🙂

No Comments

Post a Comment