File Coverage

blib/lib/Parse/Netstat/Search.pm
Criterion Covered Total %
statement 158 199 79.4
branch 62 82 75.6
condition 43 51 84.3
subroutine 11 15 73.3
pod 10 10 100.0
total 284 357 79.5


line stmt bran cond sub pod time code
1             package Parse::Netstat::Search;
2              
3 2     2   108580 use 5.006;
  2         12  
4 2     2   9 use strict;
  2         2  
  2         35  
5 2     2   16 use warnings;
  2         4  
  2         81  
6 2     2   10 use base 'Error::Helper';
  2         4  
  2         833  
7 2     2   2142 use Net::CIDR;
  2         9245  
  2         3165  
8              
9             =head1 NAME
10              
11             Parse::Netstat::Search - Searches the connection list in the results returned by Parse::Netstat
12              
13             =head1 VERSION
14              
15             Version 0.1.1
16              
17             =cut
18              
19             our $VERSION = '0.1.1';
20              
21              
22             =head1 SYNOPSIS
23              
24              
25             use Parse::Netstat::Search;
26             use Parse::Netstat qw(parse_netstat);
27              
28             my $res = parse_netstat(output => join("", `netstat -n`), flavor=>$^O);
29              
30             my $search = Parse::Netstat::Search->new();
31              
32             $search->set_cidrs( [ '10.0.0.0/24', '192.168.0.0/16' ] );
33              
34             my @found=$search->search($res);
35              
36              
37             Two big things to bet aware of is this module does not currently resulve names and this module
38             does not handle unix sockets. Unix sockets will just be skipped over.
39              
40             The connection hashes returned differ from Parse::Netstat slightly. Below is what a standard ones
41             for IPv4/6 looks like.
42              
43             {
44             'foreign_host'=>'10.0.0.1',
45             'local_host'=>'10.0.0.2',
46             'foreign_port'=>'22222',
47             'local_port'=>'22',
48             'sendq'=>'0',
49             'recvq'=>'0',
50             'state' => 'ESTABLISHED',
51             'proto' => 'tcp4',
52             }
53              
54             This module has two additional keys, "local_pp" and "foreign_pp". Which contains and data
55             after % in a address. So "fe80::1%lo0" would be split into "fe80::1" and "lo0" as in the
56             example below.
57              
58             {
59             'state' => '',
60             'foreign_host' => '*',
61             'local_port' => '123',
62             'proto' => 'udp6',
63             'foreign_pp' => undef,
64             'foreign_port' => '*',
65             'local_host' => 'fe80::1',
66             'recvq' => '44',
67             'local_pp' => 'lo0',
68             'sendq' => '33'
69             }
70              
71             =head1 methods
72              
73             =head2 new
74              
75             This initiates it.
76              
77             No values are taken.
78              
79             my $search=Parse::Netstat::Search->new;
80              
81             =cut
82              
83             sub new{
84 1     1 1 495 my $self={
85             perror=>undef,
86             error=>undef,
87             errorString=>'',
88             errorExtra=>{
89             '1'=>'badCIDR',
90             '2' =>'unknownService',
91             '3'=>'badResults',
92             },
93             cidrs=>[],
94             protocols=>{},
95             ports=>{},
96             states=>{},
97             };
98 1         2 bless $self;
99              
100 1         2 return $self;
101              
102             }
103              
104             =head2 get_cidrs
105              
106             Retrieves the CIDR match list.
107              
108             The returned value is an array.
109              
110             my @CIDRs=$search->get_cidrs;
111              
112             =cut
113              
114             sub get_cidrs{
115 0     0 1 0 my $self=$_[0];
116              
117 0 0       0 if( ! $self->errorblank ){
118 0         0 return undef;
119             }
120              
121 0         0 return @{ $self->{cidrs} };
  0         0  
122             }
123              
124             =head2 get_ports
125              
126             Gets a list of desired ports.
127              
128             The returned value is a array. Each item is a port number,
129             regardless of if it was set based on number or service name.
130              
131             my @ports=$search->get_ports;
132              
133             =cut
134              
135             sub get_ports{
136 0     0 1 0 my $self=$_[0];
137              
138 0 0       0 if( ! $self->errorblank ){
139 0         0 return undef;
140             }
141              
142 0         0 return keys( %{ $self->{ports} } );
  0         0  
143             }
144              
145             =head2 get_protocols
146              
147             Gets a list of desired protocols.
148              
149             The returned value is a array.
150              
151             Also if you've passed any named ones to it previously,
152             this will not return them, but the port number as that
153             is how they are stored internlly.
154              
155             my @protocols=$search->get_protocols;
156              
157             =cut
158              
159             sub get_protocols{
160 0     0 1 0 my $self=$_[0];
161              
162 0 0       0 if( ! $self->errorblank ){
163 0         0 return undef;
164             }
165              
166 0         0 return keys( %{ $self->{protocols} } );
  0         0  
167             }
168              
169             =head2 get_states
170              
171             Get a list of desired sets.
172              
173             The returned value is a array.
174              
175             The returned values are all lowercased. Any trailing
176             or proceeding whitespace will also have been removed.
177              
178             my @states=$search->get_states;
179              
180             =cut
181              
182             sub get_states{
183 0     0 1 0 my $self=$_[0];
184              
185 0 0       0 if( ! $self->errorblank ){
186 0         0 return undef;
187             }
188              
189 0         0 return keys( %{ $self->{states} } );
  0         0  
190             }
191              
192             =head2 search
193              
194             This runs the search results.
195              
196             my @found=$search->search( $res );
197              
198             =cut
199              
200             sub search{
201 20     20 1 85 my $self=$_[0];
202 20         23 my $res=$_[1];
203              
204 20 50       40 if( ! $self->errorblank ){
205 0         0 return undef;
206             }
207              
208             #make sure what ever we are passed is sane and very likely a return from Parse::Netdata
209 20 50 33     223 if (
      33        
210             ( ref( $res ) ne 'ARRAY' ) ||
211             ( ! defined( $res->[2] ) ) ||
212             ( ! defined( $res->[2]->{active_conns} ) )
213             ){
214 0         0 $self->{error}=3;
215 0         0 $self->{errorString}='$res->[2]->{active_conns} not defiend. Does not appear to be a Parse::Netstat return';
216 0         0 $self->warn;
217 0         0 return undef;
218             }
219              
220             # holds the found results
221 20         28 my @found;
222              
223             # requirements checks, defaulting to not required
224 20         24 my $port_require=0;
225 20         23 my $cidr_require=0;
226 20         22 my $protocol_require=0;
227 20         22 my $state_require=0;
228              
229             # figure out what we need to check for
230 20 100       39 if (defined( $self->{cidrs}[0] )){
231 6         7 $cidr_require=1;
232             }
233 20 100       24 if (defined( (keys(%{ $self->{ports} }))[0] )){
  20         62  
234 4         5 $port_require=1;
235             }
236 20 100       25 if (defined( (keys(%{ $self->{protocols} }))[0] )){
  20         39  
237 3         3 $protocol_require=1;
238             }
239 20 100       26 if (defined( (keys(%{ $self->{states} }))[0] )){
  20         38  
240 4         6 $state_require=1;
241             }
242              
243 20         25 my $res_int=0;
244 20         54 while ( defined( $res->[2]->{active_conns}->[$res_int] ) ){
245             # ignore unix sockets
246 160 100 66     442 if ( defined( $res->[2]->{active_conns}->[$res_int]->{proto} ) &&
247             ($res->[2]->{active_conns}->[$res_int]->{proto} ne 'unix')
248             ){
249 140         184 my $foreign_port=$res->[2]->{active_conns}->[$res_int]->{foreign_port};
250 140         182 my $state=$res->[2]->{active_conns}->[$res_int]->{state};
251 140         166 my $protocol=$res->[2]->{active_conns}->[$res_int]->{proto};
252 140         160 my $local_port=$res->[2]->{active_conns}->[$res_int]->{local_port};
253             #my $local_host=$res->[2]->{active_conns}->[$res_int]->{local_host};
254             #my $foreign_host=$res->[2]->{active_conns}->[$res_int]->{foreign_host};
255 140         178 my $sendq=$res->[2]->{active_conns}->[$res_int]->{sendq};
256 140         167 my $recvq=$res->[2]->{active_conns}->[$res_int]->{recvq};
257              
258             #handle IPv6 % stuff if needed
259 140         300 my ( $local_host, $local_pp ) = split( /\%/, $res->[2]->{active_conns}->[$res_int]->{local_host} );
260 140         256 my ( $foreign_host, $foreign_pp ) = split( /\%/, $res->[2]->{active_conns}->[$res_int]->{foreign_host} );
261              
262             # Handle when parse netstat chokes on lines like...
263             # udp6 0 0 fe80::4ecc:6aff:.123 *.*
264 140 100       275 if ( $local_host =~ /[0123456789AaBbCcDdEeFf]\:$/ ){
265 20         62 $local_host =~ s/\:$//;
266             }
267 140 50       227 if ( $foreign_host =~ /[0123456789AaBbCcDdEeFf]\:$/ ){
268 0         0 $foreign_host =~ s/\:$//;
269             }
270              
271             # UDP is stateless and in some cases on listening ports for it Parse::Netstat
272             # does not return any host, so use * for it.
273 140 50       226 if (!defined( $foreign_host )){
274 0         0 $foreign_host='*';
275             }
276 140 50 100     348 if (
      66        
277             ( $foreign_host eq '*' ) &&
278             ( $protocol =~ /^[Uu][Dd][Pp]/ ) &&
279             ( ! defined( $state ) )
280             ){
281 0         0 $state='';
282             }
283 140 50 33     212 if (
284             ( !defined( $state ) ) &&
285             ( $protocol =~ /^[Uu][Dd][Pp]/ )
286             ){
287 0         0 $state='';
288             }
289              
290             # checks for making sure a check is meet... defaults to 1
291 140         159 my $port_meet=1;
292 140         152 my $cidr_meet=1;
293 140         142 my $protocol_meet=1;
294 140         173 my $protocol_search=lc( $protocol );
295 140         147 my $state_meet=1;
296 140         170 my $state_search=lc( $state );
297              
298             # reset the meet checks
299 140 100       194 if ( $port_require ) {
300 28         30 $port_meet=0;
301             }
302 140 100       185 if ( $cidr_require ) {
303 42         45 $cidr_meet=0;
304             }
305 140 100       187 if ( $protocol_require ) {
306 21         23 $protocol_meet=0;
307             }
308 140 100       185 if ( $state_require ) {
309 28         46 $state_meet=0;
310             }
311              
312             # checks the forient port against each CIDR
313 140         144 my @cidrs=@{ $self->{cidrs} };
  140         237  
314 140 100       196 if ( $cidr_require ){
315             # check each one by its self... Net::CIDR will error if you tell it to search for in IPv4 and IPv6 space at the same time
316 42         54 my @cidrs=@{ $self->{cidrs} };
  42         61  
317 42         65 my $cidr=pop( @cidrs );
318 42   100     100 while (
319             ( defined( $cidr ) ) &&
320             ( ! $cidr_meet )
321             ){
322 54 100 100     109 if (
      100        
      100        
323             (
324             ( $foreign_host ne '*' ) &&
325 31         53 ( eval{ Net::CIDR::cidrlookup( $foreign_host, $cidr ) })
326             ) || (
327             ( $local_host ne '*' ) &&
328 38         5845 ( eval{ Net::CIDR::cidrlookup( $local_host, $cidr ) } )
329             )
330             ){
331 14         3022 $cidr_meet=1;
332             }
333              
334 54         8334 $cidr=pop( @cidrs );
335             }
336             }
337              
338             # handle it if port checking is required
339 140 100 100     243 if (
      100        
340             $port_require &&
341             (
342             ( defined( $self->{ports}{$foreign_port} ) ) ||
343             ( defined( $self->{ports}{$local_port} ) )
344             )
345             ) {
346 20         20 $port_meet=1;
347             }
348              
349             # check protocol to see if it is one that is required
350 140 100 100     233 if (
351             $protocol_require &&
352             defined( $self->{protocols}{$protocol_search} )
353             ){
354 11         13 $protocol_meet=1;
355             }
356              
357             # check state to see if it is one that is required
358 140 100 100     229 if (
359             $state_require &&
360             defined( $self->{states}{$state_search} )
361             ){
362 8         10 $state_meet=1;
363             }
364              
365             # if these are all good, add them
366 140 100 100     501 if (
      100        
      100        
367             $port_meet && $protocol_meet && $cidr_meet && $state_meet
368             ){
369 81         396 push( @found, {
370             'foreign_port'=>$foreign_port,
371             'foreign_host'=>$foreign_host,
372             'local_port'=>$local_port,
373             'local_host'=>$local_host,
374             'sendq'=>$sendq,
375             'recvq'=>$recvq,
376             'proto'=>$protocol,
377             'state'=>$state,
378             'local_pp'=>$local_pp,
379             'foreign_pp'=>$foreign_pp,
380             }
381             );
382             }
383              
384             }
385              
386 160         299 $res_int++;
387             }
388              
389 20         120 return @found;
390             }
391              
392             =head2 set_cidrs
393              
394             This sets the list of CIDRs to search for
395             in either the local or remote field.
396              
397             One value is taken and that is a array ref of CIDRs.
398              
399             Validating in is done by Net::CIDR::cidrvalidate.
400              
401             If you are using this, you will want to use -n with netstat
402             as this module currently does not resolve names.
403              
404             # set the desired CIDRs to the contents of @CIDRs
405             $search->set_cidrs( \@CIDRs );
406             if ( $search->error ){
407             warn("Improper CIDR");
408             }
409              
410             # clear any previously set
411             $search->set_cidrs;
412              
413             =cut
414              
415             sub set_cidrs{
416 8     8 1 2545 my $self=$_[0];
417 8         12 my @cidrs;
418 8 100       19 if ( defined( $_[1] ) ){
419 6         7 @cidrs=@{ $_[1] };
  6         13  
420             }
421              
422 8 50       19 if( ! $self->errorblank ){
423 0         0 return undef;
424             }
425              
426             #blank it if none is given
427 8 100       71 if ( !defined( $cidrs[0] ) ){
428 2         4 $self->{cidrs}=\@cidrs;
429             }
430              
431             #chueck each one
432 8         11 my $cidr_int=0;
433 8         14 while ( defined( $cidrs[$cidr_int] ) ){
434 8         12 my $cidr=$cidrs[$cidr_int];
435 8 50       25 if ( ! Net::CIDR::cidrvalidate( $cidr ) ){
436 0         0 $self->{error}=1;
437 0         0 $self->{errorString}='"'.$cidr.'" is not a valid CIDR according to Net::CIDR::cidrvalidate';
438 0         0 $self->warn;
439 0         0 return undef;
440              
441             }
442              
443 8         23338 $cidr_int++;
444             }
445              
446 8         18 $self->{cidrs}=\@cidrs;
447              
448 8         38 return 1;
449             }
450              
451             =head2 set_ports
452              
453             This sets the ports to search for in either
454             the local or remote field.
455              
456             One value is taken and that is a array ref of ports.
457              
458             The ports can be either numeric or by name.
459              
460             # Set the desired ports to the contents of @ports.
461             $search->set_ports( \@ports );
462             if ( $search->error ){
463             warn("Bad value in ports array");
464             }
465              
466             # removes any previous selections
467             $search->set_ports;
468              
469             =cut
470              
471             sub set_ports{
472 4     4 1 725 my $self=$_[0];
473 4         6 my @ports;
474 4 100       9 if ( defined( $_[1] ) ){
475 3         4 @ports=@{ $_[1] };
  3         7  
476             }
477              
478 4 50       9 if( ! $self->errorblank ){
479 0         0 return undef;
480             }
481              
482 4 100       34 if ( !defined( $ports[0] ) ){
483 1         4 $self->{ports}={};
484             }
485              
486 4         7 my $port=pop( @ports );
487 4         5 my %lookup_hash;
488 4         9 while( defined( $port ) ){
489 4         5 my $port_number=$port;
490 4 100       16 if ( $port !~ /^\d+$/ ){
491             # Find the first matching port number.
492             # Does not care what protocol comes up.
493 2         518 $port_number=(getservbyname( $port , '' ))[2];
494              
495             # If it is not defined, we did not find a matching
496             # service record for the requested port name.
497 2 50       10 if ( !defined( $port_number ) ){
498 0         0 $self->{error}=2;
499 0         0 $self->{errorString}='"'.$port.'" was not found as a known service';
500 0         0 $self->warn;
501 0         0 return undef;
502             }
503             }
504              
505 4         10 $lookup_hash{$port_number}=1;
506              
507 4         11 $port=pop( @ports );
508             }
509              
510             #save this for later
511 4         7 $self->{ports}=\%lookup_hash;
512              
513 4         10 return 1;
514             }
515              
516             =head2 set_protocols
517              
518             Sets the list of desired protocols to match.
519              
520             One value is taken and that is a array.
521              
522             If this is undef, then previous settings will be cleared.
523              
524             Lacking of exhaustive list of possible values for the
525             OSes supported by Parse::Netstat, no santity checking
526             is done.
527              
528             Starting and trailing white space is removed.
529              
530             # Set the desired ports to the contents of @protocols.
531             $search->set_protocols( \@protocols );
532             if ( $search->error ){
533             warn("Bad value in ports array");
534             }
535              
536             # removes any previous selections
537             $search->set_protocols;
538              
539             =cut
540              
541             sub set_protocols{
542 3     3 1 492 my $self=$_[0];
543 3         5 my @protocols;
544 3 100       6 if ( defined( $_[1] ) ){
545 2         3 @protocols=@{ $_[1] };
  2         5  
546             }
547              
548 3 50       7 if( ! $self->errorblank ){
549 0         0 return undef;
550             }
551              
552 3 100       22 if ( !defined( $protocols[0] ) ){
553 1         3 $self->{protocols}={};
554             }
555              
556 3         4 my %lookup_hash;
557 3         6 my $protocol=pop( @protocols );
558 3         5 while( defined( $protocol ) ){
559 2         8 $protocol=~s/^[\ \t]*//;
560 2         5 $protocol=~s/^[\ \t]*//;
561              
562             #create a LCed version of the protocol name
563 2         6 $lookup_hash{ lc( $protocol ) }=1;
564              
565 2         12 $protocol=pop( @protocols );
566             }
567              
568             #save it for usage later
569 3         6 $self->{protocols}=\%lookup_hash;
570              
571 3         8 return 1;
572             }
573              
574             =head2 set_states
575              
576             Sets the list of desired states to match.
577              
578             One value is taken and that is a array.
579              
580             If this is undef, then previous settings will be cleared.
581              
582             Lacking of exhaustive list of possible values for the
583             OSes supported by Parse::Netstat, no santity checking
584             is done.
585              
586             Starting and trailing white space is removed.
587              
588             # Set the desired ports to the contents of @protocols.
589             $search->set_protocols( \@protocols );
590             if ( $search->error ){
591             warn("Bad value in ports array");
592             }
593              
594             # removes any previous selections
595             $search->set_protocols;
596              
597             =cut
598              
599             sub set_states{
600 8     8 1 1517 my $self=$_[0];
601 8         9 my @states;
602 8 100       17 if ( defined( $_[1] ) ){
603 4         6 @states=@{ $_[1] };
  4         8  
604             }
605              
606 8 50       18 if( ! $self->errorblank ){
607 0         0 return undef;
608             }
609              
610 8 100       62 if ( !defined( $states[0] ) ){
611 4         7 $self->{staes}={};
612             }
613              
614 8         24 my %lookup_hash;
615 8         12 my $state=pop(@states);
616 8         21 while ( defined( $state ) ){
617 4         18 $state=~s/^[\ \t]*//;
618 4         10 $state=~s/^[\ \t]*//;
619              
620             #create a LCed version of the protocol name
621 4         11 $lookup_hash{ lc( $state ) }=1;
622              
623 4         9 $state=pop(@states);
624             }
625              
626             #save it for usage later
627 8         16 $self->{states}=\%lookup_hash;
628              
629 8         16 return 1;
630             }
631              
632             =head1 ERROR CODES / FLAGS
633              
634             Error handling is provided by L.
635              
636             =head2 1 / badCIDR
637              
638             Invalid CIDR passed.
639              
640             Validation is done by Net::CIDR::cidrvalidate.
641              
642             =head2 2 / unknownService
643              
644             Could not look up the port number for the specified service.
645              
646             =head2 3 / badResults
647              
648             The passed array does not appear to be properly formatted.
649              
650             =head1 AUTHOR
651              
652             Zane C. Bowers-Hadley, C<< >>
653              
654             =head1 BUGS
655              
656             Please report any bugs or feature requests to C, or through
657             the web interface at L. I will be notified, and then you'll
658             automatically be notified of progress on your bug as I make changes.
659              
660              
661              
662              
663             =head1 SUPPORT
664              
665             You can find documentation for this module with the perldoc command.
666              
667             perldoc Parse::Netstat::Search
668              
669              
670             You can also look for information at:
671              
672             =over 4
673              
674             =item * RT: CPAN's request tracker (report bugs here)
675              
676             L
677              
678             =item * AnnoCPAN: Annotated CPAN documentation
679              
680             L
681              
682             =item * CPAN Ratings
683              
684             L
685              
686             =item * Search CPAN
687              
688             L
689              
690             =item * Code Repo
691              
692             L
693              
694             =back
695              
696              
697             =head1 ACKNOWLEDGEMENTS
698              
699              
700             =head1 LICENSE AND COPYRIGHT
701              
702             Copyright 2019 Zane C. Bowers-Hadley.
703              
704             This program is free software; you can redistribute it and/or modify it
705             under the terms of the the Artistic License (2.0). You may obtain a
706             copy of the full license at:
707              
708             L
709              
710             Any use, modification, and distribution of the Standard or Modified
711             Versions is governed by this Artistic License. By using, modifying or
712             distributing the Package, you accept this license. Do not use, modify,
713             or distribute the Package, if you do not accept this license.
714              
715             If your Modified Version has been derived from a Modified Version made
716             by someone other than you, you are nevertheless required to ensure that
717             your Modified Version complies with the requirements of this license.
718              
719             This license does not grant you the right to use any trademark, service
720             mark, tradename, or logo of the Copyright Holder.
721              
722             This license includes the non-exclusive, worldwide, free-of-charge
723             patent license to make, have made, use, offer to sell, sell, import and
724             otherwise transfer the Package with respect to any patent claims
725             licensable by the Copyright Holder that are necessarily infringed by the
726             Package. If you institute patent litigation (including a cross-claim or
727             counterclaim) against any party alleging that the Package constitutes
728             direct or contributory patent infringement, then this Artistic License
729             to you shall terminate on the date that such litigation is filed.
730              
731             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
732             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
733             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
734             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
735             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
736             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
737             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
738             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
739              
740              
741             =cut
742              
743             1; # End of Parse::Netstat::Search