#!C:\Perl\bin\perl.exe -w # Hugh Brown # April 15, 2006 # # Windflower: A Small but Useful(tm) Perl wrapper around the Microsoft # Security Baseline Analyzer that *should* allow remote application of Windows # patches. # Copyright (C) 2006 Hugh Brown # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. # # Version 0.3 # First off: Windflower owes a HUGE debt to # Ivy (http://opensource.w2k.vt.edu/ivy.php) and Daisy # (http://vtntug.w2k.vt.edu/daisy.htm), from Marc DeBonis at the Virginia Tech Microsoft # Implementation Group. Not only the ideas behind Windflower but the very # idea that this sort of thing is *possible* come from those two programs. I'm very grateful # to Mark and the VTMIG for not only writing those programs but releasing them as Free # Software. Thank you. # Okay, I've managed to get pretty damn far with this. Here's what this'll do so far: # # Scan the *local* machine (a la Daisy) # Pick out which hotfixes have not been applied # Grab the URL from the XML output # Change that into something similar to what Daisy/Ivy would fetch from a repository (modulo case) # Fetch from a web server (though FTP code is in here as well, and should work just fine with some editing # Apply the patches, BUT: with the assumption that the silent switches are always "/quiet /passive /norestart" # Reboot the machine, BUT: using the present-on-XP/only-in-the-resource-kit-for-2k shutdown command # *AND* do it *ALL OVER SSH*. # # Requirements: # # ActiveState's Perl (Cygwin Perl might work, too...) # Web server # Microsoft Baseline Security Analyzer # Cygwin with SSH running (not strictly necessary for local scanning, but very handy for doing this remotely) # # Still needs fixing: # # Trim down use statements # Make .cls file, like Ivy does, that contains checksum + silent # switches. Possibly include a switch that tells Windflower not to # run it and includes a comment -- this'd help remind you (well, me) # why a particular patch is not included. # Better handling of output from MBSA # Better logging: syslog, email... # So much cleaning up to do, including leftover files # Actual checking for failure (download, MBSA failure, presence of MBSA...) # Option to scan other machines? Now this is where things are going to # get tricky. Near as I can tell, msbacli does *not*, by default, # have the same ability that HFNetfix did: to scan another machine # *and* list the *filename* needed. What mbsa *will* do is either: # scan another machine and say that hotfix such-and-such is needed, # *or* scan *localhost only* (ie, the machine mbsa is running on) and # say "Here's a URL to all the missing stuff". What this means, of # course, is that either you get each machine to run mbsa (not a bad # idea, maybe) or you do something like "wget # ftp://whatever/*$hotfix*.exe" -- which is just stupid. Hm hm hm. # Alternatively, turn this back into Daisy, not Ivy -- Pull, not Push. # Note that patch names are canonicalized to lower-case; I've used # Apache's mod_rewrite to deal with this, and made symlinks of # APatchNamedLikeThis to apatchnamedlikethis. This may well be a very # stupid way of doing things. # Note that on XP, MBSA must be run at least once before running it # through Windflower; it seems to do some sort of initialization. # Otherwise, MBSA (and therefore Windflower) just hangs; there's an # error in C:\Windows\WindowsUpdate.log that looks like "MBSA Failure # Software Synchronization Error: Agent failed detecting with reason: # 0x8024001a". Just running "mbsacli.exe" without any arguments seems # to do the trick -- even remotely via SSH, and even interrupting it # before it's finished. Running mbsa in this way on the desktop, or # running the GUI on the desktop, seem to do the trick as well. # May not need half of this... use strict; use Getopt::Std; use Win32::NetResource; use Win32::NetAdmin; use Win32::EventLog; use Win32::TieRegistry; use Win32::Service; use Net::hostent; use Net::Ping; use Net::FTP; use LWP::Simple; use Socket; use XML::Simple; use IO::File; use Data::Dumper; use MD5; sub usage { print < $report_file"); $fh = new IO::File("$report_file"); $report = XMLin($fh); if ($option{d}) { print Dumper($report); exit; } # Hash or array? If there's only one instance of "Check", it's a hash # and we need to treat it like one -- otherwise, Perl will complain and die # that we're trying to treat a hash ref like an array. # We check for hash/array by treating it like an array and seeing if it works. eval { print $#{$report->{'Check'}} }; if ($@ ne "") { # It's a hash $j = $report->{'Check'}; &logme ("$j->{'Advice'}\n"); if ($j->{'Advice'} !~ /^No .*are missing\.$/i) { push (@missing, grab_fixes ($j)); } else { &logme ("Could it be that everything's fine?\n"); } } else { foreach $i (0..$#{$report->{'Check'}}) { $j = $report->{'Check'}[$i]; &logme ("$j->{'Advice'}\n"); if ($j->{'Advice'} !~ /^No .*are missing\.$/i) { push (@missing, grab_fixes ($j)); } else { &logme ("Could it be that everything's fine?\n"); } } } if ($#missing >= 0) { foreach $i (0..$#missing) { $patch = $missing[$i]; &logme ("I want to apply $patch\n"); $patch =~ s#.*/##; $patch =~ s/_[\dabcdef]+\.exe/\.exe/; # Handwave unless (fetch_from_repository($patch)) { warn ("Can't find $patch on repository -- not approved? (You could try downloading that at $missing[$i]) "); next; } &stub_function("validate_download", $patch); $quiet_options = get_quiet_options($patch); &install_patch ($patch, $quiet_options); } &do_shutdown; } else { print "Everything's fine. See you next time!\n"; } exit; sub stub_function { my ($stub_function) = (@_); &logme ("FIXME: Need to write $stub_function\n"); } sub fetch_from_repository { my $hotfix = shift; my $cls = $hotfix; $cls =~ s/exe$/cls/; my $error; &logme ("You're looking for $hotfix\n"); &logme ("I'm going to try downloading ${repository}/${hotfix}\n"); # $ftp = Net::FTP->new($repository, Debug => 0) or die ("Cannot connect to $repository: $@"); # $ftp->login($ftp_login, $ftp_password) or die ("Cannot login: ", $ftp->message); # $ftp->cwd($repository_directory) or die ("Cannot get to $repository_directory: ", $ftp->message); # $ftp->binary and $ftp->get($hotfix) or (warn ("I can't download $hotfix: ", $ftp->message) && return -1); # $ftp->quit; if ($option{n}) { print "I'd really like to fetch ${repository}/${hotfix} right now.\n"; } else { $error = getstore ("${repository}/${hotfix}", "$hotfix"); &logme ("Fetch of $hotfix produced $error\n"); $error = getstore ("${repository}/${cls}", "$cls"); &logme ("Fetch of $cls produced $error\n"); } # This isn't working right now...not sure why. #return -1 if is_error($error); #return 0; } sub get_quiet_options { my $hotfix = shift; my $cls = $hotfix; $cls =~ s/exe$/cls/; open (IN, $cls) or warn ("Couldn't open $cls for reading: $!"); my $quiet_options = ; close (IN) or warn ("Couldn't close $cls nicely: $!"); return $quiet_options; } sub logme { print $_ if ($option{v}); } sub grab_fixes { my $j = shift; my @missing; my $url; foreach my $k (0..$#{$j->{'Detail'}->{'UpdateData'}}) { next if ($j->{'Detail'}->{'UpdateData'}[$k]{'IsInstalled'} eq 'true'); &logme ("You need to install $j->{'Detail'}->{'UpdateData'}[$k]{'Title'}\n"); &logme ("I would probably download that from $j->{'Detail'}->{'UpdateData'}[$k]{'References'}{'DownloadURL'}\n"); $url = $j->{'Detail'}->{'UpdateData'}[$k]{'References'}{'DownloadURL'}; push @missing, $url; } return @missing; } sub do_shutdown { # FIXME: See note above about depending on the presence of the Windows' shutdown command if ($option{"n"}) { print "I'd really like to run shutdown -r -f -t 30 right now.\n"; } else { system ("shutdown -r -f -t 30"); # And if that doesn't work: system ("shutdown -r -f 30"); } } sub install_patch { my ($patch, $quiet_options) = @_; if ($option{"n"}) { print "I'd really like to run $i $quiet_options right now.\n"; } else { system ("$i $quiet_options"); } }