File Coverage

blib/lib/Virani.pm
Criterion Covered Total %
statement 44 406 10.8
branch 0 228 0.0
condition 0 222 0.0
subroutine 15 29 51.7
pod 11 14 78.5
total 70 899 7.7


line stmt bran cond sub pod time code
1             package Virani;
2              
3 1     1   69007 use 5.006;
  1         4  
4 1     1   4 use strict;
  1         2  
  1         38  
5 1     1   5 use warnings;
  1         1  
  1         23  
6 1     1   570 use TOML;
  1         29588  
  1         52  
7 1     1   538 use File::Slurp;
  1         31054  
  1         60  
8 1     1   485 use Net::Subnet;
  1         5917  
  1         63  
9 1     1   512 use File::Find::IncludesTimeRange;
  1         12355  
  1         56  
10 1     1   521 use File::Find::Rule;
  1         9266  
  1         8  
11 1     1   52 use Digest::MD5 qw(md5_hex);
  1         2  
  1         66  
12 1     1   9 use File::Spec;
  1         2  
  1         21  
13 1     1   756 use IPC::Cmd qw(run);
  1         44275  
  1         59  
14 1     1   518 use File::Copy "cp";
  1         2567  
  1         59  
15 1     1   695 use Sys::Syslog;
  1         8873  
  1         62  
16 1     1   787 use JSON;
  1         8938  
  1         7  
17 1     1   113 use Time::Piece;
  1         3  
  1         10  
18              
19             =head1 NAME
20              
21             Virani - PCAP retrieval for a FPC setup writing to PCAP files.
22              
23             =head1 VERSION
24              
25             Version 1.1.0
26              
27             =cut
28              
29             our $VERSION = '1.1.0';
30              
31             =head1 SYNOPSIS
32              
33             use Virani;
34              
35             my $virani = Virani->new();
36             ...
37              
38             =head1 METHODS
39              
40             =head2 new_from_conf
41              
42             Initiates the Virani object from the specified file.
43              
44             - conf :: The config TOML to use.
45             - Default :: /usr/local/etc/virani.toml
46              
47             =cut
48              
49             sub new_from_conf {
50 0     0 1   my ( $blank, %opts ) = @_;
51              
52 0 0         if ( !defined( $opts{conf} ) ) {
53 0           $opts{conf} = '/usr/local/etc/virani.toml';
54             }
55              
56 0 0         if ( !-f $opts{conf} ) {
57 0           die( "'" . $opts{conf} . "' is not a file or does not exist" );
58             }
59              
60 0           my $raw_toml;
61 0           eval { $raw_toml = read_file( $opts{conf} ); };
  0            
62 0 0 0       if ( $@ || !defined($raw_toml) ) {
63 0           my $error = 'Failed to read config file, "' . $opts{conf} . '"';
64 0 0         if ($@) {
65 0           $error = $error . ' ' . $@;
66             }
67 0           die($error);
68             }
69              
70 0           my $toml;
71 0           eval { $toml = from_toml($raw_toml); };
  0            
72 0 0         if ($@) {
73 0           die($@);
74             }
75              
76 0           return Virani->new( %{$toml} );
  0            
77             } ## end sub new_from_conf
78              
79             =head2 new
80              
81             Initiates the object.
82              
83             - allowed_subnets :: The allowed subnets for fetching PCAPs for mojo-varini.
84             Defaults :: [ '192.168.0.0/', '127.0.0.1/8', '::1/127', '172.16.0.0/12' ]
85              
86             - apikey :: Optional API key for mojo-varini.
87             Defaults :: undef
88              
89             - auth_by_IP_only :: Auth by IP only and don't use a API key.
90             Default :: 1
91              
92             - default_set :: The default set to use.
93             Default :: default
94              
95             - cache :: Cache directory to write to.
96             Default :: /var/cache/virani
97              
98             - default_regex :: The regex to use for getting the timestamp. The regex to pass to
99             File::Find::IncludesTimeRange for finding PCAP files with timestamps
100             that include the range in question.
101             Default :: (?\\d\\d\\d\\d\\d\\d+)(\\.pcap|(?\\.\\d+)\\.pcap)$
102              
103             - verbose_to_syslog :: Send verbose items to syslog. This is used by mojo-virani.
104             Default :: 0
105              
106             - pcap_glob :: The glob to use for matching files.
107             Default :: *.pcap*
108              
109             - ts_is_unixtime :: The timestamp is unixtime and does not requires additional processing.
110             Default :: 1
111              
112             - verbose :: Print verbose info.
113             Default :: 1
114              
115             - type :: Either tcpdump, tshark, or bpf2tshark, which to use for filtering PCAP files in the
116             specified time slot. tcpdump is faster, but in general will not nicely handles
117             some VLAN types. For that tshark is needed, but it is signfigantly slower. bpf2tshark
118             is handled via Virani->bpf2tshark and that should be seen for more info on that.
119             Default :: tcpdump
120              
121             - padding :: How many seconds to add to the start and end time stamps to ensure the specified
122             time slot is definitely included.
123             Default :: 5
124              
125             - sets :: A hash of hashes of available sets.
126             Default :: { default => { path => '/var/log/daemonlogger' } }
127              
128             For sets, the following keys are usable, of which only path is required.
129              
130             - path :: The base path of which the PCAPs are located.
131              
132             - padding :: Padding value for this set.
133              
134             - regex :: The timestamp regex to use with this set.
135              
136             - type :: The default filter type to use with this set.
137              
138             - ts_is_unixtime :: The timestamp is unixtime and does not requires additional processing.
139              
140             =cut
141              
142             sub new {
143 0     0 1   my ( $blank, %opts ) = @_;
144              
145 0           my $self = {
146             allowed_subnets => [ '192.168.0.0/', '127.0.0.1/8', '::1/127', '172.16.0.0/12' ],
147             apikey => undef,
148             auth_by_IP_only => 1,
149             default_set => 'default',
150             cache => '/var/cache/virani',
151             default_regex => '(?\\d\\d\\d\\d\\d\\d+)(\\.pcap|(?\\.\\d+)\\.pcap)$',
152             default_max_time => '3600',
153             verbose_to_syslog => 0,
154             verbose => 1,
155             type => 'tcpdump',
156             padding => 5,
157             ts_is_unixtime => 1,
158             pcap_glob => '*.pcap*',
159             sets => {
160             default => {
161             path => '/var/log/daemonlogger',
162             }
163             },
164              
165             };
166 0           bless $self;
167              
168 0 0 0       if ( defined( $opts{allowed_subnets} ) && ref( $opts{allowed_subnets} ) eq 'ARRAY' ) {
    0 0        
169 0           $self->{allowed_subnets} = $opts{allowed_subnets};
170             } elsif ( defined( $opts{allowed_subnets} ) && ref( $opts{allowed_subnets} ) ne 'ARRAY' ) {
171 0           die("$opts{allowed_subnets} defined, but not a array");
172             }
173              
174 0 0 0       if ( defined( $opts{sets} ) && ref( $opts{sets} ) eq 'HASH' ) {
    0 0        
175 0           $self->{sets} = $opts{sets};
176             } elsif ( defined( $opts{sets} ) && ref( $opts{allowed_subnets} ) ne 'HASH' ) {
177 0           die("$opts{sets} defined, but not a hash");
178             }
179              
180             # real in basic values
181 0           my @real_in = (
182             'apikey', 'default_set', 'cache', 'padding',
183             'default_max_time', 'verbose_to_syslog', 'verbose', 'auth_by_IP_only',
184             'type', 'ts_is_unixtime', 'pcap_glob', 'default_regex'
185             );
186 0           for my $key (@real_in) {
187 0 0         if ( defined( $opts{$key} ) ) {
188 0           $self->{$key} = $opts{$key};
189             }
190             }
191              
192 0           return $self;
193             } ## end sub new
194              
195             =head2 bpf2tshark
196              
197             Does a quick and dumb conversion of a BPF filter to tshark.
198              
199             my $tshark=$virani->bpf2tshark($bpf);
200              
201              
202              
203             () -> ()
204             not () -> !()
205              
206             icmp -> icmp
207             tcp -> tcp
208             udp -> udp
209              
210             port $port -> ( tcp.port == $port or udp.port == $port )
211             not port $port -> ( tcp.port != $port or udp.port != $port )
212              
213             dst port $port -> ( tcp.dstport == $port or udp.dstport == $port )
214             not dst port $port -> ( tcp.dstport != $port or udp.dstport != $port )
215              
216             src port $port -> ( tcp.srcport == $port or udp.srcport == $port )
217             not src port $port -> ( tcp.srcport != $port or udp.srcport != $port )
218              
219             host $host -> ip.addr == $host
220             not host $host -> ip.addr != $host
221              
222             dst host $host -> ip.dst == $host
223             not dst host $host -> ip.dst != $host
224              
225             src host $host -> ip.src == $host
226             not src host $host -> ip.src != $host
227              
228             dst $host -> ip.dst == $host
229             not host $host -> ip.dst != $host
230              
231             src src $host -> ip.src == $host
232             not src $host -> ip.src != $host
233              
234             =cut
235              
236             sub bpf2tshark {
237 0     0 1   my $self = $_[0];
238 0           my $bpf = $_[1];
239              
240 0 0         if ( !defined($bpf) ) {
241 0           return '';
242             }
243              
244             # make sure that () have spaces on either side
245 0           $bpf =~ s/\(/\ \(\ /g;
246 0           $bpf =~ s/\)/\ \)\ /g;
247              
248 0           my @bpf_split = split( /[\ \t]+/, $bpf );
249 0           my @tshark_args;
250             my @previous;
251 0           my $not = 0;
252 0           foreach my $item (@bpf_split) {
253              
254             # sets the equality operator based of if not is true or not
255 0           my $equality = '==';
256 0 0         if ($not) {
257 0           $equality = '!=';
258             }
259              
260             # tcp/udp/icmp
261 0 0 0       if ( $item eq 'tcp' || $item eq 'udp' || $item eq 'icmp' ) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
262 0           push( @tshark_args, $item );
263 0           $not = 0;
264 0           @previous = ();
265             }
266              
267             # handle negation
268             elsif ( $item eq 'not' ) {
269 0           $not = 1;
270             }
271              
272             # handles closing )
273             elsif ( $item eq ')' ) {
274 0           $not = 0;
275 0           push( @tshark_args, ')' );
276 0           @previous = ();
277             }
278              
279             # handles opening (
280             elsif ( $item eq ')' ) {
281 0 0         if ($not) {
282 0           push( @tshark_args, '!(' );
283             } else {
284 0           push( @tshark_args, '(' );
285             }
286 0           $not = 0;
287 0           @previous = ();
288             }
289              
290             # and/or
291             elsif ( $item eq 'or' || $item eq 'and' ) {
292             # make sure we not add it twice
293 0 0 0       if ( $tshark_args[$#tshark_args] ne 'and' && $tshark_args[$#tshark_args] ne 'or' ) {
294 0           push( @tshark_args, $item );
295             }
296 0           $not = 0;
297 0           @previous = ();
298             }
299              
300             # start of src/dst
301             elsif ( !defined( $previous[0] ) && ( $item eq 'src' || $item eq 'dst' ) ) {
302 0           push( @previous, $item );
303             }
304              
305             # start of ether
306             elsif ( !defined( $previous[0] ) && $item eq 'ether' ) {
307 0           push( @previous, $item );
308             }
309              
310             # adding src/dst/host to ether
311             elsif (defined( $previous[0] )
312             && $previous[0] eq 'ether'
313             && ( $item eq 'src' || $item eq 'dst' || $item eq 'host' ) )
314             {
315 0           push( @previous, $item );
316             }
317              
318             # generic host/port
319             elsif ( !defined( $previous[0] ) && ( $item eq 'port' || $item eq 'host' ) ) {
320 0           push( @previous, $item );
321             }
322              
323             # adding host/port to src/dst
324             elsif (defined( $previous[0] )
325             && ( $previous[0] eq 'src' || $previous[0] eq 'dst' )
326             && ( $item eq 'host' || $item eq 'port' ) )
327             {
328 0           push( @previous, $item );
329             }
330              
331             # add ether src $ether
332             elsif (defined( $previous[0] )
333             && defined( $previous[1] )
334             && $previous[0] eq 'ether'
335             && $previous[1] eq 'src' )
336             {
337 0           push( @tshark_args, 'etc.src', $equality, $item );
338 0           $not = 0;
339 0           @previous = ();
340             }
341              
342             # add ether src $ether
343             elsif (defined( $previous[0] )
344             && defined( $previous[1] )
345             && $previous[0] eq 'ether'
346             && $previous[1] eq 'dst' )
347             {
348 0           push( @tshark_args, 'etc.dst', $equality, $item );
349 0           $not = 0;
350 0           @previous = ();
351             }
352              
353             # add ether host $ether
354             elsif (defined( $previous[0] )
355             && defined( $previous[1] )
356             && $previous[0] eq 'ether'
357             && $previous[1] eq 'host' )
358             {
359 0           push( @tshark_args, 'etc.addr', $equality, $item );
360 0           $not = 0;
361 0           @previous = ();
362             }
363              
364             # add src port $port
365             elsif (defined( $previous[0] )
366             && defined( $previous[1] )
367             && $previous[0] eq 'src'
368             && $previous[1] eq 'port' )
369             {
370 0           push( @tshark_args, '(', 'tcp.srcport', $equality, $item, 'or', 'udp.srcport', $equality, $item, ')' );
371 0           $not = 0;
372 0           @previous = ();
373             }
374              
375             # add dst port $port
376             elsif (defined( $previous[0] )
377             && defined( $previous[1] )
378             && $previous[0] eq 'dst'
379             && $previous[1] eq 'port' )
380             {
381 0           push( @tshark_args, '(', 'tcp.dstport', $equality, $item, 'or', 'udp.dstport', $equality, $item, ')' );
382 0           $not = 0;
383 0           @previous = ();
384             }
385              
386             # add src host $host
387             elsif (defined( $previous[0] )
388             && defined( $previous[1] )
389             && $previous[0] eq 'src'
390             && $previous[1] eq 'host' )
391             {
392 0           push( @tshark_args, 'ip.src', $equality, $item );
393 0           $not = 0;
394 0           @previous = ();
395             }
396              
397             # add dst host $host
398             elsif (defined( $previous[0] )
399             && defined( $previous[1] )
400             && $previous[0] eq 'dst'
401             && $previous[1] eq 'host' )
402             {
403 0           push( @tshark_args, 'ip.dst', $equality, $item );
404 0           $not = 0;
405 0           @previous = ();
406             }
407              
408             # add port $port
409             elsif ( defined( $previous[0] ) && !defined( $previous[1] ) && $previous[0] eq 'port' ) {
410 0           push( @tshark_args, '(', 'tcp.port', $equality, $item, 'or', 'udp.port', $equality, $item, ')' );
411 0           $not = 0;
412 0           @previous = ();
413             }
414              
415             # add host $host
416             elsif ( defined( $previous[0] ) && !defined( $previous[1] ) && $previous[0] eq 'host' ) {
417 0           push( @tshark_args, 'ip.addr', $equality, $item );
418 0           $not = 0;
419 0           @previous = ();
420             }
421              
422             # add src $host
423             elsif ( defined( $previous[0] ) && !defined( $previous[1] ) && $previous[0] eq 'src' ) {
424 0           push( @tshark_args, 'ip.src', $equality, $item );
425 0           $not = 0;
426 0           @previous = ();
427             }
428              
429             # add dst $host
430             elsif ( defined( $previous[0] ) && !defined( $previous[1] ) && $previous[0] eq 'dst' ) {
431 0           push( @tshark_args, 'ip.dst', $equality, $item );
432 0           $not = 0;
433 0           @previous = ();
434             }
435              
436             # if anything else is found, skip it
437             else {
438 0           $not = 0;
439 0           @previous = ();
440             }
441             } ## end foreach my $item (@bpf_split)
442              
443 0           return join( ' ', @tshark_args );
444             } ## end sub bpf2tshark
445              
446             =head2 filter_clean
447              
448             Removes starting and trailing whitespace as well as collapsing
449             consecutive whitespace to a single space.
450              
451             The purpose for this is to make sure that tshark/BPF filters passed
452             are consistent for cacheing, even if their white space differs.
453              
454             A undef passed to it will return ''.
455              
456             Will die if the filter matches /^\w*\-/ as it starts with a '-', which
457             tcpdump will interpret as a switch.
458              
459             my $cleaned_bpf=$virani->filter_clean($bpf);
460              
461             =cut
462              
463             sub filter_clean {
464 0     0 1   my $self = $_[0];
465 0           my $string = $_[1];
466              
467 0 0         if ( !defined($string) ) {
468 0           return '';
469             }
470              
471 0 0         if ( $string =~ /^\w*\-/ ) {
472 0           die( 'The filter, "' . $string . '", begins with a "-", which dieing for safety reasons' );
473             }
474              
475             # remove white space at the start and end
476 0           $string =~ s/^\s*//g;
477 0           $string =~ s/\s+$//g;
478              
479             # replace all multiple white space characters with a single space
480 0           $string =~ s/\s\s+/ /g;
481              
482 0           return $string;
483             } ## end sub filter_clean
484              
485             =head1 check_apikey
486              
487             Checks the API key.
488              
489             If auth_via_IP_only is 1, this will always return true.
490              
491             my $apikey=$c->param('apikey');
492             if (!$virani->check_apikey($apikey)) {
493             $c->render( text => "Invalid API key\n", status=>403, );
494             return;
495             }
496              
497             =cut
498              
499             sub check_apikey {
500 0     0 0   my $self = $_[0];
501 0           my $apikey = $_[1];
502              
503 0 0         if ( $self->{auth_by_IP_only} ) {
504 0           return 1;
505             }
506              
507 0 0         if ( !defined($apikey) ) {
508 0           return 0;
509             }
510              
511 0 0 0       if ( !defined( $self->{apikey} ) || $self->{apikey} eq '' ) {
512 0           return 0;
513             }
514              
515 0 0         if ( $apikey ne $self->{apikey} ) {
516 0           return 0;
517             }
518              
519 0           return 1;
520             } ## end sub check_apikey
521              
522             =head1 check_remote_ip
523              
524             Checks if the remote IP is allowed or not.
525              
526             if ( ! $virani->check_remote_ip( $c->{tx}{original_remote_address} )){
527             $c->render( text => "IP or subnet not allowed\n", status=>403, );
528             return;
529             }
530              
531             =cut
532              
533             sub check_remote_ip {
534 0     0 0   my $self = $_[0];
535 0           my $ip = $_[1];
536              
537 0 0         if ( !defined($ip) ) {
538 0           return 0;
539             }
540              
541 0 0         if ( !defined( $self->{allowed_subnets}[0] ) ) {
542 0           return 0;
543             }
544              
545 0           my $allowed_subnets;
546 0           eval { $allowed_subnets = subnet_matcher( @{ $self->{allowed_subnets} } ); };
  0            
  0            
547 0 0         if ($@) {
    0          
548 0           die( 'Failed it init subnet matcher... ' . $@ );
549             } elsif ( !defined($allowed_subnets) ) {
550 0           die('Failed it init subnet matcher... sub_matcher returned undef');
551             }
552              
553 0 0         if ( $allowed_subnets->($ip) ) {
554 0           return 1;
555             }
556              
557 0           return 0;
558             } ## end sub check_remote_ip
559              
560             =head1 check_type
561              
562             Verify if the check is valid or note
563              
564             Returns 0/1 based on if it a known type or not.
565              
566             if ( ! $virani->check_type( $type )){
567             print $type." is not known\n";
568             }
569              
570             =cut
571              
572             sub check_type {
573 0     0 0   my $self = $_[0];
574 0           my $type = $_[1];
575              
576 0 0         if ( !defined($type) ) {
577 0           return 0;
578             }
579              
580 0 0 0       if ( $type ne 'tshark' && $type ne 'tcpdump' && $type ne 'bpf2tshark' ) {
      0        
581 0           return 0;
582             }
583              
584 0           return 1;
585             } ## end sub check_type
586              
587             =head2 get_default_set
588              
589             Returns the deefault set to use.
590              
591             my $set=$virani->get_default_set;
592              
593             =cut
594              
595             sub get_default_set {
596 0     0 1   my ($self) = @_;
597              
598 0           return $self->{default_set};
599             }
600              
601             =head2 get_cache_file
602              
603             Takes the same args as get_pcap_lcal.
604              
605             Returns the path to the file.
606              
607             my $cache_file=$virani->get_cache_file(%opts);
608             if (! -f $cache_file.'json'){
609             echo "Cache file metadata does not exist, so either get_pcap_local died or it has not been ran\n";
610             }
611              
612             =cut
613              
614             sub get_cache_file {
615 0     0 1   my ( $self, %opts ) = @_;
616              
617             # make sure we have something for type and check to make sure it is sane
618 0 0         if ( !defined( $opts{type} ) ) {
619 0           $opts{type} = $self->{type};
620 0 0         if ( defined( $self->{sets}{ $opts{set} }{type} ) ) {
621 0           $opts{type} = $self->{sets}{ $opts{set} }{type};
622             }
623             }
624              
625             # check it here incase the config includes something off
626 0 0         if ( !$self->check_type( $opts{type} ) ) {
627 0           die( 'type "' . $opts{type} . '" is not a supported type, tcpdump or tshark,' );
628             }
629              
630             # basic sanity checking
631 0 0 0       if ( !defined( $opts{start} ) ) {
    0          
    0          
    0          
    0          
632 0           die('$opts{start} not defined');
633             } elsif ( !defined( $opts{end} ) ) {
634 0           die('$opts{start} not defined');
635             } elsif ( ref( $opts{start} ) ne 'Time::Piece' ) {
636 0           die('$opts{start} is not a Time::Piece object');
637             } elsif ( ref( $opts{end} ) ne 'Time::Piece' ) {
638 0           die('$opts{end} is not a Time::Piece object');
639             } elsif ( defined( $opts{padding} ) && $opts{padding} !~ /^\d+/ ) {
640 0           die('$opts{padding} is not numeric');
641             }
642              
643 0 0         if ( !defined( $opts{auto_no_cache} ) ) {
644 0           $opts{auto_no_cache} = 1;
645             }
646              
647 0 0 0       if ( !defined( $opts{set} ) || $opts{set} eq '' ) {
648 0           $opts{set} = $self->get_default_set;
649             }
650              
651             # make sure the set exists
652 0 0         if ( !defined( $self->{sets}->{ $opts{set} } ) ) {
    0          
    0          
653 0           die( 'The set "' . $opts{set} . '" is not defined' );
654             } elsif ( !defined( $self->{sets}->{ $opts{set} }{path} ) ) {
655 0           die( 'The path for set "' . $opts{set} . '" is not defined' );
656             } elsif ( !-d $self->{sets}->{ $opts{set} }{path} ) {
657             die( 'The path for set "'
658             . $opts{set} . '", "'
659             . $self->{sets}->{ $opts{set} }{path}
660 0           . '" is not exist or is not a directory' );
661             }
662              
663             # get the paddimg, make sure it is sane, and apply it
664 0 0         if ( !defined( $opts{padding} ) ) {
665 0           $opts{padding} = $self->{padding};
666 0 0         if ( defined( $self->{sets}{ $opts{set} }{padding} ) ) {
667 0           $opts{padding} = $self->{sets}{ $opts{set} }{padding};
668             }
669             }
670              
671             # clean the filter
672 0           $opts{filter} = $self->filter_clean( $opts{filter} );
673              
674 0           my $cache_file;
675 0 0         if ( defined( $opts{file} ) ) {
676 0           my ( $volume, $directories, $file ) = File::Spec->splitpath( $opts{file} );
677              
678             # make sure the directory the output file is using exists
679 0 0 0       if ( $directories ne '' && !-d $directories ) {
680             die( '$opts{file} is set to "'
681             . $opts{file}
682 0           . '" but the directory part,"'
683             . $directories
684             . '", does not exist' );
685             }
686              
687             # figure what what to use as the cache file
688 0 0 0       if ( $opts{no_cache} ) {
    0 0        
    0 0        
    0 0        
      0        
689 0           $cache_file = $opts{file};
690             } elsif ( $opts{auto_no_cache} && ( !-d $self->{cache} || !-w $self->{cache} ) ) {
691 0           $cache_file = $opts{file};
692              
693             } elsif ( $opts{auto_no_cache} && ( -d $self->{cache} || -w $self->{cache} ) ) {
694             $cache_file
695             = $self->{cache} . '/'
696             . $opts{set} . '-'
697             . $opts{type} . '-'
698             . $opts{start}->epoch . '-'
699             . $opts{end}->epoch . "-"
700 0           . lc( md5_hex( $opts{filter} ) );
701             } elsif ( !$opts{auto_no_cache} && ( !-d $self->{cache} || !-w $self->{cache} ) ) {
702             die( '$opts{auto_no_cache} is false and $opts{no_cache} is false, but the cache dir "'
703             . $self->{dir}
704 0           . '" does not exist, is not a dir, or is not writable' );
705             }
706             } else {
707             # make sure the cache is usable
708 0 0         if ( !-d $self->{cache} ) {
    0          
709 0           die( 'Cache dir,"' . $self->{cache} . '", does not exist or is not a dir' );
710             } elsif ( !-w $self->{cache} ) {
711 0           die( 'Cache dir,"' . $self->{cache} . '", is not writable' );
712             }
713              
714             $cache_file
715             = $self->{cache} . '/'
716             . $opts{set} . '-'
717             . $opts{start}->epoch . '-'
718             . $opts{type} . '-'
719             . $opts{end}->epoch . "-"
720 0           . lc( md5_hex( $opts{filter} ) );
721             } ## end else [ if ( defined( $opts{file} ) ) ]
722              
723 0           return $cache_file;
724             } ## end sub get_cache_file
725              
726             =head2 get_pcap_local
727              
728             Generates a PCAP locally and returns the path to it.
729              
730             - start :: A L object of when to start looking.
731             - Default :: undef
732              
733             - end :: A L object of when to stop looking.
734             - Default :: undef
735              
736             - padding :: Number of seconds to pad the start and end with.
737             - Default :: 5
738              
739             - filter :: The BPF or tshark filter to use.
740             - Default :: ''
741              
742             - set :: The PCAP set to use. Will use what ever the default is set to if undef or blank.
743             - Default :: $virani->get_default_set
744              
745             - file :: The file to output to. If undef it just returns the path to
746             the cache file.
747             - Default :: undef
748              
749             - no_cache :: If cached, don't return that, but regen and if applicable re-cache.
750             - Default :: 0
751              
752             - auto_no_cache :: If the cache dir is being used and not writeable and a file
753             as been specified, don't die, but use the output file name
754             as the basis of for the tmp file.
755             - Default :: 1
756              
757             - type :: 'tcpdump' or 'tshark', depending on what one wants the filter todo.
758             - Default :: tcpdump
759              
760             The return is a hash reference that includes the following keys.
761              
762             - pcaps :: A array of PCAPs used.
763              
764             - pcap_count :: A count of used PCAPs.
765              
766             - pcap_glob :: The value of pcap_glob used.
767              
768             - ts_is_unixtime :: The value of ts_is_unixtime used.
769              
770             - failed :: A hash of PCAPs that failed. PCAP path as key and value being the reason.
771              
772             - failed_count :: A count of failed PCAPs.
773              
774             - path :: The path to the results file. If undef, unable it was unable
775             to process any of them.
776              
777             - success_found :: A count of successfully processed PCAPs.
778              
779             - filter :: The used filter.
780              
781             - total_size :: The size of all PCAP files checked.
782              
783             - failed_size :: The size of the PCAP files that failed.
784              
785             - success_size :: the size of the PCAP files that successfully processed
786              
787             - type :: The value of $opts{type}
788              
789             - padding :: The value of padding.
790              
791             - start_s :: Start time in seconds since epoch, not including pading.
792              
793             - end :: Send time in the format '%Y-%m-%dT%H:%M:%S%z'.
794              
795             - end_s :: End time in seconds since epoch, not including pading.
796              
797             - end :: End time in the format '%Y-%m-%dT%H:%M:%S%z'.
798              
799             - using_cache :: If the cache was used or not.
800              
801             - req_start :: Timestamp of when the it started. In the format
802             %Y-%m-%dT%H:%M:%S%z
803              
804             - req_start_s :: Same as req_start, but unixtime.
805              
806             - req_end :: Timestamp of when the it finished. In the format
807             %Y-%m-%dT%H:%M:%S%z
808              
809             - req_end_s :: Same as req_end, but unixtime.
810              
811             - req_time :: Number of seconds it took.
812              
813             =cut
814              
815             sub get_pcap_local {
816 0     0 1   my ( $self, %opts ) = @_;
817              
818             # start of the request
819 0           my $req_start = localtime;
820              
821             # if set is undef or blank, use the default
822 0 0 0       if ( !defined( $opts{set} ) || $opts{set} eq '' ) {
823 0           $opts{set} = $self->get_default_set;
824             }
825 0           $self->verbose( 'info', 'Set: ' . $opts{set} );
826              
827             # make sure we have something for type and check to make sure it is sane
828 0 0         if ( !defined( $opts{type} ) ) {
829 0           $opts{type} = $self->{type};
830 0 0         if ( defined( $self->{sets}{ $opts{set} }{type} ) ) {
831 0           $opts{type} = $self->{sets}{ $opts{set} }{type};
832             }
833             }
834              
835             # figure out what to use for $ts_is_unixtime
836 0           my $ts_is_unixtime;
837 0 0         if ( defined( $self->{sets}{ $opts{set} }{ts_is_unixtime} ) ) {
838 0           $ts_is_unixtime = $self->{sets}{ $opts{set} }{ts_is_unixtime};
839             } else {
840 0           $ts_is_unixtime = $self->{ts_is_unixtime};
841             }
842              
843             # figure out what to use for $pcap_glob
844 0           my $pcap_glob;
845 0 0         if ( defined( $self->{sets}{ $opts{set} }{pcap_glob} ) ) {
846 0           $pcap_glob = $self->{sets}{ $opts{set} }{pcap_glob};
847             } else {
848 0           $pcap_glob = $self->{pcap_glob};
849             }
850 0           $self->verbose( 'info', 'PCAP Glob: ' . $pcap_glob );
851              
852             # check it here incase the config includes something off
853 0 0         if ( !$self->check_type( $opts{type} ) ) {
854 0           die( 'type "' . $opts{type} . '" is not a supported type, tcpdump or tshark,' );
855             }
856 0           $self->verbose( 'info', 'Type: ' . $opts{type} );
857              
858             # basic sanity checking
859 0 0 0       if ( !defined( $opts{start} ) ) {
    0          
    0          
    0          
    0          
860 0           die('$opts{start} not defined');
861             } elsif ( !defined( $opts{end} ) ) {
862 0           die('$opts{start} not defined');
863             } elsif ( ref( $opts{start} ) ne 'Time::Piece' ) {
864 0           die('$opts{start} is not a Time::Piece object');
865             } elsif ( ref( $opts{end} ) ne 'Time::Piece' ) {
866 0           die('$opts{end} is not a Time::Piece object');
867             } elsif ( defined( $opts{padding} ) && $opts{padding} !~ /^\d+$/ ) {
868 0           die('$opts{padding} is not numeric');
869             }
870 0           $self->verbose( 'info', 'Start: ' . $opts{start}->strftime('%Y-%m-%dT%H:%M:%S%z') . ', ' . $opts{start}->epoch );
871 0           $self->verbose( 'info', 'End: ' . $opts{end}->strftime('%Y-%m-%dT%H:%M:%S%z') . ', ' . $opts{end}->epoch );
872              
873 0 0         if ( !defined( $opts{auto_no_cache} ) ) {
874 0           $opts{auto_no_cache} = 1;
875             }
876 0           $self->verbose( 'info', 'auto_no_cache: ' . $opts{auto_no_cache} );
877              
878 0 0         if ( !defined( $opts{no_cache} ) ) {
879 0           $opts{no_cache} = 0;
880             }
881 0           $self->verbose( 'info', 'no_cache: ' . $opts{no_cache} );
882              
883             # make sure the set exists
884 0 0         if ( !defined( $self->{sets}->{ $opts{set} } ) ) {
    0          
    0          
885 0           die( 'The set "' . $opts{set} . '" is not defined' );
886             } elsif ( !defined( $self->{sets}->{ $opts{set} }{path} ) ) {
887 0           die( 'The path for set "' . $opts{set} . '" is not defined' );
888             } elsif ( !-d $self->{sets}->{ $opts{set} }{path} ) {
889             die( 'The path for set "'
890             . $opts{set} . '", "'
891             . $self->{sets}->{ $opts{set} }{path}
892 0           . '" is not exist or is not a directory' );
893             }
894              
895             # get the paddimg, make sure it is sane, and apply it
896 0 0         if ( !defined( $opts{padding} ) ) {
897 0           $opts{padding} = $self->{padding};
898 0 0         if ( defined( $self->{sets}{ $opts{set} }{padding} ) ) {
899 0           $opts{padding} = $self->{sets}{ $opts{set} }{padding};
900             }
901             }
902              
903             # clean the filter
904 0           $opts{filter} = $self->filter_clean( $opts{filter} );
905 0           $self->verbose( 'info', 'Filter: ' . $opts{filter} );
906              
907             # get the cache file to use
908 0           my $cache_file;
909 0           eval { $cache_file = $self->get_cache_file(%opts); };
  0            
910 0 0         if ($@) {
911 0           die( '$self->get_cache_files(%opts) failed... ' . $@ );
912             }
913              
914             # if applicable return the cache file
915 0           my $return_cache = 0;
916 0 0 0       if ( defined( $opts{file} )
    0 0        
      0        
      0        
      0        
      0        
      0        
917             && $opts{file} ne $cache_file
918             && !$opts{no_cache}
919             && -f $cache_file
920             && -f $cache_file . '.json' )
921             {
922 0           $return_cache = 1;
923             } elsif ( !defined( $opts{file} ) && !$opts{no_cache} && -f $cache_file && -f $cache_file . '.json' ) {
924 0           $return_cache = 1;
925             }
926 0 0         if ($return_cache) {
927 0           my $cache_message = 'Already cached... "' . $cache_file . '"';
928 0 0 0       if ( defined( $opts{file} ) && $opts{file} ne $cache_file ) {
929 0           $cache_message = $cache_message . ' -> "' . $opts{file} . '"';
930             }
931 0           $self->verbose( 'info', $cache_message );
932 0 0 0       if ( defined( $opts{file} ) && $opts{file} ne $cache_file ) {
933 0           cp( $cache_file, $opts{file} );
934             }
935 0           my $to_return;
936 0           eval {
937 0           my $cache_meta_raw = read_file( $cache_file . '.json' );
938 0           $to_return = decode_json($cache_meta_raw);
939             };
940 0 0         if ($@) {
941 0           die( 'Failed to read cache metadata JSON, "' . $cache_file . '.json"' );
942             }
943 0           $to_return->{using_cache} = 1;
944 0           return $to_return;
945             } ## end if ($return_cache)
946              
947             # check it here incase the config includes something off
948 0 0         if ( $opts{padding} !~ /^[0-9]+$/ ) {
949 0           die( '"' . $opts{padding} . '" is not a numeric' );
950             }
951              
952             # set the padding
953 0           my $start = $opts{start} - $opts{padding};
954 0           my $end = $opts{end} + $opts{padding};
955 0           $self->verbose( 'info', 'Padded Start: ' . $start->strftime('%Y-%m-%dT%H:%M:%S%z') . ', ' . $start->epoch );
956 0           $self->verbose( 'info', 'Padded End: ' . $end->strftime('%Y-%m-%dT%H:%M:%S%z') . ', ' . $end->epoch );
957              
958             # get the set
959 0           my $set_path = $self->get_set_path( $opts{set} );
960 0 0         if ( !defined($set_path) ) {
961 0           die( 'The set "' . $opts{set} . '" does not either exist or the path value for it is undef' );
962             }
963              
964             # get the pcaps
965 0           my @pcaps = File::Find::Rule->file()->name($pcap_glob)->in($set_path);
966              
967             # get the ts_regexp to use
968 0           my $ts_regexp;
969 0 0         if ( defined( $self->{sets}{ $opts{set} }{regex} ) ) {
970 0           $ts_regexp = $self->{sets}{ $opts{set} }{regex};
971             } else {
972 0           $ts_regexp = $self->{default_regex};
973             }
974 0           $self->verbose( 'info', 'Timestamp Regexp: ' . $ts_regexp );
975              
976 0           my $to_check = File::Find::IncludesTimeRange->find(
977             items => \@pcaps,
978             start => $start,
979             end => $end,
980             regex => $ts_regexp,
981             ts_is_unixtime => $ts_is_unixtime,
982             );
983              
984             # The return hash and what will be used for the cache JSON
985             # req_end stuff set later
986             my $to_return = {
987             pcaps => $to_check,
988             pcap_glob => $pcap_glob,
989             pcap_count => 0,
990             failed => {},
991             failed_count => 0,
992             success_count => 0,
993             path => $cache_file,
994             filter => $opts{filter},
995             total_size => 0,
996             failed_size => 0,
997             success_size => 0,
998             tmp_size => 0,
999             final_size => 0,
1000             type => $opts{type},
1001             padding => $opts{padding},
1002             start_s => $opts{start}->epoch,
1003             start => $opts{start}->strftime('%Y-%m-%dT%H:%M:%S%z'),
1004             end_s => $opts{end}->epoch,
1005 0           end => $opts{end}->strftime('%Y-%m-%dT%H:%M:%S%z'),
1006             req_start => $req_start->strftime('%Y-%m-%dT%H:%M:%S%z'),
1007             req_start_s => $req_start->epoch,
1008             ts_is_unixtime => $ts_is_unixtime,
1009             };
1010              
1011             # used for tracking the files to cleanup
1012 0           my @tmp_files;
1013              
1014             # puts together the tshark filter if needed
1015 0           my $tshark_filter = $opts{filter};
1016 0 0         if ( $opts{type} eq 'bpf2tshark' ) {
1017 0           $tshark_filter = $self->bpf2tshark( $opts{filter} );
1018 0           $to_return->{filter_translated} = $tshark_filter;
1019 0           $self->verbose( 'info', 'Translated Filter ' . $tshark_filter );
1020             }
1021              
1022             # the merge command
1023 0           my $to_merge = [ 'mergecap', '-w', $cache_file ];
1024 0           foreach my $pcap ( @{$to_check} ) {
  0            
1025              
1026             # get stat info for the file
1027 0           my ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks )
1028             = stat($pcap);
1029 0           $to_return->{total_size} += $size;
1030              
1031 0           $self->verbose( 'info', 'Processing ' . $pcap . ", size=" . $size . " ..." );
1032              
1033 0           my $tmp_file = $cache_file . '-' . $to_return->{pcap_count};
1034              
1035 0           my ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf );
1036 0 0         if ( $opts{type} eq 'tcpdump' ) {
1037             ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf ) = run(
1038 0           command => [ 'tcpdump', '-r', $pcap, '-w', $tmp_file, $opts{filter} ],
1039             verbose => 0
1040             );
1041             } else {
1042 0           ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf ) = run(
1043             command => [ 'tshark', '-r', $pcap, '-w', $tmp_file, $tshark_filter ],
1044             verbose => 0
1045             );
1046             }
1047 0 0         if ($success) {
1048 0           $to_return->{success_count}++;
1049 0           $to_return->{success_size} += $size;
1050 0           push( @{$to_merge}, $tmp_file );
  0            
1051 0           push( @tmp_files, $tmp_file );
1052              
1053             # get stat info for the tmp file
1054 0           ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks )
1055             = stat($tmp_file);
1056 0           $to_return->{tmp_size} += $size;
1057              
1058             } else {
1059 0           $to_return->{failed}{$pcap} = $error_message;
1060 0           $to_return->{failed_count}++;
1061 0           $to_return->{failed_size} += $size;
1062              
1063 0           $self->verbose( 'warning', 'Failed ' . $pcap . " ... " . $error_message );
1064              
1065 0           unlink $tmp_file;
1066             }
1067              
1068 0           $to_return->{pcap_count}++;
1069             } ## end foreach my $pcap ( @{$to_check} )
1070              
1071             # only try merging if we had more than one success
1072 0 0         if ( $to_return->{success_count} > 0 ) {
1073              
1074 0           $self->verbose( 'info', "Merging PCAPs... " . join( ' ', @{$to_merge} ) );
  0            
1075              
1076 0           my ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf ) = run(
1077             command => $to_merge,
1078             verbose => 0
1079             );
1080 0 0         if ($success) {
1081 0           $self->verbose( 'info', "PCAPs merged into " . $cache_file );
1082             } else {
1083             # if verbose print different messages if mergecap generated a ouput file or not when it fialed
1084 0 0         if ( -f $cache_file ) {
1085 0           $self->verbose( 'warning', "PCAPs partially(output file generated) failed " . $error_message );
1086             } else {
1087 0           $self->verbose( 'err', "PCAPs merge completely(output file not generated) failed " . $error_message );
1088             }
1089             }
1090              
1091             # remove each tmp file
1092 0           foreach my $tmp_file (@tmp_files) {
1093 0           unlink($tmp_file);
1094             }
1095              
1096             # don't bother checking size if the file was not generated
1097 0 0         if ( -f $cache_file ) {
1098 0           my ( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks )
1099             = stat($cache_file);
1100 0           $to_return->{final_size} = $size;
1101             }
1102              
1103             } else {
1104 0           $self->verbose( 'err', "No PCAPs to merge" );
1105             }
1106              
1107             $self->verbose( 'info',
1108             "PCAP sizes... failed_size="
1109             . $to_return->{failed_size}
1110             . " success_size="
1111             . $to_return->{success_size}
1112             . " total_size="
1113             . $to_return->{total_size}
1114             . " tmp_size="
1115             . $to_return->{tmp_size}
1116             . " final_size="
1117 0           . $to_return->{final_size} );
1118              
1119             # finalize info on how long the request took
1120 0           my $req_end = localtime;
1121 0           $to_return->{req_end} = $req_end->strftime('%Y-%m-%dT%H:%M:%S%z');
1122 0           $to_return->{req_end_s} = $req_end->epoch;
1123 0           $to_return->{req_time} = $req_end->epoch - $req_start->epoch;
1124              
1125 0           $self->verbose( 'info', 'Creating metadata JSON at "' . $cache_file . '.json" ' );
1126 0           my $json = JSON->new->allow_nonref->pretty->canonical(1);
1127 0           my $raw_json = $json->encode($to_return);
1128 0           write_file( $cache_file . '.json', $raw_json );
1129              
1130             # if the file and cache file are the same, then the cache dir not accessing, so no need to copy it
1131 0 0 0       if ( defined( $opts{file} ) && $cache_file ne $opts{file} ) {
1132 0           $self->verbose( 'info', 'Copying "' . $cache_file . '" to "' . $opts{file} . '"' );
1133 0           cp( $cache_file, $opts{file} );
1134             }
1135              
1136 0           $to_return->{using_cache} = 0;
1137              
1138 0           return $to_return;
1139             } ## end sub get_pcap_local
1140              
1141             =head2 get_set_path
1142              
1143             Returns the path to a set.
1144              
1145             If no set is given, the default is used.
1146              
1147             Will return undef if the set does not exist or if the set does not have a path defined.
1148              
1149             my $path=$virani->get_set_path($set);
1150              
1151             =cut
1152              
1153             sub get_set_path {
1154 0     0 1   my ( $self, $set ) = @_;
1155              
1156 0 0         if ( !defined($set) ) {
1157 0           $set = $self->get_default_set;
1158             }
1159              
1160 0 0         if ( !defined( $self->{sets}{$set} ) ) {
1161 0           return undef;
1162             }
1163              
1164 0 0         if ( !defined( $self->{sets}{$set}{path} ) ) {
1165 0           return undef;
1166             }
1167              
1168 0           return $self->{sets}{$set}{path};
1169             } ## end sub get_set_path
1170              
1171             =head2 set_verbose
1172              
1173             Set if it should be verbose or not.
1174              
1175             # be verbose
1176             $virani->verbose(1);
1177              
1178             # do not be verbose
1179             $virani->verbose(0);
1180              
1181             =cut
1182              
1183             sub set_verbose {
1184 0     0 1   my ( $self, $verbose ) = @_;
1185              
1186 0           $self->{verbose} = $verbose;
1187             }
1188              
1189             =head2 set_verbose_to_syslog
1190              
1191             Set if it should be verbose or not.
1192              
1193             # send verbose messages to syslog
1194             $virani->set_verbose_to_syslog(1);
1195              
1196             # do not send verbose messages to syslog
1197             $virani->set_verbose_to_syslog(0);
1198              
1199             =cut
1200              
1201             sub set_verbose_to_syslog {
1202 0     0 1   my ( $self, $to_syslog ) = @_;
1203              
1204 0           $self->{verbose_to_syslog} = $to_syslog;
1205             }
1206              
1207             =head2 verbose
1208              
1209             Prints out error messages. This is inteded to be internal.
1210              
1211             Only sends the string if verbose is enabled.
1212              
1213             There is no need to add a "\n" as it will automatically if not sending to syslog.
1214              
1215             Two variables are taken. The first is level the second is the message. Level is only used
1216             for syslog. Default level is info.
1217              
1218             - Levels :: emerg, alert, crit, err, warning, notice, info, debug
1219              
1220             $self->verbose('info', 'some string');
1221              
1222             =cut
1223              
1224             sub verbose {
1225 0     0 1   my ( $self, $level, $string ) = @_;
1226              
1227 0 0 0       if ( !defined($string) || $string eq '' ) {
1228 0           return;
1229             }
1230              
1231 0 0         if ( !defined($level) ) {
1232 0           $level = 'info';
1233             }
1234              
1235 0 0         if ( $self->{verbose} ) {
1236 0 0         if ( $self->{verbose_to_syslog} ) {
1237 0           openlog( 'virani', undef, 'daemon' );
1238 0           syslog( $level, $string );
1239 0           closelog();
1240             } else {
1241 0           print $string. "\n";
1242             }
1243             }
1244              
1245 0           return;
1246             } ## end sub verbose
1247              
1248             =head2 CONFIG
1249              
1250             The config format used toml, processed via L.
1251              
1252             'new_from_conf' will initiate virani by reading it in and feeding it to 'new'.
1253              
1254             =head2 DAEMONLOGGER ON FREEBSD
1255              
1256             With daemonlogger setup along the lines of like below...
1257              
1258             daemonlogger_enable="YES"
1259             daemonlogger_flags="-f /usr/local/etc/daemonlogger.bpf -d -l /var/log/daemonlogger -t 120"
1260              
1261             The following can be made available via mojo-varini or locally via varini with the set name of
1262             default as below.
1263              
1264             allowed_subnets=["192.168.14.0/23", "127.0.0.1/8"]
1265             [sets.default]
1266             path='/var/log/daemonlogger'
1267              
1268             If you want to use 'init/freebsd' to start mojo-virani, you just need to copy it
1269             it to '/usr/local/etc/rc.d/virani' and add the following or the like to '/etc/rc.conf'.
1270              
1271             virani_enable="YES"
1272             virani_flags="daemon -m production -l http://127.0.0.1:8080 -l http://192.168.14.1:8080"
1273              
1274             See the script for information on the various possible config args for it.
1275              
1276             =head1 AUTHOR
1277              
1278             Zane C. Bowers-Hadley, C<< >>
1279              
1280             =head1 BUGS
1281              
1282             Please report any bugs or feature requests to C, or through
1283             the web interface at L. I will be notified, and then you'll
1284             automatically be notified of progress on your bug as I make changes.
1285              
1286              
1287              
1288              
1289             =head1 SUPPORT
1290              
1291             You can find documentation for this module with the perldoc command.
1292              
1293             perldoc Virani
1294              
1295              
1296             You can also look for information at:
1297              
1298             =over 4
1299              
1300             =item * RT: CPAN's request tracker (report bugs here)
1301              
1302             L
1303              
1304             =item * CPAN Ratings
1305              
1306             L
1307              
1308             =item * Search CPAN
1309              
1310             L
1311              
1312             =back
1313              
1314              
1315             =head1 ACKNOWLEDGEMENTS
1316              
1317              
1318             =head1 LICENSE AND COPYRIGHT
1319              
1320             This software is Copyright (c) 2023 by Zane C. Bowers-Hadley.
1321              
1322             This is free software, licensed under:
1323              
1324             The GNU Lesser General Public License, Version 2.1, February 1999
1325              
1326              
1327             =cut
1328              
1329             1; # End of Virani