File Coverage

blib/lib/CAPE/Utils.pm
Criterion Covered Total %
statement 47 590 7.9
branch 0 266 0.0
condition 0 125 0.0
subroutine 16 36 44.4
pod 19 20 95.0
total 82 1037 7.9


line stmt bran cond sub pod time code
1             package CAPE::Utils;
2              
3 1     1   113052 use 5.006;
  1         3  
4 1     1   9 use strict;
  1         2  
  1         33  
5 1     1   6 use warnings;
  1         3  
  1         26  
6 1     1   2091 use JSON;
  1         17783  
  1         5  
7 1     1   695 use Config::Tiny;
  1         923  
  1         41  
8 1     1   3300 use DBI;
  1         19725  
  1         73  
9 1     1   1375 use File::Slurp qw(append_file write_file read_file write_file);
  1         28069  
  1         83  
10 1     1   9 use Config::Tiny;
  1         2  
  1         26  
11 1     1   767 use IPC::Cmd qw[ run ];
  1         45629  
  1         61  
12 1     1   1680 use Text::ANSITable;
  1         86000  
  1         45  
13 1     1   9 use File::Spec;
  1         2  
  1         28  
14 1     1   6 use IPC::Cmd qw(run);
  1         2  
  1         61  
15 1     1   536 use Net::Subnet;
  1         2364  
  1         63  
16 1     1   506 use Sys::Hostname;
  1         1154  
  1         53  
17 1     1   714 use Sys::Syslog;
  1         8656  
  1         69  
18 1     1   548 use File::Copy;
  1         2407  
  1         6897  
19              
20             =head1 NAME
21              
22             CAPE::Utils - A helpful library for with CAPE.
23              
24             =head1 VERSION
25              
26             Version 2.6.0
27              
28             =cut
29              
30             our $VERSION = '2.6.0';
31              
32             =head1 SYNOPSIS
33              
34             Quick summary of what the module does.
35              
36             Perhaps a little code snippet.
37              
38             use CAPE::Utils;
39              
40             my $cape_util=CAPE::Utils->new();
41              
42             my $sub_results=$cape_util->submit(items=>@to_detonate,unique=>0, quiet=>1);
43             use JSON;
44             print encode_json($sub_results)."\n";
45              
46             =head1 METHODS
47              
48             =head2 new
49              
50             Initiates the object. One argument is taken and that is the
51             path to the INI config file. The default is '/usr/local/etc/cape_utils.ini'
52             and if not found, the defaults will be used.
53              
54             my $cape_util=CAPE::Utils->new('/path/to/some/config.ini');
55              
56             =cut
57              
58             sub new {
59 0     0 1   my $ini = $_[1];
60              
61 0 0         if ( !defined($ini) ) {
62 0           $ini = '/usr/local/etc/cape_utils.ini';
63             }
64              
65 0           my $base_config = {
66             '_' => {
67             dsn => 'dbi:Pg:dbname=cape',
68             user => 'cape',
69             pass => '',
70             base => '/opt/CAPEv2/',
71             eve => '/opt/CAPEv2/log/eve.json',
72             poetry => 1,
73             fail_all => 0,
74             pending_columns => 'id,target,package,timeout,ET,route,options,clock,added_on',
75             running_columns => 'id,target,package,timeout,ET,route,options,clock,added_on,started_on,machine',
76             task_columns => 'id,target,package,timeout,ET,route,options,clock,added_on,latest,machine,status',
77             running_target_clip => 1,
78             running_time_clip => 1,
79             pending_target_clip => 1,
80             pending_time_clip => 1,
81             task_target_clip => 1,
82             task_time_clip => 1,
83             table_color => 'Text::ANSITable::Standard::NoGradation',
84             table_border => 'ASCII::None',
85             set_clock_to_now => 1,
86             timeout => 200,
87             enforce_timeout => 0,
88             subnets => '192.168.0.0/16,127.0.0.1/8,::1/128,172.16.0.0/12,10.0.0.0/8',
89             apikey => '',
90             auth => 'ip',
91             incoming => '/malware/client-incoming',
92             incoming_json => '/malware/incoming-json',
93             eve_look_back => 360,
94             malscore => 0,
95             },
96             };
97              
98 0           my $config = Config::Tiny->read( $ini, 'utf8' );
99 0 0         if ( !defined($config) ) {
100 0           $config = $base_config;
101             } else {
102 0           my @to_merge = keys( %{ $base_config->{_} } );
  0            
103 0           foreach my $item (@to_merge) {
104 0 0         if ( !defined( $config->{_}->{$item} ) ) {
105 0           $config->{_}->{$item} = $base_config->{_}->{$item};
106             }
107             }
108             }
109              
110             # init the object
111 0           my $self = { config => $config, };
112 0           bless $self;
113              
114 0           return $self;
115             } ## end sub new
116              
117             =head2 connect
118              
119             Return a DBH from DBI->connect for the CAPE SQL server.
120              
121             This will die with the output from $DBI::errstr if it fails.
122              
123             my $dbh = $cape->connect;
124              
125             =cut
126              
127             sub connect {
128 0     0 1   my $self = $_[0];
129              
130             my $dbh = DBI->connect( $self->{config}->{_}->{dsn}, $self->{config}->{_}->{user}, $self->{config}->{_}->{pass} )
131 0   0       || die($DBI::errstr);
132              
133 0           return $dbh;
134             }
135              
136             =head2 fail
137              
138             Set one or more pending tasks to failed as below.
139              
140             UPDATE tasks SET status = 'failed_processing' WHERE status = 'pending'
141              
142             The following options are also supported.
143              
144             - where :: Additional SQL where statements to add. Something must
145             be specified for this, unless fail_all in the config is
146             true. Otherwise this method will die.
147              
148             my $rows=$cape_util->fail( where=>"target like '%foo%'");
149              
150             =cut
151              
152             sub fail {
153 0     0 1   my ( $self, %opts ) = @_;
154              
155 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
156 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
157             }
158              
159 0 0 0       if ( !defined( $opts{where} ) && !$self->{config}->{_}->{fail_all} ) {
160 0           die "fail_all is disabled and nothing specified for where";
161             }
162              
163 0           my $dbh = $self->connect;
164              
165 0           my $statement = "UPDATE tasks SET status = 'failed_processing' WHERE status = 'pending'";
166              
167 0 0         if ( defined( $opts{where} ) ) {
168 0           $statement = $statement . ' AND ' . $opts{where};
169             }
170              
171 0           $statement = $statement . ';';
172              
173 0           my $sth = $dbh->prepare($statement);
174              
175 0           $sth->execute;
176              
177 0           my $rows = $sth->rows;
178              
179 0           $sth->finish;
180 0           $dbh->disconnect;
181              
182 0           return $rows;
183             } ## end sub fail
184              
185             =head2 get_pending_count
186              
187             Get pending count pending tasks.
188              
189             - where :: And additional where part of the SQL statement. "and" will
190             automatically be used to join it with the rest of the
191             statement. May not contain a ';'.
192             - Default :: undef
193              
194             my $count=$cape_util->get_pending_count;
195              
196             =cut
197              
198             sub get_pending_count {
199 0     0 1   my ( $self, %opts ) = @_;
200              
201 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
202 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
203             }
204              
205 0           my $dbh = $self->connect;
206              
207 0           my $statement = "select * from tasks where status = 'pending'";
208 0 0         if ( defined( $opts{where} ) ) {
209 0           $statement = $statement . ' AND ' . $opts{where};
210             }
211              
212 0           my $sth = $dbh->prepare($statement);
213 0           $sth->execute;
214              
215 0           my $rows = $sth->rows;
216              
217 0           $sth->finish;
218 0           $dbh->disconnect;
219              
220 0           return $rows;
221             } ## end sub get_pending_count
222              
223             =head2 get_pending
224              
225             Returns a arrah ref of hash refs of rows from the tasks table where the
226             status is set to pending via "select * from tasks where status = 'pending'"
227              
228             - where :: And additional where part of the SQL statement. "and" will
229             automatically be used to join it with the rest of the
230             statement. May not contain a ';'.
231             - Default :: undef
232              
233             - where :: Additional SQL where statements to add.
234              
235             =cut
236              
237             sub get_pending {
238 0     0 1   my ( $self, %opts ) = @_;
239              
240 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
241 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
242             }
243              
244 0           my $dbh = $self->connect;
245              
246 0           my $statement = "select * from tasks where status = 'pending'";
247 0 0         if ( defined( $opts{where} ) ) {
248 0           $statement = $statement . ' AND ' . $opts{where};
249             }
250              
251 0           my $sth = $dbh->prepare($statement);
252 0           $sth->execute;
253              
254 0           my $row;
255             my @rows;
256 0           while ( $row = $sth->fetchrow_hashref ) {
257 0           push( @rows, $row );
258             }
259              
260 0           $sth->finish;
261 0           $dbh->disconnect;
262              
263 0           return \@rows;
264             } ## end sub get_pending
265              
266             =head2 get_pending_table
267              
268             Generates a ASCII table for pending.
269              
270             The following config variables can are relevant to this and
271             may be overriden.
272              
273             table_border
274             table_color
275             pending_columns
276             pending_target_clip
277             pending_time_clip
278              
279             The following options are also supported.
280              
281             - where :: And additional where part of the SQL statement. "and" will
282             automatically be used to join it with the rest of the
283             statement. May not contain a ';'.
284             - Default :: undef
285              
286             print $cape_util->get_pending_table( pending_columns=>'id,package');
287              
288             =cut
289              
290             sub get_pending_table {
291 0     0 1   my ( $self, %opts ) = @_;
292              
293 0           my @overrides = ( 'table_border', 'table_color', 'pending_columns', 'pending_target_clip', 'pending_time_clip' );
294 0           foreach my $override (@overrides) {
295 0 0         if ( !defined( $opts{$override} ) ) {
296 0           $opts{$override} = $self->{config}->{_}->{$override};
297             }
298             }
299              
300 0           my $rows = $self->get_pending( where => $opts{where} );
301              
302 0           my $tb = Text::ANSITable->new;
303 0           $tb->border_style( $opts{table_border} );
304 0           $tb->color_theme( $opts{table_color} );
305              
306 0           my @columns = split( /,/, $opts{pending_columns} );
307 0           my $header_int = 0;
308 0           my $padding = 0;
309 0           foreach my $header (@columns) {
310 0 0         if ( ( $header_int % 2 ) != 0 ) { $padding = 1; }
  0            
311 0           else { $padding = 0; }
312              
313 0           $tb->set_column_style( $header_int, pad => $padding );
314              
315 0           $header_int++;
316             }
317              
318 0           $tb->columns( \@columns );
319              
320 0           my @td;
321 0           foreach my $row ( @{$rows} ) {
  0            
322 0           my @new_line;
323 0           foreach my $column (@columns) {
324 0 0         if ( $column eq 'ET' ) {
325 0           $row->{ET} = $row->{enforce_timeout};
326             }
327              
328 0 0         if ( defined( $row->{$column} ) ) {
329 0 0         if ( $column eq 'ET' ) {
330 0           $row->{ET} = $row->{enforce_timeout};
331             }
332              
333 0 0 0       if ( ( $column eq 'clock' || $column eq 'added_on' ) && $opts{pending_time_clip} ) {
    0 0        
      0        
334 0           $row->{$column} =~ s/\.[0-9]+$//;
335             } elsif ( $column eq 'target' && $opts{pending_target_clip} ) {
336 0           $row->{target} =~ s/^.*\///;
337             }
338 0           push( @new_line, $row->{$column} );
339             } else {
340 0           push( @new_line, '' );
341             }
342             } ## end foreach my $column (@columns)
343              
344 0           push( @td, \@new_line );
345             } ## end foreach my $row ( @{$rows} )
346              
347 0           $tb->add_rows( \@td );
348              
349 0           return $tb->draw;
350             } ## end sub get_pending_table
351              
352             =head2 get_running
353              
354             Returns a array ref of hash refs of rows from the tasks table where the
355             status is set to pending.
356              
357             select * from tasks where status = 'running'
358              
359             The statement above is used to find running tasks.
360              
361             - where :: And additional where part of the SQL statement. "and" will
362             automatically be used to join it with the rest of the
363             statement. May not contain a ';'.
364             - Default :: undef
365              
366             use Data::Dumper;
367              
368             my $running=$cape_utils->get_running;
369             print Dumper($running);
370              
371             =cut
372              
373             sub get_running {
374 0     0 1   my ( $self, %opts ) = @_;
375              
376 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
377 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
378             }
379              
380 0           my $dbh = $self->connect;
381              
382 0           my $statement = "select * from tasks where status = 'running'";
383 0 0         if ( defined( $opts{where} ) ) {
384 0           $statement = $statement . ' AND ' . $opts{where};
385             }
386              
387 0           my $sth = $dbh->prepare($statement);
388 0           $sth->execute;
389              
390 0           my $row;
391             my @rows;
392 0           while ( $row = $sth->fetchrow_hashref ) {
393 0           push( @rows, $row );
394             }
395              
396 0           $sth->finish;
397 0           $dbh->disconnect;
398              
399 0           return \@rows;
400             } ## end sub get_running
401              
402             =head2 get_running_count
403              
404             Get pending count running tasks.
405              
406             select * from tasks where status = 'running'
407              
408             The statement above is used to find running tasks.
409              
410             - where :: And additional where part of the SQL statement. "and" will
411             automatically be used to join it with the rest of the
412             statement. May not contain a ';'.
413             - Default :: undef
414              
415             my $count=$cape_util->get_running_count;
416              
417             =cut
418              
419             sub get_running_count {
420 0     0 1   my ( $self, %opts ) = @_;
421              
422 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
423 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
424             }
425              
426 0           my $dbh = $self->connect;
427              
428 0           my $statement = "select * from tasks where status = 'running'";
429 0 0         if ( defined( $opts{where} ) ) {
430 0           $statement = $statement . ' AND ' . $opts{where};
431             }
432              
433 0           my $sth = $dbh->prepare($statement);
434 0           $sth->execute;
435              
436 0           my $rows = $sth->rows;
437              
438 0           $sth->finish;
439 0           $dbh->disconnect;
440              
441 0           return $rows;
442             } ## end sub get_running_count
443              
444             =head2 get_running_table
445              
446             Generates a ASCII table for pending.
447              
448             The following config variables can are relevant to this and
449             may be overriden.
450              
451             table_border
452             table_color
453             running_columns
454             running_target_clip
455             running_time_clip
456              
457             The statement below is used to find running tasks.
458              
459             select * from tasks where status = 'running'
460              
461             The following options are also supported.
462              
463             - where :: And additional where part of the SQL statement. "and" will
464             automatically be used to join it with the rest of the
465             statement. May not contain a ';'.
466             - Default :: undef
467              
468             print $cape_util->get_pending_table( pending_columns=>'id,package');
469              
470             =cut
471              
472             sub get_running_table {
473 0     0 1   my ( $self, %opts ) = @_;
474              
475 0           my @overrides = ( 'table_border', 'table_color', 'running_columns', 'running_target_clip', 'running_time_clip' );
476 0           foreach my $override (@overrides) {
477 0 0         if ( !defined( $opts{$override} ) ) {
478 0           $opts{$override} = $self->{config}->{_}->{$override};
479             }
480             }
481              
482 0           my $rows = $self->get_running( where => $opts{where} );
483              
484 0           my $tb = Text::ANSITable->new;
485 0           $tb->border_style( $opts{table_border} );
486 0           $tb->color_theme( $opts{table_color} );
487              
488 0           my @columns = split( /,/, $opts{running_columns} );
489 0           my $header_int = 0;
490 0           my $padding = 0;
491 0           foreach my $header (@columns) {
492 0 0         if ( ( $header_int % 2 ) != 0 ) { $padding = 1; }
  0            
493 0           else { $padding = 0; }
494              
495 0           $tb->set_column_style( $header_int, pad => $padding );
496              
497 0           $header_int++;
498             }
499              
500 0           $tb->columns( \@columns );
501              
502 0           my @td;
503 0           foreach my $row ( @{$rows} ) {
  0            
504 0           my @new_line;
505 0           foreach my $column (@columns) {
506 0 0         if ( $column eq 'ET' ) {
507 0           $row->{ET} = $row->{enforce_timeout};
508             }
509              
510 0 0         if ( defined( $row->{$column} ) ) {
511 0 0         if ( $column eq 'ET' ) {
512 0           $row->{ET} = $row->{enforce_timeout};
513             }
514              
515 0 0 0       if ( ( $column eq 'clock' || $column eq 'added_on' || $column eq 'started_on' )
    0 0        
      0        
516             && $opts{running_time_clip} )
517             {
518 0           $row->{$column} =~ s/\.[0-9]+$//;
519             } elsif ( $column eq 'target' && $opts{running_target_clip} ) {
520 0           $row->{target} =~ s/^.*\///;
521             }
522 0           push( @new_line, $row->{$column} );
523             } else {
524 0           push( @new_line, '' );
525             }
526             } ## end foreach my $column (@columns)
527              
528 0           push( @td, \@new_line );
529             } ## end foreach my $row ( @{$rows} )
530              
531 0           $tb->add_rows( \@td );
532              
533 0           return $tb->draw;
534             } ## end sub get_running_table
535              
536             =head2 get_tasks
537              
538             Returns a array ref of hash refs of rows from the tasks table where the
539             status is set to pending.
540              
541             - where :: The where part of the SQL statement. May not contain a ';'.
542             - Default :: undef
543              
544             - order :: Column to order by.
545             - Default :: id
546              
547             - limit :: Number of items to return.
548             - Default :: 100
549              
550             - direction :: Direction to order in.
551             - Default :: desc
552              
553             use Data::Dumper;
554              
555             A small example showing getting running, ordering by category, and limiting to 20.
556              
557             my $tasks=$cape_utils->get_tasks(where=>"status = 'running'", limit=>20, order=>"category", direction=>'desc');
558             print Dumper($running);
559              
560             =cut
561              
562             sub get_tasks {
563 0     0 1   my ( $self, %opts ) = @_;
564              
565 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
566 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
567             }
568              
569 0 0 0       if ( defined( $opts{order} ) && $opts{order} !~ /^[0-9a-zA-Z]+$/ ) {
570 0           die '$opts{order},"' . $opts{order} . '", does not match /^[0-9a-zA-Z]+$/';
571             } else {
572 0           $opts{order} = 'id';
573             }
574              
575 0 0 0       if ( defined( $opts{limit} ) && $opts{limit} !~ /^[0-9]+$/ ) {
576 0           die '$opts{limit},"' . $opts{limit} . '", does not match /^[0-9]+$/';
577             } else {
578 0           $opts{limit} = '100';
579             }
580              
581 0 0         if ( defined( $opts{direction} ) ) {
582 0           $opts{direction} = lc( $opts{direction} );
583             }
584 0 0 0       if ( defined( $opts{direction} ) && ( $opts{direction} ne 'desc' || $opts{direction} ne 'asc' ) ) {
      0        
585 0           die '$opts{diirection},"' . $opts{direction} . '", does not match desc or asc';
586             } else {
587 0           $opts{direction} = 'desc';
588             }
589              
590 0           my $dbh;
591 0 0         eval { $dbh = $self->connect or die $DBI::errstr };
  0            
592 0 0         if ($@) {
593 0           die( 'Failed to connect to the DB... ' . $@ );
594             }
595              
596 0           my $statement = "select * from tasks";
597 0 0         if ( defined( $opts{where} ) ) {
598 0           $statement = $statement . ' where ' . $opts{where};
599             }
600              
601 0           $statement = $statement . ' order by ' . $opts{order} . ' ' . $opts{direction} . ' limit ' . $opts{limit} . ';';
602              
603 0           my $sth;
604 0           eval {
605 0 0         $sth = $dbh->prepare($statement) or die $DBI::errstr;
606 0 0         $sth->execute or die $DBI::errstr;
607             };
608 0 0         if ($@) {
609 0           die( 'Failed to connect to run the search... ' . $@ );
610             }
611              
612 0           my $row;
613             my @rows;
614 0           while ( $row = $sth->fetchrow_hashref ) {
615 0           push( @rows, $row );
616             }
617              
618 0           $sth->finish;
619 0           $dbh->disconnect;
620              
621 0           return \@rows;
622             } ## end sub get_tasks
623              
624             =head2 get_tasks_count
625              
626             Gets a count of tasks.
627              
628             - where :: The where part of the SQL statement. May not contain a ';'.
629             - Default :: undef
630              
631             use Data::Dumper;
632              
633             A small example showing getting running, ordering by category, and limiting to 20.
634              
635             my $count=$cape_util->get_tasks_count(where=>"status = 'running'", limit=>20, order=>"category", direction=>'desc');
636              
637             =cut
638              
639             sub get_tasks_count {
640 0     0 1   my ( $self, %opts ) = @_;
641              
642 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
643 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
644             }
645              
646 0           my $dbh = $self->connect;
647              
648 0           my $statement = "select * from tasks";
649 0 0         if ( defined( $opts{where} ) ) {
650 0           $statement = $statement . ' ' . $opts{where};
651             }
652              
653 0           my $sth = $dbh->prepare($statement);
654 0           $sth->execute;
655              
656 0           my $rows = $sth->rows;
657              
658 0           $sth->finish;
659 0           $dbh->disconnect;
660              
661 0           return $rows;
662             } ## end sub get_tasks_count
663              
664             =head2 get_tasks_table
665              
666             Generates a ASCII table for tasks.
667              
668             The following config variables can are relevant to this and
669             may be overriden.
670              
671             table_border
672             table_color
673             task_columns
674             task_target_clip
675             task_time_clip
676              
677             The following options are also supported.
678              
679             - where :: Additional SQL where statements to add.
680             - Default :: undef
681              
682             - order :: Column to order by.
683             - Default :: id
684              
685             - limit :: Number of items to return.
686             - Default :: 100
687              
688             - direction :: Direction to order in.
689             - Default :: desc
690              
691             print $cape_util->get_tasks_table( where => "status = 'reported'");
692              
693             =cut
694              
695             sub get_tasks_table {
696 0     0 1   my ( $self, %opts ) = @_;
697              
698 0           my @overrides = ( 'table_border', 'table_color', 'task_columns', 'task_target_clip', 'task_time_clip' );
699 0           foreach my $override (@overrides) {
700 0 0         if ( !defined( $opts{$override} ) ) {
701 0           $opts{$override} = $self->{config}->{_}->{$override};
702             }
703             }
704              
705             my $rows = $self->get_tasks(
706             where => $opts{where},
707             order => $opts{order},
708             limit => $opts{limit},
709             direction => $opts{direction}
710 0           );
711              
712 0           my $tb = Text::ANSITable->new;
713 0           $tb->border_style( $opts{table_border} );
714 0           $tb->color_theme( $opts{table_color} );
715              
716 0           my @columns = split( /,/, $opts{task_columns} );
717 0           my $header_int = 0;
718 0           my $padding = 0;
719 0           foreach my $header (@columns) {
720 0 0         if ( ( $header_int % 2 ) != 0 ) { $padding = 1; }
  0            
721 0           else { $padding = 0; }
722              
723 0           $tb->set_column_style( $header_int, pad => $padding );
724              
725 0           $header_int++;
726             }
727              
728 0           $tb->columns( \@columns );
729              
730 0           my @td;
731 0           my @latest_check = (
732             'started_on', 'analysis_started_on', 'analysis_finished_on', 'analysis_finished_on',
733             'processing_finished_on', 'signatures_started_on', 'signatures_finished_on', 'reporting_started_on',
734             'reporting_finished_on', 'completed_on'
735             );
736 0           foreach my $row ( @{$rows} ) {
  0            
737 0           my @new_line;
738 0           foreach my $column (@columns) {
739 0 0         if ( $column eq 'ET' ) {
740 0           $row->{ET} = $row->{enforce_timeout};
741             }
742              
743 0 0         if ( $column eq 'latest' ) {
744 0           $row->{latest} = '';
745 0           foreach my $item (@latest_check) {
746 0 0         if ( defined( $row->{$item} ) ) {
747 0           $row->{latest} = $row->{$item};
748             }
749             }
750             }
751              
752 0 0         if ( defined( $row->{$column} ) ) {
753 0 0         if ( $column eq 'ET' ) {
754 0           $row->{ET} = $row->{enforce_timeout};
755             }
756              
757 0 0 0       if (
    0 0        
      0        
758             (
759             $column eq 'clock'
760             || $column eq 'added_on'
761             || $column eq 'started_on'
762             || $column eq 'completed_on'
763             || $column eq 'analysis_started_on'
764             || $column eq 'analysis_finished_on'
765             || $column eq 'processing_started_on'
766             || $column eq 'processing_finished_on'
767             || $column eq 'signatures_started_on'
768             || $column eq 'signatures_finished_on'
769             || $column eq 'reporting_started_on'
770             || $column eq 'reporting_finished_on'
771             || $column eq 'latest'
772             )
773             && $opts{task_time_clip}
774             )
775             {
776 0           $row->{$column} =~ s/\.[0-9]+$//;
777             } elsif ( $column eq 'target' && $opts{task_target_clip} ) {
778 0           $row->{target} =~ s/^.*\///;
779             }
780 0           push( @new_line, $row->{$column} );
781             } else {
782 0           push( @new_line, '' );
783             }
784             } ## end foreach my $column (@columns)
785              
786 0           push( @td, \@new_line );
787             } ## end foreach my $row ( @{$rows} )
788              
789 0           $tb->add_rows( \@td );
790              
791 0           return $tb->draw;
792             } ## end sub get_tasks_table
793              
794             =head2 munge
795              
796             Munges the specified report file.
797              
798             $cape_utils->munge(file=>$report_file);
799              
800             =cut
801              
802             sub munge {
803 0     0 1   my ( $self, %opts ) = @_;
804              
805 0 0         if ( !defined( $opts{file} ) ) {
806 0           die('No file specified via $opts{file}');
807             }
808              
809 0 0         if ( !-f $opts{file} ) {
810 0           die( '"' . $opts{file} . '" is not a file' );
811             }
812              
813             # create a backup copy prior to munging
814             # also only create it if it does not exist
815 0           my $pre_munge_file = $opts{file} . '.pre-cape_utils_munge';
816 0 0         if ( !-f $pre_munge_file ) {
817             copy( $opts{file}, $pre_munge_file )
818 0 0         || die( 'Creating pre-munge file for "' . $opts{file} . '" failed... ' . $! );
819             } else {
820 0           warn( 'Pre-munge file, "' . $pre_munge_file . '", already exists, skippying coppying' );
821             }
822              
823             # read the file on in
824 0           my $report;
825 0           eval { $report = decode_json( read_file( $opts{file} ) ); };
  0            
826 0 0         if ($@) {
827 0           die( 'Failed to parse "' . $opts{file} . '"... ' . $@ );
828             }
829              
830             # find the munge keys
831 0           my @sections = sort( keys( %{ $self->{config} } ) );
  0            
832 0           my @munges;
833 0           foreach my $item (@sections) {
834 0 0         if ( $item =~ /^munge\_/ ) {
835 0           push( @munges, $item );
836             }
837             }
838              
839             # should be set by a munge if it made a change
840 0           my $changed = 0;
841             # scratch space for between all munges
842 0           my %all_scratch;
843 0           foreach my $item (@munges) {
844             # scratch space for the munges to use
845 0           my %scratch;
846              
847             # only process the specified munge if we have both keys
848 0 0 0       if ( defined( $self->{config}{$item}{check} ) && defined( $self->{config}{$item}{munge} ) ) {
849             # now that we know we have the keys, get the full file path if needed
850 0           my $check_file = $self->{config}{$item}{check};
851 0           my $munge_file = $self->{config}{$item}{munge};
852 0 0 0       if ( $check_file !~ /^\// && $check_file !~ /^.\// && $check_file !~ /^..\// ) {
      0        
853 0           $check_file = '/usr/local/etc/cape_utils_munge/' . $check_file;
854             }
855 0 0 0       if ( $munge_file !~ /^\// && $munge_file !~ /^.\// && $munge_file !~ /^..\// ) {
      0        
856 0           $munge_file = '/usr/local/etc/cape_utils_munge/' . $munge_file;
857             }
858              
859             # figure out if we need to munge it or not
860 0           my $munge_it = 0;
861 0           eval {
862 0           my $check_code = read_file($check_file);
863 0           eval($check_code);
864 0 0         if ($@) {
865 0           die($@);
866             }
867             };
868 0 0         if ($@) {
869 0           warn( 'Munge "' . $item . '" errored during the check... ' . $@ );
870              
871             # override this even if set before dying
872 0           $munge_it = 0;
873             }
874              
875             # if so, try to munge it
876 0 0         if ($munge_it) {
877 0           eval {
878 0           my $munge_code = read_file($munge_file);
879 0           eval($munge_code);
880 0 0         if ($@) {
881 0           die($@);
882             }
883             };
884 0 0         if ($@) {
885 0           warn( 'Munge "' . $item . '" errored during the munge... ' . $@ );
886             }
887             } ## end if ($munge_it)
888             } else {
889 0           warn( 'Section "' . $item . '" missing either the key "check" or munge"' );
890             }
891             } ## end foreach my $item (@munges)
892              
893             # save the file if it changed
894 0 0         if ($changed) {
895             # if changed, update the malscore
896 0           my $malscore = 0.0;
897 0           my $sig_int = 0;
898 0           while ( defined( $report->{signatures}[$sig_int] ) ) {
899 0 0         if ( $report->{signatures}[$sig_int]{severity} ) {
900             $malscore += $report->{signatures}[$sig_int]{weight} * 0.5
901 0           * ( $report->{signatures}[$sig_int]{confidence} / 100 );
902             } else {
903             $malscore
904             += $report->{signatures}[$sig_int]{weight}
905             * ( $report->{signatures}[$sig_int]{weight} - 1 )
906 0           * ( $report->{signatures}[$sig_int]{confidence} / 100 );
907             }
908              
909 0           $sig_int++;
910             } ## end while ( defined( $report->{signatures}[$sig_int...]))
911 0 0         if ( $malscore > 10.0 ) {
912 0           $malscore = 10.0;
913             }
914 0           $report->{malscore} = $malscore;
915              
916 0           eval { write_file( $opts{file}, encode_json($report) ); };
  0            
917 0 0         if ($@) {
918 0           die( 'Failed to encode updated report post munging and write it to "' . $opts{file} . '"... ' . $@ );
919             }
920             } ## end if ($changed)
921              
922 0           return 1;
923             } ## end sub munge
924              
925             =head2 search
926              
927             Searches the list of tasks. By default everything will be return ed.
928              
929             - where :: Additional SQL where statements to use for searching.
930             May not contain a ';'.
931             - Default :: undef
932              
933             Addtionally there are also helpers for searching. These may not contain either a /\'/
934             or a /\\/. They will be joined via and.
935              
936             The following are handled as a simple equality.
937              
938             - timeout
939             - memory
940             - enforce_timeout
941             - timedout
942              
943             The following are numeric. Each one may accept multiple
944             comma sperated values. The equalities =, >,>=, <=, and !
945             are supported. If no equality is specified, then = is used.
946              
947             - id
948             - timeout
949             - priority
950             - dropped_files
951             - running_processes
952             - api_calls
953             - domains
954             - signatures_total
955             - signatures_alert
956             - files_written
957             - registry_keys_modified
958             - crash_issues
959             - anti_issues
960             - sample_id
961             - machine_id
962              
963             # will result in id >= 3 and id < 44
964             id => '>=3,<44'
965              
966             # either of these will be id = 4
967             id => '=4'
968             id => '4'
969              
970             The following are string items. As is, they
971             are evaluated as a simple equality. If ending with
972             ending in '_like', they will be evaluated as a like.
973              
974             - target
975             - category
976             - custom
977             - machine
978             - package
979             - route
980             - tags_tasks
981             - options
982             - platform
983              
984             # becomes... target = 'foo'
985             target => 'foo'
986              
987             # becomes... target like 'foo%'
988             target_like => 'foo%'
989              
990             =cut
991              
992             sub search {
993 0     0 1   my ( $self, %opts ) = @_;
994              
995 0 0 0       if ( defined( $opts{where} ) && $opts{where} =~ /\;/ ) {
996 0           die '$opts{where},"' . $opts{where} . '", contains a ";"';
997             }
998              
999             #
1000             # make sure all the set variables are not dangerous or potentially dangerous
1001             #
1002              
1003 0           my @to_check = (
1004             'id', 'target', 'route', 'machine',
1005             'timeout', 'priority', 'route', 'tags_tasks',
1006             'options', 'clock', 'added_on', 'started_on',
1007             'completed_on', 'status', 'dropped_files', 'running_processes',
1008             'api_calls', 'domains', 'signatures_total', 'signatures_alert',
1009             'files_written', 'registry_keys_modified', 'crash_issues', 'anti_issues',
1010             'timedout', 'sample_id', 'machine_id', 'parent_id',
1011             'tlp', 'category', 'package'
1012             );
1013              
1014 0           foreach my $var_to_check (@to_check) {
1015 0 0 0       if ( defined( $opts{$var_to_check} ) && $opts{$var_to_check} =~ /[\\\']/ ) {
1016 0           die( '"' . $opts{$var_to_check} . '" for "' . $var_to_check . '" matched /[\\\']/' );
1017             }
1018             }
1019              
1020             #
1021             # init the SQL statement
1022             #
1023              
1024 0           my $sql = "select * from tasks where id >= 0";
1025              
1026 0 0         if ( defined( $opts{where} ) ) {
1027 0           $sql = $sql . ' AND ' . $opts{where};
1028             }
1029              
1030             #
1031             # add simple items
1032             #
1033              
1034 0           my @simple = ( 'timeout', 'memory', 'enforce_timeout', 'timedout' );
1035              
1036 0           foreach my $item (@simple) {
1037 0 0         if ( defined( $opts{$item} ) ) {
1038 0           $sql = $sql . " and " . $item . " = '" . $opts{$item} . "'";
1039             }
1040             }
1041              
1042             #
1043             # add numeric items
1044             #
1045              
1046 0           my @numeric = (
1047             'id', 'timeout', 'priority', 'dropped_files',
1048             'running_processes', 'api_calls', 'domains', 'signatures_total',
1049             'signatures_alert', 'files_written', 'registry_keys_modified', 'crash_issues',
1050             'anti_issues', 'sample_id', 'machine_id'
1051             );
1052              
1053 0           foreach my $item (@numeric) {
1054 0 0         if ( defined( $opts{$item} ) ) {
1055              
1056             # remove and tabs or spaces
1057 0           $opts{$item} =~ s/[\ \t]//g;
1058 0           my @arg_split = split( /\,/, $opts{$item} );
1059              
1060             # process each item
1061 0           foreach my $arg (@arg_split) {
1062              
1063             # match the start of the item
1064 0 0         if ( $arg =~ /^[0-9]+$/ ) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1065 0           $sql = $sql . " and " . $item . " = '" . $arg . "'";
1066             } elsif ( $arg =~ /^\=[0-9]+$/ ) {
1067 0           $arg =~ s/^\=//;
1068 0           $sql = $sql . " and " . $item . " <= '" . $arg . "'";
1069             } elsif ( $arg =~ /^\<\=[0-9]+$/ ) {
1070 0           $arg =~ s/^\<\=//;
1071 0           $sql = $sql . " and " . $item . " <= '" . $arg . "'";
1072             } elsif ( $arg =~ /^\<[0-9]+$/ ) {
1073 0           $arg =~ s/^\<//;
1074 0           $sql = $sql . " and " . $item . " < '" . $arg . "'";
1075             } elsif ( $arg =~ /^\>\=[0-9]+$/ ) {
1076 0           $arg =~ s/^\>\=//;
1077 0           $sql = $sql . " and " . $item . " >= '" . $arg . "'";
1078             } elsif ( $arg =~ /^\>[0-9]+$/ ) {
1079 0           $arg =~ s/^\>\=//;
1080 0           $sql = $sql . " and " . $item . " > '" . $arg . "'";
1081             } elsif ( $arg =~ /^\![0-9]+$/ ) {
1082 0           $arg =~ s/^\!//;
1083 0           $sql = $sql . " and " . $item . " != '" . $arg . "'";
1084             } elsif ( $arg =~ /^$/ ) {
1085              
1086             # only exists for skipping when some one has passes something starting
1087             # with a ,, ending with a,, or with ,, in it.
1088             } else {
1089             # if we get here, it means we don't have a valid use case for what ever was passed and should error
1090 0           die( '"' . $arg . '" does not appear to be a valid item for a numeric search for the ' . $item );
1091             }
1092             } ## end foreach my $arg (@arg_split)
1093             } ## end if ( defined( $opts{$item} ) )
1094             } ## end foreach my $item (@numeric)
1095              
1096             #
1097             # handle string items
1098             #
1099              
1100             my @strings
1101 0           = ( 'target', 'category', 'custom', 'machine', 'package', 'route', 'tags_tasks', 'options', 'platform', );
1102              
1103 0           foreach my $item (@strings) {
1104 0 0         if ( defined( $opts{$item} ) ) {
1105 0 0 0       if ( defined( $opts{ $item . '_like' } ) && $opts{ $item . '_like' } ) {
1106 0           $sql = $sql . " and host like '" . $opts{$item} . "'";
1107             } else {
1108 0           $sql = $sql . " and " . $item . " = '" . $opts{$item} . "'";
1109             }
1110             }
1111             }
1112              
1113             #
1114             # finalize it and search
1115             #
1116              
1117 0           $sql = $sql . ';';
1118              
1119 0           my $dbh = $self->connect;
1120 0           my $sth = $dbh->prepare($sql);
1121              
1122 0           $sth->execute;
1123              
1124 0           my $rows;
1125              
1126 0           return $rows;
1127             } ## end sub search
1128              
1129             =head2 submit
1130              
1131             Submits files to CAPE.
1132              
1133             - clock :: Timestamp to use for setting the clock to of the VM for
1134             when executing the item. If left undefined, it will be
1135             autogenerated.
1136             - Format :: mm-dd-yyy HH:MM:ss
1137              
1138             - items :: A array ref of items to submit. If a directory is listed in
1139             here, it will be read, but subdirectories will not be recursed. They
1140             will be ignored.
1141              
1142             - name_regex :: Regex to use for matching items in a submitted dir.
1143             Only used if the a submitted item is a dir.
1144             - Default :: undef
1145              
1146             - mime_regex :: Array ref of desired mime types to match via
1147             regex. Only used if the a submitted item is a dir.
1148             - Default :: undef
1149              
1150             - timeout :: Value to use for timeout. Set to 0 to not enforce.
1151             - Default :: 200
1152              
1153             - machine :: The machine to use for this. If not defined, first
1154             available will be used.
1155             - Default :: undef
1156              
1157             - package :: Package to use, if not letting CAPE decide.
1158             - Default :: undef
1159              
1160             - options :: Option string to be passed via --options.
1161             - Default :: undef
1162              
1163             - random :: If it should randomize the order of submission.
1164             - Default :: 1
1165              
1166             - tags :: Tags to be passed to the script via --tags.
1167             - Default :: undef
1168              
1169             - platform :: What to pass to --platform.
1170             - Default :: undef
1171              
1172             - custom :: Any custom values to pass via --custom.
1173             - Default :: undef
1174              
1175             - enforce_timeout :: Force it to run the entire period.
1176             - Default :: 0
1177              
1178             - unique :: Only submit it if it is unique.
1179             - Default :: 0
1180              
1181             -quiet :: Do not print the results.
1182             - Default :: 0
1183              
1184             The retuned value is a hash ref where the keys are successfully submitted files
1185             and values of those keys is the task ID.
1186              
1187             my $sub_results=$cape_util->submit(items=>@to_detonate,unique=>0, quiet=>1);
1188             use JSON;
1189             print encode_json($sub_results)."\n";
1190              
1191             =cut
1192              
1193             sub submit {
1194 0     0 1   my ( $self, %opts ) = @_;
1195              
1196 0 0         if ( !defined( $opts{items}[0] ) ) {
1197 0           die 'No items to submit passed';
1198             }
1199              
1200 0 0 0       if ( !defined( $opts{clock} ) && $self->{config}->{_}->{set_clock_to_now} ) {
1201 0           $opts{clock} = $self->timestamp;
1202             }
1203              
1204 0 0         if ( !defined( $opts{timeout} ) ) {
1205 0           $opts{timeout} = $self->{config}->{_}->{timeout};
1206             }
1207              
1208 0 0         if ( !defined( $opts{enforce_timeout} ) ) {
1209 0           $opts{enforce_timeout} = $self->{config}->{_}->{enforce_timeout};
1210             }
1211              
1212 0           my @to_submit;
1213              
1214 0           foreach my $item ( @{ $opts{items} } ) {
  0            
1215 0 0         if ( -f $item ) {
    0          
1216 0           push( @to_submit, File::Spec->rel2abs($item) );
1217             } elsif ( -d $item ) {
1218 0           opendir( my $dh, $item );
1219 0           while ( readdir($dh) ) {
1220 0 0         if ( -f $item . '/' . $_ ) {
1221 0           push( @to_submit, File::Spec->rel2abs( $item . '/' . $_ ) );
1222             }
1223             }
1224 0           closedir($dh);
1225             }
1226             } ## end foreach my $item ( @{ $opts{items} } )
1227              
1228 0 0         chdir( $self->{config}->{_}->{base} ) || die( 'Unable to CD to "' . $self->{config}->{_}->{base} . '"' );
1229              
1230 0           my @to_run = ();
1231              
1232 0 0         if ( $self->{config}->{_}->{poetry} ) {
1233 0           push( @to_run, 'poetry', 'run' );
1234             }
1235              
1236 0           push( @to_run, 'python3', $self->{config}->{_}->{base} . '/utils/submit.py' );
1237              
1238 0 0         if ( defined( $opts{clock} ) ) {
1239 0           push( @to_run, '--clock', $opts{clock} );
1240             }
1241              
1242 0 0 0       if ( defined( $opts{unique} ) && $opts{unique} ) {
1243 0           push( @to_run, '--unique' );
1244             }
1245              
1246 0 0         if ( defined( $opts{timeout} ) ) {
1247 0           push( @to_run, '--timeout', $opts{timeout} );
1248             }
1249              
1250 0 0 0       if ( $opts{enforce_timeout} && $opts{enforce_timeout} ) {
1251 0           push( @to_run, '--enforce-timeout' );
1252             }
1253              
1254 0 0         if ( defined( $opts{package} ) ) {
1255 0           push( @to_run, '--package', $opts{package} );
1256             }
1257              
1258 0 0         if ( defined( $opts{machine} ) ) {
1259 0           push( @to_run, '--machine', $opts{machine} );
1260             }
1261              
1262 0 0         if ( defined( $opts{options} ) ) {
1263 0           push( @to_run, '--options', $opts{options} );
1264             }
1265              
1266 0 0         if ( defined( $opts{tags} ) ) {
1267 0           push( @to_run, '--tags', $opts{tags} );
1268             }
1269              
1270 0           my $added = {};
1271 0           foreach (@to_submit) {
1272 0           my @tmp_to_run = @to_run;
1273 0           push( @tmp_to_run, $_ );
1274 0           my ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf ) = run(
1275             command => \@tmp_to_run,
1276             verbose => 0
1277             );
1278 0           my $results = join( '', @{$full_buf} );
  0            
1279 0 0         if ( !$opts{quiet} ) {
1280 0           print $results;
1281             }
1282              
1283 0           my @results_split = split( /\n/, $results );
1284 0           foreach my $item (@results_split) {
1285 0           $item =~ s/\e\[[0-9;]*m(?:\e\[K)?//g;
1286 0           chomp($item);
1287 0 0         if ( $item =~ /^Success\:\ File\ \".*\"\ added\ as\ task\ with\ ID\ \d+$/ ) {
1288 0           $item =~ s/^Success\:\ File\ \"//;
1289 0           my ( $file, $task ) = split( /\"\ added\ as\ task\ with\ ID\ /, $item );
1290 0           $added->{$file} = $task;
1291             }
1292             }
1293             } ## end foreach (@to_submit)
1294              
1295 0           return $added;
1296             } ## end sub submit
1297              
1298             =head2 timestamp
1299              
1300             Creates a timestamp to be used with utils/submit. localtime
1301             is used to get the current time.
1302              
1303             print $cape_util->timestamp."\n";
1304              
1305             =cut
1306              
1307             sub timestamp {
1308 0     0 1   my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime;
1309 0           $year += 1900;
1310 0           $mon++;
1311 0 0         if ( $sec < 10 ) {
1312 0           $sec = '0' . $sec;
1313             }
1314 0 0         if ( $min < 10 ) {
1315 0           $min = '0' . $min;
1316             }
1317 0 0         if ( $hour < 10 ) {
1318 0           $hour = '0' . $hour;
1319             }
1320 0 0         if ( $mon < 10 ) {
1321 0           $mon = '0' . $mon;
1322             }
1323 0 0         if ( $mday < 10 ) {
1324 0           $mday = '0' . $mday;
1325             }
1326              
1327 0           return $mon . '-' . $mday . '-' . $year . ' ' . $hour . ':' . $min . ':' . $sec;
1328             } ## end sub timestamp
1329              
1330             =head2 shuffle
1331              
1332             Performa a Fisher Yates shuffle on the passed array ref.
1333              
1334             =cut
1335              
1336             sub shuffle {
1337 0     0 1   my $self = shift;
1338 0           my $array = shift;
1339 0           my $i;
1340 0           for ( $i = @$array; --$i; ) {
1341 0           my $j = int rand( $i + 1 );
1342 0 0         next if $i == $j;
1343 0           @$array[ $i, $j ] = @$array[ $j, $i ];
1344             }
1345 0           return $array;
1346             } ## end sub shuffle
1347              
1348             =head2 check_remote
1349              
1350             Checks the remote connection.
1351              
1352             Two variablesare required, API key and IP.
1353              
1354             $results=$cape_utils->check_remote(apikey=>$apikey, remote=>$remote_ip);
1355             if (!$results){
1356             print "unauthed\n";
1357             return;
1358             }
1359              
1360             =cut
1361              
1362             sub check_remote {
1363 0     0 1   my ( $self, %opts ) = @_;
1364              
1365             # if we don't have a API key, we can only auth via IP
1366 0 0 0       if ( ( $self->{config}->{_}->{auth} ne 'ip' || $self->{config}->{_}->{auth} ne 'either' )
      0        
1367             && !defined( $opts{apikey} ) )
1368             {
1369 0           return 0;
1370             }
1371              
1372             # make sure the API key is what it is expecting if we are not using IP only
1373 0 0 0       if ( $self->{config}->{_}->{auth} ne 'ip'
      0        
1374             && defined( $opts{apikey} )
1375             && $opts{apikey} ne $self->{config}->{_}->{apikey} )
1376             {
1377             # don't return if it is either as IP may still go off
1378 0 0         if ( $self->{config}->{_}->{auth} ne 'either' ) {
1379 0           return 0;
1380             }
1381             }
1382              
1383             # if we have a apikey and method is set to apikey or either, we are good to return true
1384 0 0 0       if ( defined( $opts{apikey} )
      0        
1385             && ( $self->{config}->{_}->{auth} ne 'apikey' || $self->{config}->{_}->{auth} ne 'either' ) )
1386             {
1387 0           return 1;
1388             }
1389              
1390             # can't do anything else with out a IP
1391 0 0         if ( !defined( $opts{ip} ) ) {
1392 0           return 0;
1393             }
1394              
1395 0           my $subnets_string = $self->{config}->{_}->{subnets};
1396 0           $subnets_string =~ s/[\ \t]+//g;
1397 0           $subnets_string =~ s/\,+/,/g;
1398 0           my @subnets_split = split( /,/, $subnets_string );
1399 0           my @subnets;
1400 0           foreach my $item (@subnets_split) {
1401 0 0         if ( $item =~ /^[\:A-Fa-f0-9]+$/ ) {
    0          
    0          
    0          
1402 0           push( @subnets, $item . '/128' );
1403             } elsif ( $item =~ /^[\:A-Fa-f0-9]+\/[0-9]+$/ ) {
1404 0           push( @subnets, $item );
1405             } elsif ( $item =~ /^[\.0-9]+$/ ) {
1406 0           push( @subnets, $item . '/32' );
1407             } elsif ( $item =~ /^[\.0-9]+\/[0-9]+$/ ) {
1408 0           push( @subnets, $item );
1409             }
1410             } ## end foreach my $item (@subnets_split)
1411 0           my $allowed_subnets;
1412 0           eval { $allowed_subnets = subnet_matcher(@subnets); };
  0            
1413 0 0         if ($@) {
1414 0           die( 'Failed it init subnet matcher... ' . $@ );
1415             }
1416              
1417 0 0         if ( $allowed_subnets->( $opts{ip} ) ) {
1418 0           return 1;
1419             }
1420              
1421 0           return 0;
1422             } ## end sub check_remote
1423              
1424             =head2 eve_process
1425              
1426             Process the finished tasks for CAPEv2.
1427              
1428             $cape_utils->eve_process;
1429              
1430             =cut
1431              
1432             sub eve_process {
1433 0     0 1   my ( $self, %opts ) = @_;
1434              
1435 0           my $dbh;
1436 0 0         eval { $dbh = $self->connect or die $DBI::errstr };
  0            
1437 0 0         if ($@) {
1438 0           die( 'Failed to connect to the DB... ' . $@ );
1439             }
1440              
1441             my $statement
1442             = "select * from tasks where ( status = 'reported' ) AND ( completed_on >= CURRENT_TIMESTAMP - interval '"
1443             . $self->{config}{_}{eve_look_back}
1444 0           . " seconds' )";
1445              
1446 0           my $sth = $dbh->prepare($statement);
1447 0           $sth->execute;
1448              
1449 0           my $row;
1450             my @rows;
1451 0           while ( $row = $sth->fetchrow_hashref ) {
1452 0           push( @rows, $row );
1453             }
1454              
1455 0           $sth->finish;
1456 0           $dbh->disconnect;
1457              
1458 0           my $main_eve = $self->{config}{_}{eve};
1459              
1460 0           foreach my $row (@rows) {
1461 0           my $report = $self->{config}{_}{base} . '/storage/analyses/' . $row->{id} . '/reports/lite.json';
1462 0           my $id_eve = $self->{config}{_}{incoming_json} . '/' . $row->{id} . '.eve.json';
1463 0           my $incoming_json = $self->{config}{_}{incoming_json} . '/' . $row->{id} . '.json';
1464              
1465             # make sure we have the required files and they are accessible
1466             # id_eve is being used as a lock file to make sure we don't reprocess it
1467 0 0 0       if ( -f $report && -r $report && !-f $id_eve ) {
      0        
1468 0           my $eve_json;
1469             # the incoming json needs to exist if the following is to work
1470             # if it does not, just a mostly empty one
1471 0 0         if ( -f $incoming_json ) {
1472 0           eval {
1473 0           $eve_json = decode_json( read_file($incoming_json) );
1474 0           $eve_json->{cape_eve_process} = { incoming_json_error => undef, };
1475             };
1476 0 0         if ($@) {
1477 0           my $error_message = 'Failed to decode incoming JSON for ' . $row->{id} . ' ... ' . $@;
1478 0           $self->log_drek( 'cape_eve_process', 'err', $error_message );
1479 0           $eve_json = {
1480             cape_eve_process => {
1481             incoming_json_error => $error_message,
1482             },
1483             };
1484             }
1485             } else {
1486 0           $eve_json = { cape_eve_process => {}, };
1487             }
1488              
1489             # sets various common items so they don't need to be dealt with more than once
1490             # hash creation
1491 0           $eve_json->{cape_eve_process}{time} = time;
1492 0           $eve_json->{cape_eve_process}{host} = hostname;
1493 0           $eve_json->{row} = $row;
1494 0           $eve_json->{event_type} = 'potential_malware_detonation';
1495 0 0         if ( !defined( $eve_json->{cape_eve_process}{incoming_json_error} ) ) {
1496 0           $eve_json->{cape_eve_process}{incoming_json_error} = undef,;
1497             }
1498              
1499 0           my $lite_json;
1500 0           eval {
1501 0           $lite_json = decode_json( read_file($report) );
1502              
1503 0 0         if ( defined( $lite_json->{signatures} ) ) {
1504 0           $eve_json->{signatures} = $lite_json->{signatures};
1505             }
1506              
1507 0 0         if ( defined( $lite_json->{malscore} ) ) {
1508 0           $eve_json->{malscore} = $lite_json->{malscore};
1509              
1510 0 0         if ( $lite_json->{malscore} >= $self->{config}{_}{malscore} ) {
1511 0           $eve_json->{event_type} = 'alert';
1512             }
1513             }
1514             };
1515 0 0         if ($@) {
1516 0           my $error_message = 'Failed to decode lite.json for ' . $row->{id} . ' ... ' . $@;
1517 0           $self->log_drek( 'cape_eve_process', 'err', $error_message );
1518 0           $eve_json->{cape_eve_process}{lite_json_error} = $error_message,;
1519             }
1520              
1521             # new line is needed as encode_json does not add one and this prevents the eve file
1522             # from being one long line when appended to
1523 0           my $raw_eve_json = encode_json($eve_json) . "\n";
1524              
1525 0           eval { write_file( $id_eve, $raw_eve_json ); };
  0            
1526 0 0         if ($@) {
1527 0           my $error_message = 'Failed to write out ID EVE for ' . $row->{id} . ' at ' . $id_eve . ' ... ' . $@;
1528 0           $self->log_drek( 'cape_eve_process', 'err', $error_message );
1529             }
1530              
1531 0           eval { append_file( $self->{config}{_}{eve}, $raw_eve_json ); };
  0            
1532             } else {
1533 0 0 0       if ( !-f $report || !-r $report ) {
1534 0           warn( $row->{id} . ' reported, but lite.json does not exist for it or it is not readable' );
1535             }
1536             }
1537             } ## end foreach my $row (@rows)
1538              
1539             } ## end sub eve_process
1540              
1541             # sends stuff to syslog
1542             sub log_drek {
1543 0     0 0   my ( $self, $sender, $level, $message ) = @_;
1544              
1545 0 0         if ( !defined($level) ) {
1546 0           $level = 'info';
1547             }
1548              
1549 0 0         if ( !defined($sender) ) {
1550 0           $sender = 'CAPE::Utils';
1551             }
1552              
1553 0           openlog( $sender, 'cons,pid', 'daemon' );
1554 0           syslog( $level, '%s', $message );
1555 0           closelog();
1556             } ## end sub log_drek
1557              
1558             =head1 CONFIG FILE
1559              
1560             The default config file is '/usr/local/etc/cape_utils.ini'.
1561              
1562             The defaults are as below, which out of the box, it will work by
1563             default with CAPEv2 in it's default config.
1564              
1565             # The DBI dsn to use
1566             dsn=dbi:Pg:dbname=cape
1567             # DB user
1568             user=cape
1569             # DB password
1570             pass=
1571             # the install base for CAPEv2
1572             base=/opt/CAPEv2/
1573             # 0/1 if poetry should be used
1574             poetry=1
1575             # 0/1 if fail should be allowed to run with out a where statement
1576             fail_all=0
1577             # colums to use for pending table show
1578             pending_columns=id,target,package,timeout,ET,route,options,clock,added_on
1579             # colums to use for runniong table show
1580             running_columns=id,target,package,timeout,ET,route,options,clock,added_on,started_on,machine
1581             # colums to use for tasks table
1582             task_columns=id,target,package,timeout,ET,route,options,clock,added_on,latest,machine,status
1583             # if the target column for running table display should be clipped to the filename
1584             running_target_clip=1
1585             # if microseconds should be clipped from time for running table display
1586             running_time_clip=1
1587             # if the target column for pending table display should be clipped to the filename
1588             pending_target_clip=1
1589             # if microseconds should be clipped from time for pending table display
1590             pending_time_clip=1
1591             # if the target column for task table display should be clipped to the filename
1592             task_target_clip=1
1593             # if microseconds should be clipped from time for task table display
1594             task_time_clip=1
1595             # default table color
1596             table_color=Text::ANSITable::Standard::NoGradation
1597             # default table border
1598             table_border=ASCII::None
1599             # when submitting use now for the current time
1600             set_clock_to_now=1
1601             # default timeout value for submit
1602             timeout=200
1603             # default value for enforce timeout for submit
1604             enforce_timeout=0
1605             # how to auth for mojo_cape_submit
1606             # ip = match against subnets
1607             # apikey = use apikey
1608             # both = require both to match
1609             # either = either may work
1610             auth=ip
1611             # the api key to for with mojo_cape_submit
1612             #apikey=
1613             # comma seperated list of allowed subnets for mojo_cape_submit
1614             subnets=192.168.0.0/16,127.0.0.1/8,::1/128,172.16.0.0/12,10.0.0.0/8
1615             # incoming dir to use for mojo_cape_submit
1616             incoming=/malware/client-incoming
1617             # directory to store json data files for submissions recieved by mojo_cape_submit
1618             # this directory is also used for storing run specific eves
1619             incoming_json=/malware/incoming-json
1620             # Location to write the eve log to.
1621             eve=/opt/CAPEv2/log/eve.json
1622             # how far to go back for processing eve
1623             eve_look_back=360
1624             # malscore for changing the event_type for eve from potential_malware_detonation to alert
1625             malscore=0
1626              
1627             =head2 Report Munge Section
1628              
1629             INI sections matching /^munge\_/ will be used for report munging. This requires two values for that sections,
1630             'check' and 'munge'.
1631              
1632             'check' is a path to a Perl script that will wrapped in a eval and require to check if the file should be
1633             munged or not.
1634              
1635             'munge' is a path to a Perl script that will wrapped in a eval and require to do the munging.
1636              
1637             Below is a example showing the setup for a single script.
1638              
1639             [munge_pdf]
1640             check=/usr/local/etc/cape_utils_munge/pdf_check
1641             munge=/usr/local/etc/cape_utils_munge/pdf_munge
1642              
1643             If more than one munge section exists, they are ran in sorted order.
1644              
1645             If the paths specied do not start with a '/', './', or '../', then '/usr/local/etc/cape_utils_munge/' is
1646             applied to the start.
1647              
1648             The scripts are read as evaled strings.
1649              
1650             The relevant variables are as below.
1651              
1652             - $munge_it :: Perl boolean for if it should be munged or not. Should be set by the check script.
1653              
1654             - $report :: The hash ref containing the parsed JSON report.
1655              
1656             - $changed :: Perl boolean for if it changed or not.
1657              
1658             For some examples see the directory 'munge_examples'.
1659              
1660             =head1 CAPEv2 lite.json to EVE handling
1661              
1662             Tasks are found by looking back X number of seconds in the tasks table for tasks that have reported.
1663             The amount of time is determined by the config value 'eve_look_back'.
1664              
1665             It will check if a task has been processed already or not be seeing if a task specified EVE JSON
1666             has been created under the 'incoming_json' directory. This is in the format $task_id.'eve.json'.
1667             If not, it will proceed.
1668              
1669             It reads the 'lite.json' report for task as well as the incoming JSON. It then copies the keys
1670             'signatures' and 'malscore' into the hash for the incoming JSON and writes it out to
1671             $task_id.'eve.json' and appending it to the file specified via the config value 'eve'.
1672              
1673             The are two possible values for 'event_type', 'potential_malware_detonation' and 'alert'.
1674             'potential_malware_detonation' is changed to alert when 'malscore' goves over the value
1675             specified via config value 'malscore'.
1676              
1677             'row' is the full row for the task in question from the task table as a hash.
1678              
1679             'signatures' is copied from '.signature' in the report JSON.
1680              
1681             =head1 AUTHOR
1682              
1683             Zane C. Bowers-Hadley, C<< <vvelox at vvelox.net> >>
1684              
1685             =head1 BUGS
1686              
1687             Please report any bugs or feature requests to C<bug-cape-utils at rt.cpan.org>, or through
1688             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=CAPE-Utils>. I will be notified, and then you'll
1689             automatically be notified of progress on your bug as I make changes.
1690              
1691             =head1 SUPPORT
1692              
1693             You can find documentation for this module with the perldoc command.
1694              
1695             perldoc CAPE::Utils
1696              
1697              
1698             You can also look for information at:
1699              
1700             =over 4
1701              
1702             =item * RT: CPAN's request tracker (report bugs here)
1703              
1704             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CAPE-Utils>
1705              
1706             =item * Search CPAN
1707              
1708             L<https://metacpan.org/release/CAPE-Utils>
1709              
1710             =item * Git
1711              
1712             L<git@github.com:VVelox/CAPE-Utils.git>
1713              
1714             =item * Web
1715              
1716             L<https://github.com/VVelox/CAPE-Utils>
1717              
1718             =back
1719              
1720              
1721             =head1 ACKNOWLEDGEMENTS
1722              
1723              
1724             =head1 LICENSE AND COPYRIGHT
1725              
1726             This software is Copyright (c) 2022 by Zane C. Bowers-Hadley.
1727              
1728             This is free software, licensed under:
1729              
1730             The Artistic License 2.0 (GPL Compatible)
1731              
1732              
1733             =cut
1734              
1735             1; # End of CAPE::Utils