#!/usr/bin/perl
# $Id: scavenge,v 1.3 2005/04/19 18:57:36 mdf Exp $
# dns_scavenge - find missing, mismatched & stale forward/reverse DNS record.
# (Optional) Use nmap scan a given range.
# Parse the output inline or later, classifying the following:
# 1. Missing forward addresses (A records) and reverse (PTR) records for hosts that are UP
# 2. Stale forward addresses (A records) and reverse (PTR) records for hosts that are DOWN
use Getopt::Std;
use Net::DNS;
use strict;


# Parse command line arguments
use vars qw/ $opt_d $opt_h $opt_r /;
my $ok = getopts('dhr:');
if ( (!$ok) || $opt_h ) {
	print <<EOF;
Usage: scavenge [-r ip-range] [-d]
  -r ip-range e.g. 192.168.0.1-255
                or 192.168.0.0/16 
		or 192.168.0.0/24
	        or '192.88-90.*.*'
  -d turn on debugging
  -h this help message

  Note that this command reads from STDIN unless -r is used. The input format
  should be the greppable-format produced by nmap -oG 
EOF
 exit(1);
}

my $debug = $opt_d; #If -d was passed, set the debug flag
my $range = $opt_r || undef; 

# Setup handle to DNS resolver
my $res = Net::DNS::Resolver->new;

if ($range) {
	print STDOUT "DEBUG: nmap -sP -R $range -oG -\n" if $debug;
	open(NMAPOUT, "nmap -sP -R $range -oG - |") or die "Cannot open NMAP output for read: $!";
	# Associate NMAPOUT with STDIN
	open(MYFH, "<&=NMAPOUT") or die "Couldn't alias NMAPOUT : $!";
} else {
	print STDOUT "DEBUG: input range empty, reading from STDIN\n" if $debug;
	open(MYFH,  "<&=STDIN")  or die "Couldn't alias STDIN : $!";
}

while (<MYFH>) {
	next if /^#/;
	my ($hostlabel, $ip, $hostname, $statuslabel, $updown) = split(/\s/, $_);
	# Identification of records with problems are assigned a "case" which is correspondent
	# to a category such as Stale PTR or Missing A
	my $case = ""; # transitory case indicator
	my @cases = (); # collector for case matches
	# Scrub some data
	$hostname =~ s/[\(\)]//g;

	# if down and there is no $hostname, move along, nothing to see
	# Box A
	next if ( ($updown eq 'Down') && ($hostname eq '') );

	# Determine A records from discovered hostname
	my $r_ip = "";
	if ($hostname  && ($hostname ne '') ) {
		my @ips = &h2i($hostname);
		$r_ip  = join(",", @ips) ;
		print ("DEBUG: r_ip=", $r_ip, "\n") if $debug;
	} 
	
	if ($hostname ne '') {
		# Case Stale PTR - host is down but ip resolves
		# Box B
		if ($updown eq 'Down') {
			my $buf .= "Stale PTR"; 
			# Since down, check for  Stale A also
			if ($r_ip ne 'NXDOMAIN') {
			 	$buf .= "+A";
			}
			push @cases, $buf; undef $buf;
		} else { # Host is up 
			# Box D
			# Case Missing A
			if ($r_ip eq 'NXDOMAIN') {
				push @cases, "Missing A";
			}
			# Case Mismatch A
			elsif ($ip ne $r_ip) { 
				push @cases,  "Mismatch A";
			}	
		}
	}
	else { # $hostname blank but Host is UP
		# Case Missing PTR
		# Box C
		push @cases, "Missing PTR";
	}
	# If case has not been identified, assume it's Good
	if (@cases > 0) {
		$case = join(",", @cases);
		print "$updown\t$case\t$ip => ($hostname) => $r_ip\n";
	}
}


# Given an IP address (dotted-quad) return an array of hostnames (PTR records)
sub h2i {
	my $ip = shift;
	my @result = ();
	my $query = $res->search($ip);
	if ($query) {
		foreach my $rr ($query->answer) {
			next unless $rr->type eq "A";
			print ("DEBUG: ", $rr->address, "\n") if $debug;
			push(@result, $rr->address);
		}
	} else {
		if ($debug) {
			warn "query failed: ", $res->errorstring, "\n";
		}
		if ($res->errorstring =~ /NXDOMAIN/) {
			push(@result, 'NXDOMAIN');
		} else {
			push(@result, $res->errorstring);
		}
	} 
	return @result;
}

# Given hostname, return a list of (dotted-quad) IP addresses (A records)
sub i2h {
	my $hname = shift;
	my @result = ();
	my $query = $res->search($hname);
	if ($query) {
		foreach my $rr ($query->answer) {
			next unless $rr->type eq "PTR";
			print ("DEBUG: ", $rr->address, "\n") if $debug;
			push(@result, $rr->address);
		}
	} else {
		if ($debug) {
			warn "query failed: ", $res->errorstring, "\n";
		}
		if ($res->errorstring =~ /NXDOMAIN/) {
			push(@result, 'NXDOMAIN');
		} else {
			push(@result, $res->errorstring);
		}
	}
	return @result;
}



# Parseable lines
# Host: 172.16.100.245 (dnsslave.portseattle.org) Status: Down
# This is a stale PTR

# Host: 172.16.100.246 () Status: Down
# This is fine (ignorable)

# Host: 172.16.100.233 (p69msitc147.portseattle.org)      Status: Up
# This is fine unless there is no such A record
# Or the A record p69msitc147.portseattle.org resolves to a different IP

# Host: 172.16.100.201 () Status: Up
# This is a missing PTR record
