From 8c059e38ef1eace7f95bc69a52549c2f6392d1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Wed, 30 Jun 2004 09:25:07 +0000 Subject: [PATCH] add kavscanner plugin (thanks to Hanno Hecker) move clamav, check_for_hi_virus and klez_filter to virus/ (did anyone mentino subversion?) git-svn-id: https://svn.perl.org/qpsmtpd/trunk@255 958fd67b-6ff1-0310-b445-bb7760255be9 --- plugins/virus/check_for_hi_virus | 44 ++++++++ plugins/virus/clamav | 66 ++++++++++++ plugins/virus/kavscanner | 178 +++++++++++++++++++++++++++++++ plugins/virus/klez_filter | 37 +++++++ 4 files changed, 325 insertions(+) create mode 100644 plugins/virus/check_for_hi_virus create mode 100644 plugins/virus/clamav create mode 100644 plugins/virus/kavscanner create mode 100644 plugins/virus/klez_filter diff --git a/plugins/virus/check_for_hi_virus b/plugins/virus/check_for_hi_virus new file mode 100644 index 0000000..bc9601f --- /dev/null +++ b/plugins/virus/check_for_hi_virus @@ -0,0 +1,44 @@ +#!/usr/bin/perl -w + +sub register { + my $self = shift; + $self->register_hook('data_post', 'check_for_hi_virus'); +} + +sub check_for_hi_virus { + my ($self, $transaction) = @_; + + # make sure we read from the beginning; + $transaction->body_resetpos; + + my $line_number = 0; + my $seen_file = 0; + my $ct_filename = ''; + my $cd_filename = ''; + + while ($_ = $transaction->body_getline) { + last if $line_number++ > 40; + if (/^Content-Type: (.*)/) { + my $val = $1; + if ($val =~ /name="(.*)"/) { + $seen_file = 1; + $ct_filename = $1; + } + } + if (/^Content-Disposition: (.*)/) { + my $val = $1; + if ($val =~ /filename="(.*)"/) { + $seen_file = 1; + $cd_filename = $1; + } + } + } + + if ($seen_file and $ct_filename and $cd_filename) { + if ($ct_filename ne $cd_filename) { + return (DENY, "Probably the 'Hi' virus"); + } + } + + return DECLINED; +} diff --git a/plugins/virus/clamav b/plugins/virus/clamav new file mode 100644 index 0000000..0c6f8e0 --- /dev/null +++ b/plugins/virus/clamav @@ -0,0 +1,66 @@ +#!/usr/bin/perl -w +# Clam-AV plugin. + +use File::Temp qw(tempfile); + +sub register { + my ($self, $qp, @args) = @_; + $self->register_hook("data_post", "clam_scan"); + + 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"; + } +} + +sub clam_scan { + my ($self, $transaction) = @_; + + my ($temp_fh, $filename) = tempfile(); + print $temp_fh $transaction->header->as_string; + print $temp_fh "\n"; + $transaction->body_resetpos; + while (my $line = $transaction->body_getline) { + print $temp_fh $line; + } + seek($temp_fh, 0, 0); + + # Now do the actual scanning! + my $cmd = $self->{_clamscan_loc}." --stdout -i --max-recursion=50 --disable-summary $filename 2>&1"; + $self->log(LOGDEBUG, "Running: $cmd"); + my $output = `$cmd`; + + my $result = ($? >> 8); + my $signal = ($? & 127); + + unlink($filename); + chomp($output); + + $output =~ s/^.* (.*) FOUND$/$1 /mg; + + $self->log(LOGDEBUG, "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); + } + elsif ($result) { + $self->log(LOGWARN, "ClamAV error: $result\n"); + } + $transaction->header->add('X-Virus-Checked', 'Checked'); + return (DECLINED); +} diff --git a/plugins/virus/kavscanner b/plugins/virus/kavscanner new file mode 100644 index 0000000..0b56c06 --- /dev/null +++ b/plugins/virus/kavscanner @@ -0,0 +1,178 @@ +#!/usr/bin/perl -w +# Kasperski-AV plugin. + +=head1 NAME + +kavscanner - plugin for qpsmtpd which calls the Kasperski anti virus scanner + +=head1 DESCRIPTION + +Check a mail with the B and deny if it matches a configured virus +list. + +=head1 VERSION + +this is B version 1.0 + +=head1 CONFIGURATION + +Add (perl-)regexps to the F configuration file, one per line for the +virii you want to block, e.g.: + + I-Worm\.Sober\..* + I-Worm\.NetSky\..* + +NOTE: untested and disabled currently, need volunteers :-) + +If this list does not match the virus found in the mail, you may set +I in the plugin config to send a +B to the given mail address, i.e. the line + + kavscanner bcc_virusadmin viradm@your.company.com + +in the F file instead of just + + kavscanner + +Set the location of the binary with + + kavscanner kavscanner_bin /path/to/kavscanner + +(default: F), NOTE: this may be broken, you want to +set B explicitly ;-) + +=head1 NOTES + +This is a merge of the clam_av plugin for qpsmtpd and qmail-scanner-queue.pl +L with my own improvements ;-) +Only tested with kavscanner 4.0.x, and bcc_virusadmin untested, as we have no +use for it currently. I wait for an official change in Qpsmtpd::Transaction +(reset/set the RCPT TO list) to activate and test the currently disabled +B option. + +=cut + +use File::Temp qw(tempfile); +use Mail::Address; + +sub register { + my ($self, $qp, @args) = @_; + $self->register_hook("data_post", "kav_scan"); + + if (@args % 2) { + warn "kavscanner: Wrong number of arguments"; + $self->{_kavscanner_bin} = "/opt/AVP/kavscanner"; + } else { + my %args = @args; + foreach my $key (keys %args) { + my $arg = $key; + $key =~ s/^/_/; + $self->{$key} = $args{$arg}; + } + # Untaint scanner location + if (exists $self->{_kavscanner_bin} && + $self->{_kavscanner_bin} =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/) { + $self->{_kavscanner_bin} = $1; + } else { + $self->log(1, "FATAL ERROR: Unexpected characters in kavscanner argument"); + exit 3; + } + } +} + +sub kav_scan { + my ($self, $transaction) = @_; + + my ($temp_fh, $filename) = tempfile(); + print $temp_fh $transaction->header->as_string; + print $temp_fh "\n"; + $transaction->body_resetpos; + while (my $line = $transaction->body_getline) { + print $temp_fh $line; + } + seek($temp_fh, 0, 0); + + # Now do the actual scanning! + my $cmd = $self->{_kavscanner_bin}." -Y -P -B -MP -MD -* $filename 2>&1"; + $self->log(1, "Running: $cmd"); + my @output = `$cmd`; + chomp(@output); + + my $result = ($? >> 8); + my $signal = ($? & 127); + + unlink($filename); + close $temp_fh; + + if ($signal) { + $self->log(1, "kavscanner exited with signal: $signal"); + return (DECLINED); + } + + my $description = 'clean'; + my @infected = (); + my @suspicious = (); + if ($result > 0) { + if ($result =~ /^(2|3|4|8)$/) { + foreach (@output) { + if (/^.* infected: (.*)$/) { + # This covers the specific + push @infected, $1; + } elsif (/^\s*.* suspicion: (.*)$/) { + # This covers the potential viruses + push @suspicious, $1; + } + } + $description = "infected by: ".join(", ",@infected)."; " + ."suspicions: ".join(", ", @suspicious); + # else we may get a veeeery long X-Virus-Details: line or log entry + $description = substr($description,0,60); + $self->log(1, "There be a virus! ($description)"); + ### Untested by now, need volunteers ;-) + #if ($self->qp->config("kav_deny")) { + # foreach my $d (keys %{$self->qp->config("kav_deny", "map")}) { + # foreach my $v (@infected) { + # return(DENY, "Virus found: $description") + # if ($v =~ /^$d$/i); + # } + # foreach my $s (@suspicious) { + # return(DENY, "Virus found: $description") + # if ($s =~ /^$d$/i); + # } + # } + #} + $transaction->header->add('X-Virus-Found', 'Yes'); + $transaction->header->add('X-Virus-Details', $description); + ### maybe the spamassassin plugin can skip this mail if a virus + ### was found (and $transaction->notes('virus_flag') exists :)) + ### ...ok, works with our spamassassin plugin version + ### -- hah + $transaction->notes('virus', $description); + $transaction->notes('virus_flag', 'Yes'); + + #### requires modification of Qpsmtpd/Transaction.pm: + # if ($self->{_to_virusadmin}) { + # my @addrs = (); + # foreach (@{$transaction->recipients}) { + # push @addr, $_->address; + # } + # $transaction->header->add('X-Virus-Orig-RcptTo', join(", ", @addrs)); + # $transaction->set_recipients(@{ Mail::Address->parse($self->{_to_virusadmin}) }); + # } elsif ($self->{_bcc_virusadmin}) { + if ($self->{_bcc_virusadmin}) { + foreach ( @{ Mail::Address->parse($self->{_bcc_virusadmin}) } ) { + $transaction->add_recipient($_->address); + } + } + } else { + $self->log(0, "corrupt or unknown Kaspersky scanner/resource problems - exit status $result"); + } + } + + $self->log(1, "kavscanner results: $description"); + + $transaction->header->add('X-Virus-Checked', 'Checked by '.$self->qp->config("me")); + return (DECLINED); +} + +# vim: ts=2 sw=2 expandtab diff --git a/plugins/virus/klez_filter b/plugins/virus/klez_filter new file mode 100644 index 0000000..c169807 --- /dev/null +++ b/plugins/virus/klez_filter @@ -0,0 +1,37 @@ +sub register { + my ($self, $qp) = @_; + $self->register_hook("data_post", "check_klez"); +} + +sub check_klez { + my ($self, $transaction) = @_; + + # klez files are always sorta big .. how big? Dunno. + return (DECLINED) + if $transaction->body_size < 60_000; + # 220k was too little, so let's just disable the "big size check" + # or $transaction->body_size > 1_000_000; + + # maybe it would be worthwhile to add a check for + # Content-Type: multipart/alternative; here? + + # make sure we read from the beginning; + $transaction->body_resetpos; + + my $line_number = 0; + my $seen_klez_signature = 0; + + while ($_ = $transaction->body_getline) { + last if $line_number++ > 40; + + m/^Content-type:.*(?:audio|application)/i + and ++$seen_klez_signature and next; + + return (DENY, "Klez Virus Detected") + if $seen_klez_signature + and m!^TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQA!; + + } + + return (DECLINED); +}