#!/usr/bin/perl
#
## Copyright (C) 2005 Jason Haar <jhaar@users.sourceforge.net>
#
# (some code "acquired" from clamdwatch from the ClamAV package)

# This watchguard script will manually send the EICAR test
# virus to the AV daemon requested, and will wait for it 
# to confirm that it detected it. If it doesn't, or this 
# script is still waiting $timeout seconds later, this
# script will generate a syslog error, and exit status 1
#

# The intent is that you can call this script from cron on
# a regular basis (say every 5 minutes) in such a way as to 
# auto-restart the daemon if it fails
#

# e.g
#
# check_AV_daemons --daemon clamd --socket /tmp/clamd || svc -k /service/clamd
#

# Explanation:
# The "||" sequence in a shell script means:
# "only run what follows IFF the previous command exited with a non-zero status"
#
# It was written this way as each site will have a different way of restarting
# their daemons. This way you should never have to edit this script :-)

# The following AV (and SpamAssassin) daemons are supported

# clamd, sophie, trophie, spamd


use IO::Socket::UNIX;
use File::Temp qw/ tempfile tempdir /;
use Sys::Syslog qw(:DEFAULT setlogsock);
setlogsock('unix');
use File::Basename;
use Getopt::Long;

$|=1;

my $version='1.0';


$result=GetOptions ('daemon:s' => \$opt_daemon,'socket:s' => \$opt_socket);


if (!$opt_daemon || !$opt_socket) {
	$opt_daemon='[trophie|sophie|clamd|spamd]' if (!$opt_daemon);
	$opt_socket='/path/to/socket' if (!$opt_socket);
	print "

Version: $version

$0 --daemon $opt_daemon --socket $opt_socket

\n";
	exit 0;
}


my $Socket=$opt_socket;
my $daemon=$opt_daemon;

# Set system to allow daemon up to 15 secs to respond correctly
# - otherwise it's broken
my $timeout = 15;

my $ip = "127.0.0.1";
my $sock;


$tmp = new File::Temp( TEMPLATE => 'vtempXXXXX',
                                   DIR => '/tmp',
                                   SUFFIX => '.com');
$vfilename = $tmp->filename;

open(EICAR,">>$vfilename");
print EICAR 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*';
close(EICAR);
chmod 0755, $vfilename;

$spam_string="Subject: Test spam mail (GTUBE)\nMessage-ID: <GTUBE1.1010101\@example.net>\nPrecedence: junk\n\nXJS\*C4JDBQADN1.NSBN3\*2IDNEN\*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL\*C.34X\n";
$spam_length=length($spam_string);

sub error {
	my ($status,$string)=@_;
	unlink $vfilename, $sfilename; 
	if ($status) {
	 syslog('daemon|err',"check_AV_daemons[$$]: $daemon hung/broken - $string"); 
	 warn "$daemon hung/broken - $string\n";
	}else{
	 warn "$string\n";
	}
	exit $status;
}
print "Socket=$Socket\n";
if ( $Socket !~ /^([^:]+):([0-9]+)$/ ) {
  #Hey, it's not an IP address - must be a Unix socket
  if ( ! -e $Socket ) {
        &error(1,"$Socket missing! It doesn't look like $daemon running.");
  } else {
        $sock = new IO::Socket::UNIX(Type => SOCK_STREAM,
                        Timeout => $timeout,
                        Peer => $Socket );
  }
}else{
  $ip=$1;
  $Socket=$2;
  $sock = IO::Socket::INET->new( PeerAddr => $ip,
		PeerPort => $Socket,
		Timeout => $timeout,
		Proto     => 'tcp');
}

if (!$sock || $@ ) { 
    &error(1,"$daemon not running");
}

if ( $sock->connected ) { 

    my ($err,$err1) = "";

    if ($daemon eq "clamd") {
    	# ask clamd to scan the script
    	$sock->send("RAWSCAN $vfilename");
    } elsif ($daemon eq "trophie") {
	# ask trophie to scan the script
	#syswrite(\*sock, $path, length($path));
	$sock->send("$vfilename\n");
    }elsif ($daemon eq "sophie") {
        # ask sophie to scan the script
        $sock->send("$vfilename\n");
    } elsif ($daemon eq "spamd") {
	#ask SpamAssassin to confirm this is SPAM
	$sock->send("CHECK SPAMC/1.3\r\nUser: check_AV_daemons\r\nContent-length: $spam_length\r\n\r\n$spam_string");
    } else {
	&error(0,"Error: $daemon not a supported AV daemon");
    }
    # set the $timeout and die with a useful error if
    # daemon isn't responsive
    eval {
        local $SIG{ALRM} = sub { die "timeout\n" };
	alarm($timeout);
        $sock->recv($err, 256);
	if ($daemon eq "spamd") {
		#Receive next line of data if present too
		$sock->recv($err1, 256);
		#Put those two lines together
		$err .= $err1;
	}
	alarm(0);
    };
    if ($@) {

    	die unless $@ eq "timeout\n";
        &error(1,"$daemon not responding to scan request");

    } else { # daemon responded to the request

        if ( $err =~ /test|spam: true/i ) { 
	    #Cool, EICAR test virus or SPAM detected - exit
	    &error(0,"$daemon working OK");
        } else {
	    # something's broken
	    &error(1,"$daemon is BROKEN state.");
	}

    }

} else {
    # you should never get here either
    &error(1,"Cannot connect to socket");
}


__END__

=head1 NAME

I<check_AV_daemons> -  script that sends test viruses to Unix antivirus
daemons in order to confirm they are running correctly. Similarly it can
send a test SPAM to the SpamAssassin "clamd" daemon in order to confirm
it is running correctly.

=head2 SYNOPSIS

check_AV_daemons --daemon [clamd|trophie|sophie|spamd] \
 --socket [/path/to/socket|1.2.3.4:1122]

where "--socket" is either the full path to the socket file of the 
particular daemon, or is the IP address and port number of the daemon
if it is network-enabled (spamd is by default "127.0.0.1:783")

=head1 DESCRIPTION

This perl script primarily checks for the positive response of the
AV daemon, and can handle "hung" daemons by way of alarms. i.e.
even if the daemon has hung, the script will notice after $timeout
seconds that it hasn't had a response, so it will treat that as an
error

Currently the following antivirus and antispam systems are supported:

=over 2

=item o clamd - ClamAV daemon

=item o trophie - Trend daemon

=item o sophie - Sophos daemon

=item o spamd - SpamAssassin antispam daemon

=back
=back

The script will output to stderr the status of the test, and if it's an error,
will report it to syslog and will exit error status 1.

This allows you to call a script on error - such as to restart the daemon...

=head1 RUNTIME

The following is an example of how to use this script to check every five 
minutes that clamd and spamd are running correctly on a system. This 
system runs SpamAssassin as a standard "rc" script, but runs
ClamAV via the daemontools supervised script - so these cronjobs reflect that.

*/5 * * * *	(/usr/local/bin/check_AV_daemons --daemon clamd --socket /tmp/clamd || svc -k /service/clamav ) > /dev/null 2>&1

*/5 * * * *	(/usr/local/bin/check_AV_daemons --daemon spamd --socket 127.0.0.1:783 || /etc/rc.d/init.d/spamd restart) > /dev/null 2>&1

Obviously you would do what is appropriate for your system

=head1 AUTHOR

Jason Haar <jhaar@users.sourceforge.net>

=cut



