package SnortUnified::Database;

#########################################################################################
#  $VERSION = "SnortUnified to MySql 1.0 - Copyright (c) 2006 Jason Brvenik";
# 
# A Perl module to insert snort data from a unified file into a mysql database.
# http://www.snort.org
#
# 
#########################################################################################
# 
#
# The intellectual property rights in this program are owned by 
# Mr. Jason Brvenik.  This program may be copied, distributed and/or 
# modified only in accordance with the terms and conditions of 
# Version 2 of the GNU General Public License (dated June 1991).  By 
# accessing, using, modifying and/or copying this program, you are 
# agreeing to be bound by the terms and conditions of Version 2 of 
# the GNU General Public License (dated June 1991).
#
# 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., 51 Franklin St, Fifth Floor, 
# Boston, MA  02110-1301  USA 
# 
#
#########################################################################################
# Changes:
#########################################################################################
# TODO: in no specific order
#  - Documentation
# 
#########################################################################################
# NOTES:
#########################################################################################
# NOTE to self:
# 
# I've chosen to use internal globals so that routines can be selectively overridden
# in the future if needed without a lot of parameter passing.
# 
# EG: You could use this method to choose a routine to map into
# instead of the default.
# *{getSnortSensorID} = sub { SomeDatabaseGetSnortSensorID();};
#
# This will be useful _if_ there is a quirk somewhere that DBI does not handle
# and there is no easy way to fix it in teh existing routine.
#########################################################################################

use strict;
require Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);

use DBI;
use POSIX qw(strftime); 
use NetPacket::Ethernet qw(:ALL);
use NetPacket::IP qw(:ALL);
use NetPacket::TCP qw(:ALL);
use NetPacket::UDP qw(:ALL);
use NetPacket::ICMP qw(:ALL);

my $class_self;

BEGIN {
   $class_self = __PACKAGE__;
   $VERSION = "1.3devel";
}
my $DBLICENSE = "GNU GPL see http://www.gnu.org/licenses/gpl.txt for more information.";
sub DBVersion() { "$class_self v$VERSION - Copyright (c) 2006 Jason Brvenik" };
sub DBLicense() { DBVersion . "\nLicensed under the $DBLICENSE" };

# Pollute global namespace
@ISA = qw(Exporter);
@EXPORT = qw(
                DBLicense
                DBVersion
                getSnortDBHandle
                closeSnortDBHandle
                setSnortConnParam
                printSnortConnParams
                getSnortSensorID
                insertSnortAlert
                insertSnortLog
);

@EXPORT_OK = qw(
                $DB_INFO
                $SIG_ID_MAP
                printSnortSigIdMap
                $DBH
             );

%EXPORT_TAGS = (
               ALL => [@EXPORT, @EXPORT_OK],
);


our $DBH = undef;
our $SIG_ID_MAP = undef;
our $CLASS_ID_MAP = undef;
our $DB_INFO = { 
                'type'           => 'mysql',
                'host'           => 'localhost',
                'port'           => 3306,
                'database'       => 'snort',
                'user'           => 'snortuser',
                'password'       => 'snortpass',
                'connstr'        => 'DBI:mysql:database=snort;host=localhost;port=3306',
                'sensor_id'      => 0,
                'linktype'       => 0,
                'interface'      => '',
                'hostname'       => 'localhost',
                'filter'         => '',
                'payload'        => 1,
                'event_id'       => 0,
              };

my $REQUIRED_SCHEMA = 106;
my $SIG_MAP_H = undef;
my $CLASS_MAP_H = undef;
my $REF_MAP_H = undef;
my $EVENT_INS_H = undef;
my $IPH_INS_H = undef;
my $TCP_INS_H = undef;
my $UDP_INS_H = undef;
my $ICMP_INS_H = undef;
my $IPHDR_INS_H = undef;
my $TCPHDR_INS_H = undef;
my $UDPHDR_INS_H = undef;
my $ICMPHDR_INS_FULL_H = undef;
my $ICMPHDR_INS_H = undef;
my $REFERENCE_INS_H = undef;

sub setSnortConnParam($$) {
    my $parm = $_[0];
    my $val = $_[1];

    $DB_INFO->{$parm} = $val;
    if ( $DB_INFO->{'type'} eq 'mysql' ) {
        $DB_INFO->{'connstr'} = "DBI:" . $DB_INFO->{'type'} . 
            ":database=" . $DB_INFO->{'database'} . 
            ";host=" . $DB_INFO->{'host'} .
            ";port=" . $DB_INFO->{'port'} .
            ";";
    } else {
        print("Database " . $DB_INFO->{'database'} . " not supported\n");
    }

}

sub printSnortConnParams() {
    my $sid;
    my $rev;
    foreach my $key ( keys %{$DB_INFO} ) {
        print("$key     \t: " . $DB_INFO->{$key} . "\n");
    }
}

sub printSnortSigIdMap() {
    print("Dumping sid->sig map\n");
    foreach my $key (sort keys %{$SIG_ID_MAP}) {
        print("$key\t: " . $SIG_ID_MAP->{$key} . "\n");
    }
}

sub printSnortClassIdMap() {
    print("Dumping class->id map\n");
    foreach my $key ( sort keys %{$CLASS_ID_MAP} ) {
        print("$CLASS_ID_MAP->{$key}:$key\n");
    }
}

sub getSnortDBHandle() {

    my $schema = 0;

    $DBH = DBI->connect($DB_INFO->{'connstr'}, $DB_INFO->{'user'}, $DB_INFO->{'password'}); 
    
    ($schema) = $DBH->selectrow_array("SELECT max(vseq) from schema");
    
    if ( $schema lt $REQUIRED_SCHEMA ) { 
        print("Schema Version " . $schema . " too old\n"); 
        return 0;
    } else {
        return 1;
    }
}

sub closeSnortDBHandle() {

    $DBH->disconnect();
}

sub getSnortSensorID() {

    my $sid = 0;
    my $qh;

    $qh = $DBH->prepare("SELECT sid FROM sensor WHERE " . 
                        "hostname=? AND " . 
                        "interface=? AND " .
                        "filter=? AND " . 
                        "detail=? AND " .
                        "encoding='0'");

    $qh->execute($DB_INFO->{'hostname'}, 
                 $DB_INFO->{'interface'}, 
                 $DB_INFO->{'filter'}, 
                 $DB_INFO->{'payload'}) || print("error " . $qh->errstr . "\n");;

    ($sid) = $qh->fetchrow_array();
    
    if ( !defined $sid ) {
        $qh = $DBH->prepare("INSERT INTO sensor(hostname,interface,filter," .
                            "detail,encoding,last_cid) VALUES (?,?,?,?,'0','0')");

        $qh->execute($DB_INFO->{'hostname'}, 
                     $DB_INFO->{'interface'}, 
                     $DB_INFO->{'filter'}, 
                     $DB_INFO->{'payload'}) || print("error " . $qh->errstr . "\n");

        $qh = $DBH->prepare("SELECT sid FROM sensor WHERE " . 
                            "hostname=? AND " . 
                            "interface=? AND " . 
                            "filter=? AND " . 
                            "detail=? AND " .
                            "encoding='0'");

        $qh->execute($DB_INFO->{'hostname'}, 
                     $DB_INFO->{'interface'}, 
                     $DB_INFO->{'filter'}, 
                     $DB_INFO->{'payload'}) || print("error " . $qh->errstr . "\n");

        ($sid) = $qh->fetchrow_array();
    }

    $DB_INFO->{'sensor_id'} = $sid;

    # get the next event ID for this sensor
    ($DB_INFO->{'event_id'}) = $DBH->selectrow_array("SELECT max(cid) FROM " . 
                                                     "event WHERE sid=" . $sid);
    $DB_INFO->{'event_id'}++;

    # build the sig_id map and cache it for use
    # XXX - Note: This differs from barnyard somewhat...
    # barnyard uses the rule text in combination with the sid:rev
    # I'm using gen:sid:rev ignoring the message text as that will be pulled 
    # from the sidmap at the time of insert

    $qh = $DBH->prepare("SELECT sig_gid,sig_sid,sig_rev,sig_id,sig_name FROM signature");
    $qh->execute;

    my $sidref;
    while ( $sidref = $qh->fetchrow_hashref() ) {
        my $gensid = $sidref->{'sig_gid'}.":".$sidref->{'sig_sid'};
        $SIG_ID_MAP->{$gensid}->{'id'} = $sidref->{'sig_id'};
        $SIG_ID_MAP->{$gensid}->{'msg'} = $sidref->{'sig_name'};
        $SIG_ID_MAP->{$gensid}->{'gid'} = $sidref->{'sig_gid'};
        $SIG_ID_MAP->{$gensid}->{'sid'} = $sidref->{'sig_sid'};
    }

    # Build the classification map
    $qh = $DBH->prepare("SELECT sig_class_id, sig_class_name FROM sig_class");
    $qh->execute;
    my $classref;
    while ( $classref = $qh->fetchrow_hashref() ) {
        $CLASS_ID_MAP->{$classref->{'sig_class_name'}} = $classref->{'sig_class_id'};
    }

    return $sid;
}

sub getSigID($$) {
    my $record = $_[0];
    my $sids = $_[1];
    my $sidref;
    my $gensid = "$record->{'sig_gen'}:$record->{'sig_id'}";
    my $msg = $sids->{$record->{'sig_gen'}}->{$record->{'sig_id'}}->{'msg'};

    # sometimes we get events for things that were not updated
    # in sid-msg.map 
    # Most commonly this is for local rules
    # Using UNKNOWN for the sig message in this case
    $msg = defined $msg?$msg:"UNKNOWN";

    if ( defined $SIG_ID_MAP->{$gensid}->{'id'} ) {
        return $SIG_ID_MAP->{$gensid}->{'id'};
    }

    # in case someone slipped it in on us
    my $qh = $DBH->prepare("SELECT sig_gid,sig_sid,sig_rev,sig_id,sig_name FROM signature " .
                           "WHERE sig_gid=? AND sig_sid=?");
    $qh->execute($record->{'sig_gen'}, $record->{'sig_id'});
    $sidref = $qh->fetchrow_hashref();

    if ( !defined $sidref ) {
        if ( !defined $SIG_MAP_H ) {
            $SIG_MAP_H = $DBH->prepare("INSERT INTO " . 
               "signature(sig_name, sig_class_id, sig_priority, sig_rev, sig_sid, sig_gid) " . 
               "VALUES(?, ?, ?, ?, ?, ?)");
        }

        $SIG_MAP_H->execute($msg,
                   $record->{'class'},
                   $record->{'pri'},
                   $record->{'sig_rev'},
                   $record->{'sig_id'},
                   $record->{'sig_gen'});

        $qh->execute($record->{'sig_gen'}, $record->{'sig_id'});
        $sidref = $qh->fetchrow_hashref();

        $SIG_ID_MAP->{$gensid}->{'id'} = $sidref->{'sig_id'};
        $SIG_ID_MAP->{$gensid}->{'msg'} = $sidref->{'sig_name'};
        $SIG_ID_MAP->{$gensid}->{'gid'} = $sidref->{'sig_gid'};
        $SIG_ID_MAP->{$gensid}->{'sid'} = $sidref->{'sig_sid'};
        
        # XXX - Need to add reference handling
    } else {
        $SIG_ID_MAP->{$gensid}->{'id'} = $sidref->{'sig_id'};
        $SIG_ID_MAP->{$gensid}->{'msg'} = $sidref->{'sig_name'};
        $SIG_ID_MAP->{$gensid}->{'gid'} = $sidref->{'sig_gid'};
        $SIG_ID_MAP->{$gensid}->{'sid'} = $sidref->{'sig_sid'};
    }

    return $SIG_ID_MAP->{$gensid}->{'id'};
}

sub getClassID($$) {
    my $record = $_[0];
    my $class = $_[1];
    my $classid;
    my $msg = $class->{$record->{'class'}}->{'name'};

    $msg = defined $msg?$msg:"UNKNOWN";

    if ( defined $CLASS_ID_MAP->{$msg} ) {
        return $CLASS_ID_MAP->{$msg};
    }

    # In case someone else slipped it in on us
    my $qh = $DBH->prepare("SELECT sig_class_id FROM sig_class where sig_class_name=?");
    $qh->execute($msg);
   
    ($classid) = $qh->fetchrow_array();
    if ( !defined $classid ) {
        if ( !defined $CLASS_MAP_H ) {
            $CLASS_MAP_H = $DBH->prepare("INSERT INTO sig_class(sig_class_name) VALUES(?)");
        }
        $CLASS_MAP_H->execute($msg);
        $qh->execute($msg);
        ($classid) = $qh->fetchrow_array();
        $CLASS_ID_MAP->{$msg} = $classid;
    }
    
    return $CLASS_ID_MAP->{$msg};
}

sub getReferenceID() {
    my $record = $_[0];

}

sub check_handles() {
    if ( !defined $EVENT_INS_H ) {
        $EVENT_INS_H = $DBH->prepare("INSERT INTO " .
                                     "event(sid, cid, signature, timestamp) " .
                                     "VALUES(?, ?, ?, ?)");
    }

    if ( !defined $IPH_INS_H ) {
        $IPH_INS_H = $DBH->prepare("INSERT INTO " .
                                   "iphdr(sid, cid, ip_src, ip_dst, ip_proto) " .
                                   "VALUES(?, ?, ?, ?, ?)");
    }

    if ( !defined $TCP_INS_H ) {
        $TCP_INS_H = $DBH->prepare("INSERT INTO " .
                                   "tcphdr (sid, cid, tcp_sport, tcp_dport, tcp_flags) " .
                                   "VALUES(?, ?, ?, ?, 0)");
    }

    if ( !defined $UDP_INS_H ) {
        $UDP_INS_H = $DBH->prepare("INSERT INTO " .
                                   "udphdr (sid, cid, udp_sport, udp_dport) " .
                                   "VALUES(?, ?, ?, ?)");
    }

    if ( !defined $ICMP_INS_H ) {
        $ICMP_INS_H = $DBH->prepare("INSERT INTO " .
                                    "icmphdr (sid, cid, icmp_type, icmp_code) " .
                                    "VALUES(?, ?, ?, ?)");
    }

    if ( !defined $IPHDR_INS_H ) {
        $IPHDR_INS_H = $DBH->prepare("INSERT INTO iphdr(sid, cid, ip_src, ip_dst, ip_proto, " .
                                 "ip_ver, ip_hlen, ip_tos, ip_len, ip_id, ip_flags, ip_off, ".
                                 "ip_ttl, ip_csum) " .
                                 "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
    }
    
    if ( !defined $TCPHDR_INS_H ) {
        $TCPHDR_INS_H = $DBH->prepare("INSERT INTO tcphdr(sid, cid, tcp_sport, tcp_dport, " . 
                                 "tcp_seq, tcp_ack, tcp_off, tcp_res, tcp_flags, tcp_win, " . 
                                 "tcp_csum, tcp_urp) " . 
                                 "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
    }
 
    if ( !defined $UDPHDR_INS_H ) {
        $UDPHDR_INS_H = $DBH->prepare("INSERT INTO " . 
                                 "udphdr(sid, cid, udp_sport, udp_dport, udp_len, udp_csum) " .
                                 "VALUES(?, ?, ?, ?, ?, ?)");

    }
    
    if ( !defined $ICMPHDR_INS_FULL_H ) {
        $ICMPHDR_INS_FULL_H = $DBH->prepare("INSERT INTO " . 
                                 "icmphdr(sid, cid, icmp_type, icmp_code, icmp_csum, icmp_id, " . 
                                 "icmp_seq) " . 
                                 "VALUES(?, ?, ?, ?, ?, ?, ?)");
    }

    if ( !defined $ICMPHDR_INS_H ) {
        $ICMPHDR_INS_H = $DBH->prepare("INSERT INTO " . 
                                 "icmphdr(sid, cid, icmp_type, icmp_code, icmp_csum) " . 
                                 "VALUES(?, ?, ?, ?, ?)");
    }
};

sub insertSnortAlert($$$) {
    my $record = $_[0]; # hash with the actual data
    my $sids = $_[1];   # Hash of sids
    my $class = $_[2];  # Hash of classifications
    my $sigid;
    my $classid;
    my $timestamp = strftime("%Y-%m-%d %H:%M:%S", gmtime($record->{'tv_sec'}));
    my $gensid = "$record->{'sig_gen'}:$record->{'sig_id'}";
    
    check_handles();

    $sigid = getSigID($record, $sids);
    $classid = getClassID($record, $class);
    

    $EVENT_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'}, 
                 $sigid, $timestamp);

    $IPH_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                 $record->{'sip'}, $record->{'dip'}, $record->{'protocol'});

    if ( $record->{'protocol'} eq IP_PROTO_TCP ) {
        $TCP_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                 $record->{'sp'}, $record->{'dp'});

    } elsif ( $record->{'protocol'} eq IP_PROTO_UDP ) {
        $UDP_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                 $record->{'sp'}, $record->{'dp'});

    } elsif ( $record->{'protocol'} eq IP_PROTO_ICMP ) {
        $ICMP_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                 $record->{'sp'}, $record->{'dp'});
    }
    
    # Increment the event ID
    $DB_INFO->{'event_id'}++;
}

sub insertSnortLog($$$) {
    my $record = $_[0]; # hash with the actual data
    my $sids = $_[1];   # Hash of sids
    my $class = $_[2];  # Hash of classifications
    my $sigid;
    my $classid;
    my $timestamp = strftime("%Y-%m-%d %H:%M:%S", gmtime($record->{'tv_sec'}));
    my $gensid = "$record->{'sig_gen'}:$record->{'sig_id'}"; 
    my $eth_obj;
    my $ip_obj;
    my $tcp_obj;
    my $udp_obj;
    my $icmp_obj;

    check_handles();

    $sigid = getSigID($record, $sids) || 0;
    $classid = getClassID($record, $class) || 0;

    $EVENT_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                 $sigid, $timestamp);

    $eth_obj = NetPacket::Ethernet->decode($record->{'pkt'});
    if ( $eth_obj->{type} eq ETH_TYPE_IP ) {
        $ip_obj = NetPacket::IP->decode($eth_obj->{data});
        $IPHDR_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                              $ip_obj->{src_ip}, $ip_obj->{dest_ip},
                              $ip_obj->{proto}, $ip_obj->{ver},
                              $ip_obj->{hlen}, $ip_obj->{tos},
                              $ip_obj->{len}, $ip_obj->{id},
                              $ip_obj->{flags}, $ip_obj->{foffset},
                              $ip_obj->{ttl}, $ip_obj->{cksum});

        if ( $ip_obj->{proto} eq IP_PROTO_TCP ) {
            $tcp_obj = NetPacket::TCP->decode($ip_obj->{data});
            $TCPHDR_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                                   $tcp_obj->{src_port}, $tcp_obj->{dest_port},
                                   $tcp_obj->{seqnum}, $tcp_obj->{acknum},
                                   (($tcp_obj->{reserved} & 0xf0) >> 4),
                                   ($tcp_obj->{reserved} & 0x0f),
                                   $tcp_obj->{flags}, $tcp_obj->{winsize},
                                   $tcp_obj->{cksum}, $tcp_obj->{urg});

        } elsif ( $ip_obj->{proto} eq IP_PROTO_UDP ) {
            $udp_obj = NetPacket::UDP->decode($ip_obj->{data});
            $UDPHDR_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                                   $udp_obj->{src_port}, $udp_obj->{dest_port},
                                   $udp_obj->{len}, $udp_obj->{cksum});
              
        } elsif ( $ip_obj->{proto} eq IP_PROTO_ICMP ) {
            $icmp_obj = NetPacket::ICMP->decode($ip_obj->{data});
            if ( $icmp_obj->{type} eq 0 || $icmp_obj->{type} eq 8 ||
                 $icmp_obj->{type} eq 13 || $icmp_obj->{type} eq 14 ||
                 $icmp_obj->{type} eq 15 || $icmp_obj->{type} eq 16 ) {
                 $ICMPHDR_INS_FULL_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                                   $icmp_obj->{type}, $icmp_obj->{code},
                                   $icmp_obj->{cksum}, 
                                   unpack('n', substr($icmp_obj->{data}, 0, 2)),
                                   unpack('n', substr($icmp_obj->{data}, 2, 2)));
            } else {
                 $ICMPHDR_INS_H->execute($DB_INFO->{'sensor_id'}, $DB_INFO->{'event_id'},
                                   $icmp_obj->{type}, $icmp_obj->{code},
                                   $icmp_obj->{cksum});
            }
        } else {
            # print("DEBUGME: Why am I here - insertSnortLog\n");
        }
    }

    $DB_INFO->{'event_id'}++;
}
1
