Mini Shell
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()
Zerion Mini Shell 1.0