1#!perl -w 2 3=head1 NAME 4 5uvscan 6 7=head1 DESCRIPTION 8 9A qpsmtpd plugin for the McAfee commandline virus scanner, uvscan. 10 11=head1 INSTALL AND CONFIG 12 13Place this plugin in the plugin/virus directory beneath the standard 14qpsmtpd installation. If you installed uvscan with the default path, you 15can use this plugin with default options (nothing specified): 16 17=over 4 18 19=item B<uvscan_location> 20 21Full path to the uvscan binary and all signature files; defaults to 22/usr/local/bin/uvscan. 23 24=item B<deny_viruses> 25 26Whether the scanner will automatically delete messages which have viruses. 27Takes either 'yes' or 'no' (defaults to 'yes'). 28 29=back 30 31=head1 AUTHOR 32 33John Peacock <jpeacock@cpan.org> 34 35=head1 COPYRIGHT AND LICENSE 36 37Copyright (c) 2004 John Peacock 38 39Based heavily on the clamav plugin 40 41This plugin is licensed under the same terms as the qpsmtpd package itself. 42Please see the LICENSE file included with qpsmtpd for details. 43 44=cut 45 46sub register { 47 my ($self, $qp, @args) = @_; 48 49 while (@args) { 50 $self->{"_uvscan"}->{pop @args} = pop @args; 51 } 52 $self->{"_uvscan"}->{"uvscan_location"} ||= "/usr/local/bin/uvscan"; 53} 54 55sub hook_data_post { 56 my ($self, $transaction) = @_; 57 58 return (DECLINED) 59 if $transaction->data_size > 250_000; 60 61 # Ignore non-multipart emails 62 my $content_type = $transaction->header->get('Content-Type'); 63 $content_type =~ s/\s/ /g if defined $content_type; 64 unless ( $content_type 65 && $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i) 66 { 67 $self->log(LOGWARN, "non-multipart mail - skipping"); 68 return DECLINED; 69 } 70 71 my $filename = $transaction->body_filename; 72 return (DECLINED) unless $filename; 73 74 # Now do the actual scanning! 75 my @cmd = ( 76 $self->{"_uvscan"}->{"uvscan_location"}, 77 '--mime', '--unzip', '--secure', '--noboot', $filename, '2>&1 |' 78 ); 79 $self->log(LOGINFO, "Running: ", join(' ', @cmd)); 80 open(FILE, join(' ', @cmd)); #perl 5.6 doesn't properly support the pipe 81 # mode list form of open, but this is basically the same thing. This form 82 # of exec is safe(ish). 83 my $output; 84 while (<FILE>) { $output .= $_; } 85 close FILE; 86 87 my $result = ($? >> 8); 88 my $signal = ($? & 127); 89 90 my $virus; 91 if ($output && $output =~ m/.*\W+Found (.*)\n/m) { 92 $virus = $1; 93 } 94 if ($output && $output =~ m/password-protected/m) { 95 return (DENY, 'We do not accept password-protected zip files!'); 96 } 97 98 if ($signal) { 99 $self->log(LOGWARN, "uvscan exited with signal: $signal"); 100 return (DECLINED); 101 } 102 if ($result == 2) { 103 $self->log(LOGERROR, "Integrity check for a DAT file failed."); 104 return (DECLINED); 105 } 106 elsif ($result == 6) { 107 $self->log(LOGERROR, "A general problem has occurred."); 108 return (DECLINED); 109 } 110 elsif ($result == 8) { 111 $self->log(LOGERROR, "The program could not find a DAT file."); 112 return (DECLINED); 113 } 114 elsif ($result == 15) { 115 $self->log(LOGERROR, "The program self-check failed"); 116 return (DECLINED); 117 } 118 elsif ($result) { # all of the possible virus returns 119 if ($result == 12) { 120 $self->log(LOGERROR, 121 "The program tried to clean a file but failed."); 122 } 123 elsif ($result == 13) { 124 $self->log(LOGERROR, "One or more virus(es) found"); 125 } 126 elsif ($result == 19) { 127 $self->log(LOGERROR, "Successfully cleaned the file"); 128 } 129 130 if (lc($self->{"_uvscan"}->{"deny_viruses"}) eq "yes") { 131 return (DENY, "Virus Found: $virus"); 132 } 133 $transaction->header->add('X-Virus-Found', 'Yes'); 134 $transaction->header->add('X-Virus-Details', $virus); 135 return (DECLINED); 136 } 137 138 $transaction->header->add('X-Virus-Checked', 139 "Checked by McAfee uvscan on " . $self->qp->config("me")); 140 141 return (DECLINED); 142} 143