ear has asked for the wisdom of the Perl Monks concerning the following question:

This is ugly, works, but ugly. I'm sure there has got to be a better way. I'm tracking all my client boxes for MS patches. This script builds an HTML table of all the MS patches and compares to number of installed clients to not installed. The patches are labeled like so: MS03-001, MS03-039, MS00-005, MS01-008, etc etc. The 3rd and 4th characters represent the years. I'm sorting the data by year from 03 - 98 for display and here's how i'm doing it. I'm also padding the installed/not installed numbers with zeros in an ugly way. Please slap me around a bit and correct my ways.
. . . %pad = ('1', '000', '2', '00', '3', '0', '4', ''); @years = qw[05 04 03 02 01 00 99 98]; @keys = sort keys %AllPatches; foreach $year (@years) { foreach $BulletinID (@keys) { next if (substr($BulletinID,2,2) ne $year); next if ($MissingPatchList{$BulletinID}+$FoundPatchList{$Bulle +tinID}==0); if ($counter == 7) { print HTML "</tr><tr>\n"; $counter = 0; } $MissingPatchList{$BulletinID} = "0" if (!$MissingPatchList{$B +ulletinID}); $FoundPatchList{$BulletinID} = "0" if (!$FoundPatchList{$Bulle +tinID}); $MissingPatchList{$BulletinID} = "$pad{(length($MissingPatchLi +st{$BulletinID}))}$MissingPatchList{$BulletinID}"; $FoundPatchList{$BulletinID} = "$pad{(length($FoundPatchList{$ +BulletinID}))}$FoundPatchList{$BulletinID}"; . . .

Replies are listed 'Best First'.
Re: Got's to be a better way
by allolex (Curate) on Nov 05, 2003 at 16:14 UTC
Re: Got's to be a better way
by EvdB (Deacon) on Nov 05, 2003 at 16:09 UTC
    Here are a few pointers to get you on your way:
    • use strict; use warnings
    • Assign values to a hash like this: my %hash = ( key1 => 'val1', key2 => 'val2' );
    • Indent your code - try using perltidy or similar to do this.
    • Don't try to pack too much login into one line: for example:
      $MissingPatchList{$BulletinID} = "$pad{(length($MissingPatchList{$Bull +etinID}))}$MissingPatchList{$BulletinID}"; # better written as: $MissingPatchList{$BulletinID} = $pad{ length( $MissingPatchList{$BulletinID} ) } . $MissingPatchList{$BulletinID};
    • Comments: a few comments serve both to make the code make sense and to break up the flow into nice chunks.
    • This code could (I think) be broken down into subfunctions. This would make it easier to test. You are testing arn't you?
    The whole XP programming thing would love this task. The XP way would be to write a load of tests so that you know the code works and then to start changing the code - testing as you go. As long as the test still work then the code is good.

    You might like this article: http://www.perl.com/pub/a/2003/10/09/refactoring.html.

    --tidiness is the memory loss of environmental mnemonics

Re: Got's to be a better way
by Art_XIV (Hermit) on Nov 05, 2003 at 16:16 UTC

    There's nothing wretchedly wrong with your code that I can see. One this that might make it prettier (and a bit more intuitive) is to park your padding logic in a sub/function similar to the following:

    use strict; while (<DATA>) { chomp; print padded($_), "\n"; } sub padded { #left-pads enough zeros to a number to ensure that there are at leas +t #four digits my ($num) = @_; my $padded = '0' x (4 - length($num)); return $padded . $num; } __DATA__ 23 102 2003 9

    I am now going to make the obligatory statements about using the -w flag and use strict. There.

    Hanlon's Razor - "Never attribute to malice that which can be adequately explained by stupidity"
Re: Got's to be a better way
by Ninthwave (Chaplain) on Nov 05, 2003 at 16:40 UTC

    I am doing the same thing. But what I have done is used a database. I have a table that logs all machines on the network during a scan.

    last_ipaddress------computer_name-----timestamp
    A table that profiles the machine
    computer_name---os_version---service_pack---hotfixes_installed---hotfi +xes_not_intsalled
    And a table that keeps track of hotfixes.
    hot_fix_id---hotfix_name---os---sp_level----hotfix_fingerprint

    The fields Hotfixes_installed, Hotfixes_not_install and hotfix_fingerprint contain xml data. I have found that xml is the best way to store variable width data.

    Than I have perl scripts that get the data for these machines. The first script scans a range of IP addresses and gets the machine name of any connected machine. This updates the first table. Slowly building a list of the machines in the network. The second script connects and uses the Tie::Registry modules to build the second database defining the machines. The third script upacks all the xml files creating a list of files and registry keys to check for each os and service pack. It than connects to each machine in the second table by name, it grabs the fingerprint for that machine and service pack and checks the reg keys and files and returns what is installed and what is not.

    I started with scripts just to hunt machines but after some false positives and false negatives from the Microsoft supplied scanner for MS03-039 I needed to make my own check on if the files installed. We have 14,000 machines to check so I figured keeping a db as a storage mechanism would be more efficient. I have the first two scripts running fine. The fingerprinting of the third script I am trying to make more efficient. But the nature of our network and machines I have found this to best layout to do this. Some notes to get the Tie::Registry information and the file attributes on footprinted file the machine running the scripts has to be the domain admin for the connected machines. I use this to flag machines that are not configured properly.

    We tried Microsoft's Baseline security analyser and it just didn't do what we needed. So we are moving to this. It is my joyful work in progress. And was a handful of individual scripts run by hand to check each of these things one by one text lists of machines. The only problems I have with the design is the fingerprint system needs an entry for each os and an entry for service pack. though I am going to add an sp field that is not just the sp number but 2=>, 3, 2<=. What has become good about this is the database can be used to check other things against our network.

    I don't see it as being easier, but it is the best model for our organisation. My scripts are barely working right now and I don't have it all automated to work on its own. I am trying to get it to be scheduled doing different levels of checks on different days and times. My biggest gripe is getting my head around fork on win32 and should this be one script or multiple scripts that can call each other.

    Now from your question, I would use database queries to output to the html page. I would seperate the queries probably by OS with a sub category of service pack and list fully patched machines, partially patched machines, and completely unpatched machines. With options to display the details further. But that is the benefit of using a database. Also our large number of machines make other storage formats impratical. I was writing this and just released that instead of unpacking the xml files and creating the fingerprint in memory which is impratical, and instead on adding a new hotfix file to the database I could create the needed fingerprint file at that time for each os and service pack.

    My question is how are you determining if the patch is installed? We started by just reading HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\WINNT\CURRENT_VERSION\HOTFIXES or HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\WINNT\UPDATES, but I was never sure if that would be complete enough. So we took versions and file sizes of the files in the updates as a check. I am wondering if there is an easy way of doing this. Is there something already out there I can use instead of having to fight with these scripts I am doing.

      We are actually using a product called HFNetChkPro from Shavlik (http://www.shavlik.com/). The raw data is dumped into a DB already. Unfortunetly, we weren't happy with the reporting capability of the product so I was asked to come up with something from a higher level. So I automated a dump of the DB nightly and then run my script against the dumped data to build a pretty HTML page from the upper ups.
Re: Got's to be a better way
by jasonk (Parson) on Nov 05, 2003 at 16:15 UTC

    There is some stuff here (like $counter) that makes me think there is probably some funky code not shown here, but just working with what you've shown...

    my @years = qw[05 04 03 02 01 00 99 98]; foreach my $year (@years) { foreach my $BulletinID (keys %AllPatches) { next if (substr($BulletinID,2,2) ne $year); next unless ($MissingPatchList{$BulletinID} || $FoundPatchList +{$BulletinID}); if ($counter == 7) { print HTML "</tr><tr>\n"; $counter = 0; } $MissingPatchList{$BulletinID} = sprintf("%04d",$MissingPatchL +ist{$BulletinID} || 0); $FoundPatchList{$BulletinID} = sprintf("%04d",$FoundPatchList{ +$BulletinID} || 0);

    We're not surrounded, we're in a target-rich environment!
      Thanks. I've made use of the sprintf's. I've used printf's before but didn't realize they would pad like that for me. I still wish there was a better way to handle the years for sorting latest to oldest (03 - 98). It seems real silly to loop through all of the data 6 times.