1# Copyright 2017 Damon Atkins
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14r"""
15Collect information about software installed on Windows OS
16================
17
18:maintainer: Salt Stack <https://github.com/saltstack>
19:codeauthor: Damon Atkins <https://github.com/damon-atkins>
20:maturity: new
21:depends: pywin32
22:platform: windows
23
24Known Issue: install_date may not match Control Panel\Programs\Programs and Features
25"""
26
27# Note although this code will work with Python 2.7, win32api does not
28# support Unicode. i.e non ASCII characters may be returned with unexpected
29# results e.g. a '?' instead of the correct character
30# Python 3.6 or newer is recommended.
31
32import collections
33import datetime
34import locale
35import logging
36import os.path
37import platform
38import re
39import sys
40import time
41from functools import cmp_to_key
42
43__version__ = "0.1"
44
45try:
46    import win32api
47    import win32con
48    import win32process
49    import win32security
50    import pywintypes
51    import winerror
52
53except ImportError:
54    if __name__ == "__main__":
55        raise ImportError("Please install pywin32/pypiwin32")
56    else:
57        raise
58
59
60if __name__ == "__main__":
61    LOG_CONSOLE = logging.StreamHandler()
62    LOG_CONSOLE.setFormatter(logging.Formatter("[%(levelname)s]: %(message)s"))
63    log = logging.getLogger(__name__)
64    log.addHandler(LOG_CONSOLE)
65    log.setLevel(logging.DEBUG)
66else:
67    log = logging.getLogger(__name__)
68
69
70try:
71    from salt.utils.odict import OrderedDict
72except ImportError:
73    from collections import OrderedDict
74
75try:
76    from salt.utils.versions import LooseVersion
77except ImportError:
78    from distutils.version import LooseVersion  # pylint: disable=blacklisted-module
79
80
81# pylint: disable=too-many-instance-attributes
82
83
84class RegSoftwareInfo:
85    """
86    Retrieve Registry data on a single installed software item or component.
87
88    Attribute:
89        None
90
91    :codeauthor: Damon Atkins <https://github.com/damon-atkins>
92    """
93
94    # Variables shared by all instances
95    __guid_pattern = re.compile(
96        r"^\{(\w{8})-(\w{4})-(\w{4})-(\w\w)(\w\w)-(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)\}$"
97    )
98    __squid_pattern = re.compile(
99        r"^(\w{8})(\w{4})(\w{4})(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)$"
100    )
101    __version_pattern = re.compile(r"\d+\.\d+\.\d+[\w.-]*|\d+\.\d+[\w.-]*")
102    __upgrade_codes = {}
103    __upgrade_code_have_scan = {}
104
105    __reg_types = {
106        "str": (win32con.REG_EXPAND_SZ, win32con.REG_SZ),
107        "list": (win32con.REG_MULTI_SZ),
108        "int": (win32con.REG_DWORD, win32con.REG_DWORD_BIG_ENDIAN, win32con.REG_QWORD),
109        "bytes": (win32con.REG_BINARY),
110    }
111
112    # Search 64bit, on 64bit platform, on 32bit its ignored
113    if platform.architecture()[0] == "32bit":
114        # Handle Python 32bit on 64&32 bit platform and Python 64bit
115        if win32process.IsWow64Process():  # pylint: disable=no-member
116            # 32bit python on a 64bit platform
117            __use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY}
118        else:
119            # 32bit python on a 32bit platform
120            __use_32bit_lookup = {True: 0, False: None}
121    else:
122        __use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0}
123
124    def __init__(self, key_guid, sid=None, use_32bit=False):
125        """
126        Initialise against a software item or component.
127
128        All software has a unique "Identifer" within the registry. This can be free
129        form text/numbers e.g. "MySoftware" or
130        GUID e.g. "{0EAF0D8F-C9CF-4350-BD9A-07EC66929E04}"
131
132        Args:
133            key_guid (str): Identifer.
134            sid (str): Security IDentifier of the User or None for Computer/Machine.
135            use_32bit (bool):
136                Regisrty location of the Identifer. ``True`` 32 bit registry only
137                meaning fully on 64 bit OS.
138        """
139        self.__reg_key_guid = key_guid  # also called IdentifyingNumber(wmic)
140        self.__squid = ""
141        self.__reg_products_path = ""
142        self.__reg_upgradecode_path = ""
143        self.__patch_list = None
144
145        # If a valid GUID create the SQUID also.
146        guid_match = self.__guid_pattern.match(key_guid)
147        if guid_match is not None:
148            for index in range(1, 12):
149                # __guid_pattern breaks up the GUID
150                self.__squid += guid_match.group(index)[::-1]
151
152        if sid:
153            # User data seems to be more spreadout within the registry.
154            self.__reg_hive = "HKEY_USERS"
155            self.__reg_32bit = False  # Force to False
156            self.__reg_32bit_access = (
157                0  # HKEY_USERS does not have a 32bit and 64bit view
158            )
159            self.__reg_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format(
160                sid, key_guid
161            )
162            if self.__squid:
163                self.__reg_products_path = (
164                    "{}\\Software\\Classes\\Installer\\Products\\{}".format(
165                        sid, self.__squid
166                    )
167                )
168                self.__reg_upgradecode_path = (
169                    "{}\\Software\\Microsoft\\Installer\\UpgradeCodes".format(sid)
170                )
171                self.__reg_patches_path = (
172                    "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\"
173                    "{}\\Products\\{}\\Patches".format(sid, self.__squid)
174                )
175        else:
176            self.__reg_hive = "HKEY_LOCAL_MACHINE"
177            self.__reg_32bit = use_32bit
178            self.__reg_32bit_access = self.__use_32bit_lookup[use_32bit]
179            self.__reg_uninstall_path = (
180                "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}".format(
181                    key_guid
182                )
183            )
184            if self.__squid:
185                self.__reg_products_path = (
186                    "Software\\Classes\\Installer\\Products\\{}".format(self.__squid)
187                )
188                self.__reg_upgradecode_path = (
189                    "Software\\Classes\\Installer\\UpgradeCodes"
190                )
191                self.__reg_patches_path = (
192                    "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\"
193                    "S-1-5-18\\Products\\{}\\Patches".format(self.__squid)
194                )
195
196        # OpenKey is expensive, open in advance and keep it open.
197        # This must exist
198        try:
199            # pylint: disable=no-member
200            self.__reg_uninstall_handle = win32api.RegOpenKeyEx(
201                getattr(win32con, self.__reg_hive),
202                self.__reg_uninstall_path,
203                0,
204                win32con.KEY_READ | self.__reg_32bit_access,
205            )
206        except pywintypes.error as exc:  # pylint: disable=no-member
207            if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
208                log.error(
209                    "Software/Component Not Found  key_guid: '%s', "
210                    "sid: '%s' , use_32bit: '%s'",
211                    key_guid,
212                    sid,
213                    use_32bit,
214                )
215            raise  # This must exist or have no errors
216
217        self.__reg_products_handle = None
218        if self.__squid:
219            try:
220                # pylint: disable=no-member
221                self.__reg_products_handle = win32api.RegOpenKeyEx(
222                    getattr(win32con, self.__reg_hive),
223                    self.__reg_products_path,
224                    0,
225                    win32con.KEY_READ | self.__reg_32bit_access,
226                )
227            except pywintypes.error as exc:  # pylint: disable=no-member
228                if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
229                    log.debug(
230                        "Software/Component Not Found in Products section of registry "
231                        "key_guid: '%s', sid: '%s', use_32bit: '%s'",
232                        key_guid,
233                        sid,
234                        use_32bit,
235                    )
236                    self.__squid = None  # mark it as not a SQUID
237                else:
238                    raise
239
240        self.__mod_time1970 = 0
241        # pylint: disable=no-member
242        mod_win_time = win32api.RegQueryInfoKeyW(self.__reg_uninstall_handle).get(
243            "LastWriteTime", None
244        )
245        # pylint: enable=no-member
246        if mod_win_time:
247            # at some stage __int__() was removed from pywintypes.datetime to return secs since 1970
248            if hasattr(mod_win_time, "utctimetuple"):
249                self.__mod_time1970 = time.mktime(mod_win_time.utctimetuple())
250            elif hasattr(mod_win_time, "__int__"):
251                self.__mod_time1970 = int(mod_win_time)
252
253    def __squid_to_guid(self, squid):
254        """
255        Squished GUID (SQUID) to GUID.
256
257        A SQUID is a Squished/Compressed version of a GUID to use up less space
258        in the registry.
259
260        Args:
261            squid (str): Squished GUID.
262
263        Returns:
264            str: the GUID if a valid SQUID provided.
265        """
266        if not squid:
267            return ""
268        squid_match = self.__squid_pattern.match(squid)
269        guid = ""
270        if squid_match is not None:
271            guid = (
272                "{"
273                + squid_match.group(1)[::-1]
274                + "-"
275                + squid_match.group(2)[::-1]
276                + "-"
277                + squid_match.group(3)[::-1]
278                + "-"
279                + squid_match.group(4)[::-1]
280                + squid_match.group(5)[::-1]
281                + "-"
282            )
283            for index in range(6, 12):
284                guid += squid_match.group(index)[::-1]
285            guid += "}"
286        return guid
287
288    @staticmethod
289    def __one_equals_true(value):
290        """
291        Test for ``1`` as a number or a string and return ``True`` if it is.
292
293        Args:
294            value: string or number or None.
295
296        Returns:
297            bool: ``True`` if 1 otherwise ``False``.
298        """
299        if isinstance(value, int) and value == 1:
300            return True
301        elif (
302            isinstance(value, str)
303            and re.match(r"\d+", value, flags=re.IGNORECASE + re.UNICODE) is not None
304            and str(value) == "1"
305        ):
306            return True
307        return False
308
309    @staticmethod
310    def __reg_query_value(handle, value_name):
311        """
312        Calls RegQueryValueEx
313
314        If PY2 ensure unicode string and expand REG_EXPAND_SZ before returning
315        Remember to catch not found exceptions when calling.
316
317        Args:
318            handle (object): open registry handle.
319            value_name (str): Name of the value you wished returned
320
321        Returns:
322            tuple: type, value
323        """
324        # item_value, item_type = win32api.RegQueryValueEx(self.__reg_uninstall_handle, value_name)
325        item_value, item_type = win32api.RegQueryValueEx(
326            handle, value_name
327        )  # pylint: disable=no-member
328        if item_type == win32con.REG_EXPAND_SZ:
329            # expects Unicode input
330            win32api.ExpandEnvironmentStrings(item_value)  # pylint: disable=no-member
331            item_type = win32con.REG_SZ
332        return item_value, item_type
333
334    @property
335    def install_time(self):
336        """
337        Return the install time, or provide an estimate of install time.
338
339        Installers or even self upgrading software must/should update the date
340        held within InstallDate field when they change versions. Some installers
341        do not set ``InstallDate`` at all so we use the last modified time on the
342        registry key.
343
344        Returns:
345            int: Seconds since 1970 UTC.
346        """
347        time1970 = self.__mod_time1970  # time of last resort
348        try:
349            # pylint: disable=no-member
350            date_string, item_type = win32api.RegQueryValueEx(
351                self.__reg_uninstall_handle, "InstallDate"
352            )
353        except pywintypes.error as exc:  # pylint: disable=no-member
354            if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
355                return time1970  # i.e. use time of last resort
356            else:
357                raise
358
359        if item_type == win32con.REG_SZ:
360            try:
361                date_object = datetime.datetime.strptime(date_string, "%Y%m%d")
362                time1970 = time.mktime(date_object.timetuple())
363            except ValueError:  # date format is not correct
364                pass
365
366        return time1970
367
368    def get_install_value(self, value_name, wanted_type=None):
369        """
370        For the uninstall section of the registry return the name value.
371
372        Args:
373            value_name (str): Registry value name.
374            wanted_type (str):
375                The type of value wanted if the type does not match
376                None is return. wanted_type support values are
377                ``str`` ``int`` ``list`` ``bytes``.
378
379        Returns:
380            value: Value requested or None if not found.
381        """
382        try:
383            item_value, item_type = self.__reg_query_value(
384                self.__reg_uninstall_handle, value_name
385            )
386        except pywintypes.error as exc:  # pylint: disable=no-member
387            if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
388                # Not Found
389                return None
390            raise
391
392        if wanted_type and item_type not in self.__reg_types[wanted_type]:
393            item_value = None
394
395        return item_value
396
397    def is_install_true(self, key):
398        """
399        For the uninstall section check if name value is ``1``.
400
401        Args:
402            value_name (str): Registry value name.
403
404        Returns:
405            bool: ``True`` if ``1`` otherwise ``False``.
406        """
407        return self.__one_equals_true(self.get_install_value(key))
408
409    def get_product_value(self, value_name, wanted_type=None):
410        """
411        For the product section of the registry return the name value.
412
413        Args:
414            value_name (str): Registry value name.
415            wanted_type (str):
416                The type of value wanted if the type does not match
417                None is return. wanted_type support values are
418                ``str`` ``int`` ``list`` ``bytes``.
419
420        Returns:
421            value: Value requested or ``None`` if not found.
422        """
423        if not self.__reg_products_handle:
424            return None
425        subkey, search_value_name = os.path.split(value_name)
426        try:
427            if subkey:
428
429                handle = win32api.RegOpenKeyEx(  # pylint: disable=no-member
430                    self.__reg_products_handle,
431                    subkey,
432                    0,
433                    win32con.KEY_READ | self.__reg_32bit_access,
434                )
435                item_value, item_type = self.__reg_query_value(
436                    handle, search_value_name
437                )
438                win32api.RegCloseKey(handle)  # pylint: disable=no-member
439            else:
440                item_value, item_type = win32api.RegQueryValueEx(
441                    self.__reg_products_handle, value_name
442                )  # pylint: disable=no-member
443        except pywintypes.error as exc:  # pylint: disable=no-member
444            if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
445                # Not Found
446                return None
447            raise
448
449        if wanted_type and item_type not in self.__reg_types[wanted_type]:
450            item_value = None
451        return item_value
452
453    @property
454    def upgrade_code(self):
455        """
456        For installers which follow the Microsoft Installer standard, returns
457        the ``Upgrade code``.
458
459        Returns:
460            value (str): ``Upgrade code`` GUID for installed software.
461        """
462        if not self.__squid:
463            # Must have a valid squid for an upgrade code to exist
464            return ""
465
466        # GUID/SQUID are unique, so it does not matter if they are 32bit or
467        # 64bit or user install so all items are cached into a single dict
468        have_scan_key = "{}\\{}\\{}".format(
469            self.__reg_hive, self.__reg_upgradecode_path, self.__reg_32bit
470        )
471        if not self.__upgrade_codes or self.__reg_key_guid not in self.__upgrade_codes:
472            # Read in the upgrade codes in this section of the registry.
473            try:
474                uc_handle = win32api.RegOpenKeyEx(
475                    getattr(win32con, self.__reg_hive),  # pylint: disable=no-member
476                    self.__reg_upgradecode_path,
477                    0,
478                    win32con.KEY_READ | self.__reg_32bit_access,
479                )
480            except pywintypes.error as exc:  # pylint: disable=no-member
481                if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
482                    # Not Found
483                    log.warning(
484                        "Not Found %s\\%s 32bit %s",
485                        self.__reg_hive,
486                        self.__reg_upgradecode_path,
487                        self.__reg_32bit,
488                    )
489                    return ""
490                raise
491            squid_upgrade_code_all, _, _, suc_pytime = zip(
492                *win32api.RegEnumKeyEx(uc_handle)
493            )  # pylint: disable=no-member
494
495            # Check if we have already scanned these upgrade codes before, and also
496            # check if they have been updated in the registry since last time we scanned.
497            if (
498                have_scan_key in self.__upgrade_code_have_scan
499                and self.__upgrade_code_have_scan[have_scan_key]
500                == (
501                    squid_upgrade_code_all,
502                    suc_pytime,
503                )
504            ):
505                log.debug(
506                    "Scan skipped for upgrade codes, no changes (%s)", have_scan_key
507                )
508                return ""  # we have scanned this before and no new changes.
509
510            # Go into each squid upgrade code and find all the related product codes.
511            log.debug("Scan for upgrade codes (%s) for product codes", have_scan_key)
512            for upgrade_code_squid in squid_upgrade_code_all:
513                upgrade_code_guid = self.__squid_to_guid(upgrade_code_squid)
514                pc_handle = win32api.RegOpenKeyEx(
515                    uc_handle,  # pylint: disable=no-member
516                    upgrade_code_squid,
517                    0,
518                    win32con.KEY_READ | self.__reg_32bit_access,
519                )
520                _, pc_val_count, _ = win32api.RegQueryInfoKey(
521                    pc_handle
522                )  # pylint: disable=no-member
523                for item_index in range(pc_val_count):
524                    product_code_guid = self.__squid_to_guid(
525                        win32api.RegEnumValue(pc_handle, item_index)[0]
526                    )  # pylint: disable=no-member
527                    if product_code_guid:
528                        self.__upgrade_codes[product_code_guid] = upgrade_code_guid
529                win32api.RegCloseKey(pc_handle)  # pylint: disable=no-member
530
531            win32api.RegCloseKey(uc_handle)  # pylint: disable=no-member
532            self.__upgrade_code_have_scan[have_scan_key] = (
533                squid_upgrade_code_all,
534                suc_pytime,
535            )
536
537        return self.__upgrade_codes.get(self.__reg_key_guid, "")
538
539    @property
540    def list_patches(self):
541        """
542        For installers which follow the Microsoft Installer standard, returns
543        a list of patches applied.
544
545        Returns:
546            value (list): Long name of the patch.
547        """
548        if not self.__squid:
549            # Must have a valid squid for an upgrade code to exist
550            return []
551
552        if self.__patch_list is None:
553            # Read in the upgrade codes in this section of the reg.
554            try:
555                pat_all_handle = win32api.RegOpenKeyEx(
556                    getattr(win32con, self.__reg_hive),  # pylint: disable=no-member
557                    self.__reg_patches_path,
558                    0,
559                    win32con.KEY_READ | self.__reg_32bit_access,
560                )
561            except pywintypes.error as exc:  # pylint: disable=no-member
562                if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
563                    # Not Found
564                    log.warning(
565                        "Not Found %s\\%s 32bit %s",
566                        self.__reg_hive,
567                        self.__reg_patches_path,
568                        self.__reg_32bit,
569                    )
570                    return []
571                raise
572
573            pc_sub_key_cnt, _, _ = win32api.RegQueryInfoKey(
574                pat_all_handle
575            )  # pylint: disable=no-member
576            if not pc_sub_key_cnt:
577                return []
578            squid_patch_all, _, _, _ = zip(
579                *win32api.RegEnumKeyEx(pat_all_handle)
580            )  # pylint: disable=no-member
581
582            ret = []
583            # Scan the patches for the DisplayName of active patches.
584            for patch_squid in squid_patch_all:
585                try:
586                    patch_squid_handle = (
587                        win32api.RegOpenKeyEx(  # pylint: disable=no-member
588                            pat_all_handle,
589                            patch_squid,
590                            0,
591                            win32con.KEY_READ | self.__reg_32bit_access,
592                        )
593                    )
594                    (
595                        patch_display_name,
596                        patch_display_name_type,
597                    ) = self.__reg_query_value(patch_squid_handle, "DisplayName")
598                    patch_state, patch_state_type = self.__reg_query_value(
599                        patch_squid_handle, "State"
600                    )
601                    if (
602                        patch_state_type != win32con.REG_DWORD
603                        or not isinstance(patch_state_type, int)
604                        or patch_state != 1
605                        or patch_display_name_type  # 1 is Active, 2 is Superseded/Obsolute
606                        != win32con.REG_SZ
607                    ):
608                        continue
609                    win32api.RegCloseKey(
610                        patch_squid_handle
611                    )  # pylint: disable=no-member
612                    ret.append(patch_display_name)
613                except pywintypes.error as exc:  # pylint: disable=no-member
614                    if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
615                        log.debug("skipped patch, not found %s", patch_squid)
616                        continue
617                    raise
618
619        return ret
620
621    @property
622    def registry_path_text(self):
623        """
624        Returns the uninstall path this object is associated with.
625
626        Returns:
627            str: <hive>\\<uninstall registry entry>
628        """
629        return "{}\\{}".format(self.__reg_hive, self.__reg_uninstall_path)
630
631    @property
632    def registry_path(self):
633        """
634        Returns the uninstall path this object is associated with.
635
636        Returns:
637            tuple: hive, uninstall registry entry path.
638        """
639        return (self.__reg_hive, self.__reg_uninstall_path)
640
641    @property
642    def guid(self):
643        """
644        Return GUID or Key.
645
646        Returns:
647            str: GUID or Key
648        """
649        return self.__reg_key_guid
650
651    @property
652    def squid(self):
653        """
654        Return SQUID of the GUID if a valid GUID.
655
656        Returns:
657            str: GUID
658        """
659        return self.__squid
660
661    @property
662    def package_code(self):
663        """
664        Return package code of the software.
665
666        Returns:
667            str: GUID
668        """
669        return self.__squid_to_guid(self.get_product_value("PackageCode"))
670
671    @property
672    def version_binary(self):
673        """
674        Return version number which is stored in binary format.
675
676        Returns:
677            str: <major 0-255>.<minior 0-255>.<build 0-65535> or None if not found
678        """
679        # Under MSI 'Version' is a 'REG_DWORD' which then sets other registry
680        # values like DisplayVersion to x.x.x to the same value.
681        # However not everyone plays by the rules, so we need to check first.
682        # version_binary_data will be None if the reg value does not exist.
683        # Some installs set 'Version' to REG_SZ (string) which is not
684        # the MSI standard
685        try:
686            item_value, item_type = self.__reg_query_value(
687                self.__reg_uninstall_handle, "version"
688            )
689        except pywintypes.error as exc:  # pylint: disable=no-member
690            if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
691                # Not Found
692                return "", ""
693
694        version_binary_text = ""
695        version_src = ""
696        if item_value:
697            if item_type == win32con.REG_DWORD:
698                if isinstance(item_value, int):
699                    version_binary_raw = item_value
700                if version_binary_raw:
701                    # Major.Minor.Build
702                    version_binary_text = "{}.{}.{}".format(
703                        version_binary_raw >> 24 & 0xFF,
704                        version_binary_raw >> 16 & 0xFF,
705                        version_binary_raw & 0xFFFF,
706                    )
707                    version_src = "binary-version"
708
709            elif (
710                item_type == win32con.REG_SZ
711                and isinstance(item_value, str)
712                and self.__version_pattern.match(item_value) is not None
713            ):
714                # Hey, version should be a int/REG_DWORD, an installer has set
715                # it to a string
716                version_binary_text = item_value.strip(" ")
717                version_src = "binary-version (string)"
718
719        return (version_binary_text, version_src)
720
721
722class WinSoftware:
723    """
724    Point in time snapshot of the software and components installed on
725    a system.
726
727    Attributes:
728        None
729
730    :codeauthor: Damon Atkins <https://github.com/damon-atkins>
731    """
732
733    __sid_pattern = re.compile(r"^S-\d-\d-\d+$|^S-\d-\d-\d+-\d+-\d+-\d+-\d+$")
734    __whitespace_pattern = re.compile(r"^\s*$", flags=re.UNICODE)
735    # items we copy out of the uninstall section of the registry without further processing
736    __uninstall_search_list = [
737        ("url", "str", ["URLInfoAbout", "HelpLink", "MoreInfoUrl", "UrlUpdateInfo"]),
738        ("size", "int", ["Size", "EstimatedSize"]),
739        ("win_comments", "str", ["Comments"]),
740        ("win_release_type", "str", ["ReleaseType"]),
741        ("win_product_id", "str", ["ProductID"]),
742        ("win_product_codes", "str", ["ProductCodes"]),
743        ("win_package_refs", "str", ["PackageRefs"]),
744        ("win_install_location", "str", ["InstallLocation"]),
745        ("win_install_src_dir", "str", ["InstallSource"]),
746        ("win_parent_pkg_uid", "str", ["ParentKeyName"]),
747        ("win_parent_name", "str", ["ParentDisplayName"]),
748    ]
749    # items we copy out of the products section of the registry without further processing
750    __products_search_list = [
751        ("win_advertise_flags", "int", ["AdvertiseFlags"]),
752        ("win_redeployment_flags", "int", ["DeploymentFlags"]),
753        ("win_instance_type", "int", ["InstanceType"]),
754        ("win_package_name", "str", ["SourceList\\PackageName"]),
755    ]
756
757    def __init__(self, version_only=False, user_pkgs=False, pkg_obj=None):
758        """
759        Point in time snapshot of the software and components installed on
760        a system.
761
762        Args:
763            version_only (bool): Provide list of versions installed instead of detail.
764            user_pkgs (bool): Include software/components installed with user space.
765            pkg_obj (object):
766                If None (default) return default package naming standard and use
767                default version capture methods (``DisplayVersion`` then
768                ``Version``, otherwise ``0.0.0.0``)
769        """
770        self.__pkg_obj = pkg_obj  # must be set before calling get_software_details
771        self.__version_only = version_only
772        self.__reg_software = {}
773        self.__get_software_details(user_pkgs=user_pkgs)
774        self.__pkg_cnt = len(self.__reg_software)
775        self.__iter_list = None
776
777    @property
778    def data(self):
779        """
780        Returns the raw data
781
782        Returns:
783            dict: contents of the dict are dependent on the parameters passed
784                when the class was initiated.
785        """
786        return self.__reg_software
787
788    @property
789    def version_only(self):
790        """
791        Returns True if class initiated with ``version_only=True``
792
793        Returns:
794            bool: The value of ``version_only``
795        """
796        return self.__version_only
797
798    def __len__(self):
799        """
800        Returns total number of software/components installed.
801
802        Returns:
803            int: total number of software/components installed.
804        """
805        return self.__pkg_cnt
806
807    def __getitem__(self, pkg_id):
808        """
809        Returns information on a package.
810
811        Args:
812            pkg_id (str): Package Id of the software/component
813
814        Returns:
815            dict or list: List if ``version_only`` is ``True`` otherwise dict
816        """
817        if pkg_id in self.__reg_software:
818            return self.__reg_software[pkg_id]
819        else:
820            raise KeyError(pkg_id)
821
822    def __iter__(self):
823        """
824        Standard interation class initialisation over package information.
825        """
826        if self.__iter_list is not None:
827            raise RuntimeError("Can only perform one iter at a time")
828        self.__iter_list = collections.deque(sorted(self.__reg_software.keys()))
829        return self
830
831    def __next__(self):
832        """
833        Returns next Package Id.
834
835        Returns:
836            str: Package Id
837        """
838        try:
839            return self.__iter_list.popleft()
840        except IndexError:
841            self.__iter_list = None
842            raise StopIteration
843
844    def next(self):
845        """
846        Returns next Package Id.
847
848        Returns:
849            str: Package Id
850        """
851        return self.__next__()
852
853    def get(self, pkg_id, default_value=None):
854        """
855        Returns information on a package.
856
857        Args:
858            pkg_id (str): Package Id of the software/component.
859            default_value: Value to return when the Package Id is not found.
860
861        Returns:
862            dict or list: List if ``version_only`` is ``True`` otherwise dict
863        """
864        return self.__reg_software.get(pkg_id, default_value)
865
866    @staticmethod
867    def __oldest_to_latest_version(ver1, ver2):
868        """
869        Used for sorting version numbers oldest to latest
870        """
871        return 1 if LooseVersion(ver1) > LooseVersion(ver2) else -1
872
873    @staticmethod
874    def __latest_to_oldest_version(ver1, ver2):
875        """
876        Used for sorting version numbers, latest to oldest
877        """
878        return 1 if LooseVersion(ver1) < LooseVersion(ver2) else -1
879
880    def pkg_version_list(self, pkg_id):
881        """
882        Returns information on a package.
883
884        Args:
885            pkg_id (str): Package Id of the software/component.
886
887        Returns:
888            list: List of version numbers installed.
889        """
890        pkg_data = self.__reg_software.get(pkg_id, None)
891        if not pkg_data:
892            return []
893
894        if isinstance(pkg_data, list):
895            # raw data is 'pkgid': [sorted version list]
896            return pkg_data  # already sorted oldest to newest
897
898        # Must be a dict or OrderDict, and contain full details
899        installed_versions = list(pkg_data.get("version").keys())
900        return sorted(
901            installed_versions, key=cmp_to_key(self.__oldest_to_latest_version)
902        )
903
904    def pkg_version_latest(self, pkg_id):
905        """
906        Returns a package latest version installed out of all the versions
907        currently installed.
908
909        Args:
910            pkg_id (str): Package Id of the software/component.
911
912        Returns:
913            str: Latest/Newest version number installed.
914        """
915        return self.pkg_version_list(pkg_id)[-1]
916
917    def pkg_version_oldest(self, pkg_id):
918        """
919        Returns a package oldest version installed out of all the versions
920        currently installed.
921
922        Args:
923            pkg_id (str): Package Id of the software/component.
924
925        Returns:
926            str: Oldest version number installed.
927        """
928        return self.pkg_version_list(pkg_id)[0]
929
930    @staticmethod
931    def __sid_to_username(sid):
932        """
933        Provided with a valid Windows Security Identifier (SID) and returns a Username
934
935        Args:
936            sid (str): Security Identifier (SID).
937
938        Returns:
939            str: Username in the format of username@realm or username@computer.
940        """
941        if sid is None or sid == "":
942            return ""
943        try:
944            sid_bin = win32security.GetBinarySid(sid)  # pylint: disable=no-member
945        except pywintypes.error as exc:  # pylint: disable=no-member
946            raise ValueError(
947                "pkg: Software owned by {} is not valid: [{}] {}".format(
948                    sid, exc.winerror, exc.strerror
949                )
950            )
951        try:
952            name, domain, _account_type = win32security.LookupAccountSid(
953                None, sid_bin
954            )  # pylint: disable=no-member
955            user_name = "{}\\{}".format(domain, name)
956        except pywintypes.error as exc:  # pylint: disable=no-member
957            # if user does not exist...
958            # winerror.ERROR_NONE_MAPPED = No mapping between account names and
959            # security IDs was carried out.
960            if exc.winerror == winerror.ERROR_NONE_MAPPED:  # 1332
961                # As the sid is from the registry it should be valid
962                # even if it cannot be lookedup, so the sid is returned
963                return sid
964            else:
965                raise ValueError(
966                    "Failed looking up sid '{}' username: [{}] {}".format(
967                        sid, exc.winerror, exc.strerror
968                    )
969                )
970        try:
971            user_principal = win32security.TranslateName(  # pylint: disable=no-member
972                user_name,
973                win32api.NameSamCompatible,  # pylint: disable=no-member
974                win32api.NameUserPrincipal,
975            )  # pylint: disable=no-member
976        except pywintypes.error as exc:  # pylint: disable=no-member
977            # winerror.ERROR_NO_SUCH_DOMAIN The specified domain either does not exist
978            # or could not be contacted, computer may not be part of a domain also
979            # winerror.ERROR_INVALID_DOMAINNAME The format of the specified domain name is
980            # invalid. e.g. S-1-5-19 which is a local account
981            # winerror.ERROR_NONE_MAPPED No mapping between account names and security IDs was done.
982            if exc.winerror in (
983                winerror.ERROR_NO_SUCH_DOMAIN,
984                winerror.ERROR_INVALID_DOMAINNAME,
985                winerror.ERROR_NONE_MAPPED,
986            ):
987                return "{}@{}".format(name.lower(), domain.lower())
988            else:
989                raise
990        return user_principal
991
992    def __software_to_pkg_id(self, publisher, name, is_component, is_32bit):
993        """
994        Determine the Package ID of a software/component using the
995        software/component ``publisher``, ``name``, whether its a software or a
996        component, and if its 32bit or 64bit archiecture.
997
998        Args:
999            publisher (str): Publisher of the software/component.
1000            name (str): Name of the software.
1001            is_component (bool): True if package is a component.
1002            is_32bit (bool): True if the software/component is 32bit architecture.
1003
1004        Returns:
1005            str: Package Id
1006        """
1007        if publisher:
1008            # remove , and lowercase as , are used as list separators
1009            pub_lc = publisher.replace(",", "").lower()
1010
1011        else:
1012            # remove , and lowercase
1013            pub_lc = "NoValue"  # Capitals/Special Value
1014
1015        if name:
1016            name_lc = name.replace(",", "").lower()
1017            # remove ,   OR we do the URL Encode on chars we do not want e.g. \\ and ,
1018        else:
1019            name_lc = "NoValue"  # Capitals/Special Value
1020
1021        if is_component:
1022            soft_type = "comp"
1023        else:
1024            soft_type = "soft"
1025
1026        if is_32bit:
1027            soft_type += "32"  # Tag only the 32bit only
1028
1029        default_pkg_id = pub_lc + "\\\\" + name_lc + "\\\\" + soft_type
1030
1031        # Check to see if class was initialise with pkg_obj with a method called
1032        # to_pkg_id, and if so use it for the naming standard instead of the default
1033        if self.__pkg_obj and hasattr(self.__pkg_obj, "to_pkg_id"):
1034            pkg_id = self.__pkg_obj.to_pkg_id(publisher, name, is_component, is_32bit)
1035            if pkg_id:
1036                return pkg_id
1037
1038        return default_pkg_id
1039
1040    def __version_capture_slp(
1041        self, pkg_id, version_binary, version_display, display_name
1042    ):
1043        """
1044        This returns the version and where the version string came from, based on instructions
1045        under ``version_capture``, if ``version_capture`` is missing, it defaults to
1046        value of display-version.
1047
1048        Args:
1049            pkg_id (str): Publisher of the software/component.
1050            version_binary (str): Name of the software.
1051            version_display (str): True if package is a component.
1052            display_name (str): True if the software/component is 32bit architecture.
1053
1054        Returns:
1055            str: Package Id
1056        """
1057        if self.__pkg_obj and hasattr(self.__pkg_obj, "version_capture"):
1058            version_str, src, version_user_str = self.__pkg_obj.version_capture(
1059                pkg_id, version_binary, version_display, display_name
1060            )
1061            if src != "use-default" and version_str and src:
1062                return version_str, src, version_user_str
1063            elif src != "use-default":
1064                raise ValueError(
1065                    "version capture within object '{}' failed "
1066                    "for pkg id: '{}' it returned '{}' '{}' "
1067                    "'{}'".format(
1068                        str(self.__pkg_obj),
1069                        pkg_id,
1070                        version_str,
1071                        src,
1072                        version_user_str,
1073                    )
1074                )
1075
1076        # If self.__pkg_obj.version_capture() not defined defaults to using
1077        # version_display and if not valid then use version_binary, and as a last
1078        # result provide the version 0.0.0.0.0 to indicate version string was not determined.
1079        if (
1080            version_display
1081            and re.match(r"\d+", version_display, flags=re.IGNORECASE + re.UNICODE)
1082            is not None
1083        ):
1084            version_str = version_display
1085            src = "display-version"
1086        elif (
1087            version_binary
1088            and re.match(r"\d+", version_binary, flags=re.IGNORECASE + re.UNICODE)
1089            is not None
1090        ):
1091            version_str = version_binary
1092            src = "version-binary"
1093        else:
1094            src = "none"
1095            version_str = "0.0.0.0.0"
1096        # return version str, src of the version, "user" interpretation of the version
1097        # which by default is version_str
1098        return version_str, src, version_str
1099
1100    def __collect_software_info(self, sid, key_software, use_32bit):
1101        """
1102        Update data with the next software found
1103        """
1104
1105        reg_soft_info = RegSoftwareInfo(key_software, sid, use_32bit)
1106
1107        # Check if the registry entry is a valid.
1108        # a) Cannot manage software without at least a display name
1109        display_name = reg_soft_info.get_install_value("DisplayName", wanted_type="str")
1110        if display_name is None or self.__whitespace_pattern.match(display_name):
1111            return
1112
1113        # b) make sure its not an 'Hotfix', 'Update Rollup', 'Security Update', 'ServicePack'
1114        # General this is software which pre dates Windows 10
1115        default_value = reg_soft_info.get_install_value("", wanted_type="str")
1116        release_type = reg_soft_info.get_install_value("ReleaseType", wanted_type="str")
1117
1118        if (
1119            re.match(
1120                r"^{.*\}\.KB\d{6,}$", key_software, flags=re.IGNORECASE + re.UNICODE
1121            )
1122            is not None
1123            or (default_value and default_value.startswith(("KB", "kb", "Kb")))
1124            or (
1125                release_type
1126                and release_type
1127                in ("Hotfix", "Update Rollup", "Security Update", "ServicePack")
1128            )
1129        ):
1130            log.debug("skipping hotfix/update/service pack %s", key_software)
1131            return
1132
1133        # if NoRemove exists we would expect their to be no UninstallString
1134        uninstall_no_remove = reg_soft_info.is_install_true("NoRemove")
1135        uninstall_string = reg_soft_info.get_install_value("UninstallString")
1136        uninstall_quiet_string = reg_soft_info.get_install_value("QuietUninstallString")
1137        uninstall_modify_path = reg_soft_info.get_install_value("ModifyPath")
1138        windows_installer = reg_soft_info.is_install_true("WindowsInstaller")
1139        system_component = reg_soft_info.is_install_true("SystemComponent")
1140        publisher = reg_soft_info.get_install_value("Publisher", wanted_type="str")
1141
1142        # UninstallString is optional if the installer is "windows installer"/MSI
1143        # However for it to appear in Control-Panel -> Program and Features -> Uninstall or change a program
1144        # the UninstallString needs to be set or ModifyPath set
1145        if (
1146            uninstall_string is None
1147            and uninstall_quiet_string is None
1148            and uninstall_modify_path is None
1149            and (not windows_installer)
1150        ):
1151            return
1152
1153        # Question: If uninstall string is not set and windows_installer should we set it
1154        # Question: if uninstall_quiet is not set .......
1155
1156        if sid:
1157            username = self.__sid_to_username(sid)
1158        else:
1159            username = None
1160
1161        # We now have a valid software install or a system component
1162        pkg_id = self.__software_to_pkg_id(
1163            publisher, display_name, system_component, use_32bit
1164        )
1165        version_binary, version_src = reg_soft_info.version_binary
1166        version_display = reg_soft_info.get_install_value(
1167            "DisplayVersion", wanted_type="str"
1168        )
1169        # version_capture is what the slp defines, the result overrides. Question: maybe it should error if it fails?
1170        (version_text, version_src, user_version) = self.__version_capture_slp(
1171            pkg_id, version_binary, version_display, display_name
1172        )
1173        if not user_version:
1174            user_version = version_text
1175
1176        # log.trace('%s\\%s ver:%s src:%s', username or 'SYSTEM', pkg_id, version_text, version_src)
1177
1178        if username:
1179            dict_key = "{};{}".format(
1180                username, pkg_id
1181            )  # Use ; as its not a valid hostnmae char
1182        else:
1183            dict_key = pkg_id
1184
1185        # Guessing the architecture http://helpnet.flexerasoftware.com/isxhelp21/helplibrary/IHelp64BitSupport.htm
1186        # A 32 bit installed.exe can install a 64 bit app, but for it to write to 64bit reg it will
1187        # need to use WOW. So the following is a bit of a guess
1188
1189        if self.__version_only:
1190            # package name and package version list, are the only info being return
1191            if dict_key in self.__reg_software:
1192                if version_text not in self.__reg_software[dict_key]:
1193                    # Not expecting the list to be big, simple search and insert
1194                    insert_point = 0
1195                    for ver_item in self.__reg_software[dict_key]:
1196                        if LooseVersion(version_text) <= LooseVersion(ver_item):
1197                            break
1198                        insert_point += 1
1199                    self.__reg_software[dict_key].insert(insert_point, version_text)
1200                else:
1201                    # This code is here as it can happen, especially if the
1202                    # package id provided by pkg_obj is simple.
1203                    log.debug(
1204                        "Found extra entries for '%s' with same version "
1205                        "'%s', skipping entry '%s'",
1206                        dict_key,
1207                        version_text,
1208                        key_software,
1209                    )
1210            else:
1211                self.__reg_software[dict_key] = [version_text]
1212
1213            return
1214
1215        if dict_key in self.__reg_software:
1216            data = self.__reg_software[dict_key]
1217        else:
1218            data = self.__reg_software[dict_key] = OrderedDict()
1219
1220        if sid:
1221            # HKEY_USERS has no 32bit and 64bit view like HKEY_LOCAL_MACHINE
1222            data.update({"arch": "unknown"})
1223        else:
1224            arch_str = "x86" if use_32bit else "x64"
1225            if "arch" in data:
1226                if data["arch"] != arch_str:
1227                    data["arch"] = "many"
1228            else:
1229                data.update({"arch": arch_str})
1230
1231        if publisher:
1232            if "vendor" in data:
1233                if data["vendor"].lower() != publisher.lower():
1234                    data["vendor"] = "many"
1235            else:
1236                data["vendor"] = publisher
1237
1238        if "win_system_component" in data:
1239            if data["win_system_component"] != system_component:
1240                data["win_system_component"] = None
1241        else:
1242            data["win_system_component"] = system_component
1243
1244        data.update({"win_version_src": version_src})
1245
1246        data.setdefault("version", {})
1247        if version_text in data["version"]:
1248            if "win_install_count" in data["version"][version_text]:
1249                data["version"][version_text]["win_install_count"] += 1
1250            else:
1251                # This is only defined when we have the same item already
1252                data["version"][version_text]["win_install_count"] = 2
1253        else:
1254            data["version"][version_text] = OrderedDict()
1255
1256        version_data = data["version"][version_text]
1257        version_data.update({"win_display_name": display_name})
1258        if uninstall_string:
1259            version_data.update({"win_uninstall_cmd": uninstall_string})
1260        if uninstall_quiet_string:
1261            version_data.update({"win_uninstall_quiet_cmd": uninstall_quiet_string})
1262        if uninstall_no_remove:
1263            version_data.update({"win_uninstall_no_remove": uninstall_no_remove})
1264
1265        version_data.update({"win_product_code": key_software})
1266        if version_display:
1267            version_data.update({"win_version_display": version_display})
1268        if version_binary:
1269            version_data.update({"win_version_binary": version_binary})
1270        if user_version:
1271            version_data.update({"win_version_user": user_version})
1272
1273        # Determine Installer Product
1274        #   'NSIS:Language'
1275        #   'Inno Setup: Setup Version'
1276        if windows_installer or (
1277            uninstall_string
1278            and re.search(
1279                r"MsiExec.exe\s|MsiExec\s",
1280                uninstall_string,
1281                flags=re.IGNORECASE + re.UNICODE,
1282            )
1283        ):
1284            version_data.update({"win_installer_type": "winmsi"})
1285        elif re.match(r"InstallShield_", key_software, re.IGNORECASE) is not None or (
1286            uninstall_string
1287            and (
1288                re.search(
1289                    r"InstallShield", uninstall_string, flags=re.IGNORECASE + re.UNICODE
1290                )
1291                is not None
1292                or re.search(
1293                    r"isuninst\.exe.*\.isu",
1294                    uninstall_string,
1295                    flags=re.IGNORECASE + re.UNICODE,
1296                )
1297                is not None
1298            )
1299        ):
1300            version_data.update({"win_installer_type": "installshield"})
1301        elif key_software.endswith("_is1") and reg_soft_info.get_install_value(
1302            "Inno Setup: Setup Version", wanted_type="str"
1303        ):
1304            version_data.update({"win_installer_type": "inno"})
1305        elif uninstall_string and re.search(
1306            r".*\\uninstall.exe|.*\\uninst.exe",
1307            uninstall_string,
1308            flags=re.IGNORECASE + re.UNICODE,
1309        ):
1310            version_data.update({"win_installer_type": "nsis"})
1311        else:
1312            version_data.update({"win_installer_type": "unknown"})
1313
1314        # Update dict with information retrieved so far for detail results to be return
1315        # Do not add fields which are blank.
1316        language_number = reg_soft_info.get_install_value("Language")
1317        if (
1318            isinstance(language_number, int)
1319            and language_number in locale.windows_locale
1320        ):
1321            version_data.update(
1322                {"win_language": locale.windows_locale[language_number]}
1323            )
1324
1325        package_code = reg_soft_info.package_code
1326        if package_code:
1327            version_data.update({"win_package_code": package_code})
1328
1329        upgrade_code = reg_soft_info.upgrade_code
1330        if upgrade_code:
1331            version_data.update({"win_upgrade_code": upgrade_code})
1332
1333        is_minor_upgrade = reg_soft_info.is_install_true("IsMinorUpgrade")
1334        if is_minor_upgrade:
1335            version_data.update({"win_is_minor_upgrade": is_minor_upgrade})
1336
1337        install_time = reg_soft_info.install_time
1338        if install_time:
1339            version_data.update(
1340                {
1341                    "install_date": datetime.datetime.fromtimestamp(
1342                        install_time
1343                    ).isoformat()
1344                }
1345            )
1346            version_data.update({"install_date_time_t": int(install_time)})
1347
1348        for infokey, infotype, regfield_list in self.__uninstall_search_list:
1349            for regfield in regfield_list:
1350                strvalue = reg_soft_info.get_install_value(
1351                    regfield, wanted_type=infotype
1352                )
1353                if strvalue:
1354                    version_data.update({infokey: strvalue})
1355                    break
1356
1357        for infokey, infotype, regfield_list in self.__products_search_list:
1358            for regfield in regfield_list:
1359                data = reg_soft_info.get_product_value(regfield, wanted_type=infotype)
1360                if data is not None:
1361                    version_data.update({infokey: data})
1362                    break
1363        patch_list = reg_soft_info.list_patches
1364        if patch_list:
1365            version_data.update({"win_patches": patch_list})
1366
1367    def __get_software_details(self, user_pkgs):
1368        """
1369        This searches the uninstall keys in the registry to find
1370        a match in the sub keys, it will return a dict with the
1371        display name as the key and the version as the value
1372        .. sectionauthor:: Damon Atkins <https://github.com/damon-atkins>
1373        .. versionadded:: 2016.11.0
1374        """
1375
1376        # FUNCTION MAIN CODE #
1377        # Search 64bit, on 64bit platform, on 32bit its ignored.
1378        if platform.architecture()[0] == "32bit":
1379            # Handle Python 32bit on 64&32 bit platform and Python 64bit
1380            if win32process.IsWow64Process():  # pylint: disable=no-member
1381                # 32bit python on a 64bit platform
1382                use_32bit_lookup = {True: 0, False: win32con.KEY_WOW64_64KEY}
1383                arch_list = [True, False]
1384            else:
1385                # 32bit python on a 32bit platform
1386                use_32bit_lookup = {True: 0, False: None}
1387                arch_list = [True]
1388
1389        else:
1390            # Python is 64bit therefore most be on 64bit System.
1391            use_32bit_lookup = {True: win32con.KEY_WOW64_32KEY, False: 0}
1392            arch_list = [True, False]
1393
1394        # Process software installed for the machine i.e. all users.
1395        for arch_flag in arch_list:
1396            key_search = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
1397            log.debug("SYSTEM processing 32bit:%s", arch_flag)
1398            handle = win32api.RegOpenKeyEx(  # pylint: disable=no-member
1399                win32con.HKEY_LOCAL_MACHINE,
1400                key_search,
1401                0,
1402                win32con.KEY_READ | use_32bit_lookup[arch_flag],
1403            )
1404            reg_key_all, _, _, _ = zip(
1405                *win32api.RegEnumKeyEx(handle)
1406            )  # pylint: disable=no-member
1407            win32api.RegCloseKey(handle)  # pylint: disable=no-member
1408            for reg_key in reg_key_all:
1409                self.__collect_software_info(None, reg_key, arch_flag)
1410
1411        if not user_pkgs:
1412            return
1413
1414        # Process software installed under all USERs, this adds significate processing time.
1415        # There is not 32/64 bit registry redirection under user tree.
1416        log.debug("Processing user software... please wait")
1417        handle_sid = win32api.RegOpenKeyEx(  # pylint: disable=no-member
1418            win32con.HKEY_USERS, "", 0, win32con.KEY_READ
1419        )
1420        sid_all = []
1421        for index in range(
1422            win32api.RegQueryInfoKey(handle_sid)[0]
1423        ):  # pylint: disable=no-member
1424            sid_all.append(
1425                win32api.RegEnumKey(handle_sid, index)
1426            )  # pylint: disable=no-member
1427
1428        for sid in sid_all:
1429            if (
1430                self.__sid_pattern.match(sid) is not None
1431            ):  # S-1-5-18 needs to be ignored?
1432                user_uninstall_path = "{}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall".format(
1433                    sid
1434                )
1435                try:
1436                    handle = win32api.RegOpenKeyEx(  # pylint: disable=no-member
1437                        handle_sid, user_uninstall_path, 0, win32con.KEY_READ
1438                    )
1439                except pywintypes.error as exc:  # pylint: disable=no-member
1440                    if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
1441                        # Not Found Uninstall under SID
1442                        log.debug("Not Found %s", user_uninstall_path)
1443                        continue
1444                    else:
1445                        raise
1446                try:
1447                    reg_key_all, _, _, _ = zip(
1448                        *win32api.RegEnumKeyEx(handle)
1449                    )  # pylint: disable=no-member
1450                except ValueError:
1451                    log.debug("No Entries Found %s", user_uninstall_path)
1452                    reg_key_all = []
1453                win32api.RegCloseKey(handle)  # pylint: disable=no-member
1454                for reg_key in reg_key_all:
1455                    self.__collect_software_info(sid, reg_key, False)
1456        win32api.RegCloseKey(handle_sid)  # pylint: disable=no-member
1457        return
1458
1459
1460def __main():
1461    """This module can also be run directly for testing
1462    Args:
1463        detail|list : Provide ``detail`` or version ``list``.
1464        system|system+user: System installed and System and User installs.
1465    """
1466    if len(sys.argv) < 3:
1467        sys.stderr.write(
1468            "usage: {} <detail|list> <system|system+user>\n".format(sys.argv[0])
1469        )
1470        sys.exit(64)
1471    user_pkgs = False
1472    version_only = False
1473    if str(sys.argv[1]) == "list":
1474        version_only = True
1475    if str(sys.argv[2]) == "system+user":
1476        user_pkgs = True
1477    import salt.utils.json
1478    import timeit
1479
1480    def run():
1481        """
1482        Main run code, when this module is run directly
1483        """
1484        pkg_list = WinSoftware(user_pkgs=user_pkgs, version_only=version_only)
1485        print(
1486            salt.utils.json.dumps(pkg_list.data, sort_keys=True, indent=4)
1487        )  # pylint: disable=superfluous-parens
1488        print("Total: {}".format(len(pkg_list)))  # pylint: disable=superfluous-parens
1489
1490    print(
1491        "Time Taken: {}".format(timeit.timeit(run, number=1))
1492    )  # pylint: disable=superfluous-parens
1493
1494
1495if __name__ == "__main__":
1496    __main()
1497