File Coverage

blib/lib/Parse/YARA.pm
Criterion Covered Total %
statement 344 394 87.3
branch 150 188 79.7
condition 37 45 82.2
subroutine 37 37 100.0
pod 20 20 100.0
total 588 684 85.9


line stmt bran cond sub pod time code
1             package Parse::YARA;
2              
3 1     1   27373 use strict;
  1         4  
  1         45  
4 1     1   6 use warnings;
  1         2  
  1         37  
5 1     1   5 use Carp qw(carp);
  1         14  
  1         92  
6 1     1   1013 use Tie::IxHash;
  1         6656  
  1         32  
7 1     1   9 use File::Basename;
  1         2  
  1         21953  
8              
9             our $VERSION = '0.02';
10              
11             =head1 NAME
12              
13             Parse::YARA - Parse and create YARA rules
14              
15             =head1 VERSION
16              
17             Version 0.02
18              
19             =head1 SYNOPSIS
20              
21             use Parse::YARA;
22            
23             my $rule_string = 'rule sample_rule : sample
24             {
25             meta:
26             info = "sample rule"
27            
28             strings:
29             $ = "anon1"
30             $ = "anon2"
31             $test_string = "test_string"
32            
33             condition:
34             any of them
35             }';
36             my $rule_string_obj = Parse::YARA->new();
37             $rule_string_obj->parse($rule_string);
38             $rule_string_obj->add_string_modifier('sample_rule', '$test_string', 'all');
39             print $rule_string_obj->as_string;
40            
41             my $rule_element_hashref = {
42             modifier => 'private',
43             rule_id => 'sample_hash_rule',
44             tag => [
45             'tag1',
46             'tag2'
47             ],
48             meta => {
49             'info' => 'sample hash rule',
50             'site' => 'http://search.cpan.org/~moofu/'
51             },
52             strings => {
53             '$' => {
54             value => 'anon1',
55             type => 'text',
56             },
57             '$$' => {
58             value => 'anon2',
59             type => 'text'
60             },
61             '$test_string' => {
62             value => 'test_string',
63             type => 'text'
64             },
65             '$test_hex_string' => {
66             value => '{ AA BB CC DD }',
67             type => 'hex'
68             },
69             '$test_regex_string' => {
70             value => '/.*/',
71             type => 'regex'
72             },
73             },
74             condition => 'true'
75             };
76             my $rule_hash_obj = Parse::YARA->new(rulehash => $rule_element_hashref);
77             print $rule_hash_obj->as_string;
78            
79             my $rule_file = '/path/to/rules.yar';
80             my $rule_file_obj = Parse::YARA->new(file => $rule_file);
81            
82             my $rule_obj = Parse::YARA->new();
83             $rule_obj->set_rule_modifier('new_rule', 'global');
84             $rule_obj->set_condition('new_rule', 'one of them');
85             $rule_obj->add_tag('new_rule', 'test_only');
86             $rule_obj->add_meta('new_rule', 'author', 'Leigh');
87             $rule_obj->add_meta('new_rule', 'site', 'http://search.cpan.org/~moofu/');
88             $rule_obj->add_anonymous_string('new_rule', 'anonymous', 'text');
89             $rule_obj->add_string('new_rule', '$string1', 'A test string', 'text');
90             $rule_obj->add_string('new_rule', '$string2', 'Another example', 'text');
91             $rule_obj->add_string_modifier('new_rule', '$string1', 'ascii');
92             print $rule_obj->as_string;
93            
94             $rule_obj->modify_meta('new_rule', 'author', 'Leigh Thompson');
95             $rule_obj->modify_string('new_rule', '$string1', 'An example string');
96             print $rule_obj->as_string;
97            
98             $rule_obj->remove_string_modifier('new_rule', '$string1', 'ascii');
99             $rule_obj->remove_tag('new_rule', 'test_only');
100             $rule_obj->remove_meta('new_rule', 'site');
101             $rule_obj->remove_anonymous_string('new_rule', 'anonymous');
102             $rule_obj->remove_string('new_rule', '$string2');
103             print $rule_obj->as_string;
104              
105             =head1 NOTE FOR PERL >= 5.18
106              
107             Hash order will not be guaranteed so the use of Tie::IxHash is required for passing hashes into the module if order within the YARA rule is required.
108              
109             For the example given above, the following steps would need to be taken:
110              
111             use Tie::IxHash;
112             my $rule_element_hashref;
113             my $rule_element_hashref_knot = tie(%{$rule_element_hashref}, 'Tie::IxHash');
114             my $meta_hashref;
115             my $meta_hashref_knot = tie(%{$meta_hashref}, 'Tie::IxHash');
116             my $strings_hashref;
117             my $strings_hashref_knot = tie(%{$strings_hashref}, 'Tie::IxHash');
118             $meta_hashref->{info} = 'sample hash rule';
119             $meta_hashref->{site} = 'http://search.cpan.org/~moofu/';
120             $strings_hashref->{'$'} = { value => 'anon1', type => 'text' };
121             $strings_hashref->{'$$'} = { value => 'anon2', type => 'text' };
122             $strings_hashref->{'$test_string'}= { value => 'test_string', type => 'text' };
123             $strings_hashref->{'$test_hex_string'} = { value => '{ AA BB CC DD }', type => 'hex' };
124             $strings_hashref->{'$test_regex_string'} = { value => '/.*/', type => 'regex' };
125             $rule_element_hashref = {
126             modifier => 'private',
127             rule_id => 'sample_hash_rule_tied',
128             tag => [
129             'tag1',
130             'tag2'
131             ],
132             meta => $meta_hashref,
133             strings => $strings_hashref,
134             condition => 'true'
135             };
136              
137             =cut
138              
139             # Set some reserved words
140             our @RESERVED_ARRAY = qw/ all in and include any index ascii indexes at int8 condition int16 contains int32 entrypoint matches false meta filesize nocase fullword not for or global of private rule rva section strings them true uint8 uint16 uint32 wide /;
141             our %RESERVED_WORDS = map { $_ => 1 } @RESERVED_ARRAY;
142              
143             =head1 METHODS
144              
145             These are the object methods that can be used to read, add or modify any part of a YARA rule.
146              
147             =over
148              
149             =item new()
150              
151             Create a new C object, and return it. There are a couple of options when creating the object:
152              
153             =over 4
154              
155             =item new(disable_includes => 0, $verbose => 0)
156              
157             Create an unpopulated object, that can be filled in using the individual rule element methods, or can be populated with the read_file method.
158              
159             =item new(rule => $rule, disable_includes => 0, $verbose => 0)
160              
161             Create an object by providing a YARA rule as a string value.
162              
163             =item new(file => $file, $disable_includes => 0, $verbose => 0)
164              
165             Parse a file containing one or more YARA rules and create objects for each rule.
166              
167             The include option is turned on by default, this will ensure that all files included in the file being parsed are read in by the parser. Turn this off by setting disable_includes => 1.
168              
169             =item new(rulehash => $rule_element_hashref, $disable_includes => 0, $verbose => 0)
170              
171             Create an object based on a prepared hash reference.
172              
173             my $rule_element_hashref = {
174             modifier => 'global',
175             rule_id => 'sample_hash_rule',
176             tag => [
177             'tag1',
178             'tag2'
179             ],
180             meta => {
181             'info' => 'sample hash rule',
182             'site' => 'http://search.cpan.org/~moofu/'
183             },
184             strings => {
185             '$' => {
186             value => 'anon1',
187             type => 'text',
188             },
189             '$$' => {
190             value => 'anon2',
191             type => 'text'
192             },
193             '$test_string' => {
194             value => 'test_string',
195             type => 'text'
196             },
197             '$test_hex_string' => {
198             value => '{ AA BB CC DD }',
199             type => 'hex'
200             },
201             '$test_regex_string' => {
202             value => '/.*/',
203             type => 'regex'
204             },
205             },
206             condition => 'all of them'
207             };
208              
209             =cut
210              
211             sub new {
212 8     8 1 2958 my ($class, %args) = @_;
213 8         20 my $self = {};
214              
215 8         25 bless($self, $class);
216 8         41 $self->_init(%args);
217 8         32 return $self;
218             }
219              
220             # _init
221             #
222              
223             sub _init {
224 8     8   23 my ($self, %args) = @_;
225              
226             # Turn on verbose if requested
227 8 50       31 if($args{verbose}) {
228 0         0 $self->{verbose} = 1;
229 0         0 delete($args{verbose});
230             }
231              
232             # Turn on includes if requested
233 8 100       26 if(!$args{disable_includes}) {
234 6         23 $self->{include} = 1;
235 6         13 delete($args{disable_includes});
236             }
237              
238             # Tie a hash to contain all rules so we can assure order
239 8         12 $self->{rules_knot} = tie(%{$self->{rules}}, 'Tie::IxHash');
  8         59  
240              
241             # Check what we were passed and behave appropriately
242 8 100       147 if($args{rule}) {
    100          
    100          
243 1         5 $self->parse($args{rule});
244             } elsif($args{file}) {
245             # This in turn will call $self->parse on the contents of the file
246 2         9 $self->read_file($args{file});
247             } elsif($args{rulehash}) {
248             # We can't proceed without a rule_id so make sure it's set
249             # then add it to the new rules object
250 1 50       6 if($args{rulehash}->{rule_id}) {
251 1         4 my $rule_id = $args{rulehash}->{rule_id};
252 1         3 delete($args{rulehash}->{rule_id});
253             # Loop through the remaining components of the rulehash
254             # and add them to the rules object
255 1         9 foreach my $key (keys(%{$args{rulehash}})) {
  1         5  
256 5         56 my $sub = "_$key";
257 5         22 $self->$sub($rule_id, $args{rulehash}->{$key});
258             }
259             } else {
260 0         0 carp("Cannot add rule with no rule_id\n");
261             }
262             }
263              
264 8         98 return $self;
265             }
266              
267             # _modifier
268             # Adds any modifiers passed to new() as a hashref
269             # by calling set_rule_modifier
270              
271             sub _modifier {
272 1     1   3 my ($self, $rule_id, $modifier) = @_;
273              
274 1         5 $self->set_rule_modifier($rule_id, $modifier);
275             }
276              
277             # _tag
278             # Adds any tags passed to new() as a hashref
279             # by calling add_tag for each tag in the array
280              
281             sub _tag {
282 1     1   2 my ($self, $rule_id, $tags) = @_;
283              
284 1         3 foreach(@{$tags}) {
  1         3  
285 2         20 $self->add_tag($rule_id, $_);
286             }
287             }
288              
289             # _meta_
290             # Adds any meta attributes passed to new() as a hashref
291             # by calling add_meta for each meta name/value in the hash
292              
293             sub _meta {
294 1     1   2 my ($self, $rule_id, $meta_hash) = @_;
295              
296 1         2 foreach my $meta_name (keys(%{$meta_hash})) {
  1         7  
297 2         49 $self->add_meta($rule_id, $meta_name, $meta_hash->{$meta_name});
298             }
299             }
300              
301             # _strings
302             # Adds any string attributes passed to new() as a hashref
303             # by calling add_anonymous_string or add_string as appropriate
304             # for each string name/value in the hash
305              
306             sub _strings {
307 1     1   3 my ($self, $rule_id, $string_hash) = @_;
308              
309             # We need to make sure the strings hash element exists
310             #if(!$self->{rules}->{$rule_id}->{strings}) {
311             # $self->{rules}->{$rule_id}->{strings} = undef;
312             #}
313              
314             # Loop through each string in the hashref
315 1         2 foreach my $string_name (keys(%{$string_hash})) {
  1         5  
316             # If we find an array we need to loop through that too
317 2 50       49 if(ref($string_hash->{$string_name}) eq "ARRAY") {
318 0         0 foreach(@{$string_hash->{$string_name}}) {
  0         0  
319             # We can add multiple anonymous strings so look for them then add them
320 0 0       0 if($string_name eq "\$") {
321 0         0 $self->add_anonymous_string($rule_id, $_->{value}, $_->{type});
322             # Otherwise we need to bail as we can't add multiple strings with the same string name
323             } else {
324 0         0 carp("$rule_id: error parsing strings, found multiple strings with name $string_name");
325             }
326             }
327             } else {
328             # For unique string names, simply add the string
329 2         23 $self->add_string($rule_id, $string_name, $string_hash->{$string_name}->{value}, $string_hash->{$string_name}->{type});
330             }
331             }
332             }
333              
334             # _condition
335             # Adds the condition passed to new() as a hashref
336             # by calling set_condition
337              
338             sub _condition {
339 1     1   2 my ($self, $rule_id, $condition) = @_;
340              
341 1         4 $self->set_condition($rule_id, $condition);
342             }
343              
344             # _is_valid
345             # Checks to see if a word is valid given context
346              
347             sub _is_valid {
348 512     512   4672 my ($self, $str, $type) = @_;
349 512         648 my $valid = 1;
350              
351 512 50       808 if($str) {
352 512 100       1336 if($type eq "rule_id") {
    50          
353             # Can contain any alphanumeric character and the underscore character, but the first character can not be a digit
354 170 50       848 if($str =~ /^\d/) {
    50          
355 0         0 carp("$str: rule_id cannot start with a digit");
356 0         0 $valid = 0;
357             } elsif($str !~ /^[a-zA-Z0-9_]+$/) {
358 0         0 carp("$str: rule_id can only contain alphanumeric and underscore characters");
359 0         0 $valid = 0;
360             }
361             } elsif($type eq "string_name") {
362             # Must start with a $
363 342 100       1114 if($str !~ /^\$/) {
364 1         102 carp("String identifier must start with a \$: $str");
365 1         130 $valid = 0;
366             }
367             }
368             } else {
369 0         0 carp("Can't check empty string");
370 0         0 $valid = 0;
371             }
372              
373 512         1344 return $valid;
374             }
375              
376             # _check_reserved
377             # Checks to see if a word is reserved and exits if one is found
378              
379             sub _check_reserved {
380 381     381   575 my ($self, $str, $type) = @_;
381 381         373 my $reserved;
382              
383 381 50       709 if($str) {
384             map {
385 381 100       962 if(exists($RESERVED_WORDS{$_})) {
  851         4464  
386             # These lists are through trial and error
387 243 100 66     2838 if($type eq "meta" and $_ !~ /^(?:include|indexes)$/) {
    100 100        
    100          
388 1         4 $reserved = 1;
389             } elsif($type eq "condition" and /^(?:include|ascii|condition|meta|nocase|fullword|strings|wide)$/) {
390 1         22 $reserved = 1;
391             } elsif($type !~ /^(?:meta|condition)$/) {
392 1         6 $reserved = 1;
393             }
394             }
395             } split(/\s+/, $str);
396             } else {
397 0         0 carp("Can't check empty string\n");
398             }
399              
400 381         2326 return $reserved;
401             }
402              
403             =back
404              
405             =item parse($rule_string)
406              
407             Reads in a string of one or more rules and parses it into a hashref that can be manipulated by the other functions.
408              
409             =cut
410              
411             sub parse {
412 10     10 1 42 my ($self, $rule_string) = @_;
413 10         18 my $modifier;
414             my $rule_id;
415 0         0 my $tags;
416 10         13 my $position = 1;
417 10         19 my $rule_data = {};
418 10         17 my $knot = tie(%{$rule_data}, 'Tie::IxHash');
  10         53  
419              
420             # Strip comments, I have replaced the comments with a newline as otherwise it was stripping the newline, this hasn't broken anything so far.
421             # For an explanation, see: http://perldoc.perl.org/perlfaq6.html#How-do-I-use-a-regular-expression-to-strip-C-style-comments-from-a-file%3F
422 10 100       281 $rule_string =~ s#/\*[^*]*\*+([^/*][^*]*\*+)*/|//([^\\]|[^\n][\n]?)*?\n|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $3 ? $3 : "\n"#gse;
  534         3771  
423 10         126 $rule_string =~ s/\n\/\/.*//g;
424              
425             # Tidy up any strings that come in with strange formatting
426             # Rules with the close brace for previous rule on the same line
427 10         807 $rule_string =~ s/\n\s*}\s*(rule.*)/\n}\n$1/g;
428             # String / Meta names on one line but values on the next
429 10         21306 $rule_string =~ s/\s*(\S+)\s*=\s*\n\s*(\S+)/\n\t\t$1 = $2\n/g;
430             # Multiple strings on the same line
431 10         86 $rule_string =~ s/(\/)(\$\S+\s*=)/$1\n\t\t$2/g;
432 10         77 $rule_string =~ s/(")(\$\S+\s*=)/$1\n\t\t$2/g;
433 10         80 $rule_string =~ s/(})(\$\S+\s*=)/$1\n\t\t$2/g;
434              
435             # Parse the rule line by line
436 10         64 while($rule_string =~ /([^\n]+\n)?/g) {
437 1582         14397 my $line = $1;
438              
439             # Need to find a rule_id before we can start
440 1582 100 100     17599 if($line and $line =~ /^(?:(global|private)\s+)?rule\s+([a-zA-Z0-9_]+)(?:\s*:\s*([^{]*))?\s*({.*})?/) {
    100          
    100          
441 170         263 chomp($line);
442 170         300 $rule_id = $2;
443 170         643 $rule_data->{$rule_id}->{modifier} = $1;
444 170         4112 $rule_data->{$rule_id}->{tags} = $3;
445             # Make sure we don't set the rule_id to a YARA reserved word
446 170 50       1567 if($self->_check_reserved($rule_id, 'rule_id')) {
    50          
447 0         0 carp("Cannot use reserved word as rule identifier: $rule_id");
448 0         0 next;
449             } elsif(!$self->_is_valid($rule_id, 'rule_id')) {
450             # Or to an invalid one
451 0         0 next;
452             }
453              
454 170         674 $rule_data->{$rule_id}->{raw} = '';
455             # If $4 exists, we have found a single line rule so add all the data to raw
456 170 50       1944 if($4) {
457 0         0 $rule_data->{$rule_id}->{raw} = $4;
458             }
459             # Because their is no rule_id set we can't
460             # add the line to the rule_data
461             } elsif(!$rule_id) {
462 26         113 next;
463             # Now we have a rule_id, add the current
464             # line to the rule_data ready for parsing
465             } elsif($line) {
466 1236         4938 $rule_data->{$rule_id}->{raw} .= $line;
467             }
468             }
469              
470             # Extract meta, strings and conditions from
471             # each rule and add it to the hashref
472 10         17 foreach my $rule (keys(%{$rule_data})) {
  10         60  
473             # Tidy up the raw rule string to make sure we can easily parse this
474             # line by line
475 170         1574 $rule_data->{$rule}->{raw} =~ s/(strings:|meta:|condition:)/\n\t$1\n\t\t/g;
476 170         3628 $rule_data->{$rule}->{raw} =~ s/}\s*$/\n}/;
477 170         2616 $self->_parse_meta($rule, $rule_data->{$rule}->{raw});
478 170         944 $self->_parse_strings($rule, $rule_data->{$rule}->{raw});
479 170         814 $self->_parse_condition($rule, $rule_data->{$rule}->{raw});
480 170 100       2784 if($rule_data->{$rule}->{modifier}) {
481 10         91 $self->set_rule_modifier($rule, $rule_data->{$rule}->{modifier});
482             }
483              
484             # If we found any tags add each one as an element
485             # of an array to the tags key
486 170 100       1576 if($rule_data->{$rule}->{tags}) {
487 18         156 foreach(split(/\s+/, $rule_data->{$rule}->{tags})) {
488 28         243 $self->add_tag($rule, $_);
489             }
490             }
491             # This is useful for testing
492 170 50       1480 if($self->{verbose}) {
493 0         0 print "Added rule: $rule";
494 0 0 0     0 if($self->{rules}->{$rule}->{tags} and scalar($self->{rules}->{$rule}->{tags}) > 0) {
495 0         0 print " :";
496 0         0 foreach my $tag (@{$self->{rules}->{$rule}->{tags}}) {
  0         0  
497 0         0 print " $tag";
498             }
499             }
500 0         0 print "\n";
501             }
502             }
503             }
504              
505             =item read_file ( $file )
506              
507             Reads in a YARA rules file and any included files (if not disabled) and calls $self->parse() on the contents of the file.
508              
509             =cut
510              
511             sub read_file {
512 8     8 1 24 my ($self, $file) = @_;
513 8         13 my $rules = "";
514 8         13 my @include_files;
515              
516 8 50       29 if($self->{verbose}) { print "Parsing file: $file\n" };
  0         0  
517              
518 8 50       627 open(RULESFILE, "<", $file) or die $!;
519             # Loop through rules file and find all YARA rules
520 8         285 while() {
521             # If we are including files, push to an array so we can
522             # read them all in later
523 1720 100 100     7649 if($self->{include} and /^include\s+"(.*?)"/) {
    100          
524 4         208 push(@include_files, dirname($file) . "/" . $1);
525             } elsif(!/^include\s+"(.*?)"/) {
526 1712         4398 $rules .= $_;
527             }
528             }
529 8         291 close(RULESFILE);
530              
531 8         30 $self->parse($rules);
532              
533             # Parse any include's we found earlier on
534 8         337 foreach my $include_file (@include_files) {
535 4         16 $self->read_file($include_file);
536             }
537              
538             }
539              
540             # _parse_meta
541             # Extracts meta attributes from a YARA rule string and adds them to the rule hashref.
542              
543             sub _parse_meta {
544 170     170   1164 my ($self, $rule_id, $raw) = @_;
545 170         185 my $flag;
546              
547             # Parse the rule data line by line
548 170         683 while($raw =~ /([^\n]+)\n?/g) {
549 1832         3346 my $line = $1;
550              
551             # Once we find the meta tag set a flag
552             # so that we know to start adding to
553             # to the rule object
554 1832 100       3769 if($line =~ /meta:/) {
555 6         8 $flag = 1;
556 6         20 next;
557             }
558              
559             # When we get passed the meta section we should encounter
560             # either a strings or condition section, if we find these
561             # set the flag to 0 so we stop adding to the rule object
562 1826 100 100     3701 if($flag and $line =~ /^\s*(?:strings:|condition:)\s*$/) {
563 6         10 $flag = 0;
564 6         19 next;
565             }
566              
567             # Now we're in the meta section, so if we find a valid
568             # meta attribute/value pair then add it to the rule object
569 1820 100 100     8443 if($flag and $line =~ /^\s*(\S[^=\s]*)\s*=\s*(\S+.*)$/) {
570 16         30 my $meta_name = $1;
571 16         30 my $meta_val = $2;
572 16 100       63 if($meta_val !~ /^(\d+|true|false)$/) {
573 8         31 $meta_val =~ s/^\s*"//;
574 8         33 $meta_val =~ s/"\s*$//;
575             }
576 16         45 $self->add_meta($rule_id, $meta_name, $meta_val);
577             }
578             }
579             }
580              
581              
582             # _parse_strings
583             # Extracts string attributes from a YARA rule string and adds them to the rule hashref.
584              
585             sub _parse_strings {
586 170     170   1257 my ($self, $rule_id, $raw) = @_;
587 170         208 my $flag;
588             # Parse the rule data line by line
589 170         787 while($raw =~ /([^\n]+)\n?/g) {
590 1832         3940 my $line = $1;
591              
592             # Once we find the strings tag set a flag
593             # so that we know to start adding to
594             # to the rule object
595 1832 100       4382 if($line =~ /strings:/) {
596 122         151 $flag = 1;
597 122         416 next;
598             }
599              
600             # When we get passed the strings section we should encounter
601             # either a meta or condition section, if we find these
602             # set the flag to 0 so we stop adding to the rule object
603 1710 100 100     6001 if($flag and $line =~ /^\s*(?:meta:|condition:)\s*$/) {
604 122         141 $flag = 0;
605 122         429 next;
606             }
607              
608             # Now we're in the strings section, so if we find a valid
609             # strings attribute/value pair then add it to the rule object
610 1588 100 100     8888 if($flag and $line =~ /^\s*(\$[a-zA-Z0-9_]*)\s*=\s*((?:"|\/|{)\s*\S+.*(?:"|\/|}))\s*(.*)$/) {
611 382         655 my $str_name = $1;
612 382         628 my $str_val = $2;
613 382         524 my $str_mods = $3;
614 382         569 my $str_type = "text";
615             # In YARA hex strings are bounded by {}
616 382 100       1227 if($str_val =~ /^{.*?}$/) {
    100          
617 182         249 $str_type = "hex";
618             # And regex are bounded by //
619             } elsif($str_val =~ /^\/.*?\/$/) {
620 10         17 $str_type = "regex";
621             # Everything else should be a string.
622             # I remove the quotes now for easier use throughout the module and re-add them when calling as_string
623             } else {
624 190         691 $str_val =~ s/^"//;
625 190         715 $str_val =~ s/"$//;
626             }
627 382 100       737 if($str_name eq "\$") {
628 44         103 $self->add_anonymous_string($rule_id, $str_val, $str_type);
629             } else {
630 338         785 $self->add_string($rule_id, $str_name, $str_val, $str_type);
631             }
632 382 100       10098 if($str_mods) {
633 26         68 foreach(split(/\s+/, $str_mods)) {
634 36         204 $self->add_string_modifier($rule_id, $str_name, $_)
635             }
636             }
637             }
638             }
639             }
640              
641             # _parse_condition
642             # Extracts the condition from a YARA rule string and adds it to the rule hashref.
643              
644             sub _parse_condition {
645 170     170   1318 my ($self, $rule_id, $raw) = @_;
646 170         193 my $flag;
647 170         209 my $condition = "";
648              
649             # Parse the rule data line by line
650 170         614 while($raw =~ /([^\n]+)\n?/g) {
651 1832         3173 my $line = $1;
652              
653             # Once we find the condition tag set a flag
654             # so that we know to start adding to
655             # to the rule object
656 1832 100       4090 if($line =~ /condition:/) {
657 170         180 $flag = 1;
658 170         559 next;
659             }
660              
661             # When we get passed the condition section we should encounter
662             # either a meta or strings section (or EOR), if we find these
663             # set the flag to 0 so we stop adding to the rule object
664 1662 50 66     4885 if($flag and $line =~ /^\s*(?:meta:|strings:)\s*$/) {
665 0         0 $flag = 0;
666 0         0 next;
667             }
668              
669             # Now we're in the condition section, so if we find a valid
670             # condition then append it to the condition string
671 1662 100 100     10057 if($flag and $line =~ /^\s*(\S+.*)$/ and $line !~ /^\s*({|})\s*$/) {
      100        
672             # To deal with multi-line conditions we check first then append
673             # to the condition string til we're done before setting the
674             # condition below
675 174 100       327 if(length($condition) == 0) {
676 170         898 $condition = "$1\n";
677             } else {
678 4         24 $condition .= "\t\t$1\n";
679             }
680             }
681             }
682             # Remove any trailing new line and then set the condition
683 170         221 chomp($condition);
684 170         371 $self->set_condition($rule_id, $condition);
685             }
686              
687             =item set_rule_modifier($rule_id, $modifier)
688              
689             Set a modifier on a rule. The value for modifier must be one of the following strings:
690             private
691             global
692              
693             If modifier is set to undef the current modifier (if any) will be removed.
694              
695             =cut
696              
697             sub set_rule_modifier {
698 15     15 1 1267 my ($self, $rule_id, $modifier) = @_;
699            
700 15 100       32 if($modifier) {
701             # Their are only two valid rule modifiers, private and global
702             # make sure we are only setting to these values, otherwise bail
703 14 100       56 if($modifier =~ /^(?:private|global)$/) {
704 13         77 $self->{rules}->{$rule_id}->{modifier} = $modifier;
705             } else {
706 1         176 carp("$rule_id: unable to set rule modifier to invalid value: $modifier");
707             }
708             } else {
709             # If this is set to undef, assume the rule modifier requires deletion
710 1         6 delete($self->{rules}->{$rule_id}->{modifier});
711             }
712             }
713              
714             =item set_condition($rule_id, $condition)
715              
716             Sets the value of the condition to $condition.
717              
718             =cut
719              
720             sub set_condition {
721 173     173 1 1316 my ($self, $rule_id, $condition) = @_;
722 173         194 my $flag = 1;
723              
724 173 50       269 if($condition) {
725             # Make sure we are only setting the condition to something valid
726 173 100       364 if($self->_check_reserved($condition, 'condition')) {
727 1         99 carp("$rule_id: cannot set condition to a reserved word: $condition");
728             } else {
729             # Then set the condition directly
730 172         785 $self->{rules}->{$rule_id}->{condition} = $condition;
731             }
732             } else {
733 0         0 carp("$rule_id: can't set a null condition");
734             }
735             }
736              
737             =item add_tag($rule_id, $tag)
738              
739             Adds a tag to the rule.
740              
741             =cut
742              
743             sub add_tag {
744 32     32 1 571 my ($self, $rule_id, $tag) = @_;
745 32         45 my $flag = 1;
746              
747             # Maybe not the most efficient, but I don't expect
748             # the use of a large number of tags
749             # Check to see that the tag is both valid, and not
750             # already set. If either of these checks fails
751             # set the flag to 0 and print an error.
752 32         41 foreach(@{$self->{rules}->{$rule_id}->{tags}}) {
  32         123  
753 18 50       129 if($_ eq $tag) {
    100          
754 0         0 $flag = 0;
755 0         0 carp("$rule_id: $tag already set.");
756 0         0 last;
757             } elsif($self->_check_reserved($tag, 'tag')) {
758 1         2 $flag = 0;
759 1         112 carp("$rule_id: cannot set tag to a reserved word: $tag");
760 1         89 last;
761             }
762             }
763              
764             # If we didn't find any issues above, push the new tag to the tags array
765 32 100       203 if($flag) {
766 31         36 push(@{$self->{rules}->{$rule_id}->{tags}}, $tag);
  31         143  
767             }
768             }
769              
770             =item add_meta($rule_id, $meta_name, $meta_val)
771              
772             Adds a meta name/value pair to the rule.
773              
774             =cut
775              
776             sub add_meta {
777 20     20 1 931 my ($self, $rule_id, $meta_name, $meta_val) = @_;
778              
779             # Make sure the meta hash element exists
780 20 100       98 if(!$self->{rules}->{$rule_id}->{meta}) {
781             # If not, we need to tie a new hash to ensure meta order is maintained
782 7         178 $self->{rules}->{$rule_id}->{meta_knot} = tie(%{$self->{rules}->{$rule_id}->{meta}}, 'Tie::IxHash');
  7         31  
783             }
784             # Check validity of meta name before adding it to the rule object
785 20 100       343 if($self->_check_reserved($meta_name, 'meta')) {
    50          
786 1         114 carp("$rule_id: $meta_name contains a reserved word, please try again");
787             # Make sure we don't add duplicate meta names as this is invalid
788             } elsif($self->{rules}->{$rule_id}->{meta}->{$meta_name}) {
789 0         0 carp("$rule_id: $meta_name already set, select a new name or try modify_meta()");
790             } else {
791 19         273 $self->{rules}->{$rule_id}->{meta}->{$meta_name} = $meta_val;
792             }
793             }
794              
795             =item add_string_modifier($rule_id, $str_name, $modifier)
796              
797             Set a modifier on a string. The value for the modifier must be one of the following strings:
798             wide
799             nocase
800             ascii
801             fullword
802              
803             Use of the keyword 'all' will set all modifiers on a string.
804              
805             =cut
806              
807             sub add_string_modifier {
808 42     42 1 1089 my ($self, $rule_id, $str_name, $modifier) = @_;
809              
810 42 50       75 if($modifier) {
811             # There are only four valid string modifiers so make sure we only add one of these
812 42 100       145 if($modifier =~ /^(?:wide|nocase|ascii|fullword)$/) {
    100          
813 40         43 push(@{$self->{rules}->{$rule_id}->{strings}->{$str_name}->{modifier}}, $modifier);
  40         165  
814             # Unless we use the keyword 'all' in which case set all four string modifiers
815             } elsif($modifier eq "all") {
816 1         8 $self->{rules}->{$rule_id}->{strings}->{$str_name}->{modifier} = [ 'wide', 'ascii', 'nocase', 'fullword' ];
817             } else {
818 1         98 carp("$rule_id: unable to set string modifier to invalid value: $modifier");
819             }
820             } else {
821 0         0 carp("$rule_id: cannot set undefined modifier");
822             }
823             }
824              
825             =item remove_string_modifier($rule_id, $str_name, $modifier)
826              
827             Remove a modifier on a string. The value for the modifier must be one of the following strings:
828             wide
829             nocase
830             ascii
831             fullword
832              
833             Use of the keyword 'all' will remove all modifiers from a string.
834              
835             =cut
836              
837             sub remove_string_modifier {
838 5     5 1 9374 my ($self, $rule_id, $str_name, $modifier) = @_;
839              
840 5 50       15 if($modifier) {
841             # There are only four valid string modifiers so make sure we only both trying to remove these
842 5 100       40 if($modifier =~ /^(?:wide|nocase|ascii|fullword)$/) {
    50          
843 4         6 @{$self->{rules}->{$rule_id}->{strings}->{$str_name}->{modifier}} = grep { $_ ne $modifier } @{$self->{rules}->{$rule_id}->{strings}->{$str_name}->{modifier}};
  4         15  
  10         406  
  4         196  
844             # Unless we use the keyword 'all' in which case remove all four string modifiers
845             } elsif($modifier eq "all") {
846 1         6 $self->{rules}->{$rule_id}->{strings}->{$str_name}->{modifier} = [];
847             } else {
848 0         0 carp("$rule_id: unabled to remove invalid modifier: $modifier");
849             }
850             } else {
851 0         0 carp("$rule_id: cannot remove undefined modifier");
852             }
853             }
854              
855             =item add_anonymous_string($rule_id, $str_val, $str_type)
856              
857             Allows the addition of anonymous strings
858              
859             =cut
860              
861             sub add_anonymous_string {
862 46     46 1 993 my ($self, $rule_id, $str_val, $str_type) = @_;
863              
864             # Check if we've previously added strings
865 46 100       188 if(!$self->{rules}->{$rule_id}->{strings}) {
866             # If not, we need to tie a new hash to ensure string order is maintained
867 8         192 $self->{rules}->{$rule_id}->{strings_knot} = tie(%{$self->{rules}->{$rule_id}->{strings}}, 'Tie::IxHash');
  8         62  
868             }
869 46         596 my $val = { value => $str_val, type => $str_type, modifier => [] };
870 46         62 my $last_anon_string = undef;
871             # So that we can add multiple anonymous strings as new hash elements
872             # I am cheating and adding an extra $ for each new anon string
873             # Here we find the latest anon string and then append a $ before
874             # setting the value against this new string name
875 46         51 foreach(keys(%{$self->{rules}->{$rule_id}->{strings}})) {
  46         161  
876 153 100       1615 if(/^\$+$/) {
877 149         228 $last_anon_string = $_;
878             }
879             }
880 46         262 $last_anon_string .= '$';
881 46         178 $self->{rules}->{$rule_id}->{strings}->{$last_anon_string} = $val;
882             }
883              
884             =item add_string($rule_id, $str_name, $str_val, $str_type)
885              
886             Allows the addition of a new string name/value pair.
887              
888             =cut
889              
890             sub add_string {
891 343     343 1 1670 my ($self, $rule_id, $str_name, $str_val, $str_type) = @_;
892              
893             # Check if we've previously added strings
894 343 100       1594 if(!$self->{rules}->{$rule_id}->{strings}) {
895             # If not, we need to tie a new hash to ensure string order is maintained
896 115         2544 $self->{rules}->{$rule_id}->{strings_knot} = tie(%{$self->{rules}->{$rule_id}->{strings}}, 'Tie::IxHash');
  115         431  
897             }
898             # Make sure we don't add duplicate strings as this is invalid
899 343 100       5837 if($self->{rules}->{$rule_id}->{strings}->{$str_name}) {
    100          
900 1         118 carp("$rule_id: $str_name already set, pick a new name or try modify_string()");
901             # Check validity of string before adding it to the rule object
902             } elsif($self->_is_valid($str_name, 'string_name')) {
903 341         1535 my $val = { value => $str_val, type => $str_type, modifier => [] };
904 341         1431 $self->{rules}->{$rule_id}->{strings}->{$str_name} = $val;
905             }
906             }
907              
908             =item remove_tag($rule_id, $tag)
909              
910             Removes a tag from the rule as identified by $tag.
911              
912             =cut
913              
914             sub remove_tag {
915 1     1 1 828 my ($self, $rule_id, $tag) = @_;
916              
917             # Remove the tag from the tags array
918 1         2 @{$self->{rules}->{$rule_id}->{tags}} = grep { $_ ne $tag } @{$self->{rules}->{$rule_id}->{tags}};
  1         5  
  3         14  
  1         6  
919             # If their are no tags left, delete the tags hash element so we don't print a :
920 1 50       10 if(scalar(@{$self->{rules}->{$rule_id}->{tags}} < 1)) {
  1         5  
921 0         0 delete($self->{rules}->{$rule_id}->{tags});
922             }
923             }
924              
925             =item remove_meta($rule_id, $meta_name)
926              
927             Removes a meta name/value pair as identified by $meta_name.
928              
929             =cut
930              
931             sub remove_meta {
932 1     1 1 462 my ($self, $rule_id, $meta_name) = @_;
933              
934             # Just delete the hash element for the given meta name
935 1         6 delete($self->{rules}->{$rule_id}->{meta}->{$meta_name});
936             }
937              
938             =item remove_anonymous_string($rule_id, $str_val)
939              
940             Removes an anonymous string with the value specified.
941              
942             =cut
943              
944             sub remove_anonymous_string {
945 1     1 1 694 my ($self, $rule_id, $str_val) = @_;
946              
947             # Loop through all the strings to find anonymous strings
948 1         2 foreach my $str_name (keys(%{$self->{rules}->{$rule_id}->{strings}})) {
  1         7  
949             # Find the anonymous string with the correct value
950 3 100 66     55 if($str_name =~ /^\$+$/ and $self->{rules}->{$rule_id}->{strings}->{$str_name}->{value} eq $str_val) {
951             # Delete the hash element for the given string value then exit the loop
952 1         21 delete($self->{rules}->{$rule_id}->{strings}->{$str_name});
953 1         30 last;
954             }
955             }
956             }
957              
958             =item remove_string($rule_id, $str_name)
959              
960             Removes a string name/value pair, but only if it contains a single value.
961              
962             =cut
963              
964             sub remove_string {
965 2     2 1 1070 my ($self, $rule_id, $str_name) = @_;
966              
967             # Because their may be more than one anonymous string, bail at this point because we need the value for that string
968 2 100       8 if($str_name eq '$') {
969 1         139 carp("$rule_id: trying to remove anonymous string, use remove_anonymous_string() for this");
970             } else {
971             # Otherwise we can delete the hash element for the given string name
972 1         6 delete($self->{rules}->{$rule_id}->{strings}->{$str_name});
973             }
974             }
975              
976             =item modify_meta($rule_id, $meta_name, $meta_val)
977              
978             Modifies the value of $meta_name and sets it to $meta_val.
979              
980             =cut
981              
982             sub modify_meta {
983 1     1 1 584 my ($self, $rule_id, $meta_name, $meta_val) = @_;
984              
985             # Check meta name already exists
986 1 50       7 if(!$self->{rules}->{$rule_id}->{meta}->{$meta_name}) {
987 0         0 carp("$rule_id: $meta_name not set, select the correct name or try add_meta()");
988             } else {
989             # If it does, set the new value
990 1         19 $self->{rules}->{$rule_id}->{meta}->{$meta_name} = $meta_val;
991             }
992             }
993              
994             =item modify_string($rule_id, $str_name, $str_val)
995              
996             Modifies the value of a string name/value pair, but only if it contains a single value.
997              
998             Sets the value of $str_name to $str_val.
999              
1000             =cut
1001              
1002             sub modify_string {
1003 1     1 1 413 my ($self, $rule_id, $str_name, $str_val) = @_;
1004              
1005             # Can't modify an anonymous string without knowing the current value
1006 1 50       11 if($str_name =~ /^\$+$/) {
    50          
1007 0         0 carp("$rule_id: cannot modify value of anonymous string as their may be multiple values and I don't know which one to modify.");
1008             # Can't modify a string that doesn't exist
1009             } elsif(!$self->{rules}->{$rule_id}->{strings}->{$str_name}) {
1010 0         0 carp("$rule_id: cannot modify $str_name as it does not exist, try add_string().");
1011             } else {
1012             # Set the new value directly
1013 1         19 $self->{rules}->{$rule_id}->{strings}->{$str_name}->{value} = $str_val;
1014             }
1015             }
1016              
1017             # _rule_as_string
1018             # Parses the rule hash(es) contained within $self or if a $rule_id is provided parses that rule.
1019             # Returns a string of the rule printed in YARA format.
1020              
1021             sub _rule_as_string {
1022 171     171   237 my ($self, $rule_id) = @_;
1023 171         231 my $ret = '';
1024 171         159 my @missing;
1025              
1026             # Check for condition, if not the rule is invalid
1027 171 50       611 if(!exists($self->{rules}->{$rule_id}->{condition})) {
1028 0         0 carp("$rule_id does not contain a condition.");
1029             } else {
1030 171 100       1565 if($self->{rules}->{$rule_id}->{modifier}) {
1031 11         103 $ret .= $self->{rules}->{$rule_id}->{modifier} . " ";
1032             }
1033              
1034 171         1235 $ret .= "rule $rule_id";
1035              
1036             # If tags are set, add a : after the rule_id and then space separate each tag
1037 171 100       575 if($self->{rules}->{$rule_id}->{tags}) {
1038 19         129 $ret .= " :";
1039 19         21 foreach my $tag (@{$self->{rules}->{$rule_id}->{tags}}) {
  19         71  
1040 30         170 $ret .= " $tag";
1041             }
1042             }
1043              
1044             # Now add the opening brace on a new line
1045 171         1237 $ret .= "\n{";
1046              
1047             # If their is a meta element, loop through each entry and add to the rule string
1048 171 100       570 if($self->{rules}->{$rule_id}->{meta}) {
1049 7         65 $ret .= "\n";
1050 7         12 $ret .= "\tmeta:\n";
1051 7         10 foreach my $meta_name (keys(%{$self->{rules}->{$rule_id}->{meta}})) {
  7         29  
1052 18         193 my $meta_val;
1053 18 100       67 if($self->{rules}->{$rule_id}->{meta}->{$meta_name} =~ /^(\d+|true|false)$/) {
1054 8         130 $meta_val = $self->{rules}->{$rule_id}->{meta}->{$meta_name};
1055             } else {
1056 10         180 $meta_val = "\"$self->{rules}->{$rule_id}->{meta}->{$meta_name}\"";
1057             }
1058 18         262 $ret .= "\t\t$meta_name = $meta_val\n";
1059             }
1060             }
1061              
1062             # If their is a strings element, loop through each entry and add to the rule string
1063 171 100       1450 if($self->{rules}->{$rule_id}->{strings}) {
1064 123         838 $ret .= "\n";
1065 123         140 $ret .= "\tstrings:\n";
1066 123         141 foreach my $string_name (keys(%{$self->{rules}->{$rule_id}->{strings}})) {
  123         427  
1067 384         3779 my $display_name = $string_name;
1068 384         393 my $display_val;
1069 384 100       1100 if($string_name =~ /^\$+$/) {
1070 44         104 $display_name = '$';
1071             }
1072 384 100       1356 if($self->{rules}->{$rule_id}->{strings}->{$string_name}->{type} eq "text") {
    100          
    50          
1073 192         2674 $display_val = "\"$self->{rules}->{$rule_id}->{strings}->{$string_name}->{value}\"";
1074 192         2161 foreach my $str_mod (@{$self->{rules}->{$rule_id}->{strings}->{$string_name}->{modifier}}) {
  192         707  
1075 36         314 $display_val .= " $str_mod";
1076             }
1077             } elsif($self->{rules}->{$rule_id}->{strings}->{$string_name}->{type} eq "hex") {
1078 182         4424 $display_val = $self->{rules}->{$rule_id}->{strings}->{$string_name}->{value};
1079             } elsif($self->{rules}->{$rule_id}->{strings}->{$string_name}->{type} eq "regex") {
1080 10         535 $display_val = $self->{rules}->{$rule_id}->{strings}->{$string_name}->{value};
1081             }
1082 384         4935 $ret .= "\t\t$display_name = $display_val\n";
1083             }
1084             }
1085              
1086             # Add the condition and closing brace
1087 171         856 $ret .= "\n";
1088 171         204 $ret .= "\tcondition:\n";
1089 171         586 $ret .= "\t\t$self->{rules}->{$rule_id}->{condition}\n";
1090 171         1211 $ret .= "}";
1091             }
1092              
1093 171         706 return $ret;
1094             }
1095              
1096             =item as_string()
1097              
1098             Can take zero or one argument. With no arugments this return all rules within the rule hashref as a string.
1099              
1100             =over 4
1101              
1102             =item as_string($rule_id)
1103              
1104             Extracts a single rule from the hashref and returns it as a string value.
1105              
1106             =cut
1107              
1108             sub as_string {
1109 7     7 1 2463 my ($self, $rule_id) = @_;
1110 7         13 my $ret = '';
1111              
1112             # Check to see if their is a rule_id and return that rule as a string
1113 7 50       27 if($rule_id) {
1114 0         0 $ret = $self->_rule_as_string($rule_id);
1115             } else {
1116             # Otherwise loop through the hash and return all rules as a string
1117 7         10 foreach my $rule_id (keys(%{$self->{rules}})) {
  7         37  
1118 171         1142 $ret .= $self->_rule_as_string($rule_id) . "\n\n";
1119             }
1120 7         80 chomp($ret);
1121             }
1122              
1123 7         150 return $ret;
1124             }
1125              
1126             =back
1127              
1128             =item get_referenced_rule($rule_id)
1129              
1130             Check to see if $rule_id references any other rules and return any matched rule ID's as an array.
1131              
1132             =cut
1133              
1134             sub get_referenced_rule {
1135 1     1 1 2214 my ($self, $rule_id) = @_;
1136 1         2 my @ret;
1137              
1138             # For each rule, check to see if the rule_id passed is contained
1139             # within the rule's condition, if so add it to an array to be returned
1140 1 100       7 map { if (exists($self->{rules}->{$_})) { push(@ret, $_); } } split(/\s+/, $self->{rules}->{$rule_id}->{condition});
  3         36  
  1         12  
1141              
1142 1         5 return @ret;
1143             }
1144              
1145             =item position_rule($rule_id, $position, $relative_rule_id)
1146              
1147             Position a rule before or after another rule where:
1148              
1149             $rule_id is the rule_id of the rule to be moved
1150             $position is either before or after
1151             $relative_rule_id is the rule_id of the rule to move this rule around
1152              
1153             =cut
1154              
1155             sub position_rule {
1156 2     2 1 1443 my ($self, $rule_id, $position, $relative_rule_id) = @_;
1157 2         4 my $rule_found = 0;
1158 2         6 my $relative_rule_found = 0;
1159              
1160             # Make sure we pick a valid movement (before or after)
1161 2 50       23 if($position !~ /^(before|after)$/) {
    50          
    50          
1162 0         0 carp("$rule_id: position must be set to before or after $position");
1163             # Make sure the rule_id requested for movement exists
1164             } elsif(!$self->{rules}->{$rule_id}) {
1165 0         0 carp("Cannot position rule that does not exist: $rule_id");
1166             # Make sure the relative_rule_id to which the rule_id should be moved before or after exists
1167             } elsif(!$self->{rules}->{$relative_rule_id}) {
1168 0         0 carp("Cannot position around rule that does not exist: $relative_rule_id");
1169             } else {
1170 2 50       40 if($self->{verbose}) {
1171 0         0 print "Moving $rule_id $position $relative_rule_id\n";
1172             }
1173             # Create an array with the current order of keys
1174 2         10 my @new_order = $self->{rules_knot}->Keys;
1175             # Get the current position of the rule_id and relative_rule_id
1176 2         57 my $rule_pos = $self->{rules_knot}->Indices($rule_id);
1177 2         15 my $relative_rule_pos = $self->{rules_knot}->Indices($relative_rule_id);
1178             # Adjust the position accordingly
1179 2 50 66     29 if($position eq "after" and $rule_pos > $relative_rule_pos) {
    50 66        
1180 0         0 $rule_pos++;
1181             } elsif($position eq "before" and $rule_pos < $relative_rule_pos) {
1182 0         0 $relative_rule_pos--;
1183             }
1184             # Reorder within the array then set the order using Tie:IxHash->Reorder
1185 2         7 splice(@new_order, $relative_rule_pos, 0, splice(@new_order, $rule_pos, 1));
1186 2         9 $self->{rules_knot}->Reorder(@new_order);
1187             }
1188             }
1189              
1190             1;
1191              
1192             __END__