Bugtraq mailing list archives

Re: Checking for most recent Solaris Security Patches


From: pbrunk () arches uga edu (Paul Brunk)
Date: Fri, 8 Jan 1999 09:56:21 -0500


On Thu, 7 Jan 1999, Ronan Waide wrote:

On January 6, spamhater () GRYMOIRE COM 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: wjs () cs duke edu
#
#
# 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;
    }
  }
}



Current thread: