mirror of
https://github.com/zeek/zeek.git
synced 2025-10-06 08:38:20 +00:00
2991 lines
64 KiB
Perl
Executable file
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 );
|
|
}
|
|
|