%PDF- %PDF-
Direktori : /lib/python2.7/site-packages/salt/utils/pkg/ |
Current File : //lib/python2.7/site-packages/salt/utils/pkg/win.py |
# -*- coding: utf-8 -*- # 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, six :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 _future_ python libs first & before any other code # pylint: disable=incompatible-py3-code from __future__ import absolute_import, print_function, unicode_literals __version__ = '0.1' # Import Standard libs import sys import re import platform import locale import logging import os.path import datetime import time import collections from functools import cmp_to_key # Import third party libs try: from salt.ext import six except ImportError: import six # pylint: disable=blacklisted-external-import 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(object): ''' 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 = ('{0}\\Software\\Microsoft\\Windows\\' 'CurrentVersion\\Uninstall\\{1}').format(sid, key_guid) if self.__squid: self.__reg_products_path = \ '{0}\\Software\\Classes\\Installer\\Products\\{1}'.format(sid, self.__squid) self.__reg_upgradecode_path = \ '{0}\\Software\\Microsoft\\Installer\\UpgradeCodes'.format(sid) self.__reg_patches_path = \ ('Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\' '{0}\\Products\\{1}\\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\\{0}'.format(key_guid) if self.__squid: self.__reg_products_path = \ 'Software\\Classes\\Installer\\Products\\{0}'.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\\{0}\\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, six.integer_types) and value == 1: return True elif (isinstance(value, six.string_types) and re.match(r'\d+', value, flags=re.IGNORECASE + re.UNICODE) is not None and six.text_type(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 six.PY2 and isinstance(item_value, six.string_types) and not isinstance(item_value, six.text_type): try: item_value = six.text_type(item_value, encoding='mbcs') except UnicodeError: pass 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 = '{0}\\{1}\\{2}'.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, six.integer_types) or patch_state != 1 or # 1 is Active, 2 is Superseded/Obsolute patch_display_name_type != 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 '{0}\\{1}'.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, six.integer_types): version_binary_raw = item_value if version_binary_raw: # Major.Minor.Build version_binary_text = '{0}.{1}.{2}'.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, six.string_types) 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(object): ''' 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 dependant 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 {0} is not valid: [{1}] {2}'.format(sid, exc.winerror, exc.strerror) ) try: name, domain, _account_type = win32security.LookupAccountSid(None, sid_bin) # pylint: disable=no-member user_name = '{0}\\{1}'.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 \'{0}\' username: [{1}] {2}'.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 '{0}@{1}'.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 \'{0}\' failed ' 'for pkg id: \'{1}\' it returned \'{2}\' \'{3}\' ' '\'{4}\''.format(six.text_type(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, six.integer_types) 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:: Carbon ''' # 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 = '{0}\\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: {0} <detail|list> <system|system+user>\n'.format(sys.argv[0])) sys.exit(64) user_pkgs = False version_only = False if six.text_type(sys.argv[1]) == 'list': version_only = True if six.text_type(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()