zeek/scripts/s2b/bin/s2b.pl

2991 lines
64 KiB
Perl
Executable file

#!/usr/bin/perl -Tw
# s2b.pl
# Read and parse a Bro signature from a string or array reference
package Bro::Signature;
{
use strict;
require 5.006_001;
require Exporter;
use vars qw( $VERSION
@ISA
@EXPORT_OK
$DEBUG );
$VERSION = '1.10';
@EXPORT_OK = qw( findkeyblocks );
@ISA = qw( Exporter );
$DEBUG = 0;
sub new
{
my $sub_name = 'new';
my $self;
my $proto = shift;
my $class = ref( $proto ) || $proto;
my %args = @_;
my $sig_data = {};
if( defined( $args{string} ) )
{
if( !( $sig_data = parsesig( $args{string} ) ) )
{
return( undef );
}
}
else
{
warn( "Signature must be passed in as a string for now. Direct" .
" object creation is not supported yet\n" );
return( undef );
}
$self = $sig_data;
bless( $self, $class );
return( $self );
}
sub parsesig
{
my $sub_name = 'parsesig';
my $sig_block = shift || return( undef );
my $sig_data;
my $ret_data = {};
# Check on the storage of the data
if( ref( $sig_block ) eq 'ARRAY' )
{
$sig_data = join( "\n", @{$sig_block} );
}
else
{
# Otherwise it gets treated as a string
$sig_data = $sig_block;
}
my $parse_err = 0;
if( $sig_data =~
m/^signature[[:space:]]+([[:alnum:]_-]{3,})[[:space:]]*\{[[:space:]]*?\n? # signature declaration
(.+?) # signature data
[[:space:]]*\}$/xs ) # end of block
{
my $sig_id = $1;
my $sig_options = $2;
$ret_data->{sig_id} = $sig_id;
my @raw_data;
my $i = 0;
foreach my $line( split( /\n/, $sig_options ) )
{
# Each line of the signature is stored in an
# array to maintain the order and any comments.
# The actual data is stored in a hash.
# Access of the data should be done through the methods
# to output the whole signature along with comments.
# remove any leading spaces
$line =~ s/^[[:space:]]*//;
# Put the line into the raw_data array
$raw_data[$i] = $line;
# Strip any comments from the line
$line =~ s/(?:[[:space:]]+|)#.+$//;
my( $key, $value ) = split( /[[:space:]]+/, $line, 2 );
# if there is still data in the line then process
if( defined( $value ) )
{
# Remove any leading spaces
$value =~ s/^[[:space:]]*//;
# place a reference to the raw_data array for this attribute
# set the value for this attribute instance
push( @{$ret_data->{$key}}, {
'__raw_data_pos__' => $i,
'__value__' => $value, },
);
}
++$i;
}
$ret_data->{'__raw_data__'} = \@raw_data;
}
else
{
if( $DEBUG > 0 )
{
warn( "Signature read error. Could not find a valid signature block\n" );
}
$parse_err = 1;
}
if( $parse_err )
{
return( undef );
}
else
{
return( $ret_data );
}
}
sub findkeyblocks
{
my $sub_name = 'findkeyblocks';
my $string = shift;
my $keyword = 'signature';
my $key_len = length( $keyword );
my @ret_blocks;
my $block_idx = 0; # Current idx of the keyword blocks that will be returned
my $open_brace_count = 0; # Number of open braces encountered
my $close_brace_count = 0; # number of close braces encountered
my $len = length( $string ); # length of string passed in
my $pos_idx = 0; # current position of substring
my $st_blk_pos = 0; # string position of a new block
my $ws = ''; # working string
my $found_key_start = 0; # flag that a new keyword block has started
my $comment_line = 0; # flag if comments are in effect
my $quoted; # flag if quoting is active. contents are the quote character
# make sure the string is of non-zero length. Add one more so our
# loop below works
if( $len > 0 )
{
++$len;
}
else
{
return( undef );
}
# Push a space onto the front of the string. This just helps in
# starting the pattern matching. It's easier than writting a
# bunch more lines of code.
$string = "\n" . $string;
++$len;
while( $pos_idx < $len )
{
# If the two numbers match then a complete block has been found.
if( $open_brace_count == $close_brace_count and
$open_brace_count > 0 )
{
$ret_blocks[$block_idx] = $ws;
++$block_idx;
$open_brace_count = 0;
$close_brace_count = 0;
$found_key_start = 0;
$st_blk_pos = $pos_idx;
$ws = '';
}
elsif( $close_brace_count > $open_brace_count )
{
# Looks like there was a syntax error perhaps the
# file contains a keyword but no valid block
# Reset the counters and backup to $st_blk_pos + 1
if( $DEBUG > 0 )
{
warn( "Error in parsing $keyword, found closed brace",
" for a block but no open brace at char position $pos_idx\n" );
}
$open_brace_count = 0;
$close_brace_count = 0;
$found_key_start = 0;
$pos_idx = $st_blk_pos + 1;
$st_blk_pos = '';
$ws = '';
next;
}
# Make sure that substr returns a value
if( defined( my $wc = substr( $string, $pos_idx, 1 ) ) )
{
if( $quoted
and $found_key_start )
{
if( $wc eq $quoted
and substr( $string, $pos_idx - 1, 1 ) ne '\\' )
{
$quoted = undef;
}
}
# check for comment '#' character which is active until the
# start of a newline
elsif( ! $comment_line and
$wc eq '#' and
substr( $string, $pos_idx - 1, 1 ) ne '\\' )
{
$comment_line = 1;
}
# Check if the comment flag is set
elsif( $comment_line )
{
# If the current character is a newline then reset the comment flag
if( $wc =~ m/\n/ )
{
$comment_line = 0;
}
}
# start this if not currently working in a block and not a comment
elsif( ! $found_key_start and ! $comment_line )
{
# check if the keyword is found
if( $pos_idx + $key_len + 1 < $len
and substr( $string, $pos_idx, $key_len ) eq $keyword )
{
# check to make sure that the keyword is followed by a space
if( substr( $string, $pos_idx + $key_len, 1 ) =~
m/[[:space:]]/ )
{
$found_key_start = 1;
$st_blk_pos = $pos_idx;
}
}
}
elsif( $pos_idx + $key_len + 1 < $len
and $found_key_start
and substr( $string, $pos_idx, $key_len ) eq $keyword )
{
if( substr( $string, $pos_idx + $key_len, 1 ) =~
m/[[:space:]]/ )
{
if( $DEBUG > 0 )
{
warn( "New $keyword keyword found inside of another $keyword block",
" at char position $pos_idx\n" );
#print "STRING => $string\n";
}
# Reset the search params
$open_brace_count = 0;
$close_brace_count = 0;
$found_key_start = 0;
++$pos_idx;
$st_blk_pos = '';
$ws = '';
next;
}
}
elsif( $wc eq '{' and
substr( $string, $pos_idx - 1, 1 ) ne '\\' )
{
++$open_brace_count;
}
elsif( $wc eq '}' and
substr( $string, $pos_idx - 1, 1 ) ne '\\' )
{
++$close_brace_count;
}
elsif( ! $quoted
and ! $comment_line
and $found_key_start )
{
if( $wc eq '"'
and substr( $string, $pos_idx - 1, 1 ) ne '\\' )
{
$quoted = $wc;
}
}
else
{
}
# Only append chars to the working string if in a $keyword block
if( $found_key_start )
{
$ws = $ws . $wc;
}
++$pos_idx;
}
else
{
print "Failed to pull data out using substr at position $pos_idx\n";
++$pos_idx;
}
}
if( wantarray )
{
return( @ret_blocks );
}
else
{
return( \@ret_blocks );
}
}
sub addcomment
{
my $sub_name = 'addcomment';
my $self = shift || return( undef );
my $comment = shift || return( undef );
# Make sure the comment starts with a '#'
if( $comment !~ m/^[[:space:]]*#/ )
{
$comment = '#' . $comment;
}
if( $self->{'__raw_data__'} )
{
my $next_idx = $#{$self->{'__raw_data__'}} + 1;
$self->{'__raw_data__'}->[$next_idx] = $comment;
}
else
{
return( undef );
}
return( 1 );
}
sub addoption
{
my $sub_name = 'addoption';
my $self = shift || return( undef );
my $option = shift || return( undef );
my $option_data = shift || return( undef );
if( $self->{'__raw_data__'} )
{
my $next_idx = $#{$self->{'__raw_data__'}} + 1;
$self->{'__raw_data__'}->[$next_idx] = $option . ' ' . $option_data;
push( @{$self->{$option}},
{ '__raw_data_pos__' => $next_idx,
'__value__' => $option_data, } );
}
else
{
return( undef );
}
return( 1 );
}
sub deloption
{
my $sub_name = 'deloption';
# Accepts at a minimum the object and an option to remove
# For options that have multivalues the value can be passed in as
# option_data to remove only the one value from the object.
# If an option has multivalues and no option_data is given then all
# options are removed.
my $self = shift || return( undef );
my $match_option = shift || return( undef );
my $match_data = shift; #optional
my $success = 0;
my @raw_data_pos;
if( defined( $match_data ) )
{
if( $DEBUG > 4 )
{
warn( "Method $sub_name has been asked to delete option '" .
$match_option . "', value '" . $match_data . "'\n" );
}
# Must match on both the option key and it's value
if( exists( $self->{$match_option} ) )
{
if( ref( $self->{$match_option} ) eq 'ARRAY' )
{
if( $DEBUG > 4 )
{
warn( "Found a match on $match_option in signature\n" );
}
my $__found = 0;
my @good_data;
foreach my $opt_value( @{$self->{$match_option}} )
{
if( $opt_value->{'__value__'} eq $match_data )
{
++$__found;
$success = 1;
push( @raw_data_pos, $opt_value->{'__raw_data_pos__'} );
}
elsif( defined( my $tt = $opt_value->{'__value__'} ) )
{
push( @good_data, $opt_value );
}
}
if( $success )
{
if( defined( $good_data[0] ) )
{
# Replace the object's old data with the good data
$self->{$match_option} = [ @good_data ];
}
else
{
delete $self->{$match_option};
}
}
}
}
}
elsif( exists( $self->{$match_option} ) )
{
if( $DEBUG > 4 )
{
warn( "Method $sub_name has been asked to delete all data with" .
" an option name of $match_option\n" );
}
foreach my $opt_data( @{$self->{$match_option}} )
{
push( @raw_data_pos, $opt_data->{'__raw_data_pos__'} );
}
delete $self->{$match_option};
$success = 1;
}
if( $success )
{
# cleanup the __raw_data__ storage in the object
# get the last index of the __raw_data__ array
foreach my $idx( @raw_data_pos )
{
$self->{'__raw_data__'}->[$idx] = undef;
}
# Delete the
}
return( $success );
}
sub output
{
my $sub_name = 'output';
my $self = shift || return( undef );
my %args = @_;
my $ret_string;
my $prefix = '';
my $comments = 0;
# Check on options
if( $args{sigprefix} )
{
$prefix = $args{sigprefix};
}
if( defined( $args{comments} ) )
{
$comments = $args{comments};
}
else
{
$comments = 1;
}
# opening to a Bro signature block
$ret_string = 'signature ' . $prefix . $self->{sig_id} . ' {' . "\n";
if( $comments )
{
foreach my $line( @{$self->{'__raw_data__'}} )
{
if( defined( $line ) )
{
$ret_string = $ret_string . ' ' . $line . "\n";
}
}
}
else
{
while( my( $option, $opt_array ) = each( %{$self} ) )
{
if( $option ne '__raw_data__' and $option ne 'sig_id' )
{
foreach my $opt_data( @{$opt_array} )
{
$ret_string = $ret_string .
' ' .
$option .
' ' .
$opt_data->{'__value__'} .
"\n";
}
}
}
}
# closing of the Bro signature block
$ret_string = $ret_string . '}';
return( $ret_string );
}
sub option
{
my $sub_name = 'option';
my $self = shift || return( undef );
my $sig_option = shift || return( undef );
my $ret_data;
if( $self->{$sig_option} )
{
foreach my $data( @{$self->{$sig_option}} )
{
push( @{$ret_data}, $data->{'__value__'} );
}
}
else
{
return( undef );
}
if( @{$ret_data} > 1 )
{
return( @{$ret_data} );
}
else
{
return( $ret_data->[0] );
}
}
sub sigid
{
# This is the entire Bro signature id as parsed in from a
# signature block
my $sub_name = 'sigid';
my $self = shift || return( undef );
if( $self->{sig_id} )
{
return( $self->{sig_id} );
}
else
{
return( undef );
}
}
sub openportlist
{
my $sub_name = 'openportlist';
my $self = shift || return( undef );
}
};
package Bro::S2b::Augment;
{
use strict;
require 5.006_001;
use Config::General;
#require Bro::Signature;
use vars qw( $VERSION
$DEBUG
%VALID_AUGMENT_OPTIONS
$QUOTE_PATT );
$VERSION = '1.10';
%VALID_AUGMENT_OPTIONS = (
'active' =>
{ fatal => '{1}',
msg => "One required with values of T or F",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 0, },
'comment' =>
{ fatal => '*',
msg => "Zero or more allowed, value is plain text",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 0, },
'dst-ip' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'dst-port' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'src-ip' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'src-port' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'ip-proto' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'eval' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'ftp' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'header' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'http' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'http-request' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'http-request-header' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'http-reply-header' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'ip-options' =>
{ fatal => '?',
msg => "One or zero allowed. Not implemented yet.",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'payload' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommened.",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'payload-size' =>
{ fatal => '?',
msg => 'One or zero allowed.',
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'requires-signature' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'requires-reverse-signature' =>
{ fatal => '*',
warn => '{0,10}',
msg => "Zero or many with a max of 10 recommended",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'same-ip' =>
{ fatal => '?',
msg => 'One or zero allowed.',
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
'snort-rule-file' =>
{ fatal => '?',
msg => "Zero or one allowed, filename from where the Snort rule came from is recommened",
no_modify => 1,
write_to_cfg => 1,
write_to_sig => 0, },
'sid-rev' =>
{ fatal => '{1}',
msg => "One is required. SID rev number provided in the Snort ruleset file",
no_modify => 1,
write_to_cfg => 0,
write_to_sig => 0, },
'sid' =>
{ fatal => '{1}',
msg => "One is required. Sid number is provided in the Snort ruleset file",
no_modify => 1,
write_to_cfg => 0,
write_to_sig => 0, },
'sigaction' =>
{ fatal => '{1}',
msg => "One required with a Bro SigAction as a value",
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 0, },
'tcp-state' =>
{ fatal => '?',
msg => 'One or zero allowed.',
no_modify => 0,
write_to_cfg => 1,
write_to_sig => 1, },
);
$QUOTE_PATT = qr~(?:[=!]{2}|\;|[|]{2}|\"|\>|\<)~;
sub new
{
my $sub_name = 'new';
my $self;
my $proto = shift;
my $class = ref( $proto ) || $proto;
my %args = @_;
my $augment_objs = [];
my $sig_data = {};
if( defined( $args{filename} ) )
{
if( !( $augment_objs = getaugmentconfig( $args{filename} ) ) )
{
return( undef );
}
else
{
return( $augment_objs );
}
}
else
{
if( validate( \%args ) )
{
$sig_data = \%args;
}
else
{
return( undef );
}
}
$self = $sig_data;
bless( $self, $class );
return( $self );
}
sub getaugmentconfig
{
my $sub_name = 'getaugmentconfig';
my $augment_file = shift || undef;
my $valid_opts = \%VALID_AUGMENT_OPTIONS;
my @ret_arr;
my %aug_conf;
my $conf;
if( -r $augment_file )
{
$conf = Config::General->new( -ConfigFile => $augment_file,
-LowerCaseNames => 1,
-MergeDuplicateBlocks => 0,
);
}
else
{
if( $DEBUG > 0 )
{
warn( "Error reading augment config at \"$augment_file\".\n" );
}
return( undef );
}
%aug_conf = $conf->getall();
if( ref( $aug_conf{augment} ) eq 'HASH' )
{
%aug_conf = %{$aug_conf{augment}}
}
while( my( $sid_id, $aug_data ) = each( %aug_conf ) )
{
my( $sid_num, $sid_rev ) = split( /-/, $sid_id, 2 );
my $invalid_sid = 0;
# The sid_id is represents both the sid number and the rev
# in the form sid-rev, example: 540-2 would have a sid number
# of 540 and an rev of 2
if( ref( $aug_data ) eq 'ARRAY' )
{
if( $DEBUG > 1 )
{
warn( "SID number $sid_num with rev number $sid_rev has duplicate" .
" entries. Keeping the first instance and removing all others\n" );
}
my $keep_data = $aug_conf{$sid_id}->[0];
$aug_conf{$sid_id} = undef;
$aug_conf{$sid_id} = $keep_data;
}
else
{
if( my $aug_obj = Bro::S2b::Augment->new( 'sid' => $sid_num,
'sid-rev' => $sid_rev,
%{$aug_data} ) )
{
push( @ret_arr, $aug_obj );
}
else
{
warn( "Failed to parse sid $sid_num, rev $sid_rev\n" );
$invalid_sid = 1;
}
}
if( $invalid_sid )
{
if( $DEBUG > 0 )
{
warn( "Snort SID number $sid_num is being ignored\n" );
}
}
}
if( $DEBUG > 4 )
{
warn( "\nMemory dump of augment config file $augment_file\n" );
warn( $conf->save_string( \%aug_conf ) . "\n" );
warn( "\n" );
}
if( wantarray )
{
return( @ret_arr );
}
else
{
return( \@ret_arr );
}
}
sub validate
{
my $sub_name = 'validate';
# The hash passed in is in the form
# <snort sid> => <hash ref of augment data>
my $aug_data = shift || return( undef );
my %ret_hash;
my $invalid_sid = 0;
my $sid_num = $aug_data->{sid};
my $del_data = {}; # hash ref of options that will later be removed from a sig
# make sure that the snort sid number is greater than 100
if( $sid_num =~ m/^([[:digit:]]+)$/ and $sid_num > 100 )
{
$sid_num = $1;
}
# Check for a delete block
if( exists( $aug_data->{delete} ) )
{
$del_data = $aug_data->{delete};
}
# Check to make sure that the sid contains only valid options.
# Each option will be totalled as a string of 1's where each 1
# represents one instance of a particular option.
# Regular expression contained in %VALID_AUGMENT_OPTIONS for that
# option will be evaluated against the quantity found.
while( my( $sid_option, $sid_opt_data ) = each( %{$aug_data} ) )
{
my $option_count;
if( defined( $VALID_AUGMENT_OPTIONS{$sid_option} ) )
{
# OK, all good
}
elsif( $sid_option eq 'delete' )
{
next;
}
else
{
if( $DEBUG > 0 )
{
warn( "Option '$sid_option' in augment config" .
" for SID number $sid_num is unknown." .
" Option will be ignored\n" );
}
delete $aug_data->{$sid_option};
next;
}
if( ref( $sid_opt_data ) eq 'HASH' )
{
foreach( keys( %{$sid_opt_data} ) )
{
$option_count .= '1';
}
}
elsif( ref( $sid_opt_data ) eq 'ARRAY' )
{
foreach( @{$sid_opt_data} )
{
$option_count .= '1';
}
}
else
{
# Otherwise treat the sid_option as a scalar
$option_count = 1;
}
my $fatal_exp = qr/1$VALID_AUGMENT_OPTIONS{$sid_option}->{fatal}/;
if( $option_count =~ m/^$fatal_exp$/ )
{
# ok
if( defined( $VALID_AUGMENT_OPTIONS{$sid_option}->{warn} ) )
{
my $warn_exp = qr/1$VALID_AUGMENT_OPTIONS{$sid_option}->{warn}/;
if( $option_count =~ m/^$warn_exp$/ )
{
#ok
}
else
{
if( $DEBUG > 0 )
{
warn( "Warning for option '$sid_option' in SID number $sid_num, " .
$VALID_AUGMENT_OPTIONS{$sid_option}->{msg} .
"\n" );
}
}
}
}
else
{
if( $DEBUG > 0 )
{
warn( "Invalid SID option '$sid_option' in SID number $sid_num, " .
$VALID_AUGMENT_OPTIONS{$sid_option}->{msg} .
"\n" );
}
$invalid_sid = 1;
}
}
while( my( $sid_option, $sid_opt_data ) = each( %{$del_data} ) )
{
if( defined( $VALID_AUGMENT_OPTIONS{$sid_option} ) )
{
# OK, all good
}
else
{
if( $DEBUG > 0 )
{
warn( "Option '$sid_option' in the delete section of" .
" augment config for SID number $sid_num is unknown." .
" Option will be ignored\n" );
}
delete $aug_data->{delete}->{$sid_option};
next;
}
}
if( $invalid_sid )
{
return( 0 );
}
else
{
return( 1 );
}
}
sub augmentbrosig
{
my $sub_name = 'augmentbrosig';
my $self = shift || return( undef );
my $bro_sig_obj = shift || return( undef );
my $new_bro_sig;
my $err = 0;
my $del_data = {};
# Go through each option in the augment data
while( my( $option, $opt_data ) = each( %{$self} )
and !( $err ) )
{
# Is this the delete section of the data
if( $option eq 'delete' and ref( $opt_data ) )
{
$del_data = $opt_data;
}
# check whether the option is allowed to be exported to
# a Bro sig
elsif( exists( $VALID_AUGMENT_OPTIONS{$option} )
and $VALID_AUGMENT_OPTIONS{$option}->{write_to_sig} )
{
my @data_list;
my $remove_before_add = 0;
# Check if $opt_data has mutlivalues
if( ref( $opt_data ) eq 'ARRAY' )
{
@data_list = @{$opt_data};
}
else
{
if( $VALID_AUGMENT_OPTIONS{$option}->{fatal} eq '?'
or $VALID_AUGMENT_OPTIONS{$option}->{fatal} eq '{1}' )
{
$remove_before_add = 1;
}
@data_list = ( $opt_data );
}
foreach my $opt_value( @data_list )
{
if( $remove_before_add )
{
$bro_sig_obj->deloption( $option );
if( $DEBUG > 2 )
{
warn( "Augment option $option for sigid " . $self->sigid()
. " can only have one instance in a Bro siganture.\n" );
warn( "Found a prexisting context for \"$option\" in the Bro signature"
. " which will be replaced by the augment data.\n" );
}
}
if( $bro_sig_obj->addoption( $option, $opt_value ) )
{
# ok
}
else
{
if( $DEBUG > 0 )
{
warn( "Failed to add augment option $option to Bro" .
" signature object with sid of " .
$bro_sig_obj->sigid . "\n" );
}
$err = 1;
last;
}
}
}
}
# Loop over the delete section of the data and remove matching
# data from a bro signature.
while( my( $opt, $val ) = each( %{$del_data} ) and ! $err )
{
if( exists( $VALID_AUGMENT_OPTIONS{$opt} )
and $VALID_AUGMENT_OPTIONS{$opt}->{write_to_sig} )
{
my @data_list;
# Check if $opt_data has mutlivalues
if( ref( $val ) eq 'ARRAY' )
{
@data_list = @{$val};
}
elsif( defined( $val ) )
{
$data_list[0] = $val;
}
else
{
next;
}
foreach my $opt_value( @data_list )
{
if( $bro_sig_obj->deloption( $opt, $opt_value ) )
{
# ok
}
else
{
if( $DEBUG > 0 )
{
warn( "Failed to remove augment option '$opt', value '$opt_value'" .
" from Bro signature object with sid of " .
$bro_sig_obj->sigid() . "\n" );
}
$err = 1;
last;
}
}
}
}
if( $err )
{
if( $DEBUG > 0 )
{
warn( "Unable to apply all options from augment object to the Bro signature\n" );
}
return( undef );
}
else
{
return( $bro_sig_obj );
}
}
sub sid
{
my $sub_name = 'sid';
my $self = shift || return( undef );
if( my $sid = $self->{sid} )
{
return( $sid );
}
else
{
return( undef );
}
}
sub rev
{
my $sub_name = 'rev';
my $self = shift || return( undef );
if( my $rev = $self->{'sid-rev'} )
{
return( $rev );
}
else
{
return( undef );
}
}
sub sigid
{
my $sub_name = 'sigid';
my $self = shift || return( undef );
if( my $rev = $self->{'sid-rev'}
and my $sid = $self->{sid} )
{
return( $sid . '-' . $rev );
}
else
{
return( undef );
}
}
sub option
{
my $sub_name = 'option';
my $self = shift || return( undef );
my $aug_option = shift || return( undef );
my $ret_data;
if( $self->{$aug_option} )
{
if( ( $self->{$aug_option} ) eq 'ARRAY' )
{
foreach my $data( @{$self->{$aug_option}} )
{
push( @{$ret_data}, $data );
}
}
else
{
push( @{$ret_data}, $self->{$aug_option} );
}
}
else
{
return( undef );
}
if( @{$ret_data} > 1 )
{
return( @{$ret_data} );
}
else
{
return( $ret_data->[0] );
}
}
sub active
{
my $sub_name = 'active';
my $self = shift || return( undef );
my $new_status = shift;
my $ret_status = 0;
# NOTE, for now the active status is kept as a character value.
# This just makes it easy for importing augment data into objects
# This needs to be changed internally but for now the user doesn't
# know the difference.
if( defined( $new_status ) )
{
if( $new_status =~ m/^(?:t|1)/i )
{
$self->{active} = 'T';
}
else
{
$self->{active} = 'F';
}
}
if( ! exists( $self->{active} ) )
{
$ret_status = undef;
}
elsif( $self->{active} =~ m/^t/i )
{
$ret_status = 1;
}
return( $ret_status );
}
sub output
{
my $sub_name = 'output';
my $self = shift || return( undef );
my $ret_string = '';
my $sid_num;
my $sid_rev;
if( $sid_num = $self->sid() and $sid_rev = $self->rev() )
{
# ok
}
else
{
return( undef );
}
my $already_quoted = qr/^\".+\"$/;
foreach my $key( sort( keys( %{$self} ) ) )
{
my $value = $self->{$key};
if( $VALID_AUGMENT_OPTIONS{$key}->{write_to_cfg} )
{
# If the option data has muti values
if( ref( $value ) )
{
foreach my $value_inst( @{$value} )
{
if( $value_inst =~ $QUOTE_PATT
and $value_inst !~ $already_quoted )
{
$value_inst = '"' . $value_inst . '"';
}
$ret_string = $ret_string . ' ' . $key . ' ' . $value_inst ."\n";
}
}
# Otherwise the option value is reated as a scalar
else
{
if( $value =~ $QUOTE_PATT
and $value !~ $already_quoted )
{
$value = '"' . $value . '"';
}
$ret_string = $ret_string . ' ' . $key . ' ' . $value . "\n";
}
}
}
if( length( $ret_string ) > 3 )
{
# prepend the beginning of the augment block
$ret_string = "<augment $sid_num-$sid_rev>\n" . $ret_string;
# append the closing of the augment block
$ret_string = $ret_string . '</augment>';
return( $ret_string );
}
else
{
return( undef );
}
}
sub merge
{
my $sub_name = 'merge';
# will attempt to merge an augment object together with either a
# valid data strcuture or another augment object. There is
# no checking on whether the objects are similar in any way
# only that the data from the source does not cause the target
# to become invalid according to %VALID_AUGMENT_OPTIONS.
# If an attempt to add a value exceeds the allowable quantity for
# a given option then the option from the source object will replace
# the target object's option. Otherwise the option will just be added
# If a delete block exists then the option that matches will be
# deleted from the target. A final check will be made after all
# data has been processed to make sure that the target still conforms
# to the requirements in %VALID_AUGMENT_OPTIONS.
my $self = shift || return( undef ); # Merge to, target
my $new_data = shift || return( undef ); # Merge from, source
# Make a copy of the object. The copy will be operated on and once
# all tests have completed it will replace the contents of the
# original object.
my $wo = Bro::S2b::Augment->new( %{$self} ); # Working Object
my $del_opts = {};
my $failed = 0;
while( my( $key, $value ) = each( %{$new_data} ) )
{
if( $key eq 'delete' )
{
$del_opts = $value;
}
else
{
if( ! $wo->add( $key, $value ) )
{
# Failed to add in the value
if( $DEBUG > 0 )
{
$failed = 1;
warn( "Failed to add option $key to augment object\n" );
}
}
}
}
while( my( $del_opt, $del_val ) = each( %{$del_opts} ) )
{
if( ! $wo->del( $del_opt, $del_val ) )
{
if( $DEBUG > 0 )
{
$failed = 1;
warn( "Failed to delete option $del_opt from object\n" );
}
}
}
if( ! validate( $wo ) )
{
if( $DEBUG > 0 )
{
warn( "There was an error in validating the merged augment object." .
" No changes made to the original augment object.\n" );
}
return( undef );
}
if( ! $failed )
{
return( $wo );
}
else
{
if( $DEBUG > 0 )
{
warn( "One or more operations failed during an augment merge." .
" Original object has not been modified\n" );
}
return( undef );
}
}
sub add
{
my $sub_name = 'add';
my $self = shift || return( undef );
my $opt = shift || return( undef );
my $val = shift;
my $cur_contents;
my $opt_quan = '';
my $eval_quan = $VALID_AUGMENT_OPTIONS{$opt}->{fatal};
if( ! $VALID_AUGMENT_OPTIONS{$opt} )
{
if( $DEBUG > 0 )
{
warn( "Attempt to merge an unknown option \"$opt\" into an augment object\n" );
}
return( undef );
}
if( $VALID_AUGMENT_OPTIONS{$opt}->{no_modify} )
{
if( $DEBUG > 0 )
{
warn( "Failed to add option $opt. Modification is forbidden by configuration\n" );
}
return( undef );
}
# figure out what type of value is stored
if( exists( $self->{$opt} ) and ref( $self->{$opt} ) eq 'ARRAY' )
{
my @t1 = @{$self->{$opt}};
$cur_contents = \@t1;
foreach( @{$cur_contents} )
{
$opt_quan .= '1';
}
}
else
{
$cur_contents = $self->{$opt};
$opt_quan = '1';
}
# figure out if we should replace or add
# If the allowed option count is a max of one and we have only one option
# to add then this is an overwrite
if( $eval_quan eq'?' or $eval_quan eq '{1}' )
{
if( $opt_quan =~ m/^1|$/ )
{
$self->{$opt} = $val;
if( $DEBUG > 2 )
{
warn( "Added/replaced option $opt with contents $val\n" );
}
}
else
{
if( $DEBUG > 0 )
{
warn( "Tried to add more values to an option than the option type allows.\n" );
}
return( undef );
}
}
else
{
$opt_quan .= '1';
if( $opt_quan =~ m/^1$eval_quan$/ )
{
# Check if the data structure is an array, if not change it
if( ! exists( $self->{$opt} ) )
{
$self->{$opt} = [];
}
elsif( ref( $self->{$opt} ) ne 'ARRAY' )
{
my $cur_val = $self->{$opt};
delete( $self->{$opt} );
$self->{$opt}->[0] = $cur_val;
}
push( @{$self->{$opt}}, $val );
}
else
{
if( $DEBUG > 0 )
{
warn( $VALID_AUGMENT_OPTIONS{$opt}->{msg} . "\n" );
}
return( undef );
}
}
# Search the delete section of the data and remove any matching entries
if( ref( $self->{delete} ) )
{
# Need to complete later!
}
return( $self );
}
sub del
{
my $sub_name = 'del';
my $self = shift || return( undef );
my $opt = shift || return( undef );
my $opt_val = shift || '';
my $opt_quan = '';
my $eval_quan = $VALID_AUGMENT_OPTIONS{$opt}->{fatal};
if( ! $VALID_AUGMENT_OPTIONS{$opt} )
{
if( $DEBUG > 0 )
{
warn( "Attempt to remove an unknown option \"$opt\" from an augment object\n" );
}
return( undef );
}
if( $VALID_AUGMENT_OPTIONS{$opt}->{no_modify} )
{
if( $DEBUG > 0 )
{
warn( "Failed to delete option $opt. Modification is forbidden by configuration\n" );
}
return( undef );
}
if( ! exists( $self->{$opt} ) )
{
# no option with the name in $opt exists
return( 0 );
}
elsif( ref( $self->{$opt} ) eq 'ARRAY' )
{
my @t1 = @{$self->{$opt}};
my $cur_contents = \@t1;
my $found = 0;
my @new_contents;
foreach my $cont_inst( @{$cur_contents} )
{
if( $cont_inst eq $opt_val )
{
++$found;
}
else
{
$opt_quan .= '1';
push( @new_contents, $cont_inst );
}
}
if( $found )
{
if( $opt_quan =~ /^1$eval_quan$/ )
{
delete( $self->{$opt} );
$self->{$opt} = \@new_contents;
}
else
{
if( $DEBUG > 0 )
{
warn( "Removal of option $opt failed." . $VALID_AUGMENT_OPTIONS{$opt}->{msg} . "\n" );
}
return( undef );
}
}
else
{
if( $DEBUG > 2 )
{
warn( "Could not find '$opt' with value '$opt_val' to delete. No big deal\n" );
}
}
}
else
{
$opt_quan = '';
if( $self->{$opt} eq $opt_val )
{
if( $opt_quan =~ /^1$eval_quan$/ )
{
delete( $self->{$opt} );
}
else
{
if( $DEBUG > 0 )
{
warn( "Removal of option $opt is not allowed," . $VALID_AUGMENT_OPTIONS{$opt}->{msg} . "\n" );
}
return( undef );
}
}
else
{
if( $DEBUG > 2 )
{
warn( "Could not find '$opt' with value '$opt_val' to delete. No big deal\n" );
}
}
}
return( $self );
}
};
#####################################################################
##### s2b.pl Snort to Bro rule conversion script
##### Roger Winslow
#####
#####################################################################
use strict;
require 5.006_001; # 5.6.1 minimum is required.
use Config::General;
use Getopt::Long;
Getopt::Long::Configure qw( no_ignore_case no_getopt_compat );
# clear these shell environment variables so Taint mode doesn't complain
$ENV{PATH} = '';
$ENV{BASH_ENV} = '';
$ENV{ENV} = '';
use vars qw( $VERSION
%DEFAULT_CONFIG
$SNORT_TO_BRO_PROG
$DEBUG );
$VERSION = '1.10';
$DEBUG = 1;
%DEFAULT_CONFIG = ( configdir => '/usr/local/etc/bro/s2b',
mainconfig => 's2b.cfg',
augmentconfig => 's2b-augment.cfg',
useraugmentconfig => 's2b-user-augment.cfg',
sigactiondest => 's2b-sigaction.bro',
brosignaturedest => 's2b.sig',
sigmapconfig => 's2b-sigmap.cfg',
defaultsigaction => 'SIG_LOG',
rulesetaugmentconfig => 's2b-ruleset-augment.cfg',
sigprefix => 's2b-',
snortrulesetdir => './',
ignorehostdirection => 1,
);
# This is hardcoded until I get to intergrating the Snort conversion into
# a PERL program.
$SNORT_TO_BRO_PROG = './snort2bro';
# Until I can rewrite the python conversion script it will be used for initial conversion
# make sure it runs before continuing on.
if( my $result = `$SNORT_TO_BRO_PROG --help 2>&1` )
{
# ok.
}
else
{
warn( "Unable to run $SNORT_TO_BRO_PROG. Check to make sure that the python run" .
" path is set correctly in the program.\n" );
exit( 1 );
}
# ref to hash containing all config data
my $config = {};
$config = getconfig( \%DEFAULT_CONFIG );
# Build the ruleset exclusion list
my $ignorerules = {};
$ignorerules = getignorerules( $config );
# Parse and store the system level augment file
# An array ref containing Bro::S2b::Augment objects will be returned in
# $augment_objects
my $augment_objects = [];
if( $augment_objects = Bro::S2b::Augment->new( filename => $config->{augmentconfig} ) )
{
# ok
}
else
{
if( $DEBUG > 0 )
{
warn( "Unable to retrieve any augment data." .
" This can be expected if the file has not been created yet.\n" );
if( ! $config->{updateaugment} )
{
warn( "Perhaps you have forgotten to run --updateaugment first?\n" );
}
}
}
my $snort_rule_files = [];
$snort_rule_files = getsnortrulefiles( $config->{snortrulesetdir}, $ignorerules );
# Is this a request to update the s2b-augment.cfg file?
if( $config->{updateaugment} )
{
my $ruleset_based_augment;
my $new_augment_data;
my $sigmap = {};
my %existing_sidrev;
# Read and parse the Snort alert classtype to Bro SigAction mappings
$sigmap = getsigmap( $config->{sigmapconfig} );
# Build a hash of "sid-rev" strings from the existing augment objects
# (if any)
foreach my $aug_obj( @{$augment_objects} )
{
my $key = $aug_obj->sigid();
$existing_sidrev{$key} = 1;
}
# Read and parse the ruleset augment data which is to be included into
# the augment config based on the Snort ruleset from which they come.
if( $config->{rulesetaugmentconfig} )
{
$ruleset_based_augment = getrulesetaugment( $config->{rulesetaugmentconfig} );
}
# Build the augment file
$new_augment_data = buildaugment( $snort_rule_files, $sigmap ) || [];
my @append_aug_list;
# Check for new augment objects
foreach my $new_aug( @{$new_augment_data} )
{
if( ! $existing_sidrev{$new_aug->sigid()} )
{
$new_aug->option( 'snort-rule-file' ) =~ m/([^\/]+)$/;
my $aug_snort_rulename = $1;
if( exists( $ruleset_based_augment->{$aug_snort_rulename} ) )
{
if( !( $new_aug = $new_aug->merge( $ruleset_based_augment->{$aug_snort_rulename} ) ) )
{
next;
}
}
push( @append_aug_list, $new_aug );
}
}
if( @append_aug_list > 0 )
{
appendaugment( \@append_aug_list, $config->{augmentconfig} );
}
if( $DEBUG > 0 )
{
my $num_aug_blocks = scalar( @append_aug_list );
if( $num_aug_blocks > 0 )
{
warn( "Added a total of $num_aug_blocks new augment data blocks to augment file " . $config->{augmentconfig} . "\n" );
}
else
{
warn( "No new augment data found. Nothing added to augment file " . $config->{augmentconfig} . "\n" );
}
}
}
# Else assume this is a request to build Bro signatures
else
{
my %sigaction_list;
my @active_sigs;
# Parse and store the user level augment file
# An array ref containing Bro::S2b::Augment objects will be returned in
# $user_augment_objects
my $user_augment_objects = [];
if( -r $config->{useraugmentconfig} and
$user_augment_objects = Bro::S2b::Augment->new( filename => $config->{useraugmentconfig} ) )
{
# ok
}
else
{
if( $DEBUG > 1 )
{
warn( "Unable to retrieve any user level augment data." .
" This is non-fatal and can be expected if the file has not been created yet.\n" );
}
}
# Loop over each user augment object and build an index of sigids
my %user_aug_obj_idx;
for( my $i=0; $i < scalar( @{$user_augment_objects} ); ++$i )
{
$user_aug_obj_idx{$user_augment_objects->[$i]->sigid()} = $i;
}
my $ignore_snort_sids = {};
$ignore_snort_sids = getignoresids( $augment_objects );
# An array ref containing Bro::Signature objects will be returned in
# $converted_snort_rules
my $converted_snort_rules = {};
$converted_snort_rules = convertfromsnort( $snort_rule_files,
$ignore_snort_sids );
# Loop over the list of Bro::Signature objects and process each
foreach my $sig_obj( @{$converted_snort_rules} )
{
my $sig_is_active = 0;
my $user_aug_obj;
my $sig_obj_id = $sig_obj->sigid();
my $augment_obj;
# Find the corresponding Bro::S2b::Augment object. The match is found
# by comparing each object's sigid (minus the prefix if any).
# I realize at this point that this is a bit of a waste. I'll work on a
# better indexed version later.
foreach my $augment_obj( @{$augment_objects} )
{
# Compare the sigids and see if they match
if( $augment_obj->sigid() eq $sig_obj_id )
{
# Look for a matching user augment option and put it in
# $user_aug_obj if found.
if( exists( $user_aug_obj_idx{$sig_obj_id} ) )
{
$user_aug_obj = $user_augment_objects->[$user_aug_obj_idx{$sig_obj_id}];
}
# Determine if the rule is active
if( $augment_obj->active() )
{
$sig_is_active = 1;
}
if( $user_aug_obj and defined( $user_aug_obj->active() ) )
{
$sig_is_active = $user_aug_obj->active();
}
# Skip this instance if the sig is not set to active
if( ! $sig_is_active )
{
# must be inactive. No need to continue processing this
# signature
last;
}
# Check whether the connection direction information should be
# ignored. If so then remove it from the Bro::Signature object.
if( $config->{ignorehostdirection} )
{
if( my $dst_ip = $sig_obj->option( 'dst-ip' ) )
{
if( $dst_ip =~ m/[[:alpha:]]+/ )
{
$sig_obj->deloption( 'dst-ip' );
}
}
if( my $src_ip = $sig_obj->option( 'src-ip' ) )
{
if( $src_ip =~ m/[[:alpha:]]+/ )
{
$sig_obj->deloption( 'src-ip' );
}
}
}
# Modify the Bro::Signature object and include the augment data.
if( $augment_obj->augmentbrosig( $sig_obj ) )
{
# If user augment data exists then apply it now
if( $user_aug_obj )
{
$user_aug_obj->augmentbrosig( $sig_obj );
}
# Determine which sigaction to use, system or user
my $cur_sigaction;
if( $user_aug_obj and $user_aug_obj->option( 'sigaction' ) )
{
$cur_sigaction = $user_aug_obj->option( 'sigaction' );
}
else
{
$cur_sigaction = $augment_obj->option( 'sigaction' );
}
# Check if the sigaction is anything other than the default
# If so then add it to the hash.
if( $cur_sigaction ne $config->{defaultsigaction} )
{
$sigaction_list{$sig_obj_id} = $cur_sigaction;
}
# Put the bro signature object into the active list of
# signatures
push( @active_sigs, $sig_obj );
}
else
{
if( $DEBUG > 0 )
{
warn( "Failed to augment Bro signature $sig_obj_id\n" );
}
}
last;
}
}
}
# Write the sigactions to a file or if the file in $config is an empty string
# then send it to STDOUT
if( $config->{sigactiondest} )
{
if( open( OUTFILE, '>', $config->{sigactiondest} ) )
{
outputsigactions( \%sigaction_list, \*OUTFILE );
}
else
{
warn( "Failed to open file " . $config->{sigactiondest} . " for writing, unable to continue\n" );
exit( 1 );
}
close( OUTFILE );
}
else
{
outputsigactions( \%sigaction_list );
}
# Output the final signatures or if the file in $config is an empty string
# then send it to STDOUT
if( $config->{brosignaturedest} )
{
if( open( OUTFILE, '>', $config->{brosignaturedest} ) )
{
outputsigs( \@active_sigs, \*OUTFILE );
}
else
{
warn( "Failed to open file " . $config->{brosignaturedest} . " for writing, unable to continue\n" );
exit( 1 );
}
close( OUTFILE );
}
else
{
outputsigs( $converted_snort_rules );
}
}
exit( 0 );
#################################################
#####
##### Begin subroutines
#####
#################################################
sub getconfig
{
my $sub_name = 'getconfig';
my $arg1 = shift || \%DEFAULT_CONFIG;
my %default_config;
my %cmd_line_cfg;
my $main_config_file;
my %config;
if( ref( $arg1 ) eq 'HASH' )
{
%default_config = %{$arg1};
}
else
{
return( undef );
}
GetOptions( \%cmd_line_cfg,
'configdir=s',
'mainconfig=s',
'augmentconfig=s',
'useraugmentconfig=s',
'sigactiondest=s',
'brosignaturedest=s',
'sigmapconfig=s',
'snortrulesetdir=s',
'defaultsigaction=s',
'ignorehostdirection:s',
'rulesetaugmentconfig',
'updateaugment',
'usage|help|h',
'debug|verbose|d|v:i',
'version|V',
'copyright', );
# Check for options which will prevent the program from running
# any further
if( $cmd_line_cfg{usage} )
{
print usage();
exit( 0 );
}
elsif( $cmd_line_cfg{version} )
{
print version();
exit( 0 );
}
elsif( $cmd_line_cfg{copyright} )
{
print copyright();
exit( 0 );
}
else
{
# just continue on
}
if( ! $cmd_line_cfg{mainconfig} )
{
if( defined( $ARGV[0] ) )
{
$cmd_line_cfg{mainconfig} = $ARGV[0];
}
else
{
$cmd_line_cfg{mainconfig} = $default_config{mainconfig};
}
}
$main_config_file = $cmd_line_cfg{mainconfig};
my $conf = Config::General->new( -ConfigFile => $main_config_file,
-LowerCaseNames => 1,
);
%config = $conf->getall;
$config{'mainconfig'} = $main_config_file;
# Any args passed through the command line will override file options
while( my( $key, $value ) = each( %cmd_line_cfg ) )
{
$config{$key} = $value;
}
# Set default values for options that have not already been configured
while( my( $key, $value ) = each( %{$arg1} ) )
{
if( ! exists( $config{$key} ) )
{
$config{$key} = $value;
}
}
# Set Debug level
$DEBUG = $config{debug} if exists( $config{debug} );
if( checkconfig( \%config ) )
{
if( $DEBUG > 4 )
{
warn( "Configuration memory dump:\n" );
warn( $conf->save_string( \%config ) );
warn( "\n" );
}
return( \%config );
}
else
{
warn( "exiting program" );
exit( 1 );
}
}
sub checkconfig
{
my $sub_name = 'checkconfig';
my $cfg_hash = shift || return undef;
# Check to make sure that the config directory is defined.
if( defined( $cfg_hash->{'configdir'} ) )
{
# Check to make sure that the config directory has a sane value.
if( $cfg_hash->{configdir} !~ m/[*;`{}%]+/ and
$cfg_hash->{configdir} =~ m~^([[:print:]]{1,1024}?)/*$~ )
{
$cfg_hash->{configdir} = $1;
if( !( -d $cfg_hash->{configdir} ) )
{
warn( "configdir '" .$cfg_hash->{configdir} . "' is not a directory\n" );
return( 0 );
}
}
else
{
warn( "Config directory contains invalid characters or is longer than 1024 bytes\n" );
return( 0 );
}
}
else
{
warn( "No config directory specified\n" );
return( 0 );
}
# Check to make sure that the Snort rule directory is
# specified and readable
if( defined( $cfg_hash->{snortrulesetdir} ) )
{
if( -d $cfg_hash->{snortrulesetdir} )
{
if( ! -r $cfg_hash->{snortrulesetdir} )
{
warn( "Unable to read directory " . $cfg_hash->{snortrulesetdir} ."\n" );
return( 0 );
}
else
{
# Strip of any trailing slash on the end
$cfg_hash->{snortrulesetdir} =~ m~^([[:print:]]+?)/*$~;
$cfg_hash->{snortrulesetdir} = $1;
}
}
else
{
warn( $cfg_hash->{snortrulesetdir} . " is not a valid directory\n" );
return( 0 );
}
}
else
{
warn( "No snortruleset directory has been specified\n" );
return( 0 );
}
# Check to make sure the sidprefix only conatins alphanumeric and dash characters
if( defined( $cfg_hash->{sidprefix} ) )
{
if( $cfg_hash->{sidprefix} =~ m/^([[:alnum:]-]*)$/ )
{
$cfg_hash->{sidprefix} = $1;
}
else
{
warn( "Invalid charcters in the sidprefix. May only contain alphanumeric and dash characters\n" );
return( 0 );
}
}
else
{
# else set it to a blank string
$cfg_hash->{sidprefix} = '';
}
# Check to make sure default-sigaction is valid and set otherwise use
# the default. Need to tie into the Bro config later to thoroughly check this.
if( defined( $cfg_hash->{defaultsigaction} ) )
{
if( $cfg_hash->{defaultsigaction} =~ m/^[[:alnum:]_]+$/ )
{
# ok
}
else
{
warn( "Default Bro SigAction --default-sigaction has invalid characters\n" );
}
}
else
{
warn( "No default Bro SigAction --default-sigaction has been set\n" );
return( undef );
}
# Check the ignorehostdirection option for values other than true or false
if( defined( $cfg_hash->{ignorehostdirection} ) )
{
if( $cfg_hash->{ignorehostdirection} =~ m/^(?:f|0)/i )
{
$cfg_hash->{ignorehostdirection} = 0;
}
elsif( $cfg_hash->{ignorehostdirection} )
{
$cfg_hash->{ignorehostdirection} = 1;
}
else
{
if( $DEBUG > 0 )
{
warn( "Unknown value of " . $cfg_hash->{ignorehostdirection} .
" assigned to option ignorehostdirection, defaulting to true.\n" );
}
$cfg_hash->{ignorehostdirection} = 1;
}
}
# Check to make sure the sigmap file exists, is readable and > 0 bytes.
if( defined( $cfg_hash->{sigmapconfig} ) )
{
my $fn = $cfg_hash->{configdir} . '/' . $cfg_hash->{sigmapconfig};
if( -r $fn and -s $fn )
{
$fn =~ m/^([[:print:]]+)$/;
$cfg_hash->{sigmapconfig} = $1;
}
else
{
warn( "sigmapconfig file at '$fn' is not readable or zero length\n" );
return( 0 );
}
}
else
{
if( $cfg_hash->{updateaugment} )
{
warn( "No sigmapconfig file specified\n" );
return( 0 );
}
}
# Check if the augmentconfig option exists and validate it if it does.
if( defined( $cfg_hash->{augmentconfig} ) )
{
my $fn = $cfg_hash->{configdir} . '/' . $cfg_hash->{augmentconfig};
$fn =~ m/^([[:print:]]+)$/;
$cfg_hash->{augmentconfig} = $1;
}
# Check if the rulesetaugmentconfig file exists and validate it if it does.
if( defined( $cfg_hash->{rulesetaugmentconfig} ) )
{
my $fn = $cfg_hash->{configdir} . '/' . $cfg_hash->{rulesetaugmentconfig};
if( -r $fn and -s $fn )
{
$fn =~ m/^([[:print:]]+)$/;
$cfg_hash->{rulesetaugmentconfig} = $1;
}
else
{
warn( "rulesetaugmentconfig file at '$fn' is not readable\n" );
return( 0 );
}
}
# Check to make sure that sigactiondest is defined and contains valid characters
if( ! $cfg_hash->{updateaugment} )
{
# {brosignaturedest}
if( defined( $cfg_hash->{sigactiondest} ) )
{
if( $cfg_hash eq '' )
{
# ok, send to stdout
$cfg_hash->{sigactiondest} = '';
}
elsif( my $fn = canwritefile( $cfg_hash->{sigactiondest} ) )
{
$cfg_hash->{sigactiondest} = $fn;
}
else
{
warn( "No valid --sigactiondest, unable to continue.\n" );
return( undef );
}
}
else
{
warn( "No filename specified for --sigactiondest\n" );
return( undef );
}
}
# Check to make sure that brosignaturedest is defined and contains valid characters
if( ! $cfg_hash->{updateaugment} )
{
if( defined( $cfg_hash->{brosignaturedest} ) )
{
if( $cfg_hash eq '' )
{
# ok, send to stdout
$cfg_hash->{brosignaturedest} = '';
}
elsif( my $fn = canwritefile( $cfg_hash->{brosignaturedest} ) )
{
$cfg_hash->{brosignaturedest} = $fn;
}
else
{
warn( "No valid --brosignaturedest, unable to continue.\n" );
return( undef );
}
}
else
{
warn( "No filename specified for --brosignaturedest\n" );
return( undef );
}
}
return( 1 );
}
sub getignorerules
{
my $sub_name = 'getignorerules';
my $cfg_hash = shift || return( undef );
my $ret_hash_ref = {};
if( ref( $cfg_hash->{ignoresnortrulesets} ) eq 'HASH' )
{
foreach my $rule_name( keys( %{$cfg_hash->{ignoresnortrulesets}} ) )
{
$ret_hash_ref->{$rule_name} = 1;
}
}
elsif( ref( $cfg_hash->{ignoresnortruleset} ) eq 'ARRAY' )
{
foreach my $rule_name( @{$cfg_hash->{ignoresnortruleset}} )
{
$ret_hash_ref->{$rule_name} = 1;
}
}
return( $ret_hash_ref );
}
sub getignoresids
{
my $sub_name = 'getignoresids';
# Argument will be a ref to an array of augment objects
my $augment_list = shift || return( undef );
my $ret_hash_ref = {};
if( ref( $augment_list ) eq 'ARRAY' )
{
foreach my $aug_obj( @{$augment_list} )
{
if( $aug_obj->active() )
{
# rule is active
}
else
{
# rule is marked as not active
my $ignore_sid = $aug_obj->sid();
$ret_hash_ref->{$ignore_sid} = 1;
}
}
}
else
{
return( undef );
}
return( $ret_hash_ref );
}
sub getsnortrulefiles
{
my $sub_name = 'getsnortrulefiles';
my $rules_dir = shift || undef;
my $exclusion_hash = shift || {};
my @ret_file_list;
if( opendir( DIR, $rules_dir ) )
{
while( my $fn = readdir( DIR ) )
{
# Make sure that the filename has only sane characters,
# ends with '.rules' , and does not begin with a '.'
if( $fn =~ m/^([^.]+[[:print:]]+\.rules)$/ )
{
# Untaint
$fn = $1;
# Make sure the file isn't set as ignored in the config
if( ! $exclusion_hash->{$fn} )
{
# expand the filename to it's full path
my $full_fn = "$rules_dir/$fn";
if( -f $full_fn and -r $full_fn )
{
push( @ret_file_list, $full_fn );
if( $DEBUG > 4 )
{
warn( "Adding Snort rule file $full_fn to list of rules to convert\n" );
}
}
else
{
if( $DEBUG > 0 )
{
warn( "Unable to read Snort rule file $full_fn\n" );
}
next;
}
}
else
{
if( $DEBUG > 1 )
{
warn( "Snort ruleset \'$fn\' is being ignored as specified in the config file\n" );
}
next;
}
}
else
{
next;
}
}
}
else
{
warn( "Unable to open Snort ruleset directory for reading at $rules_dir\n" );
}
return( \@ret_file_list );
}
sub getsigmap
{
my $sub_name = 'getsigmap';
my $sigmapfile = shift || return( undef );
my %config;
my $conf;
if( $conf = Config::General->new( -ConfigFile => $sigmapfile,
-LowerCaseNames => 1,
-AutoLaunder => 1,
-AllowMultiOptions => 'no' ) )
{
%config = $conf->getall;
}
else
{
warn( "Unable to read the sigmapconfig file\n" );
return( 0 );
}
if( $DEBUG > 4 )
{
warn( "List of default Snort alert classtype to Bro SigAction maps:\n" );
while( my( $key, $value ) = each( %config ) )
{
warn( "'$key' maps to '$value'\n" );
}
}
return( \%config );
}
sub convertfromsnort
{
my $sub_name = 'convertfromsnort';
my $rule_files = shift || return( undef );
my $ignore_sids = shift || return( undef );
my @converted_rules;
foreach my $rule_file( @{$rule_files} )
{
my $convert = `$SNORT_TO_BRO_PROG 2>/dev/null $rule_file`;
if( $DEBUG > 4 )
{
warn( "SIGNATURES BEGIN for file $rule_file => \n" );
warn( $convert || '' . "\n" );
warn( "----SIGNATURES END----\n" );
}
foreach my $sig_block( Bro::Signature::findkeyblocks( $convert ) )
{
if( ! $sig_block )
{
next;
}
if( my $bro_sig_obj = Bro::Signature->new( string => $sig_block ) )
{
push( @converted_rules, $bro_sig_obj );
}
else
{
if( $DEBUG > 0 )
{
warn( "Failed to create a Bro::Signature for a rule in file $rule_file\n" );
}
}
}
}
# If successful this returns an array ref of Bro::Signature objects
return( \@converted_rules );
}
sub outputsigactions
{
my $sub_name = 'outputsigactions';
my $_sigactions = shift;
my $_output_dest = shift || \*STDOUT;
# Heading of SigAction table
my $tm = scalar( localtime() );
print $_output_dest "\# This file was created by s2b.pl on $tm.\n";
print $_output_dest "\# This file is dynamically generated each time s2b.pl is" .
" run and therefore any \n\# changes done manually will be overwritten.\n\n";
print $_output_dest 'redef signature_actions += {' . "\n";
while( my( $sigid, $sigaction ) = each( %{$_sigactions} ) )
{
print $_output_dest ' ["' .
$config->{sigprefix} .
$sigid .
'"] = ' .
$sigaction .
",\n";
}
# ending of SigAction table
print $_output_dest '}; ' . "\n";
}
sub outputsigs
{
my $sub_name = '';
my $_sig_objs = shift;
my $_output_dest = shift || \*STDOUT;
my $tm = scalar( localtime() );
print $_output_dest "\# This file was created by s2b.pl on $tm.\n";
print $_output_dest "\# This file is dynamically generated each time s2b.pl is" .
" run and therefore any \n\# changes done manually will be overwritten.\n\n";
foreach my $sig( @{$_sig_objs} )
{
print $_output_dest $sig->output( sigprefix => $config->{sigprefix} ), "\n\n";
}
}
sub getrulesetaugment
{
my $sub_name = 'getrulesetaugment';
my $raf = shift || return( undef ); # ruleset augment file
my $ret_hash;
if( $DEBUG > 2 )
{
warn( "Attempting to parse the ruleset augment file at \'$raf\'\n" );
}
my $conf = Config::General->new( -ConfigFile => $raf,
-LowerCaseNames => 1,
);
my %config = $conf->getall;
while( my( $key, $value ) = each( %config ) )
{
if( $DEBUG > 2 )
{
warn( "Looking for augment data for ruleset $key\n" );
}
if( keys( %{$value} ) > 0 )
{
while( my( $opt, $opt_val ) = each( %{$value} ) )
{
$ret_hash->{$key}->{$opt} = $opt_val;
if( $DEBUG > 2 )
{
warn( " Found option $opt\n" );
}
}
}
else
{
if( $DEBUG > 2 )
{
warn( "No augment data found\n" );
}
}
}
return( $ret_hash );
}
sub buildaugment
{
my $sub_name = 'buildaugment';
my $rulesets = shift || return( undef );
my $sigmap = shift || return( undef );
my $default_sigaction = shift || $DEFAULT_CONFIG{defaultsigaction};
my $ret_aug_objs = {};
foreach my $rule_file( @{$rulesets} )
{
if( open( IN_FILE, $rule_file ) )
{
my $full_line = '';
my $line_num = 0;
while( defined( my $line = <IN_FILE> ) )
{
++$line_num;
my $end_of_rule = 0;
if( $line =~ m/^[[:space:]]\#/
or $line =~ m/^[[:space:]]*$/ )
{
# ignore this line, it's all comments or whitespace
next;
}
else
{
if( $line =~ m/^(alert.+)/ )
{
$line = $1;
if( $line =~ m/^(.+?)[[:space:]]*\\[[:space:]]*$/ )
{
$full_line = join( ' ', $full_line, $1 );
}
elsif( $full_line )
{
$full_line = join( ' ', $full_line, $line );
$end_of_rule = 1;
}
else
{
$full_line = $line;
$end_of_rule = 1;
}
}
else
{
# Snort action is not supported for conversion
next;
}
}
if( $end_of_rule )
{
# Extract the directives section
if( $full_line =~ m/\((.+)\)/ )
{
my $sigaction;
my %directive_args;
my @new_aug_objs;
my $directive_section = $1;
my @directives = split( /[[:space:]]*\;[[:space:]]*/, $directive_section );
foreach( @directives )
{
# split the directive name from it's value
my( $directive_name, $directive_value ) = split( /:[[:space:]]*/, $_, 2 );
if( defined( $directive_name ) and defined( $directive_value ) )
{
$directive_args{$directive_name} = $directive_value;
#print "DIRECTIVE => ", $directive_name;
#print ", VALUE => ", $directive_value, "\n";
}
}
# translate the snort event classtype to a Bro SigAction
if( $sigmap->{$directive_args{classtype}} )
{
# ok, found one
$sigaction = $sigmap->{$directive_args{classtype}};
}
else
{
# Didn't find a mapping, using the default
$sigaction = $default_sigaction;
if( $DEBUG > 0 )
{
warn( "No Snort classtype to Bro SigAction mapping found",
" for classtype " . $directive_args{classtype} . " using default",
" of $default_sigaction\n" );
}
}
# create a new augment object with the directive parts
# we are interested in.
my $new_aug_obj = Bro::S2b::Augment->new(
'sid' => $directive_args{sid},
'snort-rule-file' => $rule_file,
'sid-rev' => $directive_args{rev},
'comment' => $directive_args{msg},
'active' => 'T',
'sigaction' => $sigaction,
);
#print $new_aug_obj->output(), "\n\n";
my $new_aug_sigid = $new_aug_obj->sigid();
my $new_aug_sid = $new_aug_obj->sid();
my $new_aug_rev = $new_aug_obj->rev();
if( exists( $ret_aug_objs->{$new_aug_sigid} ) )
{
if( $DEBUG > 0 )
{
warn( "Duplicate augment block found for SID number",
" $new_aug_sid, rev $new_aug_rev\n" );
}
}
else
{
$ret_aug_objs->{$new_aug_sigid} = $new_aug_obj;
}
}
else
{
warn( "Could not find a diretives section for Snort rule in",
" file $rule_file at line $line_num\n" );
}
$full_line = '';
$end_of_rule = 0;
}
}
close( IN_FILE );
}
else
{
warn( "Failed to open file $rule_file for reading while trying to",
" update the augment file\n" );
}
}
my @ret_vals = values( %{$ret_aug_objs} );
return( \@ret_vals );
}
sub appendaugment
{
my $sub_name = 'appendaugment';
my $aug_obj = shift || return( undef );
my $filename = shift || return( undef );
my @proc_objs;
if( ref( $aug_obj ) eq 'ARRAY' )
{
@proc_objs = @{$aug_obj};
}
else
{
$proc_objs[0] = $aug_obj;
}
if( open( OUTFILE, '>>', $filename ) )
{
my $tm = scalar( localtime() );
print OUTFILE '########## Start of new augment data created on ' . "$tm\n";
foreach my $aug_inst( @proc_objs )
{
print OUTFILE $aug_inst->output(), "\n\n";
}
print OUTFILE '########## End of new augment data created on ' . "$tm\n\n";
}
else
{
if( $DEBUG > 0 )
{
warn( "Unable to open file $filename for writing.\n" );
}
return( undef );
}
close( OUTFILE );
return( 1 );
}
sub sigidnum
{
# The sidnum is assumed to be the first part before the last '-'
# and up to but not including '-'
# s2b-123-1 prefix-sidnum-sidrev
my $sub_name = 'sigidnum';
my $sigid = shift || return( undef );
my $ret_sid;
if( $sigid =~ m/([[:digit:]]+)-[[:digit:]]+$/ )
{
$ret_sid = $1;
}
return( $ret_sid );
}
sub sigidrev
{
# The sidrev is assumed to be the last part of the sigid after a '-'
# s2b-123-1 prefix-sidnum-sidrev
my $sub_name = 'sigidrev';
my $sigid = shift || return( undef );
my $ret_rev;
if( $sigid =~ m/[[:digit:]]+-([[:digit:]]+)$/ )
{
$ret_rev = $1;
}
return( $ret_rev );
}
sub canwritefile
{
my $sub_name = 'canwritefile';
my $filename = shift;
my $ret_fn;
if( $filename =~ m/^([[:print:]]+)$/ )
{
my $fn = $1;
my $dir = $fn;
$dir =~ s/[^\/]+$//;
if( length( $dir ) < 1 )
{
$dir = './';
}
# Check to make sure that the file can be created/written to.
# Does the file already exist and can be written to
if( -w $fn )
{
# ok.
$ret_fn = $fn;
}
# Is the directory writtable
elsif( -d $dir and -w $dir )
{
# ok.
$ret_fn = $fn;
}
# Blow chunks
else
{
warn( "Unable to create or modify file '$fn'\n" );
}
}
else
{
warn( "Filename contains non-printable characters and is invalid.\n" );
}
return( $ret_fn );
}
sub usage
{
my $sub_name = 'usage';
my $usage_text = copyright();
$usage_text = qq~$usage_text
Options passed to the program on the command line
Command line reference
--configdir Directory containing the various configuration files
--mainconfig Main configuration file
--augmentconfig Filename of System Augment Config file
--useraugmentconfig Filename of the User Augment Config file
--rulesetaugmentconfig
Filename of the Ruleset Augment Config file
--sigmapconfig Filename of Mappings for Snort alert classtype to
Bro SigAction
--brosignaturedest Filename to write the Bro signatures to
--defaultsigaction Default Bro SigAction
--sigactiondest Filename to write the SigActions to
--snortrulesetdir Directory containing Snort rules
--ignorehostdirection
Ignore Snort connection direction information and
do not include in the Bro signature. default 'true'
--updateaugment Build or update the s2b-augment.cfg file using rulesets
found in --snortrulesdir
--usage|--help|-h Summary of command line options
--debug|-d Specify the debug level from 0 to 5. default 1
--version Output the version numberto STDOUT
--copyright Output the copyright info to STDOUT
~;
return( $usage_text );
}
sub version
{
my $sub_name = 'version';
return( $VERSION );
}
sub copyright
{
my $sub_name = 'copyright';
my $copyright =
qq~s2b.pl
version $VERSION, Copyright (C) 2004 Lawrence Berkeley National Labs, NERSC
Written by Roger Winslow~;
return( $copyright );
}