#!/usr/bin/perl -wT


=head1 NAME

update-openvas-plugins - Updates OpenVAS plugins with various options.


=head1 SYNOPSIS

  # updates openvas plugins. NB: this does nothing more than call
  #   nessus-update-plugins.
  update-openvas-plugins

  # creates a backup of old plugins and updates plugins.
  update-openvas-plugins -b

  # creates a backup, updates plugins, and prints a summary of 
  #   new / changed plugins.
  update-openvas-plugins -bs

  # updates plugins and parses new / changed plugins for errors.
  update-openvas-plugins -p


=head1 DESCRIPTION

This script updates to the latest set of plugins for openvas and
optionally creates a backup of existing plugins, prints a summary of new
/ changed plugins, and parses updated plugins to check for errors.  It
calls B<nessus-update-plugins> to do the actual updates and
B<describe-openvas-plugin> to obtain descriptive information about
updated plugins for the summary report. 

Optional behaviour can be selected using one or more variables or
commandline arguments:

    Variable            Commandline         Purpose
    $backup             -b|--backup         Create a backup of existing
                                                plugins first.
    $DEBUG              -d|--debug          Turn on debugging. NB: this
                                                will still update plugins!
    @ignores            -i|--ignores        Ignore the specified files found
                                                in the plugins directory from
                                                the summary report and parse
                                                check.
    $lang               -l|--language       Set the language preference used
                                                to retrieve information for
                                                summary report from plugins.
    $parse              -p|--parse          Parse new / changed plugins
                                                and report whether errors
                                                exist.
    $summary            -s|--summary        Print a summary report of 
                                                new / changed plugins.

B<update-openvas-plugins> is written in Perl and calls
B<nessus-update-plugins> to actually update the plugins as well as
B<describe-openvas-plugin> to obtain descriptive information about new /
changed plugins.  It should work on any unix-like system with Perl 5 or
better (Perl 5.005 if you choose to generate summary reports).  It also
requires the following Perl modules:

    o Algorithm::Diff
    o Archive::Tar
    o Carp
    o Digest::MD5
    o File::Find
    o Getopt::Long
    o IO::Zlib (used by Archive::Tar to output file in compressed form)
    o POSIX

If your system does not have these modules installed already, visit CPAN
(L<http://search.cpan.org/>) for help.  Note that C<Algorithm::Diff>,
C<Archive::Tar>, and C<IO::Zlib> are not included with Perl
distributions and that C<Digest::MD5> is not included with Perl
distributions prior to 5.8.0 so you may need to install them yourself. 


=head1 KNOWN BUGS AND CAVEATS

Currently, I am not aware of any bugs in this script. 

You need a working version of B<nessus-update-plugins> to use this
script.  If you are having trouble getting that to work, please join
C<nessus@list.nessus.org> (L<http://list.nessus.org>) and ask for
assistance there.  Note that if you compiled openvas from source,
B<nessus-update-plugins> will not be installed unless
C<nessus-plugins/configure> can find either B<curl>, B<links>, B<lynx>,
or B<wget> on your system or you invoked it with the option
C<--with-fetchcmd=E<lt>cmdE<gt>>. 

If you wish to generate summary reports of new / changed plugins, you
also need B<describe-openvas-plugin> version 2.01 or better, which is
available from L<http://www.tifaware.com/perl/describe-openvas-plugin/>. 

If you encounter an error similar to C<Insecure dependency in chdir
while running with -T switch at /usr/lib/perl/5.00503/File/Find.pm line
133> when trying to run B<update-openvas-plugins>, it's likely that
you're using an older version of the Perl module C<File::Find>. 
Versions distributed with Perl versions prior to 5.6.0 don't support the
C<no_chdir> option, which is used in this script to avoid problems with
taint checks.  The solution is to either upgrade C<File::Find>, upgrade
Perl itself, or disable taint checks (ie, remove the C<-T> option on the
first line of the script). 

The option for parsing new / changed plugins requires that the NASL
interpreter support the C<-p> option. 

If you encounter a problem with this script, I encourage you to rerun it
in debug mode (eg, add C<-d> to your commandline) and examine the
resulting output before contacting me.  Often, this will enable you to
resolve the problem by yourself. 


=head1 DIAGNOSTICS

Failure to change into the plugin directory, to read a plugin, to create
a backup, or to run an external command (B<nessus-update-plugins>,
B<describe-openvas-plugin>, B<nasl>) will be treated as fatal errors and
reported to stderr using C<croak>. 

Warnings / errors from running B<describe-openvas-plugin> will be
reported to stderr using C<warn>. 


=head1 SEE ALSO

L<nessus-update-plugins(5)>,
L<http://www.tifaware.com/perl/describe-openvas-plugin/>,
L<http://www.tifaware.com/perl/update-openvas-plugins/>.


=head1 AUTHOR

George A. Theall, E<lt>theall@tifaware.comE<gt>


=head1 COPYRIGHT AND LICENSE

Copyright (c) 2003 - 2004, George A. Theall.
All rights reserved.

This script is free software; you can redistribute it and/or modify
it under the same terms as Perl itself. 


=head1 HISTORY

19-Jul-2004, v1.30, George A. Theall
    o Changed the bang-path to use /usr/bin/perl rather than
      /usr/local/bin/perl.
    o Changed the usage message.
    o Added code to display the usage message if option parsing fails.
    o Added code to ignore selected files in the plugins directory from
      the summary report and the parse check along with the associated
      option C<-i> to specify files from the commandline.

09-Dec-2003, v1.20, George A. Theall
    o Added C<-h> option as a synonym for C<--help>.
    o Added option for selecting preferred language.
    o Replaced C<print STDERR> with C<warn> throughout the script.
    o Renamed variable C<$lang_pref> to C<$lang>.
    o Replaced code used in summary report to parse descriptive 
      information from a plugin with a call to
      B<describe-openvas-plugin>.
    o Removed descriptive info from summary report for any plugins
      that are include files since the info will always be empty.

12-Oct-2003, v1.11, George A. Theall
    o Made minor changes in the documentation.
    o Removed the superfluous variable C<@report>.

10-Oct-2003, v1.10, George A. Theall
    o Added support for BugTraq IDs and X-References in the summary 
      report.

03-Sep-2003, v1.02, George A. Theall
    o Fixed another glitch in reporting plugin information that would
      arise if comments were not anchored at the start of lines.

30-Jun-2003, v1.01, George A. Theall
    o Fixed a glitch that could lead to erroneous information about
      plugins being reported because comments in scripts were not
      ignored.

04-Jun-2003, v1.00, George A. Theall
    o Initial version.

=cut


############################################################################
# Make sure we have access to the required modules.
use 5;
use strict;
use Algorithm::Diff qw(diff);
use Archive::Tar;
use Carp;
use Digest::MD5;
use File::Find;
use Getopt::Long;
use POSIX qw(strftime);


############################################################################
# Initialize variables.
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Make %ENV safer
$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin:/usr/local/sbin';    # nb: also passed to nessus-update-plugins
$| = 1;
my $DEBUG = 0;
my $backup = 0;                             # retain backup of old plugins?
my @ignores = (                             # ignore selected files from reports.
    'MD5',                                  #   - used to verify transfer starting with 2.1.0
);
my @info_funcs = (                          # descriptive funcs of interest.
                                            # nb: passed to describe-openvas-plugin
    'bugtraq_id',
    'category',
    'cve_id',
    'family',
    'id',
    'name',
    'risk',
    'summary',
    'version',
    'xref',
);
my $lang = 'english';       # 'deutsch', 'english', 'francais', or 'portugues'
my $parse = 0;                              # parse new / changed plugins?
my $plugins_dir = '/usr/local/lib/openvas/plugins';  # where plugins are stored.
my $scratch_dir = '/tmp';                   # where archive is stored.
my $summary = 0;                            # summarize changes to plugins?


############################################################################
# Process commandline arguments.
my %options = (
    'backup'   => \$backup,
    'debug'    => \$DEBUG,
    'language' => \$lang,
    'parse'    => \$parse,
    'summary'  => \$summary,
);
Getopt::Long::Configure('bundling');
GetOptions(
    \%options,
    'backup|b!',
    'debug|d!',
    'help|h|?!',
    'ignores|i=s@',
    'language|l=s',
    'parse|p!',
    'summary|s!',
) or $options{help} = 1;
$0 =~ s/^.+\///;
if ($options{help} or @ARGV) {
    warn "\n",
        "Usage: $0 [options]\n",
        "\n",
       "Options:\n",
        "  -?, -h, --help             Display this help and exit.\n",
        "  -b, --backup               Create a backup of existing plugins first.\n",
        "  -d, --debug                Display copious debugging messages while\n",
        "                               running. Note: this will still update\n",
        "                               the plugins!\n",
        "  -i, --ignores <files>      Ignore the specified files in the plugins\n",
        "                               directory from the summary report and\n",
        "                               the parse check.\n",
        "  -l, --language <lang>      Use <lang> as the language preference; must be\n",
        "                               one of 'deutsch', 'english', 'francais', and\n",
        "                               'portugues'.\n",
        "  -p, --parse                Parse new / changed plugins and report\n",
        "                               whether errors exist.\n",
        "  -s, --summary              Print a summary report of new / changed plugins.\n";
    exit(9);
}
@ignores = split(/,\s*/, join(',', @{$options{ignores}}))
    if ($options{ignores});

chdir $plugins_dir or croak "Can't change directory to '$plugins_dir' - $!\n";


############################################################################
# Take a snapshot of plugins directory.
my %old_plugins;
if ($backup or $parse or $summary) {
    warn "debug: files in '$plugins_dir' before update:\n" if $DEBUG;
    find(
        { wanted => sub {
                if ($File::Find::dir eq '.' and !/^\.\/\.desc/ and /^\.\/(.+)$/) {
                    warn "debug:   $1\n" if $DEBUG;
                    $old_plugins{$1}++;
                }
            },
          no_chdir => 1,
          untaint => 1,
        },
        '.'
    );
}

if ($parse or $summary) {
    # Compute hashes for plugins so we can detect changes.
    warn "debug: computing hashes for plugins:\n" if $DEBUG;
    foreach my $file (sort keys %old_plugins) {
        open(FILE, $file) or croak "Can't open '$file' - $!\n";
        binmode(FILE);
        my $md5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
        close(FILE);
        warn "debug:   $file -> $md5\n" if $DEBUG;
        $old_plugins{$file} = $md5;
    }
}


############################################################################
# Create backup.
my($archive, $tar);
if ($backup or $summary) {
    warn "debug: generating backup of files in '$plugins_dir'.\n" if $DEBUG;
    $archive = "$scratch_dir/plugins-pre-" . 
        strftime("%Y%m%d-%H%M%S", localtime) .
        ".tar.gz";
    $tar = Archive::Tar->new;
    $tar->add_files(keys %old_plugins);
    $tar->write($archive, 1) or
        croak "Can't create '$archive' - " . $tar->error() . "!\n";
    # nb: for some reason, it's necessary to reread the archive 
    #     into memory; otherwise, it will appear empty. :-(
    $tar->read($archive, 1);
}


############################################################################
# Update plugins.
warn "debug: updating plugins.\n" if $DEBUG;
my $cmd = 'nessus-update-plugins';
system $cmd;
my $rc = $? >> 8;
croak "Can't retrieve plugins ($rc)!\n" if ($rc);


############################################################################
# Take a second snapshot of plugins directory.
my %new_plugins;
if ($parse or $summary) {
    warn "debug: files in '$plugins_dir' after update:\n" if $DEBUG;
    find(
        { wanted => sub {
                if ($File::Find::dir eq '.' and !/^\.\/\.desc/ and /^\.\/(.+)$/) {
                    warn "debug:   $1\n" if $DEBUG;
                    $new_plugins{$1}++;
                }
            },
          no_chdir => 1,
          untaint => 1,
        },
        '.'
    );

    # Compute hashes anew.
    warn "debug: computing hashes for plugins:\n" if $DEBUG;
    foreach my $file (sort keys %new_plugins) {
        open(FILE, $file) or croak "Can't open '$file' - $!\n";
        binmode(FILE);
        my $md5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
        close(FILE);
        warn "debug:   $file -> $md5\n" if $DEBUG;
        $new_plugins{$file} = $md5;
    }
}


############################################################################
# Report changes.
if ($summary) {
    foreach my $plugin (sort keys %new_plugins) {
        next if (grep($plugin eq $_, @ignores));

        # Determine status and skip plugins that weren't updated.
        #
        # nb: nessus-update-plugins doesn't remove plugins.
        my $status;
        if (exists $old_plugins{$plugin}) {
            if ($old_plugins{$plugin} eq $new_plugins{$plugin}) {
                next;
            }
            $status = 'changed';
        }
        else {
            $status = 'added';
        }

        # Get descriptive info about plugin.
        #
        # nb: we've already chdir'd into $plugin_dir.
        my($indent, @info);
        if ($plugin =~ /\.nasl$/) {
            my $cmd = 'describe-openvas-plugin ' . 
                '-f ' . join(',', @info_funcs) . ' ' .
                "-l $lang " .
                $plugin;
            open(CMD, "$cmd 2>&1 |") or croak "Can't run '$cmd' - $!\n";
            while (<CMD>) {
                chomp;
                warn "$_\n" if (/^\*{3} /); # nb: display any warnings / errors.
                next unless (/^\s/);
                $indent = length($1) - 4 if (!$indent and /^(.+?:\s*)/);
                push(@info, $_);
            }
            close(CMD);
            my $rc = $? >> 8;
            if ($rc or !$indent) {
                warn "*** Can't get descriptive info for '$plugin' (rc=$rc)! ***\n";
            }
        }
        # nb: include files don't have a descriptive part.
        elsif ($plugin !~ /\.inc$/) {
            warn "*** '$plugin' has an unsupported plugin type! ***\n";
        }
        $indent = 15 unless ($indent);

        # If plugin was changed, compute diffs.
        my $diffs;
        if ($status eq 'changed') {
            warn "debug:   computing diffs.\n" if $DEBUG;
            my @old = split(/\n/, $tar->get_content($plugin));

            my @new;
            open(NEW, $plugin) or croak "Can't read '$plugin' - $!\n";
            chomp(@new = <NEW>);
            close(NEW);

            $diffs = diff(\@old, \@new);
        }

        # Print report.
        #
        # nb: some of the files we're reporting on may be includes
        #     and hence won't have script ids.
        print "$plugin\n";
        printf "  %-${indent}s  %s\n", "Status:", $status;
        foreach (@info) {
            print "$_\n";
        }
        if ($diffs) {
            print "  Changes:\n";

            # nb: this block comes more or less from diff.pl as supplied 
            #     with Algorithm::Diff.
            foreach my $chunk (@$diffs) {
                foreach my $line (@$chunk) {
                    my($sign, $lineno, $text) = @$line;
                    printf "  %7d$sign %s\n", $lineno+1, $text;
                }
                print "    --------\n";
            }
        }
        print "\n";
    }
}


############################################################################
# Parse new / changed plugins to check for errors.
if ($parse) {
    my %errors;
    foreach my $plugin (keys %new_plugins) {
        next if (grep($plugin eq $_, @ignores));

        next if (
            exists $old_plugins{$plugin} and 
            ($old_plugins{$plugin} eq $new_plugins{$plugin})
        );
        unless ($plugin =~ /\.(inc|nasl)$/) {
            warn "*** unsure if '$plugin' is a NASL script; skipped! ***\n";
            next;
        }
        warn "debug: parsing '$plugin'.\n" if $DEBUG;

        my $cmd = "nasl -p $plugin";
        open(CMD, "$cmd 2>&1 |") or croak "Can't run '$cmd' - $!\n";
        while (<CMD>) {
            warn "debug:   >>$_<<.\n" if $DEBUG;
            $errors{$plugin} .= $_;
        }
        close(CMD);
    }

    if (keys %errors) {
        print "Parse Errors in New / Changed Plugins\n";
        foreach my $plugin (sort keys %errors) {
            print "  $plugin:\n",
                  "    ", join("\n    ", split("\n", $errors{$plugin})), "\n";
        }
    }
    else {
        print "No errors found parsing new / changed plugins.\n";
    }
}


############################################################################
# Clean up.
if ($backup) {
    print "Backup of '$plugins_dir' available as '$archive'.\n";
}
elsif ($summary) {
    unlink $archive;
}
