File Coverage

blib/lib/Logwatch/RecordTree/IPv4.pm
Criterion Covered Total %
statement 148 152 97.3
branch 31 40 77.5
condition 12 15 80.0
subroutine 23 23 100.0
pod 4 11 36.3
total 218 241 90.4


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #===============================================================================
3             # PODNAME: Logwatch::RecordTree::IPv4
4             # ABSTRACT: a subclass of Logwatch::RecordTree for IPv4 addresses
5             #
6             # AUTHOR: Reid Augustin (REID)
7             # EMAIL: reid@hellosix.com
8             # CREATED: Thu Mar 12 18:41:04 PDT 2015
9             #===============================================================================
10              
11 1     1   25 use 5.008;
  1         7  
  1         53  
12 1     1   6 use strict;
  1         2  
  1         45  
13 1     1   6 use warnings;
  1         2  
  1         51  
14              
15             package Logwatch::RecordTree::IPv4;
16 1     1   630 use parent 'Logwatch::RecordTree';
  1         334  
  1         6  
17 1     1   47 use Moo;
  1         1  
  1         8  
18 1     1   220 use UNIVERSAL::require;
  1         2  
  1         9  
19             # use Net::IP::Identifier 0.111
20 1     1   34 use Carp qw( croak );
  1         2  
  1         57  
21 1     1   448 use Sort::Key::IPv4;
  1         2728  
  1         43  
22 1     1   5 use Sort::Key::Natural qw( natsort natkeysort );
  1         1  
  1         43  
23 1     1   903 use Math::BigInt;
  1         14098  
  1         5  
24              
25             our $VERSION = '2.056'; # VERSION
26              
27             has identify => (
28             is => 'rw',
29             );
30             has snowshoe => ( # number indicating width of mask to consider. 1 => 24
31             is => 'rw',
32             );
33              
34             my $identifier; # class variable
35              
36             sub identifier {
37 12     12 1 18 my ($self) = @_;
38              
39 12 100       37 if (not $self->{identifier}) {
40 2 100       5 if (not $identifier) {
41 1 50       9 Net::IP::Identifier->require(0.111)
42             or croak($@);
43 1         16 $identifier = Net::IP::Identifier->new;
44             }
45 2         1783 $self->{identifier} = $identifier;
46             }
47 12         61 return $self->{identifier};
48             }
49              
50             sub create_child { # override
51 31     31 1 53 my ($self, $name, $type, $opts) = @_;
52              
53 31         90 my $child = $self->SUPER::create_child($name, $type, $opts);
54              
55             # this is why we're overriding parent's create_child method. we want
56             # to do these when child is created so caller can make changes
57             $child->sprint_name(sub {
58 17     17   135 my ($child) = @_;
59              
60 17         49 my $ip = $child->name;
61 17 100 66     204 if ($self->identify and
      66        
62             ($ip =~ m/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/ or
63             $ip =~ m/^[\d:]+(\/\d+)?$/)) {
64 12         44 my $id = $self->identifier->identify($ip);
65 12 50       8159946 if ($id) {
66 12 50       38 $id = substr($id, 0, 8) if (length $id > 8);
67 12         41 $ip = "$id-$ip";
68             }
69             }
70 17         119 return $ip;
71 31         1556 });
72              
73 31         365 return $child;
74             }
75              
76             # the IP list may contain non-IP addresses, split into two lists:
77             sub split_ips {
78 5     5 0 42 my ($self, $ips_orig) = @_;
79              
80 5         10 my (@non_ips, @ips);
81 5         8 for my $ip (@{$ips_orig}) {
  5         11  
82 34 100 66     209 if ($ip =~ m/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/ or
83             $ip =~ m/^[\d:]+(\/\d+)?$/) {
84 32         70 push @ips, $ip;
85             }
86             else {
87 2         5 push @non_ips, $ip;
88             }
89             }
90 5         25 return (\@non_ips, \@ips);
91             }
92              
93             # sort a list of hosts which may include non-IP addresses
94             sub ipv4sort {
95 3     3 0 9 my ($self, @ips_orig) = @_;
96              
97 3         16 my ($non_ips, $ips) = $self->split_ips(\@ips_orig);
98 3 50       23 my $case_sensitive = ref $self ? $self->case_sensitive : 0;
99 3         82 @{$non_ips} = $case_sensitive
  0         0  
100             ? natsort @{$non_ips}
101 3 50   2   27 : natkeysort { lc $_ } @{$non_ips};
  2         154  
  3         16  
102              
103 3         11 my %ips;
104 3         5 for my $ip (@{$ips}) {
  3         7  
105 7         26 my ($key) = $ip =~ m/([^\/]+)/; # key on just the IP part without range
106 7         16 $ips{$key} = $ip;
107             }
108 3         40 my @sorted_keys = Sort::Key::IPv4::ipv4sort(keys %ips);
109 3         8 my @ips = map { $ips{$_} } @sorted_keys;
  7         14  
110              
111 3         6 return (@{$non_ips}, @ips);
  3         14  
112             }
113              
114             sub sort_children {
115 3     3 1 7 my ($self) = @_;
116              
117 9 50       73 my %keys = map { (defined($_->sort_key) ? $_->sort_key : $_->name) => $_ }
  3         73  
118 3         5 values %{$self->children};
119 3         18 my @children = map { $keys{$_} } $self->ipv4sort(keys %keys);
  9         15  
120              
121             return wantarray
122             ? @children
123 3 50       19 : \@children;
124             }
125              
126             sub sprint {
127 3     3 1 8 my ($self, @args) = @_;
128              
129 3 100       14 if ($self->snowshoe) {
130             # create new child list and replace the old list
131 2         10 $self->children($self->condense_snowshoes);
132             }
133 3         286 return $self->SUPER::sprint(@args);
134             }
135              
136             # convert decimal dotted quad to binary IP
137             sub ip_to_bin {
138 54     54 0 106 my ($self, $ip) = @_;
139              
140 54         187 my $bin = Math::BigInt->new(0);
141 54         5603 for my $part (split '\.', $ip) {
142 216         32524 $bin <<= 8;
143 216         44284 $bin |= $part;
144             }
145 54         11209 return $bin
146             }
147              
148             # convert binary IP to decimal dotted quad
149             sub bin_to_ip {
150 3     3 0 7 my ($self, $bin) = @_;
151              
152 3         6 my @parts;
153 3         12 while (@parts < 4) {
154 12         2056 unshift @parts, $bin & 0xff;
155 12         2624 $bin >>= 8;
156             }
157 3         645 return join('.', @parts);
158             }
159              
160             # return a mask of $width
161             sub mask {
162 5     5 0 12 my ($self, $width) = @_;
163              
164 5         33 return Math::BigInt->new(1)->blsft($width)->bsub(1)->blsft(32-$width);
165             }
166              
167             sub min_range {
168 3     3 0 7 my ($self, $group) = @_; # group is ordered list of Logwatch::RecordTrees with IPs as names
169              
170 3         4 my $width = 32;
171 3         26 my $mask = $self->mask($width); # full width mask to start
172              
173 3         2014 my $masked_ip = $self->ip_to_bin($group->[0]->name);
174 3         7 for my $item (@{$group}) {
  3         11  
175 25         4811 my $ip = $self->ip_to_bin($item->name);
176 25         77 while ($width) {
177 42 100       2791 last if (($ip & $mask) == $masked_ip);
178 17         3555 $mask &= $mask->blsft(1);
179 17         4493 $width--;
180 17         52 $masked_ip &= $mask;
181             }
182             }
183 3         604 return $self->bin_to_ip($masked_ip). "/$width";
184             }
185              
186             # hackers often rent IP blocks (/24 is common) so the source IP isn't
187             # exactly duplicated. Collect IPs within a block into single child.
188             sub condense_snowshoes {
189 2     2 0 3 my ($self) = @_;
190              
191 2         6 my $mask_width = $self->snowshoe;
192             # mask width of 1 is pretty useless, so we'll interpret it as /24:
193 2 50       9 $mask_width = 24 if ($mask_width == 1);
194 2         8 my $mask = $self->mask($mask_width);
195              
196 2         1465 my ($non_ips, $ips) = $self->split_ips([keys %{$self->children}]);
  2         76  
197 2         9 @{$ips} = Sort::Key::IPv4::ipv4sort(@{$ips});
  2         10  
  2         34  
198              
199 2         5 my ($masked_ip, $count, @group, %new_children);
200 2         3 for my $ip (@{$ips}, '') { # add dummy at end to flush
  2         7  
201 27         639 my $child;
202 27 100       150 $child = $self->child_by_name($ip) if ($ip);
203 27 100       107 if ($masked_ip) { # skip the first time through
204 25 100 100     968 if ($ip and
205             $masked_ip == ($self->ip_to_bin($ip) & $mask)) { # in range?
206 22         5619 $count += $child->count;
207 22         205 push @group, $child;
208             }
209             else { # out of range (or last time through the loop with dummy)
210 3 50       232 if (@group < 3) { # require at least three before condensing
211 0         0 map { $new_children{$_->name} = $_ } @group; # copy to new list
  0         0  
212             }
213             else {
214 3         16 my $name = $self->min_range(\@group);
215 3         572 my $new_child
216             = $new_children{$name}
217             = $group[0]->new( # clone first child
218             name => $name,
219             sprint_name => $group[0]->sprint_name,
220             count_fields => [ '/', scalar @group ],
221             );
222             # transfer any children from group items to new parent
223 3         30 for my $item (@group) {
224 25         121 my @g_children = values %{$item->children};
  25         697  
225 25 100       247 if (@g_children) {
226 3         7 for my $child (@g_children) {
227 4         22 $new_child->adopt($child);
228             }
229             }
230             else { # no children, count is entirely from item
231 22         613 $new_child->count($new_child->count + $item->count);
232             }
233             }
234             }
235 3         29 undef $masked_ip; # start a new range
236             }
237             }
238 27 100 100     235 if ($ip and not $masked_ip) {
239 3         14 $masked_ip = $self->ip_to_bin($ip) & $mask;
240 3         674 @group = ( $self->child_by_name($ip) );
241 3         90 $count = $child->count;
242             }
243             }
244              
245             # rejoin the non-IP children
246 2         5 map { $new_children{$_} = $self->child_by_name($_) } @{$non_ips};
  0         0  
  2         7  
247              
248 2         201 $self->children(\%new_children);
249             }
250              
251             1;
252              
253             __END__