#!/usr/bin/perl
# Requires from CPAN:
#	Getopt::Long
#	Net::Ping

use utf8;
use strict;

binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";
$|=1;

use constant VERSION => '0.9.2';

my $cache = {};
my $pars = parseargs();
die "\n" . $pars->{error} if $pars->{error};

if ($pars->{options}{logfile}) {
	report("\n\n") if (-s  $pars->{options}{logfile});
	report(('#'x64)."\n# bindscan ".&VERSION." started on ".`date`.'#'.('-'x63)."\n") ;
	report("# Current directory: ".`pwd`);
	report("# Log file: “" . $pars->{options}{logfile} . "”\n");
	report("# [" . ($pars->{options}{noping} ? ' ' : 'X') . "] Ping test (timeout: ".$pars->{options}{timeout}."s)\n");
	report("# [" . ($pars->{options}{nosyntax} ? ' ' : 'X') . "] Syntax test\n");
	report("#     [" . ($pars->{options}{nosyntaxping} ? ' ' : 'X') . "] Syntax ping\n");
	report("#     [" . ($pars->{options}{noptrcheck} ? ' ' : 'X') . "] PTR check\n");
	report("#         [" . ($pars->{options}{noptrcheckzone} ? ' ' : 'X') . "] Report missing PTR zones\n");
	report('#'.('-'x63)."\n");
	report("# Zone: “".$pars->{zone}{zone}."”, file: “".$pars->{zone}{path}."”\n");
	if ( @{ $pars->{ptr} }) {
		foreach my $p (@{ $pars->{ptr} }) {
			report("# PTR: “".$p->{zone}."”, file: “".$p->{path}."”\n");
		}
	}
	report(('#'x64)."\n");
}

my $values = { A => {}, AAAA => {}, CNAME => {}, NS => {}, PTR => {} };
parsezonefile( $pars->{zone}, $values );
if ( $pars->{ptr} ) {
	my $max = scalar( @{ $pars->{ptr} } ) -1;
	foreach my $i (0 .. $max) {
		parsezonefile( $pars->{ptr}[$i], $values );
	}
}

# use Data::Dumper;
# print Dumper $pars;
# print Dumper $values;
# exit 0;

my $report = { A => {}, AAAA => {}, CNAME => {}, NS => {}, PTR => {} };
unless ( $pars->{options}{noping} ) {
	parsealives('NS',1);
	parsealives('A');
	parsealives('AAAA');
}
unless ( $pars->{options}{nosyntax} ) {
	parsesynA('A');
	parsesynA('AAAA');
	parsesynCNAME('CNAME');
	parsesynCNAME('PTR') unless $pars->{options}{noptrcheck};
}

exit 0;

##### subs #####

sub normalize {
	my $in = shift;
	$in =~ s/[\r\n\t ]+/ /g;
	$in =~ s/^ //;
	$in =~ s/ $//;
	return $in;
}

sub getData {
	my $section = shift || 'global';
	my $asArray = shift || 0;
	unless ( defined $cache->{dataSections} ) {
		$cache->{dataSections} = {};
		my $curs = 'global';
		while (my $line = <DATA>) {
			chomp $line;
			if ( $line =~ m/^__DATA_SECTION::(.+)__$/ ) {
				$curs = "$1";
				next;
			}
			last if $line eq '__END__';
			$cache->{dataSections}{$curs} .= "$line\n";
		}
	}
	my $data = '' . $cache->{dataSections}{$section};
	return split(/\n/,$data) if $asArray;
	return $data;
}

sub parseargs {
	my $out = { options => { timeout => 2 } };
	{
		use Getopt::Long qw(GetOptions);
		Getopt::Long::Configure qw(gnu_getopt);
		GetOptions(
			'help|h'	=> \$out->{options}{help},
			'log|l=s'	=> \$out->{options}{logfile},
			'noping|p'	=> \$out->{options}{noping},
			'quiet|q'	=> \$out->{options}{quiet},
			'nosyn|s'	=> \$out->{options}{nosyntax},
			'nosynping|P'	=> \$out->{options}{nosyntaxping},
			'noptr|r'	=> \$out->{options}{noptrcheck},
			'noptrzone|R'	=> \$out->{options}{noptrcheckzone},
			'timeout|t=i'	=> \$out->{options}{timeout},
		);
	}
	my $in = [];
	foreach my $p (@ARGV) {
		if ( $p =~ s/^\@:?// ) {
			return { error => "ERROR: file not found: $p\n" } unless -s $p;
			open my $fhandler, '<', $p;
			while (my $row = <$fhandler>) {
				chomp($row);
				$row =~ s/[;#].*//;
				$row = normalize($row);
				my @rp = split(/ /,$row);
				if ( scalar(@rp > 1)) {
					push @$in,shift(@rp);
					push @$in,join(' ',@rp);
				}
			}
			close $p;
		} else {
			push @$in,$p;
		}
	}
	my $zone = lc(normalize(shift(@$in)));
	$zone =~ s/^\.//;
	$zone =~ s/\.$//;
	if ( $out->{options}{help} or ! $zone ) {
		return { error => getData( $out->{options}{help} ? 'fullHelp' : 'basicHelp' ) };
	}
	unless ( $zone =~ m/[^.]+\.[^.]+$/ ) {
		return { error => "ERROR: Wrong zone name.\n\n" . getData('basicHelp') };
	}
	my $path = normalize(shift(@$in));
	unless ( $path ) {
		return { error => "ERROR: Missing zone-file.\n\n" . getData('basicHelp') };
	}
	unless ( -r $path ) {
		return { error => "ERROR: zone-file not readable: “$path”.\n\n" . getData('basicHelp') };
	}
	$out->{zone} = { zone => "$zone", path => "$path", type => "A" };
	if ( @$in ) {
		my @PTR;
		while ( @$in ) {
			$zone = normalize(shift(@$in));
			$zone =~ s/(\.0*)+$//;
			unless ( $zone =~ m/^([0-9]+\.)+/ ) {
				return { error => "ERROR: Wrong ptr-name ".(scalar(@PTR)+1).".\n\n" . getData('basicHelp') };
			}
			$path = @$in ? normalize(shift(@$in)) : 'Missing path';
			unless ( -r $path ) {
				return { error => "ERROR: ptr-file ".(scalar(@PTR)+1)." not readable: “$path”.\n\n" . getData('basicHelp') };
			}
			push @PTR,{ zone => "$zone\.", path => "$path", type => "PTR" };
		}
		$out->{ptr} = \@PTR;
	}
	unless ( $out->{options}{noptrcheck} ) {
		$out->{options}{noptrcheck} = 'auto' unless @{ $out->{ptr} };
	}
	if ( $out->{options}{nosyntax} ) {
		$out->{options}{nosyntaxping} = 'auto';
		$out->{options}{noptrcheck} = 'auto';
	}
	if ( $out->{options}{noptrcheck} ) {
		$out->{options}{noptrcheckzone} = 'auto';
	}
	return $out;
}

sub parsezonefile {
	my $entry = shift;
	my $out = shift || { A => {}, AAAA => {}, CNAME => {}, NS => {}, PTR => {} };
	my $zone = shift || $pars->{zone}{zone};
	my $ptrbase = $entry->{type} eq 'PTR' ? $entry->{zone} : '';
	my $path = $entry->{path};
	my $line = 0;
	my $col1;
	open my $fhandler, '<', $path;
	while (my $row = <$fhandler>) {
		$line++;
		chomp $row;
		next unless $row;
		next if $row =~ m/^[ \t]*[#;]/;
		$row =~ s/[ \t]*[#;].*//;
		if ( $col1 and $row =~ /^[ \t]/ ) {
			$row = $col1 . $row;
		}
		my @COLS = split(/ /,normalize(lc($row)));
		next unless uc($COLS[2]) eq 'IN';
		my $type = uc($COLS[3]);
		next unless defined $out->{$type};
		if ( $type eq 'PTR' and $COLS[0] =~ m/^[0-9.]+$/ ) {
			next unless $ptrbase;
			$COLS[0] =~ s/^$ptrbase//;
			$COLS[0] = $ptrbase . $COLS[0];
			$COLS[4] =~ s/\.$zone\.$//i;
			$col1 = lc($COLS[4]);
			$out->{PTRREV} = {} unless defined $out->{PTRREV};
			$out->{PTRREV}{$col1} = [] unless defined $out->{PTRREV}{$col1};
			push @{ $out->{PTRREV}{$col1} }, {
				value => $COLS[0],
				file => $entry->{path},
				line => $line
			};
		} elsif ( $type eq 'CNAME' ) {
			$COLS[0] =~ s/\.$zone\.$//i;
			$COLS[4] =~ s/\.$zone\.$//i;
			$col1 = lc($COLS[4]);
			$out->{CNAMEREV} = {} unless defined $out->{CNAMEREV};
			$out->{CNAMEREV}{$col1} = [] unless defined $out->{CNAMEREV}{$col1};
			push @{ $out->{CNAMEREV}{$col1} }, {
				value => $COLS[0],
				file => $entry->{path},
				line => $line
			};
		} elsif ( $type eq 'NS' ) {
			$COLS[0] =~ s/\.$zone\.$//i;
			if ( $COLS[4] =~ m/\.$/ ) {
				$COLS[4] =~ s/\.$//;
			} else {
				$COLS[4] =~ s/\.$zone\.$//i;
				$COLS[4] .= '.'.$zone;
			}
		} elsif ( $type eq 'A' or $type eq 'AAAA' ) {
			$COLS[0] =~ s/\.$zone\.$//i;
			$col1 = lc($COLS[4]);
			$out->{$type.'REV'} = {} unless defined $out->{$type.'REV'};
			$out->{$type.'REV'}{$col1} = [] unless defined $out->{$type.'REV'}{$col1};
			push @{ $out->{$type.'REV'}{$col1} }, {
				value => $COLS[0],
				file => $entry->{path},
				line => $line
			};
		} else {
			$COLS[0] =~ s/\.$zone\.$//i;
		}
		$col1 = lc($COLS[0]);
		$out->{$type}{$col1} = [] unless defined $out->{$type}{$col1};
		push @{ $out->{$type}{$col1} }, {
			value => $COLS[4],
			file => $entry->{path},
			line => $line
		};
	}
	close $fhandler;
	return { path => $path, type => $entry->{type}, lines => $line, out => $out };
}

sub pingInit {
	use Net::Ping;
	return '' if defined $cache->{pinger};
	$pars->{options}{timeout} = int($pars->{options}{timeout}) || 2;
	$cache->{pinged} = {};
	$cache->{pinger} = {};
	$cache->{pingerproto} = [];
	my @lines = getData('ping',1);
	foreach my $line (@lines) {
		chomp $line;
		$line =~ s/[#;].*//;
		$line = normalize($line);
		next unless $line;
		my ( $pname, $pprot, $pport ) = split(/ /,$line);
		push @{ $cache->{pingerproto} },$pname;
		$cache->{pinger}{$pname} = Net::Ping->new($pprot,$pars->{options}{timeout});
		$cache->{pinger}{$pname}->port_number(int($pport)) if $pport;
	}
	return 1;
}

sub ping {
	my $host = lc(shift) or return 0;
	pingInit();
	$host =~ s/^\*\./foobar./;
	return $cache->{pinged}{$host} if defined $cache->{pinged}{$host};
	my $success = 0;
	foreach my $pt (@{ $cache->{pingerproto} }) {
		if ( $cache->{pinger}{$pt}->ping($host) ) {
			$success = $pt;
			last;
		}
	}
	return $cache->{pinged}{$host} = $success;
}

sub progress {
	return if $pars->{options}{quiet};
	print STDERR shift;
}

sub report {
	my $in = shift;
	my $fileonly = shift;
	if ( $pars->{options}{logfile} ) {
		open my $fhandler, '>>:utf8', $pars->{options}{logfile};
		print $fhandler $in;
		close $fhandler;
	} elsif ( $fileonly ) {
		# do nothing
	} else {
		print STDOUT $in;
	}
}

sub parsealives {
	my $type = shift || 'A';
	my $skip = shift;
	$skip = 'auto' if $skip;
	progress("\n\n*** Ping test for records: $type ***\n\n");
	report("\n\n*** Ping test for records: $type ***\n\n",1);
	my @keys = sort keys %{ $values->{$type} };
	my $tot = 0;
	my $errors = 0;
	foreach my $k (@keys) {
		foreach my $record (@{ $values->{$type}{$k} }) {
			$tot++;
		}
	}
	my $count = 0;
	foreach my $k (@keys) {
		my $iname = $k . '.' . $pars->{zone}{zone};
		foreach my $record (@{ $values->{$type}{$k} }) {
			$count++;
			my $ip = $record->{value};
			progress("$count/$tot $iname =>");
			my $aliven = $skip || ping( $iname );
			progress(' ' . ( $aliven ? "OK ($aliven)" : '### FAILURE ###' ));
			my $aliveip = 0;
			if ( $aliven ) {
				progress(", $type: $ip =>");
				$aliveip = ping( $ip );
				progress(' ' . ( $aliveip ? "OK ($aliveip)" : '### FAILURE ###' ));
			} else {
				progress(", $type: $ip => ### skipped ###");
			}
			progress("\n");
			unless ( $aliveip ) {
				$errors++;
				my @ERRS;
				if ( $aliven ) {
					push @ERRS, "Failed: $type $ip ($iname)";
				} else {
					push @ERRS, "Failed: $iname ($type $ip)";
				}
				push @ERRS, "\tline: ".$record->{line}.", file: “".$record->{file}.'”';
				unless ( $aliven ) {
					if ( $values->{CNAMEREV}{$k} ) {
						push @ERRS,"\t[Related CNAMEs]";
						foreach my $cn (@{ $values->{CNAMEREV}{$k} }) {
							push @ERRS,"\t * ".$cn->{value}.
								' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
						}
					}
					if ( $values->{PTR}{$ip} ) {
						push @ERRS,"\t[Related PTRs]";
						foreach my $cn (@{ $values->{PTR}{$ip} }) {
							push @ERRS,"\t * ".$cn->{value}.
								' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
						}
					}
				}
				my $alert = join("\n",@ERRS)."\n";
				$report->{$type}{$k} .= $alert;
				report($alert."\n");
			}
		}
	}
	report("\nRecords: $tot\nErrors: $errors\n",1);
}

sub parsesynA {
	my $type = shift || 'A';
	progress("\n\n*** Syntax test for records: $type ***\n\n");
	report("\n\n*** Syntax test for records: $type ***\n\n",1);
	my @keys = sort keys %{ $values->{$type} };
	my $tot = 0;
	my $errors = 0;
	foreach my $k (@keys) {
		foreach my $record (@{ $values->{$type}{$k} }) {
			$tot++;
		}
	}
	my $count = 0;
	foreach my $k (@keys) {
		my $iname = $k . '.' . $pars->{zone}{zone};
		foreach my $record (@{ $values->{$type}{$k} }) {
			$count++;
			my $ip = $record->{value};
			progress("$count/$tot $iname =>");
			my @ERRS;
			my $localerrors = 0;
			if ( ! $pars->{options}{noptrcheck} and $type eq 'A') {
				my $hasFile = 0;
				foreach my $ptr (@{ $pars->{ptr} }) {
					my $z = $ptr->{zone};
					if ( index($ip,$ptr->{zone}) == 0 ) {
						$hasFile++;
						last;
					}
				}
				my @ptrs;
				@ptrs = @{ $values->{PTR}{ $ip } } if ($hasFile and defined $values->{PTR}{ $ip });
				if ( scalar(@ptrs) > 1 ) {
					$localerrors += scalar(@ptrs) -1;
					push @ERRS,"\t[Multiple PTR records per IP]";
					foreach my $cn (@ptrs) {
						push @ERRS, "\t* ".$cn->{value}.' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
					}
				} elsif ( @ptrs ) {
					my $cn = shift @ptrs;
					my $names = {};
					$names->{$_->{value}}++ for @{ $values->{AREV}{$ip} };
					if ( $k =~ m/^\*\./ ) {
						my @keys = keys %$names;
						$names->{'*.'.$_}++ for @keys;
					}
					unless ($names->{ $cn->{value} }) {
						$localerrors++;
						push @ERRS,"\t[PTR mismatch for “$ip”]";
						push @ERRS, "\t* ".$cn->{value}.' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
					}
				} elsif ( ! $hasFile ) {
					unless ( $pars->{options}{noptrcheckzone} ) {
						$localerrors++;
						my $cn = "$ip";
						$cn =~ s/([.:])[^.:]+$/$1/;
						push @ERRS, "\t[Missing PTR zone file]";
						push @ERRS, "\t* $ip => $cn";
					}
				} else {
					$localerrors++;
					push @ERRS, "\t[Missing PTR record]";
					push @ERRS, "\t* $ip";
				}
			}
			if ( scalar( @{ $values->{$type}{$k} } ) > 1) {
				my @dupes;
				foreach my $cn (@{ $values->{$type}{$k} }) {
					next if $cn->{value} ne $record->{value};
					next if ($cn->{line} == $record->{line} and $cn->{file} eq $record->{file});
					push @dupes,$cn;
				}
				if ( @dupes ) {
					$localerrors += scalar( @dupes );
					push @ERRS, "\t[Duplicated $type records]";
					foreach my $cn (@dupes) {
						next if $cn->{value} ne $record->{value};
						push @ERRS,"\t * ".$cn->{value}.
							' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
					}
				}
			}
			if ( defined $values->{'CNAME'}{$k} ) {
				$localerrors += scalar( @{ $values->{'CNAME'}{$k} } );
				push @ERRS, "\t[Overlapping CNAMEs]";
				foreach my $cn (@{ $values->{'CNAME'}{$k} }) {
					push @ERRS,"\t * ".$cn->{value}.
						' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
				}
			}
			if ( $localerrors ) {
				$errors += $localerrors;
				unshift @ERRS,"$iname\n\tline: ".$record->{line}.", file: “".$record->{file}.'”';
				progress(" ### ISSUES ($localerrors) ###\n");
				my $alert = join("\n",@ERRS)."\n";
				$report->{$type}{$k} .= $alert;
				report( $alert."\n" );
			} else {
				progress(" OK\n");
			}
		}
	}
	report("\nRecords: $tot\nErrors: $errors\n",1);
}


sub parsesynCNAME {
	my $type = shift || 'CNAME';
	progress("\n\n*** Syntax test for records: $type ***\n\n");
	report("\n\n*** Syntax test for records: $type ***\n\n",1);
	my @keys = sort keys %{ $values->{$type} };
	my $tot = scalar(@keys);
	my $errors = 0;
	my $count = 0;
	foreach my $k (@keys) {
		$count++;
		my $iname = $k;
		$iname .= '.' . $pars->{zone}{zone} unless $type eq 'PTR';
		progress("$count/$tot $iname =>");
		my @ERRS;
		my $localerrors = 0;
		my $record = $values->{$type}{$k}[0];
		if ( scalar( @{ $values->{$type}{$k} } ) > 1) {
			$localerrors += scalar( @{ $values->{$type}{$k} } ) -1;
			push @ERRS, "\t[Multiple $type records]";
			foreach my $cn (@{ $values->{$type}{$k} }) {
				push @ERRS,"\t * ".$cn->{value}.
					' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
			}
		}
		{
			my @missing;
			my @doublec;
			foreach my $cn (@{ $values->{$type}{$k} }) {
				my $v = $cn->{value};
				next if $v =~ m/\.$/;
				if (defined $values->{CNAME}{$v} ) {
					push(@doublec,$cn);
					next;
				}
				next if $type eq 'PTR';
				next if defined $values->{A}{$v};
				next if defined $values->{AAAA}{$v};
				push(@missing,$cn);
			}
			if ( @missing ) {
				$localerrors += scalar(@missing);
				push @ERRS, "\t[Missing A/AAAA records]";
				foreach my $cn (@missing) {
					push @ERRS,"\t * ".$cn->{value}.
						' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
				}
			}
			if ( @doublec ) {
				$localerrors += scalar(@doublec);
				push @ERRS, "\t[$type to CNAME reference]";
				foreach my $cn (@doublec) {
					push @ERRS,"\t * ".$cn->{value}.
						' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
				}
			}
		}
		unless ( $type eq 'PTR' ) {
			foreach my $ot (qw/ A AAAA /) {
				if ( defined $values->{$ot}{$k} ) {
					$localerrors += scalar( @{ $values->{$ot}{$k} } );
					push @ERRS, "\t[Overlapping $ot records]";
					foreach my $cn (@{ $values->{$ot}{$k} }) {
						push @ERRS,"\t * ".$cn->{value}.
							' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
					}
				}
			}
			unless ( $pars->{options}{nosyntaxping} ) {
				my @pingfail;
				foreach my $cn (@{ $values->{$type}{$k} }) {
					my $v = $cn->{value};
					$v .= '.' . $pars->{zone}{zone} unless $v =~ m/\.$/;
					push(@pingfail,$cn) unless ping($v);
				}
				if ( @pingfail ) {
					$localerrors += scalar(@pingfail);
					push @ERRS, "\t[Ping failure]";
					foreach my $cn (@pingfail) {
						push @ERRS,"\t * ".$cn->{value}.
							' - line: '.$cn->{line}.', file: “'.$cn->{file}.'”';
					}
				}
			}
		}
		if ( $localerrors ) {
			$errors += $localerrors;
			unshift @ERRS,"$iname\n\tline: ".$record->{line}.", file: “".$record->{file}.'”';
			progress(" ### ISSUES ($localerrors) ###\n");
			my $alert = join("\n",@ERRS)."\n";
			$report->{$type}{$k} .= $alert;
			report( $alert."\n" );
		} else {
			progress(" OK\n");
		}
	}
	report("\nRecords: $tot\nErrors: $errors\n",1);
}

=encoding utf8

=head1 NAME

B<bindscan> - I<Perl script checking DNS zone files>

=head1 VERSION

B<0.9.2>

=head1 SYNOPSIS

B<bindscan> [I<options>] I<zone-name zone-file> [I<ptr-name ptr-file>]*

=head1 OPTIONS

=head2 -h, --help

Print full help and exit

=head2 -l I<filename>, --log=I<filename>

Path to logfile (provide path, default: STDOUT)

You can manually redirect progress from STDERR and report from STDOUT, but when a logfile is specified report is more detailed.

Report is appended to existing logfile, if any.

=head2 -p, --noping

Skip ping tests (default: perform ping test)

=head2 -P, --nosynping

Don't ping during syntax tests (default: ping)

=head2 -q, --quiet

Suppress progress report on STDERR

=head2 -r, --noptr

Skip PTR syntrax check (default: check if PTR(s) files are provided)

=head2 -R, --noptrzone

Skip report if PTR zone file is missing (default: report)

=head2 -s, --nosyn

Skip all syntax tests (default: perform tests)

=head2 -t I<integer>, --timeout=I<integer>

Seconds before timeout (default: 2)

=head1 PARAMETERS

Parameters are provided in couples.

Each couple contains zone name followed by zone file path (name, full or relative path).

=head2 I<zone-name> I<zone-file>

The first couple, mandatory, contains domain name (e.g.: I<mynetwork.it>) and the path to the zone file.

=head2 I<ptr-name> I<ptr-file>

You can supply more optional couples for reverse zones, made by network class (e.g.: B<192.168.184.>) and the path to the reverse zone file.

Such couples can be repeated as many times as necessary to provide the full list of related reverse zones.

=head1 PARAMETERS FILE

Any set of one or more I<name file> couples can be read from a text file using a B<@>I<filepath> parameter.

=head2 @I<filepath> or @:I<filepath>

A text file for parameter couples.

Any set of parameters zone couples (name/file) can be replaced by a file (provide its full or local path).

This file reports per each line zone name and file path separated by white space(s) and/or tab(s).

Comments prefixed by ";" or "#", empty lines are ignored.

=head1 DEPENDENCIES

This script was developed in order to require a minimal set of tools.

It has been successfully tested on Debian distributions and Mac OS X.

Requires B<Getopt::Long> and B<Net::Ping> modules from CPAN.

=head1 COPYRIGHT and CREDITS

=over 4

=item *

This Perl script is (C) 2020 by B<Marco Balestra> I<balestra@altersoftware.it>

=item *

It's available on Altersoftware site I<http://altersoftware.it>

=item *

Analysis and development by Marco Balestra I<http://altersoftware.it>

=item *

Originally developed for Joram Marino I<http://joram.it> in behalf of Uniroma3 I<http://www.uniroma3.it>, Italian Rome's 3rd University.

=item *

Released under license GPL v. 4-

=back

=cut

__DATA__
__DATA_SECTION::basicHelp__
Syntax:
    bindscan [options] zone-name zone-file [ptr-name ptr-file]*
Full help:
    bindscan -h
    bindscan --help

__DATA_SECTION::fullHelp__
Syntax:
    bindscan [options] zone-name zone-file [ptr-name ptr-file]*
Options:
    -h --help      print help and exit - man: perldoc bindscan
    -l --log       Path to logfile (provide path, default: STDOUT)
    -p --noping    Skip ping tests (default: perform ping test)
    -P --nosynping Syntax tests without ping (default: ping)
    -q --quiet     Suppress progress report on STDERR
    -r --noptr     Skip PTR check (default: check if PTR(s) are provided)
    -R --noptrzone Skip report if PTR zone file is missing (default: report)
    -s --nosyn     Skip syntax tests (default: perform tests)
    -t --timeout   Seconds before timeout (provide integer, default: 2)
Parameters:
    zone-name      Mandatory: The name of the zone (e.g.: mylan.it)
    zone-file      Mandatory: Path to the forward zone file
    ptr-name       Optional:  Base IP of the PTR (e.g.: 205.139.26)
    ptr-file       Optional:  Path to the PTR zone file
                   Repeat PTR couples if more than one.
Parameters file:
    @filepath      Parameters couples
    @:filepath     Any set of parameters zone couples (name/file) can be
                   replaced by a file (provide its full or local path).
                   This file reports per each line zone name and file path
                   separated by white space(s) and/or tab(s).
                   Comments prefixed by ";" or "#", empty lines are ignored.

__DATA_SECTION::ping__

# Each line: name protocol port
# Omit port only for basic ping (tcp tcp).
# Pings are performed in list order.
# Comments prefixed by # or ;

tcp tcp
ssh tcp 22
http tcp 80
dns tcp 53
rdp tcp 3389
https tcp 443

__END__
