package Snort::Rule; =head1 NAME Snort::Rule - Perl extension for dynamically building snort rules =head1 SYNOPSIS use Snort::Rule; $rule = Snort::Rule->new( -action => 'alert', -proto => 'tcp', -src => 'any', -sport => 'any', -dir => '->', -dst => '192.188.1.1', -dport => '44444', ); $rule->opts('msg','Test Rule"'); $rule->opts('threshold','type limit,track by_src,count 1,seconds 3600'); $rule->opts('sid','500000'); print $rule->string()."\n"; OR $rule = 'alert tcp $SMTP_SERVERS any -> $EXTERNAL_NET 25 (msg:"BLEEDING-EDGE POLICY SMTP US Top Secret PROPIN"; flow:to_server,established; content:"Subject|3A|"; pcre:"/(TOP\sSECRET|TS)//[\s\w,/-]*PROPIN[\s\w,/-]*(?=//(25)?X[1-9])/ism"; classtype:policy-violation; sid:2002448; rev:1;)'; $rule = Snort::Rule->new(-parse => $rule); print $rule->string()."\n"; =head1 DESCRIPTION This is a very simple snort rule object. It was developed to allow for scripted dynamic rule creation. Ideally you could dynamically take a list of bad hosts and build an array of snort rule objects from that list. Then write that list using the string() method to a snort rules file. =cut use strict; use warnings; our $VERSION = '1.07'; # Put any options in here that require quotes around them my @QUOTED_OPTIONS = ('MSG','URICONTENT','CONTENT','PCRE'); =head1 OBJECT METHODS =head2 new Reads in the initial headers to generate a rule and constructs the snort::rule object around it. Accepts: -action => [string] ? [alert|log|pass|...] : 'alert' -proto => [string] ? [ip|udp|tcp|...] : 'IP' -src => [string] ? [$strIp] : 'any' -sport => [int] ? [$sport] : 'any' -dir => [string] ? [->|<-|<>] : '->' -dst => [string] ? [$strIp] : 'any' -dport => [int] ? [$dport] : 'any' -opts => [hashref] ? [hashref] : ''; -parse => $strRule # for parsing an existing rule into the object Returns: OBJECTREF =cut sub new { my ($class, %parms) = @_; my $self = {}; bless($self,$class); $self->init(%parms); $self->parseRule($parms{-parse}) if($parms{-parse}); return ($self); } # INIT sub init { my ($self,%parms) = @_; $parms{-action} = $parms{-action} ? $parms{-action} : 'alert'; $parms{-proto} = $parms{-proto} ? $parms{-proto} : 'IP'; $parms{-src} = $parms{-src} ? $parms{-src} : 'any'; $parms{-sport} = $parms{-sport} ? $parms{-sport} : 'any'; $parms{-dir} = $parms{-dir} ? $parms{-dir} : '->'; $parms{-dst} = $parms{-dst} ? $parms{-dst} : 'any'; $parms{-dport} = $parms{-dport} ? $parms{-dport} : 'any'; $parms{-opts} = '' if(!(ref($parms{-opts}) eq 'HASH')); $self->action( $parms{-action}); $self->proto( $parms{-proto}); $self->src( $parms{-src}); $self->sport( $parms{-sport}); $self->dir( $parms{-dir}); $self->dst( $parms{-dst}); $self->dport( $parms{-dport}); $self->opts( $parms{-opts}); } =head2 string Outputs the rule in string form. print $sr->string()."\n"; Prints "options only" string: print $sr->string(-optionsOnly => 1)."\n"; =cut sub string { my ($self,%parms) = @_; my $rule = ''; $rule = $self->action().' '.$self->proto().' '.$self->src().' '.$self->sport().' '.$self->dir().' '.$self->dst().' '.$self->dport().' (' unless($parms{-optionsOnly}); my @sort = sort { $a <=> $b } keys %{$self->opts()}; foreach my $key (@sort) { if ($self->opts->{$key}->{opt}) { $rule .= ' '.$self->opts->{$key}->{opt}; $rule .= ':'.$self->opts->{$key}->{val} if($self->opts->{$key}->{val} && $self->opts->{$key}->{val} ne '""'); $rule .= ';'; } } $rule .= ' )' unless($parms{-optionsOnly}); $rule =~ s/^ // if($parms{-optionsOnly}); return $rule; } =head2 action Sets and returns the rule action [alert,log,pass,...] $rule->action('alert'); =cut sub action { my ($self,$v) = @_; $self->{_action} = $v if(defined($v)); return $self->{_action}; } =head2 proto Sets and returns the protocol used in the rule [tcp,icmp,udp] $rule->proto('tcp'); =cut sub proto { my ($self,$v) = @_; $self->{_proto} = $v if(defined($v)); return $self->{_proto}; } =head2 src Sets and returns the source used in the rule. Make sure you use SINGLE QUOTES for variables!!! $rule->src('$EXTERNAL_NET'); =cut sub src { my ($self,$v) = @_; $self->{_src} = $v if(defined($v)); return $self->{_src}; } =head2 sport Sets and returns the source port used in the rule $rule->sport(80); =cut sub sport { my ($self,$v) = @_; $self->{_sport} = $v if(defined($v)); return $self->{_sport}; } =head2 dir Sets and returns the direction operator used in the rule, -> <- or <> $rule->dir('->'); =cut sub dir { my ($self,$v) = @_; $self->{_dir} = $v if(defined($v)); return $self->{_dir}; } =head2 dst Sets and returns the destination used in the rule $rule->dst('$HOME_NET'); $rule->dst('192.168.1.1'); =cut sub dst { my ($self,$v) = @_; $self->{_dst} = $v if(defined($v)); return $self->{_dst}; } =head2 dport Sets and returns the destination port used in the rule $rule->dport(6667); =cut sub dport { my ($self,$v) = @_; $self->{_dport} = $v if(defined($v)); return $self->{_dport}; } =head2 opts Sets an option and a value used in the rule. This currently can only be done one set at a time, and is printed in the order it was set. $rule->opts(option,value); $rule->opts('msg','this is a test rule'); This will return a hashref: $hashref->{$keyOrderValue}->{option} and $hashref->{$keyOrderValue}->{value} my $hashref = $rule->opts(); There is a fixQuotes() function that reads through this information before setting it, just to ensure the right options are sane. It's a very very basic function, but it seems to get the job done. This method will also accept HASHREF's for easier use: $rule->opts({ msg => 'test1', rev => '222', content => 'Subject|3A|', nocase => '', }); By passing an option => '', the parser will set its value to "''". When $self->string() is called, the option will be written as: option; ex: nocase => '', will result in an option output of: ...., nocase; ... =cut sub opts { my ($self,$opt,$v) = @_; if (defined($opt)) { if(ref($opt) eq 'HASH'){ foreach my $x (keys %$opt){ $opt->{$x} = fixQuotes($x,$opt->{$x}); my $pri = (keys %{$self->{_opts}}) + 1; $self->{_opts}->{$pri}->{opt} = $x; $self->{_opts}->{$pri}->{val} = $opt->{$x}; } } else { $v = fixQuotes($opt,$v); my $pri = (keys %{$self->{_opts}}) + 1; $self->{_opts}->{$pri}->{opt} = $opt; $self->{_opts}->{$pri}->{val} = $v; } } return $self->{_opts}; } =head2 opt Gets the value of the first option with a given name. $rule->opt(option); print $rule->opt('sid') . ': ' . $rule->opt('msg'); =cut sub opt { my ($self,$opt) = @_; if (defined($opt)) { my @sort = sort { $a <=> $b } keys %{$self->opts()}; foreach my $key (@sort) { return $self->opts->{$key}->{val} if($self->opts->{$key}->{opt} eq $opt); } } return undef; } # INTERNAL FUNCTIONS ( for now ) sub fixQuotes { my ($opt, $v) = @_; foreach my $option (@QUOTED_OPTIONS) { if (defined($v) && (uc($opt) eq $option)) { if (!($v =~ /^\"\S+|\s+\"$/)) { # do we have the appropriate quotes? (anchored for pcre) $v =~ s/^\"|\"$//g; # strip the quotes $v = "\"$v\""; # fix em } last; } elsif(!defined($v)) { $v = "\"\""; } } return $v; } sub parseRule { my ($self, $rule) = @_; my @r = split(/\(/,$rule,2); $r[1] =~ s/\)$//; my @meta = split(/\s+/,$r[0]); my @opts = split(/\s*;\s*/,$r[1]); $self->action( $meta[0]); $self->proto( $meta[1]); $self->src( $meta[2]); $self->sport( $meta[3]); $self->dir( $meta[4]); $self->dst( $meta[5]); $self->dport( $meta[6]); foreach my $x (@opts) { my ($opt, $v) = split(/\s*:\s*/, $x, 2); $self->opts($opt, $v); } } 1; __END__ =head1 AUTHOR Wes Young, Esaxguard9-cpan@yahoo.comE =head1 COPYRIGHT AND LICENSE Copyright (C) 2006 by Wes Young This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available. =cut