changeset 66:42889eed0ce8

Lots of cleanups, etc. Documentation updates.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 18 Aug 2009 03:21:30 +0300
parents d2e2b82dd2f2
children 8df5d52436a1
files README example.conf maltfilter
diffstat 3 files changed, 204 insertions(+), 157 deletions(-) [+]
line wrap: on
line diff
--- a/README	Tue Aug 18 00:43:10 2009 +0300
+++ b/README	Tue Aug 18 03:21:30 2009 +0300
@@ -1,4 +1,4 @@
-Malicious Attack Livid Termination Filter daemon (maltfilter) v0.15.0
+Malicious Attack Livid Termination Filter daemon (maltfilter) v0.16.0
 =====================================================================
 Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 (C) Copyright 2009 Tecnic Software productions (TNSP)
@@ -11,11 +11,14 @@
 Maltfilter daemon script continuously scans various system logfiles
 including auth.log, httpd logs, etc. for signs of malicious connections,
 break-in and exploitation attempts. The originating IP addresses of
-these connections are then blocked via Netfilter (iptables).
+these connections can be then acted upon in following ways, each
+being optional:
+
+ * Insertion (and eventual deletion or "weeding") of Netfilter rules.
+ * Submitting entry to DroneBL DNSBL service.
 
 Additionally Maltfilter can generate status reports (either continuously
 in daemon mode, or as once-run report), in plaintext and HTML formats
-and submit data to DroneBL DNSBL service.
 
 Since v0.14, there is also option for gathering "evidence" about certain
 PHP XSS exploit attempts into specified directory. These evidence files
@@ -65,8 +68,8 @@
 
 Configuration and usage
 =======================
-See example.conf for documentation about settings.
-Start maltfilter either via the init script or through commandline:
+See example.conf for documentation about settings. Start maltfilter
+either via the init script or through commandline:
 
 $ maltfilter /var/run/maltfilter.pid /etc/maltfilter.conf
 
--- a/example.conf	Tue Aug 18 00:43:10 2009 +0300
+++ b/example.conf	Tue Aug 18 03:21:30 2009 +0300
@@ -1,28 +1,41 @@
 #############################################################################
 ### Maltfilter configuration file.
 ### PLEASE READ THROUGH THIS FILE VERY CAREFULLY!
+#############################################################################
 
 #############################################################################
 ### General settings
 #############################################################################
-# Verbosity level (0 = quiet, bigger values add noise. valid range 0 - 4)
-VERBOSITY = 4
+## Verbosity level (0 = quiet, bigger values add noise. valid range 0 - 4)
+VERBOSITY = 3
 
-# Dry-run: 1 = disables daemonization/forking to background, disables
-# modification of netfilter/iptables, printing the iptables commands to
-# stdout instead.
-# NOTICE! IF YOU DON'T CHANGE THIS TO 0, MALTFILTER WILL NOT DAEMONIZE!
+## Dry-run: 1 = disables daemonization/forking to background, disables
+## modification of netfilter via iptables, printing the iptables commands
+## to stdout instead and DroneBL submissions will be disabled.
+## NOTICE! IF YOU DON'T CHANGE THIS TO 0, MALTFILTER WILL NOT DAEMONIZE!
 DRY_RUN = 1
 
-# Full path to iptables binary
-IPTABLES = "/sbin/iptables"
-
-# Maltfilter logfile path and name (set empty "" if you don't want logging)
+## Maltfilter logfile path and name (set empty "" if you don't want logging)
 LOGFILE = "/var/log/maltfilter"
 
-# System passwd file location (default is /etc/passwd), this file
-# is checked to figure out system account names. See also SYSACCT_
-# settings below.
+## IP addresses that should NOT be blocked under any circumstances. You should
+## set this if you wish to have a surefire open channel from some host, even in
+## the case someone tries to spoof IPs for denial of service.
+##
+## NOTICE! This setting supports only IPv4 addresses, no IPv6 or DNS names.
+## You can have any number of NOACTION_IPS settings.
+#NOACTION_IPS = "192.121.86.15"
+#NOACTION_IPS = "74.125.45.100"
+
+## For how many hours to keep general information about IP. Affects from
+## how long period statistics dump shows data. Also hitcount thresholds
+## take the old data into account, meaning that if FILTER_MAX_AGE < GLOBAL_MAX_AGE
+## hit data older than FILTER_MAX_AGE will be counted towards THRESHOLD.
+#GLOBAL_MAX_AGE = 336
+
+## System passwd file location (default is /etc/passwd), this file
+## is checked to figure out system account names. See also SYSACCT_*
+## settings below.
 #PASSWD = "/etc/passwd"
 
 ## Set range of system account UIDs here, default is 1-100.
@@ -32,35 +45,26 @@
 
 
 #############################################################################
-### Actions, etc. settings
+### Netfilter actions
 #############################################################################
+## 0 = Netfilter handling disabled
+FILTER = 0
+
+## Full path to iptables binary
+IPTABLES = "/sbin/iptables"
+
+## How many "hits" the IP needs until it is eligible to be filtered.
+## (the "hits" can be from any check, e.g. sshd crack, httpd, etc.)
+FILTER_THRESHOLD = 3
+
 ## Weeding threshold in hours. Entries older than this will be removed
 ## off from current netfilter settings. Also, entries older than this
 ## will not be added to netfilter to begin with.
-#WEED_FILTER = 168
-
-## For how many hours to keep general information about IP. Affects from
-## how long period statistics dump shows data. Also hitcount thresholds
-## take the old data into account, meaning that if WEED_FILTER < WEED_GLOBAL
-## hit data older than WEED_FILTER will be counted towards THRESHOLD.
-#WEED_GLOBAL = 336
-
-## How many "hits" the IP needs until it is eligible to be blocked.
-## (the "hits" can be from any "source", e.g. sshd crack, httpd, etc.)
-#THRESHOLD = 3
+FILTER_MAX_AGE = 168
 
-## Target iptables action for added entries, default is DROP, but you
+## Target iptables target for added entries, default is DROP, but you
 ## can use whatever rule chain name you want to here.
-#ACTION = "DROP"
-
-## IP addresses that should NOT be blocked under any circumstances. You should
-## set this if you wish to have a surefire open channel from some host, even in
-## the case someone tries to spoof IPs for denial of service.
-##
-## NOTICE! This setting supports only IPv4 addresses, no IPv6 or DNS names.
-## You can have any number of NOBLOCK_IPS settings.
-#NOBLOCK_IPS = "192.121.86.15"
-#NOBLOCK_IPS = "74.125.45.100"
+FILTER_TARGET = "DROP"
 
 
 #############################################################################
@@ -97,7 +101,7 @@
 ##
 ## NOTICE! Do not enable this setting, if you allow SSH root logins via
 ## password authentication! Mistyping password may get you blocked unless
-## your host IP is defined in NOBLOCK_IPS. If you wish to enable this
+## your host IP is defined in NOACTION_IPS. If you wish to enable this
 ## check, you should set "PermitRootLogin" to "without-password" or "no"
 ## in your sshd_config.
 CHK_ROOT_SSH_PWD    = 0
@@ -106,12 +110,11 @@
 ## Catches failed password logins for system accounts.
 ##
 ## NOTICE! If you enable this setting, make sure have defined safe
-## host IPs in NOBLOCK_IPS, and that your system DOES NOT have passwords
+## host IPs in NOACTION_IPS, and that your system DOES NOT have passwords
 ## for system accounts .. which would be stupid anyway.
 CHK_SYSACCT_SSH_PWD = 0
 
 
-
 # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin)
 # NOTICE! This matches ERRORLOG, thus it only works if you DO NOT have
 # any or some of these installed. Preferably none, or use uncommon
@@ -145,11 +148,11 @@
 ## every few minutes.) Leave empty ("") or commented if you do not want
 ## status reports.
 
-## Plain ASCII text file rerpot
+## Plain ASCII text file report
 #STATUS_FILE_PLAIN = "/var/www/maltstatus.txt"
 
 ## HTML file and optional CSS stylesheet URL for the HTML
-## (if left empty, CSS is not used.)
+## (if left empty/unset, CSS will not be linked from the HTML file.)
 #STATUS_FILE_HTML = "/var/www/maltstatus.html"
 #STATUS_FILE_CSS = "cool.css"
 
@@ -169,12 +172,13 @@
 #############################################################################
 ## By enabling EVIDENCE=1 and setting EVIDENCE_DIR to existing directory
 ## writable by the effective UID which Maltfilter runs as, it will be
-## populated by *.data and *.hosts files. If succesfully retrieved, .data
-## files will have contents of the attempted XSS URI. *.hosts files
-## list which hosts have attempted to exploit this specific URI.
+## populated by *.info, *.data and *.hosts files. If succesfully retrieved,
+## .data files will have contents of the attempted XSS URI. *.hosts files
+## list which hosts have attempted to exploit this specific URI. *.info
+## contain generic information and HTTP headers.
 
-#EVIDENCE            = 0
-#EVIDENCE_DIR        = "/var/run/malt-evidence"
+EVIDENCE            = 0
+EVIDENCE_DIR        = "/var/run/malt-evidence"
 
 
 #############################################################################
@@ -192,8 +196,8 @@
 DRONEBL_THRESHOLD    = 5
 
 ## Maximum age of hits counted towards DroneBL submission threshold.
-## There is currently no weeding of submissions.
-DRONEBL_MAX_AGE      = 30
+## NOTICE! Value this is in minutes!
+DRONEBL_MAX_AGE      = 60
 
 ## Your personal RPC key. This _MUST_ be set to a valid value, if you
 ## have enabled submissions. To get a personal key, go to:
--- a/maltfilter	Tue Aug 18 00:43:10 2009 +0300
+++ b/maltfilter	Tue Aug 18 03:21:30 2009 +0300
@@ -12,29 +12,32 @@
 use Net::DNS;
 use LWP::UserAgent;
 
-my $progversion = "0.15.0";
+my $progversion = "0.16.0";
 my $progbanner =
 "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n".
 "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n".
 "(C) Copyright 2009 Tecnic Software productions (TNSP)\n";
 
+
 #############################################################################
 ### Default settings and configuration
 #############################################################################
 my %settings = (
-  "VERBOSITY" => 3,
-  "DRY_RUN" => 1,
-  "WEED_FILTER" => 168,  # in hours
-  "WEED_GLOBAL" => 336,  # in hours
-  "THRESHOLD" => 3,
-  "ACTION" => "DROP",
-  "LOGFILE" => "",
-  "IPTABLES" => "/sbin/iptables",
+  "VERBOSITY"           => 3,
+  "DRY_RUN"             => 1,
+  "LOGFILE"             => "",
+  "STATS_MAX_AGE"       => 336,  # in hours
 
   "PASSWD"              => "/etc/passwd",
   "SYSACCT_MIN_UID"     => 1,
   "SYSACCT_MAX_UID"     => 100,
 
+  "FILTER"              => 0,
+  "FILTER_THRESHOLD"    => 3,
+  "FILTER_MAX_AGE"      => 168,  # in hours
+  "FILTER_TARGET"       => "DROP",
+  "IPTABLES"            => "/sbin/iptables",
+
   "FULL_TIME"           => 1,
   "STATUS_FILE_PLAIN"   => "",
   "STATUS_FILE_HTML"    => "",
@@ -59,7 +62,7 @@
   "DRONEBL_RPC_KEY"     => "",
 );
 
-my @noblock_ips_def = (
+my @noaction_ips_def = (
   "127.0.0.1",
 );
 
@@ -79,20 +82,21 @@
     
     # (1.1) Generic login scan attempts
     if ($merr =~ /^Failed password for invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)/) {
-      check_add_hit($2, $mdate, "SSH login scan", "", $settings{"CHK_SSHD"});
+      check_add_hit($2, $mdate, "SSH login scan", "", 13, $settings{"CHK_SSHD"});
     }
     # (1.2) Root account SSH login password bruteforcing attempts.
     elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) {
-      check_add_hit($1, $mdate, "Root SSH password bruteforce", "", $settings{"CHK_ROOT_SSH_PWD"});
+      check_add_hit($1, $mdate, "Root SSH password bruteforce", "", 13, $settings{"CHK_ROOT_SSH_PWD"});
     }
     # (1.3) System account SSH login password bruteforcing attempts.
     if ($merr =~ /^Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)/) {
       my $mip = $2; my $macct = $1;
       if (defined($systemacct{$macct})) {
-        check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, $settings{"CHK_SYSACCT_SSH_PWD"});
+        check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, 13, $settings{"CHK_SYSACCT_SSH_PWD"});
       }
     }
   }
+
   # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin)
   elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) {
     my $mdate = $1;
@@ -101,10 +105,11 @@
     if ($merr =~ /^File does not exist: (.+)$/) {
       my $tmp = $1;
       if ($tmp =~ /\/mss2|\/pma|admin|sql|\/roundcube|\/webmail|\/bin|\/mail|xampp|zen|mailto:|appserv|cube|round|_vti_bin|wiki/i) {
-        check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, $settings{"CHK_KNOWN_CGI"});
+        check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, 2, $settings{"CHK_KNOWN_CGI"});
       }
     }
   }
+
   # (3) Apache common logging format checks
   elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) {
     my $mdate = $2;
@@ -115,15 +120,15 @@
     if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
       if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
         if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) {
-          check_add_evidence($mip, $1, $merr);
+          evidence_queue($mip, $1, $merr);
         }
-        check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"});
+        check_add_hit($mip, $mdate, "PHP XSS", $merr, 2, $settings{"CHK_PHP_XSS"});
       }
     }
     # (3.2) Try to match proxy scanning attempts
     elsif ($merr =~ /^http:\/\/([^\/]+)/) {
       if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
-        check_add_hit($mip, $mdate, "Proxy scan", $merr, $settings{"CHK_PROXY_SCAN"});
+        check_add_hit($mip, $mdate, "Proxy scan", $merr, 2, $settings{"CHK_PROXY_SCAN"});
       }
     }
   }
@@ -136,15 +141,15 @@
 my $reportmode = 0;      # Full report mode
 my @scanfiles = ();      # Files to scan
 my @scanfiles_once = (); # Files to scan only once during startup or HUP (e.g. not continuously followed)
-my @noblock_ips = ();    # IPs not to block
+my @noaction_ips = ();    # IPs not to block
 my %filehandles = ();    # Global hash holding opened scanned log filehandles
 my $pid_file = "";       # Name of Maltfilter daemon pid file
 my @configfiles = ();    # Array of configuration file names
 my $LOGFILE;             # Maltfilter logfile handle
 my %dronebl = ();
 
-# IPs currently blocked in Netfilter $blocklist{$ip} = date
-my %blocklist = ();      
+# IPs currently blocked in Netfilter $filterlist{$ip} = date
+my %filterlist = ();      
 
 # Gathered information about hosts
 # $statlist{$ip}->
@@ -308,7 +313,7 @@
   );
   
   foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) {
-    my $blocked = defined($blocklist{$mip}) ? "blocked" : "unblocked";
+    my $blocked = defined($filterlist{$mip}) ? "blocked" : "unblocked";
     printElem($m, $f, " <tr class=\"$blocked\">");
     printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"}));
     printElem(!$m, $f, " | ");
@@ -383,7 +388,7 @@
   my @ncolor = (0, 0);
   
   my $printEntry = sub {
-    my $blocked = "class=\"".(defined($blocklist{$_[0]}) ? "blocked" : "unblocked")."\"";
+    my $blocked = "class=\"".(defined($filterlist{$_[0]}) ? "blocked" : "unblocked")."\"";
     if (test_ips($previp[$_[1]], $_[0]) < 3) {
       $ncolor[$_[1]]++;
     }
@@ -477,7 +482,7 @@
 ");
 
   printH($m, $f, 1, "Maltfilter v$progversion status report");
-  my $period = get_period($settings{"WEED_GLOBAL"});
+  my $period = get_period($settings{"STATS_MAX_AGE"});
 
   printP($m, $f,
   "Generated ".bb($m).get_time_str(time()).eb($m).". Data computed from ".
@@ -487,10 +492,10 @@
   "blocked IP that was in Netfilter before Maltfilter was started.\n");
 
   printH($m, $f, 2, "Currently filtered entries");
-  $period = get_period($settings{"WEED_FILTER"});
+  $period = get_period($settings{"FILTER_MAX_AGE"});
   printP($m, $f, "List of IPs that are currently filtered (or would be, if this is\n".
   "a report-only mode). Data from period of $period.\n");
-  print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked");
+  print_table1($m, $f, \%statlist, \%filterlist, \&cmp_hits, "blocked");
 
   printH($m, $f, 2, "Summary of non-ignored entries");
   printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n".
@@ -512,25 +517,26 @@
 #############################################################################
 sub dronebl_process
 {
-#  return if ($reportmode);
   return unless ($settings{"DRONEBL"} > 0);
+  return if ($settings{"DRY_RUN"});
 
   # Create submission data
   my $xml = "<?xml version=\"1.0\"?>\n<request key=\"".$settings{"DRONEBL_RPC_KEY"}."\">\n";
   my $entries = 0;
   while (my ($ip, $entry) = each(%dronebl)) {
-    if ($entry->{"sent"} == 0) {
-      $xml .= "<add ip=\"".$ip."\" type=\"".$entry->{"type"}."\" />\n";
+    if ($entry->{"sent"} == 0 && $entry->{"tries"} < 3) {
+      $xml .= "<add ip=\"".$ip."\" type=\"1\" />\n";
+#      $xml .= "<add ip=\"".$ip."\" type=\"".$entry->{"type"}."\" />\n";
       $entries++;
     }
   }
   $xml .= "</request>\n";
 
-  mlog(1, "Trying to submit $entries entries to DroneBL.\n");
-  print STDERR $xml;
-  return;
-  
+  # Bait out if no entries to submit
   return unless ($entries > 0);
+  mlog(1, "[DroneBL] Trying to submit $entries entries.\n");
+
+return;
     
   # Submit via HTTP XML-RPC
   my $tmp = LWP::UserAgent->new;
@@ -543,30 +549,46 @@
   my $res = $tmp->request($req);
 
   if ($res->is_success) {
-    while (my ($ip, $entry) = each(%dronebl)) {
-      $entry->{"sent"} = 1;
-    }
+    mlog(2, "[DroneBL] [".$res->code."] ".$res->message."\n");
+    print $res->content."\n";
+#    while (my ($ip, $entry) = each(%dronebl)) {
+#      $entry->{"sent"} = 1;
+#    }
   } else {
-    mlog(-1, "DroneBL submission failed: [".$res->code."] ".$res->message."\n");
+    mlog(-1, "[DroneBL] Submission failed: [".$res->code."] ".$res->message."\n");
   }
   
   # Remove submitted expired entries
   while (my ($ip, $entry) = each(%dronebl)) {
-    print "$ip: ".$entry->{"sent"}."\n" unless check_time3($entry->{"date"});
+    if (!check_time3($entry->{"date"})) {
+      mlog(1, "[DroneBL] $ip submission expired.\n") unless ($entry->{"sent"} > 0);
+      delete($dronebl{$ip});
+    }
   }
 }
 
+sub dronebl_queue($$$)
+{
+  my ($mip, $mdate, $mtype) = @_;
+  if (!defined($dronebl{$mip})) {
+    mlog(3, "[DroneBL] Queueing $mip \@ $mdate ($mtype)\n");
+    $dronebl{$mip}{"type"} = $mtype;
+    $dronebl{$mip}{"date"} = $mdate;
+    $dronebl{$mip}{"sent"} = 0;
+    $dronebl{$mip}{"tries"} = 0;
+  }
+}
 
 #############################################################################
 ### Evidence gathering
 #############################################################################
 my %evidence = ();
 
-sub check_add_evidence($$$)
+sub evidence_queue($$$)
 {
   my ($mip, $mdata, $mfull) = @_;
 
-  return unless ($settings{"EVIDENCE"});
+  return unless ($settings{"EVIDENCE"} > 0);
 
   my $tmp = $mdata;
   $tmp =~ s/http:\/\///;
@@ -578,7 +600,7 @@
   $evidence{$mdata}{"full"}{$mfull} = 1;
 }
 
-sub http_fetch($$)
+sub evidence_fetch($$)
 {
   my $tmp = LWP::UserAgent->new;
   $tmp->agent("-");
@@ -588,12 +610,12 @@
   return $tmp->request($req);
 }
 
-sub gather_evidence
+sub evidence_gather
 {
   my $dns = Net::DNS::Resolver->new;
   my $base = $settings{"EVIDENCE_DIR"};
 
-  return unless ($settings{"EVIDENCE"});
+  return unless ($settings{"EVIDENCE"} > 0);
 
   mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base);
   
@@ -607,7 +629,7 @@
     if (! -e $filename) {
       $did_fetch = 1;
       mlog(1, "Fetching evidence for $url\n");
-      my $res = http_fetch($url, "");
+      my $res = evidence_fetch($url, "");
       open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n");
       binmode(FILE, ":raw");
       if ($res->is_success && $res->code >= 200 && $res->code <= 201) {
@@ -697,6 +719,7 @@
 ### Execute iptables
 sub exec_iptables(@)
 {
+  $ENV{"PATH"} = "";
   my @args = ($settings{"IPTABLES"}, @_);
   if ($settings{"DRY_RUN"}) {
     mlog(3, ":: ".join(" ", @args)."\n");
@@ -706,12 +729,13 @@
 }
 
 ### Get current Netfilter INPUT table entries that match
-### entry types we manage, e.g. blocklist
-sub update_blocklist($)
+### entry types we manage, e.g. filterlist
+sub update_filterlist($)
 {
-  # NOTICE: argument not used now
+  return unless ($settings{"FILTER"} > 0);
   my $first = $_[0];
-  
+  mlog(0, "Updating initial filterlist from netfilter.\n") unless ($first > 0);
+
   $ENV{"PATH"} = "";
   open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or
     mdie("Could not execute ".$settings{"IPTABLES"}."\n");
@@ -719,12 +743,12 @@
   undef(%newlist);
   while (<STATUS>) {
     chomp;
-    if (/^\s*(\d+)\s+\d+\s+$settings{"ACTION"}\s+all\s+--\s+\*\s+\*\s+(\d+\.\d+\.\d+\.\d+)\s+0\.0\.0\.0\/0\s*$/) {
+    if (/^\s*(\d+)\s+\d+\s+$settings{"FILTER_TARGET"}\s+all\s+--\s+\*\s+\*\s+(\d+\.\d+\.\d+\.\d+)\s+0\.0\.0\.0\/0\s*$/) {
       my $mip = $2;
       my $mdate = time();
-      if (!defined($blocklist{$mip})) {
-        mlog(2, "* $mip appeared in iptables.\n") if ($first > 0);
-        $blocklist{$2} = $mdate;
+      if (!defined($filterlist{$mip})) {
+        mlog(2, "* $mip appeared in iptables.\n") unless ($first < 0);
+        $filterlist{$2} = $mdate;
       }
       $newlist{$2} = $mdate;
       update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0);
@@ -732,10 +756,10 @@
   }
   close(STATUS);
   
-  foreach my $mip (keys %blocklist) {
+  foreach my $mip (keys %filterlist) {
     if (!defined($newlist{$mip})) {
       mlog(2, "* $mip removed from iptables.\n");
-      delete($blocklist{$mip});
+      delete($filterlist{$mip});
     }
   }
 }
@@ -744,12 +768,12 @@
 ### Returns false if timestamp is over weed period, e.g. needs weeding.
 sub check_time1($)
 {
-  return ($_[0] > time() - ($settings{"WEED_FILTER"} * 60 * 60));
+  return ($_[0] > time() - ($settings{"FILTER_MAX_AGE"} * 60 * 60));
 }
 
 sub check_time2($)
 {
-  return ($_[0] > time() - ($settings{"WEED_GLOBAL"} * 60 * 60));
+  return ($_[0] > time() - ($settings{"STATS_MAX_AGE"} * 60 * 60));
 }
 
 sub check_time3($)
@@ -760,10 +784,10 @@
 ### Weed out old entries
 sub weed_do($)
 {
-  my $mtime = $blocklist{$_[0]};
+  my $mtime = $filterlist{$_[0]};
   mlog(2, "* Weeding $_[0] (".get_time_str($mtime).")\n");
-  exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"});
-  delete($blocklist{$_[0]});
+  exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"FILTER_TARGET"});
+  delete($filterlist{$_[0]});
   delete($statlist{$_[0]});
   delete($ignorelist{$_[0]});
 }
@@ -771,14 +795,14 @@
 sub weed_entries()
 {
   # Don't weed in report mode.
-  return if ($reportmode);
+  return unless ($settings{"FILTER"} > 0 && $reportmode == 0);
 
   # Weed blocked entries.
-  my @mips = keys %blocklist;
+  my @mips = keys %filterlist;
   foreach my $mip (@mips) {
-    if (defined($blocklist{$mip})) {
-      if ($blocklist{$mip} >= 0) {
-        weed_do($mip) unless check_time1($blocklist{$mip});
+    if (defined($filterlist{$mip})) {
+      if ($filterlist{$mip} >= 0) {
+        weed_do($mip) unless check_time1($filterlist{$mip});
       } else {
         weed_do($mip);
       }
@@ -789,7 +813,7 @@
   foreach my $mip (keys %statlist) {
     if (defined($statlist{$mip})) {
       my $mtime = $statlist{$mip}{"date2"};
-      if (!check_time2($mtime) && !defined($blocklist{$mip})) {
+      if (!check_time2($mtime) && !defined($filterlist{$mip})) {
         mlog(3, "* Deleting stale $mip (".get_time_str($mtime).")\n");
         delete($statlist{$mip});
       }
@@ -862,8 +886,8 @@
   my $mcond = $_[5];
   my $cnt;
 
-  if (check_hosts_array(\@noblock_ips, $mip)) {
-    mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n");
+  if (check_hosts_array(\@noaction_ips, $mip)) {
+    mlog(2, "Hit to NOACTION_IPS($mip): [$mclass] $mreason\n");
     return;
   }
   
@@ -877,21 +901,19 @@
   }
 
   # Check if we have exceeded threshold etc.
-  if ($cnt >= $settings{"THRESHOLD"} && check_time1($mdate)) {
-    # Add to blocklist, unless already there.
-    if (!defined($blocklist{$mip})) {
-      mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n");
-      exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"});
+  if ($settings{"FILTER"} > 0 && $cnt >= $settings{"FILTER_THRESHOLD"} && check_time1($mdate)) {
+    # Add to filterlist, unless already there.
+    if (!defined($filterlist{$mip})) {
+      mlog(1, "* Adding $mip \@ ".get_time_str($mdate).": [$mclass] $mreason\n");
+      exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"FILTER_TARGET"});
     }
     # Update date of last hit
-    $blocklist{$mip} = $mdate;
+    $filterlist{$mip} = $mdate;
   }
 
   # Separate check for DroneBL
   if ($settings{"DRONEBL"} > 0 && $mtype > 0 && $cnt >= $settings{"DRONEBL_THRESHOLD"} && check_time3($mdate)) {
-    $dronebl{$mip}{"type"} = $mtype;
-    $dronebl{$mip}{"date"} = $mdate;
-    $dronebl{$mip}{"sent"} = 0 unless defined($dronebl{$mip}{"sent"});
+    dronebl_queue($mip, $mdate, $mtype);
   }
 }
 
@@ -925,8 +947,7 @@
   undef(%statlist);
   %ignorelist = ();
   undef(%ignorelist);
-  mlog(0, "Updating initial blocklist from netfilter.\n");
-  update_blocklist(-1);
+  update_filterlist(-1);
 
   foreach my $filename (@scanfiles_once) {
     mlog(0, "Parsing [ONCE] ".$filename." ...\n");
@@ -1001,6 +1022,16 @@
   mlog(-1, "Reinitialization finished, resuming scanning.\n");
 }
 
+sub malt_maintenance
+{
+  update_filterlist(time());
+  weed_entries();
+  generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
+  generate_status($settings{"STATUS_FILE_HTML"}, 1);
+  evidence_gather();
+  dronebl_process();
+}
+
 ### Main scanning function
 sub malt_scan
 {
@@ -1017,15 +1048,9 @@
       }
     }
     if ($counter < 0 || $counter++ >= 30) {
-      # Every once in a while, update known IP list from iptables
-      # (in case entries have appeared there from "outside")
-      # and perform weeding of old entries.
+      # Every once in a while, execute maintenance functions
       $counter = 0;
-      update_blocklist(time());
-      weed_entries();
-      generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
-      generate_status($settings{"STATUS_FILE_HTML"}, 1);
-      gather_evidence();
+      malt_maintenance();
     }
     sleep(2);
     foreach my $filename (keys %filehandles) {
@@ -1063,8 +1088,8 @@
         push(@scanfiles, $value);
       } elsif ($key eq "SCANFILE_ONCE") {
         push(@scanfiles_once, $value);
-      } elsif ($key eq "NOBLOCK_IPS") {
-        push(@noblock_ips_def, $value);
+      } elsif ($key eq "NOACTION_IPS") {
+        push(@noaction_ips_def, $value);
       } elsif (defined($settings{$key})) {
         $settings{$key} = $value;
       } else {
@@ -1107,10 +1132,10 @@
   @scanfiles_once = grep(!$saw{$_}++, @scanfiles_once);
 
   %saw = ();
-  @noblock_ips = grep(!$saw{$_}++, @noblock_ips_def);
+  @noaction_ips = grep(!$saw{$_}++, @noaction_ips_def);
   undef(%saw);
 
-  mlog(-1, "Not blocking following IPs: ".join(", ", @noblock_ips)."\n");
+  mlog(-1, "Not acting on IPs: ".join(", ", @noaction_ips)."\n");
 
   # Check if we have anything to do
   if ($reportmode) {
@@ -1119,13 +1144,26 @@
     mdie("Nothing to do, no SCANFILE(s) defined in configuration.\n") unless ($#scanfiles > 0);
   }
 
-  # Test existence of iptables
-  if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) {
-    mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n");
+  # General settings
+  my $val = $settings{"STATS_MAX_AGE"};
+  mdie("Invalid STATS_MAX_AGE value $val, must be > 0.\n") unless ($val > 0);
+
+  # Filtering
+  if ($settings{"FILTER"} > 0) {
+    $val = $settings{"FILTER_MAX_AGE"};
+    mdie("Invalid FILTER_MAX_AGE value $val, must be > 0.\n") unless ($val > 0);
+    
+    $val = $settings{"FILTER_THRESHOLD"};
+    mdie("Invalid FILTER_THRESHOLD value $val, must be >= 0.\n") unless ($val >= 0);
+  
+    $val = $settings{"IPTABLES"};
+    mdie("iptables binary does not exist or is not executable: $val\n") unless (-e $val && -x $val);
+  } else {
+    mlog(1, "Netfilter handling disabled.\n");
   }
 
   # Check evidence settings
-  if ($settings{"EVIDENCE"}) {
+  if ($settings{"EVIDENCE"} > 0) {
     my $base = $settings{"EVIDENCE_DIR"};
     mdie("Evidence directory (EVIDENCE_DIR) not set in configuration.\n") if ($base eq "");
     mdie("Evidence directory '$base' does not exist.\n") unless (-e $base);
@@ -1133,7 +1171,12 @@
     mdie("Evidence directory '$base' is not writable by euid.\n") unless (-w $base);
   }
   
-  # Check settings
+  # Sanitize DroneBL configuration
+  if ($settings{"DRONEBL"} > 0) {
+    mdie("DroneBL RPC key not set.\n") unless ($settings{"DRONEBL_RPC_KEY"} ne "");
+  }
+  
+  # Check system account / passwd settings
   mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1);
   mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"});
 
@@ -1160,7 +1203,7 @@
 # Print banner and help if no arguments
 my $argc = $#ARGV + 1;
 if ($argc < 1) {
-  print $progbanner.
+  print STDERR $progbanner.
   "\n".
   "Usage: maltfilter <pid filename> [config filename] [config filename...]\n".
   "       maltfilter -f [config filename] [config filename...]\n".
@@ -1172,6 +1215,7 @@
 $pid_file = shift;
 if ($pid_file eq "-f") {
   $reportmode = 1;
+  print STDERR $progbanner;
 } else {
   mdie("'$pid_file' already exists, not starting.\n".
   "If the daemon is NOT running, remove the pid-file and re-start.\n")
@@ -1187,11 +1231,10 @@
 
 # Open logfile
 if ($settings{"DRY_RUN"}) {
-  print $progbanner.
-  "*********************************************\n".
-  "* NOTICE! DRY-RUN MODE ENABLED! No changes  *\n".
-  "* will actually get committed to netfilter! *\n".
-  "*********************************************\n";
+  print STDERR 
+  "*********************************\n".
+  "* NOTICE! DRY-RUN MODE ENABLED! *\n".
+  "*********************************\n";
 } elsif ($settings{"LOGFILE"} ne "") {
   open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n");
   select((select($LOGFILE), $| = 1)[0]);
@@ -1204,10 +1247,7 @@
 # Fork to background, unless dry-running
 if ($settings{"DRY_RUN"}) {
   if ($reportmode) {
-    mlog(-1, "Outputting report files.\n");
-    generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
-    generate_status($settings{"STATUS_FILE_HTML"}, 1);
-    gather_evidence();
+    malt_maintenance();
     malt_cleanup();
   } else {
     malt_scan();