view maltfilter @ 17:fe220b5a975a

Cleanups, add configuration for WHOIS linking.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 15 Aug 2009 20:42:16 +0300
parents 87c0cdc048f5
children b0017a324040
line wrap: on
line source

#!/usr/bin/perl -w
#############################################################################
#
# Malicious Attack Livid Termination Filter daemon (maltfilter)
# Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
# (C) Copyright 2009 Tecnic Software productions (TNSP)
#
#############################################################################
use strict;
use Date::Parse;
use Net::IP;

my $progversion = "0.9";
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";

#############################################################################
### Settings / configuration
#############################################################################
my %settings = (
  "VERBOSITY" => 3,
  "DRY_RUN" => 1,
  "WEEDPERIOD" => 150,
  "TRESHOLD" => 3,
  "ACTION" => "DROP",
  "LOGFILE" => "",
  "IPTABLES" => "/sbin/iptables",

  "STATUS_FILE_PLAIN" => "",
  "STATUS_FILE_HTML" => "",
  "STATUS_FILE_CSS" => "",

  "WHOIS_URL" => "http://whois.domaintools.com/",

  "CHK_SSHD"            => 1,
  "CHK_KNOWN_CGI"       => 1,
  "CHK_PHP_XSS"         => 1,
  "CHK_PROXY_SCAN"      => 1,
  "CHK_ROOT_SSH_PWD"    => 0,
  "CHK_GOOD_HOSTS"      => "",
);

# Default logfiles to monitor (SCANFILES setting of configuration overrides these)
my @scanfiles_def = (
  "/var/log/auth.log",
  "/var/log/httpd/error.log",
  "/var/log/httpd/access.log"
);

my @noblock_ips_def = (
  "127.0.0.1",
);

#############################################################################
### Script code
#############################################################################
my $reportmode = 0;      # Full report mode
my @scanfiles = ();      # Files to scan
my @noblock_ips = ();    # IPs not to block
my %filehandles = ();    # Global hash holding opened scanned log filehandles
my $pid_file = "";       # Name of Maltfilter daemon pid file
my $LOGFILE;             # Maltfilter logfile handle

# IPs currently blocked in Netfilter $blocklist{$ip} = date
my %blocklist = ();      

# Gathered information about hosts
# $statlist{$ip}->
# "date1"    = timestamp of first hit
# "date2"    = timestamp of latest hit
# "hits"     = number of hits to this IP
# $statlist{$ip}{"reason"}{$class}->
#   "msg"    = reason message (array if $reportmode)
#   "hits"   = hits to this class
#   "date1"  = timestamp of first hit
#   "date2"  = timestamp of latest hit
my %statlist = ();

# Gathered information about ignored hits (e.g. hits for tests that are not enabled)
# Same fields as in %statlist
my %ignorelist = ();


### Check given logfile line for matches
sub check_log_line($)
{
  # (1) SSHD scans
  if (/^(\S+\s+\d+\s+\d\d:\d\d:\d\d)\s+\S+\s+sshd\S*?: (.*)/) {
    my $mdate = $1;
    my $merr = $2;
    
    # (1.1) Generic login scan attempts
    if ($merr =~ /^Failed password for invalid user \S+ from (\d+\.\d+\.\d+\.\d+)/) {
      check_add_hit($1, $mdate, "SSH login scan", "", $settings{"CHK_SSHD"});
    }
    # (1.2) Root SSH login password bruteforcing attempts
    # NOTICE! Do not enable this setting, if you allow SSH root logins via
    # password authentication! Mistyping password may get you blocked then. :)
    elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) {
      check_add_hit($1, $mdate, "Root SSH password bruteforce", "", $settings{"CHK_ROOT_SSH_PWD"});
    }
  }
  # (2) Common/known exploitable 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
  # paths and prefixes.
  elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) {
    my $mdate = $1;
    my $mip = $2;
    my $merr = $3;
    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"});
      }
    }
  }
  # (3) Match Apache common logging format GET requests here
  elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) {
    my $mdate = $2;
    my $mip = $1;
    my $merr = $3;
    
    # (3.1) Simple match for generic PHP XSS vulnerability scans
    # NOTICE! If your site genuinely uses (checked) PHP parameters with
    # URIs, you should set CHK_GOOD_HOSTS to match your hostname(s)/IP(s)
    # used in the URIs.
    if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
      if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
        check_add_hit($mip, $mdate, "PHP XSS", $merr, $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"});
      }
    }
  }
}


#############################################################################
### Status output functionality
#############################################################################
sub urlencode($)
{
  my $value = $_[0];
  $value =~ s/([^a-zA-Z_0-9 ])/"%" . uc(sprintf "%lx" , unpack("C", $1))/eg;
  $value =~ tr/ /+/;
  return $value;
}

my %entities = (
  "<" => "lt",
  ">" => "gt",
  "&" => "amp",
);

sub htmlentities($)
{
  my $value = $_[0];
#  $value =~ s/([keys %entities])/"&".$entities{$1}.";"/eg;
  foreach my $val (keys %entities) {
    $value =~ s/$val/\&$entities{$val}\;/g;
  }
  return $value;
}

sub printH($$$$)
{
  my $fh = $_[1];
  if ($_[0]) {
    print $fh "<h".$_[2].">".$_[3]."</h".$_[2].">\n";
  } else {
    my $c = ($_[2] <= 1) ? "=" : "-";
    print $fh $_[3]."\n". $c x length($_[3]) ."\n";
  }
}

sub printTD($$$)
{
  my $fh = $_[1];
  if ($_[0]) {
    print $fh "<td>".$_[2]."</td>";
  } else {
    print $fh $_[2];
  }
}

sub printP($$$)
{
  my $fh = $_[1];
  if ($_[0]) {
    print $fh "<p>\n".$_[2]."</p>\n";
  } else {
    print $fh $_[2]."\n";
  }
}

sub printElem
{
  my $fh = $_[1];
  if ($_[0]) {
    print $fh $_[2];
  } elsif (defined($_[3])) {
    print $fh $_[3];
  }
}

sub bb($)
{
  return $_[0] ? "<b>" : "";
}


sub eb($)
{
  return $_[0] ? "</b>" : "";
}

sub pe($$)
{
  return $_[0] ? "<$_[1]>" : "";
}

sub getLink($$)
{
  if ($settings{"WHOIS_URL"} ne "") {
    return $_[0] ? "<a href=\"".$settings{"WHOIS_URL"}.$_[1].
      "\">".htmlentities($_[1])."</a>" : $_[1];
  } else {
    return $_[0];
  }
}

sub printTable1($$$$$)
{
  my ($m, $f, $table, $keys, $func) = @_;
  my $ntotal = 0;

  printElem($m, $f,
  "<table class=\"detailed\">\n".
  "<tr><th>Hits</th><th>IP-address</th><th>First hit</th><th>Latest hit</th><th>Reason(s)</th></tr>\n",

  "Hits       | IP-address      | First hit                | Latest hit               | Reason(s)\n"
  );
  
  foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) {
    printElem($m, $f, " <tr>");
    printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"}));
    printElem(!$m, $f, " | ");
    printTD($m, $f, sprintf("%-15s", getLink($m, $mip)));
    printElem(!$m, $f, " | ");
    printTD($m, $f, scalar localtime($table->{$mip}{"date1"}));
    printElem(!$m, $f, " | ");
    printTD($m, $f, scalar localtime($table->{$mip}{"date2"}));
    printElem(!$m, $f, " | ");
    my @reasons = ();
    foreach my $class (sort keys %{$table->{$mip}{"reason"}}) {
      my $msgs;
      if ($reportmode) {
        my @tmp = @{$table->{$mip}{"reason"}{$class}{"msg"}};
        if ($#tmp > 5) { $#tmp = 5; }
        foreach (@tmp) { $_ = htmlentities($_); }
        $msgs = join(" ".bb($m)."|".eb($m)." ", @tmp);
      } else {
        $msgs = $table->{$mip}{"reason"}{$class}{"msg"};
      }
      push(@reasons, bb($m).$class.eb($m)." #".$table->{$mip}{"reason"}{$class}{"hits"}.
        " ( ".$msgs." )");
    }
    printTD($m, $f, join(", ", @reasons));
    printElem($m, $f, "</tr>\n", "\n");
    $ntotal++;
  }
  printElem($m, $f, "</table>\n");
  printP($m, $f, bb($m).$ntotal.eb($m)." entries total.\n");
}


sub printTable2($$$$$)
{
  my ($m, $f, $table, $keys, $func) = @_;
  my $nhits = 0;
  my $str = "<th>IP-address</th><th>Hits</th><th>Latest hit</th><th>Class</th>";
  my $str2 = "IP-address      | Hits      | Latest hit               | Class                         ";

  printElem($m, $f,
  "<table class=\"summary\">\n<tr>". $str."<th> </th>".$str ."</tr>\n",
  $str2." || ".$str2."\n");
  
  my $printEntry = sub {
    printTD($m, $f, sprintf("%-15s", getLink($m, $_[0])));
    printElem(!$m, $f, " | ");
    printTD($m, $f, sprintf("%-8d ", $table->{$_[0]}{"hits"}));
    printElem(!$m, $f, " | ");
    printTD($m, $f, scalar localtime($table->{$_[0]}{"date2"}));
    printElem(!$m, $f, " | ");
    my $tmp = join(", ", sort keys %{$table->{$_[0]}{"reason"}});
    printTD($m, $f, sprintf("%-30s", $tmp));
    $nhits += $table->{$_[0]}{"hits"};
  };

  my @mkeys = sort { $func->($table, $a, $b) } keys %{$keys};
  my $nkeys = scalar @mkeys;
  my $kmax = $nkeys / 2;
  for (my $i = 0; $i <= $kmax; $i++) {
    printElem($m, $f, " <tr>");
    if ($i < $kmax) { $printEntry->($mkeys[$i]); }
    printElem($m, $f, "<th> </th>", " || ");
    if ($i + $kmax + 1 < $nkeys) { $printEntry->($mkeys[$i + $kmax + 1]); }
    printElem($m, $f, "</tr>\n", "\n");
  }

  printElem($m, $f, "</table>\n");
  printP($m, $f, bb($m).$nkeys.eb($m)." entries total, ".bb($m).$nhits.eb($m)." hits total.\n");
}

sub cmp_ips($$$)
{
  my @ipa = split(/\./, $_[1]);
  my @ipb = split(/\./, $_[2]);
  for (my $i = 0; $i < 4; $i++) {
    return -1 if ($ipa[$i] > $ipb[$i]);
    return  1 if ($ipa[$i] < $ipb[$i]);
  }
  return 0;
}

sub cmp_hits($$$)
{
  return $_[0]->{$_[2]}{"hits"} <=> $_[0]->{$_[1]}{"hits"};
}


sub generate_status($$)
{
  my $filename = shift;
  my $m = shift;
  
  return unless ($filename ne "");

  open(STATUS, ">", $filename) or die("Could not open '".$filename."'!\n");
  my $f = \*STATUS;
  my $mtime = scalar localtime();
  
  printElem($m, $f, "
<html>
<head>
 <title>Maltfilter status report</title>
");

  printElem($m, $f, "<link href=\"".$settings{"STATUS_FILE_CSS"}."\" rel=\"stylesheet\" type=\"text/css\" />")
  if ($settings{"STATUS_FILE_CSS"});

  printElem($m, $f, "
</head>
<body>
");

  printH($m, $f, 1, "Maltfilter v$progversion status report");
  my $val = $settings{"WEEDPERIOD"};
  my $period;

  if ($val > 30 * 24) {
    $period = sprintf("%1.1f months", $val / (30.0 * 24.0));
  } elsif ($val > 24 * 7) {
    $period = sprintf("%1.1f weeks", $val / 24);
  } elsif ($val > 24) {
    $period = sprintf("%d days", $val / 24);
  } else {
    $period = sprintf("%d hours", $val);
  }

  printP($m, $f,
  "Generated ".bb($m).$mtime.eb($m).". Data computed from ".
  ($reportmode ? "complete logfile scan" : "a period of last $period").".\n");

  printH($m, $f, 2, "Currently blocked entries");
  printP($m, $f, "List of IPs that are currently blocked (or would be, if this is a report-only mode).");
  printTable1($m, $f, \%statlist, \%blocklist, \&cmp_hits);

  printH($m, $f, 2, "Summary of non-ignored entries");
  printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not necessarily acted upon.\n");
  printTable2($m, $f, \%statlist, \%statlist, \&cmp_ips);

  printH($m, $f, 2, "Ignored entries");
  printP($m, $f, "List of hits that were ignored (not acted upon), because the test was disabled.\n");
  printTable1($m, $f, \%ignorelist, \%ignorelist, \&cmp_hits);

  printElem($m, $f, "</body>\n</html>\n");
  close(STATUS);
}


#############################################################################
### Entry management / handling functions
#############################################################################
### Check if given IP or host exists in array
sub check_hosts_array($$)
{
  my $chk_host = $_[1];
  my $chk_ip = new Net::IP($chk_host);
  foreach my $host (@{$_[0]}) {
    if ($chk_host eq $host) {
      return 1;
    }
    my $ip = new Net::IP($host);
    if (defined($chk_ip) && defined($ip)) {
      if ($chk_ip->binip() eq $ip->binip()) {
        return 1;
      }
    }
  }
  return 0;
}

### Check IP/host against | separated list of IPs/hosts
sub check_hosts($$)
{
  my @tmp = split(/\s*\|\s*/, $_[0]);
  return check_hosts_array(\@tmp, $_[1]);
}

### Execute iptables
sub exec_iptables(@)
{
  my @args = ($settings{"IPTABLES"}, @_);
  if ($settings{"DRY_RUN"}) {
    mlog(3, ":: ".join(" ", @args)."\n");
  } else {
    system(@args) == 0 or print join(" ", @args)." failed: $?\n";
  }
}

### Get current Netfilter INPUT table entries that match
### entry types we manage, e.g. blocklist
sub update_blocklist($)
{
  my $mdate = $_[0];
  $ENV{"PATH"} = "";
  open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or
    die("Could not execute ".$settings{"IPTABLES"}."\n");
  %blocklist = ();
  undef(%blocklist);
  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*$/) {
      my $mip = $2;
      if (!defined($blocklist{$mip}) && $mdate > 0) {
        mlog(2, "* $mip appeared in iptables.");
      }
      $blocklist{$2} = $mdate;
      update_entry(\%statlist, $mip, $mdate, "?", "From iptables.");
    }
  }
  close(STATUS);
}

### Check if given timestamp is _newer_ than weedperiod threshold.
### Returns false if timestamp is over weed period, e.g. needs weeding.
sub check_time($)
{
  return ($_[0] >= time() - ($settings{"WEEDPERIOD"} * 60 * 60));
}

### Weed out old entries
sub weed_do($)
{
  my $mtime = $blocklist{$_[0]};
  mlog(2, "* Weeding $_[0] (".($mtime >= 0 ? scalar localtime($mtime) : $mtime)."\n");
  exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"});
  delete($blocklist{$_[0]});
  delete($statlist{$_[0]});
  delete($ignorelist{$_[0]});
}

sub weed_entries()
{
  # Don't weed in report mode.
# return if ($reportmode);

  my @mips = keys %blocklist;
  foreach my $mip (@mips) {
    if (defined($blocklist{$mip})) {
      if ($blocklist{$mip} >= 0) {
        weed_do($mip) unless check_time($blocklist{$mip});
      } else {
        weed_do($mip);
      }
    }
  }
}

### Update one entry of
sub update_entry($$$$$)
{
  my ($struct, $mip, $mdate, $mclass, $mreason) = @_;

  my $cnt = $struct->{$mip}{"hits"}++;
  $struct->{$mip}{"reason"}{$mclass}{"hits"}++;

  if ($reportmode) {
    push(@{$struct->{$mip}{"reason"}{$mclass}{"msg"}}, $mreason);
  } else {
    $struct->{$mip}{"reason"}{$mclass}{"msg"} = $mreason;
  }

  if (!defined($struct->{$mip}{"date1"})) {
    $struct->{$mip}{"date1"} = $mdate;
  }
  $struct->{$mip}{"date2"} = $mdate;
    
  if (!defined($struct->{$mip}{"reason"}{$mclass}{"date2"})) {
    $struct->{$mip}{"reason"}{$mclass}{"date2"} = $mdate;
  }
  $struct->{$mip}{"reason"}{$mclass}{"date2"} = $mdate;

  return $cnt;
}

### Check if given "try count" exceeds treshold and if entry
### is NOT in Netfilter already, then add it if so.
sub check_add_hit($$$$$)
{
  my $mip = $_[0];
  my $mdate = str2time($_[1]);
  my $mclass = $_[2];
  my $mreason = $_[3];
  my $mcond = $_[4];
  my $cnt;

  if (check_hosts_array(\@noblock_ips, $mip)) {
    mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n");
    return;
  }
  
  # If condition is true, we add to regular statlist  
  if ($mcond) {
    $cnt = update_entry(\%statlist, $mip, $mdate, $mclass, $mreason);
  } else {
    # This is an ignored hit (for disabled test), add to ignorelist
    update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason);
    return;
  }

  # Check if we have exceeded treshold etc.
  if ($cnt >= $settings{"TRESHOLD"} && check_time($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"});
    }
    # Update date of last hit
    $blocklist{$mip} = $mdate;
  }
}


#############################################################################
### Main helper functions
#############################################################################
### Print log entry
sub mlog
{
  my $level = shift;
  my $msg = shift;
  if (defined($LOGFILE)) {
    print $LOGFILE "[".scalar localtime()."] ".$msg if ($settings{"VERBOSITY"} > $level);
  } elsif ($settings{"DRY_RUN"}) {
    print STDERR $msg if ($settings{"VERBOSITY"} > $level);
  }
}

### Initialize
sub malt_init {
  mlog(0, "Updating initial blocklist from netfilter.\n");
  update_blocklist(-1);

  foreach my $filename (@scanfiles) {
    local *INFILE;
    mlog(0, "Parsing ".$filename." ...\n");
    open(INFILE, "<", $filename) or die("Could not open '".$filename."'!\n");
    $filehandles{$filename} = *INFILE;
    while (<INFILE>) {
      chomp;
      check_log_line($_);
    }
  }
  
  mlog(0, "Weeding old entries.\n");
  weed_entries();
}

### Quick cleanup (not complete shutdown)
sub malt_cleanup {
  foreach my $filename (keys %filehandles) {
    close($filehandles{$filename});
  }
}

sub malt_finish {
  # Unlink pid-file
  if ($pid_file ne "" && -e $pid_file) {
    unlink $pid_file;
  }
  # Close logfile
  close($LOGFILE) if (defined($LOGFILE));
  undef($LOGFILE);
}

### Signal handlers
sub malt_int {
  mlog(-1, "\nCaught Interrupt (^C), aborting.\n");
  malt_cleanup();
  malt_finish();
  exit(1);
}

sub malt_term {
  mlog(-1, "Received TERM, quitting.\n");
  malt_cleanup();
  malt_finish();
  exit(1);
}

sub malt_hup {
  mlog(-1, "Received HUP, reinitializing.\n");
  malt_cleanup();
  malt_init();
  mlog(-1, "Reinitialization finished, resuming scanning.\n");
}

### Main scanning function
sub malt_scan {
  mlog(1, "Entering main scanning loop.\n");
  my $counter = -1;
  while (1) {
    my %filepos = ();
    foreach my $filename (keys %filehandles) {
      for ($filepos{$filename} = tell($filehandles{$filename}); $_ = <$filehandles{$filename}>; $filepos{$filename} = tell($filehandles{$filename})) {
        chomp;
        check_log_line($_);
      }
    }
    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.
      $counter = 0;
      update_blocklist(time());
      weed_entries();
      generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
      generate_status($settings{"STATUS_FILE_HTML"}, 1);
    }
    sleep(5);
    foreach my $filename (keys %filehandles) {
      seek($filehandles{$filename}, $filepos{$filename}, 0);
    }
  }
}

### Read one configuration file
sub malt_read_config($)
{
  my $filename = $_[0];
  my $errors = 0;
  my $line = 0;

  open(CONFFILE, "<", $filename) or die("Could not open configuration '".$filename."'!\n");
  while (<CONFFILE>) {
    $line++;
    chomp;
    if (/(^\s*#|^\s*$)/) {
      # Ignore comments and empty lines
    } elsif (/^\s*\"?([a-zA-Z0-9_]+)\"?\s*=>?\s*(\d+),?\s*$/) {
      my $key = uc($1);
      my $value = $2;
      if (defined($settings{$key})) {
        $settings{$key} = $value;
      } else {
        print STDERR "[$filename:$line] Unknown setting '$key' = $value\n";
        $errors = 1;
      }
    } elsif (/^\s*\"?([a-zA-Z0-9_]+)\"?\s*=>?\s*\"(.*?)\",?\s*$/) {
      my $key = uc($1);
      my $value = $2;
      if ($key eq "SCANFILE") {
        push(@scanfiles_def, $value);
      } elsif ($key eq "NOBLOCK_IPS") {
        push(@noblock_ips_def, $value);
      } elsif (defined($settings{$key})) {
        $settings{$key} = $value;
      } else {
        print STDERR "[$filename:$line] Unknown setting '$key' = '$value'\n";
        $errors = 1;
      }
    } else {
      print STDERR "[$filename:$line] Syntax error: $_\n";
      $errors = 1;
    }
  }
  close(CONFFILE);
  return $errors;
}


#############################################################################
###
### Main program
###
#############################################################################
# Setup signal handlers
$SIG{'INT'} = 'malt_int';
$SIG{'TERM'} = 'malt_term';
$SIG{'HUP'} = 'malt_hup';

# Print banner and help if no arguments
my $argc = $#ARGV + 1;
if ($argc < 1) {
  print $progbanner.
  "\n".
  "Usage: maltfilter <pid filename> [config filename] [config filename...]\n".
  "       maltfilter -f [config filename] [config filename...]\n".
  "-f turns on the full report mode.\n";
  exit;
}

# Test pid file existence unless report mode
$pid_file = shift;
if ($pid_file eq "-f") {
  $reportmode = 1;
} else {
  die("'$pid_file' already exists, not starting.\n".
  "If the daemon is NOT running, remove the pid-file and re-start.\n")
  if (-e $pid_file);
}

# Read configuration files
if (defined(my $filename = shift)) {
  # Let user define his/her own logfiles to scan
  @scanfiles_def = ();
  undef(@scanfiles_def);
  die("Errors in configuration file '$filename', bailing out.\n")
    unless (malt_read_config($filename) == 0);
}

# Force dry run mode if we are reporting only
if ($reportmode) {
  $settings{"DRY_RUN"} = 1;
  $settings{"VERBOSITY"} = 1;
}

# Clean up certain arrays duplicate entries
my %saw = ();
@scanfiles = grep(!$saw{$_}++, @scanfiles_def);

%saw = ();
@noblock_ips = grep(!$saw{$_}++, @noblock_ips_def);
undef(%saw);

# 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";
} elsif ($settings{"LOGFILE"} ne "") {
  open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n");
  mlog(-1, "Log started\n");
}

# Test existence of iptables
if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) {
  my $msg = "iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n";
  mlog(-1, $msg);
  die($msg);
}

mlog(-1, "Not blocking following IPs: ".join(", ", @noblock_ips)."\n");

# Initialize
malt_init();

# 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);
    malt_cleanup();
  } else {
    malt_scan();
    malt_cleanup();
  }
} else {
  if (my $pid = fork) {
    open(PIDFILE, ">", $pid_file) or die("Could not open pid file '".$pid_file."' for writing!\n");
    print PIDFILE "$pid\n";
    close(PIDFILE);
  } else {
    malt_scan();
    malt_cleanup();
  }
}