File Coverage

blib/lib/Net/Abuse/Utils.pm
Criterion Covered Total %
statement 23 164 14.0
branch 0 74 0.0
condition 0 12 0.0
subroutine 8 25 32.0
pod 14 14 100.0
total 45 289 15.5


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   19314 use 5.006;
  1         5  
  1         35  
5 1     1   6 use strict;
  1         2  
  1         28  
6 1     1   5 use warnings;
  1         6  
  1         25  
7              
8 1     1   839 use Net::DNS;
  1         103355  
  1         114  
9 1     1   1343 use Net::Whois::IP 1.11 'whoisip_query';
  1         6136  
  1         63  
10 1     1   1045 use Email::Address;
  1         36614  
  1         173  
11 1     1   1260 use Net::IP;
  1         63330  
  1         2721  
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.23';
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_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 @origin_as = _return_rr($lookup, 'TXT', 2) or return;
139            
140             # un-comment for testing
141             #push(@origin_as,'20738 | 212.67.192.0/24 | GB | ripencc | 1999-05-12');
142              
143             # 23028 | 216.90.108.0/24 | US | arin | 1998-09-25
144             # 701 1239 3549 3561 7132 | 216.90.108.0/24 | US | arin | 1998-09-25
145            
146 0         0 my $smallest_netmask = 0;
147 0         0 my ($smallest_prefix, %data_for_asn);
148              
149             # surely there is a better way to do this, at least the split
150             # fields are stored so they don't have to be split again ;)
151 0         0 for my $asn_info (@origin_as) {
152 0         0 my @fields = split /\|/, $asn_info;
153 0         0 my @network = split '/', $fields[1];
154              
155             # if multiple ASNs announce the same, block they are given space
156             # seperated in the first field, we just use the first
157 0 0       0 if ($fields[0] =~ /(\d+) \d+/) {
158 0         0 $fields[0] = $1;
159             }
160              
161 0         0 my $asn = $fields[0];
162            
163 0         0 $data_for_asn{$fields[1]} = [@fields];
164            
165 0 0       0 if ($network[1] > $smallest_netmask) {
166 0         0 $smallest_netmask = $network[1];
167 0         0 $smallest_prefix = $fields[1];
168             }
169             }
170              
171 0         0 return map { _strip_whitespace($_) } @{ $data_for_asn{$smallest_prefix} };
  0         0  
  0         0  
172             }
173              
174             sub get_peer_info {
175 0     0 1 0 my $ip = shift;
176              
177             # IPv4 only until Cymru has an IPv6 peer database
178 0         0 my $ver = Net::IP::ip_get_version($ip);
179 0 0 0     0 return unless $ver && $ver == 4;
180              
181 0         0 my $lookup = _reverse_ip($ip) . '.peer.asn.cymru.com';
182 0 0       0 my @origin_as = _return_rr($lookup, 'TXT', 2) or return;
183              
184 0         0 my $return = [];
185 0         0 foreach my $as (@origin_as){
186 0         0 my @peers = split(/\s\|\s?/,$as);
187 0         0 my %hash = (
188             prefix => $peers[1],
189             cc => $peers[2],
190             rir => $peers[3],
191             date => $peers[4],
192             );
193 0         0 my @asns = split(/\s/,$peers[0]);
194 0         0 foreach (@asns){
195 0         0 $hash{'asn'} = $_;
196 0         0 push(@$return,{
197             prefix => $peers[1],
198             cc => $peers[2],
199             rir => $peers[3],
200             date => $peers[4],
201             asn => $_,
202             });
203             }
204             }
205 0 0       0 return(@$return) if wantarray;
206 0         0 return($return);
207             }
208              
209             # test with 733a48a9cb49651d72fe824ca91e8d00
210             # http://www.team-cymru.org/Services/MHR/
211              
212             sub get_malware {
213 0     0 1 0 my $hash = shift;
214 0 0 0     0 return unless($hash && lc($hash) =~ /^[a-z0-9]{32}$/);
215            
216 0         0 my $lookup = $hash.'.malware.hash.cymru.com';
217              
218 0 0       0 my $res = _return_rr($lookup, 'TXT') or return;
219 0         0 my ($last_seen,$detection_rate) = split(/ /,$res);
220             return({
221 0         0 last_seen => $last_seen,
222             detection_rate => $detection_rate,
223             });
224             }
225              
226             sub get_as_description {
227 0     0 1 0 my $asn = shift;
228 0         0 my @ASdata;
229              
230 0 0       0 if ( my $data = _return_rr( "AS${asn}.asn.cymru.com", 'TXT' ) ) {
231 0         0 @ASdata = split( '\|', $data );
232             }
233             else {
234 0         0 return;
235             }
236              
237 0 0       0 return unless $ASdata[4];
238 0         0 my $org = _strip_whitespace( $ASdata[4] );
239              
240             # for arin we get "HANDLE - AS Org"
241             # we want to make it "HANDLE AS Org" to match other RIRs
242 0 0       0 $org =~ s/^(\S+) - (.*)$/$1 $2/ if ( $ASdata[2] eq ' arin ' );
243              
244 0         0 return $org;
245             }
246              
247             sub get_as_company {
248 0     0 1 0 my $asn = shift;
249              
250 0         0 my $desc = get_as_description($asn);
251 0 0       0 return unless defined($desc);
252              
253             # remove leading org id/handle/etc
254 0         0 $desc =~ s/^[-_A-Z0-9]+ //;
255              
256             # remove trailing 'AS'
257 0         0 $desc =~ s/AS(:? Number)?$//;
258              
259             # remove trailing 'Autonomous System'
260 0         0 $desc =~ s/Autonomous System(:? Number)?$//i;
261              
262 0         0 return $desc;
263             }
264              
265             sub get_soa_contact {
266 0     0 1 0 my $ip = shift;
267              
268 0         0 my $lookup = _reverse_ip($ip) . '.in-addr.arpa';
269 0         0 $lookup =~ s/^\d+\.//;
270              
271 0 0       0 if ( my $soa_contact = _return_rr($lookup, 'SOA') ) {
272 0 0       0 $soa_contact =~ s/\./@/ unless $soa_contact =~ m/@/;
273 0         0 return $soa_contact;
274             }
275            
276 0         0 return;
277             }
278              
279             sub get_rdns {
280 0     0 1 0 my $ip = shift;
281 0         0 my $ver = Net::IP::ip_get_version($ip);
282 0 0       0 return unless $ver;
283              
284 0 0       0 my $suffix = ($ver == 4) ? '.in-addr.arpa' : '.ip6.arpa';
285 0         0 return _return_rr( _reverse_ip($ip) . $suffix, 'PTR');
286             }
287              
288             sub get_dnsbl_listing {
289 0     0 1 0 my ($ip, $dnsbl) = @_;
290              
291             # IPv4 Only
292 0         0 my $ver = Net::IP::ip_get_version($ip);
293 0 0 0     0 return unless $ver && $ver == 4;
294              
295 0         0 my $lookup = join '.', _reverse_ip( $ip ), $dnsbl;
296              
297 0         0 return _return_rr($lookup, 'TXT', 1);
298             }
299              
300             sub get_ip_country {
301 0     0 1 0 my $ip = shift;
302 0         0 return (get_asn_info($ip))[2];
303             }
304              
305             sub get_asn_country {
306 0     0 1 0 my $asn = shift;
307 0 0       0 return unless $asn =~ /^\d+$/;
308              
309 0         0 my $as_cc = (split (/\|/,_return_rr("AS${asn}.asn.cymru.com", 'TXT')))[1];
310 0 0       0 if ($as_cc) {
311 0         0 return _strip_whitespace($as_cc);
312             }
313 0         0 return;
314             }
315              
316             sub get_abusenet_contact {
317 0     0 1 0 my $domain = shift;
318 0         0 return _return_rr("$domain.contacts.abuse.net", 'TXT', 1)
319             }
320              
321             sub is_ip {
322 5     5 1 7081 my $ip = shift;
323 5         20 return defined Net::IP::ip_get_version($ip);
324             }
325              
326             sub get_domain {
327 0     0 1   my $hostname = shift;
328              
329 0 0         @tlds = grep {!/^#/} unless scalar @tlds;
  0            
330 0           my @parts = reverse (split /\./, $hostname);
331              
332 0 0         if (scalar @parts == 2) {
333             # just two parts, lets return it
334 0           return join '.', @parts[1, 0];
335             }
336 0 0         if (grep /^\Q$parts[1].$parts[0]\E$/, @tlds) {
337             # last two parts found in tlds
338 0           return join '.', @parts[2, 1, 0];
339             } else {
340             # last two not found so *host.domain.name
341 0           return join '.', @parts[1, 0];
342             }
343             }
344              
345             1;
346              
347             =pod
348              
349             =head1 NAME
350              
351             Net::Abuse::Utils - Routines useful for processing network abuse
352              
353             =head1 VERSION
354              
355             version 0.23
356              
357             =head1 SYNOPSIS
358              
359             use Net::Abuse::Utils qw( :all );
360             print "IP Whois Contacts: ", join( ' ', get_ipwi_contacts($ip) ), "\n";
361             print "Abuse.net Contacts: ", get_abusenet_contact($domain), "\n";
362              
363             =head1 DESCRIPTION
364              
365             Net::Abuse::Utils provides serveral functions useful for determining
366             information about an IP address including contact/reporting addresses,
367             ASN/network info, reverse dns, and DNSBL listing status. Functions which take
368             an IP accept either IPv6 or IPv4 IPs unless indicated otherwise.
369              
370             =head1 CONFIGURATION
371              
372             There is a C<@RESOLVERS> package variable you can use to specify name servers
373             different than the systems nameservers for queries from this module. If you
374             intend to use Google's nameservers here, please see L
375             a note of caution|https://github.com/mikegrb/Net-Abuse-Utils/issues/9#issuecomment-24387435>.
376              
377             =head1 FUNCTIONS
378              
379             The following functions are exportable from this module. You may import all
380             of them into your namespace with the C<:all> tag.
381              
382             =head2 get_asn_info ( IP )
383              
384             Returns a list containing (ASN, Network/Mask, CC code, RIR, modified date)
385             for the network announcing C.
386              
387             =head2 get_peer_info ( IP )
388              
389             IPv4 Only. Returns an array of hash references containing (ASN, Network/Mask,
390             CC code, RIR, modified date) for the peers of the network announcing C.
391              
392             =head2 get_as_description ( ASN )
393              
394             Returns the AS description for C.
395              
396             =head2 get_as_company ( ASN )
397              
398             Similiar to C but attempts to clean it up some before
399             returning it.
400              
401             =head2 get_soa_contact( IP )
402              
403             Returns the SOA contact email address for the reverse DNS /24
404             zone containing C.
405              
406             =head2 get_ipwi_contacts( IP )
407              
408             Returns a list of all email addresses found in whois information
409             for C with duplicates removed.
410              
411             =head2 get_rdns( IP )
412              
413             Returns the reverse PTR for C.
414              
415             =head2 get_dnsbl_listing( IP, DNSBL zone )
416              
417             IPv4 Only. Returns the listing text for C for the designated DNSBL.
418             C should be the zone used for looking up addresses in the
419             blocking list.
420              
421             =head2 get_ip_country( IP )
422              
423             Returns the 2 letter country code for C.
424              
425             =head2 get_asn_country( ASN )
426              
427             Returns the 2 letter country code for C.
428              
429             =head2 get_abusenet_contact ( domain )
430              
431             Returns the abuse.net listed contact email addresses for C.
432              
433             =head2 is_ip ( IP )
434              
435             Returns true if C looks like an IP, false otherwise.
436              
437             =head2 get_domain ( IP )
438              
439             Takes a hostname and attempts to return the domain name.
440              
441             =head2 get_malware ( md5 )
442              
443             Takes a malware md5 hash and tests it against
444             http://www.team-cymru.org/Services/MHR. Returns a HASHREF of last_seen and
445             detection_rate.
446              
447             =head1 DIAGNOSTICS
448              
449             Each subroutine will return undef if unsuccessful. In the furture,
450             debugging output will be available.
451              
452             =head1 CONFIGURATION AND ENVIRONMENT
453              
454             There are two commented out lines that can be uncommented to enable Memoize
455             support. I haven't yet decided whether to include this option by default. It
456             may be made available in the future via an import flag to use.
457              
458             =head1 DEPENDENCIES
459              
460             This module makes use of the following modules:
461              
462             L, L, L, and L
463              
464             =head1 BUGS AND LIMITATIONS
465              
466             There are no known bugs in this module. Please report problems to
467             Michael Greb (mgreb@linode.com)
468              
469             Patches are welcome.
470              
471             =head1 ACKNOWLEDGEMENTS
472              
473             This module was inspired by Karsten M. Self's SpamTools shell scripts,
474             available at http://linuxmafia.com/~karsten/.
475              
476             Thanks as well to my employer, Linode.com, for allowing me the time to work
477             on this module.
478              
479             Rik Rose, Jon Honeycutt, Brandon Hale, TJ Fontaine, A. Pagaltzis, and
480             Heidi Greb all provided invaluable input during the development of this
481             module.
482              
483             =head1 SEE ALSO
484              
485             For a detailed usage example, please see examples/ip-info.pl included in
486             this module's distribution.
487              
488             =head1 AUTHORS
489              
490             =over 4
491              
492             =item *
493              
494             mikegrb
495              
496             =item *
497              
498             Wes Young
499              
500             =back
501              
502             =head1 COPYRIGHT AND LICENSE
503              
504             This software is copyright (c) 2013 by Mike Greb.
505              
506             This is free software; you can redistribute it and/or modify it under
507             the same terms as the Perl 5 programming language system itself.
508              
509             =cut
510              
511             __DATA__