From 8b9844a6e872cda5ce39752c245b4ac7b59a8ea2 Mon Sep 17 00:00:00 2001 From: Valko Laszlo Date: Fri, 12 Jun 2020 01:43:03 +0200 Subject: [PATCH] Implemented utilizing info extracted from the patch files and stored in a database to recognize when a patch is already installed. --- pkgtool.pm | 252 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 177 insertions(+), 75 deletions(-) diff --git a/pkgtool.pm b/pkgtool.pm index 90843da..e4ffeb3 100644 --- a/pkgtool.pm +++ b/pkgtool.pm @@ -1,4 +1,4 @@ - package pkgtool; +package pkgtool; use strict; @@ -409,6 +409,10 @@ my $global_cfg_syntax = { Type => 'string', Mandatory => 1 }, + 'msupdate-path' => { + Type => 'string', + Mandatory => 1 + }, 'log-directory' => { Type => 'string', Mandatory => 1 @@ -684,9 +688,9 @@ sub get_registry_value ($) return $value; } -sub read_os_patches ($$) +sub read_os_patches ($$$) { - my ($patches, $registry) = @_; + my ($patches, $pkgs, $registry) = @_; foreach my $name ($registry->SubKeyNames) { my $sub = $registry->{$name}; @@ -695,47 +699,74 @@ sub read_os_patches ($$) my $installclient = get_registry_value($sub->{'InstallClient'}); my $state = get_registry_value($sub->{'CurrentState'}); next unless defined $installname && defined $installclient; - next unless $installname =~ /^[^~]*KB(\d[0-9a-zA-Z]+)~/o; my $original = $installclient eq 'DISM Package Manager Provider'; my $update = $installclient eq 'WindowsUpdateAgent'; next unless $original || $update; - my $kb = $1; - my $number = $kb =~ /^(\d+)/o ? $1 : $kb; - if ($installname =~ /^[^~]*KB\d[0-9a-zA-Z]+~[^~]*~[^~]*~[^~]*~(\d+(\.\d+)*)/o) { - my $version = $1; - if (defined $version && $version ne '') { - my @versionlist = split /\./, $version; - my $revnum = $versionlist[2]; - if (defined $revnum && $revnum =~ /^\d+$/o && $revnum > 1) { - $kb .= 'v'.$revnum; + if ($installname =~ /^[^~]*KB(\d[0-9a-zA-Z]+)~/o) { + my $kb = $1; + my $number = $kb =~ /^(\d+)/o ? $1 : $kb; + if ($installname =~ /^[^~]*KB\d[0-9a-zA-Z]+~[^~]*~[^~]*~[^~]*~(\d+(\.\d+)*)/o) { + my $version = $1; + if (defined $version && $version ne '') { + my @versionlist = split /\./, $version; + my $revnum = $versionlist[2]; + if (defined $revnum && $revnum =~ /^\d+$/o && $revnum > 1) { + $kb .= 'v'.$revnum; + } } } + my $p = $$patches{$kb}; + if (! defined $p) { + $p = $$patches{$kb} = { + Type => 'OS', + Packages => { OS => 1 }, + InstallName => $name, + InstallClient => $installclient, + Original => $original, + Update => $update, + KB => $kb, + Number => $number, + Current => 0, + Flags => 0 + }; + } + if (defined $state) { + $$p{Flags} |= $state; + $$p{Current} = 1 if $state & 0x20; + } } - my $p = $$patches{$kb}; - if (! defined $p) { - $p = $$patches{$kb} = { - Type => 'OS', - Packages => { OS => 1 }, - InstallName => $name, - InstallClient => $installclient, - Original => $original, - Update => $update, - KB => $kb, - Number => $number, - Current => 0, - Flags => 0 - }; + if ($name =~ /^([^~]*)~([^~]*)~([^~]*)~([^~]*)~([^~]*)$/o) { + my $pkg = $$pkgs{$name}; + if (! defined $pkg) { + my $pkgname = $1; + my $key = $2; + my $arch = $3; + my $pkgver = $5; + $pkg = $$pkgs{$name} = { + Type => 'OS', + Packages => { OS => 1 }, + InstallName => $name, + PackageName => $pkgname, + PackageVersion => $pkgver, + Arch => $arch, + InstallClient => $installclient, + Original => $original, + Update => $update, + Current => 0, + Flags => 0 + }; + } + if (defined $state) { + $$pkg{Flags} |= $state; + $$pkg{Current} = 1 if $state & 0x20; + } } - if (defined $state) { - $$p{Flags} |= $state; - $$p{Current} = 1 if $state & 0x20; - } } } -sub xread_pkg_patches ($$) +sub xread_pkg_patches ($$$) { - my ($patches, $registry) = @_; + my ($patches, $pkgs, $registry) = @_; foreach my $pkgname ($registry->SubKeyNames) { my $pkg = $registry->{$pkgname}; @@ -771,9 +802,9 @@ sub xread_pkg_patches ($$) } } -sub read_pkg_patches ($$) +sub read_pkg_patches ($$$) { - my ($patches, $registry) = @_; + my ($patches, $pkgs, $registry) = @_; foreach my $id ($registry->SubKeyNames) { my $pkg = $registry->{$id}; @@ -891,6 +922,7 @@ sub read_installed_patches ($) delete $$db{PatchesChanged}; my $patches = {}; + my $pkgs = {}; my $winmajor = get_win_major(); if ($winmajor >= 6) { my $cbspatches = $Registry->Open('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\Packages\\', { Access => 'KEY_READ' }); @@ -898,7 +930,7 @@ sub read_installed_patches ($) print_log('global', ERROR, 'Cannot find registry entry: %s', 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\Packages'); return undef; } - read_os_patches($patches, $cbspatches); + read_os_patches($patches, $pkgs, $cbspatches); } # my $updates = $Registry->Open('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Updates\\', { Access => 'KEY_READ' }); # if (! defined $updates) { @@ -910,8 +942,9 @@ sub read_installed_patches ($) print_log('global', ERROR, 'Cannot find registry entry: %s', 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\S-1-5-18\\Products'); return undef; } - read_pkg_patches($patches, $updates); + read_pkg_patches($patches, $pkgs, $updates); $$db{Patches} = $patches; + $$db{Pkgs} = $pkgs; return $db; } @@ -2719,6 +2752,67 @@ sub get_pkg_instances ($$$$$) return $instance; } +sub get_patch_vars ($$$$$) +{ + my ($config, $base_directory, $pdef, $patchdef, $kb) = @_; + + my $vars = get_default_vars($config); + set_datetime_vars($vars); + my $basedir = substitute_variables($vars, $$pdef{'base-directory'}, 1, $base_directory, 'pkg'); + $$vars{basedir} = $basedir; + my $patchdir = substitute_variables($vars, $$patchdef{'source-directory'}, 1, $basedir, 'pkg'); + $$vars{patchdir} = $patchdir; + $$vars{patch} = $kb; + my $number = $kb =~ /^(\d+)/o ? $1 : $kb; + my $extra = $kb =~ /^\d+([^0-9].*)$/o ? '-'.$1 : ''; + $$vars{patchnum} = $number; + $$vars{patchextra} = $extra; + $$vars{patchprefix} = defined $$patchdef{prefix} ? $$patchdef{prefix} : 'Windows'.get_win_version().'-'; + $$vars{patchkbname} = defined $$patchdef{kbname} ? $$patchdef{kbname} : 'KB'; + $$vars{patchedition} = defined $$patchdef{edition} ? $$patchdef{edition} : ''; + $$vars{patcharch} = defined $$patchdef{arch} ? $$patchdef{arch} : '-'.$$vars{xarch}; + $$vars{patchsuffix} = defined $$patchdef{suffix} ? $$patchdef{suffix} : ''; + my $style = $$patchdef{style}; + $$vars{patchext} = $style eq 'exe' ? '.exe' : $style eq 'msu' ? '.msu' : $style eq 'msp' ? '.msp' : $style eq 'cab' ? '.cab' : ''; + my $sourcespec = $$patchdef{'source-file'}; + $sourcespec = '%patchprefix%%patchkbname%%patchnum%%patchextra%%patchedition%%patcharch%%patchsuffix%%patchext%' unless defined $sourcespec; + my $sourcefile = substitute_variables($vars, $sourcespec, 1, $patchdir, 'pkg'); + $$vars{sourcefile} = $sourcefile; + my $pkgid = substitute_variables($vars, $sourcespec, 0, undef, 'pkg'); + $$vars{packageid} = $1 if $pkgid =~ /^(.*)\.msu$/oi; + return $vars; +} + +sub get_msupdate_info ($$$) +{ + my ($config, $pkgid, $vars) = @_; + + return undef unless defined $pkgid; + my $urlpkgid = $pkgid; + my $url = 'http://'.$$config{'install-host'}.$$config{'msupdate-path'}.'?id='.$urlpkgid; + print_log('global', DEBUG1, 'Getting patch information from \'%s\'', $url); + my $ua = LWP::UserAgent->new; + my $response = $ua->get($url); + if (! $response->is_success) { + print_log('global', ERROR, 'Error getting patch information from \'%s\': %s', $url, $response->status_line); + return 1; + } + foreach my $line (split /\n/, $response->decoded_content) { + chomp $line; + print_log('global', DEBUG3, 'Received response line: %s', $line); + next unless $line =~ /^([^=]+)=(.*)$/o; + my $key = $1; + my $value = $2; + $$vars{'mspatch_'.$key} = $value; + print_log('global', DEBUG1, 'Found patch information %s=%s', $key, $value); + } + if (defined $$vars{mspatch_pkg_name} && defined $$vars{mspatch_pkg_key} && + defined $$vars{mspatch_pkg_arch} && defined $$vars{mspatch_pkg_version}) { + $$vars{pkgname} = $$vars{mspatch_pkg_name}.'~'.$$vars{mspatch_pkg_key}.'~'.$$vars{mspatch_pkg_arch}.'~~'.$$vars{mspatch_pkg_version}; + } + return undef; +} + sub assess_patch ($$$$$$$$$) { my ($config, $base_directory, $db, $name, $pdef, $patchdef, $kb, $update, $counters) = @_; @@ -2778,24 +2872,51 @@ sub assess_patch ($$$$$$$$$) return 1; } - my $vars = get_default_vars($config); - set_datetime_vars($vars); - my $basedir = substitute_variables($vars, $$pdef{'base-directory'}, 1, $base_directory, 'pkg'); - $$vars{basedir} = $basedir; - my $patchdir = substitute_variables($vars, $$patchdef{'source-directory'}, 1, $basedir, 'pkg'); - $$vars{patchdir} = $patchdir; - $$vars{patch} = $kb; - my $number = $kb =~ /^(\d+)/o ? $1 : $kb; - my $extra = $kb =~ /^\d+([^0-9].*)$/o ? '-'.$1 : ''; - $$vars{patchnum} = $number; - $$vars{patchextra} = $extra; - + my $vars = get_patch_vars($config, $base_directory, $pdef, $patchdef, $kb); refresh_installed_patches($db); - + return 1 if defined get_msupdate_info($config, $$vars{packageid}, $vars); + my $pkgname = $$vars{pkgname}; my $patches = $$db{Patches}; + my $pkgs = $$db{Pkgs}; my $foundpatch = $$patches{$kb}; - - if (defined $foundpatch) { + my $foundpkg = defined $pkgname ? $$pkgs{$pkgname} : undef; + if (defined $foundpkg) { + my $foundforpkgs = $$foundpkg{Packages}; + my $missing = []; + my $found = []; + foreach my $dispname (sort keys %$foundforpkgs) { + print_log('global', DEBUG4, 'Patch %s found installed for package (%s)', $kb, $dispname); + } + foreach my $pkgname (sort keys %$foundpkgs) { + my $founddispnames = $$foundpkgs{$pkgname}; + my $any = 0; + foreach my $dispname (sort keys %$founddispnames) { + if (defined $$foundforpkgs{$dispname}) { + print_log('global', DEBUG4, 'Patch %s found installed for referenced package %s (%s)', + $kb, $pkgname, $dispname); + $any = 1; + last; + } + } + if ($any) { + push @$found, $pkgname; + print_log('global', DEBUG3, 'Patch %s installed for referenced package %s', $kb, $pkgname); + } + else { + push @$missing, $pkgname; + print_log('global', DEBUG3, 'Patch %s not installed for any referenced package %s', $kb, $pkgname); + } + } + if (scalar @$missing == 0) { + print_log('global', WARNING, 'Patch %s: installed for all required packages - OK', $kb); + return 1; + } + print_log('global', WARNING, 'Patch %s: %s%s%smissing for packages %s - %s', + $kb, (scalar @$found > 0 ? 'installed for ' : ''), + join(',', @$found), (scalar @$found > 0 ? ', ' : ''), + join(',', @$missing), $update ? 'installing' : 'NEEDED'); + } + elsif (defined $foundpatch) { my $foundforpkgs = $$foundpatch{Packages}; my $missing = []; my $found = []; @@ -2849,33 +2970,14 @@ sub install_patch ($$$$$$$) print_log('pkg', INFO, 'Installing patch %s', $kb); - my $vars = get_default_vars($config); - set_datetime_vars($vars); + my $vars = get_patch_vars($config, $base_directory, $pdef, $patchdef, $kb); $$vars{pkgname} = $name; - $$vars{patch} = $kb; - my $number = $kb =~ /^(\d+)/o ? $1 : $kb; - my $extra = $kb =~ /^\d+([^0-9].*)$/o ? '-'.$1 : ''; - $$vars{patchnum} = $number; - $$vars{patchextra} = $extra; - my $basedir = substitute_variables($vars, $$pdef{'base-directory'}, 1, $base_directory, 'pkg'); - $$vars{basedir} = $basedir; - my $patchdir = substitute_variables($vars, $$patchdef{'source-directory'}, 1, $basedir, 'pkg'); - $$vars{patchdir} = $patchdir; - $$vars{patchprefix} = defined $$patchdef{prefix} ? $$patchdef{prefix} : 'Windows'.get_win_version().'-'; - $$vars{patchkbname} = defined $$patchdef{kbname} ? $$patchdef{kbname} : 'KB'; - $$vars{patchedition} = defined $$patchdef{edition} ? $$patchdef{edition} : ''; - $$vars{patcharch} = defined $$patchdef{arch} ? $$patchdef{arch} : '-'.$$vars{xarch}; - $$vars{patchsuffix} = defined $$patchdef{suffix} ? $$patchdef{suffix} : ''; - - my $style = $$patchdef{style}; - $$vars{patchext} = $style eq 'exe' ? '.exe' : $style eq 'msu' ? '.msu' : $style eq 'msp' ? '.msp' : $style eq 'cab' ? '.cab' : ''; - - my $sourcespec = $$patchdef{'source-file'}; - $sourcespec = '%patchprefix%%patchkbname%%patchnum%%patchextra%%patchedition%%patcharch%%patchsuffix%%patchext%' unless defined $sourcespec; - my $sourcefile = substitute_variables($vars, $sourcespec, 1, $patchdir, 'pkg'); my $error; my $exitcode; + my $style = $$patchdef{style}; + my $patchdir = $$vars{patchdir}; + my $sourcefile = $$vars{sourcefile}; if ($style eq 'exe') { my $chdir = defined $$patchdef{chdir} ? substitute_variables($vars, $$patchdef{chdir}, 1, $patchdir, 'pkg') : undef;