#!/usr/bin/perl -w

use strict;
use vars qw($opt_f);
use Getopt::Std;

my $VERSION = 1.01;

=head1 NAME

patcheck.pl - script to compare installed with recommended patches

=head1 DESCRIPTION

Compares the currently installed patches and their versions to the
recommended or security patch list from Sun Microsystems

=head1 README

The script retrieves the currently installed patches and their
versions by using the patchadd -p command.  As a result, this
script must be run as root; alternatively, it could be run as a
non-root user through modification of the patchadd script by
removal of the line that checks the UID.  The currently installed
patches are then compared to a flat file containing the recommended
and security patches.  The flat file must be of the format:

=over 4

=item

patch-version description

=back

The script displays to standard output (which can be re-directed to a file)
the patches with a difference between the installed and the recommended
version.  It also displays those patches that are not installed on the
system.

=head1 PREREQUISITES

strict
vars
Getopt::Std

=head1 AUTHOR

Sean C. DeZurik <sdezurik@bellsouth.net>

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2003, Sean C. DeZurik. All Rights Reserved.

This program is free software; you can redistribute it and/or modify it under
the same terms as Perl.

=begin comment

=pod OSNAMES

Solaris

=pod SCRIPT CATEGORIES

UNIX/System_administration

=end comment

=cut

##### Subroutine Prototypes #####

sub main();
# Arguments: None
# Returns: None
# Side effect: Main subroutine that drives the program execution

sub checkArgs();
# Arguments: None
# Returns: A scalar value containing the name of the input file
# Side effects: Prompts the user to enter the name of the file if it is not
#    provided with the -f option. Exits from the program if the arguments
#    are incorrect or if the filename provided via argument does not exist
#    or cannot be opened. Calls getPatchFilename() to prompt the user.

sub getPatchFilename();
# Arguments: None
# Returns: A scalar value containing the name of the input file
# Side effects: Prompts user to enter the name of the input file. Continues
#    to prompt the user if the file does not exist. Exits the program if the
#    user chooses to quit.

sub getInstalledPatches($);
# Arguments: A scalar value containing the absolute path to the patchadd script
# Returns: A hash (list) containing the name value pairs of the base patch
#    number and the patch version number.

sub comparePatchSets($%);
# Arguments: 1) A scalar with the name of the file containing the numbers and
#    versions of the recommended or security patches
#            2) A hash containing the name value pairs of the base patch
#    number and the patch version number of the patches already installed.
# Returns: None
# Side effects: Compares the currently installed patches and versions with the
#    recommended patches and versions. Calls screenDisplay() to show those
#    patches that are either missing or need to be updated.

sub screenDisplay($$$$);
# Arguments: 1) A scalar with the base patch number
#            2) A scalar with the installed patch version
#            3) A scalar with the recommended patch version
#            4) A scalar with a description of what the patch fixes
# Returns: None
# Side effects: Creates a formatted output and writes it to the standard
#    output, showing the base patch number, the installed patch version, the
#    recommended patch version, and a description of the problem fixed.

sub usage();
# Arguments: None
# Returns: None
# Side effects: Displays help on how to use the program

##### End of subroutine prototypes #####

# Call the subroutine serving as main()
main();

##### main #####

sub main() {
    my %patch = ();  # Hash to hold patches and versions
    my $patchadd = "/usr/sbin/patchadd -p";  # patchadd script
    my $patchFile = "";  # User input recommended patches file

    # See how program invoked and retrieve input filename accordingly
    $patchFile = checkArgs();
    # Populate hash with patches and version currently installed on system
    %patch = getInstalledPatches($patchadd);
    # Find any differences between what is installed and what is recommended
    comparePatchSets($patchFile, \%patch);
}

##### End of main #####

##### Subroutine definitions #####

sub checkArgs() {

    # Determine if there are any arguments given when the program was invoked.
    # If there are arguments, then the program will not be interactive.
    if (@ARGV) {
        # Retrieve the options and their arguments
        getopts('f:');

        # Determine if any argument was given to the option
        if ("$opt_f" ne "" && -f "$opt_f") {
            # Valid filename
            return $opt_f;
        }
        else {
            # No argument given to the option so the program was called
            # incorrectly, or the argument was a file that does not exist.
            usage();
            exit();
        }

    }
    else {
        # No arguments so program is interactive. Call the function to ask the
        # user to enter the full path to the file.
        return getPatchFilename();
    }

} # End of checkArgs

sub getPatchFilename() {
    my $filename = "";  # User input name of file with suggested patches

    # Get user input
    print "Enter the full path to the file with the recommended patches (q to quit):\n";
    chomp($filename = <STDIN>);

    # Make sure the file exists. If it does not, ask the user again but give
    # them a chance to exit by hitting q
    until (-f $filename || $filename eq "q") {
        print "Enter the full path to the file with the recommended patches (q to quit):\n";
        chomp($filename = <STDIN>);
    }

    # Check input and exit if the user chooses q
    if ($filename eq "q") {
        exit;
    }
    else {
        # File exists so return its full path
        return $filename;
    }

} # End of getPatchFilename

sub getInstalledPatches ($) {
    my $patchlist = $_[0];  # Command to list installed patches
    my %list = ();  # Hash for installed patches and their versions

    # Separate each patch installed into base patch and version
    for (`$patchlist`) {
        # patch-version
        /^Patch: (\d*)-(\d*)/;

        # See if patch already in the list. If it is, compare the version and
        # if this version is more recent, replace the older version in the list
        if ($list{$1} && $list{$1} < $2) {
            $list{$1} = $2;
        }
        # Base patch is not in the list so add it
        else {
            $list{$1} = $2;
        }

    }

    # Send the list's values back
    return %list;
} # End of getInstalledPatches

sub comparePatchSets($%) {
    my $file = $_[0];  # File with recommended patches
    my $patchHash = $_[1];  # Flat list of installed patches and versions

    # Open file with recommended patches
    open(PATCHES, "$file") || die "Could not open file for reading";

    # Loop through all recommended patches in the file
    while (<PATCHES>) {
        chomp;
        # Separate into patch, version, and description
        /(\d*)-(\d*)\s*(.*)/;

        # See if the recommended patch is installed. If it is, check version.
        if ($patchHash->{$1} && $patchHash->{$1} < $2) {
            # Recommended patch is newer
            screenDisplay($1, $patchHash->{$1}, $2, $3);
        }
        elsif ($patchHash->{$1} && $patchHash->{$1} >= $2) {
            #print "Patch $1 at $patchHash->{$1} is current with $2\n";
        }
        else {
            # Patch is not present at all on the system
            screenDisplay($1, "Not installed", $2, $3);
        }

    }

    # Close the file handle
    close(PATCHES);
} # End of comparePatchSets

sub screenDisplay($$$$) {
    write(STDOUT);

format STDOUT =
==============================================================================
Patch Number: @<<<<<<<<<<
$_[0]
Installed Version: @<<<<<<<<<<<<<<<
$_[1]
Recommended Version: @<<<<<
$_[2]
Fixes: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$_[3]
==============================================================================
.

} # End of screenDisplay

sub usage() {
    print "USAGE:\n";
    print "patcheck.pl can be used interactively or be given an argument\n";
    print "of a filename so that it can be used in scripts or cron jobs.\n";
    print "SYNTAX:\n";
    print "Interactive Mode: Simply type patcheck-$VERSION.pl and enter the full\n";
    print "   path to the file, when prompted, which contains the recommended\n";
    print "   or security patches from Sun Microsystems.\n";
    print "Batch Mode: patcheck-$VERSION.pl -f <full_path_to_file>\n";
} # End of usage

##### End of subroutine definitions #####
