#!/usr/bin/perl -w # # rh_advisory_update # # AUTHOR: # Dan Harkless # # COPYRIGHT: # This file is Copyright (C) 2008 by Dan Harkless, and is released under the # GNU General Public License . # # USAGE: # % rh_advisory_update [-f] [-m] [-r] [-R ] [-s] [-u ] [-x] # # EXAMPLES: # host-user> su # Password: # host-root> cd /usr/local/Red_Hat_RPMs # host-root> rh_advisory_update < ~user/Mail/inbox/28 # # host-user> cd ~/Red_Hat_RPMs # host-user> show -noshowproc 15 | rh_advisory_update -fmsx # host-user> su # Password: # host-root> rh_advisory_update -rsx < ~user/Mail/inbox/15 # # DESCRIPTION: # Takes a Red Hat Security Advisory or Bug Fix Advisory email (as delivered to # the redhat-watch-list and Bugtraq mailing lists) on stdin, downloads the # RPMs specified therein that are appropriate for the host's Red Hat release # and architecture, compares the MD5 sums given in the email to those of the # downloaded files, compares embedded checksums and signatures with 'rpm # --checksig', and then installs the RPMs. # # By default, all of these steps are performed, but if you specify some # combination of -f, -m, and -r, only the specified steps will be performed. # # If an error occurs at any point (e.g. when comparing a checksum), # rh_advisory_update will abort. Also, the script is written in such a way # that malicious emails pretending to be Red Hat advisories aren't able to # cause arbitrary commands to be run, or RPMs to be downloaded from malicious # servers. Finally, RPMs that are not GPG-signed will not be installed. # # Note that in the case of a kernel update advisory, rh_advisory_update will # download and install all variants of the kernel (i.e. including "-bigmem" # and "-smp"), which generally won't be what you want. You may wish to use # -fm to have rh_advisory_update just fetch and md5sum the kernels, and then # manually --checksig and install just the appropriate variant(s) yourself. # When I get a chance, I'll add an option to prevent rh_advisory_update from # downloading variants of the kernel not present on your system. # # COMMANDLINE OPTIONS: # -f # Use 'wget -N' to fetch the RPMs whose URLs appear in the advisory. # # -m # Use 'md5sum' to compare the MD5 sums given in the advisory vs. the # downloaded files. # # -r # Use 'rpm --checksig' to check the embedded checksums and signature, and # then install the RPMs. # # -R # By default, when we install the RPMs, we use "-Fvh". To use a different # set of options, specify them with -R. In some cases, for instance, you # might want to use "-Uvh" or "-ivh" instead. The option list can include # spaces (be sure to put it in quotes in that case). # # -s # By default, SRPMs mentioned in the advisory are ignored. To download and # install them as well, use -s. # # -u # By default, we download RPMs from the master updates location, # . If the server is too busy (as it often is # just after a major security advisory), you can specify the URL of a mirror # site to download from instead. The list of mirrors is at # . A '/' at the end of the URL # is optional, but the protocol ("ftp://") at the beginning is not. # # -x # By default, we download all RPMs into the current directory, not retaining # the directory structure from the FTP site. To retain that structure, use # -x, which causes us to use wget's -x, -nH, and --cut-dirs options (we'll # create a directory hierarchy starting with the Red Hat release version # number, not starting with the FTP server name as would be the default with # just 'wget -x'). # # TO DO: # Update to work with RHEL and Fedora Legacy Update Advisories. # # DATE MODIFICATION # ========== ================================================================== # 2008-09-02 "use English qw(-no_match_vars)": avoid regex performance penalty. # 2006-04-17 Variables that may contain regexp metacharacters need to be # surrounded by \Q and \E when appearing in regexps. # 2004-02-10 Added to-do note about Fedora Legacy Update Advisories. # 2003-09-22 Changed the default install options from -Uvh to -Fvh to prevent # unintended installs (e.g. in cases where Red Hat RPMs have been # uninstalled in favor of newer versions of packages compiled from # source). Note that no output after "Installing RPMs:" indicates # that all of the RPMs either had no version previously installed, # or an updated version already installed, so nothing was done. # 2003-08-09 Improved the EXAMPLES section. # 2003-08-09 'rpm --checksig NONEXISTENT.rpm' fails silently -- check for # nonexistent files ourself before trying to call it. # 2003-08-09 Yesterday Red Hat updated up2date to not install RPMs that aren't # GPG-signed. These are less of a concern for us, since the # malicious party would have to both break into updates.redhat.com # _and_ send a fake advisory email to redhat-watch-list subscribers, # but it's still a case worth checking for, so now we do as well. # 2003-06-27 The URL-matching regexp wasn't looking for 'noarch' RPMs. # 2003-05-14 Original. ## Modules used ################################################################ use English qw(-no_match_vars); # allow use of names like @ARG rather than @_ use Getopt::Std; # for getopts() use File::Basename; # for basename() ## Subroutines ################################################################# sub my_die { print STDERR "$progname: @ARG\n"; die; } ## Main ######################################################################## $progname = basename($PROGRAM_NAME); use vars qw($opt_f $opt_m $opt_r $opt_R $opt_s $opt_u $opt_x); # kill warnings if (!getopts("fmrR:su:x")) { print STDERR "Usage: $progname" . " [-f] [-m] [-r] [-R ] [-s] [-u ] [-x]\n"; exit 1; } $release_file = "/etc/redhat-release"; if (not open(RELEASE_FILE, $release_file)) { my_die "$release_file: $OS_ERROR."; } $release_line = ; if ($release_line !~ /release (([\d]\.)*[\d]) /) { my_die "$release_file not in the expected format."; } $release = $1; close RELEASE_FILE; $master_update_root = "ftp://updates.redhat.com/"; if (defined $opt_u) { $update_root = $opt_u; if ($opt_u !~ m) { $update_root .= "/"; # tack on a "/", if the user didn't type one } } else { $update_root = $master_update_root; } $architecture = `uname -m`; if ($architecture =~ /^i\d86$/) { $arch_re = "i\\d86"; } else { $arch_re = "$architecture"; } $arch_re .= "|noarch|SRPMS"; while () { if (m<^(\Q$master_update_root\E)(\Q$release\E/.*?/($arch_re)/.*?([^\s/]+))$> ){ # URL line. $rpm_with_path = $2; $rpm = $4; if ($rpm_with_path !~ m or $opt_s) { if ($opt_x) { $key = $rpm_with_path; } else { $key = $rpm; } if (defined $MD5{$key}) { my_die "\"$key\" appears more than once in the URL section."; } if ($rpm =~ /(.*)\.(i\d86)\.rpm$/) { # Make sure we get the appropriate RPM for this particular # version of the Intel x86 architecture. $rpm_root = $1; $rpm_arch = $2; if ($rpm_arch le $architecture and (not defined $highest_rpm_arch{$rpm_root} or $rpm_arch gt $highest_rpm_arch{$rpm_root})) { # This is the highest x86 architecture version of this RPM # we've seen so far. if (defined $highest_rpm_arch{$rpm_root}) { # We had previously seen a lower x86 architecture # version -- forget about it. ($old_key = $key) =~ s/$rpm_arch/$highest_rpm_arch{$rpm_root}/g; ($old_rpm_with_path = $rpm_with_path) =~ s/$rpm_arch/$highest_rpm_arch{$rpm_root}/g; delete $MD5{$old_key}; delete $URLs{"$update_root$old_rpm_with_path"}; } $highest_rpm_arch{$rpm_root} = $rpm_arch; $MD5{$key} = ""; $URLs{"$update_root$rpm_with_path"} = 1; } } else { # Non-x86 architecture case. $MD5{$key} = ""; $URLs{"$update_root$rpm_with_path"} = 1; } } } elsif (m<^([a-f0-9]{32}) (\Q$release\E/.*?/($arch_re)/.*?([^\s/]+))$>) { # MD5 hash line. $md5 = $1; $rpm_with_path = $2; $rpm = $4; if ($rpm_with_path !~ m or $opt_s) { if ($opt_x) { $key = $rpm_with_path; } else { $key = $rpm; } if ($rpm =~ /(.*)\.(i\d86)\.rpm$/) { if ($2 eq $highest_rpm_arch{$1}) { $MD5{$key} = $md5; } } elsif (not defined $MD5{$key}) { my_die "\"$rpm_with_path\" specified in the MD5 sum section but" . " not the URLs section."; } else { $MD5{$key} = $md5; } } } } if ($opt_f or not ($opt_m or $opt_r)) { print "Fetching RPMs:\n"; @fetch_command = ("wget", "-N"); if ($opt_x) { push @fetch_command, "-x"; push @fetch_command, "-nH"; ($directories_above_OS_version = $update_root) =~ s<^[^/]+//[^/]+/><>g; $directories_above_OS_version =~ s<[^/]><>g; push @fetch_command, "--cut-dirs=" . length($directories_above_OS_version); } @URLs_list = sort keys %URLs; if (system(@fetch_command, @URLs_list) != 0) { print "\n"; my_die "\"@fetch_command @URLs_list\" returned nonzero status."; } $did_a_previous_phase = 1; } if ($opt_m or not ($opt_f or $opt_r)) { print "\n" if $did_a_previous_phase; print "Doing 'md5sum' checks:\n"; foreach $rpm (sort keys %MD5) { if ($MD5{$rpm} eq "") { my_die "\"$rpm\" specified in the URLs section but" . " not the MD5 sum section."; } open MD5SUM, "|md5sum --check" or die; print MD5SUM "$MD5{$rpm} $rpm\n"; close MD5SUM; die if $CHILD_ERROR != 0; } $did_a_previous_phase = 1; } if ($opt_r or not ($opt_f or $opt_m)) { print "\n" if $did_a_previous_phase; print "Doing 'rpm --checksig' checks:\n"; foreach $rpm (sort keys %MD5) { if (not -e $rpm) { # 'rpm --checksig NONEXISTENT.rpm' fails silently; do our own check. my_die "$rpm: $OS_ERROR."; } die unless defined($child_pid = open(CHILD, "-|")); if ($child_pid) { # We're the parent. $saw_expected_signature_line = 0; while () { print; if (/^$rpm: /) { $saw_expected_signature_line = 1; if (not /gpg|GPG/) { my_die "$rpm: No GPG signature."; } } } close CHILD; die if $CHILD_ERROR != 0; if (not $saw_expected_signature_line) { # Protect against changed or unexpected 'rpm' output. my_die "Didn't see expected \"$rpm: ...\" line."; } } else { # We're the child. exec("rpm", "--checksig", $rpm) or die; } } print "\nInstalling RPMs:\n"; if ($opt_R) { @rpm_opts = split / /, $opt_R; } else { @rpm_opts = ("-Fvh"); } die unless system("rpm", @rpm_opts, sort keys %MD5) == 0; }