Whatever

mdf: DictionaryCheckDB.pm

=head1 NAME

DictionaryCheckDB - provides eval test using info from a anti-dictionary-attack database

=head1 SYNOPSIS

	loadplugin Mail::SpamAssassin::Plugin::DictionaryCheckDB /usr/local/etc/mail/spamassassin/plugins/DictionaryCheckDB.pm
  
	dictionarycheckdb_sql_dsn       DBI:mysql:dbname:localhost
	dictionarycheckdb_sql_username  user
	dictionarycheckdb_sql_password  pass

	dictionarycheckdb_min_window    60

	header    __DCDB_500C_24H       eval:dictionarycheckdb_count(500,'60*24')
	header    __DCDB_100C_24H       eval:dictionarycheckdb_count(100,'60*24')
	header    __DCDB_050C_24H       eval:dictionarycheckdb_count(50,'60*24')
	header    __DCDB_050C_10M       eval:dictionarycheckdb_count(50,10)
	header    __DCDB_NOTNONE        eval:dictionarycheckdb_count(1)

	header    __DCDB_20P            eval:dictionarycheckdb_calc(0.2)
	header    __DCDB_10P            eval:dictionarycheckdb_calc(0.1)
	header    __DCDB_05P            eval:dictionarycheckdb_calc(0.05)

	meta      DCDB_WORST            __DCDB_500C_24H || __DCDB_050C_10M || __DCDB_20P
	meta      DCDB_WORSE            !DCDB_WORST && (__DCDB_100C_24H || __DCDB_10P)
	meta      DCDB_BAD              !(DCDB_WORSE || DCDB_WORST) && (__DCDB_050C_24H || __DCDB_05P)
	meta      DCDB_NOTE             !(DCDB_WORSE || DCDB_WORST || DCDB_BAD) && __DCDB_NOTNONE
  
	score     DCDB_NOTE             0.01
	score     DCDB_BAD              0.5
	score     DCDB_WORSE            1.5
	score     DCDB_WORST            3.0

	describe  DCDB_NOTE             One or more dcdb-entries
	describe  DCDB_BAD              Impolite relay
	describe  DCDB_WORSE            Abusive relay
	describe  DCDB_WORST            Very abusive relay

=head1 DESCRIPTION

This module provides eval tests that checks a database containins entries
for connections that could be parts of dictionary attacks.

=head1 REQUIREMENT

This plugin uses the database used by the MIMEDefang filter at

	http://whatever.frukt.org.

=head1 CONFIGURATION

=head2 Eval test

=over

=item dictionarycheckdb_count(count,window)

True if the number of entries for the first untrusted host is count or higher.

Window specifies the number of minutes (backwards from now) entries are checked for.
If not specified, a bunch of different windows are used.

=item dictionarycheckdb_calc(value,window)

True if the number of entries divided by the time from the first to the last is
value or higher.

Window specifies the number of minutes (backwards from now) entries are checked for.
If not specified, all entries for the host is counted.

=back

=head2 Options

=over

=item dictionarycheckdb_sql_dsn

Wich database driver and database to use.

=item dictionarycheckdb_sql_username

User name for the database connection.

=item dictionarycheckdb_sql_password

Password for the database connection.

=item dictionarycheckdb_min_window

Minimum time window used in calculation.

=back

=cut

package Mail::SpamAssassin::Plugin::DictionaryCheckDB;

# $Id: DictionaryCheckDB.pm,v 1.8 2009/06/26 11:52:16 jonas Exp $

use strict;
use base 'Mail::SpamAssassin::Plugin';
use DBI;

sub dbg { 
	my $msg = shift;
	Mail::SpamAssassin::Plugin::dbg(sprintf("dictionarycheckdb: $msg",@_));
}

sub new {
	my ($class,$mailsa) = @_;
	$class = ref($class) || $class;
	my $self = $class->SUPER::new($mailsa);
	bless($self,$class);
	$self->{sqldb} = undef;
	$self->register_eval_rule('dictionarycheckdb_count');
	$self->register_eval_rule('dictionarycheckdb_calc');
	$self->{main}->{conf}->{dictionarycheckdb_sql_dsn} = 'DBI:mysql:mdf:localhost';
	$self->{main}->{conf}->{dictionarycheckdb_sql_username} = 'sa';
	$self->{main}->{conf}->{dictionarycheckdb_sql_password} = 'pwd';
	$self->{main}->{conf}->{dictionarycheckdb_min_window} = 60;
	#dbg('registered');
	return $self;
}

sub parse_config {
	my ($self,$pars) = @_;
	return 0 if ($pars->{user_config});
	return 0 unless ($pars->{key} =~ /^dictionarycheckdb_(sql_dsn|sql_username|sql_password|min_window)$/);
	my $key = $1;
	my $val = $pars->{value};
	$pars->{value} = eval($pars->{value}) if ($pars->{value} && $key eq 'min_window');
	$val = '' if ($key =~ /(username|password)/);
	$val = " = $val" if ($val);
	dbg('config %s%s',$key,$val);
	$self->{main}->{conf}->{$pars->{key}} = $pars->{value};
	$self->inhibit_further_callbacks();
	return 1;
}

sub _sql_connect {
	my ($self) = @_;
	return 1 if ($self->{sqldb});
	#dbg('sql connect');
	$self->{sqldb} = DBI->connect_cached(
				$self->{main}->{conf}->{dictionarycheckdb_sql_dsn},
				$self->{main}->{conf}->{dictionarycheckdb_sql_username},
				$self->{main}->{conf}->{dictionarycheckdb_sql_password},
				{RaiseError=>0}
	);
	return 1 if ($self->{sqldb});
	dbg('sql connect failed');
	return 0;
}

sub _sql_disconnect {
	my ($self) = @_;
	if ($self->{sqldb}) {
		#dbg('sql disconnect');
		$self->{sqldb}->disconnect();
	}
	$self->{sqldb} = undef;
}

sub _sql_select {
	my ($self,$cmd,@par) = @_;
	my $st;
	dbg('sql %s; %s',$cmd,join(', ',@par));
	$st = $self->{sqldb}->prepare($cmd);
	unless ($st) {
		dbg('sql prepare failed');
		return undef;
	}
	$st->execute(@par);
	my @res = $st->fetchrow_array;
	$st->finish;
	dbg('sql %u',scalar @res);
	return undef unless (@res);
	for (my $i=0;$i<@res;$i++) { $res[$i] = 0 unless (defined($res[$i])); }
	dbg('sql %s',join(',',@res));
	return \@res;
}

sub _get_values {
	my ($self,$pms,$now,$ip,$window) = @_;
	return $pms->{"dictionarycheckdb.c.$window.$ip"} if ($pms->{"dictionarycheckdb.c.$window.$ip"});
	dbg('sql %s %u',$ip,$window);
	my $res;
	return [0,0,0] unless ($self->_sql_connect());
	$res = $window ? 
		$self->_sql_select('SELECT COUNT(dc_stamp),MIN(dc_stamp),MAX(dc_stamp) FROM dictionary WHERE dc_host=? AND dc_stamp>?',$ip,$now-$window) :
		$self->_sql_select('SELECT COUNT(dc_stamp),MIN(dc_stamp),MAX(dc_stamp) FROM dictionary WHERE dc_host=?',$ip);
	return [0,0,0] unless ($res);
	$pms->{"dictionarycheckdb.c.$window.$ip"} = $res;
	return $res;
}

sub dictionarycheckdb_count {
	my ($self,$pms,$limit,$window) = @_;
	my $msg = $pms->get_message();
	return 0 unless ($msg && $msg->{metadata} && $msg->{metadata}->{relays_untrusted} && @{$msg->{metadata}->{relays_untrusted}});
	my $ip = $msg->{metadata}->{relays_untrusted}->[0]->{ip};
	return 0 unless ($ip);
	$limit = eval($limit) if ($limit);
	$window = eval($window) if ($window);
	$limit = 1 unless ($limit && $limit>0);
	$window = 0 unless ($window && $window>0);
	dbg('eval cnt ? %u %u %s',$limit,$window,$ip);
	my $res = $self->_get_values($pms,time(),$ip,$window*60);
	$self->_sql_disconnect();
	return 0 unless ($res && @{$res});
	dbg('eval cnt = %u',$res->[0]);
	return ($res->[0] > $limit) ? 1 : 0;
}

sub dictionarycheckdb_calc {
	my ($self,$pms,$limit,$window) = @_;
	my $msg = $pms->get_message();
	return 0 unless ($msg && $msg->{metadata} && $msg->{metadata}->{relays_untrusted} && @{$msg->{metadata}->{relays_untrusted}});
	my $ip = $msg->{metadata}->{relays_untrusted}->[0]->{ip};
	return 0 unless ($ip);
	$limit = eval($limit) if ($limit);
	$limit = 0.1 unless ($limit && $limit>0);
	my @windows = ();
	if (defined($window)) {
		$window = eval($window) if ($window);
		$window = 0 unless ($window && $window>0);
		@windows = ($window * 60);
	} else {
		@windows = (0,24*60*60,12*60*60,6*60*60,3*60*60,60*60,30*60,15*60,7*60);
	}
	my ($lth,$ltl,$ltt);
	my $now = time();
	my $lp = 0;
	foreach $window (@windows) {
		if ($lp && $window>0) {
			next unless ($window < $ltt);
			my $win = $now - $window;
			last if ($win > $lth);
			next if ($win < $ltl);
		}
		dbg('eval clc ? %u %u %s',$limit,$window,$ip);
		my $res = $self->_get_values($pms,$now,$ip,$window);
		last unless ($res && @{$res} && $res->[0] && $res->[1] && $res->[2]);
		my $tim = $res->[2] - $res->[1];
		next unless ($tim > $self->{main}->{conf}->{dictionarycheckdb_min_window});
		my $val = $tim ? $res->[0]/$tim : $res->[0];
		dbg('eval clc = %i / %i : %f',$res->[0],$tim,$val);
		if ($val > $limit) {
			$self->_sql_disconnect();
			return 1;
		}
		$ltl = $res->[1];
		$lth = $res->[2];
		$ltt = $now - $res->[1];
		$lp ++;
	}
	$self->_sql_disconnect();
	return 0;
}

1;

(2008-01-11)