From 889845af246583938245ffc5aad5300f1fac09a6 Mon Sep 17 00:00:00 2001 From: John Peacock Date: Tue, 1 Mar 2005 19:55:18 +0000 Subject: [PATCH] * plugins/virus/clamav Scan temporary file directly now that the spooled file includes the entire message * plugins/virus/bitdefender - John Peacock plugins/virus/hbedv - Hanno Hecker New AV plugins git-svn-id: https://svn.perl.org/qpsmtpd/trunk@377 958fd67b-6ff1-0310-b445-bb7760255be9 --- plugins/virus/bitdefender | 134 +++++++++++++++++++++++++++++++ plugins/virus/clamav | 21 +---- plugins/virus/hbedv | 160 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 17 deletions(-) create mode 100644 plugins/virus/bitdefender create mode 100644 plugins/virus/hbedv diff --git a/plugins/virus/bitdefender b/plugins/virus/bitdefender new file mode 100644 index 0000000..1e96152 --- /dev/null +++ b/plugins/virus/bitdefender @@ -0,0 +1,134 @@ +#!/usr/bin/perl -Tw + +=head1 NAME + +bitdefender -- BitDefender Linux Edition antivirus plugin for qpsmtpd + +=head1 DESCRIPTION + +This plugin scans incoming mail with the BitDefender Linux Edition scanner, +and can at your option reject or flag infected messages. + +=head1 CONFIGURATION + +=over 4 + +=item B + +Full path to the BitDefender binary and all signature files; defaults to +/opt/bdc/bdc. + +=item B + +Whether the scanner will automatically delete messages which have viruses. +Takes either 'yes' or 'no' (defaults to 'yes'). + +=item B + +Maximum size in kilobytes for messages which will be scanned; defaults to 128k; + +=back + +=head1 DEPENDENCIES + +=over 4 + +=item B + +The BitDefender Linux Edition is available to use, free of charge, from +this link: + + + +Please read the documentation for configuring automatic updates of the +virus profiles. + +=back + +=head1 AUTHOR + +John Peacock + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 2004 John Peacock + +Based lightly on the clamav plugin + +This plugin is licensed under the same terms as the qpsmtpd package itself. +Please see the LICENSE file included with qpsmtpd for details. + +=cut + +use File::Path; + +use strict; +use warnings; + +sub register { + my ( $self, $qp, @args ) = @_; + $self->register_hook( "data_post", "bdc_scan" ); + + while (@args) { + $self->{"_bitd"}->{ pop @args } = pop @args; + } + $self->{"_bitd"}->{"bitdefender_location"} ||= "/opt/bdc/bdc"; + $self->{"_bitd"}->{"deny_viruses"} ||= "yes"; + $self->{"_bitd"}->{"max_size"} ||= 128; + $self->{"_bitd"}->{"max_size"} *= 1024; +} + +sub bdc_scan { + my ( $self, $transaction ) = @_; + + if ( $transaction->body_size > $self->{"_bitd"}->{"max_size"} ) { + $self->log( LOGWARN, + 'Mail too large to scan (' + . $transaction->body_size . " vs " + . $self->{"_bitd"}->{"max_size"} + . ")" ); + return (DECLINED); + } + + # Ignore non-multipart emails + my $content_type = $transaction->header->get('Content-Type'); + $content_type =~ s/\s/ /g if defined $content_type; + unless ( $content_type + && $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i ) + { + $self->log( LOGERROR, "non-multipart mail - skipping" ); + return DECLINED; + } + + my $filename = $transaction->body_filename; + unless (defined $filename) { + $self->log(LOGERROR, "didn't get a filename"); + return DECLINED; + } + + # Now do the actual scanning! + open my $bdc, "-|", + $self->{"_bitd"}->{"bitdefender_location"} + . " --mail --all --arc $filename"; + + my $output; + while (<$bdc>) { + if (/infected: (.+)$/) { + $output = $1; + last; + } + } + close $bdc; + + if ($output) { + $self->log( LOGINFO, "Virus(es) found: $output" ); + if ( $self->{"_bitd"}->{"deny_viruses"} eq "yes" ) { + return ( DENY, "Virus Found: $output" ); + } + } + + return (DECLINED); +} + +1; + diff --git a/plugins/virus/clamav b/plugins/virus/clamav index 0507ef8..16f81c0 100644 --- a/plugins/virus/clamav +++ b/plugins/virus/clamav @@ -101,8 +101,6 @@ Please see the LICENSE file included with qpsmtpd for details. =cut -use File::Temp qw(tempfile); - use strict; use warnings; @@ -156,28 +154,18 @@ sub register { sub clam_scan { my ($self, $transaction) = @_; - + if ($transaction->body_size > $self->{_max_size}) { $self->log(LOGWARN, 'Mail too large to scan ('. $transaction->body_size . " vs $self->{_max_size})" ); return (DECLINED); } - my ($temp_fh, $filename) = tempfile("qpsmtpd.clamav.$$.XXXXXX", - DIR => $self->{_spool_dir}); - unless ($temp_fh) { - $self->logerror("Couldn't open tempfile in $self->{_spool_dir}: $!"); + my $filename = $transaction->body_filename; + unless (defined $filename) { + $self->log(LOGERROR, "didn't get a filename"); return DECLINED; } - print $temp_fh "From ", - $transaction->sender->format, " " , scalar gmtime, "\n"; - print $temp_fh $transaction->header->as_string, "\n"; - $transaction->body_resetpos; - while (my $line = $transaction->body_getline) { - print $temp_fh $line; - } - seek($temp_fh, 0, 0); - my $mode = (stat($self->{_spool_dir}))[2]; if ( $mode & 07077 ) { # must be sharing spool directory with external app $self->log(LOGWARN, @@ -195,7 +183,6 @@ sub clam_scan { my $result = ($? >> 8); my $signal = ($? & 127); - unlink($filename); chomp($output); $output =~ s/^.* (.*) FOUND$/$1 /mg; diff --git a/plugins/virus/hbedv b/plugins/virus/hbedv new file mode 100644 index 0000000..108f7cb --- /dev/null +++ b/plugins/virus/hbedv @@ -0,0 +1,160 @@ +#!/usr/bin/perl -w +# H+B EDV-AV plugin. +# + +=head1 NAME + +hbedv - plugin for qpsmtpd which calls the H+BEDV anti virus scanner + +=head1 DESCRIPTION + +The B plugin checks a mail for viruses with the H+BEDV anti virus +scanner (see L for info). It can deny mails if a +virus was found with a configurable deny list. + +=head1 VERSION + +this is B version 1.1 + +=head1 CONFIGURATION + +Add (perl-)regexps to the F configuration file, one per line for the +virii you want to block, e.g.: + + Worm\/Sober\..* + Worm\/NetSky\..* + +or just + + .* + +to block any virus ;) + +Set the location of the binary with + + hbedv hbedvscanner /path/to/antivir + +in the plugin config if qpsmtpd, the location defaults to I. + +=head1 NOTES + +If the hbedv_deny config file is empty or could not be found, any virus +will be blocked. + +This plugin started life as a copy of the B plugin. + +=head1 LICENCE + +Written by Hanno Hecker Ehah@uu-x.deE. + +The B plugin is published under the same licence as qpsmtpd itself. + +=cut + +sub register { + my ($self, $qp, @args) = @_; + $self->register_hook("data_post", "hbedv_scan"); + + if (@args % 2) { + $self->log(LOGERROR, "FATAL ERROR: odd number of arguments"); + exit 3; + } + my %args = @args; + if (!exists $args{hbedvscanner}) { + $self->{_hbedvscan_loc} = "/usr/bin/antivir"; + } else { + if ($args{hbedvscanner} =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/) { + $self->{_hbedvscan_loc} = $1; + } else { + $self->log(LOGERROR, "FATAL ERROR: Unexpected characters in hbedvscanner argument"); + exit 3; + } + } +} + +sub hbedv_scan { + my ($self, $transaction) = @_; + + my $filename = $transaction->body_filename; + unless (defined $filename) { + $self->log(LOGWARN, "didn't get a file name"); + return (DECLINED); + } + + # Now do the actual scanning! + my $cmd = $self->{_hbedvscan_loc}." --archive-max-recursion=50 --alltypes -z -noboot -nombr -rs $filename 2>&1"; + $self->log(LOGDEBUG, "Running: $cmd"); + my @output = `$cmd`; + + my $result = ($? >> 8); + my $signal = ($? & 127); + + chomp(@output); + my @virii = (); + foreach my $line (@output) { + next unless $line =~ /^ALERT: \[([^\]]+)\s+(\w+)?\]/; # $2 =~ /^(virus|worm)$/; + push @virii, $1; + } + @virii = unique(@virii); + + $self->log(LOGDEBUG, "results: ".join("//",@output)); + + if ($signal) { + $self->log(LOGWARN, "scanner exited with signal: $signal"); + return (DECLINED); + } + my $output = join(", ", @virii); + $output = substr($output, 0, 60); + if ($result == 1 || $result == 3) { + $self->log(LOGWARN, "Virus(es) found: $output"); + # return (DENY, "Virus Found: $output"); + # $transaction->header->add('X-Virus-Found', 'Yes', 0); + # $transaction->header->add('X-Virus-Details', $output, 0); + $transaction->header->add('X-H+BEDV-Virus-Found', 'Yes', 0); + $transaction->header->add('X-H+BEDV-Virus-Details', $output, 0); + } + elsif ($result == 200) { + $self->log(LOGWARN, "Program aborted, not enough memory available"); + } + elsif ($result == 211) { + $self->log(LOGWARN, "Programm aborted, because the self check failed"); + } + elsif ($result == 214) { + $self->log(LOGWARN, "License key not found"); + } + elsif ($result) { + $self->log(LOGWARN, "Error: $result, look for exit codes in the output of '" + .$self->{_hbedvscan_loc}." --help' for more info\n"); + } + + # $transaction->header->add('X-Virus-Checked', 'Checked', 0); + $transaction->header->add('X-H+BEDV-Virus-Checked', 'Checked', 0); + return (DECLINED) unless $result; + + if (@virii) { + return(DENY, "Virus found: $output") + unless $self->qp->config("hbedv_deny"); + foreach my $d ($self->qp->config("hbedv_deny")) { + foreach my $v (@virii) { + if ($v =~ /^$d$/i) { + $self->log(LOGWARN, "Denying mail with virus '$v'"); + return(DENY, "Virus found: $output"); + } + } + } + } + return (DECLINED); +} + +sub unique { + ## This is the short version, I haven't tried if any warnings + ## are generated by perl if you use just this... if you need + ## every cpu cycle, try this: + ## my %h;foreach (@_) { ++$h{$_}; }; return keys(%h); + my @list = @_; + my %hash; + foreach my $item (@list) { + exists $hash{$item} || ($hash{$item} = 1); + } + return keys(%hash) +}