File Coverage

blib/lib/Net/Abuse/Utils.pm
Criterion Covered Total %
statement 22 161 13.6
branch 0 74 0.0
condition 0 15 0.0
subroutine 8 26 30.7
pod 15 15 100.0
total 45 291 15.4


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   12743 use 5.006;
  1         3  
5 1     1   3 use strict;
  1         1  
  1         14  
6 1     1   3 use warnings;
  1         3  
  1         27  
7              
8 1     1   477 use Net::DNS;
  1         60798  
  1         78  
9 1     1   388 use Net::Whois::IP 1.11 'whoisip_query';
  1         2503  
  1         48  
10 1     1   407 use Email::Address;
  1         18647  
  1         47  
11 1     1   596 use Net::IP;
  1         29173  
  1         1599  
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.25';
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 foreach my $field (keys %$response) {
121 0         0 push @addresses, Email::Address->parse($response->{$field});
122             }
123              
124 0         0 @addresses = map { $_->address } @addresses;
  0         0  
125              
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 827 my $ip = shift;
315 5         12 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.25
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 NAME
365              
366             Net::Abuse::Utils - Routines useful for processing network abuse
367              
368             =head1 VERSION
369              
370             version 0.24
371              
372             =head1 CONFIGURATION
373              
374             There is a C<@RESOLVERS> package variable you can use to specify name servers
375             different than the systems nameservers for queries from this module. If you
376             intend to use Google's nameservers here, please see L
377             a note of caution|https://github.com/mikegrb/Net-Abuse-Utils/issues/9#issuecomment-24387435>.
378              
379             =head1 FUNCTIONS
380              
381             The following functions are exportable from this module. You may import all
382             of them into your namespace with the C<:all> tag.
383              
384             =head2 get_asn_info ( IP )
385              
386             Returns a list containing (ASN, Network/Mask, CC code, RIR, modified date)
387             for the network announcing C.
388              
389             =head2 get_all_asn_info ( IP )
390              
391             Returns a reference to a list of listrefs containting ASN(s), Network,Mask,
392             CC code, RIR, and modified date fall all networks announcing C.
393              
394             =head2 get_peer_info ( IP )
395              
396             IPv4 Only. Returns an array of hash references containing (ASN, Network/Mask,
397             CC code, RIR, modified date) for the peers of the network announcing C.
398              
399             =head2 get_as_description ( ASN )
400              
401             Returns the AS description for C.
402              
403             =head2 get_as_company ( ASN )
404              
405             Similiar to C but attempts to clean it up some before
406             returning it.
407              
408             =head2 get_soa_contact( IP )
409              
410             Returns the SOA contact email address for the reverse DNS /24
411             zone containing C.
412              
413             =head2 get_ipwi_contacts( IP )
414              
415             Returns a list of all email addresses found in whois information
416             for C with duplicates removed.
417              
418             =head2 get_rdns( IP )
419              
420             Returns the reverse PTR for C.
421              
422             =head2 get_dnsbl_listing( IP, DNSBL zone )
423              
424             IPv4 Only. Returns the listing text for C for the designated DNSBL.
425             C should be the zone used for looking up addresses in the
426             blocking list.
427              
428             =head2 get_ip_country( IP )
429              
430             Returns the 2 letter country code for C.
431              
432             =head2 get_asn_country( ASN )
433              
434             Returns the 2 letter country code for C.
435              
436             =head2 get_abusenet_contact ( domain )
437              
438             Returns the abuse.net listed contact email addresses for C.
439              
440             =head2 is_ip ( IP )
441              
442             Returns true if C looks like an IP, false otherwise.
443              
444             =head2 get_domain ( IP )
445              
446             Takes a hostname and attempts to return the domain name.
447              
448             =head2 get_malware ( md5 )
449              
450             Takes a malware md5 hash and tests it against
451             http://www.team-cymru.org/Services/MHR. Returns a HASHREF of last_seen and
452             detection_rate.
453              
454             =head1 DIAGNOSTICS
455              
456             Each subroutine will return undef if unsuccessful. In the furture,
457             debugging output will be available.
458              
459             =head1 CONFIGURATION AND ENVIRONMENT
460              
461             There are two commented out lines that can be uncommented to enable Memoize
462             support. I haven't yet decided whether to include this option by default. It
463             may be made available in the future via an import flag to use.
464              
465             =head1 DEPENDENCIES
466              
467             This module makes use of the following modules:
468              
469             L, L, L, and L
470              
471             =head1 BUGS AND LIMITATIONS
472              
473             There are no known bugs in this module. Please report problems to
474             Michael Greb (mgreb@linode.com)
475              
476             Patches are welcome.
477              
478             =head1 ACKNOWLEDGEMENTS
479              
480             This module was inspired by Karsten M. Self's SpamTools shell scripts,
481             available at http://linuxmafia.com/~karsten/.
482              
483             Thanks as well to my employer, Linode.com, for allowing me the time to work
484             on this module.
485              
486             Rik Rose, Jon Honeycutt, Brandon Hale, TJ Fontaine, A. Pagaltzis, and
487             Heidi Greb all provided invaluable input during the development of this
488             module.
489              
490             =head1 SEE ALSO
491              
492             For a detailed usage example, please see examples/ip-info.pl included in
493             this module's distribution.
494              
495             =head1 AUTHORS
496              
497             =over 4
498              
499             =item *
500              
501             mikegrb
502              
503             =item *
504              
505             Wes Young
506              
507             =back
508              
509             =head1 COPYRIGHT AND LICENSE
510              
511             This software is copyright (c) 2013 by Mike Greb.
512              
513             This is free software; you can redistribute it and/or modify it under
514             the same terms as the Perl 5 programming language system itself.
515              
516             =head1 AUTHORS
517              
518             =over 4
519              
520             =item *
521              
522             mikegrb
523              
524             =item *
525              
526             Wes Young
527              
528             =back
529              
530             =head1 COPYRIGHT AND LICENSE
531              
532             This software is copyright (c) 2013 by =over 4.
533              
534             This is free software; you can redistribute it and/or modify it under
535             the same terms as the Perl 5 programming language system itself.
536              
537             =head1 AUTHORS
538              
539             =over 4
540              
541             =item *
542              
543             mikegrb
544              
545             =item *
546              
547             Wes Young
548              
549             =back
550              
551             =head1 COPYRIGHT AND LICENSE
552              
553             This software is copyright (c) 2013 by =over 4.
554              
555             This is free software; you can redistribute it and/or modify it under
556             the same terms as the Perl 5 programming language system itself.
557              
558             =cut
559              
560             __DATA__