1# Copyright 2017 Damon Atkins 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14r""" 15Collect information about software installed on Windows OS 16================ 17 18:maintainer: Salt Stack <https://github.com/saltstack> 19:codeauthor: Damon Atkins <https://github.com/damon-atkins> 20:maturity: new 21:depends: pywin32 22:platform: windows 23 24Known Issue: install_date may not match Control Panel\Programs\Programs and Features 25""" 26 27# Note although this code will work with Python 2.7, win32api does not 28# support Unicode. i.e non ASCII characters may be returned with unexpected 29# results e.g. a '?' instead of the correct character 30# Python 3.6 or newer is recommended. 31 32import collections 33import datetime 34import locale 35import logging 36import os.path 37import platform 38import re 39import sys 40import time 41from functools import cmp_to_key 42 43__version__ = "0.1" 44 45try: 46 import win32api 47 import win32con 48 import win32process 49 import win32security 50 import pywintypes 51 import winerror 52 53except ImportError: 54 if __name__ == "__main__": 55 raise ImportError("Please install pywin32/pypiwin32") 56 else: 57 raise 58 59 60if __name__ == "__main__": 61 LOG_CONSOLE = logging.StreamHandler() 62 LOG_CONSOLE.setFormatter(logging.Formatter("[%(levelname)s]: %(message)s")) 63 log = logging.getLogger(__name__) 64 log.addHandler(LOG_CONSOLE) 65 log.setLevel(logging.DEBUG) 66else: 67 log = logging.getLogger(__name__) 68 69 70try: 71 from salt.utils.odict import OrderedDict 72except ImportError: 73 from collections import OrderedDict 74 75try: 76 from salt.utils.versions import LooseVersion 77except ImportError: 78 from distutils.version import LooseVersion # pylint: disable=blacklisted-module 79 80 81# pylint: disable=too-many-instance-attributes 82 83 84class RegSoftwareInfo: 85 """ 86 Retrieve Registry data on a single installed software item or component. 87 88 Attribute: 89 None 90 91 :codeauthor: Damon Atkins <https://github.com/damon-atkins> 92 """ 93 94 # Variables shared by all instances 95 __guid_pattern = re.compile( 96 r"^\{(\w{8})-(\w{4})-(\w{4})-(\w\w)(\w\w)-(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)\}$" 97 ) 98 __squid_pattern = re.compile( 99 r"^(\w{8})(\w{4})(\w{4})(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)$" 100 ) 101 __version_pattern = re.compile(r"\d+\.\d+\.\d+[\w.-]*|\d+\.\d+[\w.-]*") 102 __upgrade_codes = {} 103 __upgrade_code_have_scan = {} 104 105 __reg_types = { 106 "str": (win32con.REG_EXPAND_SZ, win32con.REG_SZ), 107 "list": (win32con.REG_MULTI_SZ), 108 "int": (win32con.REG_DWORD, win32con.REG_DWORD_BIG_ENDIAN, win32con.REG_QWORD), 109 "bytes": (win32con.REG_BINARY), 110 } 111 112 # Search 64bit, on 64bit platform, on 32bit its ignored 113 if platform.architecture()[0] == "32bit": 114 # Handle Python 32bit on 64&32 bit platform and Python 64bit 115 if win32process.IsWow64Process(): # pylint: disable=no-member 116 # 32bit python on a 64bit platform 117 __use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY} 118 else: 119 # 32bit python on a 32bit platform 120 __use_32bit_lookup = {True: 0, False: None} 121 else: 122 __use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0} 123 124 def __init__(self, key_guid, sid=None, use_32bit=False): 125 """ 126 Initialise against a software item or component. 127 128 All software has a unique "Identifer" within the registry. This can be free 129 form text/numbers e.g. "MySoftware" or 130 GUID e.g. "{0EAF0D8F-C9CF-4350-BD9A-07EC66929E04}" 131 132 Args: 133 key_guid (str): Identifer. 134 sid (str): Security IDentifier of the User or None for Computer/Machine. 135 use_32bit (bool): 136 Regisrty location of the Identifer. ``True`` 32 bit registry only 137 meaning fully on 64 bit OS. 138 """ 139 self.__reg_key_guid = key_guid # also called IdentifyingNumber(wmic) 140 self.__squid = "" 141 self.__reg_products_path = "" 142 self.__reg_upgradecode_path = "" 143 self.__patch_list = None 144 145 # If a valid GUID create the SQUID also. 146 guid_match = self.__guid_pattern.match(key_guid) 147 if guid_match is not None: 148 for index in range(1, 12): 149 # __guid_pattern breaks up the GUID 150 self.__squid += guid_match.group(index)[::-1] 151 152 if sid: 153 # User data seems to be more spreadout within the registry. 154 self.__reg_hive = "HKEY_USERS" 155 self.__reg_32bit = False # Force to False 156 self.__reg_32bit_access = ( 157 0 # HKEY_USERS does not have a 32bit and 64bit view 158 ) 159 self.__reg_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format( 160 sid, key_guid 161 ) 162 if self.__squid: 163 self.__reg_products_path = ( 164 "{}\\Software\\Classes\\Installer\\Products\\{}".format( 165 sid, self.__squid 166 ) 167 ) 168 self.__reg_upgradecode_path = ( 169 "{}\\Software\\Microsoft\\Installer\\UpgradeCodes".format(sid) 170 ) 171 self.__reg_patches_path = ( 172 "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\" 173 "{}\\Products\\{}\\Patches".format(sid, self.__squid) 174 ) 175 else: 176 self.__reg_hive = "HKEY_LOCAL_MACHINE" 177 self.__reg_32bit = use_32bit 178 self.__reg_32bit_access = self.__use_32bit_lookup[use_32bit] 179 self.__reg_uninstall_path = ( 180 "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format( 181 key_guid 182 ) 183 ) 184 if self.__squid: 185 self.__reg_products_path = ( 186 "Software\\Classes\\Installer\\Products\\{}".format(self.__squid) 187 ) 188 self.__reg_upgradecode_path = ( 189 "Software\\Classes\\Installer\\UpgradeCodes" 190 ) 191 self.__reg_patches_path = ( 192 "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\" 193 "S-1-5-18\\Products\\{}\\Patches".format(self.__squid) 194 ) 195 196 # OpenKey is expensive, open in advance and keep it open. 197 # This must exist 198 try: 199 # pylint: disable=no-member 200 self.__reg_uninstall_handle = win32api.RegOpenKeyEx( 201 getattr(win32con, self.__reg_hive), 202 self.__reg_uninstall_path, 203 0, 204 win32con.KEY_READ | self.__reg_32bit_access, 205 ) 206 except pywintypes.error as exc: # pylint: disable=no-member 207 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 208 log.error( 209 "Software/Component Not Found key_guid: '%s', " 210 "sid: '%s' , use_32bit: '%s'", 211 key_guid, 212 sid, 213 use_32bit, 214 ) 215 raise # This must exist or have no errors 216 217 self.__reg_products_handle = None 218 if self.__squid: 219 try: 220 # pylint: disable=no-member 221 self.__reg_products_handle = win32api.RegOpenKeyEx( 222 getattr(win32con, self.__reg_hive), 223 self.__reg_products_path, 224 0, 225 win32con.KEY_READ | self.__reg_32bit_access, 226 ) 227 except pywintypes.error as exc: # pylint: disable=no-member 228 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 229 log.debug( 230 "Software/Component Not Found in Products section of registry " 231 "key_guid: '%s', sid: '%s', use_32bit: '%s'", 232 key_guid, 233 sid, 234 use_32bit, 235 ) 236 self.__squid = None # mark it as not a SQUID 237 else: 238 raise 239 240 self.__mod_time1970 = 0 241 # pylint: disable=no-member 242 mod_win_time = win32api.RegQueryInfoKeyW(self.__reg_uninstall_handle).get( 243 "LastWriteTime", None 244 ) 245 # pylint: enable=no-member 246 if mod_win_time: 247 # at some stage __int__() was removed from pywintypes.datetime to return secs since 1970 248 if hasattr(mod_win_time, "utctimetuple"): 249 self.__mod_time1970 = time.mktime(mod_win_time.utctimetuple()) 250 elif hasattr(mod_win_time, "__int__"): 251 self.__mod_time1970 = int(mod_win_time) 252 253 def __squid_to_guid(self, squid): 254 """ 255 Squished GUID (SQUID) to GUID. 256 257 A SQUID is a Squished/Compressed version of a GUID to use up less space 258 in the registry. 259 260 Args: 261 squid (str): Squished GUID. 262 263 Returns: 264 str: the GUID if a valid SQUID provided. 265 """ 266 if not squid: 267 return "" 268 squid_match = self.__squid_pattern.match(squid) 269 guid = "" 270 if squid_match is not None: 271 guid = ( 272 "{" 273 + squid_match.group(1)[::-1] 274 + "-" 275 + squid_match.group(2)[::-1] 276 + "-" 277 + squid_match.group(3)[::-1] 278 + "-" 279 + squid_match.group(4)[::-1] 280 + squid_match.group(5)[::-1] 281 + "-" 282 ) 283 for index in range(6, 12): 284 guid += squid_match.group(index)[::-1] 285 guid += "}" 286 return guid 287 288 @staticmethod 289 def __one_equals_true(value): 290 """ 291 Test for ``1`` as a number or a string and return ``True`` if it is. 292 293 Args: 294 value: string or number or None. 295 296 Returns: 297 bool: ``True`` if 1 otherwise ``False``. 298 """ 299 if isinstance(value, int) and value == 1: 300 return True 301 elif ( 302 isinstance(value, str) 303 and re.match(r"\d+", value, flags=re.IGNORECASE + re.UNICODE) is not None 304 and str(value) == "1" 305 ): 306 return True 307 return False 308 309 @staticmethod 310 def __reg_query_value(handle, value_name): 311 """ 312 Calls RegQueryValueEx 313 314 If PY2 ensure unicode string and expand REG_EXPAND_SZ before returning 315 Remember to catch not found exceptions when calling. 316 317 Args: 318 handle (object): open registry handle. 319 value_name (str): Name of the value you wished returned 320 321 Returns: 322 tuple: type, value 323 """ 324 # item_value, item_type = win32api.RegQueryValueEx(self.__reg_uninstall_handle, value_name) 325 item_value, item_type = win32api.RegQueryValueEx( 326 handle, value_name 327 ) # pylint: disable=no-member 328 if item_type == win32con.REG_EXPAND_SZ: 329 # expects Unicode input 330 win32api.ExpandEnvironmentStrings(item_value) # pylint: disable=no-member 331 item_type = win32con.REG_SZ 332 return item_value, item_type 333 334 @property 335 def install_time(self): 336 """ 337 Return the install time, or provide an estimate of install time. 338 339 Installers or even self upgrading software must/should update the date 340 held within InstallDate field when they change versions. Some installers 341 do not set ``InstallDate`` at all so we use the last modified time on the 342 registry key. 343 344 Returns: 345 int: Seconds since 1970 UTC. 346 """ 347 time1970 = self.__mod_time1970 # time of last resort 348 try: 349 # pylint: disable=no-member 350 date_string, item_type = win32api.RegQueryValueEx( 351 self.__reg_uninstall_handle, "InstallDate" 352 ) 353 except pywintypes.error as exc: # pylint: disable=no-member 354 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 355 return time1970 # i.e. use time of last resort 356 else: 357 raise 358 359 if item_type == win32con.REG_SZ: 360 try: 361 date_object = datetime.datetime.strptime(date_string, "%Y%m%d") 362 time1970 = time.mktime(date_object.timetuple()) 363 except ValueError: # date format is not correct 364 pass 365 366 return time1970 367 368 def get_install_value(self, value_name, wanted_type=None): 369 """ 370 For the uninstall section of the registry return the name value. 371 372 Args: 373 value_name (str): Registry value name. 374 wanted_type (str): 375 The type of value wanted if the type does not match 376 None is return. wanted_type support values are 377 ``str`` ``int`` ``list`` ``bytes``. 378 379 Returns: 380 value: Value requested or None if not found. 381 """ 382 try: 383 item_value, item_type = self.__reg_query_value( 384 self.__reg_uninstall_handle, value_name 385 ) 386 except pywintypes.error as exc: # pylint: disable=no-member 387 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 388 # Not Found 389 return None 390 raise 391 392 if wanted_type and item_type not in self.__reg_types[wanted_type]: 393 item_value = None 394 395 return item_value 396 397 def is_install_true(self, key): 398 """ 399 For the uninstall section check if name value is ``1``. 400 401 Args: 402 value_name (str): Registry value name. 403 404 Returns: 405 bool: ``True`` if ``1`` otherwise ``False``. 406 """ 407 return self.__one_equals_true(self.get_install_value(key)) 408 409 def get_product_value(self, value_name, wanted_type=None): 410 """ 411 For the product section of the registry return the name value. 412 413 Args: 414 value_name (str): Registry value name. 415 wanted_type (str): 416 The type of value wanted if the type does not match 417 None is return. wanted_type support values are 418 ``str`` ``int`` ``list`` ``bytes``. 419 420 Returns: 421 value: Value requested or ``None`` if not found. 422 """ 423 if not self.__reg_products_handle: 424 return None 425 subkey, search_value_name = os.path.split(value_name) 426 try: 427 if subkey: 428 429 handle = win32api.RegOpenKeyEx( # pylint: disable=no-member 430 self.__reg_products_handle, 431 subkey, 432 0, 433 win32con.KEY_READ | self.__reg_32bit_access, 434 ) 435 item_value, item_type = self.__reg_query_value( 436 handle, search_value_name 437 ) 438 win32api.RegCloseKey(handle) # pylint: disable=no-member 439 else: 440 item_value, item_type = win32api.RegQueryValueEx( 441 self.__reg_products_handle, value_name 442 ) # pylint: disable=no-member 443 except pywintypes.error as exc: # pylint: disable=no-member 444 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 445 # Not Found 446 return None 447 raise 448 449 if wanted_type and item_type not in self.__reg_types[wanted_type]: 450 item_value = None 451 return item_value 452 453 @property 454 def upgrade_code(self): 455 """ 456 For installers which follow the Microsoft Installer standard, returns 457 the ``Upgrade code``. 458 459 Returns: 460 value (str): ``Upgrade code`` GUID for installed software. 461 """ 462 if not self.__squid: 463 # Must have a valid squid for an upgrade code to exist 464 return "" 465 466 # GUID/SQUID are unique, so it does not matter if they are 32bit or 467 # 64bit or user install so all items are cached into a single dict 468 have_scan_key = "{}\\{}\\{}".format( 469 self.__reg_hive, self.__reg_upgradecode_path, self.__reg_32bit 470 ) 471 if not self.__upgrade_codes or self.__reg_key_guid not in self.__upgrade_codes: 472 # Read in the upgrade codes in this section of the registry. 473 try: 474 uc_handle = win32api.RegOpenKeyEx( 475 getattr(win32con, self.__reg_hive), # pylint: disable=no-member 476 self.__reg_upgradecode_path, 477 0, 478 win32con.KEY_READ | self.__reg_32bit_access, 479 ) 480 except pywintypes.error as exc: # pylint: disable=no-member 481 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 482 # Not Found 483 log.warning( 484 "Not Found %s\\%s 32bit %s", 485 self.__reg_hive, 486 self.__reg_upgradecode_path, 487 self.__reg_32bit, 488 ) 489 return "" 490 raise 491 squid_upgrade_code_all, _, _, suc_pytime = zip( 492 *win32api.RegEnumKeyEx(uc_handle) 493 ) # pylint: disable=no-member 494 495 # Check if we have already scanned these upgrade codes before, and also 496 # check if they have been updated in the registry since last time we scanned. 497 if ( 498 have_scan_key in self.__upgrade_code_have_scan 499 and self.__upgrade_code_have_scan[have_scan_key] 500 == ( 501 squid_upgrade_code_all, 502 suc_pytime, 503 ) 504 ): 505 log.debug( 506 "Scan skipped for upgrade codes, no changes (%s)", have_scan_key 507 ) 508 return "" # we have scanned this before and no new changes. 509 510 # Go into each squid upgrade code and find all the related product codes. 511 log.debug("Scan for upgrade codes (%s) for product codes", have_scan_key) 512 for upgrade_code_squid in squid_upgrade_code_all: 513 upgrade_code_guid = self.__squid_to_guid(upgrade_code_squid) 514 pc_handle = win32api.RegOpenKeyEx( 515 uc_handle, # pylint: disable=no-member 516 upgrade_code_squid, 517 0, 518 win32con.KEY_READ | self.__reg_32bit_access, 519 ) 520 _, pc_val_count, _ = win32api.RegQueryInfoKey( 521 pc_handle 522 ) # pylint: disable=no-member 523 for item_index in range(pc_val_count): 524 product_code_guid = self.__squid_to_guid( 525 win32api.RegEnumValue(pc_handle, item_index)[0] 526 ) # pylint: disable=no-member 527 if product_code_guid: 528 self.__upgrade_codes[product_code_guid] = upgrade_code_guid 529 win32api.RegCloseKey(pc_handle) # pylint: disable=no-member 530 531 win32api.RegCloseKey(uc_handle) # pylint: disable=no-member 532 self.__upgrade_code_have_scan[have_scan_key] = ( 533 squid_upgrade_code_all, 534 suc_pytime, 535 ) 536 537 return self.__upgrade_codes.get(self.__reg_key_guid, "") 538 539 @property 540 def list_patches(self): 541 """ 542 For installers which follow the Microsoft Installer standard, returns 543 a list of patches applied. 544 545 Returns: 546 value (list): Long name of the patch. 547 """ 548 if not self.__squid: 549 # Must have a valid squid for an upgrade code to exist 550 return [] 551 552 if self.__patch_list is None: 553 # Read in the upgrade codes in this section of the reg. 554 try: 555 pat_all_handle = win32api.RegOpenKeyEx( 556 getattr(win32con, self.__reg_hive), # pylint: disable=no-member 557 self.__reg_patches_path, 558 0, 559 win32con.KEY_READ | self.__reg_32bit_access, 560 ) 561 except pywintypes.error as exc: # pylint: disable=no-member 562 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 563 # Not Found 564 log.warning( 565 "Not Found %s\\%s 32bit %s", 566 self.__reg_hive, 567 self.__reg_patches_path, 568 self.__reg_32bit, 569 ) 570 return [] 571 raise 572 573 pc_sub_key_cnt, _, _ = win32api.RegQueryInfoKey( 574 pat_all_handle 575 ) # pylint: disable=no-member 576 if not pc_sub_key_cnt: 577 return [] 578 squid_patch_all, _, _, _ = zip( 579 *win32api.RegEnumKeyEx(pat_all_handle) 580 ) # pylint: disable=no-member 581 582 ret = [] 583 # Scan the patches for the DisplayName of active patches. 584 for patch_squid in squid_patch_all: 585 try: 586 patch_squid_handle = ( 587 win32api.RegOpenKeyEx( # pylint: disable=no-member 588 pat_all_handle, 589 patch_squid, 590 0, 591 win32con.KEY_READ | self.__reg_32bit_access, 592 ) 593 ) 594 ( 595 patch_display_name, 596 patch_display_name_type, 597 ) = self.__reg_query_value(patch_squid_handle, "DisplayName") 598 patch_state, patch_state_type = self.__reg_query_value( 599 patch_squid_handle, "State" 600 ) 601 if ( 602 patch_state_type != win32con.REG_DWORD 603 or not isinstance(patch_state_type, int) 604 or patch_state != 1 605 or patch_display_name_type # 1 is Active, 2 is Superseded/Obsolute 606 != win32con.REG_SZ 607 ): 608 continue 609 win32api.RegCloseKey( 610 patch_squid_handle 611 ) # pylint: disable=no-member 612 ret.append(patch_display_name) 613 except pywintypes.error as exc: # pylint: disable=no-member 614 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 615 log.debug("skipped patch, not found %s", patch_squid) 616 continue 617 raise 618 619 return ret 620 621 @property 622 def registry_path_text(self): 623 """ 624 Returns the uninstall path this object is associated with. 625 626 Returns: 627 str: <hive>\\<uninstall registry entry> 628 """ 629 return "{}\\{}".format(self.__reg_hive, self.__reg_uninstall_path) 630 631 @property 632 def registry_path(self): 633 """ 634 Returns the uninstall path this object is associated with. 635 636 Returns: 637 tuple: hive, uninstall registry entry path. 638 """ 639 return (self.__reg_hive, self.__reg_uninstall_path) 640 641 @property 642 def guid(self): 643 """ 644 Return GUID or Key. 645 646 Returns: 647 str: GUID or Key 648 """ 649 return self.__reg_key_guid 650 651 @property 652 def squid(self): 653 """ 654 Return SQUID of the GUID if a valid GUID. 655 656 Returns: 657 str: GUID 658 """ 659 return self.__squid 660 661 @property 662 def package_code(self): 663 """ 664 Return package code of the software. 665 666 Returns: 667 str: GUID 668 """ 669 return self.__squid_to_guid(self.get_product_value("PackageCode")) 670 671 @property 672 def version_binary(self): 673 """ 674 Return version number which is stored in binary format. 675 676 Returns: 677 str: <major 0-255>.<minior 0-255>.<build 0-65535> or None if not found 678 """ 679 # Under MSI 'Version' is a 'REG_DWORD' which then sets other registry 680 # values like DisplayVersion to x.x.x to the same value. 681 # However not everyone plays by the rules, so we need to check first. 682 # version_binary_data will be None if the reg value does not exist. 683 # Some installs set 'Version' to REG_SZ (string) which is not 684 # the MSI standard 685 try: 686 item_value, item_type = self.__reg_query_value( 687 self.__reg_uninstall_handle, "version" 688 ) 689 except pywintypes.error as exc: # pylint: disable=no-member 690 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 691 # Not Found 692 return "", "" 693 694 version_binary_text = "" 695 version_src = "" 696 if item_value: 697 if item_type == win32con.REG_DWORD: 698 if isinstance(item_value, int): 699 version_binary_raw = item_value 700 if version_binary_raw: 701 # Major.Minor.Build 702 version_binary_text = "{}.{}.{}".format( 703 version_binary_raw >> 24 & 0xFF, 704 version_binary_raw >> 16 & 0xFF, 705 version_binary_raw & 0xFFFF, 706 ) 707 version_src = "binary-version" 708 709 elif ( 710 item_type == win32con.REG_SZ 711 and isinstance(item_value, str) 712 and self.__version_pattern.match(item_value) is not None 713 ): 714 # Hey, version should be a int/REG_DWORD, an installer has set 715 # it to a string 716 version_binary_text = item_value.strip(" ") 717 version_src = "binary-version (string)" 718 719 return (version_binary_text, version_src) 720 721 722class WinSoftware: 723 """ 724 Point in time snapshot of the software and components installed on 725 a system. 726 727 Attributes: 728 None 729 730 :codeauthor: Damon Atkins <https://github.com/damon-atkins> 731 """ 732 733 __sid_pattern = re.compile(r"^S-\d-\d-\d+$|^S-\d-\d-\d+-\d+-\d+-\d+-\d+$") 734 __whitespace_pattern = re.compile(r"^\s*$", flags=re.UNICODE) 735 # items we copy out of the uninstall section of the registry without further processing 736 __uninstall_search_list = [ 737 ("url", "str", ["URLInfoAbout", "HelpLink", "MoreInfoUrl", "UrlUpdateInfo"]), 738 ("size", "int", ["Size", "EstimatedSize"]), 739 ("win_comments", "str", ["Comments"]), 740 ("win_release_type", "str", ["ReleaseType"]), 741 ("win_product_id", "str", ["ProductID"]), 742 ("win_product_codes", "str", ["ProductCodes"]), 743 ("win_package_refs", "str", ["PackageRefs"]), 744 ("win_install_location", "str", ["InstallLocation"]), 745 ("win_install_src_dir", "str", ["InstallSource"]), 746 ("win_parent_pkg_uid", "str", ["ParentKeyName"]), 747 ("win_parent_name", "str", ["ParentDisplayName"]), 748 ] 749 # items we copy out of the products section of the registry without further processing 750 __products_search_list = [ 751 ("win_advertise_flags", "int", ["AdvertiseFlags"]), 752 ("win_redeployment_flags", "int", ["DeploymentFlags"]), 753 ("win_instance_type", "int", ["InstanceType"]), 754 ("win_package_name", "str", ["SourceList\\PackageName"]), 755 ] 756 757 def __init__(self, version_only=False, user_pkgs=False, pkg_obj=None): 758 """ 759 Point in time snapshot of the software and components installed on 760 a system. 761 762 Args: 763 version_only (bool): Provide list of versions installed instead of detail. 764 user_pkgs (bool): Include software/components installed with user space. 765 pkg_obj (object): 766 If None (default) return default package naming standard and use 767 default version capture methods (``DisplayVersion`` then 768 ``Version``, otherwise ``0.0.0.0``) 769 """ 770 self.__pkg_obj = pkg_obj # must be set before calling get_software_details 771 self.__version_only = version_only 772 self.__reg_software = {} 773 self.__get_software_details(user_pkgs=user_pkgs) 774 self.__pkg_cnt = len(self.__reg_software) 775 self.__iter_list = None 776 777 @property 778 def data(self): 779 """ 780 Returns the raw data 781 782 Returns: 783 dict: contents of the dict are dependent on the parameters passed 784 when the class was initiated. 785 """ 786 return self.__reg_software 787 788 @property 789 def version_only(self): 790 """ 791 Returns True if class initiated with ``version_only=True`` 792 793 Returns: 794 bool: The value of ``version_only`` 795 """ 796 return self.__version_only 797 798 def __len__(self): 799 """ 800 Returns total number of software/components installed. 801 802 Returns: 803 int: total number of software/components installed. 804 """ 805 return self.__pkg_cnt 806 807 def __getitem__(self, pkg_id): 808 """ 809 Returns information on a package. 810 811 Args: 812 pkg_id (str): Package Id of the software/component 813 814 Returns: 815 dict or list: List if ``version_only`` is ``True`` otherwise dict 816 """ 817 if pkg_id in self.__reg_software: 818 return self.__reg_software[pkg_id] 819 else: 820 raise KeyError(pkg_id) 821 822 def __iter__(self): 823 """ 824 Standard interation class initialisation over package information. 825 """ 826 if self.__iter_list is not None: 827 raise RuntimeError("Can only perform one iter at a time") 828 self.__iter_list = collections.deque(sorted(self.__reg_software.keys())) 829 return self 830 831 def __next__(self): 832 """ 833 Returns next Package Id. 834 835 Returns: 836 str: Package Id 837 """ 838 try: 839 return self.__iter_list.popleft() 840 except IndexError: 841 self.__iter_list = None 842 raise StopIteration 843 844 def next(self): 845 """ 846 Returns next Package Id. 847 848 Returns: 849 str: Package Id 850 """ 851 return self.__next__() 852 853 def get(self, pkg_id, default_value=None): 854 """ 855 Returns information on a package. 856 857 Args: 858 pkg_id (str): Package Id of the software/component. 859 default_value: Value to return when the Package Id is not found. 860 861 Returns: 862 dict or list: List if ``version_only`` is ``True`` otherwise dict 863 """ 864 return self.__reg_software.get(pkg_id, default_value) 865 866 @staticmethod 867 def __oldest_to_latest_version(ver1, ver2): 868 """ 869 Used for sorting version numbers oldest to latest 870 """ 871 return 1 if LooseVersion(ver1) > LooseVersion(ver2) else -1 872 873 @staticmethod 874 def __latest_to_oldest_version(ver1, ver2): 875 """ 876 Used for sorting version numbers, latest to oldest 877 """ 878 return 1 if LooseVersion(ver1) < LooseVersion(ver2) else -1 879 880 def pkg_version_list(self, pkg_id): 881 """ 882 Returns information on a package. 883 884 Args: 885 pkg_id (str): Package Id of the software/component. 886 887 Returns: 888 list: List of version numbers installed. 889 """ 890 pkg_data = self.__reg_software.get(pkg_id, None) 891 if not pkg_data: 892 return [] 893 894 if isinstance(pkg_data, list): 895 # raw data is 'pkgid': [sorted version list] 896 return pkg_data # already sorted oldest to newest 897 898 # Must be a dict or OrderDict, and contain full details 899 installed_versions = list(pkg_data.get("version").keys()) 900 return sorted( 901 installed_versions, key=cmp_to_key(self.__oldest_to_latest_version) 902 ) 903 904 def pkg_version_latest(self, pkg_id): 905 """ 906 Returns a package latest version installed out of all the versions 907 currently installed. 908 909 Args: 910 pkg_id (str): Package Id of the software/component. 911 912 Returns: 913 str: Latest/Newest version number installed. 914 """ 915 return self.pkg_version_list(pkg_id)[-1] 916 917 def pkg_version_oldest(self, pkg_id): 918 """ 919 Returns a package oldest version installed out of all the versions 920 currently installed. 921 922 Args: 923 pkg_id (str): Package Id of the software/component. 924 925 Returns: 926 str: Oldest version number installed. 927 """ 928 return self.pkg_version_list(pkg_id)[0] 929 930 @staticmethod 931 def __sid_to_username(sid): 932 """ 933 Provided with a valid Windows Security Identifier (SID) and returns a Username 934 935 Args: 936 sid (str): Security Identifier (SID). 937 938 Returns: 939 str: Username in the format of username@realm or username@computer. 940 """ 941 if sid is None or sid == "": 942 return "" 943 try: 944 sid_bin = win32security.GetBinarySid(sid) # pylint: disable=no-member 945 except pywintypes.error as exc: # pylint: disable=no-member 946 raise ValueError( 947 "pkg: Software owned by {} is not valid: [{}] {}".format( 948 sid, exc.winerror, exc.strerror 949 ) 950 ) 951 try: 952 name, domain, _account_type = win32security.LookupAccountSid( 953 None, sid_bin 954 ) # pylint: disable=no-member 955 user_name = "{}\\{}".format(domain, name) 956 except pywintypes.error as exc: # pylint: disable=no-member 957 # if user does not exist... 958 # winerror.ERROR_NONE_MAPPED = No mapping between account names and 959 # security IDs was carried out. 960 if exc.winerror == winerror.ERROR_NONE_MAPPED: # 1332 961 # As the sid is from the registry it should be valid 962 # even if it cannot be lookedup, so the sid is returned 963 return sid 964 else: 965 raise ValueError( 966 "Failed looking up sid '{}' username: [{}] {}".format( 967 sid, exc.winerror, exc.strerror 968 ) 969 ) 970 try: 971 user_principal = win32security.TranslateName( # pylint: disable=no-member 972 user_name, 973 win32api.NameSamCompatible, # pylint: disable=no-member 974 win32api.NameUserPrincipal, 975 ) # pylint: disable=no-member 976 except pywintypes.error as exc: # pylint: disable=no-member 977 # winerror.ERROR_NO_SUCH_DOMAIN The specified domain either does not exist 978 # or could not be contacted, computer may not be part of a domain also 979 # winerror.ERROR_INVALID_DOMAINNAME The format of the specified domain name is 980 # invalid. e.g. S-1-5-19 which is a local account 981 # winerror.ERROR_NONE_MAPPED No mapping between account names and security IDs was done. 982 if exc.winerror in ( 983 winerror.ERROR_NO_SUCH_DOMAIN, 984 winerror.ERROR_INVALID_DOMAINNAME, 985 winerror.ERROR_NONE_MAPPED, 986 ): 987 return "{}@{}".format(name.lower(), domain.lower()) 988 else: 989 raise 990 return user_principal 991 992 def __software_to_pkg_id(self, publisher, name, is_component, is_32bit): 993 """ 994 Determine the Package ID of a software/component using the 995 software/component ``publisher``, ``name``, whether its a software or a 996 component, and if its 32bit or 64bit archiecture. 997 998 Args: 999 publisher (str): Publisher of the software/component. 1000 name (str): Name of the software. 1001 is_component (bool): True if package is a component. 1002 is_32bit (bool): True if the software/component is 32bit architecture. 1003 1004 Returns: 1005 str: Package Id 1006 """ 1007 if publisher: 1008 # remove , and lowercase as , are used as list separators 1009 pub_lc = publisher.replace(",", "").lower() 1010 1011 else: 1012 # remove , and lowercase 1013 pub_lc = "NoValue" # Capitals/Special Value 1014 1015 if name: 1016 name_lc = name.replace(",", "").lower() 1017 # remove , OR we do the URL Encode on chars we do not want e.g. \\ and , 1018 else: 1019 name_lc = "NoValue" # Capitals/Special Value 1020 1021 if is_component: 1022 soft_type = "comp" 1023 else: 1024 soft_type = "soft" 1025 1026 if is_32bit: 1027 soft_type += "32" # Tag only the 32bit only 1028 1029 default_pkg_id = pub_lc + "\\\\" + name_lc + "\\\\" + soft_type 1030 1031 # Check to see if class was initialise with pkg_obj with a method called 1032 # to_pkg_id, and if so use it for the naming standard instead of the default 1033 if self.__pkg_obj and hasattr(self.__pkg_obj, "to_pkg_id"): 1034 pkg_id = self.__pkg_obj.to_pkg_id(publisher, name, is_component, is_32bit) 1035 if pkg_id: 1036 return pkg_id 1037 1038 return default_pkg_id 1039 1040 def __version_capture_slp( 1041 self, pkg_id, version_binary, version_display, display_name 1042 ): 1043 """ 1044 This returns the version and where the version string came from, based on instructions 1045 under ``version_capture``, if ``version_capture`` is missing, it defaults to 1046 value of display-version. 1047 1048 Args: 1049 pkg_id (str): Publisher of the software/component. 1050 version_binary (str): Name of the software. 1051 version_display (str): True if package is a component. 1052 display_name (str): True if the software/component is 32bit architecture. 1053 1054 Returns: 1055 str: Package Id 1056 """ 1057 if self.__pkg_obj and hasattr(self.__pkg_obj, "version_capture"): 1058 version_str, src, version_user_str = self.__pkg_obj.version_capture( 1059 pkg_id, version_binary, version_display, display_name 1060 ) 1061 if src != "use-default" and version_str and src: 1062 return version_str, src, version_user_str 1063 elif src != "use-default": 1064 raise ValueError( 1065 "version capture within object '{}' failed " 1066 "for pkg id: '{}' it returned '{}' '{}' " 1067 "'{}'".format( 1068 str(self.__pkg_obj), 1069 pkg_id, 1070 version_str, 1071 src, 1072 version_user_str, 1073 ) 1074 ) 1075 1076 # If self.__pkg_obj.version_capture() not defined defaults to using 1077 # version_display and if not valid then use version_binary, and as a last 1078 # result provide the version 0.0.0.0.0 to indicate version string was not determined. 1079 if ( 1080 version_display 1081 and re.match(r"\d+", version_display, flags=re.IGNORECASE + re.UNICODE) 1082 is not None 1083 ): 1084 version_str = version_display 1085 src = "display-version" 1086 elif ( 1087 version_binary 1088 and re.match(r"\d+", version_binary, flags=re.IGNORECASE + re.UNICODE) 1089 is not None 1090 ): 1091 version_str = version_binary 1092 src = "version-binary" 1093 else: 1094 src = "none" 1095 version_str = "0.0.0.0.0" 1096 # return version str, src of the version, "user" interpretation of the version 1097 # which by default is version_str 1098 return version_str, src, version_str 1099 1100 def __collect_software_info(self, sid, key_software, use_32bit): 1101 """ 1102 Update data with the next software found 1103 """ 1104 1105 reg_soft_info = RegSoftwareInfo(key_software, sid, use_32bit) 1106 1107 # Check if the registry entry is a valid. 1108 # a) Cannot manage software without at least a display name 1109 display_name = reg_soft_info.get_install_value("DisplayName", wanted_type="str") 1110 if display_name is None or self.__whitespace_pattern.match(display_name): 1111 return 1112 1113 # b) make sure its not an 'Hotfix', 'Update Rollup', 'Security Update', 'ServicePack' 1114 # General this is software which pre dates Windows 10 1115 default_value = reg_soft_info.get_install_value("", wanted_type="str") 1116 release_type = reg_soft_info.get_install_value("ReleaseType", wanted_type="str") 1117 1118 if ( 1119 re.match( 1120 r"^{.*\}\.KB\d{6,}$", key_software, flags=re.IGNORECASE + re.UNICODE 1121 ) 1122 is not None 1123 or (default_value and default_value.startswith(("KB", "kb", "Kb"))) 1124 or ( 1125 release_type 1126 and release_type 1127 in ("Hotfix", "Update Rollup", "Security Update", "ServicePack") 1128 ) 1129 ): 1130 log.debug("skipping hotfix/update/service pack %s", key_software) 1131 return 1132 1133 # if NoRemove exists we would expect their to be no UninstallString 1134 uninstall_no_remove = reg_soft_info.is_install_true("NoRemove") 1135 uninstall_string = reg_soft_info.get_install_value("UninstallString") 1136 uninstall_quiet_string = reg_soft_info.get_install_value("QuietUninstallString") 1137 uninstall_modify_path = reg_soft_info.get_install_value("ModifyPath") 1138 windows_installer = reg_soft_info.is_install_true("WindowsInstaller") 1139 system_component = reg_soft_info.is_install_true("SystemComponent") 1140 publisher = reg_soft_info.get_install_value("Publisher", wanted_type="str") 1141 1142 # UninstallString is optional if the installer is "windows installer"/MSI 1143 # However for it to appear in Control-Panel -> Program and Features -> Uninstall or change a program 1144 # the UninstallString needs to be set or ModifyPath set 1145 if ( 1146 uninstall_string is None 1147 and uninstall_quiet_string is None 1148 and uninstall_modify_path is None 1149 and (not windows_installer) 1150 ): 1151 return 1152 1153 # Question: If uninstall string is not set and windows_installer should we set it 1154 # Question: if uninstall_quiet is not set ....... 1155 1156 if sid: 1157 username = self.__sid_to_username(sid) 1158 else: 1159 username = None 1160 1161 # We now have a valid software install or a system component 1162 pkg_id = self.__software_to_pkg_id( 1163 publisher, display_name, system_component, use_32bit 1164 ) 1165 version_binary, version_src = reg_soft_info.version_binary 1166 version_display = reg_soft_info.get_install_value( 1167 "DisplayVersion", wanted_type="str" 1168 ) 1169 # version_capture is what the slp defines, the result overrides. Question: maybe it should error if it fails? 1170 (version_text, version_src, user_version) = self.__version_capture_slp( 1171 pkg_id, version_binary, version_display, display_name 1172 ) 1173 if not user_version: 1174 user_version = version_text 1175 1176 # log.trace('%s\\%s ver:%s src:%s', username or 'SYSTEM', pkg_id, version_text, version_src) 1177 1178 if username: 1179 dict_key = "{};{}".format( 1180 username, pkg_id 1181 ) # Use ; as its not a valid hostnmae char 1182 else: 1183 dict_key = pkg_id 1184 1185 # Guessing the architecture http://helpnet.flexerasoftware.com/isxhelp21/helplibrary/IHelp64BitSupport.htm 1186 # A 32 bit installed.exe can install a 64 bit app, but for it to write to 64bit reg it will 1187 # need to use WOW. So the following is a bit of a guess 1188 1189 if self.__version_only: 1190 # package name and package version list, are the only info being return 1191 if dict_key in self.__reg_software: 1192 if version_text not in self.__reg_software[dict_key]: 1193 # Not expecting the list to be big, simple search and insert 1194 insert_point = 0 1195 for ver_item in self.__reg_software[dict_key]: 1196 if LooseVersion(version_text) <= LooseVersion(ver_item): 1197 break 1198 insert_point += 1 1199 self.__reg_software[dict_key].insert(insert_point, version_text) 1200 else: 1201 # This code is here as it can happen, especially if the 1202 # package id provided by pkg_obj is simple. 1203 log.debug( 1204 "Found extra entries for '%s' with same version " 1205 "'%s', skipping entry '%s'", 1206 dict_key, 1207 version_text, 1208 key_software, 1209 ) 1210 else: 1211 self.__reg_software[dict_key] = [version_text] 1212 1213 return 1214 1215 if dict_key in self.__reg_software: 1216 data = self.__reg_software[dict_key] 1217 else: 1218 data = self.__reg_software[dict_key] = OrderedDict() 1219 1220 if sid: 1221 # HKEY_USERS has no 32bit and 64bit view like HKEY_LOCAL_MACHINE 1222 data.update({"arch": "unknown"}) 1223 else: 1224 arch_str = "x86" if use_32bit else "x64" 1225 if "arch" in data: 1226 if data["arch"] != arch_str: 1227 data["arch"] = "many" 1228 else: 1229 data.update({"arch": arch_str}) 1230 1231 if publisher: 1232 if "vendor" in data: 1233 if data["vendor"].lower() != publisher.lower(): 1234 data["vendor"] = "many" 1235 else: 1236 data["vendor"] = publisher 1237 1238 if "win_system_component" in data: 1239 if data["win_system_component"] != system_component: 1240 data["win_system_component"] = None 1241 else: 1242 data["win_system_component"] = system_component 1243 1244 data.update({"win_version_src": version_src}) 1245 1246 data.setdefault("version", {}) 1247 if version_text in data["version"]: 1248 if "win_install_count" in data["version"][version_text]: 1249 data["version"][version_text]["win_install_count"] += 1 1250 else: 1251 # This is only defined when we have the same item already 1252 data["version"][version_text]["win_install_count"] = 2 1253 else: 1254 data["version"][version_text] = OrderedDict() 1255 1256 version_data = data["version"][version_text] 1257 version_data.update({"win_display_name": display_name}) 1258 if uninstall_string: 1259 version_data.update({"win_uninstall_cmd": uninstall_string}) 1260 if uninstall_quiet_string: 1261 version_data.update({"win_uninstall_quiet_cmd": uninstall_quiet_string}) 1262 if uninstall_no_remove: 1263 version_data.update({"win_uninstall_no_remove": uninstall_no_remove}) 1264 1265 version_data.update({"win_product_code": key_software}) 1266 if version_display: 1267 version_data.update({"win_version_display": version_display}) 1268 if version_binary: 1269 version_data.update({"win_version_binary": version_binary}) 1270 if user_version: 1271 version_data.update({"win_version_user": user_version}) 1272 1273 # Determine Installer Product 1274 # 'NSIS:Language' 1275 # 'Inno Setup: Setup Version' 1276 if windows_installer or ( 1277 uninstall_string 1278 and re.search( 1279 r"MsiExec.exe\s|MsiExec\s", 1280 uninstall_string, 1281 flags=re.IGNORECASE + re.UNICODE, 1282 ) 1283 ): 1284 version_data.update({"win_installer_type": "winmsi"}) 1285 elif re.match(r"InstallShield_", key_software, re.IGNORECASE) is not None or ( 1286 uninstall_string 1287 and ( 1288 re.search( 1289 r"InstallShield", uninstall_string, flags=re.IGNORECASE + re.UNICODE 1290 ) 1291 is not None 1292 or re.search( 1293 r"isuninst\.exe.*\.isu", 1294 uninstall_string, 1295 flags=re.IGNORECASE + re.UNICODE, 1296 ) 1297 is not None 1298 ) 1299 ): 1300 version_data.update({"win_installer_type": "installshield"}) 1301 elif key_software.endswith("_is1") and reg_soft_info.get_install_value( 1302 "Inno Setup: Setup Version", wanted_type="str" 1303 ): 1304 version_data.update({"win_installer_type": "inno"}) 1305 elif uninstall_string and re.search( 1306 r".*\\uninstall.exe|.*\\uninst.exe", 1307 uninstall_string, 1308 flags=re.IGNORECASE + re.UNICODE, 1309 ): 1310 version_data.update({"win_installer_type": "nsis"}) 1311 else: 1312 version_data.update({"win_installer_type": "unknown"}) 1313 1314 # Update dict with information retrieved so far for detail results to be return 1315 # Do not add fields which are blank. 1316 language_number = reg_soft_info.get_install_value("Language") 1317 if ( 1318 isinstance(language_number, int) 1319 and language_number in locale.windows_locale 1320 ): 1321 version_data.update( 1322 {"win_language": locale.windows_locale[language_number]} 1323 ) 1324 1325 package_code = reg_soft_info.package_code 1326 if package_code: 1327 version_data.update({"win_package_code": package_code}) 1328 1329 upgrade_code = reg_soft_info.upgrade_code 1330 if upgrade_code: 1331 version_data.update({"win_upgrade_code": upgrade_code}) 1332 1333 is_minor_upgrade = reg_soft_info.is_install_true("IsMinorUpgrade") 1334 if is_minor_upgrade: 1335 version_data.update({"win_is_minor_upgrade": is_minor_upgrade}) 1336 1337 install_time = reg_soft_info.install_time 1338 if install_time: 1339 version_data.update( 1340 { 1341 "install_date": datetime.datetime.fromtimestamp( 1342 install_time 1343 ).isoformat() 1344 } 1345 ) 1346 version_data.update({"install_date_time_t": int(install_time)}) 1347 1348 for infokey, infotype, regfield_list in self.__uninstall_search_list: 1349 for regfield in regfield_list: 1350 strvalue = reg_soft_info.get_install_value( 1351 regfield, wanted_type=infotype 1352 ) 1353 if strvalue: 1354 version_data.update({infokey: strvalue}) 1355 break 1356 1357 for infokey, infotype, regfield_list in self.__products_search_list: 1358 for regfield in regfield_list: 1359 data = reg_soft_info.get_product_value(regfield, wanted_type=infotype) 1360 if data is not None: 1361 version_data.update({infokey: data}) 1362 break 1363 patch_list = reg_soft_info.list_patches 1364 if patch_list: 1365 version_data.update({"win_patches": patch_list}) 1366 1367 def __get_software_details(self, user_pkgs): 1368 """ 1369 This searches the uninstall keys in the registry to find 1370 a match in the sub keys, it will return a dict with the 1371 display name as the key and the version as the value 1372 .. sectionauthor:: Damon Atkins <https://github.com/damon-atkins> 1373 .. versionadded:: 2016.11.0 1374 """ 1375 1376 # FUNCTION MAIN CODE # 1377 # Search 64bit, on 64bit platform, on 32bit its ignored. 1378 if platform.architecture()[0] == "32bit": 1379 # Handle Python 32bit on 64&32 bit platform and Python 64bit 1380 if win32process.IsWow64Process(): # pylint: disable=no-member 1381 # 32bit python on a 64bit platform 1382 use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY} 1383 arch_list = [True, False] 1384 else: 1385 # 32bit python on a 32bit platform 1386 use_32bit_lookup = {True: 0, False: None} 1387 arch_list = [True] 1388 1389 else: 1390 # Python is 64bit therefore most be on 64bit System. 1391 use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0} 1392 arch_list = [True, False] 1393 1394 # Process software installed for the machine i.e. all users. 1395 for arch_flag in arch_list: 1396 key_search = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall" 1397 log.debug("SYSTEM processing 32bit:%s", arch_flag) 1398 handle = win32api.RegOpenKeyEx( # pylint: disable=no-member 1399 win32con.HKEY_LOCAL_MACHINE, 1400 key_search, 1401 0, 1402 win32con.KEY_READ | use_32bit_lookup[arch_flag], 1403 ) 1404 reg_key_all, _, _, _ = zip( 1405 *win32api.RegEnumKeyEx(handle) 1406 ) # pylint: disable=no-member 1407 win32api.RegCloseKey(handle) # pylint: disable=no-member 1408 for reg_key in reg_key_all: 1409 self.__collect_software_info(None, reg_key, arch_flag) 1410 1411 if not user_pkgs: 1412 return 1413 1414 # Process software installed under all USERs, this adds significate processing time. 1415 # There is not 32/64 bit registry redirection under user tree. 1416 log.debug("Processing user software... please wait") 1417 handle_sid = win32api.RegOpenKeyEx( # pylint: disable=no-member 1418 win32con.HKEY_USERS, "", 0, win32con.KEY_READ 1419 ) 1420 sid_all = [] 1421 for index in range( 1422 win32api.RegQueryInfoKey(handle_sid)[0] 1423 ): # pylint: disable=no-member 1424 sid_all.append( 1425 win32api.RegEnumKey(handle_sid, index) 1426 ) # pylint: disable=no-member 1427 1428 for sid in sid_all: 1429 if ( 1430 self.__sid_pattern.match(sid) is not None 1431 ): # S-1-5-18 needs to be ignored? 1432 user_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall".format( 1433 sid 1434 ) 1435 try: 1436 handle = win32api.RegOpenKeyEx( # pylint: disable=no-member 1437 handle_sid, user_uninstall_path, 0, win32con.KEY_READ 1438 ) 1439 except pywintypes.error as exc: # pylint: disable=no-member 1440 if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: 1441 # Not Found Uninstall under SID 1442 log.debug("Not Found %s", user_uninstall_path) 1443 continue 1444 else: 1445 raise 1446 try: 1447 reg_key_all, _, _, _ = zip( 1448 *win32api.RegEnumKeyEx(handle) 1449 ) # pylint: disable=no-member 1450 except ValueError: 1451 log.debug("No Entries Found %s", user_uninstall_path) 1452 reg_key_all = [] 1453 win32api.RegCloseKey(handle) # pylint: disable=no-member 1454 for reg_key in reg_key_all: 1455 self.__collect_software_info(sid, reg_key, False) 1456 win32api.RegCloseKey(handle_sid) # pylint: disable=no-member 1457 return 1458 1459 1460def __main(): 1461 """This module can also be run directly for testing 1462 Args: 1463 detail|list : Provide ``detail`` or version ``list``. 1464 system|system+user: System installed and System and User installs. 1465 """ 1466 if len(sys.argv) < 3: 1467 sys.stderr.write( 1468 "usage: {} <detail|list> <system|system+user>\n".format(sys.argv[0]) 1469 ) 1470 sys.exit(64) 1471 user_pkgs = False 1472 version_only = False 1473 if str(sys.argv[1]) == "list": 1474 version_only = True 1475 if str(sys.argv[2]) == "system+user": 1476 user_pkgs = True 1477 import salt.utils.json 1478 import timeit 1479 1480 def run(): 1481 """ 1482 Main run code, when this module is run directly 1483 """ 1484 pkg_list = WinSoftware(user_pkgs=user_pkgs, version_only=version_only) 1485 print( 1486 salt.utils.json.dumps(pkg_list.data, sort_keys=True, indent=4) 1487 ) # pylint: disable=superfluous-parens 1488 print("Total: {}".format(len(pkg_list))) # pylint: disable=superfluous-parens 1489 1490 print( 1491 "Time Taken: {}".format(timeit.timeit(run, number=1)) 1492 ) # pylint: disable=superfluous-parens 1493 1494 1495if __name__ == "__main__": 1496 __main() 1497