View file File name : win.py Content :# Copyright 2017 Damon Atkins # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. r""" Collect information about software installed on Windows OS ================ :maintainer: Salt Stack <https://github.com/saltstack> :codeauthor: Damon Atkins <https://github.com/damon-atkins> :maturity: new :depends: pywin32 :platform: windows Known Issue: install_date may not match Control Panel\Programs\Programs and Features """ # Note although this code will work with Python 2.7, win32api does not # support Unicode. i.e non ASCII characters may be returned with unexpected # results e.g. a '?' instead of the correct character # Python 3.6 or newer is recommended. import collections import datetime import locale import logging import os.path import platform import re import sys import time from functools import cmp_to_key __version__ = "0.1" try: import win32api import win32con import win32process import win32security import pywintypes import winerror except ImportError: if __name__ == "__main__": raise ImportError("Please install pywin32/pypiwin32") else: raise if __name__ == "__main__": LOG_CONSOLE = logging.StreamHandler() LOG_CONSOLE.setFormatter(logging.Formatter("[%(levelname)s]: %(message)s")) log = logging.getLogger(__name__) log.addHandler(LOG_CONSOLE) log.setLevel(logging.DEBUG) else: log = logging.getLogger(__name__) try: from salt.utils.odict import OrderedDict except ImportError: from collections import OrderedDict try: from salt.utils.versions import LooseVersion except ImportError: from distutils.version import LooseVersion # pylint: disable=blacklisted-module # pylint: disable=too-many-instance-attributes class RegSoftwareInfo: """ Retrieve Registry data on a single installed software item or component. Attribute: None :codeauthor: Damon Atkins <https://github.com/damon-atkins> """ # Variables shared by all instances __guid_pattern = re.compile( r"^\{(\w{8})-(\w{4})-(\w{4})-(\w\w)(\w\w)-(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)\}$" ) __squid_pattern = re.compile( r"^(\w{8})(\w{4})(\w{4})(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)$" ) __version_pattern = re.compile(r"\d+\.\d+\.\d+[\w.-]*|\d+\.\d+[\w.-]*") __upgrade_codes = {} __upgrade_code_have_scan = {} __reg_types = { "str": (win32con.REG_EXPAND_SZ, win32con.REG_SZ), "list": (win32con.REG_MULTI_SZ), "int": (win32con.REG_DWORD, win32con.REG_DWORD_BIG_ENDIAN, win32con.REG_QWORD), "bytes": (win32con.REG_BINARY), } # Search 64bit, on 64bit platform, on 32bit its ignored if platform.architecture()[0] == "32bit": # Handle Python 32bit on 64&32 bit platform and Python 64bit if win32process.IsWow64Process(): # pylint: disable=no-member # 32bit python on a 64bit platform __use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY} else: # 32bit python on a 32bit platform __use_32bit_lookup = {True: 0, False: None} else: __use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0} def __init__(self, key_guid, sid=None, use_32bit=False): """ Initialise against a software item or component. All software has a unique "Identifer" within the registry. This can be free form text/numbers e.g. "MySoftware" or GUID e.g. "{0EAF0D8F-C9CF-4350-BD9A-07EC66929E04}" Args: key_guid (str): Identifer. sid (str): Security IDentifier of the User or None for Computer/Machine. use_32bit (bool): Regisrty location of the Identifer. ``True`` 32 bit registry only meaning fully on 64 bit OS. """ self.__reg_key_guid = key_guid # also called IdentifyingNumber(wmic) self.__squid = "" self.__reg_products_path = "" self.__reg_upgradecode_path = "" self.__patch_list = None # If a valid GUID create the SQUID also. guid_match = self.__guid_pattern.match(key_guid) if guid_match is not None: for index in range(1, 12): # __guid_pattern breaks up the GUID self.__squid += guid_match.group(index)[::-1] if sid: # User data seems to be more spreadout within the registry. self.__reg_hive = "HKEY_USERS" self.__reg_32bit = False # Force to False self.__reg_32bit_access = ( 0 # HKEY_USERS does not have a 32bit and 64bit view ) self.__reg_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format( sid, key_guid ) if self.__squid: self.__reg_products_path = ( "{}\\Software\\Classes\\Installer\\Products\\{}".format( sid, self.__squid ) ) self.__reg_upgradecode_path = ( "{}\\Software\\Microsoft\\Installer\\UpgradeCodes".format(sid) ) self.__reg_patches_path = ( "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\" "{}\\Products\\{}\\Patches".format(sid, self.__squid) ) else: self.__reg_hive = "HKEY_LOCAL_MACHINE" self.__reg_32bit = use_32bit self.__reg_32bit_access = self.__use_32bit_lookup[use_32bit] self.__reg_uninstall_path = ( "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format( key_guid ) ) if self.__squid: self.__reg_products_path = ( "Software\\Classes\\Installer\\Products\\{}".format(self.__squid) ) self.__reg_upgradecode_path = ( "Software\\Classes\\Installer\\UpgradeCodes" ) self.__reg_patches_path = ( "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\" "S-1-5-18\\Products\\{}\\Patches".format(self.__squid) ) # OpenKey is expensive, open in advance and keep it open. # This must exist try: # pylint: disable=no-member self.__reg_uninstall_handle = win32api.RegOpenKeyEx( getattr(win32con, self.__reg_hive), self.__reg_uninstall_path, 0, win32con.KEY_READ | self.__reg_32bit_access, ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: log.error( "Software/Component Not Found key_guid: '%s', " "sid: '%s' , use_32bit: '%s'", key_guid, sid, use_32bit, ) raise # This must exist or have no errors self.__reg_products_handle = None if self.__squid: try: # pylint: disable=no-member self.__reg_products_handle = win32api.RegOpenKeyEx( getattr(win32con, self.__reg_hive), self.__reg_products_path, 0, win32con.KEY_READ | self.__reg_32bit_access, ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: log.debug( "Software/Component Not Found in Products section of registry " "key_guid: '%s', sid: '%s', use_32bit: '%s'", key_guid, sid, use_32bit, ) self.__squid = None # mark it as not a SQUID else: raise self.__mod_time1970 = 0 # pylint: disable=no-member mod_win_time = win32api.RegQueryInfoKeyW(self.__reg_uninstall_handle).get( "LastWriteTime", None ) # pylint: enable=no-member if mod_win_time: # at some stage __int__() was removed from pywintypes.datetime to return secs since 1970 if hasattr(mod_win_time, "utctimetuple"): self.__mod_time1970 = time.mktime(mod_win_time.utctimetuple()) elif hasattr(mod_win_time, "__int__"): self.__mod_time1970 = int(mod_win_time) def __squid_to_guid(self, squid): """ Squished GUID (SQUID) to GUID. A SQUID is a Squished/Compressed version of a GUID to use up less space in the registry. Args: squid (str): Squished GUID. Returns: str: the GUID if a valid SQUID provided. """ if not squid: return "" squid_match = self.__squid_pattern.match(squid) guid = "" if squid_match is not None: guid = ( "{" + squid_match.group(1)[::-1] + "-" + squid_match.group(2)[::-1] + "-" + squid_match.group(3)[::-1] + "-" + squid_match.group(4)[::-1] + squid_match.group(5)[::-1] + "-" ) for index in range(6, 12): guid += squid_match.group(index)[::-1] guid += "}" return guid @staticmethod def __one_equals_true(value): """ Test for ``1`` as a number or a string and return ``True`` if it is. Args: value: string or number or None. Returns: bool: ``True`` if 1 otherwise ``False``. """ if isinstance(value, int) and value == 1: return True elif ( isinstance(value, str) and re.match(r"\d+", value, flags=re.IGNORECASE + re.UNICODE) is not None and str(value) == "1" ): return True return False @staticmethod def __reg_query_value(handle, value_name): """ Calls RegQueryValueEx If PY2 ensure unicode string and expand REG_EXPAND_SZ before returning Remember to catch not found exceptions when calling. Args: handle (object): open registry handle. value_name (str): Name of the value you wished returned Returns: tuple: type, value """ # item_value, item_type = win32api.RegQueryValueEx(self.__reg_uninstall_handle, value_name) item_value, item_type = win32api.RegQueryValueEx( handle, value_name ) # pylint: disable=no-member if item_type == win32con.REG_EXPAND_SZ: # expects Unicode input win32api.ExpandEnvironmentStrings(item_value) # pylint: disable=no-member item_type = win32con.REG_SZ return item_value, item_type @property def install_time(self): """ Return the install time, or provide an estimate of install time. Installers or even self upgrading software must/should update the date held within InstallDate field when they change versions. Some installers do not set ``InstallDate`` at all so we use the last modified time on the registry key. Returns: int: Seconds since 1970 UTC. """ time1970 = self.__mod_time1970 # time of last resort try: # pylint: disable=no-member date_string, item_type = win32api.RegQueryValueEx( self.__reg_uninstall_handle, "InstallDate" ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: return time1970 # i.e. use time of last resort else: raise if item_type == win32con.REG_SZ: try: date_object = datetime.datetime.strptime(date_string, "%Y%m%d") time1970 = time.mktime(date_object.timetuple()) except ValueError: # date format is not correct pass return time1970 def get_install_value(self, value_name, wanted_type=None): """ For the uninstall section of the registry return the name value. Args: value_name (str): Registry value name. wanted_type (str): The type of value wanted if the type does not match None is return. wanted_type support values are ``str`` ``int`` ``list`` ``bytes``. Returns: value: Value requested or None if not found. """ try: item_value, item_type = self.__reg_query_value( self.__reg_uninstall_handle, value_name ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found return None raise if wanted_type and item_type not in self.__reg_types[wanted_type]: item_value = None return item_value def is_install_true(self, key): """ For the uninstall section check if name value is ``1``. Args: value_name (str): Registry value name. Returns: bool: ``True`` if ``1`` otherwise ``False``. """ return self.__one_equals_true(self.get_install_value(key)) def get_product_value(self, value_name, wanted_type=None): """ For the product section of the registry return the name value. Args: value_name (str): Registry value name. wanted_type (str): The type of value wanted if the type does not match None is return. wanted_type support values are ``str`` ``int`` ``list`` ``bytes``. Returns: value: Value requested or ``None`` if not found. """ if not self.__reg_products_handle: return None subkey, search_value_name = os.path.split(value_name) try: if subkey: handle = win32api.RegOpenKeyEx( # pylint: disable=no-member self.__reg_products_handle, subkey, 0, win32con.KEY_READ | self.__reg_32bit_access, ) item_value, item_type = self.__reg_query_value( handle, search_value_name ) win32api.RegCloseKey(handle) # pylint: disable=no-member else: item_value, item_type = win32api.RegQueryValueEx( self.__reg_products_handle, value_name ) # pylint: disable=no-member except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found return None raise if wanted_type and item_type not in self.__reg_types[wanted_type]: item_value = None return item_value @property def upgrade_code(self): """ For installers which follow the Microsoft Installer standard, returns the ``Upgrade code``. Returns: value (str): ``Upgrade code`` GUID for installed software. """ if not self.__squid: # Must have a valid squid for an upgrade code to exist return "" # GUID/SQUID are unique, so it does not matter if they are 32bit or # 64bit or user install so all items are cached into a single dict have_scan_key = "{}\\{}\\{}".format( self.__reg_hive, self.__reg_upgradecode_path, self.__reg_32bit ) if not self.__upgrade_codes or self.__reg_key_guid not in self.__upgrade_codes: # Read in the upgrade codes in this section of the registry. try: uc_handle = win32api.RegOpenKeyEx( getattr(win32con, self.__reg_hive), # pylint: disable=no-member self.__reg_upgradecode_path, 0, win32con.KEY_READ | self.__reg_32bit_access, ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found log.warning( "Not Found %s\\%s 32bit %s", self.__reg_hive, self.__reg_upgradecode_path, self.__reg_32bit, ) return "" raise squid_upgrade_code_all, _, _, suc_pytime = zip( *win32api.RegEnumKeyEx(uc_handle) ) # pylint: disable=no-member # Check if we have already scanned these upgrade codes before, and also # check if they have been updated in the registry since last time we scanned. if ( have_scan_key in self.__upgrade_code_have_scan and self.__upgrade_code_have_scan[have_scan_key] == ( squid_upgrade_code_all, suc_pytime, ) ): log.debug( "Scan skipped for upgrade codes, no changes (%s)", have_scan_key ) return "" # we have scanned this before and no new changes. # Go into each squid upgrade code and find all the related product codes. log.debug("Scan for upgrade codes (%s) for product codes", have_scan_key) for upgrade_code_squid in squid_upgrade_code_all: upgrade_code_guid = self.__squid_to_guid(upgrade_code_squid) pc_handle = win32api.RegOpenKeyEx( uc_handle, # pylint: disable=no-member upgrade_code_squid, 0, win32con.KEY_READ | self.__reg_32bit_access, ) _, pc_val_count, _ = win32api.RegQueryInfoKey( pc_handle ) # pylint: disable=no-member for item_index in range(pc_val_count): product_code_guid = self.__squid_to_guid( win32api.RegEnumValue(pc_handle, item_index)[0] ) # pylint: disable=no-member if product_code_guid: self.__upgrade_codes[product_code_guid] = upgrade_code_guid win32api.RegCloseKey(pc_handle) # pylint: disable=no-member win32api.RegCloseKey(uc_handle) # pylint: disable=no-member self.__upgrade_code_have_scan[have_scan_key] = ( squid_upgrade_code_all, suc_pytime, ) return self.__upgrade_codes.get(self.__reg_key_guid, "") @property def list_patches(self): """ For installers which follow the Microsoft Installer standard, returns a list of patches applied. Returns: value (list): Long name of the patch. """ if not self.__squid: # Must have a valid squid for an upgrade code to exist return [] if self.__patch_list is None: # Read in the upgrade codes in this section of the reg. try: pat_all_handle = win32api.RegOpenKeyEx( getattr(win32con, self.__reg_hive), # pylint: disable=no-member self.__reg_patches_path, 0, win32con.KEY_READ | self.__reg_32bit_access, ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found log.warning( "Not Found %s\\%s 32bit %s", self.__reg_hive, self.__reg_patches_path, self.__reg_32bit, ) return [] raise pc_sub_key_cnt, _, _ = win32api.RegQueryInfoKey( pat_all_handle ) # pylint: disable=no-member if not pc_sub_key_cnt: return [] squid_patch_all, _, _, _ = zip( *win32api.RegEnumKeyEx(pat_all_handle) ) # pylint: disable=no-member ret = [] # Scan the patches for the DisplayName of active patches. for patch_squid in squid_patch_all: try: patch_squid_handle = ( win32api.RegOpenKeyEx( # pylint: disable=no-member pat_all_handle, patch_squid, 0, win32con.KEY_READ | self.__reg_32bit_access, ) ) ( patch_display_name, patch_display_name_type, ) = self.__reg_query_value(patch_squid_handle, "DisplayName") patch_state, patch_state_type = self.__reg_query_value( patch_squid_handle, "State" ) if ( patch_state_type != win32con.REG_DWORD or not isinstance(patch_state_type, int) or patch_state != 1 or patch_display_name_type # 1 is Active, 2 is Superseded/Obsolute != win32con.REG_SZ ): continue win32api.RegCloseKey( patch_squid_handle ) # pylint: disable=no-member ret.append(patch_display_name) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: log.debug("skipped patch, not found %s", patch_squid) continue raise return ret @property def registry_path_text(self): """ Returns the uninstall path this object is associated with. Returns: str: <hive>\\<uninstall registry entry> """ return "{}\\{}".format(self.__reg_hive, self.__reg_uninstall_path) @property def registry_path(self): """ Returns the uninstall path this object is associated with. Returns: tuple: hive, uninstall registry entry path. """ return (self.__reg_hive, self.__reg_uninstall_path) @property def guid(self): """ Return GUID or Key. Returns: str: GUID or Key """ return self.__reg_key_guid @property def squid(self): """ Return SQUID of the GUID if a valid GUID. Returns: str: GUID """ return self.__squid @property def package_code(self): """ Return package code of the software. Returns: str: GUID """ return self.__squid_to_guid(self.get_product_value("PackageCode")) @property def version_binary(self): """ Return version number which is stored in binary format. Returns: str: <major 0-255>.<minior 0-255>.<build 0-65535> or None if not found """ # Under MSI 'Version' is a 'REG_DWORD' which then sets other registry # values like DisplayVersion to x.x.x to the same value. # However not everyone plays by the rules, so we need to check first. # version_binary_data will be None if the reg value does not exist. # Some installs set 'Version' to REG_SZ (string) which is not # the MSI standard try: item_value, item_type = self.__reg_query_value( self.__reg_uninstall_handle, "version" ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found return "", "" version_binary_text = "" version_src = "" if item_value: if item_type == win32con.REG_DWORD: if isinstance(item_value, int): version_binary_raw = item_value if version_binary_raw: # Major.Minor.Build version_binary_text = "{}.{}.{}".format( version_binary_raw >> 24 & 0xFF, version_binary_raw >> 16 & 0xFF, version_binary_raw & 0xFFFF, ) version_src = "binary-version" elif ( item_type == win32con.REG_SZ and isinstance(item_value, str) and self.__version_pattern.match(item_value) is not None ): # Hey, version should be a int/REG_DWORD, an installer has set # it to a string version_binary_text = item_value.strip(" ") version_src = "binary-version (string)" return (version_binary_text, version_src) class WinSoftware: """ Point in time snapshot of the software and components installed on a system. Attributes: None :codeauthor: Damon Atkins <https://github.com/damon-atkins> """ __sid_pattern = re.compile(r"^S-\d-\d-\d+$|^S-\d-\d-\d+-\d+-\d+-\d+-\d+$") __whitespace_pattern = re.compile(r"^\s*$", flags=re.UNICODE) # items we copy out of the uninstall section of the registry without further processing __uninstall_search_list = [ ("url", "str", ["URLInfoAbout", "HelpLink", "MoreInfoUrl", "UrlUpdateInfo"]), ("size", "int", ["Size", "EstimatedSize"]), ("win_comments", "str", ["Comments"]), ("win_release_type", "str", ["ReleaseType"]), ("win_product_id", "str", ["ProductID"]), ("win_product_codes", "str", ["ProductCodes"]), ("win_package_refs", "str", ["PackageRefs"]), ("win_install_location", "str", ["InstallLocation"]), ("win_install_src_dir", "str", ["InstallSource"]), ("win_parent_pkg_uid", "str", ["ParentKeyName"]), ("win_parent_name", "str", ["ParentDisplayName"]), ] # items we copy out of the products section of the registry without further processing __products_search_list = [ ("win_advertise_flags", "int", ["AdvertiseFlags"]), ("win_redeployment_flags", "int", ["DeploymentFlags"]), ("win_instance_type", "int", ["InstanceType"]), ("win_package_name", "str", ["SourceList\\PackageName"]), ] def __init__(self, version_only=False, user_pkgs=False, pkg_obj=None): """ Point in time snapshot of the software and components installed on a system. Args: version_only (bool): Provide list of versions installed instead of detail. user_pkgs (bool): Include software/components installed with user space. pkg_obj (object): If None (default) return default package naming standard and use default version capture methods (``DisplayVersion`` then ``Version``, otherwise ``0.0.0.0``) """ self.__pkg_obj = pkg_obj # must be set before calling get_software_details self.__version_only = version_only self.__reg_software = {} self.__get_software_details(user_pkgs=user_pkgs) self.__pkg_cnt = len(self.__reg_software) self.__iter_list = None @property def data(self): """ Returns the raw data Returns: dict: contents of the dict are dependent on the parameters passed when the class was initiated. """ return self.__reg_software @property def version_only(self): """ Returns True if class initiated with ``version_only=True`` Returns: bool: The value of ``version_only`` """ return self.__version_only def __len__(self): """ Returns total number of software/components installed. Returns: int: total number of software/components installed. """ return self.__pkg_cnt def __getitem__(self, pkg_id): """ Returns information on a package. Args: pkg_id (str): Package Id of the software/component Returns: dict or list: List if ``version_only`` is ``True`` otherwise dict """ if pkg_id in self.__reg_software: return self.__reg_software[pkg_id] else: raise KeyError(pkg_id) def __iter__(self): """ Standard interation class initialisation over package information. """ if self.__iter_list is not None: raise RuntimeError("Can only perform one iter at a time") self.__iter_list = collections.deque(sorted(self.__reg_software.keys())) return self def __next__(self): """ Returns next Package Id. Returns: str: Package Id """ try: return self.__iter_list.popleft() except IndexError: self.__iter_list = None raise StopIteration def next(self): """ Returns next Package Id. Returns: str: Package Id """ return self.__next__() def get(self, pkg_id, default_value=None): """ Returns information on a package. Args: pkg_id (str): Package Id of the software/component. default_value: Value to return when the Package Id is not found. Returns: dict or list: List if ``version_only`` is ``True`` otherwise dict """ return self.__reg_software.get(pkg_id, default_value) @staticmethod def __oldest_to_latest_version(ver1, ver2): """ Used for sorting version numbers oldest to latest """ return 1 if LooseVersion(ver1) > LooseVersion(ver2) else -1 @staticmethod def __latest_to_oldest_version(ver1, ver2): """ Used for sorting version numbers, latest to oldest """ return 1 if LooseVersion(ver1) < LooseVersion(ver2) else -1 def pkg_version_list(self, pkg_id): """ Returns information on a package. Args: pkg_id (str): Package Id of the software/component. Returns: list: List of version numbers installed. """ pkg_data = self.__reg_software.get(pkg_id, None) if not pkg_data: return [] if isinstance(pkg_data, list): # raw data is 'pkgid': [sorted version list] return pkg_data # already sorted oldest to newest # Must be a dict or OrderDict, and contain full details installed_versions = list(pkg_data.get("version").keys()) return sorted( installed_versions, key=cmp_to_key(self.__oldest_to_latest_version) ) def pkg_version_latest(self, pkg_id): """ Returns a package latest version installed out of all the versions currently installed. Args: pkg_id (str): Package Id of the software/component. Returns: str: Latest/Newest version number installed. """ return self.pkg_version_list(pkg_id)[-1] def pkg_version_oldest(self, pkg_id): """ Returns a package oldest version installed out of all the versions currently installed. Args: pkg_id (str): Package Id of the software/component. Returns: str: Oldest version number installed. """ return self.pkg_version_list(pkg_id)[0] @staticmethod def __sid_to_username(sid): """ Provided with a valid Windows Security Identifier (SID) and returns a Username Args: sid (str): Security Identifier (SID). Returns: str: Username in the format of username@realm or username@computer. """ if sid is None or sid == "": return "" try: sid_bin = win32security.GetBinarySid(sid) # pylint: disable=no-member except pywintypes.error as exc: # pylint: disable=no-member raise ValueError( "pkg: Software owned by {} is not valid: [{}] {}".format( sid, exc.winerror, exc.strerror ) ) try: name, domain, _account_type = win32security.LookupAccountSid( None, sid_bin ) # pylint: disable=no-member user_name = "{}\\{}".format(domain, name) except pywintypes.error as exc: # pylint: disable=no-member # if user does not exist... # winerror.ERROR_NONE_MAPPED = No mapping between account names and # security IDs was carried out. if exc.winerror == winerror.ERROR_NONE_MAPPED: # 1332 # As the sid is from the registry it should be valid # even if it cannot be lookedup, so the sid is returned return sid else: raise ValueError( "Failed looking up sid '{}' username: [{}] {}".format( sid, exc.winerror, exc.strerror ) ) try: user_principal = win32security.TranslateName( # pylint: disable=no-member user_name, win32api.NameSamCompatible, # pylint: disable=no-member win32api.NameUserPrincipal, ) # pylint: disable=no-member except pywintypes.error as exc: # pylint: disable=no-member # winerror.ERROR_NO_SUCH_DOMAIN The specified domain either does not exist # or could not be contacted, computer may not be part of a domain also # winerror.ERROR_INVALID_DOMAINNAME The format of the specified domain name is # invalid. e.g. S-1-5-19 which is a local account # winerror.ERROR_NONE_MAPPED No mapping between account names and security IDs was done. if exc.winerror in ( winerror.ERROR_NO_SUCH_DOMAIN, winerror.ERROR_INVALID_DOMAINNAME, winerror.ERROR_NONE_MAPPED, ): return "{}@{}".format(name.lower(), domain.lower()) else: raise return user_principal def __software_to_pkg_id(self, publisher, name, is_component, is_32bit): """ Determine the Package ID of a software/component using the software/component ``publisher``, ``name``, whether its a software or a component, and if its 32bit or 64bit archiecture. Args: publisher (str): Publisher of the software/component. name (str): Name of the software. is_component (bool): True if package is a component. is_32bit (bool): True if the software/component is 32bit architecture. Returns: str: Package Id """ if publisher: # remove , and lowercase as , are used as list separators pub_lc = publisher.replace(",", "").lower() else: # remove , and lowercase pub_lc = "NoValue" # Capitals/Special Value if name: name_lc = name.replace(",", "").lower() # remove , OR we do the URL Encode on chars we do not want e.g. \\ and , else: name_lc = "NoValue" # Capitals/Special Value if is_component: soft_type = "comp" else: soft_type = "soft" if is_32bit: soft_type += "32" # Tag only the 32bit only default_pkg_id = pub_lc + "\\\\" + name_lc + "\\\\" + soft_type # Check to see if class was initialise with pkg_obj with a method called # to_pkg_id, and if so use it for the naming standard instead of the default if self.__pkg_obj and hasattr(self.__pkg_obj, "to_pkg_id"): pkg_id = self.__pkg_obj.to_pkg_id(publisher, name, is_component, is_32bit) if pkg_id: return pkg_id return default_pkg_id def __version_capture_slp( self, pkg_id, version_binary, version_display, display_name ): """ This returns the version and where the version string came from, based on instructions under ``version_capture``, if ``version_capture`` is missing, it defaults to value of display-version. Args: pkg_id (str): Publisher of the software/component. version_binary (str): Name of the software. version_display (str): True if package is a component. display_name (str): True if the software/component is 32bit architecture. Returns: str: Package Id """ if self.__pkg_obj and hasattr(self.__pkg_obj, "version_capture"): version_str, src, version_user_str = self.__pkg_obj.version_capture( pkg_id, version_binary, version_display, display_name ) if src != "use-default" and version_str and src: return version_str, src, version_user_str elif src != "use-default": raise ValueError( "version capture within object '{}' failed " "for pkg id: '{}' it returned '{}' '{}' " "'{}'".format( str(self.__pkg_obj), pkg_id, version_str, src, version_user_str, ) ) # If self.__pkg_obj.version_capture() not defined defaults to using # version_display and if not valid then use version_binary, and as a last # result provide the version 0.0.0.0.0 to indicate version string was not determined. if ( version_display and re.match(r"\d+", version_display, flags=re.IGNORECASE + re.UNICODE) is not None ): version_str = version_display src = "display-version" elif ( version_binary and re.match(r"\d+", version_binary, flags=re.IGNORECASE + re.UNICODE) is not None ): version_str = version_binary src = "version-binary" else: src = "none" version_str = "0.0.0.0.0" # return version str, src of the version, "user" interpretation of the version # which by default is version_str return version_str, src, version_str def __collect_software_info(self, sid, key_software, use_32bit): """ Update data with the next software found """ reg_soft_info = RegSoftwareInfo(key_software, sid, use_32bit) # Check if the registry entry is a valid. # a) Cannot manage software without at least a display name display_name = reg_soft_info.get_install_value("DisplayName", wanted_type="str") if display_name is None or self.__whitespace_pattern.match(display_name): return # b) make sure its not an 'Hotfix', 'Update Rollup', 'Security Update', 'ServicePack' # General this is software which pre dates Windows 10 default_value = reg_soft_info.get_install_value("", wanted_type="str") release_type = reg_soft_info.get_install_value("ReleaseType", wanted_type="str") if ( re.match( r"^{.*\}\.KB\d{6,}$", key_software, flags=re.IGNORECASE + re.UNICODE ) is not None or (default_value and default_value.startswith(("KB", "kb", "Kb"))) or ( release_type and release_type in ("Hotfix", "Update Rollup", "Security Update", "ServicePack") ) ): log.debug("skipping hotfix/update/service pack %s", key_software) return # if NoRemove exists we would expect their to be no UninstallString uninstall_no_remove = reg_soft_info.is_install_true("NoRemove") uninstall_string = reg_soft_info.get_install_value("UninstallString") uninstall_quiet_string = reg_soft_info.get_install_value("QuietUninstallString") uninstall_modify_path = reg_soft_info.get_install_value("ModifyPath") windows_installer = reg_soft_info.is_install_true("WindowsInstaller") system_component = reg_soft_info.is_install_true("SystemComponent") publisher = reg_soft_info.get_install_value("Publisher", wanted_type="str") # UninstallString is optional if the installer is "windows installer"/MSI # However for it to appear in Control-Panel -> Program and Features -> Uninstall or change a program # the UninstallString needs to be set or ModifyPath set if ( uninstall_string is None and uninstall_quiet_string is None and uninstall_modify_path is None and (not windows_installer) ): return # Question: If uninstall string is not set and windows_installer should we set it # Question: if uninstall_quiet is not set ....... if sid: username = self.__sid_to_username(sid) else: username = None # We now have a valid software install or a system component pkg_id = self.__software_to_pkg_id( publisher, display_name, system_component, use_32bit ) version_binary, version_src = reg_soft_info.version_binary version_display = reg_soft_info.get_install_value( "DisplayVersion", wanted_type="str" ) # version_capture is what the slp defines, the result overrides. Question: maybe it should error if it fails? (version_text, version_src, user_version) = self.__version_capture_slp( pkg_id, version_binary, version_display, display_name ) if not user_version: user_version = version_text # log.trace('%s\\%s ver:%s src:%s', username or 'SYSTEM', pkg_id, version_text, version_src) if username: dict_key = "{};{}".format( username, pkg_id ) # Use ; as its not a valid hostnmae char else: dict_key = pkg_id # Guessing the architecture http://helpnet.flexerasoftware.com/isxhelp21/helplibrary/IHelp64BitSupport.htm # A 32 bit installed.exe can install a 64 bit app, but for it to write to 64bit reg it will # need to use WOW. So the following is a bit of a guess if self.__version_only: # package name and package version list, are the only info being return if dict_key in self.__reg_software: if version_text not in self.__reg_software[dict_key]: # Not expecting the list to be big, simple search and insert insert_point = 0 for ver_item in self.__reg_software[dict_key]: if LooseVersion(version_text) <= LooseVersion(ver_item): break insert_point += 1 self.__reg_software[dict_key].insert(insert_point, version_text) else: # This code is here as it can happen, especially if the # package id provided by pkg_obj is simple. log.debug( "Found extra entries for '%s' with same version " "'%s', skipping entry '%s'", dict_key, version_text, key_software, ) else: self.__reg_software[dict_key] = [version_text] return if dict_key in self.__reg_software: data = self.__reg_software[dict_key] else: data = self.__reg_software[dict_key] = OrderedDict() if sid: # HKEY_USERS has no 32bit and 64bit view like HKEY_LOCAL_MACHINE data.update({"arch": "unknown"}) else: arch_str = "x86" if use_32bit else "x64" if "arch" in data: if data["arch"] != arch_str: data["arch"] = "many" else: data.update({"arch": arch_str}) if publisher: if "vendor" in data: if data["vendor"].lower() != publisher.lower(): data["vendor"] = "many" else: data["vendor"] = publisher if "win_system_component" in data: if data["win_system_component"] != system_component: data["win_system_component"] = None else: data["win_system_component"] = system_component data.update({"win_version_src": version_src}) data.setdefault("version", {}) if version_text in data["version"]: if "win_install_count" in data["version"][version_text]: data["version"][version_text]["win_install_count"] += 1 else: # This is only defined when we have the same item already data["version"][version_text]["win_install_count"] = 2 else: data["version"][version_text] = OrderedDict() version_data = data["version"][version_text] version_data.update({"win_display_name": display_name}) if uninstall_string: version_data.update({"win_uninstall_cmd": uninstall_string}) if uninstall_quiet_string: version_data.update({"win_uninstall_quiet_cmd": uninstall_quiet_string}) if uninstall_no_remove: version_data.update({"win_uninstall_no_remove": uninstall_no_remove}) version_data.update({"win_product_code": key_software}) if version_display: version_data.update({"win_version_display": version_display}) if version_binary: version_data.update({"win_version_binary": version_binary}) if user_version: version_data.update({"win_version_user": user_version}) # Determine Installer Product # 'NSIS:Language' # 'Inno Setup: Setup Version' if windows_installer or ( uninstall_string and re.search( r"MsiExec.exe\s|MsiExec\s", uninstall_string, flags=re.IGNORECASE + re.UNICODE, ) ): version_data.update({"win_installer_type": "winmsi"}) elif re.match(r"InstallShield_", key_software, re.IGNORECASE) is not None or ( uninstall_string and ( re.search( r"InstallShield", uninstall_string, flags=re.IGNORECASE + re.UNICODE ) is not None or re.search( r"isuninst\.exe.*\.isu", uninstall_string, flags=re.IGNORECASE + re.UNICODE, ) is not None ) ): version_data.update({"win_installer_type": "installshield"}) elif key_software.endswith("_is1") and reg_soft_info.get_install_value( "Inno Setup: Setup Version", wanted_type="str" ): version_data.update({"win_installer_type": "inno"}) elif uninstall_string and re.search( r".*\\uninstall.exe|.*\\uninst.exe", uninstall_string, flags=re.IGNORECASE + re.UNICODE, ): version_data.update({"win_installer_type": "nsis"}) else: version_data.update({"win_installer_type": "unknown"}) # Update dict with information retrieved so far for detail results to be return # Do not add fields which are blank. language_number = reg_soft_info.get_install_value("Language") if ( isinstance(language_number, int) and language_number in locale.windows_locale ): version_data.update( {"win_language": locale.windows_locale[language_number]} ) package_code = reg_soft_info.package_code if package_code: version_data.update({"win_package_code": package_code}) upgrade_code = reg_soft_info.upgrade_code if upgrade_code: version_data.update({"win_upgrade_code": upgrade_code}) is_minor_upgrade = reg_soft_info.is_install_true("IsMinorUpgrade") if is_minor_upgrade: version_data.update({"win_is_minor_upgrade": is_minor_upgrade}) install_time = reg_soft_info.install_time if install_time: version_data.update( { "install_date": datetime.datetime.fromtimestamp( install_time ).isoformat() } ) version_data.update({"install_date_time_t": int(install_time)}) for infokey, infotype, regfield_list in self.__uninstall_search_list: for regfield in regfield_list: strvalue = reg_soft_info.get_install_value( regfield, wanted_type=infotype ) if strvalue: version_data.update({infokey: strvalue}) break for infokey, infotype, regfield_list in self.__products_search_list: for regfield in regfield_list: data = reg_soft_info.get_product_value(regfield, wanted_type=infotype) if data is not None: version_data.update({infokey: data}) break patch_list = reg_soft_info.list_patches if patch_list: version_data.update({"win_patches": patch_list}) def __get_software_details(self, user_pkgs): """ This searches the uninstall keys in the registry to find a match in the sub keys, it will return a dict with the display name as the key and the version as the value .. sectionauthor:: Damon Atkins <https://github.com/damon-atkins> .. versionadded:: 2016.11.0 """ # FUNCTION MAIN CODE # # Search 64bit, on 64bit platform, on 32bit its ignored. if platform.architecture()[0] == "32bit": # Handle Python 32bit on 64&32 bit platform and Python 64bit if win32process.IsWow64Process(): # pylint: disable=no-member # 32bit python on a 64bit platform use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY} arch_list = [True, False] else: # 32bit python on a 32bit platform use_32bit_lookup = {True: 0, False: None} arch_list = [True] else: # Python is 64bit therefore most be on 64bit System. use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0} arch_list = [True, False] # Process software installed for the machine i.e. all users. for arch_flag in arch_list: key_search = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall" log.debug("SYSTEM processing 32bit:%s", arch_flag) handle = win32api.RegOpenKeyEx( # pylint: disable=no-member win32con.HKEY_LOCAL_MACHINE, key_search, 0, win32con.KEY_READ | use_32bit_lookup[arch_flag], ) reg_key_all, _, _, _ = zip( *win32api.RegEnumKeyEx(handle) ) # pylint: disable=no-member win32api.RegCloseKey(handle) # pylint: disable=no-member for reg_key in reg_key_all: self.__collect_software_info(None, reg_key, arch_flag) if not user_pkgs: return # Process software installed under all USERs, this adds significate processing time. # There is not 32/64 bit registry redirection under user tree. log.debug("Processing user software... please wait") handle_sid = win32api.RegOpenKeyEx( # pylint: disable=no-member win32con.HKEY_USERS, "", 0, win32con.KEY_READ ) sid_all = [] for index in range( win32api.RegQueryInfoKey(handle_sid)[0] ): # pylint: disable=no-member sid_all.append( win32api.RegEnumKey(handle_sid, index) ) # pylint: disable=no-member for sid in sid_all: if ( self.__sid_pattern.match(sid) is not None ): # S-1-5-18 needs to be ignored? user_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall".format( sid ) try: handle = win32api.RegOpenKeyEx( # pylint: disable=no-member handle_sid, user_uninstall_path, 0, win32con.KEY_READ ) except pywintypes.error as exc: # pylint: disable=no-member if exc.winerror == winerror.ERROR_FILE_NOT_FOUND: # Not Found Uninstall under SID log.debug("Not Found %s", user_uninstall_path) continue else: raise try: reg_key_all, _, _, _ = zip( *win32api.RegEnumKeyEx(handle) ) # pylint: disable=no-member except ValueError: log.debug("No Entries Found %s", user_uninstall_path) reg_key_all = [] win32api.RegCloseKey(handle) # pylint: disable=no-member for reg_key in reg_key_all: self.__collect_software_info(sid, reg_key, False) win32api.RegCloseKey(handle_sid) # pylint: disable=no-member return def __main(): """This module can also be run directly for testing Args: detail|list : Provide ``detail`` or version ``list``. system|system+user: System installed and System and User installs. """ if len(sys.argv) < 3: sys.stderr.write( "usage: {} <detail|list> <system|system+user>\n".format(sys.argv[0]) ) sys.exit(64) user_pkgs = False version_only = False if str(sys.argv[1]) == "list": version_only = True if str(sys.argv[2]) == "system+user": user_pkgs = True import salt.utils.json import timeit def run(): """ Main run code, when this module is run directly """ pkg_list = WinSoftware(user_pkgs=user_pkgs, version_only=version_only) print( salt.utils.json.dumps(pkg_list.data, sort_keys=True, indent=4) ) # pylint: disable=superfluous-parens print("Total: {}".format(len(pkg_list))) # pylint: disable=superfluous-parens print( "Time Taken: {}".format(timeit.timeit(run, number=1)) ) # pylint: disable=superfluous-parens if __name__ == "__main__": __main()