File Coverage

blib/lib/Logwatch/RecordTree/IPv4.pm
Criterion Covered Total %
statement 78 152 51.3
branch 12 40 30.0
condition 5 15 33.3
subroutine 18 23 78.2
pod 4 11 36.3
total 117 241 48.5


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   16 use 5.008;
  1         5  
  1         35  
12 1     1   4 use strict;
  1         1  
  1         28  
13 1     1   3 use warnings;
  1         1  
  1         34  
14              
15             package Logwatch::RecordTree::IPv4;
16 1     1   419 use parent 'Logwatch::RecordTree';
  1         242  
  1         7  
17 1     1   49 use Moo;
  1         1  
  1         10  
18 1     1   240 use UNIVERSAL::require;
  1         1  
  1         9  
19             # use Net::IP::Identifier 0.111
20 1     1   22 use Carp qw( croak );
  1         2  
  1         56  
21 1     1   418 use Sort::Key::IPv4;
  1         2897  
  1         45  
22 1     1   6 use Sort::Key::Natural qw( natsort natkeysort );
  1         1  
  1         48  
23 1     1   1001 use Math::BigInt;
  1         15292  
  1         8  
24              
25             our $VERSION = '2.055'; # 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 1     1 1 1 my ($self) = @_;
38              
39 1 50       5 if (not $self->{identifier}) {
40 1 50       3 if (not $identifier) {
41 1 50       10 Net::IP::Identifier->require(0.111)
42             or croak($@);
43 0         0 $identifier = Net::IP::Identifier->new;
44             }
45 0         0 $self->{identifier} = $identifier;
46             }
47 0         0 return $self->{identifier};
48             }
49              
50             sub create_child { # override
51 31     31 1 51 my ($self, $name, $type, $opts) = @_;
52              
53 31         82 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 2     2   11 my ($child) = @_;
59              
60 2         4 my $ip = $child->name;
61 2 100 66     25 if ($self->identify and
      33        
62             ($ip =~ m/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/ or
63             $ip =~ m/^[\d:]+(\/\d+)?$/)) {
64 1         8 my $id = $self->identifier->identify($ip);
65 0 0       0 if ($id) {
66 0 0       0 $id = substr($id, 0, 8) if (length $id > 8);
67 0         0 $ip = "$id-$ip";
68             }
69             }
70 1         4 return $ip;
71 31         1330 });
72              
73 31         328 return $child;
74             }
75              
76             # the IP list may contain non-IP addresses, split into two lists:
77             sub split_ips {
78 1     1 0 1 my ($self, $ips_orig) = @_;
79              
80 1         2 my (@non_ips, @ips);
81 1         1 for my $ip (@{$ips_orig}) {
  1         3  
82 6 100 66     39 if ($ip =~ m/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/ or
83             $ip =~ m/^[\d:]+(\/\d+)?$/) {
84 4         6 push @ips, $ip;
85             }
86             else {
87 2         3 push @non_ips, $ip;
88             }
89             }
90 1         4 return (\@non_ips, \@ips);
91             }
92              
93             # sort a list of hosts which may include non-IP addresses
94             sub ipv4sort {
95 1     1 0 3 my ($self, @ips_orig) = @_;
96              
97 1         5 my ($non_ips, $ips) = $self->split_ips(\@ips_orig);
98 1 50       9 my $case_sensitive = ref $self ? $self->case_sensitive : 0;
99 1         50 @{$non_ips} = $case_sensitive
  0         0  
100             ? natsort @{$non_ips}
101 1 50   2   5 : natkeysort { lc $_ } @{$non_ips};
  2         100  
  1         7  
102              
103 1         4 my %ips;
104 1         2 for my $ip (@{$ips}) {
  1         2  
105 4         10 my ($key) = $ip =~ m/([^\/]+)/; # key on just the IP part without range
106 4         6 $ips{$key} = $ip;
107             }
108 1         16 my @sorted_keys = Sort::Key::IPv4::ipv4sort(keys %ips);
109 1         3 my @ips = map { $ips{$_} } @sorted_keys;
  4         6  
110              
111 1         2 return (@{$non_ips}, @ips);
  1         5  
112             }
113              
114             sub sort_children {
115 1     1 1 2 my ($self) = @_;
116              
117 6 50       28 my %keys = map { (defined($_->sort_key) ? $_->sort_key : $_->name) => $_ }
  1         15  
118 1         2 values %{$self->children};
119 1         5 my @children = map { $keys{$_} } $self->ipv4sort(keys %keys);
  6         8  
120              
121             return wantarray
122             ? @children
123 1 50       6 : \@children;
124             }
125              
126             sub sprint {
127 1     1 1 3 my ($self, @args) = @_;
128              
129 1 50       5 if ($self->snowshoe) {
130             # create new child list and replace the old list
131 0         0 $self->children($self->condense_snowshoes);
132             }
133 1         10 return $self->SUPER::sprint(@args);
134             }
135              
136             # convert decimal dotted quad to binary IP
137             sub ip_to_bin {
138 0     0 0   my ($self, $ip) = @_;
139              
140 0           my $bin = Math::BigInt->new(0);
141 0           for my $part (split '\.', $ip) {
142 0           $bin <<= 8;
143 0           $bin |= $part;
144             }
145 0           return $bin
146             }
147              
148             # convert binary IP to decimal dotted quad
149             sub bin_to_ip {
150 0     0 0   my ($self, $bin) = @_;
151              
152 0           my @parts;
153 0           while (@parts < 4) {
154 0           unshift @parts, $bin & 0xff;
155 0           $bin >>= 8;
156             }
157 0           return join('.', @parts);
158             }
159              
160             # return a mask of $width
161             sub mask {
162 0     0 0   my ($self, $width) = @_;
163              
164 0           return Math::BigInt->new(1)->blsft($width)->bsub(1)->blsft(32-$width);
165             }
166              
167             sub min_range {
168 0     0 0   my ($self, $group) = @_; # group is ordered list of Logwatch::RecordTrees with IPs as names
169              
170 0           my $width = 32;
171 0           my $mask = $self->mask($width); # full width mask to start
172              
173 0           my $masked_ip = $self->ip_to_bin($group->[0]->name);
174 0           for my $item (@{$group}) {
  0            
175 0           my $ip = $self->ip_to_bin($item->name);
176 0           while ($width) {
177 0 0         last if (($ip & $mask) == $masked_ip);
178 0           $mask &= $mask->blsft(1);
179 0           $width--;
180 0           $masked_ip &= $mask;
181             }
182             }
183 0           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 0     0 0   my ($self) = @_;
190              
191 0           my $mask_width = $self->snowshoe;
192             # mask width of 1 is pretty useless, so we'll interpret it as /24:
193 0 0         $mask_width = 24 if ($mask_width == 1);
194 0           my $mask = $self->mask($mask_width);
195              
196 0           my ($non_ips, $ips) = $self->split_ips([keys %{$self->children}]);
  0            
197 0           @{$ips} = Sort::Key::IPv4::ipv4sort(@{$ips});
  0            
  0            
198              
199 0           my ($masked_ip, $count, @group, %new_children);
200 0           for my $ip (@{$ips}, '') { # add dummy at end to flush
  0            
201 0           my $child;
202 0 0         $child = $self->child_by_name($ip) if ($ip);
203 0 0         if ($masked_ip) { # skip the first time through
204 0 0 0       if ($ip and
205             $masked_ip == ($self->ip_to_bin($ip) & $mask)) { # in range?
206 0           $count += $child->count;
207 0           push @group, $child;
208             }
209             else { # out of range (or last time through the loop with dummy)
210 0 0         if (@group < 3) { # require at least three before condensing
211 0           map { $new_children{$_->name} = $_ } @group; # copy to new list
  0            
212             }
213             else {
214 0           my $name = $self->min_range(\@group);
215 0           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 0           for my $item (@group) {
224 0           my @g_children = values %{$item->children};
  0            
225 0 0         if (@g_children) {
226 0           for my $child (@g_children) {
227 0           $new_child->adopt($child);
228             }
229             }
230             else { # no children, count is entirely from item
231 0           $new_child->count($new_child->count + $item->count);
232             }
233             }
234             }
235 0           undef $masked_ip; # start a new range
236             }
237             }
238 0 0 0       if ($ip and not $masked_ip) {
239 0           $masked_ip = $self->ip_to_bin($ip) & $mask;
240 0           @group = ( $self->child_by_name($ip) );
241 0           $count = $child->count;
242             }
243             }
244              
245             # rejoin the non-IP children
246 0           map { $new_children{$_} = $self->child_by_name($_) } @{$non_ips};
  0            
  0            
247              
248 0           $self->children(\%new_children);
249             }
250              
251             1;
252              
253             __END__