#!/usr/bin/perl

# ************************************************************************************************
# * watcher.pl - Watches for unauthorized user login attempts and when detected, evaluates
# * whether this appears to be a malicious attack and blocks the IP with iptables.  This script
# * needs to run as root and should be set to startup at boot (or you can nohup it manually after)
# *
# * KATR Inc.
# * Author: Bill Johnson
# * Web: http://www.katr.com
# * Date: 03/26/2011
# * Version: 1.0
# * Requirements: Perl 5.x or better, Linux or compatible OS
# *
# * Disclaimer: KATR Inc. and the author provides no guarantee, warranty, or liability of the
# * suitability or viability of running this script on your server.  The individual or organization
# * utilizing this application assumes all risk and responsibility of the consequences.
# * Please review the code to make sure that the application meets your needs and complies
# * with your standards for security and safety.
# *
# * Support: You may need to update the paths or names of programs called from within this
# * script to support your particular version or OS.  Search for ENVSPECIFIC to find all
# * occurrances. You may contact the author by filling out the contact form at:
# *
# * http://www.katr.com/contactus.php
# *
# * LICENSE: Simple, you are permitted to use this personally or commercially.  Just don't take
# *          credit for it.  If you like it, link back to our site.  If you really like it, 
# *          support it by purchasing one of our 99 cent mobile apps from Apple or Google.  
# *          Just search for "KATR Software" in either market.
# ************************************************************************************************

# ************************************************************************************************
# (ENVSPECIFIC)
# Depending upon your platform/distribution, the following commands may need to be changed
# ************************************************************************************************
my $IPTABLES = "/sbin/iptables -I INPUT -s [IPADDRESS] -j DROP";
my $LOGTAIL = "tail -n0 -f /var/log/auth.log|";

# ************************************************************************************************
# (ENVSPECIFIC)
# You will probably want to change the location of this logfile
# ************************************************************************************************
my $Logfile="/root/scripts/watcher.log";      # Where to send the output.
my %rogueIPs = ();                            # The array of offending IPs
my $killEnabled = 1;                      		# Set to 1 when ready to drop connections
my $maxFailures = 3;                          # Maximum CRITICAL failures before blocking
my $criticalWindow = 60*5;                    # The number of seconds where between logins
                                              # where a login failure is considered CRITICAL

# ************************************************************************************************
# (ENVSPECIFIC)
# Depending upon your platform/distribution, the following tail command may need to be changed
# ************************************************************************************************
#open( AUTH, "tail -n0 -f /var/log/auth.log|" );
open( AUTH, $LOGTAIL );
open( LOG, ">>$Logfile" );
printf(LOG "%s Watcher starting, kill is %s\n", now(), ($killEnabled?"enabled":"DISABLED" ));

# ************************************************************************************************
# * Turn off output buffer to the LOG
# ************************************************************************************************
my $ofh = select LOG;
$| = 1;
select $ofh;

# ************************************************************************************************
# * Read from the tail pipe...this is a "forever" loop as configured today.
# ************************************************************************************************
while( <AUTH> )
{
  
  # ************************************************************************************************
  # * We match on the following two patterns from auth.log:
  # * Pattern: Failed password for root from 119.149.189.52 port 44095 ssh2
  # * Pattern: Invalid user magic from 64.182.46.126
  # ************************************************************************************************
  if ( /(Failed) password for ([^\s]+) from ([^\s]+) / || /(Invalid) user ([^\s]+) from ([^\s]+)/)
  {
    my $type=$1;               # Will be "Failed" or "Invalid"
    my $user=$2;               # Username recorded in the log
    my $ip=$3;                 # IP recorded in the log
    my $critical = 0;          # The number of CRITICAL failures for this IP
    my $time = time();         # The current time since epoch
    my $flag = "";             # A flag for indicating if this was counted as CRITICAL or NOT
    my $delta = 0;             # Time delta since the last failure for this IP

    # ************************************************************************************************
    # * If we haven't seen this IP before, add it to the list
    # ************************************************************************************************
    if ( !exists $rogueIPs{$ip} )
    {
      %detail=("time"=>$time, "total"=>1, "critical"=>1 );
      $rogueIPs{$ip}=$detail;
      $critical = 1;
      $flag="NEW";
      #printf(LOG "%s IP: %s, User: %s - First failure (%s)\n", now(), $ip, $user, $type);
    }
    else
    {
      # ************************************************************************************************
      # * We have seen this IP, now "guess" as to it's intentions
      # ************************************************************************************************
      $flag="ADD";
      $detail=$rogueIPs{$ip};
      $detail{"total"}++;
      $delta = $time - $detail{"time"};

      # ************************************************************************************************
      # * If this IP had a previous failure within a set period of time, increment the critical count.
      # ************************************************************************************************
      if ( $delta < $criticalWindow )
      {
        $detail{"critical"}++;
      }
      # ************************************************************************************************
      # * Enough time has elapsed, do not count as CRITICAL, but count and keep track nevertheless
      # ************************************************************************************************
      else
      {
        $detail{"critical"}=1;
        $flag="RESET";
      }

      # ************************************************************************************************
      # * Update our IP list.
      # ************************************************************************************************
      $detail{"time"} = $time;
      $critical = $detail{"critical"};
      $rogueIPs{$ip}=$detail;

    }

    printf( LOG "%s IP: %s, User: %s, Total count: %d, Critical count: %d (%s)(%s)\n", now(), $ip, $user, $detail{"total"}, $detail{"critical"}, $flag, $type);

    # ************************************************************************************************
    # * If this IP has exceeded the number of critical failures in the given time window, block it!
    # ************************************************************************************************
    if ( $critical > $maxFailures )
    {
      # **************************************************************************************************
      # * To reset iptables: iptables --flush INPUT
      # **************************************************************************************************
      $cmd = $IPTABLES;
      $cmd =~ s/\[IPADDRESS\]/$ip/g;
      if ( $killEnabled )
      {
        printf(LOG "%s IP %s has exceeded maximum failures\nCMD: %s\n", now(), $ip, $cmd );
        system($cmd);
      }
      else
      {
        printf(LOG "%s IP %s has exceeded maximum failures\n(KILL IS DISABLED) CMD: %s\n", now(), $ip, $cmd );
      }
    }
  }
}

close(AUTH);
close(LOG);

# ************************************************************************************************
# * now() is a handy function for formatting the current date time for logging purposes.
# ************************************************************************************************
sub now()
{
  my ($second, $minute, $hour, $day, $month, $year, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime(time());
  $year += 1900;
  $month += 1;
  $time = sprintf( "%04d-%02d-%02d %02d:%02d:%02d",$year,$month,$day,$hour,$minute,$second );
  return( $time );
}

