#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell
#
# $Id: snortconfig,v 1.13 2004/05/19 18:41:54 bmc Exp $
#
# snortconfig - a simple yet complicated rules maintance system
#
# Copyright (C) 2003 Brian Caswell <bmc@shmoo.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
#      This product includes software developed by Brian Caswell.
# 4. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    
=head1 NAME

snortconfig - a simple yet complicated rules maintance system

=head1 SYNOPSIS

snortconfig -file <SNORT_CONFIG> -config <CONFIG> [-verbose] 
            [-directory <OUTPUT_DIRECTORY>] [-honeynet] [-inline]

=head1 DESCRIPTION

snortconfig is a rules modification system for snort that is generated 
from a configuration file.  This allows a user to keep their ruleset 
updated without too much of a headache.  

=head1 OPTIONS

=over 4

=item -file <SNORT_CONFIG>

Process the rules located in snort.conf

=item -config <CONFIG>

Configuration for modification of rules

=item -verbose

Increases the debug verbose level

=item -directory <PATH>

Sets the output directory for generated rulesets  (CWD by default)

=item -inline

Add snort-inline specific options.  These include drop, sdrop, reject, replace, and replace_or_drop.

=item -honeynet 

Reverse source and destination IP addresses if both are using variables.  Using -honeynet implies -inline

!!! WARNING!!!  honeypots are designed to be attacked.  while this tool may *HELP* reduce risk of running such a system, this is not a perfect solution.  PLEASE check out http://www.honeynet.org for more information on the risks on running honeynets.

=back

=head1 Configuration

Configuration is done using a basic INI style configuration.  

snortconfig supports three methods of configuration of rules.  The methods are specifing what rules to apply changes to.  These methods are files, sids, and classifications.  This allows make broad changes to snort rules very quickly.  

By specifing files, changes are made to any rules in the specified files.  By specifing sids, changes are made to specific snort rules based on the sid rule option.  By specifing classifications, changes are made to any rules that have the specified classtype rule option.

There are eight types of modifications that can be done on rules.

=over 4

=item alert

Set the rule's action to "alert", which will trigger the normal alerting mechanisms within snort.

=item disable

Disables the rule by commenting it out.

=item drop

Set the rule's action to "drop", which will cause snort to drop the packet in inline mode.  (ONLY FOR SNORT-INLINE)

=item log

Set the rule's action to "log", which will trigger the normal logging mechanisms within snort.

=item replace

Modify the payload of the packet where each pattern match is made to a random string of bytes.  This can be used to attempt to disable exploits from being successful.   (ONLY FOR SNORT-INLINE)

=item replace_or_drop

Modify the payload of the packet where each pattern match is made to a random string of bytes.  For rules that do not have content matches, the rule action is set to drop.  This can be used to attempt to disable exploits from being successful, weither they have content matches or not.   (ONLY FOR SNORT-INLINE)

=item reject

Set the rule's action to "reject", which will drop the packet and log it via normal logging mechanisms.  Additionally, if the protocol is TCP then snort will send a TCP reset, otherwise it will send an icmp port unreachable.

=item sdrop

Set the rule's action to "sdrop", which will cause snort to drop the packet in inline mode and not log the alert.  (ONLY FOR SNORT-INLINE)

=back

=head1 EXAMPLE

=over 4

 [files]
 drop: porn.rules, virus.rules
 replace: rpc.rules, icmp.rules

 [sids]
 drop: 2122, 1866, 2108, 2109
 disable: 300

 [classifications]
 replace: shellcode-detect
 sdrop: kickass-porn, policy-violation

=back

=head1 NOTES

This tool does not handle multiline rules.  Also, configuration is done all at once.  It would be nice if each block was applied in order so you can apply multiple configurations in order for even more advanced configuration.  Like I said, it would be nice, but its not there yet. 

=head1 AUTHOR

Brian Caswell <bmc@shmoo.com>

=head1 REPORTING BUGS

Report bugs to <bmc@shmoo.com>

=head1 THANKS

Thanks to The Honeynet Project

=head1 COPYRIGHT

Copyright (c) 2003 Brian Caswell 

=head1 SEE ALSO

L<snort(8)>

=head1 BUGS

snortconfig doesn't handle multiline rules properly.  Bad things may happen if you use em.  You have been warned.   

Since you probably didn't read this section of the manual until you ran into this bug, don't ask about it else I'll point and laugh because you didn't read the manual.

=cut

use Net::Snort::Parser::File;
use Net::Snort::Parser::Rule;
use Getopt::Long;
use FileCache;

$Net::Snort::Parser::Rule::DEBUG++;
my $VERSION = (qw($Revision: 1.13 $))[1]; 

my %optomizable = map { $_, 1} qw (dsize flags flow fragbits icmp_id icmp_seq icode id ipopts ip_proto itype seq session tos ttl ack window resp sameip stateless);

my @getopt_args = ('file=s', 'config=s', 'random', 'inline', 'honeynet', 'directory=s', 'verbose+', 'help|?');
my %options;
GetOptions(\%options, @getopt_args);

sub usage {
    my ($error) = @_;
    if ($error)  {
        print "ERROR : $error\n\n";
    }

    print "snortconfig -file <SNORT_CONFIG> -config <CONFIG> [-verbose]\n";
    print "            [-directory <OUTPUT_DIRECTORY>] [-honeynet]\n";
    exit;
}

usage() if ($options{'help'});

usage("No snort.conf specified") if (!defined($options{'file'}));
usage("No config specified")     if (!defined($options{'config'}));
usage("Can't read snort.conf")   if (!-r $options{'file'});
usage("Can't read config")       if (!-r $options{'config'});

my $PATH = "./";
if ($options{'directory'}) {
    if (-x $options{'directory'} && ! -d $options{'directory'}) {
        mkdir($options{'directory'}) || die "Can't make directory $!";
    }
    $PATH = $options{'directory'} . "/";
}

my $config = parse_config($options{'config'});

my %mod_map;

$mod_map{'pass'}    = *make_generic;
$mod_map{'log'}    = *make_generic;
$mod_map{'alert'}    = *make_generic;
$mod_map{'disable'} = *make_disable;

if ($options{'honeynet'} || $options{'inline'}) {
    $mod_map{'drop'}    = *make_generic;
    $mod_map{'sdrop'}   = *make_generic;
    $mod_map{'reject'}   = *make_generic;
    $mod_map{'replace'} = *make_replace;
    $mod_map{'replace_or_drop'} = *make_replace_or_drop;
}

my (@lines) = parse_file($options{'file'});
my $rule_parser = Net::Snort::Parser::Rule->new();
foreach my $line (@lines) {
    my $rule = $rule_parser->parse_rule($line->{'line'});
    my $file = $line->{'file'};
    my $fh = cacheout("$PATH/$file");
    if ($rule) {
        if ($rule->{'failed'}) {
            print "failed to parse: $rule->{'failed'}\n";
            print "file: $file\n";
            print "line: $line->{'line'}\n";
            exit;
        } else {

            foreach my $method (sort {$a cmp $b} keys %$config) {
                foreach my $arg (sort {$a cmp $b} keys %{$config->{$method}}) {
                    my $type = $config->{$method}->{$arg};
                    if ($method eq 'files') {
                        if ($line->{'file'} eq $arg) {
                            if (defined($mod_map{$type})) {
                                $rule = &{$mod_map{$type}} ($type, $rule);
                            } else {
                                die "Don't know how to handle $type in this mode.\n";
                            }
                        }
                    } elsif ($method eq 'sids') {
                        if (defined($rule->{'sid'}) && $rule->{'sid'} eq $arg) {
                            $rule = &{$mod_map{$type}} ($type, $rule);
                        }
                    } elsif ($method eq 'classifications') {
                        if (defined($rule->{'classtype'})) {
                            if ($rule->{'classtype'} eq $arg) {
                                $rule = &{$mod_map{$type}} ($type, $rule);
                            } else {
                                warn "no classtype for $rule->{'sid'}\n";
                            }
                        }
                    } else {
                        die "Unknown method $method!\n";
                    }
                }
            }
            print $fh "" . build_rule($rule) . "\n";
        }
    } else {
        warn "failed to parse as rule: $line->{'file'} : $line->{'line'}\n" if ($options{'verbose'});
        print $fh "$line->{'line'}\n";
    }
}

sub parse_config {
    my ($file) = @_;
    open(FILE, "<$file") || die "Can't open $file $!";

    my $method;

    my %config;

    while (<FILE>) {
        chomp;
        next if (/^\s*\#/);
        next if (/^\s*$/);
        if (/^\[(\w+)\]$/) {
            $method = $1;
        } else {
            die "Undefined method..." if (!defined($method));
            my ($type, $rest) = split (/:\s*/, $_, 2);
            foreach my $arg (split (/,\s*/, $rest)) {
                $config{$method}{$arg} = $type;
            }
        }
    }
    return (\%config);
}

sub make_generic {
   my ($type, $rule) = @_;
   $rule->{'action'} = $type;
   return ($rule);
}

sub make_disable {
   my ($type, $rule) = @_;
   $rule->{'state'} = 0;
   return ($rule);
}

sub make_replace {
   my ($type, $rule) = @_;

   my $did;
   foreach my $id (sort { $a <=> $b } keys %{$rule->{'options'}}) {
       if ($rule->{'options'}->{$id}->{'type'} eq 'content') {
           my $string;
           for (my $i = 0 ; $i < length($rule->{'options'}->{$id}->{'string'}) ; $i++) {
               if (!defined($options{'random'})) {
                   $string .= chr(int(rand(255)));
               } else {
                   $string .= "\xff";
               }
           }

           $rule->{'options'}->{$id}->{'replace'} = $string;
           $did++;
       }
   }

   warn "Tried adding replace to $rule->{'sid'}.  No contents\n"
     if (!$did && $options{'verbose'});
   return ($rule);
}

sub make_replace_or_drop {
   my ($type, $rule) = @_;

   my $did;
   foreach my $id (sort { $a <=> $b } keys %{$rule->{'options'}}) {
       if ($rule->{'options'}->{$id}->{'type'} eq 'content') {
           my $string;
           for (my $i = 0 ; $i < length($rule->{'options'}->{$id}->{'string'}) ;
           $i++)
           {
               if (!defined($options{'random'})) {
                   $string .= chr(int(rand(255)));
               } else {
                   $string .= "\xff";
               }
           }

           $rule->{'options'}->{$id}->{'replace'} = $string;
           $did++;
       }
   }

   if (!$did) {
       $rule->{'action'} = 'drop';
       warn "Tried adding replace to $rule->{'sid'}.  No contents\n" if ($options{'verbose'});
   }
   return ($rule);
}

sub build_rule {
   my ($rule) = @_;

   my (@opts);

   my $show_rule;
   if ($options{'honeynet'}) {
      my $src = $rule->{'src'};
      my $dst = $rule->{'dst'};
      if ($src =~ /^\$/ && $dst =~ /^\$/) {
         $rule->{'dst'} = $src;
         $rule->{'src'} = $dst;
      } elsif ($options{'verbose'}) {
         $show_rule++ if ($options{'verbose'} > 1);
         warn "Did not switch $rule->{'sid'}.  Doesn't use variables.\n";
      }
   }
   my $header = join (
      " ",                 $rule->{'action'},
      $rule->{'protocol'}, $rule->{'src'},
      $rule->{'srcport'},  $rule->{'direction'},
      $rule->{'dst'},      $rule->{'dstport'}
   );

   
   foreach my $id (sort { $a <=> $b } keys %{$rule->{'options'}}) {
       my $type = $rule->{'options'}->{$id}->{'type'};
       if (!defined($type)) {
           require Data::Dumper;
           die "Undefined type for $rule->{'sid'}:\n" .  Data::Dumper::Dumper($rule);
       }

       if ($type eq 'content' || $type eq 'uricontent') {
           my $content = $rule->{'options'}->{$id}->{'optomized'};
           die "no content: sid $rule->{'sid'}: $content" if !length($content);

           if ($rule->{'options'}->{$id}->{'not'}) {
               push (@opts, "$type:!\"$content\";");
           } else {
               push (@opts, "$type:\"$content\";");
           }

           if ($rule->{'options'}->{$id}->{'relative'}) {
               if ($rule->{'options'}->{$id}->{'depth'}) {
                   push (@opts, "within:$rule->{'options'}->{$id}->{'depth'};");

                   # here, we see if exists as well as is non 0, since distance
                   # 0 with a within is a noop.
                   if ($rule->{'options'}->{$id}->{'offset'}) {
                       push (@opts, "distance:$rule->{'options'}->{$id}->{'offset'};");
                   }
               } elsif (defined($rule->{'options'}->{$id}->{'offset'})) {
                   push (@opts, "distance:$rule->{'options'}->{$id}->{'offset'};");
               } 
           } else  {
               if ($rule->{'options'}->{$id}->{'depth'}) {
                   push (@opts, "depth:$rule->{'options'}->{$id}->{'depth'};");

                   # here, we see if exists as well as is non 0, since distance
                   # 0 with a within is a noop.
                   if ($rule->{'options'}->{$id}->{'offset'}) {
                       push (@opts, "offset:$rule->{'options'}->{$id}->{'offset'};");
                   }
               } elsif (defined($rule->{'options'}->{$id}->{'offset'})) {
                   push (@opts, "offset:$rule->{'options'}->{$id}->{'offset'};");
               }
           }

           if ($rule->{'options'}->{$id}->{'replace'}) {
               my $replace = make_hex($rule->{'options'}->{$id}->{'replace'});
               push (@opts, "replace:\"|$replace|\";");
           }

           if ($rule->{'options'}->{$id}->{'nocase'}) {
               push (@opts, "nocase;");
           }
       } elsif ($type eq 'flow') {
           unshift (@opts, "flow:" . join(",",sort keys %{$rule->{'options'}->{$id}->{'args'}}) . ";");
       } elsif (defined($rule->{'options'}->{$id}{'args'})) {
           if (defined $optomizable{$type}) {
               unshift(@opts, "$type:$rule->{'options'}->{$id}{'args'};");
           } else {
           push (@opts, "$type:$rule->{'options'}->{$id}{'args'};");
           }
       } else {
           if (defined $optomizable{$type}) {
               unshift (@opts, "$type;");
           } else {
               push (@opts, "$type;");
           }
       }
   }
   
   
   my $msg = $rule->{'name'};
   $msg =~ s/([\\:;\(\)])/\\$1/g;
   unshift (@opts, "msg:\"$msg\";");
 
   if ($rule->{'references'}) {
       foreach my $ref_type (sort {$a cmp $b} keys % { $rule->{'references'} } ) {
           foreach my $ref (sort {$a cmp $b} keys % { $rule->{'references'}->{$ref_type} }) {
               push (@opts, "reference:$ref_type,$ref;");
           }
       }
   }

   if ($rule->{'classification'}) {
       push (@opts, "classtype:$rule->{'classification'};");
   }
  
   push (@opts, "sid:$rule->{'sid'};") if (defined($rule->{'sid'}));
   push (@opts, "rev:$rule->{'revision'};");

   my $string = $header . " (" . join (" ", @opts) . ")";
   if (!$rule->{'state'}) {
      $string = "# " . $string;
   }
#   use Data::Dumper;
#   $Data::Dumper::Sortkeys++;
#   print Dumper($rule);
   warn "$string\n" if ($show_rule);
   return ($string);
}

sub make_hex {
   my ($string) = @_;
   my @contents;
   foreach my $c (unpack('C*', $string)) {
      push (@contents, sprintf('%2.2X', $c));
   }
   return (join (" ", @contents));
}
