Update Nessus Plugins Script
"christopher ashby" <[email protected]> Sat, 6 Dec 2008 11:10:28 -0500
| Newsgroups | gmane.comp.security.nessus.general |
|---|---|
| Message-ID | <[email protected]> |
List- Has anyone successfully implemented these scripts with Nessus? I currently have Nessus 3.2.1 on a RH Server and use the command line to execute my scans. Here is what happens when I attempt to use these scripts: The 1st script (update-nessus-plugins.pl) executes properly and updates the plugins, creates the MD5, and archives everything according to plan. When this scripts executes the describe-nessus-plugin.pl script, it gives me an error saying that the ""plugin isn't available?? I have execute this in debug mode, and receive the same generic error. If i execute the perl script (describe-nessus-plugin.pl) on it's own, it properly gives me the output? I have modified the update-nessus-plugin.pl script adding/updating the paths (line161 & 183) and then modifying lines 352-355 which call the describe-nessus-plugins.pl script. This modification was made because if i execute the script (describe-nessus-plugins.pl) without any switches it's outcome is exactly what i need. Here is the command i execute in an attempt to get the outcome desired: sudo perl -T /opt/nessus/auto/update-nessus-plugins.pl -b -p -s >> /home/cashby/plugins-report.txt i have also attempted this with the same outcome: sudo perl -T /opt/nessus/auto/update-nessus-plugins.pl -bps >> /home/cashby/plugins-report.txt I have attached the scripts for anyone to view, if anyone has any suggestions that would be appreciated! thanks- Christopher Ashby _______________________________________________ Nessus mailing list [email protected] http://mail.nessus.org/mailman/listinfo/nessus
describe-nessus-plugin.pl
(application/octet-stream, 26.1 KB)
#!/usr/bin/perl # # ---------------------------------------------------------------------- # describe-nessus-plugin # # Written by George A. Theall, [email protected] # # Copyright (c) 2003 - 2005, George A. Theall. All rights reserved. # # This module is free software; you can redistribute it and/or modify # it under the same terms as Perl itself. # # $Id: describe-nessus-plugin 185 2005-09-28 02:38:10Z theall $ # --------------------------------------------------------------------- =head1 NAME describe-nessus-plugin - describes one or more Nessus plugins. =head1 SYNOPSIS # Describe assorted NASL plugins related to MS SQL. describe-nessus-plugin /usr/local/lib/nessus/plugins/mssql*.nasl # Describe assorted NASL plugins related to Zope using # French strings whenever possible. describe-nessus-plugin -l francais /usr/local/lib/nessus/plugins/zope*.nasl # Show how the script parses the specified NASL plugin. describe-nessus-plugin -d wip.nasl # Report CVE ID(s) for all Oracle-related plugins. describe-nessus-plugin -f cve_id /usr/local/lib/nessus/plugins/oracle*.nasl # Same as above but avoid line-wrap. describe-nessus-plugin -f cve_id -w 999 /usr/local/lib/nessus/plugins/oracle*.nasl # Report all information except the description for all Apache-related plugins. describe-nessus-plugin -f _all_ -f '!description' /usr/local/lib/nessus/plugins/apache*.nasl =head1 DESCRIPTION This script prints out assorted descriptive information about each Nessus plugin named on the commandline: id, name, family, category, etc. It works by reading the plugin directly and parsing out the information of interest from the various C<script_*> functions in the its description block. As such, it only works with plugins written in NASL (C<*.nasl>), not NASL include files (C<*.inc>) or plugins written in C (C<*.nes>). It does not require access to a Nessus server but does require read access to the plugin. The decision about what information to report can be controlled either by setting C<@funcs> in this script or by using the option C<--functions> on the commandline. In either case, function names should be specified without the leading C<script_> string; for example, C<cve_id> represents the information supplied as an argument to C<script_cve_id>. The order in which information is reported is controlled by setting C<@func_order> in this script; there is no way to change it via the commandline. Some of the descriptive information is available in languages other than English -- typically French, but occasionally German and Portuguese. You can control the language used by adjusting the variable C<$lang> in the source or with the option C<--language>; if information in the desired language is not available, this script defaults to English, like the NASL interpreter. B<describe-nessus-plugin> is written in Perl. It should work on any system with Perl 5.005 or better. It also requires the following Perl modules: o Carp o Getopt::Long o Text::Balanced o Text::Wrap If your system does not have these modules installed already, visit CPAN (L<http://search.cpan.org/>) for help. Note that C<Text::Balanced> does not work with versions of Perl older than 5.005; further, it is not included with Perl distributions prior to 5.8.0 so you will probably need to install it if you're running an older version of Perl. =head1 KNOWN BUGS AND CAVEATS Currently, I am not aware of any bugs in this script. Understand that this script is not a NASL parser - on one hand, it may not handle some constructs the language allows; on the other, it may accept some the language prohibits. Still, it seems to properly process all the scripts currently available via B<nessus-update-plugins>. Long lines in the output are wrapped and indented to agree with the report format, which sometimes messes up formatting that plugin authors have done. There is a limit to the size of the arguments passed to C<script_cve_id()>, which sets the CVE IDs of the flaws tested by the plugin. Additional CVE IDs, which by convention are listed in comments, are not reported by this script since they can not be reliably identified. 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 read a plugin or to isolate the description part will result in a warning and cause that plugin to be skipped. Failure to identify function argument(s) will result in a warning and cause that function to be skipped. Failure to fully parse the arguments will cause them to be reported as missing. =head1 SEE ALSO L<http://cgi.nessus.org/plugins/>, L<http://www.nessus.org/doc/nasl2_reference.pdf>, L<http://www.tifaware.com/perl/describe-nessus-plugin/>. =cut ############################################################################ # Make sure we have access to the required modules. use 5.005; use strict; use Carp; use Getopt::Long; use Text::Balanced qw(extract_bracketed extract_delimited extract_multiple); use Text::Wrap; ############################################################################ # Initialize variables. $| = 1; my $DEBUG = 0; my %cat_labels = ( # see nessus-core/doc/WARNING.En and # send_plug_info() in # nessus-core/nessusd/comm.c 'ACT_ATTACK' => 'attack', 'ACT_DENIAL' => 'denial', 'ACT_DESTRUCTIVE_ATTACK' => 'destructive_attack', 'ACT_END' => 'unknown', 'ACT_FLOOD' => 'flood', 'ACT_GATHER_INFO' => 'infos', 'ACT_KILL_HOST' => 'kill_host', 'ACT_MIXED_ATTACK' => 'mixed', 'ACT_SCANNER' => 'scanner', 'ACT_SETTINGS' => 'settings', ); my @funcs = ( # functions to report 'id', 'name', 'version', # 'copyright', 'family', 'category', 'risk', 'summary', # 'description', 'cve_id', 'bugtraq_id', 'xref', # 'add_preference', # 'dependencies', # 'exclude_keys', # 'require_keys', # 'require_ports', # 'require_udp_ports', # 'timeout', ); my %func_labels = ( # descriptive labels for functions. 'add_preference' => 'Preference(s)', 'bugtraq_id' => 'BugTraq Id(s)', 'category' => 'Category', 'copyright' => 'Copyright', 'cve_id' => 'CVE Id(s)', 'dependencies' => 'Dependencies', 'description' => 'Description', 'exclude_keys' => 'KB Items That Must Be Unset', 'family' => 'Family', 'id' => 'Id', 'name' => 'Name', 'require_keys' => 'Required KB Items', 'require_ports' => 'Required TCP Ports', 'require_udp_ports' => 'Required UDP Ports', 'risk' => 'Risk Factor', 'summary' => 'Summary', 'timeout' => 'Timeout', 'version' => 'Version', 'xref' => 'X-Reference(s)', ); my @func_order = ( # order of functions in output. 'id', 'name', 'version', 'copyright', 'family', 'category', 'risk', 'summary', 'description', 'cve_id', 'bugtraq_id', 'xref', 'add_preference', 'dependencies', 'require_keys', 'exclude_keys', 'require_ports', 'require_udp_ports', 'timeout', ); my $lang = 'english'; # 'deutsch', 'english', 'francais', or 'portugues' # Subroutine prototypes. sub find_args($;$); sub eval_expr($); ############################################################################ # Process commandline arguments. my %options = ( 'debug' => \$DEBUG, 'language' => \$lang, ); Getopt::Long::Configure('bundling'); GetOptions( \%options, 'debug|d!', 'functions|f=s@', 'help|h|?!', 'language|l=s', 'width|w=i', ) or $options{help} = 1; $0 =~ s/^.+\///; if ($options{help} or @ARGV == 0) { warn "\n", "Usage: $0 [options] file(s)\n", "\n", "Options:\n", " -?, -h, --help Display this help and exit.\n", " -d, --debug Display copious debugging messages.\n", " -f, --functions <funcs> Display information about the functions <funcs>.\n", " Possible values are:\n", " 'add_preference', 'bugtraq_id', 'category',\n", " 'copyright', 'cve_id', 'dependencies',\n", " 'description', 'exclude_keys', 'family', 'id',\n", " 'name', 'require_keys', 'require_ports',\n", " 'require_udp_ports', 'risk', 'summary',\n", " 'timeout', 'version', 'xref', and '_all_'.\n", " Functions prefixed with '!' are ignored.\n", " -l, --language <lang> Use <lang> as language preference; must be one of\n", " 'deutsch', 'english', 'francais', and\n", " 'portugues'.\n", " -w, --width <width> Use <width> as the screen width (for controlling\n", " line wrap).\n"; exit(9); } @funcs = split(/,\s*/, join(',', @{$options{functions}})) if ($options{functions}); ############################################################################ # Validate @funcs. my @badfuncs; foreach (@funcs) { my $func = $_; next if ($func eq '_all_'); $func =~ s/^!//; push(@badfuncs, $_) unless (exists $func_labels{$func}); } croak "Invalid function(s) - '" . join("' & '", @badfuncs) . "'!\n" if (@badfuncs); ############################################################################ # Adjust selected functions if necessary. my @negates = grep(s/^!//, @funcs); @funcs = split(/,/, join(',', keys %func_labels)) if (grep($_ eq '_all_', @funcs)); foreach my $negate (@negates) { @funcs = grep($_ ne $negate, @funcs); } if ($DEBUG) { warn "debug: functions to describe:\n"; foreach (@funcs) { warn "debug: $_\n"; } } ############################################################################ # Iterate over each plugin named on the commandline. my $contents; # nb: scope must be global. $Text::Wrap::columns = $options{width} if (exists $options{width}); $Text::Wrap::unexpand = 0; foreach my $plugin (@ARGV) { # Read contents of plugin into a scalar. warn "debug: reading '$plugin'.\n" if $DEBUG; open(FILE, $plugin) or warn "Can't read '$plugin' - $!\n" and next; { local $/; $contents = <FILE>; } close(FILE); unless ($contents) { warn "*** '$plugin' is empty! ***\n"; next; } unless ($contents =~ /(?s)if\s*\(\s*description\s*\)\s*{.+}/) { warn "*** '$plugin' does not have a description part! ***\n"; next; } # Isolate description part of the plugin. my $desc; # nb: call extract_bracketed in array context to preserve $contents. ($desc) = extract_bracketed( $contents, '{}', '(?s).*?if\s*\(\s*description\s*\)\s*' ); unless ($desc) { if ($@) { warn "*** extract_bracketed returned '$@' when parsing '$plugin'! ***\n"; } else { warn "*** Can't isolate description part of '$plugin'! ***\n"; } next; } # nb: we need to escape any backslashes in $desc to ensure # things like extract_delimited work. $desc =~ s/\\/\\\\/g; # Parse description info. warn "debug: parsing description info.\n" if $DEBUG; my %info; # nb: spurious leading characters are removed since extract_bracketed() # expects string to start with '(' in order to isolate the argument. while ($desc =~ s/^(?s).*?script_(\w+)\s*\(/(/) { # nb: ignore it if part of a comment next if ($& =~ /(?s)#[^\n]*script_\w+\($/); my $func = $1; # Process only functions of interest. $func = 'dependencies' if ($func eq 'dependencie'); next unless ( grep( ( $func eq $_ or # nb: name is used in outputing info about add_preference. ($func eq 'name' and $_ eq 'add_preference') or # nb: risk is embedded in description, not a separate function. ($func eq 'description' and $_ eq 'risk') ), @funcs ) ); warn "debug: function name is '$func'.\n" if $DEBUG; # Identify arguments. my $argstr = extract_bracketed( $desc, '()', ); unless ($argstr and $argstr =~ s/^(?s)\(\s*(.+)\s*\)$/$1/) { warn "*** Can't identify arg string for '$func' in '$plugin'! ***\n"; next; } # nb: unescape any backslashes. $argstr =~ s/\\\\/\\/g; warn "debug: arg string at start is '$argstr'.\n" if $DEBUG; my @args = find_args($argstr); if ($DEBUG) { warn "debug: args:\n"; foreach (@args) { warn "debug: '$_'.\n"; } } # Adjust args for language preferences if applicable. # # nb: according to the NASL 2 reference, named arguments are not # mixed with unnamed ones in the description functions so we # simply replace @args with the result. if (grep(/^$lang\s*:/, @args)) { @args = grep(s/^$lang\s*:\s*//, @args); if ($DEBUG) { warn "debug: args adjusted for language pref ($lang):\n"; foreach (@args) { warn "debug: '$_'\n"; } } } if (grep(/^english\s*:/, @args)) { @args = grep(s/^english\s*:\s*//, @args); if ($DEBUG) { warn "debug: args adjusted for english language:\n"; foreach (@args) { warn "debug: '$_'\n"; } } } # Evaluate / expand arguments. foreach (@args) { $_ = eval_expr($_); } my %named_args = map { /^(?s)(\w+)\s*:\s*(.*)$/ && $1 => $2 } @args; # Handle other named variables. if ($func eq 'add_preference') { if (exists($named_args{name}) and exists($named_args{type}) and exists($named_args{value})) { if ($named_args{type} eq 'radio') { # nb: make radio-type preferences look nicer. $named_args{value} =~ s/;/\n /g; $named_args{value} = "\n " . $named_args{value}; } @args = ( '[' . $named_args{type} . ']:' . $named_args{name} . ' = ' . $named_args{value} ); } else { warn "*** Can't handle arguments for 'add_preference' in '$plugin'! ***\n"; next; } } elsif ($func eq 'xref') { if (exists($named_args{name}) and exists($named_args{value})) { @args = ( $named_args{name} . ':' . $named_args{value} ); } else { warn "*** Can't handle arguments for 'xref' in '$plugin'! ***\n"; next; } } # Regenerate $argstr. $argstr = join(", ", @args); # Fix up $argstr for a few functions. $argstr = $cat_labels{$argstr} if ($func eq 'category' and exists $cat_labels{$argstr}); $argstr =~ s/^\$Revision: (\S+) \$$/$1/i if ($func eq 'version'); # Identify risk if desired. # # nb: this must occur *before* removing newlines. if ($func eq 'description' and grep($_ eq 'risk', @funcs)) { # nb: pattern catches last instance of risk factor and uses # rest of the line, until the last non-whitespace # character (to avoid trailing whitespace). if ($argstr =~ /.+(Risk|Risk\s+factor)\s*:\s*(\S+\s+\/\s+CVSS [^"']+|[^\n]*\S)/is) { $info{risk} = $2; $info{risk} =~ s/\n+/ /s; } } # Removing newlines except those separating paragraphs. $argstr =~ s/^\n*(.*)\n*$/$1/s; $argstr =~ s/\n{3,}/\n\n/sg; $argstr =~ s/(\S)[ \t]*\n(?!(\-|(Synopsis|Description|Note|See also|Solution|Risk(\s*Factor)?)\s*:))(\S)/$1 $5/sig; # Stash $argstr for the report, allowing for multiple occurences. warn "debug: arg string at end is '$argstr'.\n" if $DEBUG; if (exists $info{$func}) { $info{$func} .= ($func eq 'add_preference' ? "\n" : ", ") . $argstr; } else { $info{$func} = $argstr; } } # Print report. # - determine maximal function label width my $width = 0; grep( $width = ( $width < length($func_labels{$_}) ? length($func_labels{$_}) : $width ), @funcs ); # - account for colon. $width++; # - print report. print "$plugin\n"; foreach my $func (@func_order) { next unless (grep($func eq $_, @funcs)); my $label = exists($func_labels{$func}) ? $func_labels{$func} : $_; print wrap( "", " " x ($width+4), sprintf " %-${width}s %s", "$label:", $info{$func} || 'n/a' ), "\n"; } print "\n"; } ############################################################################ # Finds arguments in the string $str delimited by $delim (or a comma if not # specified). Comments and empty lines are ignored. # # Returns an array of arguments or undef on failure. # sub find_args($;$) { confess "syntax error" unless (@_ == 1 or @_ == 2); my $str = shift; my $delim = shift || ','; warn "debug find_args: finding args in '$str' delimited by '$delim'.\n" if $DEBUG; my @args = extract_multiple( $str, [ # - comment(s). sub { if ($_[0] =~ s/^(?s)(\s*\Q$delim\E\s*)?(\s*#.+?(\n|$))+//) { # nb: need to reset \G for Text::Balanced routines. pos($_[0]) = 1; warn "debug find_args: found comment.\n" if $DEBUG; return undef; } }, # - empty line(s). sub { if ($_[0] =~ s/^(?s)(\s*(\n|$))+//) { # nb: need to reset \G for Text::Balanced routines. pos($_[0]) = 1; warn "debug find_args: found empty line(s).\n" if $DEBUG; return undef; } }, # - end of statement. sub { if ($_[0] =~ s/^(?s)\s*;.*$//) { # nb: need to reset \G for Text::Balanced routines. pos($_[0]) = 1; warn "debug find_args: found end of statement.\n" if $DEBUG; return undef; } }, # - function. sub { if ($_[0] =~ s/^(?s)(\s*\Q$delim\E\s*)?(\w+\s*:\s*)?(ereg_replace|raw_string|string)\s*\(\s*/(/) { # nb: $1 is leading garbage, # $2 is named argument, if any # $3 is function name my $item = extract_bracketed($_[0], '('); unless ($item) { warn "*** extract_bracketed returned '$@'! ***\n" if ($@); return ""; } $item = $3 . $item; $item = $2 . $item if ($2); warn "debug find_args: found $3 function '$item'.\n" if $DEBUG; return $item; } }, # - array value. sub { if ($_[0] =~ s/^(?s)(\s*\Q$delim\E\s*)?(\w+\s*:?\s*\w*)\s*\[/[/) { my $item = extract_bracketed($_[0], '['); unless ($item) { warn "*** extract_bracketed returned '$@'! ***\n" if ($@); return ""; } $item = $2 . $item; warn "debug find_args: found bracketed item '$item'.\n" if $DEBUG; return $item; } }, # - quoted item. sub { if ($_[0] =~ s/^(?s)(\s*\Q$delim\E\s*)?(\w+\s*:\s*)?(['"])/$3/) { # nb: $1 is leading garbage, # $2 is named argument, if any # $3 is quote character # nb: extract_delimited by default uses '\' as an # escape when generating Friedl-style optimized # regexes. This will lead to problems if the # string contains something like "c:\". To # get around this, I use an arbitrary value # (ASCII 0) that should never occur in the # string. my $item = extract_delimited($_[0], $3, undef, "\000"); unless ($item) { warn "*** extract_delimited returned '$@'! ***\n" if ($@); return ""; } $item = $2 . $item if ($2); warn "debug find_args: found quoted item '$item'.\n" if $DEBUG; return $item; } }, # - delimited item. sub { if ($_[0] =~ s/^(?s)(\s*\Q$delim\E?\s*)?([^\Q$delim\E;#]+)//) { # nb: need to reset \G for Text::Balanced routines. pos($_[0]) = 1; my $item = $2; $item =~ s/^(?s)\s*(.+?)\s*$/$1/; warn "debug find_args: found delimited item '$item'.\n" if $DEBUG; return $item; } }, ], undef, 0 ); return @args; } ############################################################################ # Evaluates the expression in the scalar $expr, handling quoted values, # string functions, sums, and references. # # Returns an array of arguments or undef on failure. # sub eval_expr($) { confess "syntax error" unless (@_ == 1); my $expr = shift; warn "debug eval_expr: evaluating '$expr'.\n" if $DEBUG; # Strip off name temporarily if it's a named argument. my $name; $name = $1 if ($expr =~ s/^(?s)(\w+)\s*:\s*//); # Evaluate quoted values. if ($expr =~ /^(?s)("[^"]*"|'[^']*')$/) { warn "debug eval_expr: it's a quoted value.\n" if $DEBUG; $expr = $1; # nb: pattern above captures enclosing quotes. $expr =~ s/^(?s)(['"])(.*)\1$/$2/; } # Evaluate string functions. elsif ($expr =~ /^(?s)string\s*\(\s*(.+)\s*\)/) { warn "debug eval_expr: it's a string function.\n" if $DEBUG; my @args; foreach (find_args($1)) { # nb: expand selected escape sequences. s/\\n/\n/gs; s/\\t/\t/gs; push(@args, eval_expr($_)); } $expr = join('', @args); } # Evaluate ereg_replace functions. elsif ($expr =~ /^(?s)ereg_replace\s*\(\s*(.+)\s*\)/) { warn "debug eval_expr: it's an ereg_replace function.\n" if $DEBUG; my @args = find_args($1); my %named_args = map { /^(?s)(\w+)\s*:\s*(.*)$/ && $1 => $2 } @args; if (exists($named_args{pattern}) and exists($named_args{replace}) and exists($named_args{string})) { my $pattern = eval_expr($named_args{pattern}); my $replace = eval_expr($named_args{replace}); my $str = eval_expr($named_args{string}); if (exists($named_args{icase}) and $named_args{icase}) { $str =~ s/\Q$pattern\E/$replace/gsi; } else { $str =~ s/\Q$pattern\E/$replace/gs; } $expr = $str; } } # Evaluate raw_string functions. # # nb: this mirrors its NASL version incompletely - it only converts # integers to ASCII; other args are treated as strings but # otherwise left alone. elsif ($expr =~ /^(?s)raw_string\s*\(\s*(.+)\s*\)/) { warn "debug eval_expr: it's a raw_string function.\n" if $DEBUG; $expr = ''; foreach my $arg (find_args($1)) { $arg = eval_expr($arg); if ($arg =~ /^(\d+|0x[0-9a-f]{2})$/i) { $expr .= pack('C', oct($1)); } else { $expr .= $arg; } } } # Evaluate sums. elsif ($expr =~ /^(?s)\s*("[^"]+"|'[^']+'|\S+)\s*\+\s*\S/) { warn "debug eval_expr: it's a sum.\n" if $DEBUG; my @args; foreach (find_args($expr, '+')) { push(@args, eval_expr($_)); } # nb: evaluate sum depending on whether there are any non-numeric args. if (grep(!/^[\d\.]$/, @args)) { $expr = join('', @args); } else { $expr = 0; foreach (@args) { $expr += $_; } } } # Evaluate references. # # nb: the convoluted regex pattern ensures we use definitions that # aren't commented out and we focus only on the reference. elsif ($expr =~ /\w/ and $contents =~ /(?s)(^|\n)[^#\n]*\b\Q$expr\E\s*=\s*(("[^"]*"|'[^']*'|[^"';]*)+);/) { warn "debug eval_expr: it's a reference.\n" if $DEBUG; $expr = eval_expr($2); } $expr = $name . ':' . $expr if ($name); warn "debug eval_expr: result is '$expr'.\n" if $DEBUG; return $expr; }
update-nessus-plugins.pl
(application/octet-stream, 16 KB)
#!/usr/bin/perl -wT # # ---------------------------------------------------------------------- # update-nessus-plugins # # Written by George A. Theall, [email protected] # # Copyright (c) 2004-2008, George A. Theall. All rights reserved. # # This module is free software; you can redistribute it and/or modify # it under the same terms as Perl itself. # # $Id: update-nessus-plugins 33 2008-05-27 10:40:41Z theall $ # --------------------------------------------------------------------- =head1 NAME update-nessus-plugins - Updates Nessus plugins with various options. =head1 SYNOPSIS # updates nessus plugins. NB: this does nothing more than call # nessus-update-plugins. update-nessus-plugins # creates a backup of old plugins and updates plugins. update-nessus-plugins -b # creates a backup, updates plugins, and prints a summary of # new / changed plugins. update-nessus-plugins -bs # updates plugins and parses new / changed plugins for errors. update-nessus-plugins -p =head1 DESCRIPTION This script updates to the latest set of plugins for Nessus 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-nessus-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-nessus-plugins> is written in Perl and calls B<nessus-update-plugins> to actually update the plugins as well as B<describe-nessus-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<[email protected]> (L<http://list.nessus.org>) and ask for assistance there. If you wish to generate summary reports of new / changed plugins, you also need B<describe-nessus-plugin> version 2.01 or better, which is available from L<http://www.tifaware.com/perl/describe-nessus-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-nessus-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, which was introduced with Nessus version 2.0.7. 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-nessus-plugin>, B<nasl>) will be treated as fatal errors and reported to stderr using C<croak>. Warnings / errors from running B<describe-nessus-plugin> will be reported to stderr using C<warn>. =head1 SEE ALSO L<nessus-update-plugins(5)>, L<http://www.tifaware.com/perl/describe-nessus-plugin/>, L<http://www.tifaware.com/perl/update-nessus-plugins/>. =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:/opt/nessus/bin:/opt/nessus/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-nessus-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 = '/opt/nessus/lib/nessus/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-nessus-plugin ' . "-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: some files don't have a descriptive part. elsif ($plugin !~ /\.(inc|nbin)$/) { warn "*** '$plugin' has an unsupported plugin type! ***\n"; } $indent = 15 unless ($indent); # If plugin was changed, compute diffs. my $diffs; if ($status eq 'changed' and $plugin =~ /\.(inc|nasl)$/ and -T $plugin) { 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}) ); if ($plugin =~ /\.nbin$/ or -B $plugin) { next; } 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; }