#!/usr/bin/perl

=pod

FILE: ssh-scanguard 
AUTHOR: Gyepi Sam <gyepi@praxis-sw.com>
DATE: 04 November 2004

Copyright (C) 2004 Gyepi Sam <gyepi@praxis-sw.com>
All rights reserved.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

=cut

use strict;
use Getopt::Long;

my $version = 0.3;

our %opts = ( 
            #how long to block an offending IP address
            blockfor => 60 * 60 * 12,
            
            #where do we store files
            datadir => '/usr/local/etc/ssh-scanguard',          
           );

my $result = GetOptions(\%opts,
              'blockfor|b=i',
              'datadir|d=s',
              'exempt|e=s',
              'verbose|v+',
              'version',
              'help|h');

exit unless $result;

if (exists $opts{help}){
  &print_usage();
  exit (0);
}

if (exists $opts{version}){
  print STDERR <<EOF;
ssh-scanguard version $version
Copyright (C) 2004 Gyepi Sam <gyepi\@praxis-sw.com>
This is free software; see the source for copying conditions.
EOF
  exit(0);
}

my $data_dir = $opts{datadir};
my $verbose = $opts{verbose} || 0;

print qq[verbosity: $verbose\n];

$data_dir =~ s:/+$::;

unless (-d $data_dir){
  warn "data directory [$data_dir] does not exist.";
  exit (1);
}

my $data_file = "$data_dir/data";
my $cache_file = $data_file . '.cache';

my $exempt_users_file = $opts{'exempt'} || qq[${data_dir}/exempt_users];

my $qip_pattern = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
my %ip;

my ($rc, $msg);

#This file does not have to exist
($rc, $msg) = &read_file($cache_file,
                         sub { $ip{$1} = $2 if $_[0] =~ m/^($qip_pattern)\s+(\d+)/ });
if ($rc){
  warn ($msg, "\n");
  exit ($rc)
}  

#This file does not have to exist either
my %exempt_users;
($rc, $msg) = &read_file($exempt_users_file,
                         sub { $exempt_users{$1} = 1 if $_[0] =~ m/^\s*(\S+)/ });
if ($rc){
  warn ($msg, "\n");
  exit ($rc)
}  
else {
  if ($verbose > 1){
    warn "exempting user $_\n" for keys %exempt_users;
  }
}

for(;;)
{
	next unless defined($_ = <>);
	chomp;
	if (/sshd\[\d+\]:\s+(?:Failed password|(?:Invalid|Illegal) user)\s+for\s(?:illegal user )?(\S+).+?from\s($qip_pattern)/){
    warn "matched: user [$1] ip [$2]\n" if $verbose;
    next if exists $exempt_users{$1}; 
		next if exists $ip{$2};
    warn "blocking ip address [$2]\n" if $verbose;
		$ip{$2}=time();
		&update_files();
	}
}

sub update_files
{
	my $time = time();
	for my $k (keys %ip)
	{
	   if (($time - $ip{$k}) > $opts{'blockfor'}){
	   	delete $ip{$k};
	   }
	}
	return unless %ip;
	&write_data($data_file,"\n", map { qq[${_}] } keys %ip);
	&write_data($cache_file,"\n", map { qq[$_ $ip{$_}] } keys %ip);
}

sub write_data
{
	my $file = shift;
	my $sep  = shift;
	my $tmp = $file . '.tmp';
	open(F, ">$tmp") or die "cannot write to file $tmp. $!\n";
	print F join($sep, @_), "\n";
	close(F);
	rename($tmp, $file) or die "cannot rename file $tmp to $file. $!\n";
}

sub read_file {
  my ($file, $callback) = @_;
  if ( -f $file ){
    if (open(F, $file)){
      while(<F>){
        &$callback($_);
      }
      close F;
      return (0);
    }
    else {
      return(1, "cannot open file [$file]. $!\n");
    }
  }
}

sub print_usage
{
  print STDERR <<EOF;
$0 -- Blocks IP addresses after failed ssh logins.

Usage:
$0 [ options ]

Options
  
--blockfor n    Blocks addresses for n seconds. Default [$opts{blockfor}].
 -b n
 
--datadir  d    Store files in d. Default [$opts{datadir}].
 -d
 
--exempt  f     Do not block addresses for users listed in file f.
 -e             Default: datadir/exempt_users, if it exists.
 
--verbose       Prints miscellaneous messages to stderr.
 -v
 
--help          You are reading it.
 -h
EOF
}
__END__

Oct 16 10:03:44 praxis-sw sshd[21074]: Failed password for root from 218.38.136.47 port 39478 ssh2
Oct 16 10:03:46 praxis-sw sshd[21076]: Failed password for root from 218.38.136.47 port 39522 ssh2
Oct 16 10:03:47 praxis-sw sshd[21078]: Illegal user test from 218.38.136.47
Nov  3 21:01:31 magnetic sshd[6878]: Failed password for illegal user test from 203.98.175.189 port 2114 ssh2
