# # Copyright (C) 2020-2024 Soleta Networks # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the # Free Software Foundation; either version 3 of the License, or # (at your option) any later version. import platform import re import os import logging from collections import namedtuple import hivex from src.utils.probe import os_probe from src.utils.winreg import * Package = namedtuple('Package', ['name', 'version']) Package.__str__ = lambda pkg: f'{pkg.name} {pkg.version}' DPKG_STATUS_PATH = '/var/lib/dpkg/status' OSRELEASE_PATH = '/etc/os-release' def _fill_package_set(hive, key, pkg_set): """ Fill the package set looking for entries at the current registry node childs. Any valid node child must have "DisplayVersion" or "DisplayName" keys. """ childs = hive.node_children(key) valid_childs = [] for child in childs: child_name = hive.node_name(child) values = hive.node_values(child) for value in values: if hive.value_key(value) == 'DisplayVersion': valid_child = hive.node_get_child(key, child_name) valid_childs.append(valid_child) for ch in valid_childs: try: name = hive.value_string(hive.node_get_value(ch, 'DisplayName')) value = hive.node_get_value(ch, 'DisplayVersion') version = hive.value_string(value) pkg = Package(name, version) pkg_set.add(pkg) except RuntimeError: logging.warning('Unable to fill package set with invalid child') pass def _fill_package_set_1(hive, pkg_set): """ Looks for entries in registry path /Microsoft/Windows/CurrentVersion/Uninstall Fills the given set with Package instances for each program found. """ root_node = hive.root() key = get_node_child_from_path(hive, root_node, 'Microsoft/Windows/CurrentVersion/Uninstall') _fill_package_set(hive, key, pkg_set) def _fill_package_set_32_bit_compat(hive, pkg_set): """ Looks for entries in registry path /Wow6432Node/Microsoft/Windows/CurrentVersion/Uninstall 64 bit Windows only. Fills the given set with Package instances for each program found. """ root_node = hive.root() key = get_node_child_from_path(hive, root_node, 'Wow6432Node/Windows/CurrentVersion/Uninstall') _fill_package_set(hive, key, pkg_set) def _get_package_set_windows(hivepath): packages = set() try: h = hive_handler_open(hivepath, write = False) _fill_package_set_1(h, packages) except (RuntimeError, OgError) as e: logging.error(f'Hivex was not able to operate over {hivepath}. Reported: {e}') try: _fill_package_set_32_bit_compat(h, packages) except (RuntimeError, OgError) as e: pass return packages def _get_package_set_dpkg(dpkg_status_path): regex_pkg = '(?:^Package: )(?P.*)\n(Essential:.*\n)?(?:Status: install ok installed)' regex_ver = '(?:^Version: )(?P.*)' packages = set() with open(dpkg_status_path, 'r') as f: # Split by empty line for par in re.split('^\n+', f.read(), flags=re.MULTILINE): # Search for package with "Status: install ok installed" result = re.search(regex_pkg, par) if result is None: continue else: pkg_name = result.group('name') # If we hit a properly installed package, search for its version result = re.search(regex_ver, par, flags=re.MULTILINE) if result is None: continue else: pkg_version = result.group('version') pkg = Package(pkg_name, pkg_version) packages.add(pkg) return packages def get_package_set(mountpoint): dpkg_status_path = f'{mountpoint}{DPKG_STATUS_PATH}' softwarehive = f'{mountpoint}{WINDOWS_HIVE_SOFTWARE}' if os.path.exists(softwarehive): pkgset = _get_package_set_windows(softwarehive) elif os.path.exists(dpkg_status_path): pkgset = _get_package_set_dpkg(dpkg_status_path) else: pkgset = set() osname = os_probe(mountpoint) # Legacy software inventory first element is the OS name return [osname] + list(pkgset)