From 26de7de964e6222ccb126490d9778291fe35690a Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Sat, 17 Jul 2004 23:31:40 +0000 Subject: [PATCH] Integrate fixes/enhancements from myself and Peter Eisch : - name=value style configuration arguments (old format still supported) - max_size for scan (default 512k) - Pass messages to clamscan in mbox format to satisfy clamdscan - Made detect action configurable (reject or add-header) - Logging fixes - POD git-svn-id: https://svn.perl.org/qpsmtpd/trunk@273 958fd67b-6ff1-0310-b445-bb7760255be9 --- plugins/virus/clamav | 161 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 24 deletions(-) diff --git a/plugins/virus/clamav b/plugins/virus/clamav index 0c6f8e0..ee6e104 100644 --- a/plugins/virus/clamav +++ b/plugins/virus/clamav @@ -1,32 +1,140 @@ -#!/usr/bin/perl -w -# Clam-AV plugin. +#!/usr/bin/perl -Tw + +=head1 NAME + +clamav -- ClamAV antivirus plugin for qpsmtpd + +$Id$ + +=head1 DESCRIPTION + +This plugin scans incoming mail with the clamav A/V scanner, and can at your +option reject or flag infected messages. + +=head1 CONFIGURATION + +Arguments to clamav should be specified in the form of name=value pairs, +separated by whitespace. For sake of backwards compatibility, a single +leading argument containing only alphanumerics, -, _, . and slashes will +be tolerated, and interpreted as the path to clamscan/clamdscan. All +new installations should use the name=value form as follows: + +=over 4 + +=item clamscan_path=I (e.g. I) + +Path to the clamav commandline scanner. Using clamdscan is recommended +for sake of performance. + +Mail will be passed to the clamav scanner in Berkeley mbox format (that is, +with a "From " line). + +=item action=EI | IE (e.g. I) + +Selects an action to take when an inbound message is found to be infected. +Valid arguments are 'add-header' and 'reject'. All rejections are hard +5xx-code rejects; the SMTP error will contain an explanation of the virus +found in the mail (for example, '552 Virus Found: Worm.SomeFool.P'). + +The default action is 'add-header'. + +=item max_size=I (e.g. I) + +Specifies the maximum size, in bytes, for mail to be scanned. Any mail +exceeding this size will be left alone. This is recommended, as large mail +can take an exceedingly long time to scan. The default is 524288, or 512k. + +=item tmp_dir=I (e.g. I) + +Specify an alternate temporary directory. If not specified, the qpsmtpd +I will be used. If neither is available, I<~/tmp/> will be tried, +and if that that fails the plugin will gracefully fail. + +=back + +=head2 CLAMAV CONFIGURATION + +At the least, you should have 'ScanMail' supplied in your clamav.conf file. +It is recommended that you also have sane limits on ArchiveMaxRecursion and +StreamMaxLength also. + +=head1 LICENSE + +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::Temp qw(tempfile); + +use strict; +use warnings; sub register { my ($self, $qp, @args) = @_; - $self->register_hook("data_post", "clam_scan"); + my %args; - if (@args > 0) { - # Untaint scanner location - if ($args[0] =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/) { - $self->{_clamscan_loc} = $1; - } else { - $self->log(LOGERROR, "FATAL ERROR: Unexpected characters in clamav argument 1"); - exit 3; - } - $self->log(LOGWARN, "WARNING: Ignoring additional arguments.") if (@args > 1); - } else { - $self->{_clamscan_loc} = "/usr/local/bin/clamscan"; + if ($args[0] && $args[0] =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/ && -x $1) { + $self->{_clamscan_loc} = $1; + shift @args; } + + for (@args) { + if (/^max_size=(\d+)$/) { + $self->{_max_size} = $1; + } + elsif (/^clamscan_path=(\/[\/\-\_\.a-z0-9A-Z]*)$/) { + $self->{_clamscan_loc} = $1; + } + elsif (/^tmp_dir=(\/[\/\-\_\.a-z0-9A-Z]*)$/) { + $self->{_spool_dir} = $1; + } + elsif (/^action=(add-header|reject)$/) { + $self->{_action} = $1; + } + else { + $self->log(LOGERROR, "Unrecognized argument '$_' to clamav plugin"); + return undef; + } + } + + $self->{_max_size} ||= 512 * 1024; + $self->{_spool_dir} ||= + $self->qp->config('spool_dir') || + Qpsmtpd::Utils::tildeexp('~/tmp/'); + $self->{_spool_dir} = $1 if $self->{_spool_dir} =~ /(.*)/; + + unless ($self->{_spool_dir}) { + $self->log(LOGERROR, "No spool dir configuration found"); + return undef; + } + unless (-d $self->{_spool_dir}) { + $self->log(LOGERROR, "Spool dir $self->{_spool_dir} does not exist"); + return undef; + } + + $self->register_hook("data_post", "clam_scan"); + 1; } sub clam_scan { my ($self, $transaction) = @_; - my ($temp_fh, $filename) = tempfile(); - print $temp_fh $transaction->header->as_string; - print $temp_fh "\n"; + 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}: $!"); + 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; @@ -46,21 +154,26 @@ sub clam_scan { $output =~ s/^.* (.*) FOUND$/$1 /mg; - $self->log(LOGDEBUG, "clamscan results: $output"); + $self->log(LOGINFO, "clamscan results: $output"); if ($signal) { $self->log(LOGINFO, "clamscan exited with signal: $signal"); return (DECLINED); } if ($result == 1) { - $self->log(LOGINFO, "Virus(es) found"); - # return (DENY, "Virus Found: $output"); - $transaction->header->add('X-Virus-Found', 'Yes'); - $transaction->header->add('X-Virus-Details', $output); + $self->log(LOGINFO, "Virus(es) found: $output"); + if ($self->{_action} eq 'add-header') { + $transaction->header->add('X-Virus-Found', 'Yes'); + $transaction->header->add('X-Virus-Details', $output); + } else { + return (DENY, "Virus Found: $output"); + } } elsif ($result) { - $self->log(LOGWARN, "ClamAV error: $result\n"); + $self->log(LOGERROR, "ClamAV error: $cmd: $result\n"); } - $transaction->header->add('X-Virus-Checked', 'Checked'); return (DECLINED); } + +1; +