#!/usr/bin/perl -w # # MailScanner - SMTP E-Mail Virus Scanner # Copyright (C) 2001 Julian Field # # $Id: panda-wrapper 3087 2005-06-08 14:26:29Z jkf $ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # The author, Julian Field, can be contacted by email at # Jules@JulianField.net # or by paper mail at # Julian Field # Dept of Electronics & Computer Science # University of Southampton # Southampton # SO17 1BJ # United Kingdom # # # This wrapper, and the Panda support in MailScanner itself, is all # actually implemented by Rick Cooper . # All queries to him please. # # To test from the command line change to the directory you wish to # check and issue this command (change paths to reflect your install) # "/opt/MailScanner/lib/panda-wrapper /usr -nsb -eng -aex -nso -aut -cmp ." # Make sure your testing dir is one directory deep (don't for get the . BTW) # example # test+ # .+ testfiles # .+ moretestfiles # execute from directory test and it will scan the testfiles and moretestfiles # directories. There should be no sub-dirs below those two, this simulates # MailScanner's process-dir->message-dir structure my $pavcl; my $base; # Just a default in case there is a problem with getting # the scanner timeout value - belt and suspenders my $CmdTimeOut = '500'; my $CmdArgv=""; # Set Terminal Rows and Columns my $TermRows = 24; my $TermCols = 80; my @VirusStrings; my $VirusCount=0; # Define as global so I don't have to pass private in the # vt_scroll function. No need to trap all that license crap # that scrolls off the screen when the -aut switch is passed # so we will dump everything that scrolls until we find the copyright line # that is what $FoundTop is for, It is checked in the vt_scroll sub since # that is where the license information will be found my $VirtualScreens=""; my $FoundTop = 0; $pavcl = shift; $pavcl .= '/bin/pavcl'; if ($ARGV[0] eq '-IsItInstalled') { exit 0 if -x "$pavcl"; exit 1; } # If pavcl is not there, and executable, then exit with an error that the # panda output parser can do something with print_and_exit("Panda:ERROR: Could Not Find $pavcl scanner\n") if !-x "$pavcl"; # Since we must have Term::VT102 let's issue an error warning if we don't # find it installed here. The rest are used my MailScanner in many places # putting them here prevents wasting time if pavcl is not installed print_and_exit("Panda:ERROR: YOU NEED TO INSTALL MODULE Term::VT102 PLEASE!(CPAN?)\n") unless eval { require Term::VT102 }; use Cwd; use Cwd 'abs_path'; use POSIX qw(:signal_h setsid); use FileHandle; # We do not want buffering $| = 1; # Get the current working directory and make sure it's # not a symlink #$base = cwd(); $base = abs_path(); chomp $base; # Get the last entry in @ARGV pop @ARGV; # Need to get the value of ScannerTimeOut from the # argument list, and remove it from the list as well foreach $c_arg (@ARGV){ if ($c_arg =~ /\-t:(\d+)/){ $CmdTimeOut = $1; }else{ $CmdArgv .= "$c_arg "; } } #There will be a trailing space so let's remove it. $CmdArgv =~ s/\s+$//; my @MessageDirs; # Now read in any message directories under the current # process directory my @DirList = <$base/*>; $VirusCount =0; # Scan the process directory for any message directories # there will be files in here as well but they are not scanned # since they should only be header files foreach $dir (@DirList){ my $temp; # If this is a directory then split the directory name # from the full path (which includes the process directory) # Scan this entire directory in one pass, process and continue if (-d $dir){ $dir =~ s/^.+\/(.+)$/$1/; $temp = scan_virus($dir,$base); if ($temp =~ /\t\tFOUND:/){ $VirusStrings[$VirusCount]= $temp; $VirusCount++; } } } # If no virus was found print string to tell parser no viruses were # found, otherwise print our report and exit print "Virus: 0\n" unless $VirusCount; foreach $outline (@VirusStrings){ print "$outline\n"; } exit 0; sub scan_virus{ # Make sure our Virtual Screen is clean when called. $VirtualScreens = ""; # Setup our scan variables # Make sure they are free of newlines my ($DirName,$ProcessDir) = @_; $DirName =~ s/\n//g; my @TLast; # Now make sure our current msg dir is free from any parts of the # base process dir $DirName =~ s/^.*?\/(.*)$/$1/g; # Exit with an error the output parser can understand and log if # we cannot change to the message directory print_and_exit("Panda:ERROR: Could not change to message dir - > $DirName\n") if !chdir($DirName); # Intialize our virus name and string vars my $VName; my $Str; # Set up the scan command to scan the current directory my $Cmd = "$pavcl './' $CmdArgv 2>&1"; # We need to track the previous line, with all processing completed # because that is where the file name will be when/if we find a virus. # We also track the last archive file (filename.ext[filename] formated) # so when we find an infected file from inside an archive (name will end # with a ]) we can add the archive_name-> string before the file name my $LastLine = ""; my $LastArchive = ""; # We need to check our return string to see if we have already found # this file. pavcl will return an archived file as the bare name as well # as the archive_name[file_name] format and we don't want to show the # same file twice. # Also initialize the return string var my $TestString = ""; my $TestString2 = ""; my $rc = ""; my $StrIdx=0; # We are going to use SafePipe, with a the configured scanner time out # Exit with error if the command times out my $PipeOut = SafePipe($Cmd,$CmdTimeOut); print_and_exit("Panda:ERROR: $pavcl timed out!\n") if $PipeOut eq "COMMAND_TIMED_OUT"; # Now we intialize our VT102 object.It will process # the output into a virtual screen which we can then deal with # easily my $vt = Term::VT102->new ('cols' => $TermCols, 'rows' => $TermRows); # We have to watch for a screen scroll if there are more than # four infections found so we set the callback for scroll_down # to a function that will grab the line that is being scrolled off $vt->callback_set ('SCROLL_DOWN', \&vt_scroll); # Now process the command output, if there is a scroll_down callback # $VirtualScreens will be intialized with what ever lines are being # scrolled off the screen $vt->process ($PipeOut); # This section is for the current, last screen. The last screen's out # put will only hold four infections at a time, anything prior to # that was handled in vt_scroll. If there was no scrolling then # we will will need to process the current and/or only screen here for($StrIdx=0;$StrIdx <= $TermRows;$StrIdx++){ # if we call for a value out of our virtual screen's column/row # it will return undef so we don't want to attempt a .= on an # undefined error, it's so messy. We don't worry about # the license information here because it's alreay gone. $VirtualScreens.= $vt->row_plaintext($StrIdx)."\n" if defined($vt->row_text($StrIdx)); } # Now we split our VT102 output on the newlines and feed each line # into the parse loop to look for any viruses @CurrentScreen = split(/\n/,$VirtualScreens); foreach $Str(@CurrentScreen){ # Depending on the version of panda one is using there is a difference # in the wording of the virus found string if ($Str =~ /(found virus|virus encontrado|encontrado virus|virus found)\s+:\s?(.*?)$/i) { # Don't want leading and trailing spaces in the virus name $VName = $2; $VName =~ s/^\s+|\s+$//g; # Now build the return string containing the virus name, # the infected file, the message dir and process/base dir # We get the file name from the line just before the one that # triggered the virus found logic. Since we may encounter # multiple viruses we will continue to join the "found strings" # until the entire output is processed $LastLine =~ s/\n|^\s+|\s+$//g; # Now we build the test strings to find duplicates. We need to # check for bare name only and archive_name->file_name # Cannot really do that with a | because we have to quote all # special characters in each string and the ($ArchiveName->)? # would get escaped and never match \($ArchiveName->\)\? $TestString = "FOUND:$VName##::##$LastLine##::##$DirName##::##$ProcessDir"; $TestString =~ s/(\/|\.|\^|\$|\*|\+|\?|\{|\}|\[|\]|\(|\))/\\$1/g; $TestString2 = "FOUND:$VName##::##$LastArchive->$LastLine##::##$DirName##::##$ProcessDir"; $TestString2 = "FOUND:$VName##::##$LastLine##::##$DirName##::##$ProcessDir" if $LastLine =~ /.\-\>./mg; $TestString2 =~ s/(\/|\.|\^|\$|\*|\+|\?|\{|\}|\[|\]|\(|\))/\\$1/g; # Append the current virus information, if we have not already # reported this virus $rc .= "\t\tFOUND:$VName##::##$LastLine##::##$DirName##::##$ProcessDir\n" if $rc !~ /$TestString/mg and $rc !~ /$TestString2/mg; }# End of found a virus # We get the file name from the line preceeding the virus found # string. The file name can appear in two formats # If there is only one file in the archive the entire # archive_name[file_name] is returned and we want to # change this so the reports look more like the other scanners # in the format of my.zip->my.file. $LastLine = $Str; $LastArchive = $LastLine if $LastLine =~ /^.+\[.*?\].*$/; $LastArchive =~ s/^(.+)\[.*?\]/$1/ if $LastLine =~ /^.+\[.*?\]/; $LastArchive =~ s/^.+\/(.+)$/$1/g; $LastArchive =~ s/^\s+|\s+$//g; $LastLine = "$1->$2" if $LastLine =~ /^(.+)\[(.+)\]/; $LastLine =~ s/^.+\/(.+)$/$1/g; $LastLine =~ s/^\s+|\s+$//g; } # Pull off any terminating new line here. Return OK if no virus found # or our report string if there was. chomp($rc); print_and_exit("Panda:ERROR: Could not return from message dir - > $DirName\n") if !chdir("../"); return "OK" if $rc eq ""; return $rc; } # This is the same SafePipe from MailScanner::Message.pm sub SafePipe { my ($Cmd, $TimeOut) = @_; my($Kid, $pid, $TimedOut, $Str); $Kid = new FileHandle; $TimedOut = 0; $? = 0; # Make sure there's no junk left in here eval { die "Can't fork: $!" unless defined($pid = open($Kid, '-|')); if ($pid) { # In the parent # Set up a signal handler and set the alarm time to the timeout # value passed to the function local $SIG{ALRM} = sub { $TimedOut = 1; die "Command Timed Out" }; alarm $TimeOut; # while the command is running we will collect it's output # in the $Str variable. We don't process it in any way here so # whatever called us will get back exactly what they would have # gotten with a system() or backtick call while(<$Kid>) { $Str .= $_; #print STDERR "SafePipe : Processing line \"$_\"\n"; } $pid = 0; # 2.54 alarm 0; # Workaround for bug in perl shipped with Solaris 9, # it doesn't unblock the SIGALRM after handling it. eval { my $unblockset = POSIX::SigSet->new(SIGALRM); sigprocmask(SIG_UNBLOCK, $unblockset) or die "Could not unblock alarm: $!\n"; }; } else { # In the child POSIX::setsid(); # Execute the command via an exec call, bear in mind this will only # capture STDIN so if you need STDERR, or both you have to handle, for # example, 2>&1 as part of the command line just as you would with # system() or backticks # #the line following the # call should *never* be reached unless the call it's self fails # In this instance I need to know what kind of terminal pavcl # is writting to and every *nix understands vt100 and it's easy # to parse the output as sent by pavcl to a vt100 terminal $ENV{TERM} = 'vt100'; my @args = ( "$Cmd" ); # Just to be safe let's take control of STDIN and then exec the # command open STDIN, "< /dev/null"; exec @args or die "Failed to execute commad in safepipe"; exit 1; } }; alarm 0; # 2.53 # Catch failures other than the alarm die "pavcl died with real error $@" if $@ and $@ !~ /Command Timed Out/; # In which case any failures must be the alarm if ($@ or $pid>0) { # Kill the running child process my($i); kill -15, $pid; # Wait for up to 5 seconds for it to die for ($i=0; $i<5; $i++) { sleep 1; waitpid($pid, &POSIX::WNOHANG); ($pid=0),last unless kill(0, $pid); kill -15, $pid; } # And if it didn't respond to 11 nice kills, we kill -9 it if ($pid) { kill -9, $pid; waitpid $pid, 0; # 2.53 } } # If the command timed out return the string below, otherwise # return the command output in $Str return $Str unless $TimedOut; return "COMMAND_TIMED_OUT"; } # This sub is just a way to do a single line error string return and # exit with information the output parser would understand sub print_and_exit{ $ES = $_[0]; print $ES; exit 0; } # This sub handles the scroll callbacks so we can keep the lines # scrolling up and off the virus display area of the pavcl output sub vt_scroll { my ($vtobject, $type, $arg1, $arg2, $private) = @_; my ($i,$ts); $ts = ""; # pavcl scrolls one line at a time so we just grab # the line that is leaving and append it to our VirtualScreen $ts = $vtobject->row_plaintext($arg1)if defined($vtobject->row_plaintext($arg1)); $ts =~ s/\s+$//g if $ts ne ""; $FoundTop = 1 if $ts =~ /Panda Antivirus Linux,\s+\(c\)\s+Panda Software \d{4}/i; $VirtualScreens .= $ts."\n" if $ts ne "" and $FoundTop; }