File Coverage

blib/lib/Bro/Log/Parse.pm
Criterion Covered Total %
statement 95 114 83.3
branch 35 50 70.0
condition 11 24 45.8
subroutine 13 13 100.0
pod 2 4 50.0
total 156 205 76.1


line stmt bran cond sub pod time code
1             package Bro::Log::Parse;
2             # ABSTRACT: Perl interface for parsing Bro logfiles
3              
4 7     7   136544 use strict;
  7         15  
  7         183  
5 7     7   33 use warnings;
  7         16  
  7         172  
6 7     7   80 use 5.10.1;
  7         26  
7              
8             # use Exporter;
9 7     7   930742 use autodie;
  7         7917341  
  7         35  
10 7     7   47650 use Carp;
  7         12  
  7         465  
11 7     7   31 use Scalar::Util qw/openhandle/;
  7         19  
  7         932  
12              
13             our $VERSION = '0.07';
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 7     7   22 my @accessors = qw/fh file line headers headerlines fields/;
25              
26 7         15 for my $accessor ( @accessors ) {
27 7     7   33 no strict 'refs';
  7         8  
  7         489  
28             *$accessor = sub {
29 8     8   547 my $self = shift;
30 8         33 return $self->{$accessor};
31             }
32 42         9059 }
33              
34             }
35              
36             sub new {
37 10     10 1 3345 my $class = shift;
38 10         19 my $arg = shift;
39              
40 10         19 my $self = {};
41 10         31 $self->{line} = undef;
42              
43 10 100       70 if ( !defined($arg) ) {
    100          
    100          
44 1         3 $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 3         10 $self->{file} = $arg;
51             }
52              
53 10         23 bless $self, $class;
54              
55 10 100 66     103 if ( defined($self->{file}) && !(defined($self->{fh})) ) {
56 5 50       115 unless ( -f $self->{file} ) {
57 0         0 croak("Could not open ".$self->{file});
58             }
59              
60             open( my $fh, "<", $self->{file} )
61 5 50       37 or croak("Cannot open ".$self->{file});
62 5         13717 $self->{fh} = $fh;
63             }
64              
65 10 100 66     56 if ( !defined($self->{fh}) && ( !defined($self->{diamond}) || !$self->{diamond} ) ) {
      66        
66 1         181 croak("No filename given in constructor. Aborting");
67             }
68              
69 9         24 $self->{json_file} = 0;
70 9         39 $self->{names} = [ $self->readheader() ];
71 9         34 $self->{fields} = $self->{names};
72 9   100     52 $self->{empty_as_undef} //= 0;
73              
74 9   50     29 $self->{headers} //= {};
75 9   50     26 $self->{headerlines} //= [];
76              
77 9         52 return $self;
78             }
79              
80             sub readheader {
81 9     9 0 18 my $self = shift;
82              
83 9         17 my @headerlines;
84             my @names;
85 9         19 my $firstline = 1;
86             # first: read header line. This is a little brittle, but... welll... well, it is.
87 9         39 while ( my $line = $self->extractNextLine() ) {
88 72 100       138 if ( $firstline ) {
89 9         17 $firstline = 0;
90 9 50 33     83 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 72         107 chomp($line);
103 72         109 push(@headerlines, $line);
104              
105 72         236 my @fields = split /\t/,$line;
106              
107 72 50       204 unless ( $line =~ /^#/ ) {
108 0         0 croak("Did not find required fields and types header lines: $line");
109             }
110              
111 72         99 my $type = shift(@fields);
112 72 100       343 if ( "#fields" eq $type ) {
    100          
113             # yay.
114             # we have our field names...
115 9         60 @names = @fields;
116             } elsif ( "#types" eq $type) {
117 9         31 last;
118             }
119             }
120              
121 9         69 $self->{headerlines} = \@headerlines;
122 9         27 $self->{headers} = { map {/#(\w+)\s+(.*)/;$1=>$2} @headerlines };
  72         178  
  72         247  
123              
124 9         79 return @names;
125             }
126              
127              
128             sub getLine {
129 19     19 1 33597 my $self = shift;
130              
131 19         25 my @names = @{$self->{names}};
  19         95  
132              
133 19         57 while ( my $line = $self->extractNextLine ) {
134 28         52 my $removed = chomp($line);
135 28         57 $self->{line} = $line;
136              
137 28 50       76 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 28         145 my @fields = split "\t", $line;
147              
148 28 100       87 if ( $line =~ /^#/ ) {
149 12 100       29 if ( "#fields" eq shift(@fields) ) {
150 1         5 @names = @fields;
151 1         3 $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         6 $self->{headers} = [ join("\t", ("#fields", @fields)) ];
156             }
157 12         39 next;
158             }
159 16         22 my %f;
160              
161 16 50       68 unless (scalar @names == scalar @fields) {
162 0 0       0 next if ( $removed == 0 );
163 0         0 croak("Number of expected fields does not match number of fields in file");
164             }
165              
166 16         34 for my $name ( @names ) {
167 320         375 my $field = shift(@fields);
168 320 100       643 if ( ( $field eq "-" ) ) {
    100          
169 80         162 $f{$name} = undef;
170             } elsif ( $field eq "(empty)" ) {
171 14 100       73 $f{$name} = $self->{empty_as_undef} ? undef : [];
172             } else {
173 226         480 $f{$name} = $field;
174             }
175             }
176              
177 16         71 return \%f;
178             }
179             }
180              
181             sub extractNextLine {
182 103     103 0 139 my $self = shift;
183              
184 103 50       237 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 103         136 my $in = $self->{fh};
191              
192 103 100       696 return defined($in) ? <$in> : <>;
193             }
194              
195             1;