#!/usr/bin/perl -w 
#
# Description: Port Scan Attack Detector (psad)
# - uses ipchains/iptables logs.  Uses input from /var/log/psad/fwdata 
# generated by kmsgsd
#
# version 0.9.6
# Copyright (C) 1999-2002 Michael B. Rash (mbr@cipherdyne.com)
#
# Credits:  (see the CREDITS file)
#
#    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
#
# TODO: 
#   - Make use of other logging options available in iptables to detect
#     more tcp signatures.  (E.g. --log-tcp-options, --log-ip-options,
#     --log-tcp-sequence, etc.) for better signature recognition.
#   - Allow ipchains to use udp signatures as well as tcp signatures that
#     only require a syn packet to a port.
#   - Deal with the possibility that psad could eat lots of memory over
#     time if $ENABLE_PERSISTENCE="Y". This should involve periodically
#     deleting entries in %Scan (or maybe the entire hash), but this 
#     should be done in a way that allows some scan data to persist.
#   - Put source and destination ip addresses back into psad_signatures.
#   - Ipfilter support on *BSD platforms.
#   - Re-write significant components (kmsgsd, diskmond, psadwatchd) in C.
#   - Possibly add a daemon to take into account ACK PSH, ACK FIN, RST etc.
#     packets that the client may generate after the ip_conntrack module
#     is reloaded.  Without anticipating such packets psad will interpret
#     them as a belonging to a port scan.  NOTE: This problem is mostly
#     corrected by the conntrack patch to the kernel.
#   - Improve check_firewall_rules() to check for a state rule (iptables) 
#     since having such a rule greatly improves the quality of the data 
#     stream provided to psad by kmsgsd since more packet types will be 
#     denied without requiring overly complicated firewall rules to detect 
#     odd tcp flag combinations.
#   - Investigate the possibility of passive OS fingerprinting by looking
#     at TTL and other fields in the headers (good idea Jay).
#   - Enhance the check_firewall_rules() routine to correct any problems
#     with the firewall ruleset.  This is part of the integration of psad 
#     with Bastille Linux.
#   - (psad-init) make sure the init script will work on different Linux
#     distros.
#   - perldoc
#
# Sample packet (rejected by ipchains)
# Dec 19 11:54:07 orthanc kernel: Packet log: input REJECT lo PROTO=1
# 10.0.0.4:3127.0.0.1:3 L=88 S=0xC0 I=49513 F=0x0000 T=255
#
# Sample tcp packet (rejected by iptables... --log-prefix = "DENY")
# Mar 11 13:15:52 orthanc kernel: DENY IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00
# SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=44847 
# DPT=35 WINDOW=32304 RES=0x00 SYN URGP=0
#
# Sample icmp packet rejected by iptables
# Nov 27 15:45:51 orthanc kernel: DENY IN=eth1 OUT= MAC=00:a0:cc:e2:1f:f2:00:20:78:10:70:e7:08:00
# SRC=192.168.10.20 DST=192.168.10.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 
# CODE=0 ID=61055 SEQ=256
#
# There may be a bug in iptables, since occasionally log entries are generated 
# by a long port scan like this (note there is no 'DPT' field): 
#   Mar 16 23:50:25 orthanc kernel: DENY IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 
#   SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=39935 
#   DINDOW=32304 RES=0x00 SYN URGP=0
#################################################################################################

### modules used by psad
use Psad;
use File::stat "stat";
use File::Copy;
use Getopt::Long "GetOptions";
use Data::Dumper;
use Socket;
use Sys::Hostname "hostname";
use POSIX;
use strict;

### could implement autouse for these modules, but is the performance hit worth it?
# use autouse 'Data::Dumper' => "Dumper";  ### this one is not used very much so don't load immediately
# use autouse 'Socket';       ### not used until a scan is detected so don't load until it is needed

# use Sys::Syslog qw(openlog syslog closelog);   ### this does not seem to play nice with perl-5.005_03

### globals
use vars qw($VERSION %Scan $PRINT_SCAN_HASH $USE_IPCHAINS $USE_IPTABLES $DEBUG $HOSTNAME $PSADCMD);

#==================== config ======================= ## do not remove this line (used by install.pl to preserve configs) ##
### Initialize config variables.  Some can be overriden with command line args
my $PSAD_DIR                  = "/var/log/psad";
my $PSAD_LOGFILE              = "${PSAD_DIR}/scanlog";
my $FW_DATA                   = "${PSAD_DIR}/fwdata";
my $ERROR_LOG                 = "${PSAD_DIR}/fwerrorlog";
my $EMAIL_ALERTFILE           = "${PSAD_DIR}/email_alert";
$PRINT_SCAN_HASH              = "${PSAD_DIR}/scan_hash";  ### Global for sub print_scan.
my $CHECK_INTERVAL            = 15;                       ### (seconds).
my $PORT_RANGE_SCAN_THRESHOLD = 1;
my $ENABLE_PERSISTENCE        = "Y";    ### If "Y", means that scans will never timeout.
my $SCAN_TIMEOUT              = 3600;   ### This is used only if $ENABLE_PERSISTENCE = "N";
my $SHOW_ALL_SIGNATURES       = "N";    ### If "Y", means all signatures will be shown since the scan started instead of just the current ones.
my %DANGER_LEVELS;                      ### Scope the %DANGER_LEVELS hash.
$DANGER_LEVELS{'1'}           = 5;      ### Number of packets.
$DANGER_LEVELS{'2'}           = 50;
$DANGER_LEVELS{'3'}           = 1000;
$DANGER_LEVELS{'4'}           = 5000;
$DANGER_LEVELS{'5'}           = 10000;
my $ENABLE_EMAIL_ALERTS       = "Y";
my $EMAIL_ALERT_DANGER_LEVEL  = 1;                  ### Send email alert if danger level >= to this value.
my $EMAIL_LIMIT               = 50;                 ### Send no more than this number of emails for a single scanning source ip.
my @EMAIL_ADDRESSES           = qw(root@localhost); ### Supports multiple email addresses.
my $ALERT_ALL                 = "Y";                ### If "Y", send email for all new bad packets instead of just when a danger level increases
my $ENABLE_AUTO_IDS           = "N";                ### If "Y", enable automated IDS response (auto manages firewall rulesets). 
my $AUTO_IDS_DANGER_LEVEL     = 5;                  ### Block all traffic from offending IP if danger level >= to this value 
my $WHOIS_TIMEOUT             = 60;                 ### (seconds)

### The following variable can be modified to look for logging messages
### that are specific to iptables firewalls (specified by the "--log-prefix"
### option).  For example, if your firewall uses the string "Audit" for
### packets that have been blocked, then you could set $IPTABLES_MSG_SEARCH = "Audit";
my $IPTABLES_MSG_SEARCH = "DROP|DENY|REJECT";

### system binaries ###
my $ipchainsCmd   = "/sbin/ipchains";
my $iptablesCmd   = "/sbin/iptables";
my $mknodCmd      = "/bin/mknod";
my $psCmd         = "/bin/ps";
my $wcCmd         = "/usr/bin/wc";
my $tailCmd       = "/usr/bin/tail";
my $mailCmd       = "/bin/mail";
my $ifconfigCmd   = "/sbin/ifconfig";
my $grepCmd       = "/bin/grep";
my $sysloginitCmd = "/etc/rc.d/init.d/syslog";
my $netstatCmd    = "/bin/netstat";
my $unameCmd      = "/bin/uname";
my $whoisCmd      = "/usr/bin/whois.psad";
my $psadwatchdCmd = "/usr/sbin/psadwatchd";
my $kmsgsdCmd     = "/usr/sbin/kmsgsd";
my $diskmondCmd   = "/usr/sbin/diskmond";
my $psadCmd       = "/usr/sbin/psad";
#================== end config ==================== ## do not remove this line (used by install.pl to preserve configs) ##
#===================== main =======================

### A few more configuration variables that should not really be in the config section.
$VERSION = "0.9.6";

### pid files
my @PIDFILES = qw(/var/run/psadwatchd.pid
                    /var/run/psad.pid
                    /var/run/kmsgsd.pid
                    /var/run/diskmond.pid);

### File used to store the psad command line.
my $cmdline_file = "/var/run/psad.cmd";

### Initialize some of the globals.
$USE_IPCHAINS = 0;
$USE_IPTABLES = 0;
$DEBUG        = 0;
$HOSTNAME     = hostname;  ### Calculate hostname of the machine on which psad is running.

### initialize and scope some default variables (command line args can override some default values)
my $Scan_href;  ### main psad data structure; contains ips, port ranges, tcp flags, and danger levels
my ($found_new_packets, $daemon, $output, $errors, $dnslookups, $whoislookups,
    $signatures, $auto_ips, $netstat_lookup, $fwcheck, $Syslog_server, $kill,
    $restart, $status, $usr1, $print_version, $help, $config) = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);

### save a copy of the command line arguments
my @args_cp = @ARGV;

Getopt::Long::Configure("no_ignore_case");  # make Getopts case sensitive

&usage_and_exit(1) unless (GetOptions (
    'help'           => \$help,          # display help
    'auto_ips=s'     => \$auto_ips,      # enable automatic ip danger level assignment
    'output'         => \$output,        # write scanlog messages to STDOUT
    'Daemon'         => \$daemon,        # do not run as a daemon
    'debug'          => \$DEBUG,         # run in debug mode
    'interval=s'     => \$CHECK_INTERVAL,# set $CHECK_INTERVAL from the command line
    'firewallcheck'  => \$fwcheck,       # do not check firewall rules
    'config=s'       => \$config,        # specify configuration file
    'reversedns'     => \$dnslookups,    # do not issue dns lookups against scanning ip address
    'signatures=s'   => \$signatures,    # scan signatures
    'localport'      => \$netstat_lookup,# do not check to see if the firewall is listening on localport that has been scanned
    'errors'         => \$errors,        # do not write malformed packet messages to error log
    'Logging_server' => \$Syslog_server, # we are running psad on a syslog logging server
    'Kill'           => \$kill,          # kill all running psad processes (psadwatchd, psad, kmsgsd, diskmond)
    'Restart'        => \$restart,       # restart psad with all options of the currently running psad process
    'Status'         => \$status,        # display status of any currently running psad processes
    'USR1'           => \$usr1,          # send an existing psad process a USR1 signal (useful for debugging)
    'Version'        => \$print_version, # print the psad version and exit
    'whois'          => \$whoislookups   # do not issue whois lookups against the scanning ip
));
&usage_and_exit(0) if ($help);

### Print the version number and exit if -V given on the command line.
print "psad version $VERSION, by Michael B. Rash (mbr\@cipherdyne.com)\n" and exit 0 if $print_version;

### Everthing after this point must be executed as root.
$< == 0 && $> == 0 or die "\n@@@@@  psad: You must be root (or equivalent UID 0 account) to execute psad!  Exiting.\n\n";

### The --Kill command line switch was given.
if ($kill) {
    &kill_psad(\@PIDFILES);
    exit 0;
}

### The --USR1 command line switch was given.
if ($usr1) {
    my $rv = &psad_usr1(\@PIDFILES);
    exit $rv;
}

### Define our %Cmds hash, which will be used throughout psad.
my %Cmds = (
    "ipchains"      => $ipchainsCmd,
    "iptables"      => $iptablesCmd,
    "mknod"         => $mknodCmd,
    "wc"            => $wcCmd,
    "ps"            => $psCmd,
    "tail"          => $tailCmd,
    "mail"          => $mailCmd,
    "ifconfig"      => $ifconfigCmd,
    "grep"          => $grepCmd,
    "syslog_init"   => $sysloginitCmd,
    "netstat"       => $netstatCmd,
    "uname"         => $unameCmd,
    "whois"         => $whoisCmd,
    "psadwatchd"    => $psadwatchdCmd,
    "kmsgsd"        => $kmsgsdCmd,
    "diskmond"      => $diskmondCmd,
    "psad"          => $psadCmd
);

### check to make sure the commands specified in the config section 
### are in the right place, and attempt to correct automatically if not.
%Cmds = &Psad::check_commands(\%Cmds);

### Now that we are sure the psad command is where it should be,
### assign $PSADCMD (used by the SEGV handler)
$PSADCMD = $Cmds{'psad'};

### the --Status command line switch was given
if ($status) {
    my $rv = &psad_status(\@PIDFILES, $cmdline_file, \%Cmds);
    exit $rv;
}

### make sure $PSAD_DIR, $FW_DATA, and /var/log/psadfifo, etc. actually exist
&psad_setup($PSAD_DIR, $FW_DATA, $PSAD_LOGFILE, $ERROR_LOG, \%Cmds);

### the --Restart command line switch was given
if ($restart) {
    &restart_psad(\@PIDFILES, $cmdline_file, \%Cmds);
    exit 0;
}

### check to make sure another psad process is not already running.
&Psad::unique_pid($PIDFILES[1]);

### make sure the permissions on these files is 0600
&check_permissions($PSAD_LOGFILE, $FW_DATA, $ERROR_LOG, $EMAIL_ALERTFILE);

### get the ip addresses that are local to this machine
my $local_ips_href = &get_local_ips(\%Cmds);

### disable whois lookups if for some reason the whois client that is 
### bundled with psad can't be found
$whoislookups = 1 if ($Cmds{'whois'} !~ /psad/);

### if psad is running on a syslog server, don't check the firewall 
### rules since they may not be local.
unless ($fwcheck || $Syslog_server) {
    unlink "/var/log/psad/fw_check.txt";
    &Psad::check_firewall_rules($IPTABLES_MSG_SEARCH, \@EMAIL_ADDRESSES, ["/var/log/psad/fw_check.txt"], \%Cmds);
}

### daemonize psad unless running with --Daemon or --debug
unless ($daemon || $DEBUG) {
    my $pid = fork;
    exit if $pid;
    die "@@@@@  $0: Couldn't fork: $!" unless defined($pid);
    POSIX::setsid() or die "@@@@@  $0: Can't start a new session: $!\n";
}

### write the current pid associated with psad to the psad pid file
&Psad::writepid($PIDFILES[1]);

### write the command line args used to start psad to $cmdline_file
&Psad::writecmdline(\@args_cp, $cmdline_file);

### psad _requires_ that kmsgsd is running to receive any data, so let's 
### start it here for good measure (as of 0.9.2 it makes use of the pid 
### files and unique_pid(), so we don't have to worry about starting a 
### duplicate copy).  While we're at it, start psadwatchd and diskmond too.
### Note that this is the best place to start the other daemons since we 
### just wrote the psad pid to $PIDFILES[1] above.
system "$Cmds{'kmsgsd'}";
system "$Cmds{'diskmond'}";
system "$Cmds{'psadwatchd'}" unless ($DEBUG);   ### If running in debug mode, starting psadwatchd can start unwanted
                                                ### copies of psad after the debugging process is killed.

### import config variables from the config file if running with 
### --config=<config file>.  Note that we can't use "do <config file>"
### because the variables would not be in scope.
if ($config) {
    open CONF, "< $config";
    while(<CONF>) {
        ### don't allow system calls and other mischief in the config file... 
        ### just variable assignments.  Thanks Jay Beale (Bastille Linux).
        next and print $_ if ($_ =~ /system/ && $_ !~ /\#.*?system/);
        next if ($_ =~ /\`.*?\`/);
        next if ($_ =~ /open/ && $_ !~ /\#.*?open/);
        next if ($_ =~ /qx/);
        next unless ($_ =~ /my\s+\$|\@|\%.*?\=.*?\;/ || $_ =~ /\$|\@|\%.*?\=.*?\;/);
        eval $_;
    }
    close CONF;
}

unless (-e $FW_DATA) {
    open F, ">> $FW_DATA";
    close F;
}
my $Sigs_href = &import_signatures($signatures) if $signatures;
my $Auto_ips_href;
if ($auto_ips) {  ### support automatic ip danger level increases/decreases.
    $Auto_ips_href = &import_auto_ips($auto_ips);
} else {
    $Auto_ips_href = 0;
}

### install signal handlers for debugging %Scan with Data::Dumper,
### and for reaping zombie whois processes
$SIG{'USR1'} = \&print_scan;
$SIG{'SEGV'} = \&print_scan_and_exit;
$SIG{'CHLD'} = \&REAPER;

### initialize fwdata_start_lines
my $fwdata_start_lines = (split /\s+/, `$Cmds{'wc'} -l $FW_DATA`)[1];

#=================================== main loop ======================================
for (;;) {
    my $auto_ips_mtime_start = stat($auto_ips)->mtime if $auto_ips;
    my $sigs_mtime_start = stat($signatures)->mtime if $signatures;
    sleep $CHECK_INTERVAL;
    my $fwdata_end_lines = (split /\s+/, `$Cmds{'wc'} -l $FW_DATA`)[1];
    ### scan $signatures for any signature updates
    if ($signatures) {
        my $sigs_mtime_end = stat($signatures)->mtime;
        ### the signatures were updated... import the new signatures.
        if ($sigs_mtime_start != $sigs_mtime_end) {
            $Sigs_href = &import_signatures($signatures);
            for my $email_address (@EMAIL_ADDRESSES) {
                system "$Cmds{'mail'} $email_address -s \"psad: re-read $signatures file on $HOSTNAME\" < /dev/null > /dev/null 2>&1";
            }
        }
    }
    ### scan $auto_ips for any ips that should automatically have a certain danger threshold set.
    if ($auto_ips) {
        my $auto_ips_mtime_end = stat($auto_ips)->mtime;
        if ($auto_ips_mtime_start != $auto_ips_mtime_end) {  ### auto_ips file was updated... import ip/danger level pairs.
            $Auto_ips_href = &import_auto_ips($auto_ips);
            ### need to set $Scan_href->{$src}->{$dst}->{'AUTO'} = "N" since $Auto_ips_href was updated.
            $Scan_href = &reset_auto_tags($Scan_href);
            for my $email_address (@EMAIL_ADDRESSES) {
                system "$Cmds{'mail'} $email_address -s \"psad: re-read $auto_ips file on $HOSTNAME\" < /dev/null > /dev/null 2>&1";
            }
        }
    }
    ### New packets have been written to $FW_DATA by kmsgsd for psad analysis.
    if ($fwdata_end_lines - $fwdata_start_lines > 0) {
        my $grabnum = $fwdata_end_lines - $fwdata_start_lines;
        my @process_lines = `$Cmds{'tail'} -$grabnum $FW_DATA`;
        ($Scan_href, $found_new_packets) = &check_scan(\@process_lines, $Sigs_href, $signatures,
                                                       $netstat_lookup, $local_ips_href, $errors, $ERROR_LOG,
                                                       $ENABLE_PERSISTENCE, $SHOW_ALL_SIGNATURES, $SCAN_TIMEOUT, \%Cmds);
        if ($found_new_packets) {
            ### Assign a danger level to the scan.
            &assign_danger_level($Scan_href, $Auto_ips_href, $PORT_RANGE_SCAN_THRESHOLD, $ALERT_ALL, \%DANGER_LEVELS);
            ### Log and send an email alert about the scan.
            &scan_logr($Scan_href, $PSAD_LOGFILE, $output, $dnslookups,
                       $whoislookups, $WHOIS_TIMEOUT, $ENABLE_EMAIL_ALERTS, $EMAIL_ALERT_DANGER_LEVEL,
                       $EMAIL_ALERTFILE, $EMAIL_LIMIT, \@EMAIL_ADDRESSES, $SHOW_ALL_SIGNATURES, \%Cmds);

            ### Don't manage the firewall rules if psad is running on a syslog server.
            if ($ENABLE_AUTO_IDS eq "Y" && ! $Syslog_server) {
                &auto_psad_response($Scan_href, $AUTO_IDS_DANGER_LEVEL, \%Cmds, \@EMAIL_ADDRESSES);
            }
        }
    }
    if ($DEBUG) {
        print "MAIN: number of lines in $FW_DATA: $fwdata_end_lines\n";
    }
    ### reset fwdata_start_lines to where we just left off so that we don't miss any packets
    $fwdata_start_lines = $fwdata_end_lines;
}
exit 0;

#==================================== end main =========================================
### Keeps track of scanning ip's, increments packet counters,
### keep track of tcp flags for each scan (iptables only)
sub check_scan() {
    my ($process_lines_aref, $sigs_href, $signatures, $netstat_lookup, $local_ips_href, $errors,
            $error_log, $enable_persistence, $show_all_signatures, $scan_timeout, $Cmds_href) = @_;
    my @bad_packets;
    my $local_listening_ports_href;
    my ($src, $dst, $len, $ttl, $proto, $srcport, $dstport, $flags, $type, $code, $id, $seq);
    ### If necessary, check which firewall (ipchains vs. iptables)
    unless ($USE_IPCHAINS || $USE_IPTABLES) {
        &check_fw($process_lines_aref->[0]);
    }
    unless ($netstat_lookup) {
        $local_listening_ports_href = &get_listening_ports($Cmds_href);
    }
    my $matched_packet = 0;
    READPKT: for my $l (@$process_lines_aref) {
        chomp $l;
        if ($USE_IPTABLES) {
            ### Sometimes the log entry is messed up by iptables so we write it to the error log.
            if ($l =~ /SRC\=(\S+)\sDST\=(\S+)\sLEN\=(\d+)\s.+?TTL\=(\d+)\s.+?PROTO\=(\w{3,4})\sSPT\=(\d+)\sDPT\=(\d+)/) {
                ($src, $dst, $len, $ttl, $proto, $srcport, $dstport) = ($1,$2,$3,$4,$5,$6,$7,$8);
                if ($proto ne "TCP" && $proto ne "UDP") {  ### it was some weird non-tcp/udp packet with source and destination ports
                    push @bad_packets, $l;
                    next READPKT;
                }
                if ($proto eq "TCP") {
                    if ($l =~ /RES\S+\s(.*?)URGP/ || $l =~ /RES(\s)URGP/) {
                        $flags = $1;
                        chop $flags;
                        $flags = "NULL" if ($flags eq "");
                        ### per page 595 of the Camel book, "if /blah1|blah2/" can be slower than "if /blah1/ || /blah2/
                        unless (($flags =~ /SYN/ || $flags =~ /FIN/ || $flags =~ /URG/ || $flags =~ /PSH/ || $flags =~ /ACK/
                                     || $flags =~ /RST/ || $flags =~ /NULL/) && ($flags !~ /LEN/ || $flags !~ /TOS/
                                     || $flags !~ /PREC/ || $flags !~ /PROTO/ || $flags !~ /WINDOW/ ||
                                        $flags !~ /RES/)) {
                            push @bad_packets, $l;
                            next READPKT;
                        }
                    } else {
                        push @bad_packets, $l;
                        next READPKT;
                    }
                } ### else it is UDP, but there are no more fields we need to defined since we already have the ports, etc.
            } elsif ($l =~ /SRC\=(\S+)\sDST\=(\S+)\sLEN\=(\d+)\s.+?TTL\=(\d+)\s.+?PROTO\=ICMP\sTYPE\=(\d+)\sCODE\=(\d+)\sID\=(\d+)\sSEQ\=(\d+)/) {
                ($src, $dst, $len, $ttl, $type, $code, $id, $seq) = ($1,$2,$3,$4,$5,$6,$7,$8);
                $proto = "ICMP";
            } else {
                push @bad_packets, $l;
                next READPKT;
            }
        } elsif ($USE_IPCHAINS) {
            ### could implement source port checking here
            if ($l =~ /PROTO\=(\d+)\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\:(\d+)\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\:(\d+)/) {
                ($proto, $src, $srcport, $dst, $dstport) = ($1,$2,$3,$4,$5);
                $flags = "NONE";
            } else {
                push @bad_packets, $l;
                next READPKT;
            }
        }
        if ($enable_persistence eq "N") {
            my $currtime = time();
            if (defined $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'}) {
                my $tmp = $currtime - $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'};
                if (($currtime - $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'}) >= $scan_timeout) {
                    delete $Scan{$src}{$dst};
                }
            }
        } 
        $matched_packet = 1;
        ### hash initialization
        $Scan{$src}{$dst}{'LOGR'} = "Y";
        $Scan{$src}{$dst}{'AUTO'} = "N" unless (defined $Scan{$src}{$dst}{'AUTO'});
        $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} = 0 unless (defined $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'});
        unless (defined $Scan{$src}{$dst}{'START_PORT'} || $proto eq "ICMP") {  # this is the absolute starting port since the first packet was detected
            $Scan{$src}{$dst}{'START_PORT'} = 65535; # make sure the initial start port is not too low
            $Scan{$src}{$dst}{'END_PORT'} = 0; # make sure the initial end port is not too high
        }
        my $epoch_seconds = time() if ($enable_persistence eq "N");
        my @time = split /\s+/, scalar localtime;   # Get the current time as a nice ASCII string.
        pop @time; shift @time;    # Get rid of the day and the year to make the time consistent with syslog
        my $time = join ' ', @time;
        unless (defined $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}) {  ### initialize values for the current interval
            $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'START_TIME'} = $time;
            $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'PACKETS'} = 0;
            unless ($proto eq "ICMP") {
                ### make sure the initial start port is not too low
                $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'START_PORT'} = 65535;
                ### make sure the initial end port is not too high
                $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'END_PORT'} = 0;
            }
            if ($proto eq "TCP") {
                $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'FLAGS'}{$flags} = 0;
            }
        }
        unless (defined $Scan{$src}{$dst}{'START_TIME'}{'READABLE'}) {
            $Scan{$src}{$dst}{'START_TIME'}{'READABLE'} = $time;
            $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'} = $epoch_seconds if ($enable_persistence eq "N");
        }
        $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'PACKETS'}++;
        $Scan{$src}{$dst}{'END_TIME'}{'READABLE'}= $time;
        $Scan{$src}{$dst}{'END_TIME'}{'EPOCH_SECONDS'} = $epoch_seconds if ($enable_persistence eq "N");
        ### increment hash values
        ### if $Scan{$src}{$dst}{'ABSNUM'} is not yet defined, incrementing it here will make it equal to 1 anyway
        $Scan{$src}{$dst}{'ABSNUM'}++;  # if $Scan{$src}{$dst}{'ABSNUM'} is not yet defined, incrementing it here will make it equal to 1 anyway
        if ($proto eq "TCP") {
            $Scan{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'FLAGS'}{$flags}++;
        }
        # see if this port lies outside our current range
        unless ($proto eq "ICMP") {
            ($Scan{$src}{$dst}{'START_PORT'}, $Scan{$src}{$dst}{'END_PORT'}) =
                &check_range($dstport, $Scan{$src}{$dst}{'START_PORT'}, $Scan{$src}{$dst}{'END_PORT'});
            ($Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'START_PORT'}, $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'END_PORT'}) =
                &check_range($dstport, $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'START_PORT'},
                                $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'END_PORT'});
        } 
        if ($DEBUG) {
            print STDOUT "check_scan():\n";
            print STDOUT "     src: $src, dst: $dst\n";
            print STDOUT "     Scan{src}{dst}{'LOGR'} = $Scan{$src}{$dst}{'LOGR'}\n";
            print STDOUT "     Scan{src}{dst}{'ABSNUM'} = $Scan{$src}{$dst}{'ABSNUM'}\n";
            print STDOUT "     flags: $flags\n" if ($proto eq "TCP");
            print STDOUT "     Scan{src}{dst}{'AUTO'} $Scan{$src}{$dst}{'AUTO'}\n";
            print STDOUT "     Scan{src}{dst}{'CURRENT_DANGER_LEVEL'} = $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'}\n";
            print STDOUT "     Scan{src}{dst}{'START_PORT'} = $Scan{$src}{$dst}{'START_PORT'}\n" unless ($proto eq "ICMP");
            print STDOUT "     Scan{src}{dst}{'END_PORT'} = $Scan{$src}{$dst}{'END_PORT'}\n" unless ($proto eq "ICMP");
            print STDOUT "     Scan{src}{dst}{'START_TIME'}{'READABLE'} = $Scan{$src}{$dst}{'START_TIME'}{'READABLE'}\n";
            print STDOUT "     Scan{src}{dst}{'START_TIME'}{'EPOCH_SECONDS'} = $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'}\n" if ($enable_persistence eq "N");
            print STDOUT "     Scan{src}{dst}{'END_TIME'}{'READABLE'} = $Scan{$src}{$dst}{'END_TIME'}{'READABLE'}\n";
            print STDOUT "     Scan{src}{dst}{'END_TIME'}{'EPOCH_SECONDS'} = $Scan{$src}{$dst}{'END_TIME'}{'EPOCH_SECONDS'}\n" if ($enable_persistence eq "N");
            unless ($proto eq "ICMP") {
                print "     Scan{src}{dst}{$proto}{'CURRENT_INTERVAL'}{'START_PORT'} = "
                    . "$Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'START_PORT'}\n";
                print "     Scan{src}{dst}{$proto}{'CURRENT_INTERVAL'}{'END_PORT'} = "
                    . "$Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'END_PORT'}\n";
            }
        }
        if ($signatures && $USE_IPTABLES) {  # might try to use ipchains also, but then cannot use tcp flags except for -y -l rules
            for my $msg (keys %{$sigs_href->{$proto}}) {   # need to iterate through all signatures since a packet may match several
                my $dstport_criteria = 0;
                my $srcport_criteria = 0;
                my $rv = 0;
                if ($proto eq "TCP") {
                    if (&check_port($sigs_href, $msg, $srcport, $dstport, $proto)
                        && &check_misc_fields($sigs_href, $msg, $proto, $len, $ttl)
                            && &check_tcp_flags($sigs_href, $msg, $flags, $proto)) {   ### tripped a tcp signature
                        $Scan{$src}{$dst}{'TCP'}{'SIGMATCH'}{'SIGDL'} = $sigs_href->{'TCP'}->{$msg}->{'DANGERLEVEL'};
                        $rv = 1;
                    }
                } elsif ($proto eq "UDP") {
                    if (&check_port($sigs_href, $msg, $srcport, $dstport, $proto)
                        && &check_misc_fields($sigs_href, $msg, $proto, $len, $ttl)) {   ### tripped a udp signature
                        $Scan{$src}{$dst}{'UDP'}{'SIGMATCH'}{'SIGDL'} = $sigs_href->{'UDP'}->{$msg}->{'DANGERLEVEL'};
                        $rv = 1;
                    }
                } elsif ($proto eq "ICMP") {
                    if (&check_icmp_sigs($sigs_href, $msg, $ttl, $type, $code, $id, $seq)) {
                        $Scan{$src}{$dst}{'ICMP'}{'SIGMATCH'}{'SIGDL'} = $sigs_href->{'ICMP'}->{$msg}->{'DANGERLEVEL'};
                        $rv = 1;
                    }
                }
                if ($rv) {   ### we matched a signature
                    if ($DEBUG) {
                        unless ($proto eq "ICMP") {
                            print "     SIGMATCH on dstport: $dstport\n";
                        } else {
                            print "     ICMP SIGMATCH\n";
                        }
                    }
                    my $listening_port = "";
                    unless ($netstat_lookup || $proto eq "ICMP") {
                        my $lprot;
                        $lprot = "tcp" if ($proto eq "TCP");
                        $lprot = "udp" if ($proto eq "UDP");
                        my $dst_is_local = 0;
                        ### check to see if the scan destination ip is directed at the firewall.  If yes,
                        ### then check to see if a server is listening on the DSTPORT by parsing netstat
                        ### output.  If not, psad would have to connect to the deestination port on the
                        ### remote machine, but it should not do this so it is not implemented.
                        $dst_is_local = 1 if defined $local_ips_href->{$dst};
                        if ($dst_is_local) {
                            my $key = $lprot . $dstport;
                            if (defined $local_listening_ports_href->{$key}) {
                                $listening_port = "YOUR MACHINE IS LISTENING ON ($lprot) PORT: $dstport";
                            } else {
                                $listening_port = "There is no server listening on $lprot port $dstport";
                            }
                        } # else $listening_port is already ""
                    }
                    ### including sp almost always changes the hash key:    \
                    ### my $alert_string = "$msg  sp=$srcport, dp=$dstport, flags=$flags.  $listening_port";
                    my $alert_string;
                    if ($proto eq "TCP") {
                        $alert_string = "$msg  dp=$dstport, flags=$flags. $listening_port";
                    } elsif ($proto eq "UDP") {
                        $alert_string = "$msg  dp=$dstport. $listening_port";
                    } else {
                        $alert_string = "$msg";
                    }
                    if (defined $Scan{$src}{$dst}{$proto}{'SIGMATCH'}{$alert_string}) {
                        $Scan{$src}{$dst}{$proto}{'SIGMATCH'}{$alert_string}++;
                    } else {
                        $Scan{$src}{$dst}{$proto}{'SIGMATCH'}{$alert_string} = 1;
                    }
#                   unless (defined $Scan{$src}{$dst}{'CURRENT_SIGMATCH'}{'START_TIME'}) {
                    unless (defined $Scan{$src}{$dst}{'CURRENT_SIGMATCH'}) {
#                       $Scan{$src}{$dst}{'CURRENT_SIGMATCH'}{'START_TIME'} = $time;
                        $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}{$alert_string} = 0;
                        $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}{'PACKETS'} = 0;
                    }
                    $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}{$alert_string}++;
                    $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}{'PACKETS'}++;
                }
            } 
        }
    }
    &collect_errors(\@bad_packets, $error_log) unless $errors;
    return \%Scan, $matched_packet;
}
# check_tcp_flags will eventually need to include all possible permutations of the six 
# tcp flags so that any signature can be written, but for now this should be most 
# of the important ones.
sub check_tcp_flags() {
    my ($sigs_href, $msg, $flags_to_check, $proto) = @_;
    return 0 if ($proto ne "TCP");
    my $msgflags = $sigs_href->{$proto}->{$msg}->{'FLAGS'};
    return 1 if ($msgflags eq "S" && $flags_to_check eq "SYN");      ### syn scan
    return 1 if ($msgflags eq "F" && $flags_to_check eq "FIN");      ### fin scan
    return 1 if ($msgflags eq "SF" && $flags_to_check eq "SYN FIN"); ### "syn/fin" scan
    return 1 if ($msgflags eq "UPF" && $flags_to_check eq "URG PSH FIN"); ### nmap Xmas scan
    return 1 if ($msgflags eq "NULL" && $flags_to_check eq "NULL");       ### nmap NULL scan
    return 1 if ($msgflags eq "UPSF" && $flags_to_check eq "URG PSH SYN FIN");  ### nmap fingerprint scan
    return 1 if ($msgflags eq "AP" && $flags_to_check eq "ACK PSH");            ### see the signatures for these
    return 1 if ($msgflags eq "AS" && $flags_to_check eq "ACK SYN");
    return 0;
}
sub check_port() {
    my ($sigs_href, $msg, $srcport, $dstport, $proto) = @_;
    print "check_port(): msg: $msg, srcport: $srcport, dstport: $dstport, proto: $proto\n" if $DEBUG;
    ### check dst port first
    if (defined $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'UNIQUE_OR_ANY'}) {
        unless ($sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'UNIQUE_OR_ANY'} eq "any") {
            return 0 if ($dstport != $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'UNIQUE_OR_ANY'});
        }
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'START'}) {
        my $start = $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'START'};
        my $end = $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'END'};
        return 0 if ($dstport < $start || $dstport > $end);
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NOT'}) {
        return 0 if ($dstport == $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NOT'});
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NEGSTART'}) {
        my $start = $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NEGSTART'};
        my $end = $sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NEGEND'};
        return 0 if ($dstport > $start || $dstport < $end);
    }
    ### check src port
    if (defined $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'UNIQUE_OR_ANY'}) {
        unless ($sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'UNIQUE_OR_ANY'} eq "any") {
            return 0 if ($dstport != $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'UNIQUE_OR_ANY'});
        }
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'START'}) {
        my $start = $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'START'};
        my $end = $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'END'};
        return 0 if ($dstport < $start || $dstport > $end);
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NOT'}) {
        return 0 if ($dstport == $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NOT'});
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NEGSTART'}) {
        my $start = $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NEGSTART'};
        my $end = $sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NEGEND'};
        return 0 if ($dstport > $start || $dstport < $end);
    }
    return 1;   ### if we made it to here, then we matched both the src and dst port criteria
}
sub check_icmp_sigs() {
    my ($sigs_href, $msg, $ttl, $type, $code, $icmp_id, $icmp_seq) = @_;
    ### check icmp type first
    if (defined $sigs_href->{'ICMP'}->{$msg}->{'TYPE'}) {
        return 0 if ($sigs_href->{'ICMP'}->{$msg}->{'TYPE'} != $type);
    }
    if (defined $sigs_href->{'ICMP'}->{$msg}->{'TTL'}) {
        return 0 if ($sigs_href->{'ICMP'}->{$msg}->{'TTL'} != $ttl);
    }
    if (defined $sigs_href->{'ICMP'}->{$msg}->{'CODE'}) {
        return 0 if ($sigs_href->{'ICMP'}->{$msg}->{'CODE'} != $code);
    }
    if (defined $sigs_href->{'ICMP'}->{$msg}->{'ID'}) {
        return 0 if ($sigs_href->{'ICMP'}->{$msg}->{'ICMP_ID'} != $icmp_id);
    }
    if (defined $sigs_href->{'ICMP'}->{$msg}->{'SEQ'}) {
        return 0 if ($sigs_href->{'ICMP'}->{$msg}->{'ICMP_SEQ'} != $icmp_seq);
    }
    return 1; ### if we got to this point, then we matched the signature
}
sub check_misc_fields() {
    my ($sigs_href, $msg, $proto, $len, $ttl) = @_;
    if (defined $sigs_href->{$proto}->{$msg}->{'LEN'}) {
        return 0 if ($sigs_href->{$proto}->{$msg}->{'LEN'} != $len);
    }
    if (defined $sigs_href->{$proto}->{$msg}->{'TTL'}) {
        return 0 if ($sigs_href->{$proto}->{$msg}->{'TTL'} != $ttl);
    }
    return 1;
}
sub import_signatures() {
    my $sigfile = shift;
    my ($proto, $srcport, $dstport, $msg, $flags, $dangerlevel);
    my %Sigs;
    my ($start, $end, $tmpport);
    open SIGS, "< $sigfile" or die "Could not open the signatures file $sigfile: $!\n";
    my @sigs = <SIGS>;
    close SIGS;
    for my $sig (@sigs) {
        chomp $sig;
        next if ($sig =~ /^#/);
        &get_signature_fields(\%Sigs, $sig);
    }
    print STDOUT Dumper %Sigs if $DEBUG;
    return \%Sigs;
}
sub get_signature_fields() {
    my ($Sigs_href, $sig) = @_;
    my ($len, $ttl, $proto, $srcport, $dstport, $flags, $type, $code, $id, $seq);
    my $msg;
    if ($sig =~ /^tcp/) {
        $proto = "TCP";
    } elsif ($sig =~ /^udp/) {
        $proto = "UDP";
    } elsif ($sig =~ /^icmp/) {
        $proto = "ICMP";
    } else {
        return;
    }
    my @fields = split /\;/, $sig;
    for my $f (@fields) {  ### get the msg first
        if ($f =~ /msg\:\s*?(\".*?\")/) {
            $msg = $1;
        }
    }
    if ($msg) {
        if (defined $Sigs_href->{$proto}->{$msg}) {
            $msg .= " ";   ### make sure we have a unique signature by appending whitespace if necessary
        }
        if ($proto ne "ICMP") {   ### it is either tcp or udp
            &get_signature_ports($Sigs_href, $sig, $proto, $msg);
        }
        for my $f (@fields) {
            if ($f =~ /flags\:\s*?(\w+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'FLAGS'} = $1;
            } elsif ($f =~ /ttl\:\s*(\d+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'TTL'} = $1;
            } elsif ($f =~ /itype\:\s*?(\d+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'TYPE'} = $1;
            } elsif ($f =~ /icode\:\s*?(\d+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'CODE'} = $1;
            } elsif ($f =~ /icmp_seq\:\s*?(\d+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'ICMP_SEQ'} = $1;
            } elsif ($f =~ /icmp_id\:\s*?(\d+)/i) {
                $Sigs_href->{$proto}->{$msg}->{'ICMP_ID'} = $1;
            } elsif ($f =~ /dlevel\:\s*?(\d{1})/i) {
                $Sigs_href->{$proto}->{$msg}->{'DANGERLEVEL'} = $1;
            }
        }
    }
    return;
}
sub get_signature_ports() {
    my ($Sigs_href, $sig, $proto, $msg) = @_;
    my ($srcport, $dstport);
    my ($start, $end, $tmpport);
    if ($sig =~ /^\w{3,4}\s+(\S+)\s+\-\>\s+(\S+)\s/) {
        ($srcport, $dstport) = ($1, $2);
    } else {
        return;
    }
    if ($srcport =~ /\:/ && $srcport !~ /\!/) {
        ($start, $end) = split /:/, $srcport;
        $start = 1 if ($start eq '');
        $end = 65535 if ($end eq '');
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'START'} = $start;
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'END'} = $end;
    } elsif ($srcport =~ /\!/ && $srcport !~ /\:/) {
        $tmpport = (split /\!/, $srcport)[1];
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NOT'} = $tmpport;
    } elsif ($srcport =~ /\:/ && $srcport =~ /\!/) {
        ($start, $end) = split /:/, $srcport;
        $start = 1 if ($start !~ /\d/);
        $end = 65535 if ($end !~ /\d/);
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NEGSTART'} = $start;
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'NEGEND'} = $end;
    } else {
        $Sigs_href->{$proto}->{$msg}->{'SRCPORT'}->{'UNIQUE_OR_ANY'} = $srcport;
    }
    if ($dstport =~ /\:/ && $dstport !~ /\!/) {
        ($start, $end) = split /:/, $dstport;
        $start = 1 if ($start eq '');
        $end = 65535 if ($end eq '');
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'START'} = $start;
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'END'} = $end;
    } elsif ($dstport =~ /\!/ && $dstport !~ /\:/) {
        $tmpport = (split /\!/, $dstport)[1];
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NOT'} = $tmpport;
    } elsif ($dstport =~ /\:/ && $dstport =~ /\!/) {
        ($start, $end) = split /:/, $dstport;
        $start = 1 if ($start !~ /\d/);
        $end = 65535 if ($end !~ /\d/);
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NEGSTART'} = $start;
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'NEGEND'} = $end;
    } else {
        $Sigs_href->{$proto}->{$msg}->{'DSTPORT'}->{'UNIQUE_OR_ANY'} = $dstport;
    }
    return;
}
sub import_auto_ips() {
    my $auto_ips_file = shift;
    my %Auto_ips;
    open AUTO, "< $auto_ips_file";
    my @lines = <AUTO>;
    close AUTO;
    for my $l (@lines) {
        next if ($l =~ /^#/);
        if ($l =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+([0-5]|\-1)/) {
            $Auto_ips{$1} = $2;
        }
    }
    return \%Auto_ips;
}
sub reset_auto_tags() {
    my $Scan_href = shift;
    for my $src (keys %$Scan_href) {
        for my $dst (keys %{$Scan_href->{$src}}) {
            $Scan_href->{$src}->{$dst}->{'AUTO'} = "N";
        }
    }
    return $Scan_href;
}
sub check_range() {
    my ($port, $start, $end) = @_;
    $start = $port if ($port < $start);
    $end = $port if ($port > $end);
    return $start, $end;
}
sub assign_danger_level() {
    my ($Scan_href, $Auto_ips_href, $port_range_scan_threshold, $alert_all, $danger_levels_href) = @_;
    $Scan_href = &automatic_ip_danger_assignment($Scan_href, $Auto_ips_href) if ($Auto_ips_href); 
    for my $src (keys %$Scan_href) {
        print "assign_danger_level(): source ip: $src\n" if $DEBUG;
        DST: for my $dst (keys %{$Scan_href->{$src}}) {
            my $absnum = $Scan_href->{$src}->{$dst}->{'ABSNUM'};
            my $range;
            if (defined $Scan_href->{$src}->{$dst}->{'START_PORT'}) {
                $range = $Scan_href->{$src}->{$dst}->{'END_PORT'} - $Scan_href->{$src}->{$dst}->{'START_PORT'};
            } else {
                $range = $absnum;
            }
            if ($DEBUG) {
                print "assign_danger_level(): destination ip: $dst\n";
                print "assign_danger_level(): ABSNUM: $Scan_href->{$src}->{$dst}->{'ABSNUM'}\n";
                if (defined $Scan_href->{$src}->{$dst}->{'START_PORT'}) {
                    print "assign_danger_level(): START_PORT: $Scan_href->{$src}->{$dst}->{'START_PORT'}, "
                        . "END_PORT: $Scan_href->{$src}->{$dst}->{'END_PORT'}\n";
                }
                print "assign_danger_level(): CURRENT_DANGER_LEVEL (before assignment) = "
                    . "$Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'}\n";
            }
            # If a scan signature packet has been detected but no other packets are detected, assign a danger
            # level of SIGDL.
            my $sigmatch = 0;
            my $sigproto = "";
            if (defined $Scan_href->{$src}->{$dst}->{'TCP'}->{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = "TCP";
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if (defined $Scan_href->{$src}->{$dst}->{'UDP'}->{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = "UDP";
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if (defined $Scan_href->{$src}->{$dst}->{'ICMP'}->{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = "ICMP";
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if ($sigmatch && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} <
                            $Scan_href->{$src}->{$dst}->{$sigproto}->{'SIGMATCH'}->{'SIGDL'}) {
                $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} =
                                $Scan_href->{$src}->{$dst}->{$sigproto}->{'SIGMATCH'}->{'SIGDL'};
                $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                print "assign_danger_level(): danger level: "
                    . "$Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'}\n" if $DEBUG;
            }
            ### if $port_range_scan_threshold is >= 1, then psad will not assign a
            ### danger level to repeated packets to the same port
            if ($absnum < $danger_levels_href->{'1'}) {
                ### we don't have enough packets to even reach danger level 1 yet.
                next DST;
            } elsif ($absnum < $danger_levels_href->{'2'} && $range >= $port_range_scan_threshold) {
                if ($Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} < 1
                        && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} = 1;
                }
            } elsif ($absnum < $danger_levels_href->{'3'} && $range >= $port_range_scan_threshold) {
                if ($Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} < 2
                        && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} = 2;
                }
            } elsif ($absnum < $danger_levels_href->{'4'} && $range >= $port_range_scan_threshold) {
                if ($Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} < 3
                        && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} = 3;
                }
            } elsif ($absnum < $danger_levels_href->{'5'} && $range >= $port_range_scan_threshold) {
                if ($Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} < 4
                        && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} = 4;
                }
            } elsif ($range >= $port_range_scan_threshold) {
                if ($Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} < 5
                        && $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} = 5;
                }
            }
            ### we will always send an alert email for any new "bad" packet if $alert_all eq "Y"...
            ### Else email sent only if the scan increments its D.L.
            if ($alert_all eq "Y") {
                $Scan_href->{$src}->{$dst}->{'ALERTED'} = "N";
            }
            print "assign_danger_level(): CURRENT_DANGER_LEVEL (after assignment) = "
                . "$Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'}\n" if $DEBUG;
        }
    }
    return $Scan_href;
}
sub automatic_ip_danger_assignment() {
    my ($Scan_href, $Auto_ips_href) = @_;
    for my $scan_ip (keys %$Scan_href) {
        for my $dst (keys %{$Scan_href->{$scan_ip}}) {
            for my $auto_ip (keys %$Auto_ips_href) {
                if ($scan_ip eq $auto_ip && $Scan_href->{$scan_ip}->{$dst}->{'AUTO'} eq "N") {
                    $Scan_href->{$scan_ip}->{$dst}->{'CURRENT_DANGER_LEVEL'} = $Auto_ips_href->{$auto_ip};
                    $Scan_href->{$scan_ip}->{$dst}->{'ALERTED'} = "N";
                    $Scan_href->{$scan_ip}->{$dst}->{'AUTO'} = "Y";
                }
            }
        }
    }
    return $Scan_href;
}
sub collect_errors() {
    my ($bad_packets_aref, $error_log) = @_;
    open ERRORS, ">> $error_log";
    for my $l (@$bad_packets_aref) {
        print ERRORS "$l\n";
    }
    close ERRORS;
}
sub scan_logr() {
    my ($Scan_href, $logfile, $output, $dnslookups, $whoislookups, $whois_timeout,
                    $enable_email_alerts, $email_alert_danger_level, $email_alertfile,
                        $email_limit, $email_addresses_aref, $show_all_signatures, $Cmds_href) = @_;
    my $flags_msg;
    my $range;
    my ($tcp_newrange, $udp_newrange, $tcp_new_start_range, $tcp_new_end_range);
    my ($udp_new_start_range, $udp_new_end_range);
    my ($tcp_new_start_time, $udp_new_start_time, $icmp_new_start_time);
    my $dnsstring = "";
    my $whois_info_aref;
    $logfile = *STDOUT if ($output || $DEBUG);
    my @print_array = ($logfile, $email_alertfile);
    for my $src (keys %$Scan_href) {
        print STDOUT "scan_logr(): source ip: $src\n" if $DEBUG;
        DST: for my $dst (keys %{$Scan_href->{$src}}) {
            unless (defined $Scan_href->{$src}->{$dst}->{'EMAIL_LIMIT'}) {
                $Scan_href->{$src}->{$dst}->{'EMAIL_LIMIT'} = 0;
            } elsif ($Scan_href->{$src}->{$dst}->{'EMAIL_LIMIT'} > $email_limit) {
                unless (defined $Scan_href->{$src}->{$dst}->{'EMAIL_STOPPED'}) {
                    &email_limit_reached($email_addresses_aref, $src, $dst, $Cmds_href);
                    $Scan_href->{$src}->{$dst}->{'EMAIL_STOPPED'} = 1;
                }
                next DST;
            }
            print STDOUT "scan_logr(): dst ip: $dst\n" if $DEBUG;
            my ($tcp, $udp, $icmp) = (0,0,0);
            my $current_danger_level = $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'};
            if ($current_danger_level >= 1 && $Scan_href->{$src}->{$dst}->{'LOGR'} eq "Y") {
                my ($abs_start_range, $abs_end_range);
                my ($tcp_new_start_range, $tcp_new_end_range, $tcp_new_start_time, $tcp_new_num_pkts);
                my ($udp_new_start_range, $udp_new_end_range, $udp_new_start_time, $udp_new_num_pkts);
                my ($icmp_new_start_time, $icmp_new_num_pkts);
                if (defined $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}) {
                    $tcp = 1;
                    $tcp_new_start_range = $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'START_PORT'};
                    $tcp_new_end_range = $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'END_PORT'};
                    $tcp_new_start_time = $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'START_TIME'};
                    $tcp_new_num_pkts = $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'PACKETS'};
                }
                if (defined $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'}) {
                    $udp = 1;
                    $udp_new_start_range = $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'}->{'START_PORT'};
                    $udp_new_end_range = $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'}->{'END_PORT'};
                    $udp_new_start_time = $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'}->{'START_TIME'};
                    $udp_new_num_pkts = $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'}->{'PACKETS'};
                }
                if (defined $Scan_href->{$src}->{$dst}->{'ICMP'}->{'CURRENT_INTERVAL'}) {
                    $icmp = 1;
                    $icmp_new_start_time = $Scan_href->{$src}->{$dst}->{'ICMP'}->{'CURRENT_INTERVAL'}->{'START_TIME'};
                    $icmp_new_num_pkts = $Scan_href->{$src}->{$dst}->{'ICMP'}->{'CURRENT_INTERVAL'}->{'PACKETS'};
                }
                my $start_time = $Scan_href->{$src}->{$dst}->{'START_TIME'}->{'READABLE'};
                my $end_time = $Scan_href->{$src}->{$dst}->{'END_TIME'}->{'READABLE'};
                my @time = split /\s+/, scalar localtime;   ### Get the current time as a nice ASCII string.
                pop @time; shift @time;    ### Get rid of the day and the year to make the time consistent with syslog
                my $time = join ' ', @time;
                unless ($dnslookups) {
                    my $src_tmp = $src;
                    $src_tmp =~ s/\.//g;
                    if ($src_tmp =~ /\D/) {  ### $ipaddr was reported as a host name by iptables
                        $dnsstring = $src;
                    } else {
                        my $ipaddr = gethostbyname($src);
                        # my $rdns = gethostbyaddr($ipaddr, AF_INET);
                        my $rdns = gethostbyaddr($ipaddr, 2);
                        $rdns = "No reverse dns info available" unless $rdns;
                        $dnsstring = "$src -> $rdns";
                    }
                }
                unless ($whoislookups) {
                    $whois_info_aref = &get_whois_data($whois_timeout, $src, $Cmds_href);
                }
                if ($tcp || $udp) {
                    $abs_start_range = $Scan_href->{$src}->{$dst}->{'START_PORT'};
                    $abs_end_range = $Scan_href->{$src}->{$dst}->{'END_PORT'};
                    if ($abs_start_range == $abs_end_range) {
                        $range = $abs_start_range;
                    } else {
                        $range = "$abs_start_range-$abs_end_range";
                    }
                }
                my @tcp_flags;
                my $flags_msg;
                if ($tcp) {
                    if ($tcp_new_start_range == $tcp_new_end_range) {
                        $tcp_newrange = $tcp_new_start_range;
                    } else {
                        $tcp_newrange = "$tcp_new_start_range-$tcp_new_end_range";
                    }
                    for my $flags (keys %{$Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'FLAGS'}}) {
                        my $nmapOpts = "";
                        $nmapOpts = "-sT or -sS" if ($flags eq "SYN");
                        $nmapOpts = "-sF" if ($flags eq "FIN");
                        $nmapOpts = "-sX" if ($flags eq "URG PSH FIN");
                        $nmapOpts = "-O" if ($flags eq "URG PSH SYN FIN");
                        if ($nmapOpts) {
                            $nmapOpts = "  Nmap: [$nmapOpts]";
                        }
                        my $num_pkts = $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'}->{'FLAGS'}->{$flags};
                        $flags_msg = "TCP flags:                   [$flags: $num_pkts packets]${nmapOpts}\n";
                        push @tcp_flags, $flags_msg;
                    }
                    ### need to delete the current interval so it won't show up in the next alert
                    delete $Scan_href->{$src}->{$dst}->{'TCP'}->{'CURRENT_INTERVAL'};
                }
                if ($udp) {
                    if ($udp_new_start_range == $udp_new_end_range) {
                        $udp_newrange = $udp_new_start_range;
                    } else {
                        $udp_newrange = "$udp_new_start_range-$udp_new_end_range";
                    }
                    ### need to delete the current interval so it won't show up in the next alert
                    delete $Scan_href->{$src}->{$dst}->{'UDP'}->{'CURRENT_INTERVAL'};
                }
                if ($icmp) {
                    delete $Scan_href->{$src}->{$dst}->{'ICMP'}->{'CURRENT_INTERVAL'};
                }
                print STDOUT "scan_logr():  generating email......\n" if $DEBUG;
                unlink $email_alertfile;  ### remove $email_alertfile so we don't send old alert info
                &Psad::logr("=-=-=-=-=-=-=-=-=-=-=-=-=-= $time =-=-=-=-=-=-=-=-=-=-=-=-=-=\n", \@print_array);
                &Psad::logr("psad: Portscan Detected on $HOSTNAME ($dst).\n", \@print_array);
                &Psad::logr("\n", [$logfile, $email_alertfile]);
                &Psad::logr("Source:                      $src\n", \@print_array);
                &Psad::logr("Destination:                 $dst\n", \@print_array);
                &Psad::logr("Newly scanned TCP ports:     [$tcp_newrange]   (since: $tcp_new_start_time)\n", \@print_array) if $tcp;
                &Psad::logr("Newly Blocked TCP packets:   [$tcp_new_num_pkts]   (since: $tcp_new_start_time)\n", \@print_array) if $tcp;
                if (@tcp_flags) {
                    for my $flag (@tcp_flags) {
                        &Psad::logr($flag, \@print_array);
                    }
                }
                &Psad::logr("Newly scanned UDP ports:     [$udp_newrange]   (since: $udp_new_start_time)\n", \@print_array) if $udp;
                &Psad::logr("Newly Blocked UDP packets:   [$udp_new_num_pkts]   (since: $udp_new_start_time)\n", \@print_array) if $udp;
                &Psad::logr("Newly Blocked ICMP packets:  [$icmp_new_num_pkts]  (since: $icmp_new_start_time)\n", \@print_array) if $icmp;
                &Psad::logr("Complete TCP/UDP port range: [$range]  (since: $start_time)\n", \@print_array) unless $icmp;
                &Psad::logr("Total blocked packets:       $Scan_href->{$src}->{$dst}->{'ABSNUM'}\n", \@print_array);
                &Psad::logr("Start time:                  $start_time\n", \@print_array);
                &Psad::logr("End time:                    $end_time\n", \@print_array);
                &Psad::logr("Danger level:                $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'} out of 5\n", \@print_array);
                &Psad::logr("DNS info:                    $dnsstring\n", \@print_array) unless $dnslookups;

                ### write a syslog message
#               openlog("psad", "ndelay", "daemon");
#               my $syslog_print_range = "";
#               if ($tcp) {
#                   $syslog_print_range .= "TCP ports: [$tcp_newrange] ";
#               }
#               if ($udp) {
#                   $syslog_print_range .= "UCP ports: [$udp_newrange] ";
#               }
#               my $syslog_num_pkts = $Scan_href->{$src}->{$dst}->{'ABSNUM'};
#               my $syslog_danger_level = $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'};
#
#               syslog("warning", "Port scan detected: src: $src, dst: $dst, $syslog_print_range, pkts: $syslog_num_pkts, dangerlevel: $syslog_danger_level");
#               closelog();
                if ($USE_IPTABLES) {
                    SIGS: for my $proto ("TCP", "UDP", "ICMP") {
                        if (defined $Scan_href->{$src}->{$dst}->{$proto}->{'CURRENT_SIGMATCH'}) {
                            my $sig_start_time = $tcp_new_start_time;
                            &Psad::logr("\n", \@print_array);
                            &Psad::logr("=-=-= $proto alert signatures found since [$sig_start_time]\n", \@print_array);
                            for my $sigmatch (keys %{$Scan_href->{$src}->{$dst}->{$proto}->{'CURRENT_SIGMATCH'}}) {
                                next if ($sigmatch eq "PACKETS" || $sigmatch eq "START_TIME");
                                my $sig_num_packets = $Scan_href->{$src}->{$dst}->{$proto}->{'CURRENT_SIGMATCH'}->{$sigmatch};
                                &Psad::logr("$sigmatch  Packets=$sig_num_packets\n", \@print_array);
                            }
                            ### need to delete the current interval so it won't show up in the next alert
                            delete $Scan_href->{$src}->{$dst}->{$proto}->{'CURRENT_SIGMATCH'};
                        }
                        if (defined $Scan_href->{$src}->{$dst}->{$proto}->{'SIGMATCH'} && $show_all_signatures eq "Y") {
                            &Psad::logr("\n", \@print_array);
                            &Psad::logr("=-=-= ALL $proto alert signatures found since [$start_time]\n", \@print_array);
                            for my $sigmatch (keys %{$Scan_href->{$src}->{$dst}->{$proto}->{'SIGMATCH'}}) {
                                my $sig_num_packets = $Scan_href->{$src}->{$dst}->{$proto}->{'SIGMATCH'}->{$sigmatch};
                                &Psad::logr("$sigmatch  Packets=$sig_num_packets\n", \@print_array);
                            }
                        }
                    }
                } else {
                    $flags_msg = 0;
                }
                unless ($whoislookups) {
                    &Psad::logr("\n\n", \@print_array);
                    &Psad::logr("   =-=-=-=-=-=-= Whois Information: =-=-=-=-=-=-=\n", [$logfile, $email_alertfile]);
                    for my $w (@$whois_info_aref) {
                        &Psad::logr($w, \@print_array);
                    }
                    &Psad::logr("\n", \@print_array);
                }
                if ($USE_IPCHAINS) {
                    &Psad::logr("=-=-=-=-=-=-=-=-=-=-=-=-=-= $time =-=-=-=-=-=-=-=-=-=-=-=-=-=\n", \@print_array);
                }
                if ($Scan_href->{$src}->{$dst}->{'ALERTED'} eq "N"
                                        && $current_danger_level >= $EMAIL_ALERT_DANGER_LEVEL) {
                    $Scan_href = &send_email_alert($Scan_href, $src, $dst, $flags_msg,
                                                    $email_alertfile, $email_addresses_aref, $Cmds_href);
                }
                &Psad::logr("=-=-=-=-=-=-=-=-=-=-=-=-=-= $time =-=-=-=-=-=-=-=-=-=-=-=-=-=\n", \@print_array);
                $Scan_href->{$src}->{$dst}->{'LOGR'} = "N";
                $Scan_href->{$src}->{$dst}->{'EMAIL_LIMIT'}++;
            }
        }
    }
    return $Scan_href;
}
sub auto_psad_response(){
    my ($Scan_href, $auto_psad_level, $Cmds_href, $email_addresses_aref) = @_;
    SOURCE: for my $src (keys %$Scan_href) {
        for my $dst (keys %{$Scan_href->{$src}}) {
            my $current_danger_level = $Scan_href->{$src}->{$dst}->{'CURRENT_DANGER_LEVEL'};
            ### We only want to block the IP once.  Currently this will block all traffic from the host to _all_ destinations
            ### that are protected by the firewall if the ip trips the $auto_psad_level threshold for _any_ destination. 
            if ($current_danger_level >= $auto_psad_level && !(defined $Scan_href->{$src}->{$dst}->{'BLOCKED'})) {
                if ($USE_IPCHAINS) {
                    my @chains = &get_input_chains($Cmds_href->{'ipchains'});
                    for my $inchain (@chains) {
                        `$Cmds_href->{'ipchains'} -I $inchain 1 -s $src -l -j DENY`;
                    }
                    for my $dst (keys %{$Scan_href->{$src}}) {
                        $Scan_href->{$src}->{$dst}->{'BLOCKED'} = "Y";
                    }
                    for my $email_address (@$email_addresses_aref) {
                        system "$Cmds_href->{'mail'} $email_address -s \"psad: All traffic from $src has been BLOCKED on $HOSTNAME ($dst)\" < /dev/null > /dev/null 2>&1";
                    }
                    next SOURCE;
                } elsif ($USE_IPTABLES) {
                    my @chains = &get_input_chains($Cmds_href->{'iptables'});
                    for my $inchain (@chains) {
                        `$Cmds_href->{'iptables'} -I $inchain 1 -s $src -j DROP`;
                    }
                    for my $dst (keys %{$Scan_href->{$src}}) {
                        $Scan_href->{$src}->{$dst}->{'BLOCKED'} = "Y";
                    }
                    for my $email_address (@$email_addresses_aref) {
                        system "$Cmds_href->{'mail'} $email_address -s \"psad: All traffic from $src has been BLOCKED on $HOSTNAME ($dst)\" < /dev/null > /dev/null 2>&1";
                    }
                    next SOURCE;
                }
            }
        }
    }
    return $Scan_href;
}
sub email_limit_reached() {
    my ($email_addresses_aref, $src, $dst, $Cmds_href) = @_;
    for my $email_address (@$email_addresses_aref) {
        system "$Cmds_href->{'mail'} $email_address -s \"psad: Email message limit for $src has been reached on $HOSTNAME ($dst)!!!\" < /dev/null > /dev/null 2>&1";
    }
    return;
}
sub get_input_chains() {
    my $fwCmd = shift;
    my @rules;
    my @chains;
    @rules = `$fwCmd -L`;
    for my $r (@rules) {
        next unless ($r =~ /Chain/);
        my ($cname) = ($r =~ /Chain\s(\w+)/);
        next unless ($cname =~ /in/i);  # we don't have an input chain
        push @chains, $cname;
    }
    return @chains;
}
sub send_email_alert() {
    my ($Scan_href, $src, $dst, $flags_msg, $email_alertfile, $email_addresses_aref, $Cmds_href) = @_;
    for my $email_address (@$email_addresses_aref) {
        print "send_email_alert(): sending scan alert email to: $email_address\n" if $DEBUG;
        system "$Cmds_href->{'mail'} $email_address -s \"psad WARNING: $HOSTNAME ($dst) has been scanned!\" < $email_alertfile > /dev/null 2>&1";
    }
    $Scan_href->{$src}->{$dst}->{'ALERTED'} = "Y";
    return $Scan_href;
}
sub print_scan() {  ### this should primarily be used for debugging
    my $scanfile = $PRINT_SCAN_HASH . ".$$";
    open PSCAN, "> $scanfile";
    print PSCAN Dumper $Scan_href;
    close PSCAN;
    chmod 0600, $scanfile;
    print STDOUT "\n ... Printing scan data structures to $scanfile\n\n";
    return;
}
sub print_scan_and_exit() {  ### this should primarily be used for debugging
    my $scanfile = $PRINT_SCAN_HASH . ".$$";
    open PSCAN, "> $scanfile";
    print PSCAN Dumper $Scan_href;
    close PSCAN;
    chmod 0600, $scanfile;
    print STDOUT "\n ... Printing scan data structures to $scanfile\n\n";
    system "$PSADCMD --Kill";
    exit 0;
}
sub check_fw() {
    my $line = shift;
    if ($line !~ /MAC/) {  ### ipchains log messages do not have a MAC address field
        $USE_IPCHAINS = 1;
    } else {
        $USE_IPTABLES = 1;
    }
}
sub get_local_ips() {
    my $Cmds_href = shift;
    my %localips;
    my @ips = `$Cmds_href->{'ifconfig'} -a |$Cmds_href->{'grep'} -w inet`;
    for my $iptmp (@ips) {
        my $ip = (split /:/, (split /\s+/, $iptmp)[2])[1];
        $localips{$ip} = "";
    }
    return \%localips;
}
sub get_listening_ports() {
    my $Cmds_href = shift;
    my %listening_ports;
    my @ports = `$Cmds_href->{'netstat'} -an |$Cmds_href->{'grep'} \"LISTEN\\b\"`;
    for my $port_tmp (@ports) {
        my ($proto, $p_tmp) = (split /\s+/, $port_tmp)[0,3];
        my $port = (split /:/, $p_tmp)[1];
        my $key = $proto . $port;
        $listening_ports{$key} = "";
    }
    return \%listening_ports;
}
sub get_whois_data() {
    my ($whois_timeout, $ip, $Cmds_href) = @_;
    my @whois_data;
    $whois_timeout = 1;
    eval {
        local $SIG{'ALRM'} = sub {die "alarm\n"};
        alarm $whois_timeout;
        @whois_data = `$Cmds_href->{'whois'} $ip`;
        alarm 0;
    };
    if ($@) {
        die unless $@ eq "alarm\n";
        $#whois_data = 0;
        @whois_data = ("Whois data not available!\n");
        return \@whois_data;
    } else {
        return \@whois_data;
    }
}
sub REAPER {
    my $pid;
    $pid = waitpid(-1, WNOHANG);
#   if (WIFEXITED($?)) {
#       print STDERR "@@@@@  Process $pid exited.\n";
#   }
    $SIG{'CHLD'} = \&REAPER;
    return;
}
sub check_permissions() {
    my @files = @_;
    for my $f (@files) {
        if (-e $f) {
            chmod 0600, $f;
        } else {
            open T, "> $f";
            close T;
            chmod 0600, $f;
        }
    }
    return;
}
sub kill_psad() {
    my $pidfiles_aref = shift;

    ### must kill psadwatchd first since if not, it might try to restart any of the other three daemons; 
    ### (it's the first element of $pidfiles_aref)
    for my $pidfile (@$pidfiles_aref) {
        # my $pidname = (split /\./, $pidfile)[1];
        my $pidname = (split /\./, (split /\//, $pidfile)[$#_])[0];
        if (-e $pidfile) {
            open PIDFILE, "< $pidfile";
            my $pid = <PIDFILE>;
            close PIDFILE;
            chomp $pid;
            if (kill 0, $pid) {
                print " ... Killing $pidname, pid: $pid\n";
                kill 15, $pid or print "@@@@@  psad: Could not kill $pidname, pid: $pid\n";
            } else {
                print "@@@@@  psad: $pidname is not running on $HOSTNAME.\n";
            }
        } else {
            print "@@@@@  psad: pid file $pidfile does not exist for $pidname on $HOSTNAME\n";
        }
    }
    return;
}
sub restart_psad() {
    my ($pidfiles_aref, $cmdline_file, $Cmds_href) = @_;
    my $cmdline;
    if (-e $cmdline_file) {
        open CMD, "< $cmdline_file";
        $cmdline = <CMD>;
        close CMD;
        chomp $cmdline;
    } else {
        die "@@@@@  psad:  No other psad process is currently running on $HOSTNAME!\n";
    }
    &kill_psad($pidfiles_aref);
    print " ... Restarting the psad daemons on $HOSTNAME\n";
    system "$Cmds_href->{'psad'} $cmdline";
    return;
}
sub psad_status() {
    my ($pidfiles_aref, $cmdline_file, $Cmds_href) = @_;
    my $cmdline;
    ### only the psad daemon runs with command line arguments
    if (-e $cmdline_file) {
        open CMD, "< $cmdline_file";
        $cmdline = <CMD>;
        chomp $cmdline;
    }
    my $rv = 0;   ### assume psad is not running and test...
    for my $pidfile (@$pidfiles_aref) {
#       my $pidname = (split /\./, (split /\//, $pidfile)[$#_])[0];
        my @p_tmp = split /\//, $pidfile;
        my $pname_tmp = $p_tmp[$#p_tmp];
        my $pidname = (split /\./, $pname_tmp)[0];
        if (-e $pidfile) {
            my $pid_mtime = stat($pidfile)->mtime;
            ### ($sec, $min, $hr, $day_of_month, $mon, $year, $wday, $yday, $isdst) = localtime($mtime);
            my @timevars = localtime($pid_mtime);
            $timevars[4] += 1;
            if ($timevars[4] =~ /^\d$/) {
                $timevars[4] = "0" . $timevars[4];
            }
            if ($timevars[3] =~ /^\d$/) {
                $timevars[3] = "0" . $timevars[3];
            }
            if ($timevars[0] =~ /^\d$/) {
                $timevars[0] = "0" . $timevars[0];
            }
            if ($timevars[1] =~ /^\d$/) {
                $timevars[1] = "0" . $timevars[1];
            }
            if ($timevars[2] =~ /^\d$/) {
                $timevars[2] = "0" . $timevars[2];
            }
            $timevars[5] += 1900;
            open PIDFILE, "< $pidfile";
            my $pid = <PIDFILE>;
            close PIDFILE;
            chomp $pid;
            if (kill 0, $pid) {
                print " ... $pidname is running on $HOSTNAME as pid: $pid\n";
                my @grep_output = `$Cmds_href->{'ps'} -auxww |$Cmds_href->{'grep'} $pid`;
                GP: for my $g (@grep_output) {
                    if ($g =~ /^\S+\s+$pid\s+(\S+)\s+(\S+)/) {
                        print "     %CPU: $1  %MEM: $2\n";
                        if ($pidname eq "psad" && $cmdline) {
                            print "     Command line arguments: $cmdline\n";
                        } elsif ($pidname eq "psad") {
                            print "     Command line arguments: [none specified]\n";
                        }
                        last GP;
                    }
                }
                print "     Running since: $timevars[2]:$timevars[1]:$timevars[0] $timevars[4]/$timevars[3]/$timevars[5]\n";
                print "\n";
                $rv = 1;
            } else {
                print " ... $pidname is not currently running on $HOSTNAME\n";
            }
        } else {
            print "@@@@@  psad: pid file $pidfile does not exist for $pidname on $HOSTNAME\n";
        }
    }
    return $rv;
}
sub psad_usr1() {
    my $pidfiles_aref = shift;
    my $rv = 0;
    my $psad_pidfile = $pidfiles_aref->[1];
    if (-e $psad_pidfile) {
        open PIDFILE, "< $psad_pidfile" or die "@@@@@  Could not open $psad_pidfile: $!\n";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  ### make sure psad is actually running
            if (kill "USR1", $pid) {
                $rv = 1;
                sleep 1;  ### sleep to give time for the USR1 signal to be delivered and for the file to be created.
                open U, "< /var/log/psad/scan_hash.${pid}" 
                or print "@@@@@  Sent psad pid $pid a USR1 signal, but could not open\n" . 
                     "\"/var/log/psad/scan_hash.${pid}\n\"" and return $rv;
                print while(<U>);
                close U;
            } else {
                print "@@@@@  Could not send psad the USR1 signal on $HOSTNAME\n";
            }
        } else {
            print " ... psad is not currently running on $HOSTNAME\n";
        }
    }
    return $rv;
}
sub psad_setup() {
    my ($psad_dir, $fw_data, $psad_logfile, $error_log, $Cmds_href) = @_;
    unless (-d $psad_dir) {
        mkdir $psad_dir, 400;
    }
    unless (-e $fw_data) {
        open F, "> $fw_data";
        close F;
    }
    unless (-e $psad_logfile) {
        open L, "> $psad_logfile";
        close L;
    }
    unless (-e $error_log) {
        open E, "> $error_log";
        close E;
    }
    unless (-e "/var/log/psadfifo") {
        `$Cmds_href->{'mknod'} -m 600 /var/log/psadfifo p`;
    }
    unless (`$Cmds_href->{'grep'} psadfifo /etc/syslog.conf`) {
        copy("/etc/syslog.conf", "/etc/syslog.conf.orig") unless (-e "/etc/syslog.conf.orig");
        open SYSLOG, ">> /etc/syslog.conf" or die "@@@@@  Unable to open /etc/syslog.conf: $!\n";
        print SYSLOG "kern.info  |/var/log/psadfifo\n\n";  #reinstate kernel logging to our named pipe
        close SYSLOG;
        system("$Cmds_href->{'syslog_init'} restart");
    }
    return;
}
sub usage_and_exit() {
        my $exitcode = shift;
        print <<_HELP_;

psad; the Port Scan Attack Detector
Version: $VERSION
By Michael B. Rash (mbr\@cipherdyne.com, http://www.cipherdyne.com)

USAGE: psad [-D] [-d] [-o] [-e] [-L] [-f] [-r] [-w] [-l] [-i <interval>] [-h]
       [-V] [-K] [-R] [-U] [-S] [-c <config file>] [-s <signature file>]
       [-a <auto ips file>]

OPTIONS:
        -D   --Daemon                   - do not run as a daemon.
        -e   --errors                   - do not write errors to the error
                                          log.
        -d   --debug                    - run psad in debugging mode.
        -w   --whois                    - disable whois lookups.
        -i   --interval                 - configure the check interval from
                                          the command line to override the 15
                                          second default.
        -f   --firewallcheck            - disable firewall rules verification.
        -o   --output                   - print all messages to STDOUT (this
                                          does not include bad packet messages
                                          that are printed to the error log).
        -c   --config <config file>     - use config file instead of the
                                          values contained within the psad
                                          script.
        -L   --Logging_server           - psad is being run on a syslog
                                          logging server.
        -r   --reversedns               - disable name resolution against
                                          scanning ips.
        -s   --signatures <sig file>    - import scan signatures.
        -a   --auto_ips <ips file>      - import auto ips file for automatic
                                          ip danger level increases/decreses.
        -l   --local_port_lookup        - disable local port lookups for scan
                                          signatures.
        -K   --Kill                     - kill all running psad processes.
        -R   --Restart                  - restart all running psad processes.
        -S   --Status                   - displays the status of any
                                          currently running psad processes.
        -U   --USR1                     - send a running psad process a USR1
                                          signal.
        -V   --Version                  - print the psad version and exit.
        -h   --help                     - prints this help message.

_HELP_
        exit $exitcode;
}
