File Coverage

blib/lib/Parse/Snort.pm
Criterion Covered Total %
statement 100 103 97.0
branch 41 42 97.6
condition 2 3 66.6
subroutine 16 17 94.1
pod 8 8 100.0
total 167 173 96.5


line stmt bran cond sub pod time code
1             package Parse::Snort;
2              
3 9     9   644732 use strict;
  9         86  
  9         280  
4 9     9   53 use warnings;
  9         20  
  9         334  
5 9     9   55 use base qw(Class::Accessor);
  9         20  
  9         4746  
6 9     9   17773 use List::Util qw(first);
  9         24  
  9         936  
7 9     9   67 use Carp qw(carp);
  9         20  
  9         14463  
8              
9             our $VERSION = '0.9';
10              
11             =head1 NAME
12              
13             Parse::Snort - Parse and create Snort rules
14              
15             =head1 VERSION
16              
17             Version 0.9
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_ELEMENTS_REQUIRED = qw/ action proto src src_port direction dst dst_port /;
49             our @RULE_ELEMENTS = ( @RULE_ELEMENTS_REQUIRED, 'opts' );
50              
51             # create the accessors for the standard parts (note; opts comes later)
52             __PACKAGE__->mk_accessors(@RULE_ELEMENTS_REQUIRED);
53              
54              
55             =head1 METHODS
56              
57             These are the object methods that can be used to read or modify any part of a Snort rule. B
58              
59             If input validation is required, check out the L module.
60              
61             =head2 new ()
62              
63             Create a new C object, and return it. There are a couple of options when creating the object:
64              
65             =over 4
66              
67             =item new ( )
68              
69             Create an unpopulated object, that can be filled in using the individual rule element methods, or can be populated with the L<< parse|Parse::Snort/"PARSE" >> method.
70              
71             =item new ( $rule_string )
72              
73             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.
74              
75             $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;)'
76              
77              
78             =item new ( $rule_element_hashref )
79              
80             Create an object baesd on a prepared hash reference similar to the internal strucutre of the L object.
81              
82             $rule_element_hashref = {
83             action => 'alert',
84             proto => 'tcp',
85             src => '$EXTERNAL_NET',
86             src_port => 'any',
87             direction => '->',
88             dst => '$HOME_NET',
89             dst_port => 'any',
90             opts => [
91             ['msg' => '"perl 6 download detected\; may the world rejoice!"'],
92             ['depth' => 150],
93             ['offset' => 0],
94             ['content' => 'perl-6.0.0'],
95             ['nocase'],
96             ],
97             };
98              
99             =back
100              
101             =cut
102              
103             sub new {
104 14     14 1 2320 my ( $class, $data ) = @_;
105              
106 14         35 my $self = {
107             };
108              
109 14         36 bless $self, $class;
110 14         53 $self->_init($data);
111             }
112              
113             sub _init {
114 14     14   38 my ( $self, $data ) = @_;
115              
116             # were we passed a hashref? (formatted rule in hashref form)
117 14 100       77 if ( ref($data) eq "HASH" ) {
    100          
118             # loop through the bits and set the values
119 2         13 while ( my ( $method, $val ) = each %$data ) {
120 16         254 $self->$method($val);
121             }
122             } elsif ( defined($data) ) {
123             # otherwise, interpret this as a plain text rule.
124 4         18 $self->parse($data);
125             }
126             # nothing
127 14         120 return $self;
128             }
129              
130             =head2 parse( $rule_string )
131              
132             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.
133              
134             $rule->parse($rule_string);
135              
136             =cut
137              
138             sub parse {
139 8     8 1 101 my ( $self, $rule ) = @_;
140              
141             # nuke extra whitespace pre/post rule
142 8         35 $rule =~ s/^\s+//;
143 8         83 $rule =~ s/\s+$//;
144              
145             # Rules are distributed without being enabled
146 8 100       30 if ($rule =~ /^#/) {
147 1         5 $rule =~ s/^#+\s*//g;
148 1         3 $self->state(0);
149             }
150             else {
151 7         29 $self->state(1);
152             }
153              
154             # 20090823 RGH: m/\s+/ instead of m/ /; bug reported by Leon Ward
155 8         75 my @values = split(m/\s+/, $rule, scalar @RULE_ELEMENTS); # no critic
156              
157             # Support for 'Decoder and Preprocessor Rules'
158 8 100       30 if ($values[1] eq '(') {
159 1         3 $self->{preprocessed} = 1;
160 1         7 $self->action($values[0]);
161 1         23 shift @values;
162 1         8 $self->opts(join(' ', @values));
163             }
164             # Regular rules
165             else {
166 7         28 for my $i ( 0 .. $#values ) {
167 56         575 my $meth = $RULE_ELEMENTS[$i];
168 56         178 $self->$meth( $values[$i] );
169             }
170             }
171             }
172              
173             =head2 state
174              
175             The state of the rule: active (1) or commented (0)
176              
177             =cut
178              
179             sub state {
180 14     14 1 1272 my ($self, $state) = @_;
181              
182 14 100       42 if (defined $state) {
183 9         61 $self->{state} = $state;
184             }
185 14 100       45 if (!defined $self->{state}) {
186 2         37 return 1;
187             }
188 12         47 return $self->{state};
189             }
190              
191             =head2 METHODS FOR ACCESSING RULE ELEMENTS
192              
193             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.
194              
195             =over 4
196              
197             =item action
198              
199             The rule action. Generally one of the following: C, C, C, C, or C.
200              
201             =item proto
202              
203             The protocol of the rule. Generally one of the following: C, C, C, or C.
204              
205             =item src
206              
207             The source IP address for the rule. Generally a dotted decimal IP address, Snort $HOME_NET variable, or CIDR block notation.
208              
209             =item src_port
210              
211             The source port for the rule. Generally a static port, or a contigious range of ports.
212              
213             =item direction
214              
215             The direction of the rule. One of the following: C<< -> >> C<< <> >> or C<< <- >>.
216              
217             =item dst
218              
219             The destination IP address for the rule. Same format as C
220              
221             =item dst_port
222              
223             The destination port for the rule. Same format as C
224              
225             =item opts ( $opts_array_ref )
226              
227             =item opts ( $opts_string )
228              
229             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.
230              
231             =over 4
232              
233             =item $opts_array_ref
234              
235             $opts_array_ref = [
236             [ 'msg' => '"perl 6 download detected\; may the world rejoice!"' ],
237             [ 'depth' => 150 ],
238             [ 'offset' => 0 ],
239             [ 'content' => 'perl-6.0.0' ],
240             [ 'nocase' ],
241             ]
242              
243             =item $opts_string
244              
245             $opts_string='(msg:"perl 6 download detected\; may the world rejoice!";depth:150; offset:0; content:"perl-6.0.0"; nocase;)';
246              
247             The parenthesis surround the series of C pairs are optional.
248              
249             =back
250              
251             =cut
252              
253             sub opts {
254 27     27 1 16727 my ( $self, $args ) = @_;
255              
256 27 100       70 if ($args) {
257              
258             # setting
259 17 100       84 if ( ref($args) eq "ARRAY" ) {
260              
261             # list interface:
262             # ([depth => 50], [offset => 0], [content => "perl6"], ["nocase"])
263 7         25 $self->set( 'opts', $args );
264             } else {
265              
266             # string interface
267             # 'depth:50; offset:0; content:"perl\;6"; nocase;'
268 10 100       52 if ( $args =~ m/^\(/ ) {
269             # remove opts parens if they exist
270 8         96 $args =~ s/^\((.+)\)$/$1/;
271             }
272              
273             # When I first wrote this regex I thought it was slick.
274             # I still think that, but 2y after doing it the first time
275             # it just hurt to look at. So, /x modifier we go!
276 10         1939 my @set = map { [ split( m/\s*:\s*/, $_, 2 ) ] } $args =~ m/
  174         646  
277             \s* # ignore preceeding whitespace
278             ( # begin capturing
279             (?: # grab characters we want
280             \\. # skip over escapes
281             |
282             [^;] # or anything but a ;
283             )+? # ? greedyness hack lets the \s* actually match
284             ) # end capturing
285             \s* # ignore whitespace between value and ; or end of line
286             (?: # stop anchor at ...
287             ; # semicolon
288             | # or
289             $ # end of line
290             )
291             \s*/gx;
292 10         67 $self->set( 'opts', @set );
293             }
294             } else {
295             # getting
296 10         29 return $self->get('opts');
297             }
298             }
299              
300             sub _single_opt_accessor {
301 72     72   129 my $opt = shift;
302             return sub {
303 47     47   19377 my ( $self, $val ) = @_;
304              
305             # find the (hopefully) pre-existing option in the opts AoA
306 47         78 my $element;
307              
308 47 100       132 if ( defined $self->get('opts') ) {
309 43     556   443 $element = first { $_->[0] eq $opt } @{ $self->get('opts') };
  556         1081  
  43         113  
310             }
311              
312 47 100       219 if ( ref($element) ) {
313              
314             # preexisting
315 37 100       81 if ($val) { $element->[1] = $val; }
  10         48  
316 27         126 else { return $element->[1]; }
317             } else {
318              
319             # doesn't exist
320 10 100       22 if ($val) {
321              
322             # setting
323 8 100       19 if ( scalar $self->get('opts') ) {
324              
325             # other opts exist, tack it on the end
326             $self->set(
327             'opts',
328 6         34 @{ $self->get('opts') },
  6         11  
329             [ $opt, $val ]
330             );
331             } else {
332              
333             # blank slate, create the AoA
334 2         22 $self->set( 'opts', [ [ $opt, $val ] ] );
335             }
336             } else {
337              
338             # getting
339 2         12 return;
340             }
341             }
342             }
343 72         336 }
344              
345             # helper accessors that poke around inside rule options
346              
347             *sid = _single_opt_accessor('sid');
348             *rev = _single_opt_accessor('rev');
349             *msg = _single_opt_accessor('msg');
350             *classtype = _single_opt_accessor('classtype');
351             *gid = _single_opt_accessor('gid');
352             *metadata = _single_opt_accessor('metadata');
353             *priority = _single_opt_accessor('priority');
354             *flow = _single_opt_accessor('flow');
355              
356             =back
357              
358             =head2 HELPER METHODS FOR VARIOUS OPTIONS
359              
360             =over 4
361              
362             =item sid
363              
364             =item rev
365              
366             =item msg
367              
368             =item classtype
369              
370             =item gid
371              
372             =item metadata
373              
374             =item flow
375              
376             =item priority
377              
378             The these methods allow direct access to the rule option of the same name
379              
380             my $sid = $rule_obj->sid(); # reads the sid of the rule
381             $rule_obj->sid($sid); # sets the sid of the rule
382             ... etc ...
383              
384             =item references
385              
386             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
387              
388             [ 'reference_type' => 'reference_value' ]
389              
390             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.
391              
392              
393             $references = $rule->references(); # just the references
394             $no_references = grep { $_->[0] ne "reference" } @{ $rule->opts() }; # everything but the references
395              
396             =cut
397              
398             sub references {
399 2     2 1 7 my ($self) = shift;
400             return [
401 26         86 map { [split(m/,/, $_->[1], 2)] }
402 2         4 grep { $_->[0] eq "reference" } @{ $self->get('opts') }
  42         92  
  2         8  
403             ];
404             }
405              
406             =item as_string
407              
408             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.
409              
410              
411             =cut
412              
413             sub as_string {
414 8     8 1 4547 my $self = shift;
415 8         15 my $ret;
416             my @missing;
417              
418             # we may be incomplete
419 8 100       20 @missing = grep { $_ } map { exists( $self->{$_} ) ? undef : $_ } @RULE_ELEMENTS_REQUIRED;
  56         83  
  56         114  
420              
421             # stitch together the required bits
422 8 100       23 if (!scalar @missing) {
423             $ret .= sprintf("%s %s %s %s %s %s %s",
424 4         28 @$self{@RULE_ELEMENTS_REQUIRED});
425             }
426              
427             # tack on opts if they exist
428 8 100       28 if (defined $self->get('opts')) {
429             $ret .= sprintf(
430             " (%s)",
431             join(" ",
432 90 100       318 map { defined($_->[1]) ? "$_->[0]:$_->[1];" : "$_->[0];" }
433 6         44 @{ $self->get('opts') })
  6         15  
434             );
435             }
436              
437 8 100 66     85 return undef if @missing && !$self->{preprocessed};
438 4 100       13 return $self->state ? $ret : "# $ret";
439             }
440              
441             =pod
442              
443             =item clone
444              
445             Returns a clone of the current rule object.
446              
447             =cut
448              
449             # poor man's deep cloning. This will have to be maintained if the internal structure ever changes.
450             sub clone {
451 1     1 1 2 my $self = shift;
452              
453             # initial shallow copy
454 1         8 my $copy = bless { %$self }, ref $self;
455              
456             # deeper copy, for opts
457 1 50       3 if ($self->opts()) {
458 1         8 $copy->opts( [ map { [ @$_ ] } @{ $self->opts } ]);
  9         19  
  1         2  
459             }
460 1         7 return $copy;
461             }
462              
463             =pod
464              
465             =item reset
466              
467             Resets the internal state (deletes it!) of the current rule object, and returns the rule object itself. Useful for parsing multiple rules, one after another. Just call C<< $rule->reset() >> after you're done with the current rule, and before you C<< $rule->parse() >> or set new values via the accessor methods.
468              
469             =back
470              
471             =cut
472              
473             sub reset {
474 0     0 1   my $self = shift;
475 0           delete $self->{$_} for keys %$self;
476 0           return $self;
477             }
478              
479             =head1 AUTHOR
480              
481             Richard G Harman Jr, C<< >>
482              
483             =head1 BUGS
484              
485             Please report any bugs or feature requests to
486             C, or through the web interface at
487             L.
488             I will be notified, and then you' ll automatically be notified of progress on your bug as I make changes.
489              
490             =head1 SUPPORT
491              
492             You can find documentation for this module with the perldoc command.
493              
494             perldoc Parse::Snort
495              
496             You can also look for information at:
497              
498             =over 4
499              
500             =item * AnnoCPAN: Annotated CPAN documentation
501              
502             L
503              
504             =item * CPAN Ratings
505              
506             L
507              
508             =item * RT: CPAN's request tracker
509              
510             L
511              
512             =item * Search CPAN
513              
514             L
515              
516             =back
517              
518             =head1 DEPENDENCIES
519              
520             L, L, L, L
521              
522             =head1 ACKNOWLEDGEMENTS
523              
524             MagNET #perl for putting up with me :)
525              
526             =head1 COPYRIGHT & LICENSE
527              
528             Copyright 2007 Richard Harman, all rights reserved.
529              
530             This program is free software; you can redistribute it and/or modify it
531             under the same terms as Perl itself.
532              
533             =cut
534              
535             !!'mtfnpy!!';