mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
1498 lines
55 KiB
Python
1498 lines
55 KiB
Python
# 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 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
|
|
|
|
try:
|
|
from salt.utils.versions import LooseVersion
|
|
except ImportError:
|
|
from setuptools._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 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("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()
|