#!/usr/bin/perl -T
# Copyright (c) 2003, 2004, 2005
#     Stephan Schmieder <ssc@unix-geek.info>.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY STEPHAN SCHMIEDER AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL STEPHAN SCHMIEDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

use strict;

#use warnings;
#use diagnostics;

my ( $alertfile, $pfctl, $tail, $amnesty, %bad_hosts, $tmp, $size );

# <configuration>
$alertfile = '/var/log/snort/alert';
$pfctl     = '/sbin/pfctl';
$tail      = '/usr/bin/tail';
$amnesty   = 3600;
# </configuration>
my $version   = '3.4';

&initalize();

while ( sleep(1) ) {

    # unblock old hosts
    &unblock( time - $amnesty );

    # process alertfile if modified
    $tmp = ( stat($alertfile) )[7];
    if ( $tmp > $size ) {
        open( DATA, "$tail -c ".($tmp - $size)." $alertfile |" )
          or die("Unable to read last bytes from alertfile ($!)\n");
        $size = $tmp;
        while ( defined( $tmp = <DATA> ) ) {
            chomp $tmp;
            if ($tmp) {
                &block( &check_for_attack( &check_for_portscan($tmp) ) );
            }
        }
        close(DATA);
    }
}

sub initalize {

    # parse command line
    for ( my $i = 0; $i < $#ARGV; $i++ ) {
        if ( $ARGV[$i] eq '-s' ) {
            if ( $ARGV[++$i] =~ /^(?:\d+)$/ ) {
                $amnesty = $1;
            } else {
                die("insane option for \"-s\"");
            }
        } elsif ( $ARGV[$i] eq '-f') {
            if ( $ARGV[++$i] =~ /^([0-9A-z\.\/]+)$/ ) {
                $alertfile = $1;
            } else {
                die("insane option for \"-f\"");
            }
        }
    }

    # set/check environment
    delete @ENV{qw(PATH IFS CDPATH ENV)};
    &update_title();
    if ( !-r $alertfile ) {
        die("\"$alertfile\" is not readable");
    }
    if ( !-x $pfctl ) {
        die("\"$pfctl\" is not executeable");
    }
    if ( !-x $tail ) {
        die("\"$tail\" is not executeable");
    }
    $size = ( stat($alertfile) )[7];
}

sub update_title {
  # update the string that's shown by ps(1)
  my $hosts = scalar keys %bad_hosts;
  $0 = 'Snort2Pf '.$version.' :: blocking '.(scalar keys %bad_hosts).' hosts';
}

sub check_for_attack {
    if ( $_[0] =~ /^(?:(?:\d{2}(?:\/|\-|\:|\.)){5}\d{6}\ ){0,1}((?:\d{1,3}\.){3}\d{1,3})(?:\:(?:\d{1,5})) \-\> / ) {
        $_[0] = $1;
    }
    return ( $_[0] );
}

sub check_for_portscan {
    if ( $_[0] =~ /PORTSCAN DETECTED from ((?:\d{1,3}\.){3}\d{1,3})[: ]/i ) {
        $_[0] = $1;
    }
    return ( $_[0] );
}

sub block {
    # validate IPv4 address
    my $tmp = $_[0];
    if ( $tmp =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ ) {
        if (    $1 >= 1
            and $1 <= 255
            and $1 != 127
            and $2 >= 0
            and $2 <= 255
            and $3 >= 0
            and $3 <= 255
            and $4 >= 1
            and $4 <= 254 )
        {
            # block host
            $bad_hosts{$tmp} = time;
            open( PFCTL, "| $pfctl -a snort2pf/$tmp -f -" )
              or warn("Can't block $tmp($!)\n");
            print PFCTL "block in quick from $tmp to any\n";
            close(PFCTL) or die("Can't write to pfctl pipe($!)\n");
        }
    }
    &update_title();
}

sub unblock {
    foreach $tmp ( keys %bad_hosts ) {
        if ( $bad_hosts{$tmp} <= $_[0] ) {
            delete $bad_hosts{$tmp};
            !system("\"$pfctl\" -a snort2pf/$tmp -F rules")
              or warn("Can't unblock $tmp($!)\n");
        }
    }
    &update_title();
}
