#!/usr/bin/perl -w
###
# Project: pflogstats
# Module: pflogstats-statistics-antivirus.pm
# Type: statistics
# Description: Statistics module for antivirus
# Copyright: Dr. Peter Bieringer <pbieringer at aerasec dot de>
# AERAsec GmbH <http://www.aerasec.de/>
# License: GNU GPL v2
# CVS: $Id: pflogstats-statistics-antivirus.pm,v 1.22 2005/04/26 15:55:48 peter Exp $
###
###
# ChangeLog:
# 0.01
# - initial creation
# 0.02
# - add types hash
# - check also reject lines for I-Worm.Sobig (big@boss.com)
# 0.03
# - support new format code style
# 0.04
# - some speed-ups
# 0.05
# - adjust loglineparser arguments
# 0.06
# - tag some match patterns with "o"
# 0.07
# - check special from addresses
# 0.08
# - enable addressmodify hook for reject match also
# 0.09
# - replace warning on empty information with a token
# 0.10
# - replace all hash references with proper code
# 0.11
# - make Perl 5.0 compatible
# 0.12
# - add support for Sobig.f body_check log line
# - be more relaxed on extractin info from avcheck lines
# - add option "--av_skip_sender_statistic"
# 0.13
# - replace option "treeview" by "show_domain_list"
# - add support for intermediate data storage
# 0.14
# - move info about show_user|domain_list into options-support
# 0.15
# - extend reject/discard for Kaspersky AV to I-Worm.*
# - add support for newer amavis log lines
# - add support for av_type=amavis (also default now)
# 0.16
# - proper handling of bad e-mail addresses
# 0.17
# - catch also "warning: (virus message)" on Kaspersky AV
# 0.18
# - extend log line parser of amavis
# - fix Eicar detection
# - minor code optimization
# 0.19
# - reorganize hook names
###
use strict;
## Local constants
my $module_type = "statistics";
my $module_name = $module_type . "-antivirus";
my $module_version = "0.19";
package pflogstats::statistics::antivirus;
## Export module info
$main::moduleinfo{$module_name}->{'version'} = $module_version;
$main::moduleinfo{$module_name}->{'type'} = $module_type;
$main::moduleinfo{$module_name}->{'name'} = $module_name;
## Global prototyping
## Local prototyping
## Register options
$main::options{'av_skip_eicar'} = \$main::opts{'av_skip_eicar'};
$main::options{'av_skip_sender_statistic'} = \$main::opts{'av_skip_sender_statistic'};
$main::options{'av_type:s'} = \$main::opts{'av_type'};
## Register calling hooks
$main::hooks{'loglineparser'}->{$module_name} = \&loglineparser;
$main::hooks{'print_result'}->{$module_name} = \&print_result;
$main::hooks{'checkoptions'}->{$module_name} = \&checkoptions;
$main::hooks{'help'}->{$module_name} = \&help;
$main::hooks{'loop_afterfinish'}->{$module_name} = \&loop_afterfinish;
$main::hooks{'before_print_result'}->{$module_name} = \&before_print_result;
# Define type
$main::types{'av'} = 0;
## Global variables
## Local variables
my ($from, $to, $avMsg, $to_new);
my ($p_hook);
## Antivirus statistics
my $antivirusCounter;
# per User
my %antivirusUserStats;
# per Domain
my %antivirusDomainStats;
# per Virus
my %antivirusVirusStats;
# for treeview
my %antivirusTreeview;
## Global callable functions
# Help
sub help() {
my $helpstring = "
Type: av
[--av_skip_eicar] Do not count EICAR test virus pattern
[--av_type avp|amavis] Type of antivirus software
(currently 'avp' and 'amavis' are supported)
[--av_skip_sender_statistic] Do not show per sender statistics
(rather long since 'Sobig'...)
[--debug <debug>] Debug value
| 0x0100 : display native antivirus log lines
| 0x0200 : display extracted antivirus log lines
";
return $helpstring;
};
# Check options
sub checkoptions() {
# antivirus statistics
if ( ! defined $main::opts{'av_type'} ) {
# default Kasperski AVP
print STDERR "WARNING(av): no antivirus software type specified, use 'amavis' as default\n" if $main::types{'av'} != 0;
$main::opts{'av_type'} = "amavis";
};
if ($main::opts{'av_type'} !~ /^(amavis|avp)$/) {
die "Antivirus sofware type not supported: " . $main::opts{'avtype'};
};
};
# Parse logline
sub loglineparser(\$\$) {
return if ( $main::types{'av'} == 0);
if (! defined $_[0]) { die "Missing time pointer (arg1)"; };
if (! defined $_[1]) { die "Missing logline pointer (arg2)"; };
#print ${$_[1]} . "\n";
# Looking for special rejects (e.g. I-Worm.Sorbig)
if ( ${$_[1]} =~ / reject: RCPT from ([^:]+): [45]54 <big\@boss.com>: Sender address rejected: .* from=<([^>]*)> to=<([^>]*)> /o ) {
printf STDERR "DEBUG(av): %s\n", ${$_[1]} if ($main::opts{'debug'} & 0x0100 ) ;
return if (! defined $2 || ! defined $3 );
$from = lc($2);
$to = lc($3);
if ($from eq "") { $from = "from=<>" };
if ($from eq "#@[]") { $from = "from=<#@[]>"; };
if ( $main::opts{'av_type'} eq "avp" ) {
# Kasperski AVP
$avMsg = "I-Worm.Sobig";
} else {
die "Antivirus sofware type not supported: " . $main::opts{'avtype'};
};
# Hook "modifyaddress"
$from = ::modify_address($from);
$to = ::modify_address($to);
$antivirusCounter++;
$antivirusUserStats{'to'}->{$to}++;
$antivirusDomainStats{'to'}->{::extract_domain($to)}++;
$antivirusUserStats{'from'}->{$from}++;
$antivirusDomainStats{'from'}->{::extract_domain($from)}++;
$antivirusVirusStats{$avMsg}++;
$antivirusTreeview{::extract_domain($to)}->{$to}->{$avMsg}->{$from}++;
printf STDERR "DEBUG(av): from=%s to=%s msg=%s\n", $from, $to, $avMsg if ($main::opts{'debug'} & 0x0200 ) ;
return;
};
# Looking for special discard|rejects (e.g. I-Worm.Sorbig.f) using body checks
# Catch Aug 22 12:37:26 postfix postfix/cleanup[24933]: 6B8A9137E6: discard: body RSLxwtYBDB6FCv8ybBcS0zp9VU5of3K4BXuwyehTM0RI9IrSjVuwP94xfn0wgOjouKWzGXHVk3qg from brmea-mail-3.Sun.COM[192.18.98.34]; from=<> to=<rcpt@domain.example> proto=ESMTP helo=<brmea-mail-3.sun.com>: 554 Please clean your infected system (I-Worm.Sobig.f)
if ( ${$_[1]} =~ / (discard|reject): body .*; from=<(.*)> to=<(.*)> .*: [45]54 .*\((.*)\).*$/o ) {
printf STDERR "DEBUG(av): %s\n", ${$_[1]} if ($main::opts{'debug'} & 0x0100 ) ;
return if (! defined $2 || ! defined $3 );
$from = lc($2);
$to = lc($3);
$avMsg = $4;
if ($from eq "") { $from = "from=<>" };
if ($from eq "#@[]") { $from = "from=<#@[]>"; };
if ( $main::opts{'av_type'} eq "avp" ) {
# Kasperski AVP I-Worm.*
if ($avMsg =~ /I-Worm\./o) {
} else {
$avMsg = "UNCHECKED_AV_MESSAGE";
};
} else {
die "Antivirus sofware type not supported here: " . $main::opts{'avtype'};
};
# Hook "modifyaddress"
$from = ::modify_address($from);
$to = ::modify_address($to);
$antivirusCounter++;
$antivirusUserStats{'to'}->{$to}++;
$antivirusDomainStats{'to'}->{::extract_domain($to)}++;
$antivirusUserStats{'from'}->{$from}++;
$antivirusDomainStats{'from'}->{::extract_domain($from)}++;
$antivirusVirusStats{$avMsg}++;
$antivirusTreeview{::extract_domain($to)}->{$to}->{$avMsg}->{$from}++;
printf STDERR "DEBUG: AV: from=%s to=%s msg=%s\n", $from, $to, $avMsg if ($main::opts{'debug'} & 0x0200 ) ;
return;
};
# Looking for antivirus lines, nothing else
return unless (${$_[1]} =~ /.*(avcheck|amavis)\[\d+\]: /o);
if ( ${$_[1]} =~ / (avcheck|amavis)\[\d+\]: infected: from=([^,]*), to=([^,]*),.* msg=(.*)$/o ) {
printf STDERR "DEBUG(av): %s\n", ${$_[1]} if ($main::opts{'debug'} & 0x0100 ) ;
return if ( ! defined $2 && ! defined $3 );
return if ( $2 eq "" && $3 eq "" );
$from = lc($2);
$to = lc($3);
$avMsg = $4;
## extract virus information
if ( $main::opts{'av_type'} eq "avp" ) {
# Kasperski AVP
# Example: msg=archive: Mail suspicion: Exploit.IFrame.FileDownload infected: I-Worm.Klez.h ok.
if ( $avMsg =~ /infected: ([^ ]+).*$/o ) {
$avMsg = $1;
} elsif ( $avMsg =~ /suspicion: ([^ ]+).*$/o ) {
$avMsg = $1;
} elsif ( $avMsg =~ /warning: ([^ ]+).*$/o ) {
$avMsg = $1;
} else {
$avMsg .= " <no more information given>";
#warn "Cannot extract virus information: '$avMsg':\nLINE: " . ${$_[1]} . "\n";
};
return if ( lc($avMsg) eq "eicar-test-file" && defined $main::opts{'av_skip_eicar'} );
} elsif ( $main::opts{'av_type'} eq "amavis" ) {
warn "Antivirus sofware type not really supported here: " . $main::opts{'avtype'};
};
if ($from eq "") { $from = "from=<>" };
if ($from eq "#@[]") { $from = "from=<#@[]>"; };
# Hook "modifyaddress"
$from = ::modify_address($from);
$to = ::modify_address($to);
$antivirusCounter++;
# Check for more than one recipient
foreach $to_new (split " ", $to) {
$antivirusUserStats{'to'}->{$to_new}++;
$antivirusDomainStats{'to'}->{::extract_domain($to_new)}++;
$antivirusUserStats{'from'}->{$from}++;
$antivirusDomainStats{'from'}->{::extract_domain($from)}++;
$antivirusVirusStats{$avMsg}++;
$antivirusTreeview{::extract_domain($to_new)}->{$to_new}->{$avMsg}->{$from}++;
printf STDERR "DEBUG(av): from=%s to=%s msg=%s\n", $from, $to_new, $avMsg if ($main::opts{'debug'} & 0x0200 ) ;
};
return;
};
# Catch:
# Jan 20 23:41:26 hostname amavis[6821]: (06821-08) INFECTED (W32/Sober.C@mm), <sender@domain.example> --> <recipient@domain.example>, quarantine virus-20040120-234126-06821-08, Message-ID: <1234567890@domain.example>, Hits: -
# Feb 16 09:14:15 hostname amavis[16155]: (16155-04) Blocked INFECTED (Trojan-Spy.HTML.Paylap.r), [<IP>] [<IP>] <?@[<IP>]> -> <recipient@domain.example>, quarantine: virus-20050216-091414-16155-04, Message-ID: <20050215210928.D033019373A@leda.snu.ac.kr>, Hits: -, 1232 ms
if ( ${$_[1]} =~ / amavis\[\d+\]: \([0-9-]+\).* INFECTED \(([^)]*)\),.* <([^>]*)> -?-> <([^>]*)>,/o ) {
printf STDERR "DEBUG(av): %s\n", ${$_[1]} if ($main::opts{'debug'} & 0x0100 ) ;
return if ( ! defined $2 && ! defined $3 );
return if ( $2 eq "" && $3 eq "" );
$from = lc($2);
$to = lc($3);
$avMsg = $1;
#printf STDERR "DEBUG(av): from=" . $from . " to=" . $to . " avMsg=" . $avMsg . "\n" if ($main::opts{'debug'} & 0x0100 ) ;
## extract virus information
return if ( lc($avMsg) eq "eicar-test-file" && defined $main::opts{'av_skip_eicar'} );
if ($from eq "") { $from = "from=<>" };
if ($from eq "#@[]") { $from = "from=<#@[]>"; };
# Hook "modifyaddress"
$from = ::modify_address($from);
$to = ::modify_address($to);
$antivirusCounter++;
# Check for more than one recipient
foreach $to_new (split " ", $to) {
$antivirusUserStats{'to'}->{$to_new}++;
$antivirusDomainStats{'to'}->{::extract_domain($to_new)}++;
$antivirusUserStats{'from'}->{$from}++;
$antivirusDomainStats{'from'}->{::extract_domain($from)}++;
$antivirusVirusStats{$avMsg}++;
$antivirusTreeview{::extract_domain($to_new)}->{$to_new}->{$avMsg}->{$from}++;
printf STDERR "DEBUG(av): from=%s to=%s msg=%s\n", $from, $to_new, $avMsg if ($main::opts{'debug'} & 0x0200 ) ;
};
};
return;
};
# After loop finished
sub loop_afterfinish() {
## Hook 'register_intermediate_data'
for my $p_hook (keys %{$main::hooks{'register_intermediate_data'}}) {
&{$main::hooks{'register_intermediate_data'}->{$p_hook}} ("antivirusTreeview", \%antivirusTreeview);
&{$main::hooks{'register_intermediate_data'}->{$p_hook}} ("antivirusUserStats", \%antivirusUserStats);
&{$main::hooks{'register_intermediate_data'}->{$p_hook}} ("antivirusDomainStats", \%antivirusDomainStats);
&{$main::hooks{'register_intermediate_data'}->{$p_hook}} ("antivirusVirusStats", \%antivirusVirusStats);
};
};
# Before printing result
sub before_print_result() {
## Hook 'retrieve_intermediate_data'
for my $p_hook (keys %{$main::hooks{'retrieve_intermediate_data'}}) {
&{$main::hooks{'retrieve_intermediate_data'}->{$p_hook}} ("antivirusTreeview", \%antivirusTreeview);
&{$main::hooks{'retrieve_intermediate_data'}->{$p_hook}} ("antivirusUserStats", \%antivirusUserStats);
&{$main::hooks{'retrieve_intermediate_data'}->{$p_hook}} ("antivirusDomainStats", \%antivirusDomainStats);
&{$main::hooks{'retrieve_intermediate_data'}->{$p_hook}} ("antivirusVirusStats", \%antivirusVirusStats);
};
};
# Print result
sub print_result() {
return if ( $main::types{'av'} == 0);
my $info = "";
my $format;
if ( defined $main::opts{'av_skip_eicar'} ) {
$info = " (excluding EICAR test virus pattern)";
};
# Format: treeview
if (defined $main::format{"treeview"}) {
$format = "treeview";
my $info2 = $info . " (treeview)";
if (defined $main::opts{'show_domain_list'}) {
$main::opts{'show_domain_list'} =~ s/,/ /go;
if ($main::opts{'show_domain_list'} ne "") {
$info2 .= ":\n " . $main::opts{'show_domain_list'};
};
};
::print_headline("Antivirus statistics" . $info2, $format);
::print_timerange($format);
if (! defined $main::opts{'av_skip_sender_statistic'}) {
::print_treeview( \%antivirusTreeview, $main::opts{'show_domain_list'} );
} else {
::print_treeview2( \%antivirusTreeview, 3, $main::opts{'show_domain_list'} );
};
};
# Format: computer
if (defined $main::format{"computer"}) {
$format = "computer";
print "\n\nWARNING(av): Format '" . $format . "' is currently not supported!\n\n";
};
# Format: indented
if (defined $main::format{"indented"}) {
$format = "indented";
print "\n\nWARNING(av): Format '" . $format . "' is currently not supported!\n\n";
};
# Format: txttable
if (defined $main::format{"txttable"}){
$format = "txttable";
::print_headline("Antivirus statistics" . $info, $format);
::print_timerange($format);
::print_stat "Antivirus statistics per recipient" . $info, $antivirusUserStats{'to'} if (defined $main::opts{'show_users'});
::print_stat "Antivirus statistics per recipient domain" . $info, $antivirusDomainStats{'to'};
if (! defined $main::opts{'av_skip_sender_statistic'}) {
::print_stat "Antivirus statistics per sender" . $info, $antivirusUserStats{'from'} if (defined $main::opts{'show_users'});
::print_stat "Antivirus statistics per sender domain" . $info, $antivirusDomainStats{'from'};
};
::print_stat "Antivirus statistics per virus" . $info, \%antivirusVirusStats;
};
};
## Local functions
## End of module
return 1;
syntax highlighted by Code2HTML, v. 0.9.1