qpsmtpd/plugins/virus/clamav

238 lines
7.4 KiB
Plaintext
Raw Permalink Normal View History

#!perl -w
=head1 NAME
clamav -- ClamAV antivirus plugin for qpsmtpd
=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<path> (e.g. I<clamscan_path=/usr/bin/clamdscan>)
Path to the clamav commandline scanner. Mail will be passed to the clamav
scanner in Berkeley mbox format (that is, with a "From " line). See the
discussion below on which commandline scanner to use.
=item clamd_conf=I<path> (e.g. I<clamd_conf=/etc/sysconfig/clamd.conf>)
Path to the clamd configuration file. Passed as an argument to the
command-line scanner (--config-file=I<path>).
The default value is '/etc/clamd.conf'.
=item action=E<lt>I<add-header> | I<reject>E<gt> (e.g. I<action=reject>)
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<bytes> (e.g. I<max_size=1048576>)
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<path> (e.g. I<tmp_dir=/tmp>)
Specify an alternate temporary directory. If not specified, the qpsmtpd
I<spool_dir> will be used. If neither is available, I<~/tmp/> will be tried,
and if that that fails the plugin will gracefully fail.
=item back_compat
If you are using a version of ClamAV prior to 0.80, you need to set this
variable to include a couple of now deprecated options.
=back
=head2 CLAMAV COMMAND LINE SCANNER
You can use either clamscan or clamdscan, but the latter is recommended for
sake of performance. However, in this case, the user executing clamd
requires access to the qpsmtpd spool directory, which usually means either
running clamd as the same user as qpsmtpd does (by far the easiest method)
or by doing the following:
=over 4
=item * Change the group ownership of the spool directory to be a group
of which clamav is a member or add clamav to the same group as the qpsmtpd
user.
=item * Enable the "AllowSupplementaryGroups" option in clamd.conf.
=item * Change the permissions of the qpsmtpd spool directory to 0750 (this
will emit a warning when the qpsmtpd service starts up, but can be safely
ignored).
=item * Make sure that all directories above the spool directory (to the
root) are g+x so that the group has directory traversal rights; it is not
necessary for the group to have any read rights except to the spool
directory itself.
=back
It may be helpful to temporary grant the clamav user a shell and test to
make sure you can cd into the spool directory and read files located there.
Remember to remove the shell from the clamav user when you are done
testing.
=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 strict;
use warnings;
POD corrections, additional tests, plugin consistency on files in plugins dir: fixed a number of POD errors formatted some # comments into POD removed bare 1; (these are plugins, not perl modules) most instances of this were copy/pasted from a previous plugin that had it removed instances of # vim ts=N ... they weren't consistent, many didn't match .perltidyrc on modules that failed perl -c tests, added 'use Qpsmtpd::Constants;' Conflicts: plugins/async/check_earlytalker plugins/async/dns_whitelist_soft plugins/async/dnsbl plugins/async/queue/smtp-forward plugins/async/require_resolvable_fromhost plugins/async/rhsbl plugins/async/uribl plugins/auth/auth_checkpassword plugins/auth/auth_cvm_unix_local plugins/auth/auth_flat_file plugins/auth/auth_ldap_bind plugins/auth/auth_vpopmail plugins/auth/auth_vpopmail_sql plugins/auth/authdeny plugins/check_badmailfromto plugins/check_badrcptto_patterns plugins/check_bogus_bounce plugins/check_earlytalker plugins/check_norelay plugins/check_spamhelo plugins/connection_time plugins/dns_whitelist_soft plugins/dnsbl plugins/domainkeys plugins/greylisting plugins/hosts_allow plugins/http_config plugins/logging/adaptive plugins/logging/apache plugins/logging/connection_id plugins/logging/transaction_id plugins/logging/warn plugins/milter plugins/queue/exim-bsmtp plugins/queue/maildir plugins/queue/postfix-queue plugins/queue/smtp-forward plugins/quit_fortune plugins/random_error plugins/rcpt_map plugins/rcpt_regexp plugins/relay_only plugins/require_resolvable_fromhost plugins/rhsbl plugins/sender_permitted_from plugins/spamassassin plugins/tls plugins/tls_cert plugins/uribl plugins/virus/aveclient plugins/virus/bitdefender plugins/virus/clamav plugins/virus/clamdscan plugins/virus/hbedv plugins/virus/kavscanner plugins/virus/klez_filter plugins/virus/sophie plugins/virus/uvscan
2012-04-08 02:11:16 +02:00
use Qpsmtpd::Constants;
sub register {
my ($self, $qp, @args) = @_;
my %args;
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 (/^clamd_conf=(\/[\/\-\_\.a-z0-9A-Z]*)$/) {
$self->{_clamd_conf} = "$1";
}
elsif (/^tmp_dir=(\/[\/\-\_\.a-z0-9A-Z]*)$/) {
$self->{_spool_dir} = $1;
}
elsif (/^action=(add-header|reject)$/) {
$self->{_action} = $1;
}
elsif (/back_compat/) {
$self->{_back_compat} = '-i --max-recursion=50';
}
elsif (/declined_on_fail/) {
$self->{_declined_on_fail} = 1;
}
else {
$self->log(LOGERROR, "Unrecognized argument '$_' to clamav plugin");
return undef;
}
}
$self->{_max_size} ||= 512 * 1024;
$self->{_spool_dir} ||= $self->spool_dir();
$self->{_back_compat} ||= ''; # make sure something is set
$self->{_clamd_conf} ||= '/etc/clamd.conf'; # make sure something is set
$self->{_declined_on_fail} ||= 0; # decline the message on clamav failure
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;
}
}
sub hook_data_post {
my ($self, $transaction) = @_;
if ($transaction->data_size > $self->{_max_size}) {
$self->log(LOGWARN,
'Mail too large to scan ('
. $transaction->data_size
. " vs $self->{_max_size})"
);
return DECLINED;
}
my $filename = $transaction->body_filename;
unless (defined $filename) {
Add plugable logging support include sample plugin which replicates the existing core code. Add OK hook. * lib/Qpsmtpd.pm (init_logger): replaced with log_level() (load_logging): NEW - load logging plugins without calling log() (log_level): NEW - set/get global $LogLevel scalar (log): now just a wrapper for varlog(); called only by core code (varlog): initializes logging if not already done, calls logging plugins in turn and falls back to interal logging unless plugins OK or DECLINED (_load_plugins): only display "Loading plugin" when actually loading one (run_hooks): load logging plugins without calling log(); add OK hook as else of the DENY* case (spool_dir): use global $Spool_dir scalar to cache location * lib/Qpsmtpd/Plugin.pm (%hooks): add "logging" and "ok" (register_hook): add local _hook to object cache (log): call varlog() with additional parameters hook and plugin_name except for logging hook (compile): add accessor sub for local _hook scalar * lib/Qpsmtpd/SMTP.pm (mail, rcpt): change loglevel to LOGALERT instead of LOGWARN for from/to * qpsmtpd-forkserver (REAPER): use package ::log() instead of warn() (main): defer calling log until $plugin_loader has been initialized (log): call logging using the $plugin_loader object * plugins/logging/warn NEW: sample plugin which replicates the core logging functionality * plugins/logging/devnull NEW: sample plugin which logs nothing (for testing multiple logging plugin functionality) * config.sample/logging sample configuration file for logging plugins * plugins/virus/uvscan plugins/virus/clamav Increase loglevel for non-serious warnings to LOGWARN from LOGERROR git-svn-id: https://svn.perl.org/qpsmtpd/trunk@398 958fd67b-6ff1-0310-b445-bb7760255be9
2005-03-24 22:16:35 +01:00
$self->log(LOGWARN, "didn't get a filename");
return DECLINED;
}
my $mode = (stat($self->{_spool_dir}))[2];
if ($mode & 07077) { # must be sharing spool directory with external app
$self->log(LOGWARN,
"Changing permissions on file to permit scanner access");
chmod $mode, $filename;
}
# Now do the actual scanning!
my $cmd =
$self->{_clamscan_loc}
. " --stdout "
. $self->{_back_compat}
. " --config-file="
. $self->{_clamd_conf}
. " --no-summary $filename 2>&1";
$self->log(LOGDEBUG, "Running: $cmd");
my $output = `$cmd`;
my $result = ($? >> 8);
my $signal = ($? & 127);
chomp($output);
$output =~ s/^.* (.*) FOUND$/$1 /mg;
$self->log(LOGINFO, "clamscan results: $output");
if ($signal) {
$self->log(LOGINFO, "clamscan exited with signal: $signal");
return DENYSOFT if !$self->{_declined_on_fail};
return DECLINED;
}
if ($result == 1) {
$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(LOGERROR, "ClamAV error: $cmd: $result\n");
return DENYSOFT if !$self->{_declined_on_fail};
}
else {
$transaction->header->add('X-Virus-Checked',
"Checked by ClamAV on " . $self->qp->config("me"));
}
return DECLINED;
}