File Coverage

blib/lib/Logwatch/RecordTree/IPv4.pm
Criterion Covered Total %
statement 84 161 52.1
branch 11 40 27.5
condition 4 15 26.6
subroutine 20 26 76.9
pod 5 13 38.4
total 124 255 48.6


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