File Coverage

blib/lib/Parse/Snort.pm
Criterion Covered Total %
statement 78 78 100.0
branch 30 30 100.0
condition n/a
subroutine 14 14 100.0
pod 5 5 100.0
total 127 127 100.0


line stmt bran cond sub pod time code
1             package Parse::Snort;
2              
3 5     5   247917 use strict;
  5         15  
  5         445  
4 5     5   31 use warnings;
  5         10  
  5         170  
5 5     5   31 use base qw(Class::Accessor);
  5         16  
  5         11847  
6 5     5   15519 use List::Util qw(first);
  5         12  
  5         731  
7 5     5   37 use Carp qw(carp);
  5         16  
  5         14175  
8              
9             our $VERSION = '0.6';
10              
11             =head1 NAME
12              
13             Parse::Snort - Parse and create Snort rules
14              
15             =head1 VERSION
16              
17             Version 0.05
18              
19             =head1 SYNOPSIS
20              
21             use Parse::Snort;
22              
23             my $rule = Parse::Snort->new(
24             action => 'alert',
25             proto => 'tcp',
26             src => '$HOME_NET', src_port => 'any',
27             direction => '->'
28             dst =>'$EXTERNAL_NET', dst_port => 'any'
29             );
30              
31             $rule->action("pass");
32              
33             $rule->opts(
34             [ 'depth' => 50 ],
35             [ 'offset' => 0 ],
36             [ 'content' => "perl6" ],
37             [ "nocase" ]
38             );
39              
40             my $rule = Parse::Snort->new();
41             $rule->parse('pass tcp $HOME_NET any -> $EXTERNAL_NET 6667;');
42             $rule->msg("IRC server");
43             my $rule_string = $rule->as_string;
44             );
45              
46             =cut
47              
48             our @RULE_ACTIONS = qw/ alert pass drop sdrop log activate dynamic reject /;
49             our @RULE_ELEMENTS_REQUIRED = qw/ action proto src src_port direction dst dst_port /;
50             our @RULE_ELEMENTS = ( @RULE_ELEMENTS_REQUIRED, 'opts' );
51              
52             # create the accessors for the standard parts (note; opts comes later)
53             __PACKAGE__->mk_accessors(@RULE_ELEMENTS_REQUIRED);
54              
55              
56             =head1 METHODS
57              
58             These are the object methods that can be used to read or modify any part of a Snort rule. B
59              
60             =for comment If input validation is required, check out the L module.
61              
62             =over 4
63              
64             =item new ()
65              
66             Create a new C object, and return it. There are a couple of options when creating the object:
67              
68             =over 4
69              
70             =item new ( )
71              
72             Create an unpopulated object, that can be filled in using the individual rule element methods, or can be populated with the L<< parse|/"PARSE" >> method.
73              
74             =item new ( $rule_string )
75              
76             Create an object based on a plain text Snort rule, all on one line. This module doesn't understand the UNIX style line continuations (a backslash at the end of the line) that Snort does.
77              
78             $rule_string = 'alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"perl 6 download detected\; may the world rejoice!";depth:150; offset:0; content:"perl-6.0.0"; nocase;)'
79              
80              
81             =item new ( $rule_element_hashref )
82              
83             Create an object baesd on a prepared hash reference similar to the internal strucutre of the L object.
84              
85             $rule_element_hashref = {
86             action => 'alert',
87             proto => 'tcp',
88             src => '$EXTERNAL_NET', src_port => 'any',
89             direction => '->',
90             dst => '$HOME_NET', dst_port => 'any',
91             opts => [
92             [ 'msg' => '"perl 6 download detected\; may the world rejoice!"' ],
93             [ 'depth' => 150 ],
94             [ 'offset' => 0 ].
95             [ 'content' => 'perl-6.0.0' ],
96             [ 'nocase' ],
97             ],
98            
99             };
100              
101             =back
102              
103             =cut
104              
105             sub new {
106 8     8 1 2646 my ( $class, $data ) = @_;
107              
108 8         22 my $self = {
109             };
110              
111 8         27 bless $self, $class;
112 8         33 $self->_init($data);
113             }
114              
115             =for comment
116             The _init method is called by the new method, to figure out what sort of data was passed to C. If necessary, it calls $self->parse(), individual element accessor methods, or simply returns $self.
117              
118             =cut
119              
120              
121             sub _init {
122 8     8   21 my ( $self, $data ) = @_;
123              
124             # were we passed a hashref? (formatted rule in hashref form)
125 8 100       52 if ( ref($data) eq "HASH" ) {
    100          
126             # loop through the bits and set the values
127 2         14 while ( my ( $method, $val ) = each %$data ) {
128 16         275 $self->$method($val);
129             }
130             } elsif ( defined($data) ) {
131             # otherwise, interpret this as a plain text rule.
132 1         4 $self->parse($data);
133             }
134             # nothing
135 8         91 return $self;
136             }
137              
138             =item parse( $rule_string )
139              
140             The parse method is what interprets a plain text rule, and populates the rule object. Beacuse this module does not support the UNIX style line-continuations (backslash at the end of a line) the rule must be all on one line, otherwise the parse will fail in unpredictably interesting and confusing ways. The parse method tries to interpret the rule from left to right, calling the individual accessor methods for each rule element. This will overwrite the contents of the object (if any), so if you want to parse multiple rules at once, you will need multiple objects.
141              
142             $rule->parse($rule_string);
143              
144             =cut
145              
146             sub parse {
147 2     2 1 9 my ( $self, $rule ) = @_;
148              
149             # nuke extra whitespace pre/post rule
150 2         12 $rule =~ s/^\s+//;
151 2         26 $rule =~ s/\s+$//;
152              
153             # 20090823 RGH: m/\s+/ instead of m/ /; bug reported by Leon Ward
154 2         16 my @values = split( m/\s+/, $rule, scalar @RULE_ELEMENTS ); # no critic
155              
156 2         12 for my $i ( 0 .. $#values ) {
157 16         184 my $meth = $RULE_ELEMENTS[$i];
158 16         75 $self->$meth( $values[$i] );
159             }
160             }
161              
162             =back
163              
164             =head2 METHODS FOR ACCESSING RULE ELEMENTS
165              
166             You can access the core parts of a rule (action, protocol, source IP, etc) with the method of their name. These are read/write L accessors. If you want to read the value, don't pass an argument. If you want to set the value, pass in the new value. In either case it returns the current value, or undef if the value has not been set yet.
167              
168             =for comment Need to figure out "truth" again in perl sense, do I simply "return;" or "return undef" if the value doesn't exist? For Parse::Snort::Strict, I need to have two things: 1) make it known to the user that the rule failed to parse, 2) which (may?) be a different meaning than the rule element being empty/undefined.
169              
170             =over 4
171              
172             =item action
173              
174             The rule action. Generally one of the following: C, C, C, C, or C.
175              
176             =item proto
177              
178             The protocol of the rule. Generally one of the following: C, C, C, or C.
179              
180             =item src
181              
182             The source IP address for the rule. Generally a dotted decimal IP address, Snort $HOME_NET variable, or CIDR block notation.
183              
184             =item src_port
185              
186             The source port for the rule. Generally a static port, or a contigious range of ports.
187              
188             =item direction
189              
190             The direction of the rule. One of the following: C<->> C<<>> or C<<->.
191              
192             =item dst
193              
194             The destination IP address for the rule. Same format as C
195              
196             =item dst_port
197              
198             The destination port for the rule. Same format as C
199              
200             =item opts ( $opts_array_ref )
201              
202             =item opts ( $opts_string )
203              
204             The opts method can be used to read existing options of a parsed rule, or set them. The method takes two forms of arguments, either an Array of Arrays, or a rule string.
205              
206             =over 4
207              
208             =item $opts_array_ref
209              
210             $opts_array_ref = [
211             [ 'msg' => '"perl 6 download detected\; may the world rejoice!"' ],
212             [ 'depth' => 150 ],
213             [ 'offset' => 0 ].
214             [ 'content' => 'perl-6.0.0' ],
215             [ 'nocase' ],
216             ]
217              
218             =item $opts_string
219              
220             $opts_string='(msg:"perl 6 download detected\; may the world rejoice!";depth:150; offset:0; content:"perl-6.0.0"; nocase;)';
221              
222             The parenthesis surround the series of C pairs are optional.
223              
224             =back
225              
226             =cut
227              
228             sub opts {
229 18     18 1 33024 my ( $self, $args ) = @_;
230              
231 18 100       49 if ($args) {
232              
233             # setting
234 10 100       40 if ( ref($args) eq "ARRAY" ) {
235              
236             # list interface:
237             # ([depth => 50], [offset => 0], [content => "perl6"], ["nocase"])
238 6         30 $self->set( 'opts', $args );
239             } else {
240              
241             # string interface
242             # 'depth:50; offset:0; content:"perl\;6"; nocase;'
243 4 100       29 if ( $args =~ m/^\(/ ) {
244             # remove opts parens if they exist
245 2         22 $args =~ s/^\((.+)\)$/$1/;
246             }
247              
248             # When I first wrote this regex I thought it was slick.
249             # I still think that, but 2y after doing it the first time
250             # it just hurt to look at. So, /x modifier we go!
251 4         1145 my @set = map { [ split( m/\s*:\s*/, $_, 2 ) ] } $args =~ m/
  69         304  
252             \s* # ignore preceeding whitespace
253             ( # begin capturing
254             (?: # grab characters we want
255             \\. # skip over escapes
256             |
257             [^;] # or anything but a ;
258             )+? # ? greedyness hack lets the \s* actually match
259             ) # end capturing
260             \s* # ignore whitespace between value and ; or end of line
261             (?: # stop anchor at ...
262             ; # semicolon
263             | # or
264             $ # end of line
265             )
266             \s*/gx;
267 4         36 $self->set( 'opts', @set );
268             }
269             } else {
270             # getting
271 8         34 return $self->get('opts');
272             }
273             }
274              
275             sub _single_opt_accessor {
276 35     35   168 my $opt = shift;
277             return sub {
278 22     22   8125 my ( $self, $val ) = @_;
279              
280             # find the (hopefully) pre-existing option in the opts AoA
281 22         31 my $element;
282              
283 22 100       69 if ( defined $self->get('opts') ) {
284 18     214   187 $element = first { $_->[0] eq $opt } @{ $self->get('opts') };
  214         433  
  18         54  
285             }
286              
287 22 100       120 if ( ref($element) ) {
288              
289             # preexisting
290 12 100       25 if ($val) { $element->[1] = $val; }
  8         32  
291 4         28 else { return $element->[1]; }
292             } else {
293              
294             # doesn't exist
295 10 100       25 if ($val) {
296              
297             # setting
298 8 100       26 if ( scalar $self->get('opts') ) {
299              
300             # other opts exist, tack it on the end
301 6         17 $self->set(
302             'opts',
303 6         42 @{ $self->get('opts') },
304             [ $opt, $val ]
305             );
306             } else {
307              
308             # blank slate, create the AoA
309 2         27 $self->set( 'opts', [ [ $opt, $val ] ] );
310             }
311             } else {
312              
313             # getting
314 2         13 return;
315             }
316             }
317             }
318 35         175 }
319              
320             # helper accessors that poke around inside rule options
321              
322             *sid = _single_opt_accessor('sid');
323             *rev = _single_opt_accessor('rev');
324             *msg = _single_opt_accessor('msg');
325             *classtype = _single_opt_accessor('classtype');
326             *gid = _single_opt_accessor('gid');
327             *metadata = _single_opt_accessor('metadata');
328             *priority = _single_opt_accessor('priority');
329              
330             =back
331              
332             =head2 HELPER METHODS FOR VARIOUS OPTIONS
333              
334             =over 4
335              
336             =item sid
337              
338             =item rev
339              
340             =item msg
341              
342             =item classtype
343              
344             =item gid
345              
346             =item metadata
347              
348             =item priority
349              
350             The these methods allow direct access to the rule option of the same name
351              
352             my $sid = $rule_obj->sid(); # reads the sid of the rule
353             $rule_obj->sid($sid); # sets the sid of the rule
354             ... etc ...
355              
356             =item references
357              
358             The C method permits read-only access to the C options in the rule. This is in the form of an array of arrays, with each reference in the format
359              
360             [ 'reference_type' => 'reference_value' ]
361              
362             To modify references, use the C method to grab all the rule options, modify it to your needs, and use the C method to save your changes back to the rule object.
363              
364              
365             $references = $rule->references(); # just the references
366             $no_references = grep { $_->[0] != "reference" } @{ $rule->opts() }; # everything but the references
367              
368             =cut
369              
370             sub references {
371 2     2 1 6 my ($self) = shift;
372 26         101 my @references =
373 42         87 map { [ split( m/,/, $_->[1], 2 ) ] }
374 2         7 grep { $_->[0] eq "reference" } @{ $self->get('opts') };
  2         10  
375 2         55 return \@references;
376             }
377              
378             =item as_string
379              
380             The C method returns a string that matches the normal Snort rule form of the object. This is what you want to use to write a rule to an output file that will be read by Snort.
381              
382             =cut
383              
384             sub as_string {
385 6     6 1 989 my $self = shift;
386 6         10 my $ret;
387             my @missing;
388              
389             # we may be incomplete
390 6 100       18 @missing = grep { $_ } map { exists( $self->{$_} ) ? undef : $_ } @RULE_ELEMENTS_REQUIRED;
  42         68  
  42         115  
391              
392             # stitch together the required bits
393 6 100       31 if (! scalar @missing)
394 2         18 { $ret .= sprintf( "%s %s %s %s %s %s %s", @$self{@RULE_ELEMENTS_REQUIRED} ); }
395              
396             # tack on opts if they exist
397 6 100       32 if ( defined $self->get('opts') )
398 4 100       36 { $ret .= sprintf( " (%s)", join( " ", map { defined($_->[1]) ? "$_->[0]:$_->[1];" : "$_->[0];" } @{ $self->get('opts') } )); }
  48         230  
  4         12  
399              
400             #carp sprintf( "Missing required rule element(s): %s", join( " ", @missing )) if (scalar @missing);
401 6 100       59 return ! scalar @missing ? $ret : undef;
402             }
403              
404             =back
405              
406             =head1 AUTHOR
407              
408             Richard G Harman Jr, C<< >>
409              
410             =head1 BUGS
411              
412             Please report any bugs or feature requests to
413             C, or through the web interface at
414             L.
415             I will be notified, and then you' ll automatically be notified of progress on your bug as I make changes.
416              
417             =head1 SUPPORT
418              
419             You can find documentation for this module with the perldoc command.
420              
421             perldoc Parse::Snort
422              
423             You can also look for information at:
424              
425             =over 4
426              
427             =item * AnnoCPAN: Annotated CPAN documentation
428              
429             L
430              
431             =item * CPAN Ratings
432              
433             L
434              
435             =item * RT: CPAN's request tracker
436              
437             L
438              
439             =item * Search CPAN
440              
441             L
442              
443             =back
444              
445             =head1 DEPENDENCIES
446              
447             L, L, L
448              
449             =head1 ACKNOWLEDGEMENTS
450              
451             MagNET #perl for putting up with me :)
452              
453             =head1 COPYRIGHT & LICENSE
454              
455             Copyright 2007 Richard Harman, all rights reserved.
456              
457             This program is free software; you can redistribute it and/or modify it
458             under the same terms as Perl itself.
459              
460             =cut
461              
462             !!'mtfnpy!!';