Mercurial > hg > maltfilter
view maltfilter @ 23:cb0a4b747cf0
Handle importing of current netfilter entries differently.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sat, 15 Aug 2009 23:05:29 +0300 |
parents | 403cb1a110ce |
children | f22cfa761753 |
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.2"; 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 ($class ne "IPTABLES") { if ($reportmode) { my @tmp = reverse(@{$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"); printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n". "blocked IP that was in Netfilter before Maltfilter was started.\n"); printH($m, $f, 2, "Currently blocked entries"); printP($m, $f, "List of IPs that are currently blocked (or would be, if this is\n". "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\n". "necessarily acted upon. Sorted by descending IP address.\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() { $ENV{"PATH"} = ""; open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or die("Could not execute ".$settings{"IPTABLES"}."\n"); my %newlist = (); 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*$/) { my $mip = $2; my $mdate = time(); if (!defined($blocklist{$mip})) { mlog(2, "* $mip appeared in iptables.\n"); $blocklist{$2} = $mdate; } $newlist{$2} = $mdate; update_entry(\%statlist, $mip, $mdate, "IPTABLES", ""); } } close(STATUS); foreach my $mip (keys %blocklist) { if (!defined($newlist{$mip})) { mlog(2, "* $mip removed from iptables.\n"); delete($blocklist{$mip}); } } } ### 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 ($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(); 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(); 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(); } }