Date: Fri, 8 Jan 1999 09:56:21 -0500
From: Paul Brunk <[email protected]>
To: [email protected]Subject: Re: Checking for most recent Solaris Security Patches
On Thu, 7 Jan 1999, Ronan Waide wrote:
> On January 6, [email protected] said:
> > Enclosed is a script that checks if your Solaris system has the
> > latest security patches applied.
> Funnily enough... :)
>
> I've a version of a similar program sitting on one of the Solaris
> boxen here for the last few months[...]
Even more hilarious:
W. Joseph Shamblin posted somewhere an excellent program for this about a
year ago. Uses perl, many options. Uses either "patchdiag.xref or"
"Solaris2.x.PatchReport".
--
Tired of the pretence,
Paul Brunk, Workstation Support Droid
"Hungry like the wolf"
#!/usr/local/bin/perl
# This program is intended to parse the patchdiag.xref file downloaded from
# the sunsolve ftp site. It parses the file, and returns a list of patches that
# need to be evalutated for installation.
# @(#) PatchReport 2.8@(#) (Shamblin) 01/24/98 21:54:04
# Copyright (c) 1997 by W. Joseph Shamblin. All rights reserved.
# Permission is granted to reproduce and distribute this program
# with the following restrictions:
# 1) This copyright notice and the author identification below
# must be left intact in the program and in any copies.
# 2) Any modifications to the program must be clearly identified
# in the source file.
#
# UNIX Systems Administrator
# Department of Computer Science
# Duke University, Durham, NC
# Phone: 919.660.6582
# Email: [email protected]
#
#
# THIS SOFTWARE IS PROVIDED AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. YOU ARE RESPONSIBLE
# FOR ANY DAMAGE THIS MIGHT DO TO YOUR MACHINES!!! IN NO EVENT SHALL THE
# AUTHOR OF THIS PROGRAM BE LIABLE FOR DAMAGE THIS PROGRAM CAUSES.
# Load all needed modules
use English;
use MD5;
use Net::FTP;
use FileHandle;
use Getopt::Std;
autoflush STDERR 1;
autoflush STDOUT 1;
getopts('Aa:cdE:e:Ff:g:hiL:N:np:Q:qRrS:s:vX:Z:');
# account name, only users with contract accounts at sunsolve can get
# patchdiag.xref file. The account will be "ID/passwd".
!defined $opt_a
? ($account = "PUT YOUR ACCOUNT HERE")
: ($account = $opt_a );
#version number
$version_number = "2.8";
# pase the options
$usage_message = qq|
USAGE: patchreport [-A] [-a "ID/passwd"] [-cd]
[-E "/path/to/excluded_patches"]
[-e "103594 104117 105408 105616"]
[-Ffghi] [-N "/path/to/recommended_list"]
[-n] [-p "/path/to/patches"] [-Rr]
[-S "/path/to/CHECKSUMS"]
[-s "message"] [-X "/path/to/patchdiag.xref"]
-A Prompt for account information
-a SunSolve "ID/passwd"
-c Prints patches which are current (UP)
-d Debugging option
-E "/path/to/excluded_patches"
-e Exclude patch IDs (e.g. 103594 sendmail patch for sparcs)
-F Force patch installation without any questions
-f Arguments to fastpatch, i.e. -f nsI for -n -s -I ( see fastpatch documentation )
Defaults for fast patch are the following:
-n Never call installpatch (by default, fastpatch will
fall back to installpatch when it can't find package
matches)
-s Save old files so the patch can be backed out.
(Works for new style patches)
-I Ignore backoutpatch failures
(instead, the system state is updated as if the patch
has been backed out)
-g Grace period for shutdown (in seconds)
-h Prints this message and exits
-i Install patches
-L "/path/to/file_with_list_of_patches"
-N "/path/to/recommended_list"
-n No contract support (use Recommended patch list)
-p "/path/to/patches" (default: /var/tmp/patches)
-Q "/path/to/fastpatch"
-q Use Casper Dik's fastpatch program to install patches
-R Remove compressed patches and directories after installation,
but not if uncompressing to a different directory ( -Z option ).
In that case just clean up the uncompressed directory and leave
compressed patch in place.
-r Retrieve patches
-S "/path/to/CHECKSUMS"
-s Shutdown with "Message"
-v Version number
-X "/path/to/patchdiag.xref"
-Z "/path/to/uncompress_patches"\n
|;
# If the program is called with the -h option simply print
# the usage message and exit.
if (defined $opt_h){
print "$usage_message\n";
exit 0;
}
# If the program is called with the -v option simply print
# the version, and the usage message and exit.
if (defined $opt_v) {
print "\n\tPatchReport version $version_number\n";
exit 0;
}
# If we are called with the -i (install option) make sure
# that we have the appropriate permissions
if ((defined $opt_i) and ($> or $< != 0)) {
print "\n You must be root to install patches.";
print "$usage_message\n";
exit 0;
}
# if the account is set to the default assume that we
# need to print an error message asking for an account.
if ($account eq "PUT YOUR ACCOUNT HERE" and !$opt_S and !$opt_X and !$opt_n and !$opt_A ) {
print qq|
You must have a SunSolve account to use this script. The
-n option can be used to by-pass this check, and use the
Recommended patch list instead of the patchdiag.xref file.
You can hard code the account and ID into the script. |;
print "$usage_message\n";
exit 0;
}
# Let's figure out where fastpatch is located if possible. If not just exit.
if (defined $opt_q ) {
if (defined $opt_Q) {
$INSTALL_PATCH_PROG = $opt_Q;
if ( ! -e $INSTALL_PATCH_PROG ) {
print qq|
Fastpatch was not found in that location. Please use -Q to
the specify correct path to the fastpatch program.\n\n|;
exit 1;
}
} elsif ( !defined $opt_Q) {
$INSTALL_PATCH_PROG = `which fastpatch`;
if ($INSTALL_PATCH_PROG =~ /no fastpatch in/) {
print qq|
Fastpatch program not found. Please use -Q to specify
where the fastpatch program is located or add its
directory to your path.\n\n|;
exit 1;
}
}
} else {
$INSTALL_PATCH_PROG = "./installpatch";
}
# When summomed with the -A option the user will be asked
# for the account name and password for SunSolve's FTP site
if ( defined $opt_A) {
print qq|
Please provide the account and password in the form "ID/passwd"
\n\naccount/passwd? |;
chomp($account = <STDIN>);
}
# The -p option allows for the output of the patches downloaded
# to go into another directory. This is good for large sites that
# share a common patch directory.
if (!defined $opt_p) {
$patch_dir = "/var/tmp/patches";
} elsif (defined $opt_p) {
$patch_dir = "$opt_p";
}
# Setp the arguments to fast patch. By default use -n -s -I
# -n Never call installpatch (by default, fastpatch will
# fall back to installpatch when it can't find package
# matches)
# -s Save old files so the patch can be backed out.
# (Works for new style patches)
# This should be optional for PatchReport
#
# -I Ignore backoutpatch failures
# (instead, the system state is updated as if the patch
# has been backed out)
if ( defined $opt_f and defined $opt_q ) {
map { $FAST_PATCH_ARGS = $FAST_PATCH_ARGS . "-$_ " } split(//,$opt_f);
} elsif (defined $opt_q and !defined $opt_f ) {
$FAST_PATCH_ARGS = "-n -s -I";
}
# Get some information about who we are
@uname = split ' ',`uname -a`;
$uname[2] =~ s/^5/2/ or die "can't convert SunOS-Solaris version number";
if ($uname[5] eq "i386") { $os = "$uname[2]_x86";} else { $os = "$uname[2]";}
# Print a nice little message to let the users know
# what is going on when the script first starts up
print qq|\n
Analyzing needed patches on your machine, this might take
a minute or two depending on the options you chose, and/or
your net connection.\n\n|;
################################################
############## File Retrieval ##################
################################################
# If the -X option or the -S options are not used this means that
# the patchdiag.xref file and/or the CHECKSUMS file are not stored
# locally. Since this is the case we have to get the files from the net.
if ((!defined $opt_X or !defined $opt_S) and !$opt_n){
$ftp = Net::FTP->new("sunsolve.sun.com", Debug => $opt_d ? 1 : 0);
$ftp->login("sunsolve","sunmicro","$account") ;#or warn "can't login";
# If the -n option is used then we need to go and get the Recommended
# patch list. We need to do this in regular anonymous mode.
} elsif (defined $opt_n) {
$ftp = Net::FTP->new("sunsolve.sun.com", Debug => $opt_d ? 1 : 0);
$ftp->login() ;#or warn "can't login, bad password perhaps\?";
}
# If the -X option or the option is used this means that the
# patchdiag.xref file are stored locally. Since this is the
# case we do not have to get the files from the net.
if (defined $opt_X and !$opt_n) {
$xref_fd = new FileHandle "$opt_X", "r";
} elsif (!defined $opt_X and !defined $opt_n) {
# -X was not used so we need to get the file from
# Sunsolve's site
$xref_fd = new FileHandle "/tmp/patchdiag_$$", "w+";
$ftp->get("patchdiag.xref", $xref_fd);
} elsif (defined $opt_n and !defined $opt_N) {
# If -n was used, and not -N then we need to retrieve the
# file from the net.
$recommended_fd = new FileHandle "/tmp/Recommended_$$", "w+";
$ftp->cwd("/pub/patches");
$ftp->get("$os\_Recommended.README", $recommended_fd);
} elsif (defined $opt_n and defined $opt_N) {
# if -n is used with -N then that means that the file is
# stored locally. Set the file handle to the argument
# given to -N. This should be the path to the
# Recommended list.
$recommended_fd = new FileHandle "$opt_N", "r";
}
if (!$opt_n) {
# Make sure that we are not working in non-contract mode
# if we are not, and the -S option is defined the
# the path to the checksums file should be the argument
# given to -S
if (defined $opt_S) {
$checksum_fd = new FileHandle "$opt_S", "r";
} else {
$checksum_fd = new FileHandle "/tmp/CHECKSUMS_$$", "w+";
$ftp->get("CHECKSUMS", $checksum_fd);
}
}
# play it again sam
# if the -n option was not used putting us into non-contract mode
# then we likely retreived the files from the net. We have to
# go to the beginning to read the entire contents of the file.
if (!defined $opt_n) {
seek $xref_fd,0,0;
seek $checksum_fd,0,0;
} elsif ($opt_n) {
seek $recommended_fd,0,0;
}
################################################
############ Formatting and parsing ############
############ for the needed patches ############
################################################
format patch_top =
Patch-ID Security Recommended ID Description
--------- -------- ----------- -- ------------------------
.
format patch_out =
@<<<<<<<< @<<<<<<< @<<<<<<<<<< @< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
"$x_id-$x_rev", $security, $recommended, $showrev{$x_id}, $x_desc
.
$^="patch_top";
$~="patch_out";
# We need to get the current patches on the machine. the command showrev -p
# will get the desired information. The map function will take every occurance
# found in the showrev -p and preform the block operation on it. In this case
# the operation is to double split the output, and then create an associative
# array.
map {($s_id,$s_rev) = split '-',(split)[1];$showrev{$s_id} = $s_rev} `showrev -p|sort`;
# Show patches taht apply to add-on programs like Disksuite and veritas if
# we are called with the -o option
# If we are in contract mode then we will need to take the output of the
# patchdiag.xref file. We split the file, and then test to see if we have
# the patch-id from the showrev -p array. We also do a little formatting
# depending on whether or not the file is recommended or a security patch
# or both. We also make use of the write function, to keep things formatted
# nicely.
if (!defined $opt_n) {
while(<$xref_fd>){
map {($x_id,$x_rev,$x_rec,$x_sec,$x_os,$x_arch,$x_desc) = (split(/\|/,$_))[0,1,3,4,7,8,10]} $_;
if ((!defined $showrev{$x_id} or $showrev{$x_id} < $x_rev)
and ($x_arch =~ /$uname[5]\;|$uname[5]\.$uname[4]\;|all\;$uname[5]\;|all\;/)
and ($x_os eq "$os")) {
if ($x_rec eq "R") { local $recommended = "Recommended ";} else {local $recommended = " N/A ";}
if ($x_sec eq "S") { local $security = "Security ";} else {local $security = " N/A ";}
push @needed, "$x_id-$x_rev";
$patch_description{"$x_id-$x_rev"} = "$x_desc";
write;
}
# if we get a hit then that means we are current. So we should
# print up in the patch revision place.
elsif ((defined $showrev{$x_id} or $showrev{$x_id} = $x_rev)
and ($x_arch =~ /$uname[5]\;|$uname[5]\.$uname[4]\;|all\;$uname[5]\;|all\;/)
and ($x_os eq "$os") and defined $opt_c) {
if ($x_rec eq "R") { local $recommended = "Recommended ";} else {local $recommended = " N/A ";}
if ($x_sec eq "S") { local $security = "Security ";} else {local $security = " N/A ";}
$showrev{$x_id} = "UP";
write;
}
}
# If we are using the -n non-contract mode then don't do too much.
# just parse the file, and get the basics like the patch-id
} elsif (defined $opt_n) {
while (<$recommended_fd>) {
($x_id,$x_rev) = map{split '-',(split)[0]} grep /^\d{6}/,$_ or next;
($junk,@x_desc) = split;
if (!$showrev{$x_id} or $showrev{$x_id} < $x_rev) {
push @needed, "$x_id-$x_rev";
$security = ""; $recommended = "";
$x_desc = join ' ', @x_desc;
$patch_description{"$x_id-$x_rev"} = $x_desc;
write;
}
# if we get a hit then that means we are current. So we should
# print up in the patch revision place.
elsif ((defined $showrev{$x_id} or $showrev{$x_id} = $x_rev) and defined $opt_c) {
$showrev{$x_id} = "UP";
write;
}
}
}
################################################
############## MD5 checksum test ###############
################################################
# Now, if we weren't run with the -n option, we parse the checksums
# file making an array of the values of patch-id to the actual
# MD5 checksum as calculated by Sun. We need to go into the
# multiline mode so we set the record separator (RS).
if (!$opt_n) {
$RS='';
map {($patch_checksum_id) = /^(\d{6}-\d{2}).tar.Z/m;
($patch_checksum) = /MD5: (.*)/;
if ($patch_checksum_id ne "") {
$actual_checksum{$patch_checksum_id} = $patch_checksum;
}
} <$checksum_fd>;
$RS="\n";
}
format get_top =
Patch-ID Checksum status Description
--------- ------------------ --------------------
.
format get_out =
@<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$get_status, $checksum_status,$patch_description{$_}
.
# If we are called with the -r switch get the patches, and check the checksums
# for each file we will calculate our own checksums, and compare then. If they
# match then they can be installed. If not print an error message. This will
# be done for all of the needed patches from, as determined from above.
$md5 = new MD5;
if (defined @needed and defined $opt_r) {
mkdir "$patch_dir",0755;
print "\n**Retrieving Patches**\n";
print "Patch-ID Checksum status Description\n--------- ------------------ --------------------\n";
$^ = "get_top";
$~ = "get_out";
map {
$get_status = "$_\t";
$ftp->binary;
$~ = "get_out";
$ftp->get("$_.tar.Z","$patch_dir/$_.tar.Z");
if (!defined $opt_n) {
$subject = new FileHandle "$patch_dir/$_.tar.Z";
if (!defined $opt_n) {
$md5->reset();
$md5->addfile($subject);
$retrieved_checksum = $md5->hexdigest();
$calculated_checksum{$_} = $retrieved_checksum;
($actual_checksum{$_} eq "$retrieved_checksum")
? ($checksum_status = "checksum match")
: ($checksum_status = "*CHECKSUM FAILED*");
}
}
write
} @needed;
# We also need to check the checksum if the file is stored on
# a local file system, hence called without the -r option.
} elsif (defined @needed and defined $opt_i and !defined $opt_r) {
map {
$subject = new FileHandle "$patch_dir/$_.tar.Z";
if (!defined $opt_n) {
$md5->reset();
$md5->addfile($subject);
$retrieved_checksum = $md5->hexdigest();
$calculated_checksum{$_} = $retrieved_checksum;
($actual_checksum{$_} eq "$retrieved_checksum")
? ($checksum_status = "checksum match")
: ($checksum_status = "*CHECKSUM FAILED*");
}
} @needed;
}
# If we do not need patches, then you are pretty up on things.
# print a nice message.
if (!defined @needed) {
print qq|
Congratulations you do not need any patches installed.
Send this note to your boss, and ask for a raise!!!
|;
}
# This avoids an error if the ftp module was never opened.
if (!defined $opt_X or !defined $opt_S){
$ftp->quit;
}
# Clean up the files that were downloaded from the net.
if (!defined $opt_n) {
unlink "/tmp/patchdiag_$$", "/tmp/CHECKSUMS_$$";
} else {
unlink "/tmp/Recommended_$$";
}
format install_top =
Patch-ID Install status Description
--------- --------------- --------------------
.
format install_out =
@<<<<<<<<<< @<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$installing ,$patch_install_status, $patch_description{$patch_to_install}
.
################################################
############# Patch installation ###############
################################################
if (defined @needed and defined $opt_i and !defined $opt_n) {
&question_patch_install_sub;
} elsif (defined $opt_n and defined $opt_i) {
&question_patch_install_sub;
}
################################################
############# Shutdown message #################
################################################
if ( defined $opt_s) {
print "\n\n**SHUTTING DOWN WITH MESSAGE: $opt_s\n\n";
`/usr/sbin/shutdown -y -g$opt_g -i6 "$opt_s" &`;
}
sub question_patch_install_sub {
if (!defined $opt_F) {
# Print an ominous message to let the user know this might
# be a bad idea. If they still want to do it, they probably
# know what they are doing
print qq|
** Installing all patches without checking them first **
** can have negative consequences. I am assuming that **
** you know this, and think that all of these patches **
** are a good idea. Using the -F option will turn off **
** this message. **
\n|;
if (!defined $opt_L) {
# Get a confirmation or a list of patches to install
print "Which patches do you want to install (all/none/list of patches) ";
chomp(local $answer_install_patch = <STDIN>);
if ( $answer_install_patch eq "all") {
&install_patch_sub;
} elsif ($answer_install_patch eq "none") {
print "\n\tExiting install procedure\n";
exit 0;
} elsif ($answer_install_patch =~ /^10/) {
@needed = split ' ',$answer_install_patch;
&install_patch_sub;
} else {
print "\n\tCan't determine answer, aborting installation procedure\n";
exit 0;
}
}
elsif ( defined $opt_L) {
print "Would you like to install all patches listed in $opt_L? (yes/no)\n";
chomp(local $answer_install_patch = <STDIN>);
if ($answer_install_patch =~ "n") {
print "\n\tExiting install procedure\n";
exit 0;
}
elsif ($answer_install_patch =~ "y") {
&install_patch_sub;
}
}
# If called with the -F flag then skip the formality, and just install
# the patches.
} elsif (defined $opt_F) {
&install_patch_sub;
}
}
sub install_patch_sub {
if (defined $opt_q ) {
print "\n\n**Installing patches with fastpatch**\n";
}
else {
print "\n**Installing Patches (this can take a while)**\n";
}
if (!defined $opt_q ) {
print "\nPatch-ID Install status Description\n";
print "--------- --------------- -------------------- \n";
}
# for all of the patches left in the @needed array we do a regulat old
# installation. Just uncompress the patch, cd into the directory and run
# the installpatch program. Also check the return code of the installpatch
# program. If the return code is something other than 0 then grep the error
# code from the installpatch program and print it on the install status
# column of the output.
$^ = "install_top";
$~ = "install_out";
if (defined $opt_E) {
$excluded_patches_fd = new FileHandle "$opt_E", "r";
chomp(@excluded_patches = <$excluded_patches_fd>);
}
if (defined $opt_e) {
push @excluded_patches,split ' ',$opt_e;
}
if (defined $opt_L) {
$needed_patches_fd = new FileHandle "$opt_L", "r";
chomp(@needed = <$needed_patches_fd>);
print "\n\nInstalling patches listed in $opt_L\n\n";
}
foreach $patch_to_install (@needed) {
# Make sure that we do not have any white space in the patch-id from the
# possible input on the command line.
$patch_to_install =~ s/\s//g;
$skip_this_patch = 0;
if (defined $opt_e or defined $opt_E) {
map { (substr($patch_to_install,0,6) eq substr($_,0,6)) ? $skip_this_patch = 1 : ""} @excluded_patches;
}
if (($actual_checksum{$patch_to_install} eq $calculated_checksum{$patch_to_install}) and
($patch_to_install ne "") and ($skip_this_patch != 1)) {
if (defined $opt_Z) {
chdir "$opt_Z";
`/usr/bin/uncompress < $patch_dir/$patch_to_install.tar.Z | /bin/tar xf -`;
chdir "$opt_Z/$patch_to_install";
}
else {
chdir "$patch_dir";
`/usr/bin/uncompress < $patch_to_install.tar.Z | /bin/tar xf -`;
chdir "$patch_dir/$patch_to_install";
}
$installing = "$patch_to_install";
if (defined $opt_q and defined $opt_Z) {
exec `$INSTALL_PATCH_PROG -p $opt_Z $FAST_PATCH_ARGS $patch_to_install`;
} elsif (defined $opt_q and !defined $opt_Z ) {
exec `$INSTALL_PATCH_PROG -p $patch_dir $FAST_PATCH_ARGS $patch_to_install`;
} elsif (!defined $opt_q) {
`$INSTALL_PATCH_PROG .`;
}
if ($? != 0 and !defined $opt_q){
$error = $?/256;
$installpatch_fd = new FileHandle "./installpatch", "r";
map { $patch_install_status = "$1" if /\#\t\t$error\t(.*)/} <$installpatch_fd>;
}
elsif ($? != 0 and defined $opt_q) {
$patch_install_status = "*NOT INSTALLED*";
} else {
$patch_install_status = "Patch installed\t";
if (defined $opt_R and !defined $opt_Z) {
chdir "$patch_dir";
`rm -rf $patch_to_install`;
unlink "$patch_to_install.tar.Z";
}
elsif (defined $opt_R and defined $opt_Z) {
chdir "$opt_Z";
`rm -rf $patch_to_install`;
}
}
} elsif ($actual_checksum{$patch_to_install} ne $calculated_checksum{$patch_to_install}
and ($skip_this_patch != 1) ) {
$patch_install_status = "*NOT INSTALLED*"; $installing = "$patch_to_install";
}
elsif ($skip_this_patch == 1) {
$patch_install_status = "*EXCLUDED PATCH*"; $installing = "$patch_to_install";
}
if (defined $opt_q and $skip_this_patch != 1){
print "Fastpatch messages for $patch_to_install:\n-----------------------------------------\n\n";
write;
}
elsif (!defined $opt_q) {
write;
}
}
}