#!/usr/bin/perl
# Copyright 2004-2013 SPARTA, Inc.  All rights reserved.
# See the COPYING file included with the DNSSEC-Tools package for details.

#
# If we're executing from a packed environment, make sure we've got the
# library path for the packed modules.
#
BEGIN {
    if ($ENV{'PAR_TEMP'}) {
	unshift @INC, ("$ENV{'PAR_TEMP'}/inc/lib");
    }
}

use Net::DNS::SEC::Tools::QWPrimitives;
use Net::DNS::SEC::Tools::BootStrap;
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::Donuts::Rule;
use Net::DNS::SEC::Tools::dnssectools;
use Date::Parse;

######################################################################
# detect needed perl module requirements
#
dnssec_tools_load_mods('Date::Parse' => "");
use Data::Dumper;

my $have_qw = 
  eval {
      require QWizard;
     };
my $qw;

use strict;

use Config;
use Net::DNS::SEC::Tools::Donuts;

our @guiargs;

my %opts = (l => 5,
	    c => $ENV{'HOME'} . "/.donuts.conf",
	    T => 'port 53 || ip[6:2] & 0x1fff != 0',
	    o => '%d.%t.pcap',
	    O => "wrapped",
	    L => "stdout",
	    r => "/usr/local/share/dnssec-tools/donuts/rules/*.txt," .
	    $ENV{'HOME'} . "/.dnssec-tools/donuts/rules/*.txt");

my $TCPDUMP = "tcpdump";

# override some defaults if we're a self-extracting perl archive with
# the files contained within the archive.
if (runpacked()) {
    $opts{'r'} = $ENV{'PAR_TEMP'} . "/inc/rules/" . "*.txt,";

    setconffile("$ENV{'PAR_TEMP'}/inc/dnssec-tools.conf");

    $TCPDUMP = "./tcpdump";
}

our ($rf, $current_zone_file,
     @ignorelist, @onlylist, 
     $netdns, $netdns_error, %outstructure, $current_domain,
     @filestested);


my %primaries = 
  (display_errors =>
   {
    title => 'Zone Errors',
    introduction => 'Below are the errors found when analyzing the zones',
    leftside => 
    ["Browse Results", 
     { type => 'tree',
       name => 'showthis',
       refresh_on_change => 1,
       expand_all => 2,
       root => 'Errors',
       parent => \&get_gui_parent,
       children => \&get_gui_children },
    ],
    questions =>
    [{ type => 'table',
       name => 'results',
       text => sub { ((qwparam('showthis') eq 'Errors' ||
		       qwparam('showthis') eq '') ?
		      'Summary:' : 'Results:') },
       values => sub {
	   if (qwparam('showthis') =~ /::/) {
	       my $tab;
	       my ($spot, $value) = (qwparam('showthis') =~ /(.*)::(.*)/);
	       foreach my $data (@{$outstructure{$spot}{$value}}) {
		   push @$tab, $data;
	       }
	       return [$tab];
	   } elsif (qwparam('showthis') eq 'Errors' ||
		    qwparam('showthis') eq '') {
	       my $tab;

	       push @$tab, ['results on testing:', join(', ',@filestested)];
#	       push @$tab, ['rules considered:', $#{$donuts->rules()}];
#	       push @$tab, ['rules tested:',     "FIX ME: only rules run"];
#	       push @$tab, ['records analyzed:', $#{$donuts->records()}];
#	       push @$tab, ['names analyzed:',   "FIX ME"];
#	       push @$tab, ['errors found:',     $globalerrcount];
	       return [$tab];
	   }
	   return [[[""]]];
       }
     }]
   });

DTGetOptions(\%opts,
		['GUI:VERSION',"DNSSEC-Tools Version: 2.1"],

		['GUI:screen',"Rule Set Configuration:"],
		["l|level=i", "The maximum rule level to run (default = 5)",
		 helpdesc => '(higher number = include more nit-picking tests)',
		 question => { type => 'menu', values => [1..9] }],

		['GUI:separator','Output Format Options:'],
		["show-gui", "Display the results in a browsable window.",
		 nocgi => 1,
		 question => { type => 'checkbox', default => 1, indent => 1}],
	        ["O|output-format=s", "Output format (wrapped, or text)",
		 nocgi => 1,
		 question => { type => 'menu', default => 'wrapped', indent => 1,
		 values => [qw(wrapped text)]}],
	        ["L|output-location=s", "Output location (stdout, stderr, file:/path)",
		 nocgi => 1,
		 question => { type => 'menu', default => 'stdout', indent => 1,
		 values => [qw(stdout stderr)]}],
                ["s|sorted", "Sort the loaded records so the output is consistent"],
		["v|verbose+",
		 "Verbose output (show extra processing information).  Use multiple times for increasing amounts of output.",
		 question => { type => 'checkbox', default => 1, indent => 1 }],
	        ["V|show-records", "Display the contents of the records read or queried"],
		 
		["q|quiet", "Quiet output (Do not print summary information)",
		 indent => 1],

		['GUI:separator','Advanced Options:'],
		['GUI:guionly',{type => 'checkbox',
				values => [1,0],
				default => 0,
				indent => 1,
				text => 'Show Advanced Options',
				name => 'advanced'}],

		"",
		['GUI:guionly',{type => 'button',
				values => 'Help',
				default => 1,
				nocgi => 1,
				text => 'Display Help Options',
				name => 'displayhelp'},
		],

		['GUI:screen','Advanced Configuration:', doif => 'advanced'],
		['GUI:separator','Rules Selection Configuration:'],
		["r|rules=s", "glob pattern for rule files to load",
		 indent => 1,
		 doif =>
		 sub { ref($_[1]->{'generator'}) !~ /HTML/}, # not safe for web
		],
		["i|ignore=s", "Regular expression for rules to ignore",
		 indent => 1],
		["only=s", "Regular expression for rules to include (default = all)",
		 indent => 1],
		["f|features=s", "Extra features to turn on",
		 helpdesc => '(comma separated)',
		 indent => 1],

		['GUI:separator',"Configuration Files:", nocgi => 1],
		["C|no-config","Do not load personal configuration files",
		 indent => 1, nocgi => 1],
		["c|config-file=s",
		 "Use an alternate personal configuration file",
		 indent => 1,
		 doif =>
		 sub { ref($_[1]->{'generator'}) !~ /HTML/}, # not safe for web
		],

		['GUI:otherargs_text',"FILE DOMAIN [FILE DOMAIN...]"],
		['GUI:otherargs_required',1],
		
		['GUI:screen',"Extra Live Query Options:",
		 doif => sub { 
		     $_[1]->qwparam('live') && !$_[1]->qwparam('displayhelp') &&
		       ref($_[1]->{'generator'}) !~ /HTML/  # not safe for web
			 ;
		 }
		],
		["t|tcpdump-capture=s",
		 "Start tcpdump on interface STRING during run"],
		["T|tcpdump-filter=s",
		 "Use tcpdump filter (default: port 53 or fragments)"],
		["o|tcpdump-output-file=s",
		 "Save tcpdump results to file STRING."],

		['GUI:screen',"Help Options:",
		 doif => 'displayhelp'],
		["R|help-rules", 'Show the rules that donuts checks'],
		["F|help-features",
		 "Show available additional features of available rules."],
		["H|help-config",
		 'Show configuration tokens supported by the rules'],
		
		['GUI:nootherargs',1],
		['GUI:submodules','getzonefiles','getzonenames'],
		['GUI:otherprimaries',
		 dnssec_tools_get_qwprimitives(%primaries)],
	       ) || exit;

push @main::ARGV, @guiargs;

if (!$opts{'R'} && !$opts{'F'} && !$opts{'H'} && 
    ($#ARGV == -1 || $#ARGV % 2 != 1)) {
    print STDERR "\nUsage Error: $0 called with wrong number of arguments\n";
    print STDERR "  file and zone name arguments are both needed\n";
    print STDERR "  (EG: $0 FILE1 example.com FILE2 other.example.com)\n\n";
    exit 1;
}

#
# Create the main Donuts object
#
my $donuts = new Net::DNS::SEC::Tools::Donuts();
$donuts->set_config('verbose', $opts{'v'});
$donuts->set_config('sorted', $opts{'s'});
$donuts->set_output_format($opts{'O'});
$donuts->set_output_location($opts{'L'});

#
# initialize ignore list
#
if ($opts{'i'}) {
    $donuts->set_ignore_list(split(/,\s*/, $opts{'i'}));
}

if ($opts{'only'}) {
    $donuts->set_only_list(split(/,\s*/, $opts{'only'}));
}

#
# create the feature set
#
my %features;
if ($opts{'f'}) {
    $donuts->set_feature_list(split(/,\s*/, $opts{'f'}));
}

# start the output wrapper (eg, <?xml ...>)
$donuts->output()->StartOutput();

#
# load rule files
#   (comma separated list)
#
$donuts->load_rule_files($opts{'r'});

#
# load optional user-config file
#
if ($opts{'c'} && !$opts{'C'} && -f $opts{'c'}) {
    $donuts->parse_config_file($opts{'c'});
}

#
# display config file help
#
if ($opts{'H'}) {
    maybe_output_to_web();
    print STDERR "$0 configuration tokens for loaded rules:\n\n";
    printf STDERR sprintf("%-20s %-15s%s\n",
			  "RULE NAME", "TOKEN", "DESCRIPTION");
    printf STDERR sprintf("%-20s %-15s%s\n", "_" x 19, "_" x 13, 
			  "_" x (80-20-15-2));
    my @rules = $donuts->rules();
    foreach my $rule (@rules) {
	$rule->print_help();
    }
    exit;
}

#
# display a list of rules
#
if ($opts{'R'}) {
    maybe_output_to_web();
    print STDERR "\n$0 rules:\n\n";
    printf STDERR "RULE NAME\n  DESCRIPTION...\n";
    printf STDERR "_" x 75 . "\n";
    my @rules = $donuts->rules();
    foreach my $rule (@rules) {
	$rule->print_description() if (!$rule->{'internal'});
    }
    exit;
}

#
# display a list of rules
#
if ($opts{'F'}) {
    maybe_output_to_web();
    print STDERR "\n$0 feature list:\n";
    print STDERR "  (Turn these on using the --features flag)\n\n";
    my %shown;
    my @features = $donuts->available_features();
    foreach my $feature (@features) {
	print "  $feature\n";
    }
    exit;
}

#
# must specify at least one zone file
#
exit() if ($opts{'h'} || $#ARGV == -1);

#
# load zone files
#
my $exitcode = 0;
my $parseerror;
my $errcount;

maybe_output_to_web();
while ($#ARGV > -1) {
    $errcount = 0;
    my $rulecount = 0;
    my ($rulesrun, $errorsfound);
    $current_zone_file = shift;
    push @filestested, $current_zone_file;
    $current_domain = shift;
    $current_domain =~ s/\.$//;  # remove potential trailing dot

    #
    # Start collecting TCPDUMP data if requested
    #
    my $tcpdumpproc;
    if ($opts{'t'}) {
	my $file = $opts{'o'};
	$file =~ s/\%t/time()/eg; # replace %t with epoch
	$file =~ s/\%d/$current_domain/g; # replace %d with domain
	my @args = ("-i", $opts{t},
		    "-f", $opts{T},
		    "-s", 4096,
		    "-w", $file);
	if ($tcpdumpproc = fork()) {
	    # parent
	    sleep(2);  # wait for child to get going
	    print STDERR "--- Starting tcpdump\n" if ($opts{v});
	} else {
	    # child

	    # close stderr/out since we don't want the output
	    close(STDOUT);
	    close(STDERR);

	    open(STDOUT,">/dev/null");
	    open(STDERR,">/dev/null");

	    # exec tcpdump
	    exec($TCPDUMP, @args);
	}
    }

    #
    # Parse the file into an array
    #
    my $parse_error = $donuts->load_zone($current_zone_file, $current_domain);
    my $rrset = $donuts->zone_records();

    if ($opts{'V'}) {
	print "Records parsed:\n";
	dump_records($rrset, "  ");
    }

    next if ($parse_error);
    if (!$rrset) {
	print STDERR "WARNING: failed to read $current_zone_file for an unknown reason\n";
	print STDERR "$@\n" if ($@);
	next;
    }

    #
    # call each rule on each record
    #
    $donuts->output()->Separator();
    $donuts->output()->StartSection("Donuts Analysis", "$current_domain");
    ($rulecount, $errcount) = $donuts->analyze($opts{'l'});

    $donuts->show_statuses() if (!$opts{'q'});
    $donuts->summarize_results() if (!$opts{'q'});
    
    
#    print "$errcount errors found in $current_zone_file\n";

    if (scalar($donuts->rules()) == -1) {
	$donuts->output()->Comment("WARNING: no rules found to be executed!!!");
	$donuts->output()->Comment("WARNING: (maybe use the --rules switch to fix this?)");
    }
    if ($#$rrset == -1) {
	$donuts->output()->Comment("WARNING: no records found to be analyzed in $current_domain!!!");
    }
    if ($errcount) {
	$exitcode = 1;
    }

    $donuts->output()->EndSection();

    #
    # stop tcpdump if we had started it
    #

    if ($tcpdumpproc) {
	print STDERR "--- Stopping tcpdump.\n" if ($opts{v});
	kill(15, $tcpdumpproc);
	sleep(1);
	kill(9, $tcpdumpproc);
    }
}
$donuts->output()->EndOutput();

if ($opts{'show-gui'}) {
    setup_gui();
    display_gui_results();
}
exit($exitcode);

######################################################################
#
# GUI support (requires the QWizard module)
#

#
# setup: creates the qwizard instance and needed primaries
#
sub setup_gui {
    return if (!$have_qw);
    import QWizard;

    # the primaries
    $qw = $Getopt::Long::GUI_qw || new QWizard();
    $qw->merge_primaries(\%primaries);
}

#
# calls QWizard
#
sub display_gui_results {
    return if (!$have_qw);
    $qw->reset_qwizard();
    $qw->{'generator'}{'noheaders'} = 1;
    $qw->magic('display_errors');
}

#
# returns the parent of a given node
#
sub get_gui_parent {
    my ($wiz, $name) = @_;
    return if ($name eq 'Errors');
    return 'Errors' if ($name eq 'By Record Name' || $name eq 'By Rule Type');
    return 'By Record Name' if ($name =~ /^location::/);
    return 'By Rule Type' if ($name =~ /^rulename::/);
}

#
# returns the children of a given node
#
sub get_gui_children {
    my ($wiz, $name) = @_;
    return ['By Record Name', 'By Rule Type'] if ($name eq 'Errors');

    if ($name eq 'By Record Name') {
	my @ret;
	map { push @ret, { name => 'location::' . $_,
			   label => $_ }
	  } keys(%{$outstructure{'location'}});
	return \@ret;
    }

    if ($name eq 'By Rule Type') {
	my @ret;
	map { push @ret, { name => 'rulename::' . $_,
			   label => $_ }
	  } keys(%{$outstructure{'rulename'}});
	return \@ret;
    }
    return;
}


#######################################################################

#
# subroutines for doing live queries on running systems
#

sub get_query {
    my ($name, $type, $resolver) = @_;
    $resolver = $netdns if (!$resolver);
    my $query = $resolver->query($name, $type);
    if ($query) {
	return $query;
    } else {
	# print STDERR "DNS error " . $resolver->errorstring . "\n";
	$netdns_error = $resolver->errorstring;
	return;
    }
}

sub live_query {
    my $query = get_query(@_);
    if ($query) {
	return $query->answer;
    }
    return ();
}

#
# returns 0 when arrays of records are identical.
# returns -1 if the arrays are non-equal length
# returns the index+1 where the arrays differ otherwise.
sub compare_arrays {
    my ($a1, $b1, $sortfun) = @_;
    $sortfun = sub { $a cmp $b } if (!$sortfun);
    return -1 if ($#$a1 != $#$b1);
    my @a = sort $sortfun @$a1;
    my @b = sort $sortfun @$b1;

    for (my $i = 0; $i <= $#a && $i <= $#b; $i++) {
	if ($a[$i]->string() ne $b[$i]->string()) {
	    return $i+1;
	}
    }
    return $#a+1 if ($#a < $#b);
    return $#b+1 if ($#a < $#a);
    return 0;
}

sub compare_RR_arrays {
    my ($a1, $b1, $hashval) = @_;
    return -1 if ($#$a1 != $#$b1);
    my @a = sort @$a1;
    my @b = sort @$b1;

    for (my $i = 0; $i <= $#$a1; $i++) {
	print STDERR "$a[$i]{$hashval} ne $b[$i]{$hashval}\n";
	if ($a[$i]{$hashval} ne $b[$i]{$hashval}) {
	    return $i;
	}
    }
    return;
}

sub print_parse_error {
    my ($line, $err) = @_;
    $errcount++;
    print STDERR "$current_zone_file:$line $err\n";
}

#
# setup for printing some things to a web page instead
#
sub maybe_output_to_web {
    #
    # some stuff for web purposes should be redirected to the screen
    #
    if (defined($Getopt::GUI::Long::GUI_qw) && $Getopt::GUI::Long::GUI_qw->{'generator'} =~ /HTML/) {
	$donuts->set_output_format('html');
	$donuts->set_output_location('stdout');
    }
}

sub debug_dump_data {
    my ($datastorage) = @_;
    print "data dump: \n";
    foreach my $domain (keys(%$datastorage)) {
	print "  $domain\n";
	foreach my $type (keys(%{$datastorage->{$domain}})) {
	    print "    $type: ", 1 + $#{$datastorage->{$domain}{$type}}, "\n";
	}
    }
}

sub records_sorter {
    if ($a->name eq $b->name) {
	return $a->type cmp $b->type;
    }
    return $a->name cmp $b->type;
}

sub dump_records {
    my ($records, $prefix, $short) = @_;
    $prefix = "" if (!defined($prefix));
    $short = 0 if (!defined($short));

    my $formatstring = $prefix . ($short ? "%-75.75s\n" : "%s\n");

    foreach my $record (sort records_sorter @$records) {
	printf($formatstring, $record->string);
    }
}

# this is merely a convenience function for rule authors to place into
# rules so a break point can be put on the function to stop in a
# particular location within a rule definition.
sub break_here {
    my $x = 1;
}

=pod

=head1 NAME

donuts - analyze DNS zone files for errors and warnings

=head1 SYNOPSIS

  donuts [-v] [-l LEVEL] [-r RULEFILES] [-i IGNORELIST]
         [-C] [-c configfile] [-h] [-H] ZONEFILE DOMAINNAME...

=head1 DESCRIPTION

B<donuts> is a DNS lint application that examines DNS zone files
looking for particular problems.  This is especially important for
zones making use of DNSSEC security records, since many subtle
problems can occur.  The default mode of operation assumes you want to
check for DNSSEC-related issues; to turn off the invocation of the
DNSSEC-related rules run B<donuts> with "-i DNSSEC".

If the B<Text::Wrap> Perl module is installed, B<donuts> will give better
output formatting.

=head2 QUERYING LIVE ZONES

If the I<ZONEFILE> may be a live name prefixed with "live:" and the
records afterwards will be queried and analyized as if it was in a
zonefile.  For example, running donuts as:

  donuts -i NSEC live:badsign-a,good-a test.dnssec-tools.org

Will query the test.dnssec-tools.org zone for the I<badsign-a> and
I<good-a> records, collect the data and run them through the donuts
analysis.  Because other data is needed as well for the analysis to be
useful, donuts will also automatically collect the DNSKEYs, SOAs, and
NS records for the zone.  Because NSEC records aren't loaded, it is
advisable to add that rule exclusion for rules to be run.

Other data types may be queried by appending a ':' and the type name
to a record.  For example:

  donuts -i NSEC live:good-a,good-aaaa:aaaa test.dnssec-tools.org

=head2 QUERYING LIVE ZONES WITH AFXR

If your host is allowed to query for afxr transfers of a particular
zone, you may use the I<afxr:> filename token to indicate it should
pull the zone data using a AFXR transfer.

  donuts -i NSEC afxr: dnssec-tools.org

=head1 OPTIONS

=head2 Rule Set Configuration:

=over

=item -l I<LEVEL>

=item --level=I<LEVEL>

Sets the level of errors to be displayed.  The default is level 5.
The maximum value is level 9, which displays many debugging results.
You probably want to run no higher than level 8.

=item -r I<RULEFILES>

=item --rules=I<RULEFILES>

A comma-separated list of rule files to load.  The strings will be
passed to I<glob()> so * wildcards can be used to specify multiple files.

Defaults to B</usr/local/share/dnssec-tools/donuts/rules/*.txt> and
B<$HOME/.dnssec-tools/donuts/rules/*.txt>.

=item -i I<IGNORELIST>

=item --ignore=I<IGNORELIST>

A comma-separated list of regex patterns which are checked against
rule names to determine if some should be ignored.  Run with I<-v> to
figure out rule names if you're not sure which rule is generating
errors you don't wish to see.

=item -f LIST

=item --features=LIST

The I<--features> option specifies additional rule features that should
be executed.  Some rules are turned off by default because they are
more intensive or require a live network connection, for instance.
Use the I<--features> flag to turn them on.  The LIST argument should be
a comma-separated list.  Example usage:

  --features live,nsec_check

Features available in the default rule set distributed with B<donuts>:

=over

=item live

The I<live> feature allows rules that need to perform live DNS queries
to run.  Most of these I<live> rules query parent and children of the
current zone, when appropriate, to see that the parent/child
relationships have been built properly.  For example, if you have a
DS record which authenticates the key used in a child zone the I<live>
feature will let a rule run which checks to see if the child is
actually publishing the DNSKEY that corresponds to the test zone's DS
record.

=item nsec_check

This checks all the NSEC or NSEC3 records (as appropriate for the
zone) to ensure the chain is complete and that no-overlaps exist.  It
is fairly memory- and cpu-intensive in large zones.

=back

=back

=head2 Configuration File Options:

=over

=item -c I<CONFIGFILE>

=item --config-file=I<CONFIGFILE>

Parse a configuration file to change constraints specified by rules.
This defaults to B<$HOME/.donuts.conf>.

=item -C

=item --no-config

Don't read user configuration files at all, such as those specified by
the I<-c> option or the B<$HOME/.donuts.conf> file.

=back

=head2 Extra Live Query Options:

Live Queries are enabled through the use of the I<-f live> arguments.
These options are only useful if that feature has been enabled.

=over

=item -t I<INTERFACE>

=item --tcpdump-capture=I<INTERFACE>

Specifies that B<tcpdump> should be started on I<INTERFACE> (e.g.,
"eth0") just before B<donuts> begins its run of rules for each domain
and will stop it just after it has processed the rules.  This is
useful when you wish to capture the traffic generated by the I<live>
feature, described above.

=item -T I<FILTER>

=item --tcpdump-filter=I<FILTER>

When B<tcpdump> is run, this I<FILTER> is passed to it for purposes of
filtering traffic.  By default, this is set to I<port 53 || ip[6:2] &
0x1fff != 0>, which limits the traffic to traffic destined to port 53
(DNS) or fragmented packets.

=item -o I<FILE>

=item --tcpdump-output-file=I<FILE>

Saves the B<tcpdump>-captured packets to I<FILE>.  The following
special fields can be used to help generate unique file names:

=over

=item %d

This is replaced with the current domain name being analyzed (e.g.,
"example.com").

=item %t

This is replaced with the current epoch time (i.e., the number of
seconds since Jan 1, 1970).

=back

This field defaults to I<%d.%t.pcap>.

=item --show-gui

[alpha code]

Displays a browsable GUI screen showing the results of the B<donuts> tests.

The B<QWizard> and B<Gtk2> Perl modules must be installed for this to work.

=back

=head2 Help Options

=over

=item -H

Displays the personal configuration file rules and tokens that are
acceptable in a configuration file.  The output will
consist of a rule name, a token, and a description of its meaning.

Your configuration file (e.g., B<$HOME/.donuts.conf>) may have lines in it
that look like this:

  # change the default minimum number of legal NS records from 2 to 1
  name: DNS_MULTIPLE_NS
  minnsrecords: 1

  # change the level of the following rule from 8 to 5
  name: DNS_REASONABLE_TTLS
  level: 5

This allows you to override certain aspects of how rules are executed.

=item -R

Displays a list of all known rules along with their description (if
available).

=item -h

Displays a help message.

=item --help

Displays a help message more tailored to people who prefer long-style
options.

=item -q

Turns on a quieter output mode where only the errors and warnings are
shown.  IE, the summary line of "N errors found ..." is not shown.

-q is ignored if a -v argument is present; the -v argument requests a
longer output summary and thus it doesn't make sense to use them both
at the same time.

=item -v

Turns on more verbose output.  Multiple I<-v>'s will turn on increasing
amounts of output.  The number of -v's will dictate output:

=item -s

Sorts the resource records so that the order they're processed in is
always consistent.  If the incoming zone is not always consistently
ordered, the output can vary unless the resource records are always in
the same order.  When sorted, however, they're always evaluated in the
same order even if the zone file (or similar) order changes, and thus
the output is consistent, making it easier for tools like I<diff> to
detect where changes occur in the output.  This comes at a higher CPU
cost, as it takes more time to sort the entries.

=over

=item 1

Describes which rules are being loaded and extra detail for rules that found errors (rule Level and extra text detail)

=item 2

Even more detail about rules that found errors: file name, file line
number, rule type.

=item 3

Shows extra detail on the record text being analyzed (the detail is
not always available, however).

=item 4

Even more detail about rules that found errors: dumps the rule code itself.

=item 5

Even more detail about rules that found errors: dumps the internal
rule structure.

=back

=back

=head2 Obsolete Options

=over

=item -L

Obsolete command line option.  Please use I<--features live> instead.

=back

=head1 EXAMPLES

Run B<donuts> in its default mode on the I<example.com> zone which is
contained in the B<db.example.com> file:

  % donuts db.example.com example.com

Run B<donuts> with significantly more output, both in terms of verbosity
and in terms of the number of rules that are run to analyze the file:

  % donuts -v -v --level 9 db.example.com example.com

=head1 COPYRIGHT

Copyright 2004-2013 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wes Hardaker <hardaker@users.sourceforge.net>

=head1 SEE ALSO

For more information on the dnssec-tools project:

  http://www.dnssec-tools.org/

For writing rules that can be loaded by B<donuts>:

  B<Net::DNS::SEC::Tools::Donuts::Rule>, 

General DNS and DNSSEC usage:

  B<Net::DNS>, B<Net::DNS::SEC>

=cut
