PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/pkg/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/pkg/win.py |
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 """ 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 pywintypes import win32api import win32con import win32process import win32security 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 from salt.utils.versions import Version # 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 = ( f"{sid}\\Software\\Microsoft\\Installer\\UpgradeCodes" ) 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 = ( f"Software\\Classes\\Installer\\Products\\{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 f"{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 next(self) 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 Version(ver1) > Version(ver2) else -1 @staticmethod def __latest_to_oldest_version(ver1, ver2): # pylint: disable=unused-private-member """ Used for sorting version numbers, latest to oldest """ return 1 if Version(ver1) < Version(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 = f"{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 f"{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 Version(version_text) <= Version(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(f"usage: {sys.argv[0]} <detail|list> <system|system+user>\n") 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 timeit import salt.utils.json 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(f"Total: {len(pkg_list)}") # pylint: disable=superfluous-parens print( f"Time Taken: {timeit.timeit(run, number=1)}" ) # pylint: disable=superfluous-parens if __name__ == "__main__": __main()