How To use ClamAV to monitor a ?NIX Server for malware

IT_Architect

Verified User
Joined
Feb 27, 2006
Messages
1,114
2013-03-13 - Version 1.20 - Important update! - See change log below

Motivation: A couple of users who had simple passwords that had their sites hosed by a bots.

Research: I searched for and spent a lot of time working with scripts that are out there, but they either didn't catch the malware, or did work, but the code was not well thought out, inefficient, or not flexible. While testing a simple script I found at digitalsanctuary, I gained a lot of respect for ClamAV's excellent detection capabilities, not only to find PC e-mail viruses, but also web page exploits. At that point, I decided to use ClamAV's clamscan as the basis for these scripts. Another result of my research was that none of the ?NIX versions, unlike Windows, has an efficient hook to determine when files have been created or changed. Neither iNotify nor kqueue have the potential to form the basis of an efficient filesystem monitor, even though there are some products based on them. That leaves us with crontab to do scheduled scans.

Goals: To provide a framework to maintain ?NIX server’s free of malware, that is efficient, flexible, and easily extensible to leverage the many capabilities of ClamAV’s clamscan. You can see the many useful options available here: http://linux.die.net/man/1/clamscan

Requirements:
- ?NIX
- bash shell
- ClamAV, which includes the requisite clamscan utility

The Scripts: The following three files is supplied.
clamscan_full.sh - Scans the entire system except areas you identify NOT TO scan.
clamscan_delta2.sh - As with clamscan_full.sh, this script scans the entire system except areas you identify NOT TO scan. The key difference is it only scans files that have changed within the last <user defined> minutes. Hence, the delta name. Thus, you could run this script every hour, and set it to look back <user defined> minutes + a few minutes, to scan only the files that have been recently created or changed.
clamscan_delta.sh - As with delta2, it only scans files that have changed within the last <user defined> minutes. Unlike delta2, this script requires you to identify where TO scan. This has a few advantages. It can be easier to limit where you want to scan. Because of that, it can often be much faster, and have less server impact. On large servers, delta2 may impact your server for longer than you want. The reason for the delta naming order is because I tend to use the delta version because it is much quicker, and has less server impact, which is important during normal business hours.

To Deploy
1. Copy scripts to /etc or other suitable location
2. Flag scripts as executable
3. Make sure the bash is available at the link shown on the top of the script. If it's not, create a symlink to its location.
E.G. ln -s /usr/local/bin/bash /bin/bash
Alternatively change the scripts to point to the location of bash.
4. Edit the well-documented parameters in the User Settings area
5. Schedule the files to run in crontab.
- Linux Example:
##### Anti-malware processes
# Scan server with ClamAV 30 minutes after the hour
30 * * * * /etc/clamscan_delta.sh
# Scan server with ClamAV each day
0 2 * * * /etc/clamscan_full.sh
- FreeBSD Example:
##### Anti-malware processes
# Scan server with ClamAV 30 minutes after the hour
30 * * * * root /etc/clamscan_delta.sh
# Scan server with ClamAV each day
0 2 * * * root /etc/clamscan_full.sh

RFC: This is also an RFC. My goal is to have these scripts work in any ?NIX environment. I wouldn't call myself a shell guru, nor have I worked with Linux since 2007, so I don't know if these scripts will work in those environments. I would appreciate comments of a shell script guru to help make these scripts work across all of ?NIX platforms. The reason I have Author in the script is because you're supposed to, and so people know who to ask if they absolutely have to, but not make it too easy.
 
Last edited:
clamscan_delta.sh
Code:
#!/bin/bash

# ****************************************************************************
#
#							clamscan_delta.sh
#
#
# ****************************************************************************
# clamscan_delta.sh - Scans the paths named in SCAN_PATHS for changed files
# Version 1.20 2013-03-13
# Purpose: Scan for malware and send an e-mail alert if detected
# Author: IT_Architect http://forum.directadmin.com
# Requirements: ?NIX computer, bash shell, and ClamAV
#
# ****************************************************************************
#							User Settings
# ****************************************************************************
### Scan Type Name: Prints in log, and forms base for log tmp file names.
SCAN_TYPE="delta"

### E-mail settings
EMAIL_SUBJECT="VIRUS DETECTED ON `hostname`!!!"
EMAIL_TO="[email protected]"
EMAIL_FROM="[email protected]"
LOG_TAIL_SIZE=50; # Number of lines from tail of scan log to email in alerts 

### Log File Path
LOG_DIR="/var/log/clamav"

# Rotates log at size stated, but maintains only one previous.  If you use the
# quarantine capabilities, the log file is the only source of information that
# contains where the files in the quarantine came from.
MAX_LOG_SIZE_KB=1024

# Character string used to separate scan instances in log file. 
LOG_SEPARATOR="printf '*%.0s' {1..78};echo;"

# Directory for scratch files - default="$LOG_DIR/tmp"
SCRATCH_DIR="$LOG_DIR/tmp"

# Path to clamscan - default="/usr/local/bin/clamscan"
CLAMSCAN_PATH="/usr/local/bin/clamscan"

### Scan Parameters
# Check only files that have changed in the last X minutes
# Set to match scan frequency plus a minute or more.
# Example: If you scan every hour, LOOK_BACK_MINUTES=61
LOOK_BACK_MINUTES=61

# Valid quarantine path required for SCAN_ACTIONs --copy or --move
# Will create if not present default="$LOG_DIR/quarantine"
QUARANTINE_DIR="$LOG_DIR/quarantine"

# Scan Action: Default "" = no action.  Other actions are:
# "--remove=yes" or "--move=$QUARANTINE_DIR" or "--copy=$QUARANTINE_DIR"
SCAN_ACTION=""

# Scan Paths
# Ensure QUARANTINE_DIR is not in path if SCAN_ACTION is --copy or --move
# Cannot specify directories with spaces in name but will scan them in path
SCAN_PATHS=( \
/home/*/domains/*/private_html \
/home/*/domains/*/public_html \
/home/*/domains/*/private_ftp \
/home/*/domains/*/public_ftp \
/home/*/Maildir \
/home/*/imap/*/*/Maildir \
/var/tmp \
/tmp \
)

# ****************************************************************************
# 					MAKE NO CHANGES BELOW THIS LINE
# ****************************************************************************
# ****************************************************************************
# 								Functions
# ****************************************************************************
# ******************
#Function find_files
# ******************
find_files () {

# Load files to be scanned to file
  for (( i = 0 ; i < ${#SCAN_PATHS[@]} ; i++ )) do
    eval find ${SCAN_PATHS[$i]} -type f -mmin -$LOOK_BACK_MINUTES >> $FILES_UNSORTED
    eval find ${SCAN_PATHS[$i]} -type f -cmin -$LOOK_BACK_MINUTES >> $FILES_UNSORTED
  done
  cat $FILES_UNSORTED | sort -u > $FILES_SORTED

}

# ******************
#Function check_scan
# ******************
check_scan () {
 
    # Check the last set of results. If there are any "Infected" counts that
    # aren't zero, e-mail an alert.
    if [ `tail -n 12 ${LOG}  | grep Infected | grep -v 0 | wc -l` != 0 ]
    then
        echo "To: ${EMAIL_TO}" >>  ${EMAIL_MESSAGE}
        echo "From: ${EMAIL_FROM}" >>  ${EMAIL_MESSAGE}
        echo "Subject: ${EMAIL_SUBJECT}" >>  ${EMAIL_MESSAGE}
        echo "Importance: High" >> ${EMAIL_MESSAGE}
        echo "X-Priority: 1" >> ${EMAIL_MESSAGE}
        echo "*** SEE ${LOG} FOR MALWARE DETAILS ***" >> ${EMAIL_MESSAGE}
        echo "*** Last $LOG_TAIL_SIZE lines from log file ***" >> ${EMAIL_MESSAGE}
        echo >> ${EMAIL_MESSAGE}
		echo "`tail -n $LOG_TAIL_SIZE ${LOG}`" >> ${EMAIL_MESSAGE}
        eval sendmail -t < ${EMAIL_MESSAGE}
    fi
}

# ******************
#Function initialize
# ******************
initialize () {

# *** Sanity checks
# Make LOG directory specified if necessary
if [[ ! -z $LOG_DIR ]]; then  # if not unset or empty
  if [ ! -d "$LOG_DIR" ]; then
    mkdir -p ${LOG_DIR}
    if [ ! -d "$LOG_DIR" ]; then
      echo "Fatal Error: LOG_DIR path $LOG_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: LOG_DIR path not set"
  exit
fi

# Check for SCRATCH_DIR directory and make if necessary
if [[ ! -z $SCRATCH_DIR ]]; then  # if not unset or empty
  if [ ! -d "$SCRATCH_DIR" ]; then
    mkdir -p ${SCRATCH_DIR}
    if [ ! -d "$SCRATCH_DIR" ]; then
      echo "Fatal Error: SCRATCH_DIR path $SCRATCH_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: SCRATCH_DIR path not set"
  exit
fi

# Make QUARANTINE_DIR specified if necessary
if [[ -z $QUARANTINE_DIR ]] && \
[[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
$SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then
  echo "Fatal Error: QUARANTINE_DIR not set as required by SCAN_ACTION"
  exit
else  # QUARANTINE_DIR not empty or not required by SCAN_ACTION
  if [[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
  $SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then  # If you need the path
    if [ ! -d "$QUARANTINE_DIR" ]; then
      mkdir -p ${QUARANTINE_DIR}
      if [ ! -d "$QUARANTINE_DIR" ]; then
        echo "Fatal Error: QUARANTINE_DIR $QUARANTINE_DIR invalid or could not be created"
        exit;
      fi
    fi
  fi
  # QUARANTINE_DIR not required
fi

### Initialize files.
  FILE_PREFIX=$(echo $SCAN_TYPE | sed -e 's/ /_/g')
  LOG="$LOG_DIR/$FILE_PREFIX""_scan.log"
  FILES_UNSORTED="$SCRATCH_DIR/$FILE_PREFIX""_unsorted.tmp"
  FILES_SORTED="$SCRATCH_DIR/$FILE_PREFIX""_sorted.tmp"
  EMAIL_MESSAGE="$SCRATCH_DIR/$FILE_PREFIX""_email.tmp"
  cat /dev/null > $FILES_UNSORTED
  cat /dev/null > $FILES_SORTED
  cat /dev/null > $EMAIL_MESSAGE

# *** Rotate log if necessary (Maintains only one previous revision)
if [ -f "$LOG" ]; then
  LOG_SIZE_KB=`du -k $LOG | cut -f1`
  if [[ "${LOG_SIZE_KB:-0}" -ge "${MAX_LOG_SIZE_KB:-0}" ]]
  then
    mv $LOG ${LOG}.old
  fi
fi
}

# ****************************************************************************
# 									Main
# ****************************************************************************
START_TIME="$(date +%s)"

# *** Initialize - Sanity checks, file setup, file maintenance
initialize

# *** Write header
eval $LOG_SEPARATOR >> $LOG
echo Start Time: Date $(date) >> $LOG
echo Scan Type: $SCAN_TYPE >> $LOG

# *** Find files and scan.
find_files
$CLAMSCAN_PATH --quiet --infected --log=${LOG} ${SCAN_ACTION} --file-list=${FILES_SORTED}

# *** Write trailer to log
echo Finish Time: Date $(date) >> $LOG
ET="$(($(date +%s)-START_TIME))"
printf "Elapsed Time: %d hrs %02d mins %02d secs\n" "$((ET/3600%24))" "$((ET/60%60))" "$((ET%60))" >> $LOG
eval $LOG_SEPARATOR >> $LOG

# *** Check log for malware detectiions and Send e-mail alert if found
check_scan
clamscan_delta2.sh
Code:
#!/bin/bash

# ****************************************************************************
#
#							clamscan_delta2.sh
#
#
# ****************************************************************************
# clamscan_delta2.sh - Scans everything except the paths in NO_SCAN_PATHS
# Version 1.20 2013-03-13
# Purpose: Scan for malware and send an e-mail alert if detected
# Author: IT_Architect http://forum.directadmin.com
# Requirements: ?NIX computer, bash shell, and ClamAV
#
# ****************************************************************************
#							User Settings
# ****************************************************************************
### Scan Type Name: Prints in log, and forms base for log tmp file names.
SCAN_TYPE="delta2"

### E-mail settings
EMAIL_SUBJECT="VIRUS DETECTED ON `hostname`!!!"
EMAIL_TO="[email protected]"
EMAIL_FROM="[email protected]"
LOG_TAIL_SIZE=50; # Number of lines from tail of scan log to email in alerts 

### Log File Path
LOG_DIR="/var/log/clamav"

# Rotates log at size stated, but maintains only one previous.  If you use the
# quarantine capabilities, the log file is the only source of information that
# contains where the files in the quarantine came from.
MAX_LOG_SIZE_KB=1024

# Character string used to separate scan instances in log file. 
LOG_SEPARATOR="printf '*%.0s' {1..78};echo;"

# Directory for scratch files - default="$LOG_DIR/tmp"
SCRATCH_DIR="$LOG_DIR/tmp"

# Path to clamscan - default="/usr/local/bin/clamscan"
CLAMSCAN_PATH="/usr/local/bin/clamscan"

### Scan Parameters
# Check only files that have changed in the last X minutes
# Set to match scan frequency plus a minute or more.
# Example: If you scan every hour, LOOK_BACK_MINUTES=61
LOOK_BACK_MINUTES=61

# Valid quarantine path required for SCAN_ACTIONs --copy or --move
# Will create if not present default="$LOG_DIR/quarantine"
QUARANTINE_DIR="$LOG_DIR/quarantine"

# Scan Action: Default "" = no action.  Other actions are:
# "--remove=yes" or "--move=$QUARANTINE_DIR" or "--copy=$QUARANTINE_DIR"
SCAN_ACTION=""

# Directory level at which to start scanning
SCAN_ROOT="/"

# Do-Not-Scan Paths
# Cannot specify directories with spaces in name here but does scan them
# Ensure QUARANTINE_DIR is not in path if SCAN_ACTION is --copy or --move
# Default="-not -path '/sys/*' -and -not -path '/proc/*' \
#-and -not -path '${QUARANTINE_DIR}/*'"
NO_SCAN_PATHS="-not -path '/sys/*' -and -not -path '/proc/*' -and \
-not -path '${QUARANTINE_DIR}/*'"


# ****************************************************************************
# 					MAKE NO CHANGES BELOW THIS LINE
# ****************************************************************************
# ****************************************************************************
# 								Functions
# ****************************************************************************
# ******************
#Function find_files
# ******************
find_files () {

  # Load files to be scanned to file
  eval find ${SCAN_ROOT} ${NO_SCAN_PATHS} -mmin -$LOOK_BACK_MINUTES -type f >> $FILES_UNSORTED
  eval find ${SCAN_ROOT} ${NO_SCAN_PATHS} -cmin -$LOOK_BACK_MINUTES -type f >> $FILES_UNSORTED
  cat $FILES_UNSORTED | sort -u > $FILES_SORTED

}

# ******************
#Function check_scan
# ******************
check_scan () {
 
    # Check the last set of results. If there are any "Infected" counts that
    # aren't zero, e-mail an alert.
    if [ `tail -n 12 ${LOG}  | grep Infected | grep -v 0 | wc -l` != 0 ]
    then
        echo "To: ${EMAIL_TO}" >>  ${EMAIL_MESSAGE}
        echo "From: ${EMAIL_FROM}" >>  ${EMAIL_MESSAGE}
        echo "Subject: ${EMAIL_SUBJECT}" >>  ${EMAIL_MESSAGE}
        echo "Importance: High" >> ${EMAIL_MESSAGE}
        echo "X-Priority: 1" >> ${EMAIL_MESSAGE}
        echo "*** SEE ${LOG} FOR MALWARE DETAILS ***" >> ${EMAIL_MESSAGE}
        echo "*** Last $LOG_TAIL_SIZE lines from log file ***" >> ${EMAIL_MESSAGE}
        echo >> ${EMAIL_MESSAGE}
		echo "`tail -n $LOG_TAIL_SIZE ${LOG}`" >> ${EMAIL_MESSAGE}
        sendmail -t < ${EMAIL_MESSAGE}
    fi
}

# ******************
#Function initialize
# ******************
initialize () {

# *** Sanity checks
# Make LOG directory specified if necessary
if [[ ! -z $LOG_DIR ]]; then  # if not unset or empty
  if [ ! -d "$LOG_DIR" ]; then
    mkdir -p ${LOG_DIR}
    if [ ! -d "$LOG_DIR" ]; then
      echo "Fatal Error: LOG_DIR path $LOG_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: LOG_DIR path not set"
  exit
fi

# Check for SCRATCH_DIR directory and make if necessary
if [[ ! -z $SCRATCH_DIR ]]; then  # if not unset or empty
  if [ ! -d "$SCRATCH_DIR" ]; then
    mkdir -p ${SCRATCH_DIR}
    if [ ! -d "$SCRATCH_DIR" ]; then
      echo "Fatal Error: SCRATCH_DIR path $SCRATCH_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: SCRATCH_DIR path not set"
  exit
fi

# Make QUARANTINE_DIR specified if necessary
if [[ -z $QUARANTINE_DIR ]] && \
[[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
$SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then
  echo "Fatal Error: QUARANTINE_DIR not set as required by SCAN_ACTION"
  exit
else  # QUARANTINE_DIR not empty or not required by SCAN_ACTION
  if [[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
  $SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then  # If you need the path
    if [ ! -d "$QUARANTINE_DIR" ]; then
      mkdir -p ${QUARANTINE_DIR}
      if [ ! -d "$QUARANTINE_DIR" ]; then
        echo "Fatal Error: QUARANTINE_DIR $QUARANTINE_DIR invalid or could not be created"
        exit;
      fi
    fi
  fi
  # QUARANTINE_DIR not required
fi

### Initialize files.
  FILE_PREFIX=$(echo $SCAN_TYPE | sed -e 's/ /_/g')
  LOG="$LOG_DIR/$FILE_PREFIX""_scan.log"
  FILES_UNSORTED="$SCRATCH_DIR/$FILE_PREFIX""_unsorted.tmp"
  FILES_SORTED="$SCRATCH_DIR/$FILE_PREFIX""_sorted.tmp"
  EMAIL_MESSAGE="$SCRATCH_DIR/$FILE_PREFIX""_email.tmp"
  cat /dev/null > $FILES_UNSORTED
  cat /dev/null > $FILES_SORTED
  cat /dev/null > $EMAIL_MESSAGE

# *** Rotate log if necessary (Maintains only one previous revision)
if [ -f "$LOG" ]; then
  LOG_SIZE_KB=`du -k $LOG | cut -f1`
  if [[ "${LOG_SIZE_KB:-0}" -ge "${MAX_LOG_SIZE_KB:-0}" ]]
  then
    mv $LOG ${LOG}.old
  fi
fi
}

# ****************************************************************************
# 									Main
# ****************************************************************************
START_TIME="$(date +%s)"

# *** Initialize - Sanity checks, file setup, file maintenance
initialize

# *** Write header
eval $LOG_SEPARATOR >> $LOG
echo Start Time: Date $(date) >> $LOG
echo Scan Type: $SCAN_TYPE >> $LOG

# *** Find files and scan.
find_files
$CLAMSCAN_PATH --quiet --infected --log=${LOG} ${SCAN_ACTION} --file-list=${FILES_SORTED}

# *** Write trailer to log
echo Finish Time: Date $(date) >> $LOG
ET="$(($(date +%s)-START_TIME))"
printf "Elapsed Time: %d hrs %02d mins %02d secs\n" "$((ET/3600%24))" "$((ET/60%60))" "$((ET%60))" >> $LOG
eval $LOG_SEPARATOR >> $LOG

# *** Check log for malware detectiions and Send e-mail alert if found
check_scan
clamscan_full.sh
Code:
#!/bin/bash

# ****************************************************************************
#
#							clamscan_full.sh
#
#
# ****************************************************************************
# Performs full scans of files indicated by SCAN_ROOT and NO_SCAN_PATHS
# Version 1.20 2013-03-13
# Purpose: Scan for malware and send an e-mail alert if detected
# Author: IT_Architect http://forum.directadmin.com
# Requirements: ?NIX computer, bash shell, and ClamAV
#
# ****************************************************************************
#							User Settings
# ****************************************************************************
### Scan Type Name: Prints in log, and forms base for log tmp file names.
SCAN_TYPE="full"

### E-mail settings
EMAIL_SUBJECT="VIRUS DETECTED ON `hostname`!!!"
EMAIL_TO="[email protected]"
EMAIL_FROM="[email protected]"
LOG_TAIL_SIZE=50; # Number of lines from tail of scan log to email in alerts 

### Log File Path
LOG_DIR="/var/log/clamav"

# Rotates log at size stated, but maintains only one previous.  If you use the
# quarantine capabilities, the log file is the only source of information that
# contains where the files in the quarantine came from.
MAX_LOG_SIZE_KB=1024

# Character string used to separate scan instances in log file. 
LOG_SEPARATOR="printf '*%.0s' {1..78};echo;"

# Directory for scratch files - default="$LOG_DIR/tmp"
SCRATCH_DIR="$LOG_DIR/tmp"

# Path to clamscan - default="/usr/local/bin/clamscan"
CLAMSCAN_PATH="/usr/local/bin/clamscan"

### Scan Parameters
# Check only files that have changed in the last X minutes
# Set to match scan frequency plus a minute or more.
# Example: If you scan every hour, LOOK_BACK_MINUTES=61
LOOK_BACK_MINUTES=61

# Valid quarantine path required for SCAN_ACTIONs --copy or --move
# Will create if not present default="$LOG_DIR/quarantine"
QUARANTINE_DIR="$LOG_DIR/quarantine"

# Scan Action: Default "" = no action.  Other actions are:
# "--remove=yes" or "--move=$QUARANTINE_DIR" or "--copy=$QUARANTINE_DIR"
SCAN_ACTION=""

# Directory level at which to start scanning
SCAN_ROOT="/"

# Do-Not-Scan Paths
# Cannot specify directories with spaces in name here but does scan them
# Ensure QUARANTINE_PATH is not in path if SCAN_ACTION is --copy or --move
# Example: NO_SCAN_PATHS="--exclude-dir=/sys/ --exclude-dir=/proc/ \
#--exclude-dir=${QUARANTINE_PATH}/"
NO_SCAN_PATHS="--exclude-dir=/sys/ --exclude-dir=/proc/ \
--exclude-dir=${QUARANTINE_DIR}/"

# ****************************************************************************
# 					MAKE NO CHANGES BELOW THIS LINE
# ****************************************************************************
# ****************************************************************************
# 								Functions
# ****************************************************************************
# ******************
#Function check_scan
# ******************
check_scan () {
 
    # Check the last set of results. If there are any "Infected" counts that
    # aren't zero, e-mail an alert.
    if [ `tail -n 12 ${LOG}  | grep Infected | grep -v 0 | wc -l` != 0 ]
    then
        echo "To: ${EMAIL_TO}" >>  ${EMAIL_MESSAGE}
        echo "From: ${EMAIL_FROM}" >>  ${EMAIL_MESSAGE}
        echo "Subject: ${EMAIL_SUBJECT}" >>  ${EMAIL_MESSAGE}
        echo "Importance: High" >> ${EMAIL_MESSAGE}
        echo "X-Priority: 1" >> ${EMAIL_MESSAGE}
        echo "*** SEE ${LOG} FOR MALWARE DETAILS ***" >> ${EMAIL_MESSAGE}
        echo "*** Last $LOG_TAIL_SIZE lines from log file ***" >> ${EMAIL_MESSAGE}
        echo >> ${EMAIL_MESSAGE}
		echo "`tail -n $LOG_TAIL_SIZE ${LOG}`" >> ${EMAIL_MESSAGE}
        sendmail -t < ${EMAIL_MESSAGE}
    fi
}

# ******************
#Function initialize
# ******************
initialize () {

# *** Sanity checks
# Make LOG directory specified if necessary
if [[ ! -z $LOG_DIR ]]; then  # if not unset or empty
  if [ ! -d "$LOG_DIR" ]; then
    mkdir -p ${LOG_DIR}
    if [ ! -d "$LOG_DIR" ]; then
      echo "Fatal Error: LOG_DIR path $LOG_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: LOG_DIR path not set"
  exit
fi

# Check for SCRATCH_DIR directory and make if necessary
if [[ ! -z $SCRATCH_DIR ]]; then  # if not unset or empty
  if [ ! -d "$SCRATCH_DIR" ]; then
    mkdir -p ${SCRATCH_DIR}
    if [ ! -d "$SCRATCH_DIR" ]; then
      echo "Fatal Error: SCRATCH_DIR path $SCRATCH_DIR invalid or could not be created"
      exit
    fi
  fi
else
  echo "Fatal Error: SCRATCH_DIR path not set"
  exit
fi

# Make QUARANTINE_DIR specified if necessary
if [[ -z $QUARANTINE_DIR ]] && \
[[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
$SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then
  echo "Fatal Error: QUARANTINE_DIR not set as required by SCAN_ACTION"
  exit
else  # QUARANTINE_DIR not empty or not required by SCAN_ACTION
  if [[ $SCAN_ACTION == "--copy=$QUARANTINE_DIR" || \
  $SCAN_ACTION = "--move=$QUARANTINE_DIR" ]]; then  # If you need the path
    if [ ! -d "$QUARANTINE_DIR" ]; then
      mkdir -p ${QUARANTINE_DIR}
      if [ ! -d "$QUARANTINE_DIR" ]; then
        echo "Fatal Error: QUARANTINE_DIR $QUARANTINE_DIR invalid or could not be created"
        exit;
      fi
    fi
  fi
  # QUARANTINE_DIR not required
fi

### Initialize files.
  FILE_PREFIX=$(echo $SCAN_TYPE | sed -e 's/ /_/g')
  LOG="$LOG_DIR/$FILE_PREFIX""_scan.log"
  EMAIL_MESSAGE="$SCRATCH_DIR/$FILE_PREFIX""_email.tmp"
  cat /dev/null > $EMAIL_MESSAGE

# *** Rotate log if necessary (Maintains only one previous revision)
if [ -f "$LOG" ]; then
  LOG_SIZE_KB=`du -k $LOG | cut -f1`
  if [[ "${LOG_SIZE_KB:-0}" -ge "${MAX_LOG_SIZE_KB:-0}" ]]
  then
    mv $LOG ${LOG}.old
  fi
fi
}

# ****************************************************************************
# 									Main
# ****************************************************************************
START_TIME="$(date +%s)"

# *** Initialize - Sanity checks, file setup, file maintenance
initialize

# *** Write header
eval $LOG_SEPARATOR >> $LOG
echo Start Time: Date $(date) >> $LOG
echo Scan Type: $SCAN_TYPE >> $LOG

# *** Find files and scan.
eval $CLAMSCAN_PATH -r ${SCAN_ROOT} ${NO_SCAN_PATHS} --quiet --infected --log=${LOG}

# *** Write trailer to log
echo Finish Time: Date $(date) >> $LOG
ET="$(($(date +%s)-START_TIME))"
printf "Elapsed Time: %d hrs %02d mins %02d secs\n" "$((ET/3600%24))" "$((ET/60%60))" "$((ET%60))" >> $LOG
eval $LOG_SEPARATOR >> $LOG

# *** Check log for malware detectiions and Send e-mail alert if found
check_scan
 
Last edited:
Testing tips:
2013-03-09 Create a directory path with a few subdirectories under it that are at least a two deep. For testing clamscan_delta.sh, simply set the SCAN_PATHS to wherever you want in your newly created directory structure. For clamscan_delta2.sh and clamscan_full.sh use the SCAN_ROOT parameter to do the same. Now all you need is a virus. The easiest place to get that is http://en.wikipedia.org/wiki/EICAR_test_file and copy it into a text file in one or more of your test directories. Now you have an environment that will prove it is working, and allows you to play with the parameters to learn their effects and build confidence in the efficacy of the ClamAV and the scripts. The user-settable parameters are well explained in the script. You can learn about many
2013-03-11 Even if you decide not to use the quarantine, set up the path and make the quarantine directory anyway. It gives you the perfect place to store viruses you want to use for testing since that directory doesn't get scanned.

FAQs
What do I need to deploy these scripts?
1. Copy them to your computer
2. Flag them as executable by root
3. Make sure the bash is available at the link shown on the top of the script. If it's not, the best way is to create a symlink to it.
E.G. ln -s /usr/local/bin/bash /bin/bash
Alternatively you can change the script.
4. Edit the well-documented parameters in the User Settings area
5. Schedule the files to run in crontab.
- Linux Example:
##### Anti-malware processes
# Scan server with ClamAV 30 minutes after the hour
30 * * * * /etc/clamscan_delta.sh
# Scan server with ClamAV each day
0 2 * * * /etc/clamscan_full.sh
- FreeBSD Example:
##### Anti-malware processes
# Scan server with ClamAV 30 minutes after the hour
30 * * * * root /etc/clamscan_delta.sh
# Scan server with ClamAV each day
0 2 * * * root /etc/clamscan_full.sh

Is ClamAV hard on server resources while scanning?
Yes, but it depends on how many cores and how much capacity you have available whether or not it will be noticeable. ClamAV is a single-thread app, so it can't steal all of your power if you have multiple cores. Moreover, two of the scripts employ delta scanning, so they only scan the files that have changed since the last scan. The following numbers were taken from one VM, and while every environment is different, it gives you an idea from a relative standpoint of what you can expect with the different scenarios. If I do a delta scan on these areas
/home/*/domains/*/private_html \
/home/*/domains/*/public_html \
/home/*/domains/*/private_ftp \
/home/*/domains/*/public_ftp \
it requires about 60 seconds for the process to complete. Of that time, scanning requires 13 seconds, with the remainder of the time being finding the files that need to be scanned, and loading clamscan. If I expand it to this:
/home/*/domains/*/private_html \
/home/*/domains/*/public_html \
/home/*/domains/*/private_ftp \
/home/*/domains/*/public_ftp \
/home/*/Maildir \
/home/*/imap/*/*/Maildir \
/var/tmp \
/tmp \
it takes a minute and 30 seconds for the process to run, and a delta on the entire server requires ~11 minutes. A full scan of the server requires ~2 hours and 20 minutes. You could run the deltas most of the time, and perhaps a full scan daily or once a week during off-peak time.

Couldn't you just add some parameters and have one script?
That would not be difficult, and I played with that, but what it does, is make the parameters much more confusing, which is why I opted to go with the three scripts. The user needs more than one anyway, one for delta scans and another for full.
 
Last edited:
Change log:
2013-03-09 - Version 1.00 - Initial - Tested on FreeBSD only. Awaiting feedback from Linux users about any portability issues.
2013-03-11 - Version 1.01 - Very minor changes
- Made Sanity checking more thorough.
- Check for log file existence before attempting to determine size to avoid error message on first run after new install.
2013-03-12 - Version 1.10 - It was brought to my attention that delta and delta 2 were forming many temporary files. This was due to a broken pipe. (I got just a little too clever before I rolled it out here.) Fixed the code causing the problem. No changes in functionality.
2013-03-13 - Version 1.20 - Important update! Functionally the same, but instead of using the temporary file, it now uses dedicated scratch files. This accomplishes a few things. The file prefixes used are the name SCAN_TYPE to make troubleshooting easier, it removes the potential for temporary files to consume disk space even if the process is aborted, and you can store the scratch files wherever you wish, rather than the system default areas of /tmp and /var/tmp, where they are readable by all users. Due to change in paradigm, changes were also made in the User Settings area.
 
Last edited:
All:
I discovered this very old post today. I went ahead and tested these on my current Server. They still work well. Even thought they haven't been touched in 6 years..
 
Back
Top