#!/usr/bin/perl -w
#
# File: psad
#
# Purpose: psad makes use of ipchains/iptables logs to detect port
#          scans.  Data is provided by kmsgsd which reads firewall
#          messages out of the /var/lib/psad/psadfifo named pipe
#          (syslog is reconfigured to write kern.info messages there
#          which include firewall messages).  For more information
#          read the psad man page.
#
# Author: Michael B. Rash (mbr@cipherdyne.com)
#
# Credits:  (see the CREDITS file)
#
# Version: 1.0.0-pre1
#
# Copyright (C) 1999-2002 Michael B. Rash (mbr@cipherdyne.com)
#
# License (GNU Public License):
#
#    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:
#   - Build data directories by ip address in /var/log/psad in a manner
#     similar to snort.
#   - 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 tcp signatures that only require a syn packet
#     to a port as well as udp signatures.
#   - 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).
#   - 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
#
# Occaisonally there must be a buffering issue with klogd since log
# entries are sometimes 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
#
#################################################################################################
#
# $Id: psad,v 1.7 2002/09/24 02:06:20 mbr Exp $
#

### modules used by psad
use Psad;
use File::stat 'stat';
use File::Copy;
use Getopt::Long 'GetOptions';
use Socket;
use Sys::Hostname 'hostname';
use Unix::Syslog qw(:subs :macros);
use POSIX;
use IO::Handle;
use Data::Dumper;
use strict;

### ====================== config defaults ==========================
my $CONFIG_DIR        = '/etc/psad';
my $CONFIG_FILE       = "${CONFIG_DIR}/psad.conf";
my $SIGS_FILE         = "${CONFIG_DIR}/psad_signatures";
my $AUTOIPS_FILE      = "${CONFIG_DIR}/psad_auto_ips";
my $AUTO_BLOCKED_FILE = "${CONFIG_DIR}/psad_auto_blocked_ips";
my $USE_IPCHAINS      = 0;
my $USE_IPTABLES      = 0;
my $DEBUG             = 0;
my $HOSTNAME          = hostname; ### Calculate hostname of the machine on which psad is running.
my $VERSION           = '1.0.0-pre1';
### ======================== end config =============================

### ========================== main =================================
### main psad data structure; contains ips, port ranges, tcp flags, and danger levels
my %Scan;

### %Sigs holds all scan signatures (only initialized if "-s <sig file>"
### is specified on the command line).
my %Sigs;

### %Auto_ips holds all ip addresses that should automatically
### be assigned a danger level (or ignored).  (Only initialized
### if the "-a <auto ips file>" is specified on the command line).
my %Auto_ips;

### cache the addresses we have executed whois lookups for
my %whois_cache;

### initialize and scope some default variables (command line args can override some default values)
my ($found_new_packets, $daemon, $output, $errors, $dnslookups, $whoislookups,
    $netstat_lookup, $fwcheck, $Syslog_server, $kill, $restart, $status, $usr1,
    $print_version, $help, $check_interval) = (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'     => \$AUTOIPS_FILE,   ### 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_FILE,    ### specify configuration file
    'reversedns'     => \$dnslookups,     ### do not issue dns lookups against scanning ip address
    'signatures=s'   => \$SIGS_FILE,      ### 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";

### read in configuration file
my ($Config_href, $Cmds_href) = &Psad::buildconf($CONFIG_FILE);

### make sure the configuration is complete
&check_config();

my %Config = %$Config_href;
my %Cmds   = %$Cmds_href;
my $Emailaddrs_aref = $Config{'EMAIL_ADDRESSES'};
my $CMDLINE_FILE    = $Config{'PSAD_CMDLINE_FILE'}; ### File used to store the psad command line.

### pid file array
my @PIDFILES = ($Config{'PSADWATCHD_PID_FILE'}, $Config{'PSAD_PID_FILE'},
                    $Config{'KMSGSD_PID_FILE'}, $Config{'DISKMOND_PID_FILE'});

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

### The --USR1 command line switch was given.
&psad_usr1() if $usr1;

### The -i <check interval> command line argument was given
if ($check_interval) {
    $Config{'PSAD_CHECK_INTERVAL'} = $check_interval;
}

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

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

### the --Status command line switch was given
&psad_status() if $status;

### make sure $PSAD_DIR, $FW_DATA, and /var/lib/psad/psadfifo, etc. actually
### exist
&psad_setup();

### the --Restart command line switch was given
if ($restart) {
    &restart_psad();
    exit 0;
}

### check to make sure another psad process is not already running.
&Psad::unique_pid($Config{'PSAD_PID_FILE'});

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

### get the ip addresses that are local to this machine
my %Local_ips;
&get_local_ips();

### 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'} !~ /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($Config{'FW_MSG_SEARCH'}, $Emailaddrs_aref, ['/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: $!";
}

### write the current pid associated with psad to the psad pid file
&Psad::writepid($Config{'PSAD_PID_FILE'});

### 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 PSAD_PID_FILE above.
if ($CONFIG_FILE eq '/etc/psad/psad.conf') {
    system "$Cmds{'kmsgsd'}";
    system "$Cmds{'diskmond'}";
    system "$Cmds{'psadwatchd'}" unless ($DEBUG);
} else {  ### start the other daemons with the new (non-default) config file
    system "$Cmds{'kmsgsd'} -c $CONFIG_FILE";
    system "$Cmds{'diskmond'} -c $CONFIG_FILE";
    system "$Cmds{'psadwatchd'} -c $CONFIG_FILE" unless ($DEBUG);
}

### import the scan signatures and auto ips file
&import_signatures() if $SIGS_FILE;
&import_auto_ips()   if $AUTOIPS_FILE;

### Check to see if psad automatically blocked some ips from
### a previous run.  This feature is most useful for preserving
### automatically blocked ips after a reboot.
if ($Config{'ENABLE_AUTO_IDS'} eq 'Y') {
    &check_auto_blocked();
}

### archive old firewall data
&archive_fwdata();

### Install signal handlers for debugging %Scan with Data::Dumper,
### and for reaping zombie whois processes
$SIG{'__WARN__'} = \&Psad::warn_handler;
$SIG{'__DIE__'}  = \&Psad::die_handler;
$SIG{'CHLD'}     = \&REAPER;
$SIG{'USR1'}     = \&print_scan;

### Get the mtimes of the signatures and auto ips files.
my $sigs_mtime     = stat($SIGS_FILE)->mtime;
my $auto_ips_mtime = stat($AUTOIPS_FILE)->mtime;
my $config_mtime   = stat($CONFIG_FILE)->mtime;

### Get an open filehandle for the main firewall data file $FW_DATA.
### All firewall drop/deny/reject log messages are written to $FW_DATA
### by kmsgsd.
open FWDATA, $Config{'FW_DATA'};

###=========================================================###
######                    MAIN LOOP                      ######
###=========================================================###
for (;;) {
    ### See if we need to import any changed config variables
    &check_import_config(\$config_mtime, $CONFIG_FILE);

    ### See if we need to import any new signatures
    &check_import(\$sigs_mtime, $SIGS_FILE, 1);

    ### See if we need to re-import the psad_auto_ips file
    &check_import(\$auto_ips_mtime, $AUTOIPS_FILE, 0);

    ### Get any new packets have been written to
    ### $FW_DATA by kmsgsd for psad analysis.
    my @new_packets = <FWDATA>;
    if (@new_packets) {
        my $found_potential_scan = &check_scan(\@new_packets, $SIGS_FILE,
                                                 $netstat_lookup, $errors);
        if ($found_potential_scan) {

            ### Assign a danger level to the scan
            &assign_danger_level();

            ### Log and send an email/syslog alert
            &scan_logr($output, $dnslookups, $whoislookups);

            ### Don't manage the firewall rules if
            ### psad is running on a syslog server
            if ($Config{'ENABLE_AUTO_IDS'} eq 'Y' && ! $Syslog_server) {
                &auto_psad_response();
            }
        }
    }
    ### Print how many new packets we got in $FW_DATA if we are
    ### running in $DEBUG mode
    if ($DEBUG) {
        print "MAIN: number of new packets: $#new_packets\n";
    }

    sleep $Config{'PSAD_CHECK_INTERVAL'};
    ### clearerr() on the FWDATA filehandle to be ready for new packets
    FWDATA->clearerr();
}
exit 0;
###=========================================================###
######                    END MAIN                       ######
###=========================================================###

#=================== BEGIN SUBROUTINES ========================
### 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, $signatures, $netstat_lookup, $errors) = @_;
    my @bad_packets;
    my $local_listening_ports_href;
    my ($src, $dst, $len, $ttl, $proto, $srcport, $dstport, $flags, $type, $code, $id, $seq);
    my $matched_packet = 0;
    ### 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();
    }
    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+)\s+DST=(\S+)\s+LEN=(\d+).*TTL=(\d+).*PROTO=(\S+)\s+SPT=(\d+)\s+DPT=(\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+(.*)\s+URGP=/) {
                        $flags = $1;
                    } else {
                        $flags = 'NULL';
                    }
                    ### 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 !~ /WINDOW/)) {
                        push @bad_packets, $l;
                        next READPKT;
                    }
                } ### else it is UDP, but there are no more fields we need to define since we already have the ports, etc.
            } elsif ($l =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+).*TTL=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\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);
                ### should implement protocol mapping with /etc/protocols here
                if ($proto == 1) {
                    $proto = 'ICMP';
                } elsif ($proto == 6) {
                    $proto = 'TCP';
                } elsif ($proto == 17) {
                    $proto = 'UDP';
                }
                $flags = 'NONE';
            } else {
                push @bad_packets, $l;
                next READPKT;
            }
        }
        if ($Config{'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'}) >= $Config{'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 ($Config{'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;
            if ($Config{'ENABLE_PERSISTENCE'} eq 'N') {
                $Scan{$src}{$dst}{'START_TIME'}{'EPOCH_SECONDS'} = $epoch_seconds;
            }
        }
        $Scan{$src}{$dst}{$proto}{'CURRENT_INTERVAL'}{'PACKETS'}++;
        $Scan{$src}{$dst}{'END_TIME'}{'READABLE'}= $time;
        if ($Config{'ENABLE_PERSISTENCE'} eq 'N') {
            $Scan{$src}{$dst}{'END_TIME'}{'EPOCH_SECONDS'} = $epoch_seconds;
        }
        ### 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 ($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 ($Config{'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 ($Config{'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"    .
                      "     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{$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($msg, $srcport, $dstport, $proto)
                        && &check_misc_fields($msg, $proto, $len, $ttl)
                            && &check_tcp_flags($msg, $flags, $proto)) {   ### tripped a tcp signature
                        $Scan{$src}{$dst}{'TCP'}{'SIGMATCH'}{'SIGDL'} = $Sigs{'TCP'}{$msg}{'DANGERLEVEL'};
                        $rv = 1;
                    }
                } elsif ($proto eq 'UDP') {
                    if (&check_port($msg, $srcport, $dstport, $proto)
                        && &check_misc_fields($msg, $proto, $len, $ttl)) { ### tripped a udp signature
                        $Scan{$src}{$dst}{'UDP'}{'SIGMATCH'}{'SIGDL'} = $Sigs{'UDP'}{$msg}{'DANGERLEVEL'};
                        $rv = 1;
                    }
                } elsif ($proto eq 'ICMP') {
                    if (&check_icmp_sigs($msg, $ttl, $type, $code, $id, $seq)) {
                        $Scan{$src}{$dst}{'ICMP'}{'SIGMATCH'}{'SIGDL'} = $Sigs{'ICMP'}{$msg}{'DANGERLEVEL'};
                        $rv = 1;
                    }
                }
                if ($rv) {   ### we matched some 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{$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'}) {
                        $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) unless $errors;
    return $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 ($msg, $flags_to_check, $proto) = @_;
    return 0 if ($proto ne 'TCP');
    my $msgflags = $Sigs{$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 ( $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{$proto}{$msg}{'DSTPORT'}{'UNIQUE_OR_ANY'}) {
        unless ($Sigs{$proto}{$msg}{'DSTPORT'}{'UNIQUE_OR_ANY'} eq 'any') {
            return 0 if ($dstport != $Sigs{$proto}{$msg}{'DSTPORT'}{'UNIQUE_OR_ANY'});
        }
    }
    if (defined $Sigs{$proto}{$msg}{'DSTPORT'}{'START'}) {
        my $start = $Sigs{$proto}{$msg}{'DSTPORT'}{'START'};
        my $end = $Sigs{$proto}{$msg}{'DSTPORT'}{'END'};
        return 0 if ($dstport < $start || $dstport > $end);
    }
    if (defined $Sigs{$proto}{$msg}{'DSTPORT'}{'NOT'}) {
        return 0 if ($dstport == $Sigs{$proto}{$msg}{'DSTPORT'}{'NOT'});
    }
    if (defined $Sigs{$proto}{$msg}{'DSTPORT'}{'NEGSTART'}) {
        my $start = $Sigs{$proto}{$msg}{'DSTPORT'}{'NEGSTART'};
        my $end = $Sigs{$proto}{$msg}{'DSTPORT'}{'NEGEND'};
        return 0 if ($dstport > $start || $dstport < $end);
    }
    ### check src port
    if (defined $Sigs{$proto}{$msg}{'SRCPORT'}{'UNIQUE_OR_ANY'}) {
        unless ($Sigs{$proto}{$msg}{'SRCPORT'}{'UNIQUE_OR_ANY'} eq 'any') {
            return 0 if ($dstport != $Sigs{$proto}{$msg}{'SRCPORT'}{'UNIQUE_OR_ANY'});
        }
    }
    if (defined $Sigs{$proto}{$msg}{'SRCPORT'}{'START'}) {
        my $start = $Sigs{$proto}{$msg}{'SRCPORT'}{'START'};
        my $end = $Sigs{$proto}{$msg}{'SRCPORT'}{'END'};
        return 0 if ($dstport < $start || $dstport > $end);
    }
    if (defined $Sigs{$proto}{$msg}{'SRCPORT'}{'NOT'}) {
        return 0 if ($dstport == $Sigs{$proto}{$msg}{'SRCPORT'}{'NOT'});
    }
    if (defined $Sigs{$proto}{$msg}{'SRCPORT'}{'NEGSTART'}) {
        my $start = $Sigs{$proto}{$msg}{'SRCPORT'}{'NEGSTART'};
        my $end   = $Sigs{$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 ($msg, $ttl, $type, $code, $icmp_id, $icmp_seq) = @_;
    ### check icmp type first
    if (defined $Sigs{'ICMP'}{$msg}{'TYPE'}) {
        return 0 if ($Sigs{'ICMP'}{$msg}{'TYPE'} != $type);
    }
    if (defined $Sigs{'ICMP'}{$msg}{'TTL'}) {
        return 0 if ($Sigs{'ICMP'}{$msg}{'TTL'} != $ttl);
    }
    if (defined $Sigs{'ICMP'}{$msg}{'CODE'}) {
        return 0 if ($Sigs{'ICMP'}{$msg}{'CODE'} != $code);
    }
    if (defined $Sigs{'ICMP'}{$msg}{'ID'}) {
        return 0 if ($Sigs{'ICMP'}{$msg}{'ICMP_ID'} != $icmp_id);
    }
    if (defined $Sigs{'ICMP'}{$msg}{'SEQ'}) {
        return 0 if ($Sigs{'ICMP'}{$msg}{'ICMP_SEQ'} != $icmp_seq);
    }
    return 1; ### if we got to this point, then we matched the signature
}
sub check_misc_fields() {
    my ($msg, $proto, $len, $ttl) = @_;
    if (defined $Sigs{$proto}{$msg}{'LEN'}) {
        return 0 if ($Sigs{$proto}{$msg}{'LEN'} != $len);
    }
    if (defined $Sigs{$proto}{$msg}{'TTL'}) {
        return 0 if ($Sigs{$proto}{$msg}{'TTL'} != $ttl);
    }
    return 1;
}
sub check_import() {
    my ($mtime_ref, $file, $sigs) = @_;
    my $mtime_tmp = stat($file)->mtime;
    if ($mtime_tmp != $$mtime_ref) {  ### the file was modified, so import
        if ($sigs) {  ### import signatures
            &import_signatures($file);
        } else {
            &import_auto_ips($file);
            ### need to set $Scan{$src}{$dst}{'AUTO'} = "N" foreach
            ### src and dst since %Auto_ips was updated.
            &reset_auto_tags();
        }
        for my $email_address (@$Emailaddrs_aref) {
            system "$Cmds{'mail'} $email_address -s \"psad: re-read $file" .
                            " file on $HOSTNAME\" < /dev/null > /dev/null 2>&1";
        }
        $$mtime_ref = $mtime_tmp;
    }
    return;
}
sub check_import_config() {
    my ($mtime_ref, $file) = @_;
    my $mtime_tmp = stat($file)->mtime;
    if ($mtime_tmp != $$mtime_ref) {  ### the file was modified, so import

        ($Config_href, $Cmds_href) = &Psad::buildconf($file);

        ### make sure the configuration is complete
        &check_config();

        %Config = %$Config_href;
        %Cmds   = %$Cmds_href;
        $Emailaddrs_aref = $Config{'EMAIL_ADDRESSES'};
        for my $email_address (@$Emailaddrs_aref) {
            system "$Cmds{'mail'} $email_address -s \"psad: re-read $file" .
                            " file on $HOSTNAME\" < /dev/null > /dev/null 2>&1";
        }
        $$mtime_ref = $mtime_tmp;

        ### restart kmsgsd since it does not have the capability of
        ### checking for a modified config file
        open K, "< $Config{'KMSGSD_PID_FILE'}" or warn " @@@ Could not open $Config{'KMSGSD_PID_FILE'}";
        my $pid = <K>;
        close K;
        if (kill 0, $pid) {
            kill 15, $pid;
            if ($file eq '/etc/psad/psad.conf') {
                system "$Cmds{'kmsgsd'}";
            } else {
                system "$Cmds{'kmsgsd'} -c $file";
            }
        }
    }
    return;
}
sub import_signatures() {
    open SIGS, "< $SIGS_FILE" or die "Could not open the signatures file $SIGS_FILE: $!";
    my @sigs = <SIGS>;
    close SIGS;
    for my $sig (@sigs) {
        chomp $sig;
        next if ($sig =~ /^\s*#/);
        &get_signature_fields($sig);
    }
    print STDOUT Dumper %Sigs if $DEBUG;
    return;
}
sub get_signature_fields() {
    my $sig = shift;
    my ($proto, $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{$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($sig, $proto, $msg);
        }
        for my $f (@fields) {
            if ($f =~ /flags\:\s*?(\w+)/i) {
                $Sigs{$proto}{$msg}{'FLAGS'} = $1;
            } elsif ($f =~ /ttl\:\s*(\d+)/i) {
                $Sigs{$proto}{$msg}{'TTL'} = $1;
            } elsif ($f =~ /itype\:\s*?(\d+)/i) {
                $Sigs{$proto}{$msg}{'TYPE'} = $1;
            } elsif ($f =~ /icode\:\s*?(\d+)/i) {
                $Sigs{$proto}{$msg}{'CODE'} = $1;
            } elsif ($f =~ /icmp_seq\:\s*?(\d+)/i) {
                $Sigs{$proto}{$msg}{'ICMP_SEQ'} = $1;
            } elsif ($f =~ /icmp_id\:\s*?(\d+)/i) {
                $Sigs{$proto}{$msg}{'ICMP_ID'} = $1;
            } elsif ($f =~ /dlevel\:\s*?(\d{1})/i) {
                $Sigs{$proto}{$msg}{'DANGERLEVEL'} = $1;
            }
        }
    }
    return;
}
sub get_signature_ports() {
    my ($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{$proto}{$msg}{'SRCPORT'}{'START'} = $start;
        $Sigs{$proto}{$msg}{'SRCPORT'}{'END'}   = $end;
    } elsif ($srcport =~ /\!/ && $srcport !~ /\:/) {
        $tmpport = (split /\!/, $srcport)[1];
        $Sigs{$proto}{$msg}{'SRCPORT'}{'NOT'} = $tmpport;
    } elsif ($srcport =~ /\:/ && $srcport =~ /\!/) {
        ($start, $end) = split /:/, $srcport;
        $start = 1   if ($start !~ /\d/);
        $end = 65535 if ($end   !~ /\d/);
        $Sigs{$proto}{$msg}{'SRCPORT'}{'NEGSTART'} = $start;
        $Sigs{$proto}{$msg}{'SRCPORT'}{'NEGEND'}   = $end;
    } else {
        $Sigs{$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{$proto}{$msg}{'DSTPORT'}{'START'} = $start;
        $Sigs{$proto}{$msg}{'DSTPORT'}{'END'}   = $end;
    } elsif ($dstport =~ /\!/ && $dstport !~ /\:/) {
        $tmpport = (split /\!/, $dstport)[1];
        $Sigs{$proto}{$msg}{'DSTPORT'}{'NOT'} = $tmpport;
    } elsif ($dstport =~ /\:/ && $dstport =~ /\!/) {
        ($start, $end) = split /:/, $dstport;
        $start = 1   if ($start !~ /\d/);
        $end = 65535 if ($end   !~ /\d/);
        $Sigs{$proto}{$msg}{'DSTPORT'}{'NEGSTART'} = $start;
        $Sigs{$proto}{$msg}{'DSTPORT'}{'NEGEND'}   = $end;
    } else {
        $Sigs{$proto}{$msg}{'DSTPORT'}{'UNIQUE_OR_ANY'} = $dstport;
    }
    return;
}
sub import_auto_ips() {
    open AUTO, "< $AUTOIPS_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;
}
sub reset_auto_tags() {
    for my $src (keys %Scan) {
        for my $dst (keys %{$Scan{$src}}) {
            $Scan{$src}{$dst}{'AUTO'} = 'N';
        }
    }
    return;
}
sub check_range() {
    my ($port, $start, $end) = @_;
    $start = $port if ($port < $start);
    $end   = $port if ($port > $end);
    return $start, $end;
}
sub assign_danger_level() {
    &automatic_ip_danger_assignment() if (%Auto_ips);
    for my $src (keys %Scan) {
        print "assign_danger_level(): source ip: $src\n" if $DEBUG;
        DST: for my $dst (keys %{$Scan{$src}}) {
            my $absnum = $Scan{$src}{$dst}{'ABSNUM'};
            my $range;
            if (defined $Scan{$src}{$dst}{'START_PORT'}) {
                $range = $Scan{$src}{$dst}{'END_PORT'} - $Scan{$src}{$dst}{'START_PORT'};
            } else {
                $range = $absnum;
            }
            if ($DEBUG) {
                print "assign_danger_level(): destination ip: $dst\n";
                print "assign_danger_level(): ABSNUM: $Scan{$src}{$dst}{'ABSNUM'}\n";
                if (defined $Scan{$src}{$dst}{'START_PORT'}) {
                    print "assign_danger_level(): START_PORT: $Scan{$src}{$dst}{'START_PORT'}, "
                        . "END_PORT: $Scan{$src}{$dst}{'END_PORT'}\n";
                }
                print 'assign_danger_level(): CURRENT_DANGER_LEVEL (before assignment) = '
                    . "$Scan{$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{$src}{$dst}{'TCP'}{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = 'TCP';
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if (defined $Scan{$src}{$dst}{'UDP'}{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = 'UDP';
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if (defined $Scan{$src}{$dst}{'ICMP'}{'SIGMATCH'}) {
                $sigmatch = 1;
                $sigproto = 'ICMP';
                print "assign_danger_level(): sigmatch = $sigmatch\n" if $DEBUG;
            }
            if ($sigmatch && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} <
                            $Scan{$src}{$dst}{$sigproto}{'SIGMATCH'}{'SIGDL'}) {
                $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} =
                                $Scan{$src}{$dst}{$sigproto}{'SIGMATCH'}{'SIGDL'};
                $Scan{$src}{$dst}{'ALERTED'} = 'N';
                print "assign_danger_level(): danger level: "
                    . "$Scan{$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 < $Config{'DANGER_LEVEL1'}) {
                ### we don't have enough packets to even reach danger level 1 yet.
                next DST;
            } elsif ($absnum < $Config{'DANGER_LEVEL2'} && $range >= $Config{'PORT_RANGE_SCAN_THRESHOLD'}) {
                if ($Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} < 1
                        && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan{$src}{$dst}{'ALERTED'} = 'N';
                    $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} = 1;
                }
            } elsif ($absnum < $Config{'DANGER_LEVEL3'} && $range >= $Config{'PORT_RANGE_SCAN_THRESHOLD'}) {
                if ($Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} < 2
                        && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan{$src}{$dst}{'ALERTED'} = 'N';
                    $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} = 2;
                }
            } elsif ($absnum < $Config{'DANGER_LEVEL4'} && $range >= $Config{'PORT_RANGE_SCAN_THRESHOLD'}) {
                if ($Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} < 3
                        && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan{$src}{$dst}{'ALERTED'} = 'N';
                    $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} = 3;
                }
            } elsif ($absnum < $Config{'DANGER_LEVEL5'} && $range >= $Config{'PORT_RANGE_SCAN_THRESHOLD'}) {
                if ($Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} < 4
                        && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan{$src}{$dst}{'ALERTED'} = 'N';
                    $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} = 4;
                }
            } elsif ($range >= $Config{'PORT_RANGE_SCAN_THRESHOLD'}) {
                if ($Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} < 5
                        && $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'} != -1) {
                    $Scan{$src}{$dst}{'ALERTED'} = 'N';
                    $Scan{$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 ($Config{'ALERT_ALL'} eq 'Y') {
                $Scan{$src}{$dst}{'ALERTED'} = 'N';
            }
            print 'assign_danger_level(): CURRENT_DANGER_LEVEL (after assignment) = '
                . "$Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'}\n" if $DEBUG;
        }
    }
    return;
}
sub automatic_ip_danger_assignment() {
    for my $scan_ip (keys %Scan) {
        for my $dst (keys %{$Scan{$scan_ip}}) {
            for my $auto_ip (keys %Auto_ips) {
                if ($scan_ip eq $auto_ip && $Scan{$scan_ip}{$dst}{'AUTO'} eq 'N') {
                    $Scan{$scan_ip}{$dst}{'CURRENT_DANGER_LEVEL'} = $Auto_ips{$auto_ip};
                    $Scan{$scan_ip}{$dst}{'ALERTED'} = 'N';
                    $Scan{$scan_ip}{$dst}{'AUTO'} = 'Y';
                }
            }
        }
    }
    return;
}
sub collect_errors() {
    my ($bad_packets_aref) = @_;
    open ERRORS, ">> $Config{'ERROR_LOG'}";
    for my $l (@$bad_packets_aref) {
        print ERRORS "$l\n";
    }
    close ERRORS;
}
sub scan_logr() {
    my ($output, $dnslookups, $whoislookups) = @_;
    my $range;
    my ($tcp_newrange, $udp_newrange);
    my $dnsstring = '';
    my $whois_info_aref;
    $Config{'PSAD_LOGFILE'} = *STDOUT if ($output || $DEBUG);
    my @print_array = ($Config{'PSAD_LOGFILE'}, $Config{'EMAIL_ALERTFILE'});
    for my $src (keys %Scan) {
        print STDOUT "scan_logr(): source ip: $src\n" if $DEBUG;
        DST: for my $dst (keys %{$Scan{$src}}) {
            unless (defined $Scan{$src}{$dst}{'EMAIL_LIMIT'}) {
                $Scan{$src}{$dst}{'EMAIL_LIMIT'} = 0;
            } elsif ($Scan{$src}{$dst}{'EMAIL_LIMIT'} > $Config{'PSAD_EMAIL_LIMIT'}) {
                unless (defined $Scan{$src}{$dst}{'EMAIL_STOPPED'}) {
                    &email_limit_reached($src, $dst);
                    $Scan{$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{$src}{$dst}{'CURRENT_DANGER_LEVEL'};
            if ($current_danger_level >= 1 && $Scan{$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{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}) {
                    $tcp = 1;
                    $tcp_new_start_range = $Scan{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'START_PORT'};
                    $tcp_new_end_range   = $Scan{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'END_PORT'};
                    $tcp_new_start_time  = $Scan{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'START_TIME'};
                    $tcp_new_num_pkts    = $Scan{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'PACKETS'};
                }
                if (defined $Scan{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'}) {
                    $udp = 1;
                    $udp_new_start_range = $Scan{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'}{'START_PORT'};
                    $udp_new_end_range   = $Scan{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'}{'END_PORT'};
                    $udp_new_start_time  = $Scan{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'}{'START_TIME'};
                    $udp_new_num_pkts    = $Scan{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'}{'PACKETS'};
                }
                if (defined $Scan{$src}{$dst}{'ICMP'}{'CURRENT_INTERVAL'}) {
                    $icmp = 1;
                    $icmp_new_start_time = $Scan{$src}{$dst}{'ICMP'}{'CURRENT_INTERVAL'}{'START_TIME'};
                    $icmp_new_num_pkts   = $Scan{$src}{$dst}{'ICMP'}{'CURRENT_INTERVAL'}{'PACKETS'};
                }
                my $start_time = $Scan{$src}{$dst}{'START_TIME'}{'READABLE'};
                my $end_time   = $Scan{$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($src);
                }
                if ($tcp || $udp) {
                    $abs_start_range = $Scan{$src}{$dst}{'START_PORT'};
                    $abs_end_range   = $Scan{$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 $syslog_print_flags = '';
                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";
                    }
                    $syslog_print_flags = 'flags=[';
                    my %individual_flags_tmp;
                    for my $flags (keys %{$Scan{$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{$src}{$dst}{'TCP'}{'CURRENT_INTERVAL'}{'FLAGS'}{$flags};
                        push @tcp_flags, "TCP flags:                   [$flags: $num_pkts packets]${nmapOpts}\n";
                        my @flags_tmp = split /\s+/, $flags;
                        for my $f (@flags_tmp) {
                            $individual_flags_tmp{$f} = '';
                        }
                    }
                    for my $f (keys %individual_flags_tmp) {
                        $syslog_print_flags .= "$f ";
                    }
                    $syslog_print_flags =~ s/\s+$//;
                    $syslog_print_flags .= ']';
                    ### need to delete the current interval so it won't show up in the next alert
                    delete $Scan{$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{$src}{$dst}{'UDP'}{'CURRENT_INTERVAL'};
                }
                if ($icmp) {
                    delete $Scan{$src}{$dst}{'ICMP'}{'CURRENT_INTERVAL'};
                }
                print STDOUT "scan_logr():  generating email......\n" if $DEBUG;
                unlink $Config{'EMAIL_ALERTFILE'};  ### remove $EMAIL_ALERTFILE so we don't send old alert info
                &Psad::logr("=-=-=-=-=-=-=-=-=-=-=-=-=-= $time =-=-=-=-=-=-=-=-=-=-=-=-=-=\n", \@print_array);
                &Psad::logr("psad: portscan detected against $HOSTNAME ($dst).\n", \@print_array);
                &Psad::logr("\n", [$Config{'PSAD_LOGFILE'}, $Config{'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{$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{$src}{$dst}{'CURRENT_DANGER_LEVEL'} out of 5\n", \@print_array);
                &Psad::logr("DNS info:                    $dnsstring\n", \@print_array) unless $dnslookups;

#                my $syslog_print_sig_title = "";
                if ($USE_IPTABLES) {
                    SIGS: for my $proto ('TCP', 'UDP', 'ICMP') {
                        if (defined $Scan{$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{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}}) {
                                next if ($sigmatch eq 'PACKETS' || $sigmatch eq 'START_TIME');
                                my $sig_num_packets = $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'}{$sigmatch};
                                &Psad::logr("$sigmatch  Packets=$sig_num_packets\n", \@print_array);
                                ### signature logging with syslog is not yet supported
                                ### (requires a message for each matched signature).
#                                if ($sigmatch =~ /^(\".*\")/) {
#                                    $syslog_print_sig_title = "signature=$1";
#                                }
                            }
                            ### need to delete the current interval so it won't show up in the next alert
                            delete $Scan{$src}{$dst}{$proto}{'CURRENT_SIGMATCH'};
                        }
                        if (defined $Scan{$src}{$dst}{$proto}{'SIGMATCH'} && $Config{'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{$src}{$dst}{$proto}{'SIGMATCH'}}) {
                                my $sig_num_packets = $Scan{$src}{$dst}{$proto}{'SIGMATCH'}->{$sigmatch};
                                &Psad::logr("$sigmatch  Packets=$sig_num_packets\n", \@print_array);
                            }
                        }
                    }
                }
                ### write a message to syslog
                openlog('psad', LOG_DAEMON, LOG_LOCAL7);
                my $syslog_print_range = '';
                if ($tcp) {
                    $syslog_print_range .= "tcp=[$tcp_newrange] $syslog_print_flags";
                }
                if ($udp) {
                    $syslog_print_range .= "udp=[$udp_newrange] ";
                }
                my $syslog_num_pkts     = $Scan{$src}{$dst}{'ABSNUM'};
                my $syslog_danger_level = $Scan{$src}{$dst}{'CURRENT_DANGER_LEVEL'};

                syslog(LOG_INFO, "port scan detected: $src -> $dst $syslog_print_range pkts=$syslog_num_pkts dangerlevel: $syslog_danger_level");
                closelog();
                unless ($whoislookups) {
                    &Psad::logr("\n\n", \@print_array);
                    &Psad::logr("---- Whois Information: ----\n", [$Config{'PSAD_LOGFILE'}, $Config{'EMAIL_ALERTFILE'}]);
                    for my $w (@$whois_info_aref) {
                        &Psad::logr($w, \@print_array);
                    }
                    &Psad::logr("\n", \@print_array);
                }
                &Psad::logr("=-=-=-=-=-=-=-=-=-=-=-=-=-= $time =-=-=-=-=-=-=-=-=-=-=-=-=-=\n", \@print_array);
                if ($Scan{$src}{$dst}{'ALERTED'} eq 'N'
                                        && $current_danger_level >= $Config{'EMAIL_ALERT_DANGER_LEVEL'}) {
                    &send_email_alert($src, $dst);
                }
                $Scan{$src}{$dst}{'LOGR'} = 'N';
                $Scan{$src}{$dst}{'EMAIL_LIMIT'}++;
            }
        }
    }
    return;
}
sub check_auto_blocked() {
    if (-e $AUTO_BLOCKED_FILE) {
        open B, "< $AUTO_BLOCKED_FILE" or warn " @@@ Could not open $AUTO_BLOCKED_FILE\n" and return;
        my @blines = <B>;
        close B;
        my $use_ipchains;
        my $use_iptables;
        my @block_ips;
        for my $l (@blines) {
            chomp $l;
            if ($l =~ /IPTABLES/) {
                $use_iptables = 1;
            } elsif ($l =~ /IPCHAINS/) {
                $use_ipchains = 1;
            }
            if ($l =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/) {
                push @block_ips, $1;
            }
        }
        if ($use_iptables) {
            my $inchains_aref = &get_input_chains($Cmds{'iptables'});
            for my $inchain (@$inchains_aref) {
                my @rules = `$Cmds{'iptables'} -nL $inchain`;
                for my $blockip (@block_ips) {
                    my $blocked = 0;
                    RULE: for my $rule (@rules) {
                        ### DROP       all  --  1.1.1.1        0.0.0.0/0
                        if ($rule =~ /^DROP\s+all.*?$blockip\s+0\.0\.0\.0\/0/) {
                            ### all traffic from $blockup is being blocked
                            $blocked = 1;
                            last RULE;
                        }
                    }
                    unless ($blocked) {
                        system "$Cmds{'iptables'} -I $inchain 1 -s $blockip -j DROP 2> /dev/null 1>&2";
                    }
                }
            }
        } elsif ($use_ipchains) {
            my $inchains_aref = &get_input_chains($Cmds{'ipchains'});
            for my $inchain (@$inchains_aref) {
                my @rules = `$Cmds{'ipchains'} -nL $inchain`;
                for my $blockip (@block_ips) {
                    my $blocked = 0;
                    RULE: for my $rule (@rules) {
                        ### DENY       udp  ------  0.0.0.0/0      0.0.0.0/0      * ->   *
                        if ($rule =~ /^DENY\s+all.*?$blockip.*0\.0\.0\.0\/0\s+\*\s+\-\>\s+\*/) {
                            ### all traffic from $blockup is being blocked
                            $blocked = 1;
                            last RULE;
                        }
                    }
                    unless ($blocked) {
                        system "$Cmds{'ipchains'} -I $inchain 1 -s $blockip -j DENY 2> /dev/null 1>&2";
                    }
                }
            }
        }
    }
    return;
}
sub auto_psad_response(){
    SOURCE: for my $src (keys %Scan) {
        for my $dst (keys %{$Scan{$src}}) {
            my $current_danger_level = $Scan{$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 >= $Config{'AUTO_IDS_DANGER_LEVEL'} && !(defined $Scan{$src}{$dst}{'BLOCKED'})) {
                if ($USE_IPCHAINS) {
                    my $chains_aref = &get_input_chains($Cmds{'ipchains'});
                    my $blocked_str = '';
                    if ($Config{'IPCHAINS_BLOCK_METHOD'} eq 'Y') {
                        for my $inchain (@$chains_aref) {
                            system "$Cmds{'ipchains'} -I $inchain 1 -s $src -j DENY 2> /dev/null 1>&2";
                        }
                        $blocked_str = 'via ipchains';
                    }
                    if ($Config{'TCPWRAPPERS_BLOCK_METHOD'} eq 'Y') {
                        open H, ">> /etc/hosts.deny" or die "Could not open /etc/hosts.deny: $!";
                        print H "ALL: $src\n";
                        close H;
                        $blocked_str = 'via tcp wrappers';
                    }
                    for my $dst (keys %{$Scan{$src}}) {
                        $Scan{$src}{$dst}{'BLOCKED'} = 'Y';
                    }
                    if ($Config{'ALERT_AUTO_BLOCKED'} eq 'Y' && $blocked_str) {
                        for my $email_address (@$Emailaddrs_aref) {
                            system "$Cmds{'mail'} $email_address -s \"psad: All traffic from $src" .
                                   " has been BLOCKED $blocked_str on $HOSTNAME ($dst)\" < /dev/null > /dev/null 2>&1";
                        }
                    }
                    ### write the $src ip to the $AUTO_BLOCKED_FILE
                    &write_blocked_ip($src);
                    next SOURCE;
                } elsif ($USE_IPTABLES) {
                    my $chains_aref = &get_input_chains($Cmds{'iptables'});
                    my $blocked_str = '';
                    if ($Config{'IPTABLES_BLOCK_METHOD'} eq 'Y') {
                        for my $inchain (@$chains_aref) {
                            system "$Cmds{'iptables'} -I $inchain 1 -s $src -j DROP 2> /dev/null 1>&2";
                        }
                        $blocked_str = 'via iptables';
                    }
                    if ($Config{'TCPWRAPPERS_BLOCK_METHOD'} eq 'Y') {
                        open H, '>> /etc/hosts.deny' or die "Could not open /etc/hosts.deny: $!";
                        print H "ALL: $src\n";
                        close H;
                        $blocked_str = 'via tcp wrappers';
                    }
                    for my $dst (keys %{$Scan{$src}}) {
                        $Scan{$src}{$dst}{'BLOCKED'} = 'Y';
                    }
                    if ($Config{'ALERT_AUTO_BLOCKED'} eq 'Y' && $blocked_str) {
                        for my $email_address (@$Emailaddrs_aref) {
                            system "$Cmds{'mail'} $email_address -s \"psad: All traffic from $src" .
                                   " has been BLOCKED $blocked_str on $HOSTNAME ($dst)\" < /dev/null > /dev/null 2>&1";
                        }
                    }
                    ### write the $src ip to the $AUTO_BLOCKED_FILE
                    &write_blocked_ip($src);
                    next SOURCE;
                }
            }
        }
    }
    return;
}
sub write_blocked_ip() {
    my $src = shift;
    if (-e $AUTO_BLOCKED_FILE) {
        open B, ">> $AUTO_BLOCKED_FILE" or warn " @@@ Could not append to $AUTO_BLOCKED_FILE\n" and return;
        print B "$src\n";
        close B;
    } else {
        open B, "> $AUTO_BLOCKED_FILE" or warn " @@@ Could not create $AUTO_BLOCKED_FILE\n" and return;
        print B "IPTABLES\n" if $USE_IPTABLES;
        print B "IPCHAINS\n" if $USE_IPCHAINS;
        print B "$src\n";
        close B;
    }
    return;
}
sub email_limit_reached() {
    my ($src, $dst) = @_;
    for my $email_address (@$Emailaddrs_aref) {
        system "$Cmds{'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 -nL`;
    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 ($src, $dst) = @_;
    for my $email_address (@$Emailaddrs_aref) {
        print "send_email_alert(): sending scan alert email to: $email_address\n" if $DEBUG;
        system "$Cmds{'mail'} $email_address -s \"psad WARNING: $HOSTNAME ($dst)" .
                       " has been scanned!\" < $Config{'EMAIL_ALERTFILE'} > /dev/null 2>&1";
    }
    $Scan{$src}{$dst}{'ALERTED'} = 'Y';
    return;
}
sub print_scan() {  ### this should primarily be used for debugging
    my $scanfile = $Config{'PRINT_SCAN_HASH'} . ".$$";
    open PSCAN, "> $scanfile";
    print PSCAN Dumper \%Scan;
    close PSCAN;
    chmod 0600, $scanfile;
    print STDOUT "\n ... Printing scan data structures to $scanfile\n\n";
    return;
}
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 @ips = `$Cmds{'ifconfig'} -a |$Cmds{'grep'} -w inet`;
    for my $ipline (@ips) {
        if ($ipline =~ /inet\s+addr:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s/) {
            $Local_ips{$1} = '';
        }
    }
    return;
}
sub get_listening_ports() {
    my %listening_ports;
    my @ports = `$Cmds{'netstat'} -an |$Cmds{'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 $ip = shift;
    my @whois_data;
    my $whois_datafile = $Config{'WHOIS_DATAFILE'} . "_${ip}";
    if (defined $whois_cache{$ip} && $whois_cache{$ip} < $Config{'WHOIS_THRESHOLD'} && -e $whois_datafile) {
        $whois_cache{$ip}++;
    } else {
        $whois_cache{$ip} = 0;
        eval {
            local $SIG{'ALRM'} = sub {die "whois alarm\n"};
            alarm $Config{'WHOIS_TIMEOUT'};
            system "$Cmds{'whois.psad'} $ip > $whois_datafile";
            alarm 0;
        };
        if ($@) {
            ### die unless $@ eq "whois alarm\n";
            ### warn "$@: $?";  ### let the warning handler save the error.
            warn $@;
            $#whois_data = 0;
            @whois_data = ("Whois data not available!\n");
            unlink $whois_datafile;
            return \@whois_data;
        }
    }
    open W, "< $whois_datafile" or die " ... @@@ Could not open $whois_datafile: $!";
    @whois_data = <W>;
    close W;
    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() {
    ### must kill psadwatchd first since if not, it might try to restart
    ### any of the other three daemons (its pid file is listed first in
    ### @PIDFILES)
    for my $pidfile (@PIDFILES) {
        my ($pidname) = ($pidfile =~ m|.*/(\w+)\.pid|);
        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 $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!";
    }
    &kill_psad();
    print " ... Restarting the psad daemons on $HOSTNAME\n";
    system "$Cmds{'psad'} $cmdline";
    return;
}
sub psad_status() {
    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) {
        my ($pidname) = ($pidfile =~ /(\w+)\.pid$/);
        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) {
                printf "%-16s %s", " ... $pidname ", "is running on $HOSTNAME as pid: $pid\n";
                my @grep_output = `$Cmds{'ps'} -auxww |$Cmds{'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 {
                printf "%-16s %s", " ... $pidname ", "is not currently running on $HOSTNAME\n";
            }
        } else {
            print " ... @@@  psad: pid file $pidfile does not exist for $pidname on $HOSTNAME\n";
        }
    }
    exit $rv;
}
sub psad_usr1() {
    my $rv = 0;
    my $psad_pidfile = $PIDFILES[1];
    if (-e $psad_pidfile) {
        open PIDFILE, "< $psad_pidfile" or die " ... @@@  Could not open $psad_pidfile: $!";
        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";
        }
    }
    exit $rv;
}
sub archive_fwdata() {
    my $fwdata    = $Config{'FW_DATA'};
    my $fwarchive = $Config{'FW_DATA_ARCHIVE'};
    ### first see how big the archive file is and zero out if
    ### it is larger than about 10,000 lines
    if (-e $fwarchive) {
        my $size = -s $fwarchive;
        if ($size > 2367766) {  ### about 10,000 lines
            open F, "> $fwarchive";
            close F;
        }
    }
    unless (-e $fwdata) {
        return;
    }
    open FW, "< $fwdata" or die "$fwdata exists but couldn't open it: $!";
    my @fwlines = <FW>;
    close FW;
    open AR, ">> $fwarchive" or die "Could not open $fwarchive: $!";
    print AR $_ for @fwlines;
    close AR;
    ### zero out $FW_DATA
    open F, "> $fwdata";
    close F;
    return;
}
sub psad_setup() {
    unless (-d $Config{'PSAD_DIR'}) {
        mkdir $Config{'PSAD_DIR'}, 400;
    }
    unless (-e $Config{'FW_DATA'}) {
        open F, "> $Config{'FW_DATA'}";
        close F;
    }
    unless (-e $Config{'PSAD_LOGFILE'}) {
        open L, "> $Config{'PSAD_LOGFILE'}";
        close L;
    }
    unless (-e $Config{'ERROR_LOG'}) {
        open E, "> $Config{'ERROR_LOG'}";
        close E;
    }
    unless (-e $Config{'PSAD_FIFO'}) {
        system "$Cmds{'mknod'} -m 600 $Config{'PSAD_FIFO'} p";
    }
    copy('/etc/syslog.conf', '/etc/syslog.conf.orig') unless (-e '/etc/syslog.conf.orig');
    open RS, '< /etc/syslog.conf' or die " ... @@@  Unable to open /etc/syslog.conf: $!";
    my @slines = <RS>;
    close RS;
    open SYSLOG, '> /etc/syslog.conf' or die " ... @@@  Unable to open /etc/syslog.conf: $!";
    for my $l (@slines) {
        chomp $l;
        unless ($l =~ /psadfifo/) {
            print SYSLOG "$l\n";
        }
    }
    print SYSLOG "kern.info		|$Config{'PSAD_FIFO'}\n\n";  ### reinstate kernel logging to our named pipe
    close SYSLOG;
    system "$Cmds{'killall'} -HUP $Cmds{'syslogd'}";
    return;
}
sub check_config() {
    my @required_vars = qw(
        EMAIL_ADDRESSES  PSAD_CHECK_INTERVAL  PSAD_LOGFILE
        FW_DATA ERROR_LOG  EMAIL_ALERTFILE   FW_MSG_SEARCH
        ENABLE_AUTO_IDS  ENABLE_PERSISTENCE   SCAN_TIMEOUT
        DANGER_LEVEL1     DANGER_LEVEL2      DANGER_LEVEL3
        DANGER_LEVEL4 DANGER_LEVEL5 PORT_RANGE_SCAN_THRESHOLD
        ALERT_ALL    PSAD_EMAIL_LIMIT   IPTABLES_BLOCK_METHOD
        IPCHAINS_BLOCK_METHOD TCPWRAPPERS_BLOCK_METHOD
        EMAIL_ALERT_DANGER_LEVEL PSAD_FIFO WHOIS_THRESHOLD
        WHOIS_TIMEOUT
    );
    &Psad::validate_config($CONFIG_FILE, \@required_vars, $Config_href);
    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;
}
