File Coverage

lib/Net/Abuse/Utils.pm
Criterion Covered Total %
statement 22 163 13.5
branch 0 76 0.0
condition 0 15 0.0
subroutine 8 26 30.7
pod 15 15 100.0
total 45 295 15.2


line stmt bran cond sub pod time code
1             package Net::Abuse::Utils;
2             # ABSTRACT: Routines useful for processing network abuse
3              
4 1     1   50917 use 5.006;
  1         17  
5 1     1   4 use strict;
  1         1  
  1         21  
6 1     1   4 use warnings;
  1         1  
  1         18  
7              
8 1     1   432 use Net::DNS;
  1         69483  
  1         177  
9 1     1   397 use Net::Whois::IP 1.11 'whoisip_query';
  1         3427  
  1         55  
10 1     1   457 use Email::Address::XS;
  1         3966  
  1         39  
11 1     1   535 use Net::IP;
  1         45705  
  1         1842  
12             # use Memoize;
13              
14             require Exporter;
15              
16             our @ISA = qw(Exporter);
17              
18             our %EXPORT_TAGS = ( 'all' => [ qw(
19             get_asn_info get_peer_info get_as_description get_soa_contact get_ipwi_contacts
20             get_rdns get_dnsbl_listing get_ip_country get_asn_country
21             get_abusenet_contact is_ip get_as_company get_domain get_malware
22             ) ] );
23              
24             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
25             our $VERSION = '0.27';
26             $VERSION = eval $VERSION;
27              
28             # memoize('_return_rr');
29             my @tlds;
30             our @RESOLVERS;
31              
32             sub _reverse_ip {
33 0     0   0 my $ip = shift;
34 0         0 my $ver = Net::IP::ip_get_version($ip);
35 0 0       0 my @parts = split( /\./, Net::IP::ip_reverse( $ip, $ver == 4 ? 32 : 128 ) );
36             # strip in-addr.arp or ip6.arpa from results
37 0         0 return join('.', @parts[0 .. $#parts-2]);
38             }
39              
40             sub _return_rr {
41 0     0   0 my $lookup = shift;
42 0         0 my $rr_type = shift;
43 0         0 my $concat = shift;
44              
45 0         0 my @result;
46              
47 0         0 my $res = Net::DNS::Resolver->new( );
48 0 0       0 $res->nameservers(@RESOLVERS) if @RESOLVERS;
49              
50 0         0 my $query = $res->query($lookup, $rr_type);
51 0 0       0 if ($query) {
52 0         0 foreach my $rr ($query->answer) {
53 0 0       0 if ($rr->type eq $rr_type) {
54 0 0       0 if ($rr_type eq 'TXT') {
    0          
    0          
55 0         0 push @result, $rr->txtdata;
56             }
57             elsif ($rr_type eq 'SOA') {
58 0         0 push @result, $rr->rname;
59             }
60             elsif ($rr_type eq 'PTR') {
61 0         0 push @result, $rr->ptrdname;
62             }
63 0 0       0 last if !$concat;
64             }
65             }
66              
67 0 0 0     0 if ($concat && $concat == 2) {
68 0         0 return @result;
69             }
70             else {
71 0         0 return join ' ', @result;
72             }
73             }
74              
75 0         0 return;
76             }
77              
78             sub _return_unique {
79 0     0   0 my $array_ref = shift;
80 0         0 my %unique_elements;
81              
82 0         0 foreach my $element (@$array_ref) {
83 0         0 $unique_elements{ $element }++;
84             }
85              
86 0         0 return keys %unique_elements;
87             }
88              
89             sub _strip_whitespace {
90 0     0   0 my $string = shift;
91              
92 0 0       0 return unless $string;
93              
94 0         0 for ($string) {
95 0         0 s/^\s+//;
96 0         0 s/\s+$//;
97             }
98              
99 0         0 return $string;
100             }
101              
102             sub get_ipwi_contacts {
103 0     0 1 0 my $ip = shift;
104 0         0 my $ver = Net::IP::ip_get_version($ip);
105 0 0       0 return unless $ver;
106              
107 0         0 my @addresses;
108             my %unique_addresses;
109              
110             # work-around for the new way arin works
111             # it doesn't like networks very well.
112 0         0 my @bits = split(/\//,$ip);
113 0 0       0 $ip = $bits[0] if($#bits > 0);
114              
115 0         0 my $response = whoisip_query($ip);
116              
117             # whoisip_query returns array ref if not found
118 0 0       0 return unless ref($response) eq 'HASH';
119              
120 0 0       0 my @fields = exists $response->{'abuse-mailbox'} ? ( 'abuse-mailbox' ) : keys %$response;
121 0         0 foreach my $field (@fields) {
122 0         0 push @addresses, Email::Address::XS->parse($response->{$field});
123             }
124              
125 0         0 @addresses = grep { defined $_ } map { $_->address } @addresses;
  0         0  
  0         0  
126 0         0 return _return_unique (\@addresses);
127             }
128              
129             sub get_all_asn_info {
130 0     0 1 0 my $ip = shift;
131 0         0 my $ver = Net::IP::ip_get_version($ip);
132 0 0       0 return unless $ver;
133              
134 0 0       0 my $domain
135             = ( $ver == 4 ) ? '.origin.asn.cymru.com' : '.origin6.asn.cymru.com';
136              
137 0         0 my $lookup = _reverse_ip($ip) . $domain;
138 0 0       0 my $data = [ _return_rr( $lookup, 'TXT', 2 ) ] or return;
139              
140             # Separate fields and order by netmask length
141             # 23028 | 216.90.108.0/24 | US | arin | 1998-09-25
142             # 701 1239 3549 3561 7132 | 216.90.108.0/24 | US | arin | 1998-09-25
143 0         0 for my $asinfo (@$data) {
144 0         0 $asinfo = { data => [ split m/ ?\| ?/, $asinfo ] };
145 0         0 $asinfo->{length} = ( split m|/|, $asinfo->{data}[1] )[1];
146             }
147 0         0 $data = [ map { $_->{data} }
148 0         0 reverse sort { $a->{length} <=> $b->{length} } @$data ];
  0         0  
149              
150 0         0 return $data;
151             }
152              
153             sub get_asn_info {
154 0     0 1 0 my $data = get_all_asn_info(shift);
155 0 0 0     0 return unless $data && @$data;
156              
157             # just the first AS if multiple ASes are listed
158 0 0       0 if ($data->[0][0] =~ /^(\d+) \d+/) {
159 0         0 $data->[0][0] = $1;
160             }
161              
162             # return just the first result, as a list
163 0         0 return @{ $data->[0] };
  0         0  
164             }
165              
166             sub get_peer_info {
167 0     0 1 0 my $ip = shift;
168              
169             # IPv4 only until Cymru has an IPv6 peer database
170 0         0 my $ver = Net::IP::ip_get_version($ip);
171 0 0 0     0 return unless $ver && $ver == 4;
172              
173 0         0 my $lookup = _reverse_ip($ip) . '.peer.asn.cymru.com';
174 0 0       0 my @origin_as = _return_rr($lookup, 'TXT', 2) or return;
175              
176 0         0 my $return = [];
177 0         0 foreach my $as (@origin_as){
178 0         0 my @peers = split(/\s\|\s?/,$as);
179 0         0 my %hash = (
180             prefix => $peers[1],
181             cc => $peers[2],
182             rir => $peers[3],
183             date => $peers[4],
184             );
185 0         0 my @asns = split(/\s/,$peers[0]);
186 0         0 foreach (@asns){
187 0         0 $hash{'asn'} = $_;
188 0         0 push(@$return,{
189             prefix => $peers[1],
190             cc => $peers[2],
191             rir => $peers[3],
192             date => $peers[4],
193             asn => $_,
194             });
195             }
196             }
197 0 0       0 return(@$return) if wantarray;
198 0         0 return($return);
199             }
200              
201             # test with 733a48a9cb49651d72fe824ca91e8d00
202             # http://www.team-cymru.org/Services/MHR/
203              
204             sub get_malware {
205 0     0 1 0 my $hash = shift;
206 0 0 0     0 return unless($hash && lc($hash) =~ /^[a-z0-9]{32}$/);
207              
208 0         0 my $lookup = $hash.'.malware.hash.cymru.com';
209              
210 0 0       0 my $res = _return_rr($lookup, 'TXT') or return;
211 0         0 my ($last_seen,$detection_rate) = split(/ /,$res);
212             return({
213 0         0 last_seen => $last_seen,
214             detection_rate => $detection_rate,
215             });
216             }
217              
218             sub get_as_description {
219 0     0 1 0 my $asn = shift;
220 0         0 my @ASdata;
221              
222 0 0       0 if ( my $data = _return_rr( "AS${asn}.asn.cymru.com", 'TXT' ) ) {
223 0         0 @ASdata = split( '\|', $data );
224             }
225             else {
226 0         0 return;
227             }
228              
229 0 0       0 return unless $ASdata[4];
230 0         0 my $org = _strip_whitespace( $ASdata[4] );
231              
232             # for arin we get "HANDLE - AS Org"
233             # we want to make it "HANDLE AS Org" to match other RIRs
234 0 0       0 $org =~ s/^(\S+) - (.*)$/$1 $2/ if ( $ASdata[2] eq ' arin ' );
235              
236 0         0 return $org;
237             }
238              
239             sub get_as_company {
240 0     0 1 0 my $asn = shift;
241              
242 0         0 my $desc = get_as_description($asn);
243 0 0       0 return unless defined($desc);
244              
245             # remove leading org id/handle/etc
246 0         0 $desc =~ s/^[-_A-Z0-9]+ //;
247              
248             # remove trailing 'AS'
249 0         0 $desc =~ s/AS(:? Number)?$//;
250              
251             # remove trailing 'Autonomous System'
252 0         0 $desc =~ s/Autonomous System(:? Number)?$//i;
253              
254 0         0 return $desc;
255             }
256              
257             sub get_soa_contact {
258 0     0 1 0 my $ip = shift;
259              
260 0         0 my $lookup = _reverse_ip($ip) . '.in-addr.arpa';
261 0         0 $lookup =~ s/^\d+\.//;
262              
263 0 0       0 if ( my $soa_contact = _return_rr($lookup, 'SOA') ) {
264 0 0       0 $soa_contact =~ s/\./@/ unless $soa_contact =~ m/@/;
265 0         0 return $soa_contact;
266             }
267              
268 0         0 return;
269             }
270              
271             sub get_rdns {
272 0     0 1 0 my $ip = shift;
273 0         0 my $ver = Net::IP::ip_get_version($ip);
274 0 0       0 return unless $ver;
275              
276 0 0       0 my $suffix = ($ver == 4) ? '.in-addr.arpa' : '.ip6.arpa';
277 0         0 return _return_rr( _reverse_ip($ip) . $suffix, 'PTR');
278             }
279              
280             sub get_dnsbl_listing {
281 0     0 1 0 my ($ip, $dnsbl) = @_;
282              
283             # IPv4 Only
284 0         0 my $ver = Net::IP::ip_get_version($ip);
285 0 0 0     0 return unless $ver && $ver == 4;
286              
287 0         0 my $lookup = join '.', _reverse_ip( $ip ), $dnsbl;
288              
289 0         0 return _return_rr($lookup, 'TXT', 1);
290             }
291              
292             sub get_ip_country {
293 0     0 1 0 my $ip = shift;
294 0         0 return (get_asn_info($ip))[2];
295             }
296              
297             sub get_asn_country {
298 0     0 1 0 my $asn = shift;
299 0 0       0 return unless $asn =~ /^\d+$/;
300              
301 0         0 my $as_cc = (split (/\|/,_return_rr("AS${asn}.asn.cymru.com", 'TXT')))[1];
302 0 0       0 if ($as_cc) {
303 0         0 return _strip_whitespace($as_cc);
304             }
305 0         0 return;
306             }
307              
308             sub get_abusenet_contact {
309 0     0 1 0 my $domain = shift;
310 0         0 return _return_rr("$domain.contacts.abuse.net", 'TXT', 1)
311             }
312              
313             sub is_ip {
314 5     5 1 1219 my $ip = shift;
315 5         16 return defined Net::IP::ip_get_version($ip);
316             }
317              
318             sub get_domain {
319 0     0 1   my $hostname = shift;
320              
321 0 0         @tlds = grep {!/^#/} unless scalar @tlds;
  0            
322 0           my @parts = reverse (split /\./, $hostname);
323              
324 0 0         if (scalar @parts == 2) {
325             # just two parts, lets return it
326 0           return join '.', @parts[1, 0];
327             }
328 0 0         if (grep /^\Q$parts[1].$parts[0]\E$/, @tlds) {
329             # last two parts found in tlds
330 0           return join '.', @parts[2, 1, 0];
331             } else {
332             # last two not found so *host.domain.name
333 0           return join '.', @parts[1, 0];
334             }
335             }
336              
337             1;
338              
339             =pod
340              
341             =encoding UTF-8
342              
343             =head1 NAME
344              
345             Net::Abuse::Utils - Routines useful for processing network abuse
346              
347             =head1 VERSION
348              
349             version 0.27
350              
351             =head1 SYNOPSIS
352              
353             use Net::Abuse::Utils qw( :all );
354             print "IP Whois Contacts: ", join( ' ', get_ipwi_contacts($ip) ), "\n";
355             print "Abuse.net Contacts: ", get_abusenet_contact($domain), "\n";
356              
357             =head1 DESCRIPTION
358              
359             Net::Abuse::Utils provides serveral functions useful for determining
360             information about an IP address including contact/reporting addresses,
361             ASN/network info, reverse dns, and DNSBL listing status. Functions which take
362             an IP accept either IPv6 or IPv4 IPs unless indicated otherwise.
363              
364             =head1 CONFIGURATION
365              
366             There is a C<@RESOLVERS> package variable you can use to specify name servers
367             different than the systems nameservers for queries from this module. If you
368             intend to use Google's nameservers here, please see L
369             a note of caution|https://github.com/mikegrb/Net-Abuse-Utils/issues/9#issuecomment-24387435>.
370              
371             =head1 FUNCTIONS
372              
373             The following functions are exportable from this module. You may import all
374             of them into your namespace with the C<:all> tag.
375              
376             =head2 get_asn_info ( IP )
377              
378             Returns a list containing (ASN, Network/Mask, CC code, RIR, modified date)
379             for the network announcing C.
380              
381             =head2 get_all_asn_info ( IP )
382              
383             Returns a reference to a list of listrefs containting ASN(s), Network,Mask,
384             CC code, RIR, and modified date fall all networks announcing C.
385              
386             =head2 get_peer_info ( IP )
387              
388             IPv4 Only. Returns an array of hash references containing (ASN, Network/Mask,
389             CC code, RIR, modified date) for the peers of the network announcing C.
390              
391             =head2 get_as_description ( ASN )
392              
393             Returns the AS description for C.
394              
395             =head2 get_as_company ( ASN )
396              
397             Similiar to L but attempts to clean it up some before
398             returning it.
399              
400             =head2 get_soa_contact( IP )
401              
402             Returns the SOA contact email address for the reverse DNS /24
403             zone containing C.
404              
405             =head2 get_ipwi_contacts( IP )
406              
407             Returns a list of all email addresses found in whois information
408             for C with duplicates removed.
409              
410             =head2 get_rdns( IP )
411              
412             Returns the reverse PTR for C.
413              
414             =head2 get_dnsbl_listing( IP, DNSBL zone )
415              
416             IPv4 Only. Returns the listing text for C for the designated DNSBL.
417             C should be the zone used for looking up addresses in the
418             blocking list.
419              
420             =head2 get_ip_country( IP )
421              
422             Returns the 2 letter country code for C.
423              
424             =head2 get_asn_country( ASN )
425              
426             Returns the 2 letter country code for C.
427              
428             =head2 get_abusenet_contact ( domain )
429              
430             Returns the abuse.net listed contact email addresses for C.
431              
432             =head2 is_ip ( IP )
433              
434             Returns true if C looks like an IP, false otherwise.
435              
436             =head2 get_domain ( IP )
437              
438             Takes a hostname and attempts to return the domain name.
439              
440             =head2 get_malware ( md5 )
441              
442             Takes a malware md5 hash and tests it against
443             http://www.team-cymru.org/Services/MHR. Returns a HASHREF of last_seen and
444             detection_rate.
445              
446             =head1 DIAGNOSTICS
447              
448             Each subroutine will return undef if unsuccessful. In the future,
449             debugging output will be available.
450              
451             =head1 CONFIGURATION AND ENVIRONMENT
452              
453             There are two commented out lines that can be uncommented to enable Memoize
454             support. I haven't yet decided whether to include this option by default. It
455             may be made available in the future via an import flag to use.
456              
457             =head1 DEPENDENCIES
458              
459             This module makes use of the following modules:
460              
461             L, L, L, and L
462              
463             =head1 BUGS AND LIMITATIONS
464              
465             There are no known bugs in this module. Please report problems to
466             Michael Greb (mgreb@linode.com)
467              
468             Patches are welcome.
469              
470             =head1 ACKNOWLEDGEMENTS
471              
472             This module was inspired by Karsten M. Self's SpamTools shell scripts,
473             available at http://linuxmafia.com/~karsten/.
474              
475             Thanks as well to my employer, Linode.com, for allowing me the time to work
476             on this module.
477              
478             Rik Rose, Jon Honeycutt, Brandon Hale, TJ Fontaine, A. Pagaltzis, and
479             Heidi Greb all provided invaluable input during the development of this
480             module.
481              
482             =head1 SEE ALSO
483              
484             For a detailed usage example, please see examples/ip-info.pl included in
485             this module's distribution.
486              
487             =head1 AUTHORS
488              
489             =over 4
490              
491             =item *
492              
493             mikegrb
494              
495             =item *
496              
497             Wes Young
498              
499             =back
500              
501             =head1 COPYRIGHT AND LICENSE
502              
503             This software is copyright (c) 2013 by Mike Greb.
504              
505             This is free software; you can redistribute it and/or modify it under
506             the same terms as the Perl 5 programming language system itself.
507              
508             =head1 AUTHORS
509              
510             =over 4
511              
512             =item *
513              
514             mikegrb
515              
516             =item *
517              
518             Wes Young
519              
520             =back
521              
522             =head1 COPYRIGHT AND LICENSE
523              
524             This software is copyright (c) 2013-2018 by Michael Greb
525              
526             This is free software; you can redistribute it and/or modify it under
527             the same terms as the Perl 5 programming language system itself.
528              
529             =head1 AUTHORS
530              
531             =over 4
532              
533             =item *
534              
535             mikegrb
536              
537             =item *
538              
539             Wes Young
540              
541             =back
542              
543             =head1 COPYRIGHT AND LICENSE
544              
545             This software is copyright (c) 2013 by =over 4.
546              
547             This is free software; you can redistribute it and/or modify it under
548             the same terms as the Perl 5 programming language system itself.
549              
550             =cut
551              
552             __DATA__