File Coverage

blib/lib/Bro/Log/Parse.pm
Criterion Covered Total %
statement 96 114 84.2
branch 37 50 74.0
condition 12 24 50.0
subroutine 13 13 100.0
pod 2 4 50.0
total 160 205 78.0


line stmt bran cond sub pod time code
1             package Bro::Log::Parse;
2             # ABSTRACT: Perl interface for parsing Bro logfiles
3              
4 8     8   442338 use strict;
  8         60  
  8         213  
5 8     8   58 use warnings;
  8         15  
  8         218  
6 8     8   103 use 5.10.1;
  8         23  
7              
8             # use Exporter;
9 8     8   3229 use autodie;
  8         101559  
  8         30  
10 8     8   44084 use Carp;
  8         16  
  8         458  
11 8     8   40 use Scalar::Util qw/openhandle/;
  8         13  
  8         966  
12              
13             our $VERSION = '0.08';
14              
15             #@EXPORT_OK = qw//;
16              
17             my $json = eval {
18             require JSON;
19             JSON->import();
20             1;
21             }; # true if we support reading from json
22              
23             BEGIN {
24 8     8   38 my @accessors = qw/fh file line headers headerlines fields/;
25              
26 8         22 for my $accessor ( @accessors ) {
27 8     8   45 no strict 'refs';
  8         11  
  8         573  
28             *$accessor = sub {
29 8     8   692 my $self = shift;
30 8         34 return $self->{$accessor};
31             }
32 48         8404 }
33              
34             }
35              
36             sub new {
37 11     11 1 3513 my $class = shift;
38 11         19 my $arg = shift;
39              
40 11         20 my $self = {};
41 11         26 $self->{line} = undef;
42              
43 11 100       66 if ( !defined($arg) ) {
    100          
    100          
44 1         2 $self->{diamond} = 1;
45             } elsif ( ref($arg) eq 'HASH' ) {
46 5         9 $self = $arg;
47             } elsif ( defined(openhandle($arg)) ) {
48 1         3 $self->{fh} = $arg;
49             } else {
50 4         9 $self->{file} = $arg;
51             }
52              
53 11         23 bless $self, $class;
54              
55 11 100 66     75 if ( defined($self->{file}) && !(defined($self->{fh})) ) {
56 6 50       108 unless ( -f $self->{file} ) {
57 0         0 croak("Could not open ".$self->{file});
58             }
59              
60             open( my $fh, "<", $self->{file} )
61 6 50       91 or croak("Cannot open ".$self->{file});
62 6         12846 $self->{fh} = $fh;
63             }
64              
65 11 100 66     49 if ( !defined($self->{fh}) && ( !defined($self->{diamond}) || !$self->{diamond} ) ) {
      100        
66 1         154 croak("No filename given in constructor. Aborting");
67             }
68              
69 10         40 $self->{json_file} = 0;
70 10         38 $self->{names} = [ $self->readheader() ];
71 10         32 $self->{fields} = $self->{names};
72 10   100     50 $self->{empty_as_undef} //= 0;
73              
74 10   50     51 $self->{headers} //= {};
75 10   50     44 $self->{headerlines} //= [];
76              
77 10         56 return $self;
78             }
79              
80             sub readheader {
81 10     10 0 18 my $self = shift;
82              
83 10         15 my @headerlines;
84             my @names;
85 10         16 my $firstline = 1;
86             # first: read header line. This is a little brittle, but... welll... well, it is.
87 10         28 while ( my $line = $self->extractNextLine() ) {
88 80 100       129 if ( $firstline ) {
89 10         19 $firstline = 0;
90 10 50 33     62 if ( length($line) > 1 && substr($line, 0, 1) eq "{" ) {
91             # Json file. stuff line in saved_line and try to extract header fields...
92 0 0       0 croak("Parsing json formatted log files needs JSON module") unless ( $json );
93 0         0 my $val = decode_json($line);
94 0         0 $self->{saved_line} = $line;
95 0 0 0     0 if ( !defined($val) || ref($val) ne "HASH" ) {
96 0         0 croak("Error parsing first line of json formatted log - $line");
97             }
98 0         0 $self->{json_file} = 1;
99 0         0 return sort keys %$val;
100             }
101             }
102 80         96 chomp($line);
103 80         107 push(@headerlines, $line);
104              
105 80         200 my @fields = split /\t/,$line;
106              
107 80 50       200 unless ( $line =~ /^#/ ) {
108 0         0 croak("Did not find required fields and types header lines: $line");
109             }
110              
111 80         132 my $type = shift(@fields);
112 80 100       214 if ( "#fields" eq $type ) {
    100          
113             # yay.
114             # we have our field names...
115 10         56 @names = @fields;
116             } elsif ( "#types" eq $type) {
117 10         40 last;
118             }
119             }
120              
121 10         22 $self->{headerlines} = \@headerlines;
122 10         23 $self->{headers} = { map {/#(\w+)\s+(.*)/;$1=>$2} @headerlines };
  80         169  
  80         223  
123              
124 10         100 return @names;
125             }
126              
127              
128             sub getLine {
129 22     22 1 33620 my $self = shift;
130              
131 22         53 my @names = @{$self->{names}};
  22         102  
132              
133 22         61 while ( my $line = $self->extractNextLine ) {
134 31         51 my $removed = chomp($line);
135 31         46 $self->{line} = $line;
136              
137 31 50       59 if ( $self->{json_file} ) {
138 0         0 my $val = decode_json($line);
139 0 0 0     0 if ( !defined($val) || ref($val) ne "HASH" ) {
140 0         0 croak("Error parsing line of json formatted log - $line");
141             }
142 0         0 $self->{names} = [ sort keys %$val ];
143 0         0 return $val;
144             }
145              
146 31         137 my @fields = split "\t", $line;
147              
148 31 100       102 if ( $line =~ /^#/ ) {
149 12 100       25 if ( "#fields" eq shift(@fields) ) {
150 1         4 @names = @fields;
151 1         1 $self->{names} = \@fields;
152             # This is not really nice, but for the moment we do not really need any
153             # of the other header lines for parsing files - and we do not keep track
154             # of them. Sorry...
155 1         7 $self->{headers} = [ join("\t", ("#fields", @fields)) ];
156             }
157 12         27 next;
158             }
159 19         24 my %f;
160              
161 19 100       51 unless (scalar @names == scalar @fields) {
162 1 50       5 next if ( $removed == 0 );
163 0         0 croak("Number of expected fields does not match number of fields in file");
164             }
165              
166 18         33 for my $name ( @names ) {
167 360         387 my $field = shift(@fields);
168 360 100       537 if ( ( $field eq "-" ) ) {
    100          
169 90         157 $f{$name} = undef;
170             } elsif ( $field eq "(empty)" ) {
171 14 100       52 $f{$name} = $self->{empty_as_undef} ? undef : [];
172             } else {
173 256         429 $f{$name} = $field;
174             }
175             }
176              
177 18         89 return \%f;
178             }
179             }
180              
181             sub extractNextLine {
182 115     115 0 141 my $self = shift;
183              
184 115 50       197 if( defined($self->{saved_line}) ) {
185 0         0 my $sl = $self->{saved_line};
186 0         0 undef $self->{saved_line};
187 0         0 return $sl;
188             }
189              
190 115         132 my $in = $self->{fh};
191              
192 115 100       718 return defined($in) ? <$in> : <>;
193             }
194              
195             1;