#!/usr/bin/perl -w

# SnortSnarf, a utility to convert snort log files to HTML pages
# Authors: Stuart Staniford, Silicon Defense (stuart@SiliconDefense.com)
#          James Hoagland, Silicon Defense (hoagland@SiliconDefense.com)
# copyright (c) 2000,2001 by Silicon Defense (http://www.silicondefense.com/)

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.  

# SnortSnarf description:
#
# Code to parse files of snort alerts and portscan logs, and produce
# HTML output intended to allow an intrusion detection analyst to
# engage in diagnostic inspection and tracking down problems.  
# The model is that one is using a cron job or similar to
# produce a daily/hourly/whatever file of snort alerts.  This script
# can be run on each such file to produce a convenient HTML breakout
# of all the alerts.
#
# The idea is that the analyst can click around the alerts instead
# of wearily grepping and awking through them.

# Please send complaints, kudos, and especially improvements and bugfixes to
# hoagland@SiliconDefense.com. This is a quick hack with features added and
# may only be worth what you paid for it.  It is still under active
# development and may change at any time.

# this file (snortsnarf.pl) is part of SnortSnarf v020316.1

##############################################################################

# Usage:

# SnortSnarf.pl <options> <file1 file2 ...>

# The script will produce a directory snfout.file1 (by default) full of
# a large number of html files.  These are placed under the current
# directory.  It also produces a file index.html in that directory.  
# This is a good place to start with a browser.

# The usage file describes the command line options

##############################################################################

# Credits, etc

# Initial alert parsing code borrowed from Joe McAlerney, Silicon 
# Defense.
# A couple of ideas were stolen from Snort2html by Dan Swan.
# Thanks to SANS GIAC and Donald McLachlan for a paper with ideas 
# on how to look up IP addresses.

# Huge thanks to DARPA for supporting us for part of the time developing
# this code.  Major thanks to Paul Arabelo for being our test customer
# while we learned to do operational intrusion detection instead of being
# only a researcher.

# Shouts to Marty Roesch and Patrick Mullen and the rest of the snort
# community for developing snort and the snort portscan preprocessor.  

# Kudos to Steve Northcutt who has taught us all what an intrusion
# detection analyst should be.

# Version control info: $Id: snortsnarf.pl,v 1.16 2000/06/14 18:40:45 jim Exp $

use lib qw(./include);
use Cwd;

# avoid needing to refer to SnortSnarf packages as SnortSnarf::*, even if
# that is where they really are:
sub BEGIN { push(@INC,map("$_/SnortSnarf",grep(-d "$_/SnortSnarf",@INC))); }

use SnortFileInput;
use HTMLMemStorage;
use HTMLAnomMemStorage;
use HTMLOutput;
use Filter;
use Input;
use SnortRules;


$html = 'html';         # usually html or htm
$os = 'unix';  # Either 'windows' or 'unix'



$script = "<a href=\"http://www.silicondefense.com/software/snortsnarf/\">".
                        "SnortSnarf</a>";
$version = "v020316.1";
#$author_email = "hoagland\@SiliconDefense.com";
$author = "<a href=\"mailto:hoagland\@SiliconDefense.com\">Jim Hoagland</a>".
 " and <a href=\"mailto:stuart\@SiliconDefense.com\">Stuart Staniford</a>";

$foot= "<CENTER>$script brought to you courtesy of ".
 "<A HREF=\"http://www.silicondefense.com/\">Silicon Defense</A><BR>\n".
 "Authors: $author<BR>\nSee also the <a href=\"http://www.snort.org/\">".
 "Snort Page</a> by Marty Roesch\n";
$prog_line= "$script $version";

#if ($::sap_version ne $version) {die "SnortSnarf.pl is version \"$version\" but snort_alert_parse.pl is version \"$::sap_version\"; did you remember to install the $version include directory someplace perl would find it?\n";}

##############################################################################

# input params for SnortFileInput
%in_params= ('year' => 'rec'); # can also specify alert and packet id sources

# output params for HTMLOutput
%out_params= (
    'html' => $html,
    'dirsep' => "\/",
    'root' => "\/",
    'logfileext' => '',
    'logfileprototerm' => ':',
    'gmt' => 0,
    'mostsigfirst' => 0, # should the signature with the most alerts appear first (versus last)
    'foot' => $foot, # footer text
    'prog_line' => $prog_line # fixed line of text for the bottom of the header
);



# Main program

$rules_file= undef;
$rules_cacheall= 0;
&process_options();

# portability stuff - toggle for Unix/Windows.
if ($os eq 'windows') {
    $dirsep= $out_params{'dirsep'}= "\\";       
    $root= $out_params{'root'}= "e:\\"; # Do not make this your system drive;
                                        # don't want it to fill up
    $out_params{'logfileext'}= '.ids';
    $out_params{'logfileprototerm'}= '_';
    $def_source= $root."util".$dirsep."snort".$dirsep."log".$dirsep."alert.ids"; # default input file
} elsif ($os eq 'unix') {
    $dirsep= $out_params{'dirsep'}= "\/";
    $root= $out_params{'root'}= "\/";
    $out_params{'logfileext'}= '';
    $out_params{'logfileprototerm'}= ':';
    # default input file
    $def_source= $root."var".$dirsep."log".$dirsep."snort.alert"; 
}

&initialize();
@ins= (); # input module instances
%ins_sources= (); # input sources

# for testing, use last input source for a different input module
#@in2_sources= @in_sources > 1 ? (pop(@in_sources)) : ();

# create SnortFileInput module
@in_tasks= qw(snort spp_portscan spade);
$in= SnortFileInput->new(\%in_params,[@in_tasks],$in_filter,@in_sources);
$ins_sources{'SnortFileInput'}= [@in_sources];
push(@ins,$in);
$in_nofilter= SnortFileInput->new(\%in_params,[@in_tasks],$Filter::true,@in_sources);
push(@ins_nofilter,$in_nofilter);

# if (@in2_sources) {
#     # create SnortFileInput2 module
#     require SnortFileInput2;
#     $in2= SnortFileInput2->new(\%in_params,[@in_tasks],$in_filter,@in2_sources);
#     $ins_sources{'SnortFileInput2'}= [@in2_sources];
#     push(@ins,$in2);
# }

# string to recreate the input modules
$in_recreate= Input::stringify_input_mods(@ins);
$innofilter_recreate= Input::stringify_input_mods(@ins_nofilter);

# create HTMLMemStorage and HTMLAnomMemStorage storage modules
%store_params= ();
$gstore= HTMLMemStorage->new(%store_params);
$astore= HTMLAnomMemStorage->new(%store_params);
%stores= (  'snort' => $gstore, # where different types are to be stored
            'spp_portscan' => $gstore,
            'spade' => $astore);
$out= HTMLOutput->new(%out_params);

# go through each input module grabbing all alerts and adding them to the
# approriate storage module
foreach $in (@ins) {
    while ($alert= $in->get()) {
        $stores{$alert->type()}->store($alert);
    }
}

%output_per_params= ( # output paramaters for a call to "output"
    'insources_str' => $in_recreate,
    'insources_str_noinfilter' => $innofilter_recreate,
    'insources' => \%ins_sources
);
$out->output(\%output_per_params,%stores);


##############################################################################

# process the command line options and leave @ARGV with just the input files
# at the end
sub process_options
{
    my $arg;
    my(@in_filters)= ();
    my(@inputs)= ();
    my $want_min_prio_filter=0;
    my $min_prio_filter_num;

    # go through arguments
    while(@ARGV) {
        $arg = shift @ARGV;
        if ($arg eq '-dns') {
            if (!@ARGV || $ARGV[0] !~ /^\d/ ) { $out_params{'dnslookupnet'}= '0.0.0.0'; }
            else { $out_params{'dnslookupnet'} = shift @ARGV; }
        } elsif ($arg eq '-ldir') {
            $out_params{'log_base'} = shift @ARGV;
            $out_params{'log_base'}.='/'
                unless $out_params{'log_base'} =~ /\/$/;
        } elsif ($arg eq '-homenet') {
            $out_params{'homenet'}= shift @ARGV;
        } elsif ($arg =~ s/^-color//) {    
            if ($arg =~ /=(.*)/) {
                $out_params{'color_opt'}= ($1 eq 'yes')?'rotate':$1;
            } else {
                $out_params{'color_opt'}= 'rotate';
            }
        } elsif ($arg =~ s/^-split=//) {    
            $out_params{'split_thresh'}= $arg;
        } elsif ($arg =~ s/^-top=//) {    
            $out_params{'topquant'}= $arg;
        } elsif ($arg eq '-d') {
            $out_params{'output_dir'}= shift @ARGV;
        } elsif ($arg eq '-cgidir') {
            $out_params{'cgi_dir'}= shift @ARGV;
            $out_params{'cgi_dir'}=~ s/\/$//;
        } elsif ($arg eq '-db') {
            $out_params{'db_file'}= shift @ARGV;
        } elsif ($arg eq '-nmapdir') {
            $out_params{'nmap_dir'}= shift @ARGV;
        } elsif ($arg eq '-nmapurl') {
            $out_params{'nmap_url'}= shift @ARGV;
            $out_params{'nmap_url'}.='/'
                unless $out_params{'nmap_url'} =~ /\/$/;
        } elsif ($arg eq '-sisr') {
            $out_params{'sisr_config'}= shift @ARGV;
        } elsif ($arg eq '-rulesfile') {
            $rules_file= shift @ARGV;
        } elsif ($arg eq '-rulesdir') {
            $rules_dir= shift @ARGV;
        } elsif ($arg eq '-rulesscanonce') {
            $rules_cacheall= 1;
        } elsif ($arg eq '-onewindow') {
            $out_params{'onewindow'} = 1;
        } elsif ($arg =~  /^-win/) {
            $os = 'windows';
        } elsif ($arg eq '-rs') {
            $out_params{'mostsigfirst'}= 1;
        } elsif ($arg eq '-hiprioisworse') {
            $out_params{'hiprioisworse'}= 1;
        } elsif ($arg eq '-sortsigcount1st') {
            $out_params{'sortsigcountfirst'}= 1;
        } elsif ($arg =~ s/^-minprio(|ity)=//) {
            $want_min_prio_filter=1;
            $min_prio_filter_num=$arg;
        } elsif ($arg =~ s/^-sipin=//) {
            push(@in_filters,HasSourceIPInFilter->new($arg));
        } elsif ($arg =~ s/^-dipin=//) {
            push(@in_filters,HasDestIPInFilter->new($arg));
        } elsif ($arg =~ s/^-refresh=//) {
            unless ($arg =~ /^\d+\s*$/) {
                warn "\"$arg\" does not look like a number of seconds for use with -refresh; skipping\n";
            } else {
                $out_params{'refreshsecs'}= $arg;
            }
        } elsif ($arg =~ s/^-year=//) {
            $arg= 'rec' if $arg =~ /^rec\w+/;  
            $arg= 'cur' if $arg =~ /^cur\w+/;
            unless ($arg =~ /(rec|cur|\d+)/) {
                warn "year option \"$arg\" not recognized, skipping\n";
            } else {
                $in_params{'year'}= $arg;
            }
        } elsif ($arg =~ /^-gmt$/) {
            $out_params{'gmt'} = 1;
        } elsif ($arg =~ /^-ymd$/) {
            $out_params{'ymd'} = 1;
        } elsif ($arg =~ /^-/) {
            warn "Unknown option $arg\n";
        } else {
            push(@inputs,$arg);
        }
    }
    @ARGV= @inputs;
    
    if ($want_min_prio_filter) {
        if ($out_params{'hiprioisworse'}) {
            unshift(@in_filters,&Filter::for_minprioritynum($min_prio_filter_num));
        } else {
            unshift(@in_filters,&Filter::for_maxprioritynum($min_prio_filter_num));
        }
    }
    if (@in_filters > 1) {
        $in_filter= AndFilter->new(@in_filters);
    } elsif (@in_filters) {
        $in_filter= shift(@in_filters);
    } else {
        $in_filter= $Filter::true;
    }
}

##############################################################################

sub initialize
{
    # Setup to use default file if no args
    @in_sources_text= @ARGV;
    unless (@in_sources_text) {
        warn "Warning: no input files specified, so we are using the default ($def_source); you can remove this warning by explicitly indicating input file(s) you want on the command line\n";
        @in_sources_text= ($def_source);
    }
    $cwd= getcwd();
    # fully qualify file names
    if ($os eq 'unix') {
        @in_sources= map((/^\// ? $_ : "$cwd/$_"),@in_sources_text);
    } else {
        $cwd =~ s:/:\\:g; # convert forward slashes in getcwd output to backslashes
        @in_sources= map((/^\w+\:/ ? $_ : "$cwd$dirsep$_"),@in_sources_text);
    }
    
    if (defined($rules_file)) {
	    my $rulesource= SnortRules->new($rules_file,$rules_dir,$dirsep,$rules_cacheall);
	    $in_params{'rulessource'}= $rulesource;
	    $out_params{'rulessource'}= $rulesource;
    }
}
