# # MailScanner - SMTP E-Mail Virus Scanner # Copyright (C) 2002 Julian Field # # $Id: SweepViruses.pm 3893 2007-05-18 20:44:09Z sysjkf $ # # 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 # package MailScanner::SweepViruses; use strict 'vars'; use strict 'refs'; no strict 'subs'; # Allow bare words for parameter %'s use POSIX qw(:signal_h setsid); # For Solaris 9 SIG bug workaround use DirHandle; use vars qw($VERSION $ScannerPID); ### The package version, both in 1.23 style *and* usable by MakeMaker: $VERSION = substr q$Revision: 3893 $, 10; # Locking definitions for flock() which is used to lock the Lock file my($LOCK_SH) = 1; my($LOCK_EX) = 2; my($LOCK_NB) = 4; my($LOCK_UN) = 8; # Sophos SAVI Library object and ide directory modification time my($SAVI, $SAVIidedirmtime, $SAVIlibdirmtime, $SAVIinuse, %SAVIwatchfiles); $SAVIidedirmtime = 0; $SAVIlibdirmtime = 0; $SAVIinuse = 0; %SAVIwatchfiles = (); # ClamAV Module object and library directory modification time my($Clam, $Claminuse, %Clamwatchfiles); $Claminuse = 0; %Clamwatchfiles = (); # So we can kill virus scanners when we are HUPped $ScannerPID = 0; my $scannerlist = ""; # # Virus scanner definitions table # my ( $S_NONE, # Not present $S_UNSUPPORTED, # Present but you're on your own $S_ALPHA, # Present but not tested -- we hope it works! $S_BETA, # Present and tested to some degree -- we think it works! $S_SUPPORTED, # People use this; it'd better work! ) = (0,1,2,3,4); my %Scanners = ( generic => { Name => 'Generic', Lock => 'GenericBusy.lock', CommonOptions => '', DisinfectOptions => '-disinfect', ScanOptions => '', InitParser => \&InitGenericParser, ProcessOutput => \&ProcessGenericOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, sophossavi => { Name => 'SophosSAVI', Lock => 'SophosBusy.lock', # In next line, '-ss' makes it work nice and quietly CommonOptions => '', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitSophosSAVIParser, ProcessOutput => \&ProcessSophosSAVIOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, sophos => { Name => 'Sophos', Lock => 'SophosBusy.lock', # In next line, '-ss' makes it work nice and quietly CommonOptions => '-sc -f -all -rec -ss -archive -cab -loopback ' . '--no-follow-symlinks --no-reset-atime -TNEF', DisinfectOptions => '-di', ScanOptions => '', InitParser => \&InitSophosParser, ProcessOutput => \&ProcessSophosOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, mcafee => { Name => 'McAfee', Lock => 'McAfeeBusy.lock', CommonOptions => '--recursive --ignore-links --analyze --mime ' . '--secure --noboot', DisinfectOptions => '--clean', ScanOptions => '', InitParser => \&InitMcAfeeParser, ProcessOutput => \&ProcessMcAfeeOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, command => { Name => 'Command', Lock => 'CommandBusy.lock', CommonOptions => '-packed -archive', DisinfectOptions => '-disinf', ScanOptions => '', InitParser => \&InitCommandParser, ProcessOutput => \&ProcessCommandOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, etrust => { Name => 'eTrust', Lock => 'eTrustBusy.lock', CommonOptions => '-nex -arc -mod reviewer -spm h ', DisinfectOptions => '-act cure -sca mf', ScanOptions => '', InitParser => \&InitInoculateParser, ProcessOutput => \&ProcessInoculateOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, inoculate => { Name => 'Inoculate', Lock => 'InoculateBusy.lock', CommonOptions => '-nex -arc -mod reviewer -spm h ', DisinfectOptions => '-act cure -sca mf', ScanOptions => '', InitParser => \&InitInoculateParser, ProcessOutput => \&ProcessInoculateOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, inoculan => { Name => 'Inoculan', Lock => 'InoculanBusy.lock', CommonOptions => '-nex -rev ', DisinfectOptions => '-nex -cur', ScanOptions => '', InitParser => \&InitInoculanParser, ProcessOutput => \&ProcessInoculanOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "kaspersky-4.5" => { Name => 'Kaspersky', Lock => 'KasperskyBusy.lock', CommonOptions => '', DisinfectOptions => '-i2', ScanOptions => '-i0', InitParser => \&InitKaspersky_4_5Parser, ProcessOutput => \&ProcessKaspersky_4_5Output, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, kaspersky => { Name => 'Kaspersky', Lock => 'KasperskyBusy.lock', CommonOptions => '', DisinfectOptions => '-- -I2', ScanOptions => '-I0', InitParser => \&InitKasperskyParser, ProcessOutput => \&ProcessKasperskyOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, kavdaemonclient => { Name => 'KavDaemon', Lock => 'KavDaemonClientBusy.lock', CommonOptions => '', DisinfectOptions => '-- -I2', ScanOptions => '', InitParser => \&InitKavDaemonClientParser, ProcessOutput => \&ProcessKavDaemonClientOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "f-secure" => { Name => 'F-Secure', Lock => 'FSecureBusy.lock', CommonOptions => '--dumb --archive', DisinfectOptions => '--auto --disinf', ScanOptions => '', InitParser => \&InitFSecureParser, ProcessOutput => \&ProcessFSecureOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "f-prot" => { Name => 'F-Prot', Lock => 'FProtBusy.lock', CommonOptions => '-old -archive -dumb', DisinfectOptions => '-disinf -auto', ScanOptions => '', InitParser => \&InitFProtParser, ProcessOutput => \&ProcessFProtOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, nod32 => { Name => 'Nod32', Lock => 'Nod32Busy.lock', CommonOptions => '-log- -all', DisinfectOptions => '-clean -delete', ScanOptions => '', InitParser => \&InitNOD32Parser, ProcessOutput => \&ProcessNOD32Output, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "nod32-1.99" => { Name => 'Nod32', Lock => 'Nod32Busy.lock', CommonOptions => '--arch --all -b', DisinfectOptions => '--action clean --action-uncl none', ScanOptions => '', InitParser => \&InitNOD32199Parser, ProcessOutput => \&ProcessNOD32Output, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "antivir" => { Name => 'AntiVir', Lock => 'AntiVirBusy.lock', CommonOptions => '-allfiles -s -noboot -rs -z', DisinfectOptions => '-e -ren', ScanOptions => '', InitParser => \&InitAntiVirParser, ProcessOutput => \&ProcessAntiVirOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "panda" => { Name => 'Panda', Lock => 'PandaBusy.lock', CommonOptions => '-nsb -heu -eng -aex -nso -aut -cmp', DisinfectOptions => '-clv', ScanOptions => '-nor', InitParser => \&InitPandaParser, ProcessOutput => \&ProcessPandaOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "rav" => { Name => 'Rav', Lock => 'RavBusy.lock', CommonOptions => '--all --mail --archive', DisinfectOptions => '--clean', ScanOptions => '', InitParser => \&InitRavParser, ProcessOutput => \&ProcessRavOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "clamavmodule" => { Name => 'ClamAV Module', Lock => 'ClamAVBusy.lock', CommonOptions => '', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitClamAVModParser, ProcessOutput => \&ProcessClamAVModOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "clamd" => { Name => 'ClamAV Daemon', Lock => 'ClamAVBusy.lock', CommonOptions => '--no-summary --stdout', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitClamAVParser, ProcessOutput => \&ProcessClamAVOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "clamav" => { Name => 'ClamAV', Lock => 'ClamAVBusy.lock', CommonOptions => '-r --no-summary --stdout', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitClamAVParser, ProcessOutput => \&ProcessClamAVOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "trend" => { Name => 'Trend', Lock => 'TrendBusy.lock', CommonOptions => '-a -za -r', DisinfectOptions => '-c', ScanOptions => '', InitParser => \&InitTrendParser, ProcessOutput => \&ProcessTrendOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "bitdefender" => { Name => 'Bitdefender', Lock => 'BitdefenderBusy.lock', CommonOptions => '--arc --mail --all', DisinfectOptions => '--disinfect', ScanOptions => '', InitParser => \&InitBitdefenderParser, ProcessOutput => \&ProcessBitdefenderOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "drweb" => { Name => 'DrWeb', Lock => 'drweb.lock', CommonOptions => '-ar -fm -ha- -fl- -ml -sd -up', DisinfectOptions => '-cu', ScanOptions => '', InitParser => \&InitDrwebParser, ProcessOutput => \&ProcessDrwebOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "norman" => { Name => 'Norman', Lock => 'NormanBusy.lock', CommonOptions => '-c -sb:1 -s -u', DisinfectOptions => '-cl:2', ScanOptions => '', InitParser => \&InitNormanParser, ProcessOutput => \&ProcessNormanOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "css" => { Name => 'SYMCScan', Lock => 'SYMCScan.lock', CommonOptions => '', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitCSSParser, ProcessOutput => \&ProcessCSSOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "avg" => { Name => 'Avg', Lock => 'AvgBusy.lock', CommonOptions => '-arc', # Remove by Chris Richardson: -ext=*', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitAvgParser, ProcessOutput => \&ProcessAvgOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_NONE, }, "vexira" => { Name => 'Vexira', Lock => 'VexiraBusy.lock', #CommonOptions => '--allfiles -s -z -noboot -nombr -r1 -rs -lang=EN --alltypes', #DisinfectOptions => '-e', CommonOptions => '-qq --scanning=full', DisinfectOptions => '--remove-macro --action=kill', ScanOptions => '--action=skip', InitParser => \&InitVexiraParser, ProcessOutput => \&ProcessVexiraOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "symscanengine" => { Name => 'SymantecScanEngine', Lock => 'SymScanEngineBusy.lock', CommonOptions => '-details -recurse', DisinfectOptions => '-mode scanrepair', ScanOptions => '-mode scan', InitParser => \&InitSymScanEngineParser, ProcessOutput => \&ProcessSymScanEngineOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "avast" => { Name => 'Avast', Lock => 'Avast.lock', CommonOptions => '-n -t=A', DisinfectOptions => '-p=3', ScanOptions => '', InitParser => \&InitAvastParser, ProcessOutput => \&ProcessAvastOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "avastd" => { Name => 'AvastDaemon', Lock => 'AvastDaemon.lock', CommonOptions => '-n', DisinfectOptions => '', ScanOptions => '', InitParser => \&InitAvastdParser, ProcessOutput => \&ProcessAvastdOutput, SupportScanning => $S_SUPPORTED, SupportDisinfect => $S_SUPPORTED, }, "none" => { Name => 'None', Lock => 'NoneBusy.lock', CommonOptions => '', DisinfectOptions => '', ScanOptions => '', InitParser => \&NeverHappens, ProcessOutput => \&NeverHappens, SupportScanning => $S_NONE, SupportDisinfect => $S_NONE, }, ); # Initialise the Sophos SAVI library if we are using it. sub initialise { my(@scanners); $scannerlist = MailScanner::Config::Value('virusscanners'); # If they have not configured the list of virus scanners, then try to # use all the scanners they have installed, by using the same system # that update_virus_scanners uses to locate them all. #print STDERR "Scanner list read from MailScanner.conf is \"$scannerlist\"\n"; if ($scannerlist =~ /^\s*auto\s*$/i) { $scannerlist = join(' ', InstalledScanners()); MailScanner::Log::InfoLog("I have found %s scanners installed, and will use them all by default.", $scannerlist); if ($scannerlist =~ /^\s*$/) { MailScanner::Log::WarnLog("You appear to have no virus scanners installed at all! This is not good. If you have installed any, then check your virus.scanners.conf file to make sure the locations of your scanners are correct"); #print STDERR "No virus scanners found to be installed at all!\n"; $scannerlist = "none"; } } $scannerlist =~ tr/,//d; @scanners = split(" ", $scannerlist); # Import the SAVI code and initialise the SAVI library if (grep /^sophossavi$/, @scanners) { $SAVIinuse = 1; #print STDERR "SAVI in use\n"; InitialiseSAVI(); } # Import the ClamAV code and initialise the ClamAV library if (grep /^clamavmodule$/, @scanners) { $Claminuse = 1; #print STDERR "ClamAV Module in use\n"; InitialiseClam(); } # Set the Unrar command path for the ClamAV code if (grep /^clamav$/, @scanners) { my $rarcmd = MailScanner::Config::Value('unrarcommand'); if ($rarcmd && -x $rarcmd) { $Scanners{clamav}->{CommonOptions} .= " --unrar=$rarcmd"; MailScanner::Log::InfoLog("ClamAV scanner using unrar command %s", $rarcmd); } } } sub InitialiseClam { # Initialise ClamAV Module MailScanner::Log::DieLog("ClamAV Perl module not found, did you install it?") unless eval 'require Mail::ClamAV'; my $ver = $Mail::ClamAV::VERSION + 0.0; MailScanner::Log::DieLog("ClamAV Perl module must be at least version 0.12" . " and you only have version %.2f, and ClamAV must" . " be at least version 0.80", $ver) unless $ver >= 0.12; $Clam = new Mail::ClamAV(Mail::ClamAV::retdbdir()) or MailScanner::Log::DieLog("ClamAV Module ERROR:: Could not load " . "databases from %s", Mail::ClamAV::retdbdir()); $Clam->buildtrie; # Impose limits $Clam->maxreclevel(MailScanner::Config::Value('clamavmaxreclevel')); $Clam->maxfiles (MailScanner::Config::Value('clamavmaxfiles')); $Clam->maxfilesize(MailScanner::Config::Value('clamavmaxfilesize')); $Clam->maxratio (MailScanner::Config::Value('clamavmaxratio')); # Build the hash of the size of all the watch files my(@watchglobs, $glob, @filelist, $file, $filecount); @watchglobs = split(" ", MailScanner::Config::Value('clamwatchfiles')); $filecount = 0; foreach $glob (@watchglobs) { @filelist = glob($glob); foreach $file (@filelist) { $Clamwatchfiles{$file} = -s $file; $filecount++; } } MailScanner::Log::DieLog("None of the files matched by the \"Monitors " . "For ClamAV Updates\" patterns exist!") unless $filecount>0; #MailScanner::Log::WarnLog("\"Allow Password-Protected Archives\" should be set to just yes or no when using clamavmodule virus scanner") # unless MailScanner::Config::IsSimpleValue('allowpasszips'); } sub InitialiseSAVI { # Initialise Sophos SAVI library MailScanner::Log::DieLog("SAVI Perl module not found, did you install it?") unless eval 'require SAVI'; my $SAVIidedir = MailScanner::Config::Value('sophoside'); $SAVIidedir = '/usr/local/Sophos/ide' unless $SAVIidedir; my $SAVIlibdir = MailScanner::Config::Value('sophoslib'); $SAVIlibdir = '/usr/local/Sophos/lib' unless $SAVIlibdir; $ENV{'SAV_IDE'} = $SAVIidedir; print "INFO:: Meaningless output that goes nowhere, to keep SAVI happy\n"; $SAVI = new SAVI(); MailScanner::Log::DieLog("SophosSAVI ERROR:: initializing savi: %s (%s)", SAVI->error_string($SAVI), $SAVI) unless ref $SAVI; my $version = $SAVI->version(); MailScanner::Log::DieLog("SophosSAVI ERROR:: getting version: %s (%s)", $SAVI->error_string($version), $version) unless ref $version; MailScanner::Log::InfoLog("SophosSAVI %s (engine %d.%d) recognizing " . "%d viruses", $version->string, $version->major, $version->minor, $version->count); my($ide,$idecount); $idecount = 0; foreach $ide ($version->ide_list) { #MailScanner::Log::InfoLog("SophosSAVI IDE %s released %s", # $ide->name, $ide->date); $idecount++; } MailScanner::Log::InfoLog("SophosSAVI using %d IDE files", $idecount); # I have removed "Mac" and "SafeMacDfHandling" from here as setting # them gives an error. my @options = qw( FullSweep DynamicDecompression FullMacroSweep OLE2Handling IgnoreTemplateBit VBA3Handling VBA5Handling OF95DecryptHandling HelpHandling DecompressVBA5 Emulation PEHandling ExcelFormulaHandling PowerPointMacroHandling PowerPointEmbeddedHandling ProjectHandling ZipDecompression ArjDecompression RarDecompression UueDecompression GZipDecompression TarDecompression CmzDecompression HqxDecompression MbinDecompression !LoopBackEnabled Lha SfxArchives MSCabinet TnefAttachmentHandling MSCompress !DeleteAllMacros Vbe !ExecFileDisinfection VisioFileHandling Mime ActiveMimeHandling !DelVBA5Project ScrapObjectHandling SrpStreamHandling Office2001Handling Upx PalmPilotHandling HqxDecompression Pdf Rtf Html Elf WordB OutlookExpress ); my $error = $SAVI->set('MaxRecursionDepth', 30, 1); MailScanner::Log::DieLog("SophosSAVI ERROR:: setting MaxRecursionDepth:" . " %s", $error) if defined $error; foreach (@options) { my $value = ($_ =~ s/^!//) ? 0 : 1; $error = $SAVI->set($_, $value); MailScanner::Log::WarnLog("SophosSAVI ERROR:: Setting %s: %s", $_, $error) if defined $error; } ## Store the last modified time of the SAVI lib directory, so we can check ## for major upgrades my(@statresults); #@statresults = stat($SAVIidedir); #$SAVIidedirmtime = $statresults[9] or # MailScanner::Log::WarnLog("Failed to read mtime of IDE dir %s",$SAVIidedir); @statresults = stat($SAVIlibdir); $SAVIlibdirmtime = $statresults[9] or MailScanner::Log::WarnLog("Failed to read mtime of lib dir %s",$SAVIlibdir); #MailScanner::Log::InfoLog("Watching modification date of %s and %s", # $SAVIidedir, $SAVIlibdir); # Build the hash of the size of all the watch files my(@watchglobs, $glob, @filelist, $file, $filecount); @watchglobs = split(" ", MailScanner::Config::Value('saviwatchfiles')); $filecount = 0; foreach $glob (@watchglobs) { @filelist = glob($glob); foreach $file (@filelist) { $SAVIwatchfiles{$file} = -s $file; $filecount++; } } MailScanner::Log::DieLog("None of the files matched by the \"Monitors " . "For Sophos Updates\" patterns exist!") unless $filecount>0; } # Are there new Sophos IDE files? # If so, abandon this child process altogether and start again. # This is called from the main WorkForHours() loop # # If the lib directory has been updated, then a major Sophos update has # happened. If the watch files have changed their size at all, or any # of them have disappeared, then an IDE updated has happened. # Normally just watch /u/l/S/ide/*.zip. # sub SAVIUpgraded { my(@result, $idemtime, $libmtime, $watch, $size); # If we aren't even using SAVI, then obviously we don't want to restart return 0 unless $SAVIinuse; #@result = stat(MailScanner::Config::Value('sophoside') || # '/usr/local/Sophos/ide'); #$idemtime = $result[9]; @result = stat(MailScanner::Config::Value('sophoslib') || '/usr/local/Sophos/lib'); $libmtime = $result[9]; #if ($idemtime != $SAVIidedirmtime || $libmtime != $SAVIlibdirmtime) { if ($libmtime != $SAVIlibdirmtime) { MailScanner::Log::InfoLog("Sophos library update detected, " . "resetting SAVI"); return 1; } while (($watch, $size) = each %SAVIwatchfiles) { if ($size != -s $watch) { MailScanner::Log::InfoLog("Sophos update of $watch detected, " . "resetting SAVI"); return 1; } } # No update detected return 0; } # Have the ClamAV database files been modified? (changed size) # If so, abandon this child process altogether and start again. # This is called from the main WorkForHours() loop # sub ClamUpgraded { my($watch, $size); return 0 unless $Claminuse; while (($watch, $size) = each %Clamwatchfiles) { if ($size != -s $watch) { MailScanner::Log::InfoLog("ClamAV update of $watch detected, " . "resetting ClamAV Module"); return 1; } } # No update detected return 0; } # Constructor. sub new { my $type = shift; my $this = {}; #$this->{dir} = shift; bless $this, $type; return $this; } # Do all the commercial virus checking in here. # If 2nd parameter is "disinfect", then we are disinfecting not scanning. sub ScanBatch { my $batch = shift; my $ScanType = shift; my($NumInfections, $success, $id, $BaseDir); my(%Types, %Reports); #%Types = (); # Create the has structure #%Reports = (); # for each one. $NumInfections = 0; $BaseDir = $global::MS->{work}->{dir}; chdir $BaseDir or die "Cannot chdir $BaseDir for virus scanning, $!"; #print STDERR (($ScanType =~ /dis/i)?"Disinfecting":"Scanning") . " using ". # "commercial virus scanners\n"; $success = TryCommercial($batch, '.', $BaseDir, \%Reports, \%Types, \$NumInfections, $ScanType); #print STDERR "Found $NumInfections infections\n"; unless ($success) { # Virus checking the whole batch of messages timed out, so now check them # one at a time to find the one with the DoS attack in it. my $BaseDirH = new DirHandle; MailScanner::Log::WarnLog("Virus Scanning: Denial Of Service attack " . "detected!"); $BaseDirH->open('.') or MailScanner::Log::DieLog("Can't open directory for scanning 1 message, $!"); while(defined($id = $BaseDirH->read())) { next unless -d "$id"; # Only check directories next if $id =~ /^\.+$/; # Don't check myself or my parent next unless MailScanner::Config::Value('virusscan',$batch->{messages}{id}) =~ /1/; # The "./" is important as it gets the path right for parser code $success = TryCommercial($batch, "./$id", $BaseDir, \%Reports, \%Types, \$NumInfections, $ScanType); unless ($success) { # We have found the DoS attack message $Reports{"$id"}{""} .= MailScanner::Config::LanguageValue($batch->{messages}{$id}, 'dosattack') . "\n"; $Types{"$id"}{""} .= "d"; MailScanner::Log::WarnLog("Virus Scanning: Denial Of Service " . "attack is in message %s", $id); # No way here of incrementing the "otherproblems" counter. Ho hum. } } $BaseDirH->close(); } # Add all the %Reports and %Types to the message batch fields MergeReports(\%Reports, \%Types, $batch); # Return value is the number of infections we found #print STDERR "Found $NumInfections infections!\n"; return $NumInfections; } # Merge all the virus reports and types into the properties of the # messages in the batch. Doing this separately saves me changing # the code of all the parsers to support the new OO structure. # If we have at least 1 report for a message, and the "silent viruses" list # includes the special keyword "All-Viruses" then mark the message as silent # right now. sub MergeReports { my($Reports, $Types, $batch) = @_; my($id, $reports, $attachment, $text); my($cachedid, $cachedsilentflag); my(%seenbefore); # Let's do all the reports first... $cachedid = 'uninitialised'; while (($id, $reports) = each %$Reports) { #print STDERR "Report merging for \"$id\" and \"$reports\"\n"; next unless $id && $reports; my $message = $batch->{messages}{"$id"}; # Skip this message if we didn't actually want it to be scanned. next unless MailScanner::Config::Value('virusscan', $message) =~ /1/; #print STDERR "Message is $message\n"; $message->{virusinfected} = 1; # If the cached message id matches the current one, we are working on # the same message as last time, so don't re-fetch the silent viruses # list for this message. if ($cachedid ne $id) { my $silentlist = ' ' . MailScanner::Config::Value('silentviruses', $message) . ' '; $cachedsilentflag = ($silentlist =~ / all-viruses /i)?1:0; $cachedid = $id; } # We can't be here unless there was a virus report for this message $message->{silent} = 1 if $cachedsilentflag; while (($attachment, $text) = each %$reports) { #print STDERR "\tattachment \"$attachment\" has text \"$text\"\n"; #print STDERR "\tEntity of \"$attachment\" is \"" . $message->{file2entity} . "\"\n"; next unless $text; # Sanitise the reports a bit $text =~ s/\s{20,}/ /g; $message->{virusreports}{"$attachment"} .= $text; } unless ($seenbefore{$id}) { MailScanner::Log::NoticeLog("Infected message %s came from %s", $id, $message->{clientip}); $seenbefore{$id} = 1; } } # And then all the report types... while (($id, $reports) = each %$Types) { next unless $id && $reports; my $message = $batch->{messages}{"$id"}; while (($attachment, $text) = each %$reports) { next unless $text; $message->{virustypes}{"$attachment"} .= $text; } } } # Try all the installed commercial virus scanners # We are passed the directory to start scanning from, # the message batch we are scanning, # a ref to the infections counter. # $ScanType can be one of "scan", "rescan", "disinfect". sub TryCommercial { my($batch, $dir, $BaseDir, $Reports, $Types, $rCounter, $ScanType) = @_; my($scanner, @scanners, $disinfect, $result, $counter); my($logtitle); # If we aren't virus scanning *anything* then don't call the scanner return 1 if MailScanner::Config::IsSimpleValue('virusscan') && !MailScanner::Config::Value('virusscan'); # $scannerlist is now a global for this file. If it was set to "auto" # then I will have searched for all the scanners that appear to be # installed. So by the time we get here, it should never be "auto" either. # Unless of course they really have no scanners installed at all! #$scannerlist = MailScanner::Config::Value('virusscanners'); $scannerlist =~ tr/,//d; $scannerlist = "none" unless $scannerlist; # Catch empty setting @scanners = split(" ", $scannerlist); $counter = 0; # Change actions and outputs depending on what we are trying to do $disinfect = 0; $disinfect = 1 if $ScanType !~ /scan/i; $logtitle = "Virus Scanning"; $logtitle = "Virus Re-scanning" if $ScanType =~ /re/i; # Rescanning $logtitle = "Disinfection" if $ScanType =~ /dis/i; # Disinfection foreach $scanner (@scanners) { $result = TryOneCommercial($scanner, MailScanner::Config::ScannerCmds($scanner), $batch, $dir, $BaseDir, $Reports, $Types, $rCounter, $disinfect); unless ($result) { MailScanner::Log::WarnLog("%s: Failed to complete, timed out", $scanner); return 0; } $counter += $result; MailScanner::Log::NoticeLog("%s: %s found %d infections", $logtitle, $Scanners{$scanner}{Name}, $$rCounter) if $$rCounter; } return $counter; } # Try one of the commercial virus scanners sub TryOneCommercial { my($scanner, $sweepcommandAndPath, $batch, $subdir, $BaseDir, $Reports, $Types, $rCounter, $disinfect) = @_; my($sweepcommand, $instdir); my($rScanner, $VirusLock, $voptions, $Name); my($Counter, $TimedOut, $PipeReturn, $pid); MailScanner::Log::DieLog("Virus scanner \"%s\" not found " . "in virus.scanners.conf file. Please check your " . "spelling in \"Virus Scanners =\" line of " . "MailScanner.conf", $scanner) if $sweepcommandAndPath eq ""; # Split the sweepcommandAndPath into its 2 elements $sweepcommandAndPath =~ /^([^,\s]+)[,\s]+([^,\s]+)$/ or MailScanner::Log::DieLog("Your virus.scanners.conf file does not " . " have 3 words on each line. See if you " . " have an old one left over by mistake."); ($sweepcommand, $instdir) = ($1, $2); MailScanner::Log::DieLog("Never heard of scanner '$scanner'!") unless $sweepcommand; $rScanner = $Scanners{$scanner}; # If they want the scanner name, then set it to non-blank $Name = ""; $Name = $rScanner->{"Name"} if MailScanner::Config::Value('showscanner'); if ($rScanner->{"SupportScanning"} == $S_NONE){ MailScanner::Log::DebugLog("Scanning using scanner \"$scanner\" " . "not supported; not scanning"); return 1; } if ($disinfect && $rScanner->{"SupportDisinfect"} == $S_NONE){ MailScanner::Log::DebugLog("Disinfection using scanner \"$scanner\" " . "not supported; not disinfecting"); return 1; } CheckCodeStatus($rScanner->{$disinfect?"SupportDisinfect":"SupportScanning"}) or MailScanner::Log::DieLog("Bad return code from CheckCodeStatus - " . "should it have quit?"); $VirusLock = MailScanner::Config::Value('lockfiledir') . "/" . $rScanner->{"Lock"}; # lock file $voptions = $rScanner->{"CommonOptions"}; # Set common command line options # Add the configured value for scanner time outs to the command line # if the scanner is Panda $voptions .= " -t:".MailScanner::Config::Value('virusscannertimeout') if $rScanner->{"Name"} eq 'Panda'; # Add command line options to "scan only", or to disinfect $voptions .= " " . $rScanner->{$disinfect?"DisinfectOptions":"ScanOptions"}; &{$$rScanner{"InitParser"}}(); # Initialise scanner-specific parser my $Lock = new FileHandle; my $Kid = new FileHandle; my $pipe; # Check that the virus checker files aren't currently being updated, # and wait if they are. if (open($Lock, ">$VirusLock")) { print $Lock "Virus checker locked for " . ($disinfect?"disinfect":"scann") . "ing by $scanner $$\n"; } else { #The lock file already exists, so just open for reading open($Lock, "<$VirusLock") or MailScanner::Log::WarnLog("Cannot lock $VirusLock, $!"); } flock($Lock, $LOCK_SH); MailScanner::Log::DebugLog("Commencing " . ($disinfect?"disinfect":"scann") . "ing by $scanner..."); $TimedOut = 0; eval { $pipe = $disinfect?'|-':'-|'; die "Can't fork: $!" unless defined($pid = open($Kid, $pipe)); if ($pid) { # In the parent local $SIG{ALRM} = sub { $TimedOut = 1; die "Command Timed Out" }; alarm MailScanner::Config::Value('virusscannertimeout'); $ScannerPID = $pid; # Only process the output if we are scanning, not disinfecting if ($disinfect) { # Tell sweep to disinfect all files print $Kid "A\n" if $scanner eq 'sophos'; #print STDERR "Disinfecting...\n"; } else { while(<$Kid>) { # Note: this is a change in the spec for all the parsers $Counter += &{$$rScanner{"ProcessOutput"}}($_, $Reports, $Types, $BaseDir, $Name); #print STDERR "Processing line \"$_\" produced $Counter\n"; } } close $Kid; $PipeReturn = $?; $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(); if ($scanner eq 'sophossavi') { SophosSAVI($subdir, $disinfect); exit; } elsif ($scanner eq 'clamavmodule') { ClamAVModule($subdir, $disinfect, $batch); exit; } else { exec "$sweepcommand $instdir $voptions $subdir"; MailScanner::Log::WarnLog("Can't run commercial checker $scanner " . "(\"$sweepcommand\"): $!"); exit 1; } } }; alarm 0; # 2.53 # Note to self: I only close the KID in the parent, not in the child. MailScanner::Log::DebugLog("Completed scanning by $scanner"); $ScannerPID = 0; # Not running a scanner any more # Catch failures other than the alarm MailScanner::Log::DieLog("Commercial virus checker failed with real error: $@") if $@ and $@ !~ /Command Timed Out|[sS]yslog/; #print STDERR "pid = $pid and \@ = $@\n"; # 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 } } flock($Lock, $LOCK_UN); close $Lock; # Use the maximum value of all the numbers of viruses found by each of # the virus scanners. This should hopefully reflect the real number of # viruses in the messages, in the case where all of them spot something, # but only a subset spot more/all of the viruses. # Viruses = viruses or phishing attacks in the case of ClamAV. $$rCounter = $Counter if $Counter>$$rCounter; # Set up output value # Return failure if the command timed out, otherwise return success MailScanner::Log::WarnLog("Commercial scanner $scanner timed out!") if $TimedOut; return 0 if $TimedOut; return 1; } # Use the ClamAV module (already initialised) to scan the contents of # a directory. Outputs in a very simple format that ProcessClamAVModOutput() # expects. 3 output fields separated by ":: ". sub ClamAVModule { my($dirname, $disinfect, $messagebatch) = @_; my($dir, $child, $childname, $filename, $results, $virus); # Cannot disinfect yet #if ($disinfect) { # # Enable the disinfection options # ; #} else { # # Disable the disinfection options # ; #} # Do we have an unrar on the path? my $unrar = MailScanner::Config::Value('unrarcommand'); MailScanner::Log::WarnLog("Unrar command %s does not exist or is not " . "executable, please either install it or remove the setting from " . "MailScanner.conf", $unrar) unless $unrar eq "" || -x $unrar; my $haverar = 1 if $unrar && -x $unrar; $| = 1; $dir = new DirHandle; $child = new DirHandle; $dir->open($dirname) or MailScanner::Log::DieLog("Can't open directory %s for scanning, %s", $dirname, $!); # Find all the subdirectories while($childname = $dir->read()) { next unless -d "$dirname/$childname"; # Only search subdirs next if $childname eq '.' || $childname eq '..'; $child->open("$dirname/$childname") or MailScanner::Log::DieLog("Can't open directory %s for scanning, %s", "$dirname/$childname", $!); # Scan all the files in the subdirectory # check to see if rar is available. If it is we don't want to # have clamav check for password protected since that has already # been done and will be reported correctly # if we are not allowing password protected archives and do not have rar # then have clamav check for password protected archives but it will # be reported as a virus (at least it will block passworded rar files) while($filename = $child->read()) { next unless -f "$dirname/$childname/$filename"; # Only check files if (MailScanner::Config::Value('allowpasszips', $messagebatch->{messages}{$childname})) { # || $haverar) { $results = $Clam->scan("$dirname/$childname/$filename", Mail::ClamAV::CL_SCAN_STDOPT() | Mail::ClamAV::CL_SCAN_ARCHIVE() | Mail::ClamAV::CL_SCAN_PE() | Mail::ClamAV::CL_SCAN_BLOCKBROKEN() | Mail::ClamAV::CL_SCAN_OLE2()); } else { $results = $Clam->scan("$dirname/$childname/$filename", Mail::ClamAV::CL_SCAN_STDOPT() | Mail::ClamAV::CL_SCAN_ARCHIVE() | Mail::ClamAV::CL_SCAN_PE() | Mail::ClamAV::CL_SCAN_BLOCKBROKEN() | # Let MS find these: #Mail::ClamAV::CL_SCAN_BLOCKENCRYPTED() | Mail::ClamAV::CL_SCAN_OLE2()); } unless ($results) { print "ERROR:: $results" . ":: $dirname/$childname/$filename\n"; next; } if ($results->virus) { print "INFECTED::"; print " $results" . ":: $dirname/$childname/$filename\n"; } else { print "CLEAN:: :: $dirname/$childname/$filename\n"; } } $child->close; } $dir->close; } # Use the Sophos SAVI library (already initialised) to scan the contents of # a directory. Outputs in a very simple format that ProcessSophosSAVIOutput() # expects. 3 output fields separated by ":: ". sub SophosSAVI { my($dirname, $disinfect) = @_; my($dir, $child, $childname, $filename, $results, $virus); # Cannot disinfect yet #if ($disinfect) { # # Enable the disinfection options # ; #} else { # # Disable the disinfection options # ; #} $| = 1; $dir = new DirHandle; $child = new DirHandle; $dir->open($dirname) or MailScanner::Log::DieLog("Can't open directory %s for scanning, %s", $dirname, $!); # Find all the subdirectories while($childname = $dir->read()) { next unless -d "$dirname/$childname"; # Only search subdirs next if $childname eq '.' || $childname eq '..'; $child->open("$dirname/$childname") or MailScanner::Log::DieLog("Can't open directory %s for scanning, %s", "$dirname/$childname", $!); # Scan all the files in the subdirectory while($filename = $child->read()) { next unless -f "$dirname/$childname/$filename"; # Only check files $results = $SAVI->scan("$dirname/$childname/$filename"); unless (ref $results) { print "ERROR:: " . $SAVI->error_string($results) . " ($results):: " . "$dirname/$childname/$filename\n"; next; } if ($results->infected) { print "INFECTED::"; foreach $virus ($results->viruses) { print " $virus"; } print ":: $dirname/$childname/$filename\n"; } else { print "CLEAN:: :: $dirname/$childname/$filename\n"; } } $child->close; } $dir->close; } # Initialise any state variables the Generic output parser uses sub InitGenericParser { ; } # Initialise any state variables the Sophos SAVI output parser uses sub InitSophosSAVIParser { ; } # Initialise any state variables the Sophos output parser uses sub InitSophosParser { ; } # Initialise any state variables the McAfee output parser uses my($currentline); sub InitMcAfeeParser { $currentline = ''; } # Initialise any state variables the Command (CSAV) output parser uses sub InitCommandParser { ; } # Initialise any state variables the Inoculate-IT output parser uses sub InitInoculateParser { ; } # Initialise any state variables the Inoculan 4.x output parser uses sub InitInoculanParser { ; } # Initialise any state variables the Kaspersky 4.5 output parser uses my ($kaspersky_4_5Version); sub InitKaspersky_4_5Parser { $kaspersky_4_5Version = 0; } # Initialise any state variables the Kaspersky output parser uses my ($kaspersky_CurrentObject); sub InitKasperskyParser { $kaspersky_CurrentObject = ""; } # Initialise any state variables the Kaspersky Daemon Client output parser uses sub InitKavDaemonClientParser { ; } # Initialise any state variables the F-Secure output parser uses my ($fsecure_InHeader, $fsecure_Version, %fsecure_Seen); sub InitFSecureParser { $fsecure_InHeader=(-1); $fsecure_Version = 0; %fsecure_Seen = (); } # Initialise any state variables the F-Prot output parser uses my ($fprot_InCruft); sub InitFProtParser { $fprot_InCruft=(-3); } # Initialise any state variables the Nod32 output parser uses my ($NOD32Version, $NOD32InHeading); sub InitNOD32Parser { $NOD32Version = undef; $NOD32InHeading = 1; } # Initialise any state variables the Nod32 1.99 and above output parser uses sub InitNOD32199Parser { $NOD32Version = undef; $NOD32InHeading = 2; } # Initialise any state variables the AntiVir output parser uses sub InitAntiVirParser { ; } # Initialise any state variables the Panda output parser uses sub InitPandaParser { ; } # Initialise any state variables the RAV output parser uses sub InitRavParser { ; } # Initialise any state variables the ClamAV output parser uses my ($clamav_archive, $qmclamav_archive); sub InitClamAVParser { $clamav_archive = ""; $qmclamav_archive = ""; } # Initialise any state variables the ClamAV Module output parser uses sub InitClamAVModParser { ; } # Initialise any state variables the Vscan output parser uses my ($trend_prevline); sub InitTrendParser { $trend_prevline = ""; } # Initialise any state variables the Bitdefender output parser uses sub InitBitdefenderParser { ; } # Initialise any state variables the DrWeb output parser uses sub InitDrwebParser { ; } # Initialise any state variables the Norman output parser uses sub InitNormanParser { ; } # Initialise any state variables the Symantec output parser uses my ($css_filename, $css_infected); sub InitCSSParser { $css_filename=""; $css_infected=""; } # Initialise any state variables the AVG output parser uses sub InitAvgParser { ; } # Initialise any state variables the Vexira output parser uses my($VexiraPathname); sub InitVexiraParser { $VexiraPathname = ''; } # Initialise any state variables the ScanEngine output parser uses sub InitSymScanEngineParser { ; } # Initialise any state variables the Avast output parser uses sub InitAvastParser { ; } # Initialise any state variables the Avastd output parser uses sub InitAvastdParser { ; } # These functions must be called with, in order: # * The line of output from the scanner # * The MessageBatch object the reports are written to # * The base directory in which we are working. # # The base directory must contain subdirectories named # per message ID, and must have no trailing slash. # # # These functions must return with: # * return code 0 if no problem, 1 if problem. # * type of problem (currently only "v" for virus) # appended to $types{messageid}{messagepartname} # * problem report from scanner appended to # $infections{messageid}{messagepartname} # -- NOTE: Don't forget the terminating newline. # # If the scanner may refer to the same file multiple times, # you should consider appending to the $infections rather # than just setting it, I guess. # sub ProcessClamAVModOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logout, $keyword, $virusname, $filename); my($dot, $id, $part, @rest, $report); chomp $line; $logout = $line; $logout =~ s/\s{20,}/ /g; #$logout =~ s/%/%%/g; #print STDERR "Output is \"$logout\"\n"; ($keyword, $virusname, $filename) = split(/:: /, $line, 3); if ($keyword =~ /^error/i && $logout !~ /rar module failure/i) { MailScanner::Log::InfoLog("ClamAVModule::%s", $logout); return 1; } elsif ($keyword =~ /^info/i || $logout =~ /rar module failure/i) { return 0; } elsif ($keyword =~ /^clean/i) { return 0; } else { # Must be an infection reports MailScanner::Log::InfoLog("ClamAVModule::%s", $logout); ($dot, $id, $part, @rest) = split(/\//, $filename); $report = $Name . ': ' if $Name; $infections->{"$id"}{"$part"} .= "$report$part was infected: $virusname\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } } sub ProcessGenericOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logout, $keyword, $virusname, $filename); my($id, $part, @rest, $report); chomp $line; $logout = $line; $logout =~ s/\s{20,}/ /g; ($keyword, $virusname, $filename) = split(/::/, $line, 3); MailScanner::Log::InfoLog("GenericScanner::%s", $logout); return 1 if $keyword =~ /^error/i; return 0 if $keyword =~ /^clean|^info/i; # Must be an infection report ($id, $part, @rest) = split(/\//, $filename); $report = $Name . ': ' if $Name; $infections->{"$id"}{"$part"} .= "$report$part was infected by $virusname\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } sub ProcessSophosSAVIOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logout, $keyword, $virusname, $filename); my($dot, $id, $part, @rest, $report); chomp $line; $logout = $line; $logout =~ s/\s{20,}/ /g; #$logout =~ s/%/%%/g; ($keyword, $virusname, $filename) = split(/:: /, $line, 3); if ($keyword =~ /^error/i) { ($dot, $id, $part, @rest) = split(/\//, $filename); $report = $Name . ': ' if $Name; # Allow any error messages that are mentioned in the # Allowed Sophos Error Messages option. my($errorlist, @errorlist, @errorregexps, $choice); $errorlist = MailScanner::Config::Value('sophosallowederrors'); $errorlist =~ s/^\"(.+)\"$/$1/; # Remove leading and trailing quotes @errorlist = split(/\"\s*,\s*\"/, $errorlist); # Split up the list foreach $choice (@errorlist) { push @errorregexps, quotemeta($choice) if $choice =~ /[^\s]/; } $errorlist = join('|',@errorregexps); # Turn into 1 big regexp if ($errorlist ne "" && $virusname =~ /$errorlist/) { MailScanner::Log::WarnLog("Ignored SophosSAVI '%s' error in %s", $virusname, $id); return 0; } else { MailScanner::Log::InfoLog("SophosSAVI::%s", $logout); $infections->{"$id"}{"$part"} .= "$report$part caused an error: $virusname\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } } elsif ($keyword =~ /^info/i) { return 0; } elsif ($keyword =~ /^clean/i) { return 0; } else { # Must be an infection reports MailScanner::Log::InfoLog("SophosSAVI::%s", $logout); ($dot, $id, $part, @rest) = split(/\//, $filename); $report = $Name . ': ' if $Name; $infections->{"$id"}{"$part"} .= "$report$part was infected by $virusname\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } } sub ProcessSophosOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest, $error); my($logout); #print "$line"; chomp $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout) if $line =~ /error/i; # JKF Improved to handle multi-part split archives, # JKF which Sophos whinges about #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.com #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.doc #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.rar/eicar.com #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.rar3a/eicar.doc #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.rar3a/eicar.com #>>> Virus 'EICAR-AV-Test' found in file /root/q/qeicar/eicar.zip/eicar.com return 0 unless $line =~ /(virus.*found)|(could not check)|(password[\s-]*protected)/i; MailScanner::Log::InfoLog($logout); $report = $line; $infected = $line; $infected =~ s/^.*found\s*in\s*file\s*//i; # Catch the extra stuff on the end of the line as well as the start $infected =~ s/^Could not check\s*(.+) \(([^)]+)\)$/$1/i; #print STDERR "Infected = \"$infected\"\n"; $error = $2; #print STDERR "Error = \"$error\"\n"; if ($error eq "") { $error = "Password protected file" if $infected =~ s/^Password[ -]*protected\s+file\s+(.+)$/$1/i; #print STDERR "Error 2 = \"$error\"\n"; } # If the error is one of the allowed errors, then don't report any # infections on this file. if ($error ne "") { # Treat their string as a command-separated list of strings, each of # which is in quotes. Any of the strings given may match. # If there are no quotes, then there is only 1 string (for backward # compatibility). my($errorlist, @errorlist, @errorregexps, $choice); $errorlist = MailScanner::Config::Value('sophosallowederrors'); $errorlist =~ s/^\"(.+)\"$/$1/; # Remove leading and trailing quotes @errorlist = split(/\"\s*,\s*\"/, $errorlist); # Split up the list foreach $choice (@errorlist) { push @errorregexps, quotemeta($choice) if $choice =~ /[^\s]/; } $errorlist = join('|',@errorregexps); # Turn into 1 big regexp if ($errorlist ne "" && $error =~ /$errorlist/) { MailScanner::Log::WarnLog("Ignored Sophos '%s' error", $error); return 0; } } #$infected =~ s/^Could not check\s*//i; # JKF 10/08/2000 Used to split into max 3 parts, but this doesn't handle # viruses in zip files in attachments. Now pull out first 3 parts instead. ($dot, $id, $part, @rest) = split(/\//, $infected); #system("echo $dot, $id, $part, @rest >> /tmp/jkf"); #system("echo $infections >> /tmp/jkf"); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } sub ProcessMcAfeeOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($lastline, $report, $dot, $id, $part, @rest); my($logout); chomp $line; $lastline = $currentline; $currentline = $line; #MailScanner::Log::InfoLog("McAfee said \"$line\""); # SEP: need to add code to log warnings return 0 unless $line =~ /Found/; # McAfee prints the whole path as opposed to # ./messages/part so make it the same $lastline =~ s/$BaseDir//; # make an equivalent report line from the last 2 $report = "$lastline$currentline"; $logout = $report; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); # note: '$dot' does not become '.' ($dot, $id, $part, @rest) = split(/\//, $lastline); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; return 1; } # This next function originally contributed in its entirety by # "Richard Brookhuis" # # ./gBJNiNQG014777/eicar.zip->eicar.com is what a zip file looks like. sub ProcessCommandOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; #my($line) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); #print "$line"; chomp $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout) if $line =~ /error/i; if ($line =~ /(is|could be) a (security risk|virus construction|joke program)/) { # Reparse the rest of the line to turn it into an infection report $line =~ s/(is|could be) a (security risk|virus construction|joke program).*$/Infection: /; } return 0 unless $line =~ /Infection:/i; MailScanner::Log::InfoLog($logout); $report = $line; $infected = $line; $infected =~ s/\s+Infection:.*$//i; # JKF 10/08/2000 Used to split into max 3 parts, but this doesn't handle # viruses in zip files in attachments. Now pull out first 3 parts instead. $infected =~ s/-\>/\//; # JKF Handle archives rather better ($dot, $id, $part, @rest) = split(/\//, $infected); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus #print "ID: $id PART: $part REPORT: $report\n"; return 1; } # This next function contributed in its entirety by # # sub ProcessInoculateOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); #print "$line"; chomp $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout) if $line =~ /error/i; #JKF MailScanner::Log::WarnLog($line) if $line =~ /Error/i; return 0 unless $line =~ /is infected by virus:/i; MailScanner::Log::InfoLog($logout); # Ino prints the whole path as opposed to # ./messages/part so make it the same # Scott Farrell's system definitely requires the extra / # Output looks like this: # File: /var/spool/MailScanner/incoming/./message-id/filename $line =~ s/$BaseDir\///; # ino uses instead of /files.ext/ in archives $line =~ s//\//; $report = $line; $infected = $line; # $infected =~ s/^.*found\s*in\s*file\s*//i; # Next 2 lines added on advice from . $infected =~ s/File //; $infected =~ s/ is infected by virus:.*//; # JKF 10/08/2000 Used to split into max 3 parts, but this doesn't handle # viruses in zip files in attachments. Now pull out first 3 parts instead. ($dot, $id, $part, @rest) = split(/\//, $infected); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # Inoculan 4.x parser, contributed in its entirety by # # Comment from : # This next function is the modified version of sfarrell@icconsulting.com.au's # inoculateit 6.0 section by gabor.funk@hunetkft.hu - 2002.03.01 - v1.0 # It works with Inoculan 4.x inocucmd which is a beta/test/unsupported version # Can be downloaded from: ftp://ftp.ca.com/getbbs/linux.eng/inoctar.LINUX.Z # This package is rarely modified but you can download virsig.dat from other # 4.x package such as the NetWare package (smallest and non MS compressed) # It can be found at: ftp://ftp.ca.com/pub/InocuLAN/il0156.zip # wget it; unzip il0156.zip VIRSIG.DAT; mv VIRSIG.DAT virsig.dat # and since the last engine was "corrected" not to accept newer signature # files, you have to patch the major version number to the same or below as # the one which come with the inoctar.LINUX.Z (currently 34.19) otherwise # it would refuse to run and misleadingly report the following: # "Error during Initialization. Please check configuration." # In virsig.dat the major version number is located at address 10h, for # virsig.dat version 35.15 this would be 35h. You simply have to change it # to 34h and it should work. Note: using a higher version virsig.dat with a # lower version engine is highly discouraged by CA and can result not to # recognize newer viruses. Automatic procedure for this: Get bview (bvi) from # http://bvi.sourceforge.net, create a file called "patch" containing the # following: "16 c h[LF]34[LF].[LF]w[LF]q[LF]" where [LF] means linefeed # and of course without the quotes. Run "bvi -f patch virsig.dat" to change # major version number automatically to 34 in virsig.dat. # inocucmd needs libstdc++-libc6.1-1.so.2 so you need to link it to your # closest one (it was libstdc++-3-libc6.2-2-2.10.0.so on my debian testing). # location: inocucmd and virsig.dat (the two required files) should be at # /opt/CA, /usr/local/bin or other location specified in $CAIGLBL0000 # test: inocucmd . (inocucmd without argument can report bogus virsig.dat # version number but it's ok if it scans the file with no error) # I like inocucmd because it needs 2 file alltogether, requires no building # and/or "installation" so is very ideal for testing. # # [text updated and expanded at 2002. April 22.] sub ProcessInoculanOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); chomp $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout) if $line =~ /error/i; #JKF MailScanner::Log::WarnLog($line) if $line =~ /Error/i; return 0 unless $line =~ /was infected by virus/i; MailScanner::Log::InfoLog($logout); # Sample outputs for an unpacked and a packed virus # "[././cih-sfl.exe] was infected by virus [Win95/CIH.1003]" # "[././w95.arj:SLIDER10.EXE] was infected by virus [Win95/Slider 1.0.Trojan]" $report = $line; $infected = $line; $infected =~ s/^\[\.\///i; $infected =~ s/([:\]]).*//i; ($dot, $id, $part, @rest) = split(/\//, $infected); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # Kaspersky 4.5 onwards is totally different to its predecessors. # It looks like they finally made a decent interface to it. sub ProcessKaspersky_4_5Output { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logout, $report, $infected, $id, $part, @rest); chomp $line; if (!$kaspersky_4_5Version) { # Version is on a line before any files are scanned $kaspersky_4_5Version = $1 if $line =~ /version\D+([\d.]+)/i; return 0; } return 0 unless $line =~ /\s(INFECTED|SUSPICION)\s/i; $line =~ s/^\[[^\]]+\] //; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); # Sample outputs for an unpacked and a packed virus # /tmp/bernhard/message.zip # /tmp/bernhard/message.zip/message.html INFECTED I-Worm.Mimail.a # /tmp/bernhard/message.html INFECTED I-Worm.Mimail.a $report = $line; # Save a copy $line =~ s/^$BaseDir\///; # Remove basedir/ off the front # Now have id/part followed possibly by /rest $line =~ /^(.+)\s(INFECTED|SUSPICION)\s[^\s]+$/; $infected = $1; ($id, $part, @rest) = split(/\//, $infected); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # If you use Kaspersky, look at this code carefully # and then be very grateful you didn't have to write it. # Note that Kaspersky will now change long paths so they have "..." # in the middle of them, removing the middle of the path. # *WHY* do people have to do dumb things like this? # sub ProcessKasperskyOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; #my($line) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); # Don't know what kaspersky means by "object" yet... # Lose trailing cruft return 0 unless defined $kaspersky_CurrentObject; if ($line =~ /^Current\sobject:\s(.*)$/) { $kaspersky_CurrentObject = $1; } elsif ($kaspersky_CurrentObject eq "") { # Lose leading cruft return 0; } else { chomp $line; $line =~ s/^\r//; # We can rely on BaseDir not having trailing slash. # Prefer s/// to m// as less likely to do unpredictable things. if ($line =~ / infected: /) { $line =~ s/.* \.\.\. (.*)/\.$1/; # Kav will now put ... in long paths $report = $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); $line =~ s/^$BaseDir//; $line =~ s/(.*) infected:.*/\.$1/; # To handle long paths again ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # see commented code below if you think this regexp looks fishy if ($line =~ /^([\r ]*)Scan\sprocess\scompleted\.\s*$/) { undef $kaspersky_CurrentObject; # uncomment this to see just one reason why I hate kaspersky AVP -- nwp # foreach(split //, $1) { # print ord($_) . "\n"; # } } } return 0; } # It uses AvpDaemonClient from /opt/AVP/DaemonClients/Sample # or AvpTeamDream from /opt/AVP/DaemonClients/Sample2. # This was contributed in its entirety by # Nerijus Baliunas . # sub ProcessKavDaemonClientOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; #my($line) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); chomp $line; $line =~ s/^\r//; # We can rely on BaseDir not having trailing slash. # Prefer s/// to m// as less likely to do unpredictable things. if ($line =~ /infected: /) { $line =~ s/.* \.\.\. (.*)/\.$1/; # Kav will now put ... in long paths $report = $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); $line =~ s/^$BaseDir//; $line =~ s/(.*)\sinfected:.*/\.$1/; # To handle long paths again ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } return 0; } # Sample output from version 4.50 of F-Secure: # [./eicar2/eicar.zip] eicar.com: Infected: EICAR-Test-File [AVP] # ./eicar2/eicar.co: Infected: EICAR_Test_File [F-Prot] sub ProcessFSecureOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout, $virus, $BeenSeen); chomp $line; #print STDERR "$line\n"; #print STDERR "InHeader $fsecure_InHeader\n"; #system("echo -n '$line' | od -c"); # Lose header if ($fsecure_InHeader < 0 && $line =~ /version ([\d.]+)/i && !$fsecure_Version) { $fsecure_Version = $1 + 0.0; $fsecure_InHeader -= 2 if $fsecure_Version >= 4.51 && $fsecure_Version < 4.60; #MailScanner::Log::InfoLog("Found F-Secure version $1=$fsecure_Version\n"); return 0; } if ($line eq "") { $fsecure_InHeader++; return 0; } # This test is more vague than it used to be, but is more tolerant to # output changes such as extra headers. Scanning non-scanning data is # not a great idea but causes no harm. $fsecure_InHeader >= 0 or return 0; $report = $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); # If we are running the new version then there's a totally new parser here if ($fsecure_Version >= 4.50) { #./g4UFLJR23090/Keld Jrn Simonsen: Infected: EICAR_Test_File [F-Prot] #./g4UFLJR23090/Keld Jrn Simonsen: Infected: EICAR-Test-File [AVP] #./g4UFLJR23090/cokegift.exe: Infected: is a joke program [F-Prot] # Version 4.61: #./eicar.com: Infected: EICAR_Test_File [Libra] #./eicar.com: Infected: EICAR Test File [Orion] #./eicar.com: Infected: EICAR-Test-File [AVP] #./eicar.doc: Infected: EICAR_Test_File [Libra] #./eicar.doc: Infected: EICAR Test File [Orion] #./eicar.doc: Infected: EICAR-Test-File [AVP] #[./eicar.zip] eicar.com: Infected: EICAR_Test_File [Libra] #[./eicar.zip] eicar.com: Infected: EICAR Test File [Orion] #[./eicar.zip] eicar.com: Infected: EICAR-Test-File [AVP] return 0 unless $line =~ /: Infected: /; # The last 3 words are "Infected:" + name of virus + name of scanner $line =~ s/: Infected: +(.+) \[.*?\]$//; #print STDERR "Line is \"$line\"\n"; MailScanner::Log::NoticeLog("Virus Scanning: F-Secure found virus %s", $1); # We are now left with the filename, or # then archive name followed by the filename within the archive. $line =~ s/^\[(.*?)\] .*$/$1/; # Strip signs of an archive # We now just have the filename ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender # Only report results once for each file return 0 if $fsecure_Seen{$line}; $fsecure_Seen{$line} = 1; return 1; } else { # We are running the old version, so use the old parser # Prefer s/// to m// as less likely to do unpredictable things. # We hope. if ($line =~ /\tinfection:\s/) { # Get to relevant filename in a reasonably but not # totally robust manner (*impossible* to be totally robust # if we have square brackets and spaces in filenames) # Strip archive bits if present $line =~ s/^\[(.*?)\] .+(\tinfection:.*)/$1$2/; # Get to the meat or die trying... $line =~ s/\tinfection:([^:]*).*$// or MailScanner::Log::DieLog("Dodgy things going on in F-Secure output:\n$report\n"); $virus = $1; $virus =~ s/^\s*(\S+).*$/$1/; # 1st word after Infection: is the virus MailScanner::Log::NoticeLog("Virus Scanning: F-Secure found virus %s",$virus); ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } MailScanner::Log::DieLog("Either you've found a bug in MailScanner's F-Secure output parser, or F-Secure's output format has changed! Please mail the author of MailScanner!\n"); } } sub ProcessFProtOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; #my($line) = @_; my($report, $infected, $dot, $id, $part, $virus, @rest); my($logout); #print STDERR "$fprot_InCruft $line"; chomp $line; # Look for the "Program version: 4...." line which shows we are running # version 4 and therefore have different headers at the start of the # scan output. if ($fprot_InCruft==-2) { my $version = $1 if $line =~ /program\s+version:\s*([\d.]+)/i; if ($version > 3.12) { $fprot_InCruft -= 1; return 0; } } return 0 if $fprot_InCruft > 0; # Return if we are still in headers # One header paragraph has finished, count it if ($line eq "") { $fprot_InCruft += 1; return 0; } $fprot_InCruft == 0 or return 0; # Prefer s/// to m// as less likely to do unpredictable things. # We hope. # JKF 5+11/1/2002 Make "security risk" and "joke program" lines look like # virus infections for easier parsing. # JKF 25/02/2002 Add all sorts of patterns gleaned from a coredump of F-Prot # JKF 24/07/2002 Reparse the lines to turn them into infection reports # JKF 07/06/2005 Make log output contain the whole path of the file. $report = $line; $report =~ s/^.+\/(.+\/.+$)/\.\/$1/; # New $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); $logout =~ s/^.+\/(.+\/.+$)/\.\/$1/; # New if ($line =~ /(is|could be) a (security risk|virus construction)/) { $line =~ s/(is|could be) a (security risk|virus construction).*$/Infection: /; } if ($line =~ /(is|could be) a mass-mailing worm/) { $line =~ s/(is|could be) a mass-mailing worm.*$/Infection: /; } elsif ($line =~ /(is|could be) a( boot sector)? virus dropper/) { $line =~ s/(is|could be) a( boot sector)? virus dropper.*$/Infection: /; } elsif ($line =~ /(is|could be) a corrupted or intended/) { $line =~ s/(is|could be) a corrupted or intended.*$/Infection: /; } elsif ($line =~ /(is|could be) a (joke|destructive) program/) { $line =~ s/(is|could be) a (joke|destructive) program.*$/Infection: /; } elsif ($line =~ /(is|could be) infected with an unknown virus/) { $line =~ s/(is|could be) infected with an unknown virus.*$/Infection: /; } elsif ($line =~ /(is|could be) a suspicious file/) { $line =~ s/(is|could be) a suspicious file.*$/Infection: /; } elsif ($line =~ /(is|could be) an archive bomb/) { $line =~ s/(is|could be) an archive bomb.*$/Infection: /; } elsif ($line =~ /(could\s*)?contain.*the exploit/i) { $line =~ s/(could\s*)?contains?\s*/Infection: /i; } elsif ($line =~ /contains.*\(non-working\)/) { $line =~ s/contains /Infection: /; #} elsif ($line =~ /[Nn]ot scanned \(encrypted\)/) { # $line =~ s/[Nn]ot scanned \(encrypted\).*$/Infection: /; } if ($line =~ /\s\sInfection:\s/) { # Get to relevant filename in a reasonably but not # totally robust manner (*impossible* to be totally robust # if we have slashes, spaces and "->" in filenames) $line =~ s/^(.*?)->.+(\s\sInfection:.*)/$1$2/; # strip archive bits if present $line =~ s/^.*(\/.*\/.*)\s\sInfection:([^:]*).*$/$1/ # get to the meat or die trying or MailScanner::Log::DieLog("Dodgy things going on in F-Prot output:\n$report\n"); #print STDERR "**$line\n"; $virus = $2; $virus =~ s/^\s*(\S+).*$/$1/; # 1st word after Infection: is the virus MailScanner::Log::NoticeLog("Virus Scanning: F-Prot found virus %s", $virus); ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # Have now seen F-Prot produce infection lines without Infection: in them! # Look for W32 in the last word of the line if ($line =~ /W32\/\S+$/) { # Get to relevant filename in a reasonably but not # totally robust manner (*impossible* to be totally robust # if we have slashes, spaces and "->" in filenames) $line =~ s/^(.*?)->.+(\sW32\/\S+)/$1$2/; # strip archive bits if present $line =~ s/^.*(\/.*\/.*)\s(W32\/\S+)$/$1/ # get to the meat or die trying or MailScanner::Log::DieLog("Dodgy things going on in F-Prot output2:\n$report\n"); #print STDERR "**$line\n"; $virus = $2; MailScanner::Log::NoticeLog("Virus Scanning: F-Prot found problem %s", $virus); ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } # Ignore files we couldn't scan as they were encrypted if ($line =~ /\s\sNot scanned \(unsupported compression method\)/ || $line =~ /\s\sNot scanned \(unknown file format\)/ || $line =~ /[Nn]ot scanned \(encrypted\)/ || $line =~ /Virus-infected files in archives cannot be deleted\./) { return 0; } MailScanner::Log::WarnLog("Either you've found a bug in MailScanner's F-Prot output parser, or F-Prot's output format has changed! F-Prot said this \"$line\". Please mail the author of MailScanner"); return 0; } # This function provided in its entirety by Ing. Juraj Hanták # sub ProcessNOD32Output { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); chomp $line; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::WarnLog($logout) if $line =~ /error/i; # Yet another new NOD32 parser! :-( # This one is for 2.04 in which the output, again, looks totally different # to all the previous versions. if ($line =~ /^object=\"file\",\s*name=\"([^\"]+)\",\s*(virus=\"([^\"]+)\")?/i) { my($fileentry, $virusname) = ($1,$3); $fileentry =~ s/^$BaseDir//; ($dot, $id, $part, @rest) = split(/\//, $fileentry); $part =~ s/^.*\-\> //g; $report = "Found virus $virusname in $part"; $report = $Name . ': '. $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } if (!$NOD32Version && $NOD32InHeading>0 && $line =~ /^NOD32.*Version[^\d]*([\d.]+)/) { $NOD32Version = $1; $NOD32InHeading--; # = 0; return 0; } $NOD32InHeading-- if /^$/; # Was = 0 return 0 unless $line =~ /\s-\s/i; if ($NOD32Version >= 1.990) { # New NOD32 output parser $line =~ /(.*) - (.*)$/; my($file, $virus) = ($1, $2); return 0 if $virus =~ /not an archive file|is OK/; return 0 if $file =~ /^ /; MailScanner::Log::InfoLog("%s", $line); ($dot, $id, $part, @rest) = split(/\//, $file); $report = $line; $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } else { # Pull out the last line of the output text my(@lines); chomp $line; chomp $line; @lines = split(/[\r\n]+/, $line); $line = $lines[$#lines]; #my ($part1,$part2,$part3,$part4,@ostatne)=split(/\.\//,$line); #$line="./".$part4; $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); $report = $line; $infected = $line; $infected =~ s/^.*\s*-\s*//i; # JKF 10/08/2000 Used to split into max 3 parts, but this doesn't handle # viruses in zip files in attachments. Now pull out first 3 parts instead. ($dot, $id, $part, @rest) = split(/[\/,-]/, $report); $part =~ s/\s$//g; $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus return 1; } } # This function originally contributed by Cornelius Kölbel # sub ProcessAntiVirOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; # my($line) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); # From CK's mail: # # checking drive/path (list): /etc/mail # !Virus! /etc/mail/eicar.com Eicar-Test-Signatur (exact) # !Virus! /etc/mail/eicar file.com Eicar-Test-Signatur (exact) # # And I am not very sure, where I should to the sepeartion, if the file # (attachment) has a space in it (as in the second case). # So I took this regular expression: # $part=~ s/^(.*)\s\S*\s\S*$/$1/g; # # Since I asume, that the output has always a column with the Name of the # Virus (Eicar-Test-Signatur) and something, that says "exact". # Open questions: # - Does it *always* say "!Virus!" or can it sometimes say, for example, # "!Trojan!" or "!Joke!"?? # - I am assuming that they are not braindead and therefore never have # spaces in their virus names... # - What does the output of antivir look like when invoked on "." (does # it report relative paths? # # -- nwp 6/5/02 # Now produces output like this: # ALERT: [Eicar-Test-Signatur virus] ./eicar1.zip --> eicar.com <<< Contains code of the Eicar-Test-Signatur virus # ALERT: [Eicar-Test-Signatur virus] ./eicar2.com <<< Contains code of the Eicar-Test-Signatur virus chomp $line; $report = $line; if ($line =~ /.*!Virus!.*/) { $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); ($dot,$id,$part,@rest) = split(/\//, $line); # The Filename is all, except the last two comma seperated elements $part =~ s/^(.*)\s\S*\s\S*$/$1/g; $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender #print STDERR "dot: $dot, id: $id, part: $part, rest: @rest\n"; return 1; #print STDERR "dot: $dot, id: $id, part: $part, rest: @rest\n"; # dot: , id: g28C22m03310, part: eicar.com, rest: } # New output format? if ($line =~ /^ALERT:/) { $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); # Get rid of the virus name $line =~ s/^ALERT: \[[^\]]+\] //; if ($line =~ / --\> .*\<\<\ .*\<\<\<.*$//; } else { # Line describes a normal file $line =~ s/ \<\<\<.*$//; } ($dot,$id,$part,@rest) = split(/\//, $line); chomp $part; #print STDERR "ID = $id and PART = $part\n"; $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; return 1; } # Don't warn any more, new output format includes other gumph we aren't # interested in. #MailScanner::Log::WarnLog("Either you've found a bug in MailScanner's AntiVir output parser, or AntiVir's output format has changed! AntiVir said this \"$line\". Please mail the author of MailScanner"); return 0; } # This function originally contributed by Héctor García Álvarez # # From comment (now removed), it looks to be based on Sophos parser at # some point in its history. # Updated by Rick Cooper 05/10/2005 # sub ProcessPandaOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($numviruses); # Return if there were no viruses found return 0 if $line =~ /^Virus: 0/i; my $ErrStr = ""; $ErrStr = $line if $line =~ /^Panda:ERROR:/; $ErrStr =~ s/^Panda:ERROR:(.+)/$1/ if $ErrStr ne ""; chomp($ErrStr) if $ErrStr ne ""; MailScanner::Log::InfoLog("Panda WARNING: %s",$ErrStr) if $ErrStr ne ""; return 0 if $ErrStr ne ""; # the wrapper returns the information in the following format # EXAMPLE OUTPUT PLEASE? -- nwp 6/5/02 # FOUND: EICAR-AV-TEST-FILE ##::##eicar_com.zip##::##1DVXmB-0006R4-Fv##::##/var/spool/mailscanner/incoming/24686 # Virus Name File Name Message Dir Base Dir my $temp = $line; $numviruses = 0; # If the line is a virus report line parse it # Simple while($temp =~ /\t\tFOUND:(.+?)##::##(.+?)##::##(.+?)##::##(.+?)$/){ $part = $2; $BaseDir = $4; $id = $3; $report = $1; $report =~ s/^\s+|\s+$|\t|\n//g; $report = $report." found in $part"; $report = $Name . ": " . $report if $Name; $report =~ s/\s{2,}/ /g; $part =~ s/^(.+)\-\>(.+)/$2/; MailScanner::Log::InfoLog("%s",$report); $infections->{"$id"}{"$part"} .= "$report\n"; #print STDERR "'$part'\n"; $types->{"$id"}{"$part"} .= "v"; $numviruses++; $temp = $'; } return $numviruses; } # This function originally contributed by Luigino Masarati # Looks like it's based on F-Secure function... # sub ProcessRavOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($report, $infected, $dot, $id, $part, @rest); my($logout); # Sample output: # # [root@pico /tmp]# /usr/local/rav8/ravwrapper --all --mail --archive ./eicar # RAV AntiVirus command line for Linux i686. # Version: 8.3.0. # Copyright (c) 1996-2001 GeCAD The Software Company. All rights reserved. # # Scan engine 8.5 for i386. # Last update: Wed May 1 16:57:02 2002 # Scanning for 66321 malwares (viruses, trojans and worms). # # Scan started on Wed May 8 08:52:55 2002 # # ./eicar/eicarcom2.zip->eicar_com.zip->eicar.com Infected: EICAR_Test_File # ./eicar/eicar.com Infected: EICAR_Test_File # ./eicar/eicar.com.txt Infected: EICAR_Test_File # ./eicar/eicar_com.zip->eicar.com Infected: EICAR_Test_File # # Scan ended on Wed May 8 08:52:55 2002 # # Objects scanned: 7. # Infected: 4. # Warnings: 0. # Time: 0 second(s). # [root@pico /tmp]# #print STDERR ">>$line"; # # This is the original code contributed. It's not perfect. # #chomp $line; #$report = $line; #if ($line =~ /\s+Infected:/i) { # MailScanner::Log::InfoLog($line); # # Get to relevant filename in a reasonably but not # # totally robust manner (*impossible* to be totally robust # # if we have slashes, spaces and "->" in filenames) # $line =~ s/^(.*?)\-\>.+(\s+Infected:.*)/$1$2/; # strip archive bits if present # $line =~ s/^.*(\/.*\/.*)\s+Infected:[^:]*$/$1/ # get to the meat or die trying # or MailScanner::Log::DieLog("Dodgy things going on in Rav output:\n$report\n"); # #print STDERR "**$line\n"; # ($dot,$id,$part,@rest) = split(/\//, $line); # $infections->{"$id"}{"$part"} .= $report . "\n"; # $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender # return 1; #} #return 0; # # This is my rewritten code (JKF). Now supporting RAV officially. # # Syntax of infection report lines is like this: # pathname->zippart\tInfected: virusname # pathname->zippart\tSuspicious: virusname # chomp $line; $report = $line; if ($line =~ /\t+(Infected|Suspicious): /i) { $logout = $line; $logout =~ s/%/%%/g; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog($logout); # Get to relevant filename in a reasonably but not # totally robust manner (*impossible* to be totally robust # if we have slashes, spaces and "->" in filenames) # Strip the infection report off the end, leaves us with the path # and the archive element name $line =~ s/\t(Infected|Suspicious): \S+$//; # Strip any archive elements so we should just have the path and filename $line =~ s/^(.*?)\-\>.*$/$1/; $line =~ /\-\>/ and MailScanner::Log::DieLog("Dodgy things going on in Rav " . "output:\n%s\n", $report); #print STDERR "**$line\n"; ($dot,$id,$part,@rest) = split(/\//, $line); $report = $Name . ': ' . $report if $Name; $infections->{"$id"}{"$part"} .= $report . "\n"; $types->{"$id"}{"$part"} .= "v"; # so we know what to tell sender return 1; } return 0; } # Parse the output of the DrWeb output. # Konrad Madej sub ProcessDrwebOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; return 0 unless $line =~ /^(.+)\s+infected\s+with\s+(.*)$/i; my ($file, $virus) = ($1, $2); my $logout = $line; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog("%s", $logout); # Sample output: # # /tmp/del.com infected with EICAR Test File (NOT a Virus!) # or # >/tmp/del1.com infected with EICAR Test File (NOT a Virus!) # Remove path elements before /./, // if any and # , >, $BaseDir leaving just id/part/rest $file =~ s/\/\.\//\//g; $file =~ s/\/\//\//g; $file =~ s/^>+//g; $file =~ s/^$BaseDir//; $file =~ s/^\///g; my($id, $part, @rest) = split(/\//, $file); #MailScanner::Log::InfoLog("#### $BaseDir - $id - $part"); $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virus in file $part\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender return 1; } # Process ClamAV (v0.22) output # This code contributed in its entirety by # Adrian Bridgett . # Please contact him with any support questions. sub ProcessClamAVOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logline); if ($line =~ /^ERROR:/ or $line =~ /^execv\(p\):/ or $line =~ /^Autodetected \d+ CPUs/) { chomp $line; $logline = $line; $logline =~ s/%/%%/g; $logline =~ s/\s{20,}/ /g; MailScanner::Log::WarnLog($logline); return 0; } # clamscan currently stops as soon as one virus is found # therefore there is little point saying which part # it's still a start mind! # Only tested with --unzip since only windows boxes get viruses ;-) if (/^Archive: (.*)$/) { $clamav_archive = $1; $qmclamav_archive = quotemeta($clamav_archive); return 0; } return 0 if /Empty file\.?$/; # Normally means you just havn't asked for it if (/: (\S+ module failure\.)/) { MailScanner::Log::InfoLog("ProcessClamAVOutput: %s", $1); return 0; } return 0 if /^ |^Extracting|module failure$/; # " inflating", " deflating.." from --unzip if ($clamav_archive ne "" && /^$qmclamav_archive:/) { $clamav_archive = ""; $qmclamav_archive = ""; return 0; } return 0 if /OK$/; $logline = $line; $logline =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog("%s", $logline); #(Real infected archive: /var/spool/MailScanner/incoming/19746/./i75EFmSZ014248/eicar.rar) if (/^\(Real infected archive: (.*)\)$/) { my ($file, $ReportStart); $file = $1; $file =~ s/^(.\/)?$BaseDir\/?//; $file =~ s/^\.\///; my ($id,$part) = split /\//, $file, 2; $ReportStart = $part; $ReportStart = $Name . ': ' . $ReportStart if $Name; $infections->{"$id"}{"$part"} .= "$ReportStart contains a virus\n"; $types->{"$id"}{"$part"} .= "v"; return 1; } if (/^(\(raw\) )?(.*?): (.*) FOUND$/) { my ($file, $subfile, $virus, $report, $ReportStart); $virus = $3; if ($clamav_archive ne "") { $file = $clamav_archive; ($subfile = $2) =~ s/^.*\///; # get basename of file $report = "in $subfile (possibly others)"; } else { $file = $2; } ## If it doesn't start with $BaseDir/./ then it isn't a real report # Don't release this just yet #return 0 unless $file =~ /^\/$BaseDir\/\.\//; $file =~ s/^(.\/)?$BaseDir\/?//; $file =~ s/^\.\///; my ($id,$part) = split /\//, $file, 2; $ReportStart = $part; $ReportStart = $Name . ': ' . $ReportStart if $Name; $infections->{"$id"}{"$part"} .= "$ReportStart contains $virus $report\n"; $types->{"$id"}{"$part"} .= "v"; return 1; } ## If it doesn't start with $BaseDir/./ then it isn't a real report # Don't release this just yet #return 0 unless /^\/$BaseDir\/\.\//; if (/^(.*?): File size limit exceeded\.$/) { return 0; # my ($file, $ReportStart); # $file = $1; # # $file =~ s/^(.\/)?$BaseDir\/?//; # $file =~ s/^\.\///; # my ($id,$part) = split /\//, $file, 2; # $ReportStart = $part; # $ReportStart = $Name . ': ' . $ReportStart if $Name; # $infections->{"$id"}{"$part"} .= "$ReportStart contains dangerous broken zip file\n"; # $types->{"$id"}{"$part"} .= "v"; # return 1; } chomp $line; return 0 if $line =~ /^$/; # Catch blank lines $logline = $line; $logline =~ s/%/%%/g; # This generates so much noise we're better off without it. #MailScanner::Log::WarnLog("ProcessClamAVOutput: unrecognised " . # "line \"$logline\". Please contact the authors!") # unless $logline =~ /Empty.*file/; return 0; } # Parse the output of the Trend VirusWall vscan output. # Contributed in its entirety by Martin Lorensen sub ProcessTrendOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; return if $line =~ /^\s*(=====|Directory:|Searched :|File:|Searched :|Scan :|Infected :|Time:|Start :|Stop :|Used :|Configuration:|$)/; # Next line didn't work with zip (and other) archives #$line =~ y/\t//d and $trend_prevline = $line; $line =~ s/^\t+\././ and $trend_prevline = $line; #MailScanner::Log::InfoLog("%s", $line); # Sample output: # # Scanning 2 messages, 1944 bytes # Virus Scanner v3.1, VSAPI v5.500-0829 # Trend Micro Inc. 1996,1997 # ^IPattern version 329 # ^IPattern number 46849 # Configuration: -e'{* # Directory . # Directory ./g72CdVd6018935 # Directory ./g72CdVd7018935 # ^I./g72CdVd7018935/eicar.com # *** Found virus Eicar_test_file in file /var/spool/MailScanner/incoming_virus/g72CdVd7018935/eicar.com if ( $line =~ /Found virus (\S+) in file/i ) { my($virus ) = $1; # Name of virus found # Unfortunately vscan shows the full filename even though it was given # a relative name to scan. The previous line is relative, though. # So use that instead. my($dot, $id, $part, @rest) = split(/\//, $trend_prevline); $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virus in file $trend_prevline\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender MailScanner::Log::NoticeLog("Trend found %s in %s", $virus, $trend_prevline); return 1; } return 0; } # Parse the output of the Bitdefender bdc output. sub ProcessBitdefenderOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; #print STDERR "$line\n"; return 0 unless $line =~ /\t(infected|suspected): ([^\t]+)$/; my $virus = $2; my $logout = $line; $logout =~ s/\s{20,}/ /g; #print STDERR "virus = \"$virus\"\n"; # strip the base from the message dir and remove the ^I junk $logout =~ s/^.+\/\.\///; # New $logout =~ s/\cI/:/g; # New MailScanner::Log::InfoLog("%s", $logout); # Sample output: # # /var/spool/MailScanner/incoming/1234/./msgid/filename infected: virus # /var/spool/MailScanner/incoming/1234/./msgid/filename=>subpart infected: virus # Remove path elements before /./ leaving just id/part/rest $line =~ s/^.*\/\.\///; my($id, $part, @rest) = split(/\//, $line); $part =~ s/\t.*$//; $part =~ s/=\>.*$//; #print STDERR "id = $id\npart = $part\n"; $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virus in file $part\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender return 1; } # Process Norman virus scanner output # NORMAN #Norman Virus Control Version 5.60.10 Sep 9 2003 12:31:01 #Copyright (c) 1993-2003 Norman ASA # #NSE revision 5.60.13 #nvcbin.def revision 5.60 of 2003/10/03 (49233 variants) #nvcmacro.def revision 5.60 of 2003/09/30 (9514 variants) #Total number of variants: 58747 # #Logging to '/opt/norman/logs/nvc00002.log' #Possible virus in './q11/barendsesaunastoom.doc' -> 'W97M/Verlor.A' #Possible virus in '/root/q/./q4/new : eicar.com' -> 'EICAR_Test_file_not_a_virus!' #Possible virus in '/root/q/./q4/new2 : eicar.com' -> 'EICAR_Test_file_not_a_virus!' #Possible virus in '/root/q/./qeicar/dfgBJNiNQG014777 : eicar.doc' -> 'EICAR_Test_file_not_a_virus!' #Possible virus in '/root/q/./qeicar/message : eicar.com' -> 'EICAR_Test_file_not_a_virus!' sub ProcessNormanOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; #print STDERR "$line\n"; return 0 unless $line =~ /^[^']+'([^']+)' -> '([^']+)'\s*$/; my ($filename, $virus) = ($1, $2); #print STDERR "virus = \"$virus\"\n"; my $logout = $line; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog("%s", $logout); # Remove $BaseDir from front of filename if it's there $filename =~ s/^$BaseDir\///; # Remove the leading './' $filename =~ s/^\.\///; my($id, $part, @rest) = split(/\//, $filename); $part =~ s/ : .*$//; # Remove archive member filename #print STDERR "id = $id\npart = $part\n"; $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virus in file $part\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender return 1; } # Parse Symantec CSS Output. # Written by Martin Foster . # Modified by Kevin Spicer to handle output # of cscmdline. sub ProcessCSSOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($css_virus, $css_report, $logline, $file, $ReportStart); chomp $line; if ($line =~ /^\*\*\*\*\s+ERROR!/ ) { $logline = $line; $logline =~ s/%/%%/g; $logline =~ s/\s{20,}/ /g; MailScanner::Log::WarnLog($logline); return 0; } if ($line =~ /^File:\s+(.*)$/) { $css_filename = $1; $css_infected = ""; return 0; } if ($line =~ /^Infected:\s+(.*)$/) { $css_infected = $1; return 0; } if ($line =~ /^Info:\s+(.*)\s*\(.*\)$/) { $css_virus = $1; # Okay, we have three pieces of information... # $css_filename - the name of the scanned file # $css_infected - the name of the infected file (maybe subpart of # an archive) # $css_virus - virus name etc. # Wipe out the original filename from the infected report $css_infected =~ s/^\Q$css_filename\E(\/)?//; # If anything is left this is a subfile of an archive if ($css_infected ne "") { $css_infected = "in part $css_infected" } $file=$css_filename; $file =~ s/^(.\/)?$BaseDir\/?//; $file =~ s/^\.\///; my ($id,$part) = split /\//, $file, 2; $ReportStart = $part; $ReportStart = $Name . ': ' . $ReportStart if $Name; $infections->{"$id"}{"$part"} .= "$ReportStart contains $css_virus $css_infected\n"; $types->{"$id"}{"$part"} .= "v"; return 1; } # Drop through - weed out known reporting lines if ($line =~ /^Symantec CarrierScan Version/ || $line =~ /^Cscmdline Version/ || $line =~ /^Command Line:/ || $line =~ /^Completed.\s+Directories:/ || $line =~ /^Virus Definitions:/ || $line =~ /^File \[.*\] was infected/ || $line =~ /^Scan (start)|(end):/ || $line =~ /^\s+(Files Scanned:)|(Files Infected:)|(Files Repaired:)|(Errors:)|(Elapsed:)/) { return 0 } return 0 if $line =~ /^$/; # Catch blank lines $logline = $line; $logline =~ s/%/%%/g; MailScanner::Log::WarnLog("ProcessCSSOutput: unrecognised " . "line \"$logline\". Please contact the authors!"); return 0; } # Line: ./gBJNiNQG014777/eicar.doc Virus identified EICAR_Test # Line: ./gBJNiNQG014777/eicar.zip:\eicar.com Virus identified EICAR_Test (+2) sub ProcessAvgOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; # Sample output: #./1B978O-0000g2-Iq/eicar.com Virus identified EICAR_Test (+2) #./1B978O-0000g2-Iq/eicar.zip:\eicar.com Virus identified EICAR_Test (+2) # Remove all the duff carriage-returns from the line $line =~ s/[\r\n]//g; #print STDERR "Line: $line\n"; # Patch supplied by Chris Richardson to fix AVG7 problem # return 0 unless $line =~ /Virus (identified|found) +(.+)$/; return 0 unless $line =~ /(virus.*found)|(trojan.*horse)\s+(.+)$/i; # Patch supplied by Chris Richardson /Virus (identified|found) +(.+)$/; my $virus = $3; #print STDERR "Line: $line\n"; #print STDERR "virus = \"$virus\"\n"; my $logout = $line; $logout =~ s/\s{20,}/ /g; MailScanner::Log::InfoLog("%s", $logout); # Change all the spaces into / for the split coming up # Also the second variant prepends the archive name to the # infected filename with a:\ so we need to change that to # something else. I chose another / so it would end up in the # @rest wich is also why I changed the \s+ to / # then Remove path elements before /./ leaving just id/part/rest $line =~ s/\s+/\//g; $line =~ s/:\\/\//g; $line =~ s/\.\///; my($id, $part, @rest) = split(/\//, $line); $part =~ s/\t.*$//; $part =~ s/=\>.*$//; #print STDERR "id:$id:part = $part\n"; #print STDERR "$Name : Found virus $virus in file $part ID:$id\n"; $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virus in file $part\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender return 1; } sub ProcessVexiraOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; # Interesting output is either a filename starting with ./ or # a virus report starting with whitespace return 0 unless $line =~ /^\.\/|^\s+/; # Is it a filename? if ($line =~ /^\.\//) { $VexiraPathname = $line; return 0; } # Dig the message id and attachment filename out of the VexiraPathname my($dot, $id, $part, @rest, $virusname); ($dot, $id, $part, @rest) = split(/\//, $VexiraPathname); $line =~ s/^\s+//g; $line =~ s/\s+$//g; # virus found: EICAR_test_file ... (NOT killable) skipped #print STDERR "Line is \"$line\"\n"; $virusname = $1 if $line =~ /found:\s+(\S+)\s+\.\.\./i; #print STDERR "Virusname is \"$virusname\"\n"; MailScanner::Log::NoticeLog("Vexira: found %s in %s (%s)", $virusname, $id, $part); #print STDERR "Id is \"$id\"\nPart is \"$part\"\n"; $infections->{$id}{$part} .= $Name . ': ' if $Name; $infections->{$id}{$part} .= "Found virus $virusname in file $part\n"; $types->{$id}{$part} .= "v"; # so we know what to tell sender return 1; } #sub ProcessVexiraOutput { # my($line, $infections, $types, $BaseDir, $Name) = @_; # chomp $line; # # Sample output: # # ALERT: [Eicar-Test-Signatur virus] ./gBJNiNQG014777/eicar.zip --> eicar.com <<< Contains code of the Eicar-Test-Signatur virus # # ALERT: [Eicar-Test-Signatur virus] ./gBJNiNQG014777/eicar.com <<< Contains code of the Eicar-Test-Signatur virus # # ALERT: [Eicar-Test-Signatur virus] ./gBJNiNQG014777/eicar.doc <<< Contains code of the Eicar-Test-Signatur virus # # print STDERR "Line: $line\n"; # return 0 unless $line =~ /^ALERT: \[([^\]]+)\] /; # # my $virus = $1; # print STDERR "Line = $line\n"; # print STDERR "virus = \"$virus\"\n"; # my $logout = $line; # $logout =~ s/\s{20,}/ /g; # MailScanner::Log::InfoLog("%s", $logout); # # # Change all the spaces into / for the split coming up # # Also the second variant prepends the archive name to the # # infected filename with a:\ so we need to change that to # # something else. I chose another / so it would end up in the # # @rest wich is also why I changed the \s+ to / # # then Remove path elements before /./ leaving just id/part/rest # # #$line =~ s/^ALERT: //g; Patch provided by Alex Kerkhove # $line =~ s/^ALERT: //; # $line =~ s/\[.+\] *//g; # $line =~ s/\.\///; # #$line =~ s/^([^\/]+)\///g; Patch provided by Alex Kerkhove # $line =~ s/^([^\/]+)\///; # my $id = $1; # $line =~ /^(.+) <<< (.+)$/; # my $part = $1; # $part =~ s/ -->.*$//g; # print STDERR "id:$id:part = $part\n"; # print STDERR "$Name : Found virus $virus in file $part ID:$id\n"; # $infections->{$id}{$part} .= $Name . ': ' if $Name; # $infections->{$id}{$part} .= "Found $virus in file $part\n"; # $types->{$id}{$part} .= "v"; # so we know what to tell sender # return 1; #} sub ProcessSymScanEngineOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; my($logout, $virusid, $virusname, $filename, $action, $shortname); my($file, @files, $virus_found); my($dot, $id, $part, @rest, $report); chomp $line; # Split all lines that start with a '.' @files=split(/\|\./, $line); $virus_found=0; foreach $file (@files) { $file=~/^(.*) had [0-9]+ infection\(s\):\|File Name:\s+([^\|]*)\|Virus Name:\s+([^\|]*)\|Virus ID:\s+([^\|]*)\|(.*)$/ || next; $filename=".$1"; # We removed the '.', so put it back $shortname=$2; $virusname=$3; $virusid=$4; $action=$5; # Ignore it if it's not a real virus if(int($virusid) < 0) { next; } ($dot, $id, $part, @rest) = split(/\//, $filename); MailScanner::Log::InfoLog("SymantecScanEngine::$shortname $virusname "); $report = $Name . ': ' if $Name; $infections->{"$id"}{"$part"} .= "$report$part was infected: $virusname\n"; $types->{"$id"}{"$part"} .= "v"; # it's a real virus $virus_found=1; } return $virus_found; } sub ProcessAvastOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; #MailScanner::Log::InfoLog("Avast said \"$line\""); # Extract the infection report. Return 0 if it's not there or is OK. return 0 unless $line =~ /\t\[([^[]+)\]$/; my $infection = $1; return 0 if $infection =~ /^OK$/i; MailScanner::Log::InfoLog("%s", $line); # Avast prints the whole path as opposed to # ./messages/part so make it the same $line =~ s/^Archived\s//i; $line =~ s/^$BaseDir//; #my $logout = $line; #$logout =~ s/%/%%/g; #$logout =~ s/\s{20,}/ /g; #$logout =~ s/^\///; #MailScanner::Log::InfoLog("%s found %s", $Name, $logout); # note: '$dot' does not become '.' # This removes the "Archived" bit off the front if present, too :) $line =~ s/\t\[[^[]+\]$//; # Trim the virus report off the end my ($dot, $id, $part, @rest) = split(/\//, $line); #print STDERR "Dot, id, part = \"$dot\", \"$id\", \"$part\"\n"; $infection = $Name . ': ' . $infection if $Name; $infections->{"$id"}{"$part"} .= $infection . "\n"; $types->{"$id"}{"$part"} .= "v"; #print STDERR "Infection = $infection\n"; return 1; } sub ProcessAvastdOutput { my($line, $infections, $types, $BaseDir, $Name) = @_; chomp $line; #MailScanner::Log::InfoLog("Avastd said \"$line\""); # Extract the infection report. Return 0 if it's not there or is OK. return 0 unless $line =~ /\t\[([^[]+)\](\t(.*))?$/; my $result = $1; my $infection = $3; return 0 if $result eq '+'; MailScanner::Log::InfoLog("%s", $line); MailScanner::Log::WarnLog("Avastd scanner found new response type \"%s\", report this to mailscanner\@ecs.soton.ac.uk immediately!", $result) if $result ne 'L'; # Avast prints the whole path as opposed to # ./messages/part so make it the same $line =~ s/^$BaseDir//; #my $logout = $line; #$logout =~ s/%/%%/g; #$logout =~ s/\s{20,}/ /g; #$logout =~ s/^\///; #MailScanner::Log::InfoLog("%s found %s", $Name, $logout); # note: '$dot' does not become '.' # This removes the "Archived" bit off the front if present, too :) $line =~ s/\t\[[^[]+\]\t.*$//; # Trim the virus report off the end my ($dot, $id, $part, @rest) = split(/\//, $line); #print STDERR "Dot, id, part = \"$dot\", \"$id\", \"$part\"\n"; $infection = $Name . ': ' . $infection if $Name; $infections->{"$id"}{"$part"} .= $infection . "\n"; $types->{"$id"}{"$part"} .= "v"; #print STDERR "Infection = $infection\n"; return 1; } # Generate a list of all the virus scanners that are installed. It may # include extras that are not installed in the case where there are # scanners whose name includes a version number and we could not tell # the difference. sub InstalledScanners { my(@installed, $scannername, $nameandpath, $name, $path, $command, $result); # Get list of all the names of the scanners to look up. There are a few # rogue ones! my @scannernames = keys %Scanners; foreach $scannername (@scannernames) { next unless $scannername; next if $scannername =~ /generic|none/i; $nameandpath = MailScanner::Config::ScannerCmds($scannername); ($name, $path) = split(',', $nameandpath); $command = "$name $path -IsItInstalled"; #print STDERR "$command gave: "; $result = system($command) >> 8; #print STDERR "\"$result\"\n"; push @installed, $scannername unless $result; } # Now look for clamavmodule and sophossavi library-based scanners. # Assume they are installed if I can read the code at all. # They over-ride the command-line based versions of the same product. if (eval 'require Mail::ClamAV') { foreach (@installed) { s/^clamav$/clamavmodule/i; } } if (eval 'require SAVI') { foreach (@installed) { s/^sophos$/sophossavi/i; } } #print STDERR "Found list of installed scanners \"" . join(', ', @installed) . "\"\n"; return @installed; } # Should be called when we're about to try to run some code to # scan or disinfect (after checking that code is present). # Nick: I'm not convinced this is really worth the bother, it causes me # quite a lot of work explaining it to people, and I don't think # that the people who should be worrying about this understand # enough about it all to know that they *should* worry about it. sub CheckCodeStatus { my($codestatus) = @_; my($allowedlevel); my $statusname = MailScanner::Config::Value('minimumcodestatus'); $allowedlevel = $S_SUPPORTED; $allowedlevel = $S_BETA if $statusname =~ /^beta/i; $allowedlevel = $S_ALPHA if $statusname =~ /^alpha/i; $allowedlevel = $S_UNSUPPORTED if $statusname =~ /^unsup/i; $allowedlevel = $S_NONE if $statusname =~ /^none/i; return 1 if $codestatus>=$allowedlevel; #MailScanner::Log::WarnLog("Looks like a problem... dumping " . # "status information"); #MailScanner::Log::WarnLog("Minimum acceptable stability = $allowedlevel " . # "($Config::CodeStatus)"); #MailScanner::Log::WarnLog("Using Scanner \"$Config::VirusScanner\""); #foreach (keys %Scanners) { # my $statusinfo = "Scanner \"$_\": scanning code status "; # $statusinfo .= $Scanners{$_}{"SupportScanning"}; # $statusinfo .= " - disinfect code status "; # $statusinfo .= $Scanners{$_}{"SupportDisinfect"}; # MailScanner::Log::WarnLog($statusinfo); #} MailScanner::Log::WarnLog("FATAL: Encountered code that does not meet " . "configured acceptable stability"); MailScanner::Log::DieLog("FATAL: *Please go and READ* " . "http://www.sng.ecs.soton.ac.uk/mailscanner/install/codestatus.shtml" . " as it will tell you what to do."); } 1;