From f7e44cf2b0e06e59d1332ce5e5a70d6a7f835105 Mon Sep 17 00:00:00 2001 From: Mürsel Türk Date: Fri, 26 Aug 2022 12:06:04 +0200 Subject: Add tools for inspecting os and apps --- tools/inspect_apps.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ tools/inspect_os.py | 143 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 tools/inspect_apps.py create mode 100644 tools/inspect_os.py diff --git a/tools/inspect_apps.py b/tools/inspect_apps.py new file mode 100644 index 0000000..014625c --- /dev/null +++ b/tools/inspect_apps.py @@ -0,0 +1,180 @@ +import logging +import os +import tempfile + +from . import log + +__all__ = [ + "list_applications_rpm", + "list_applications_windows" +] + +L = logging.getLogger(__name__) + +try: + import rpm # type: ignore +except ModuleNotFoundError: + L.error( + "You need to install the following package:\n" + "sudo apt install python3-rpm" + ) + raise + +try: + from Registry import Registry # type: ignore +except ModuleNotFoundError: + L.error( + "You need to install the following package:\n" + "pip3 install python-registry" + ) + raise + + +@log +def list_applications_rpm(path): + """Find all packages installed on a rpm-based linux distribution. + + Args: + path (str): Path to the mounted filesystem. + + Returns: + List of packages. For example: + [{'name': 'libgcc', 'version': '12.0.1'}, ...] + """ + rpm_db = None + for root, dirs, _ in os.walk(path): + if "usr" in dirs: + db = os.path.join(root, "usr/share/rpm") + if os.path.exists(db): + rpm_db = db + break + # https://fedoraproject.org/wiki/Changes/RelocateRPMToUsr + db = os.path.join(root, "usr/lib/sysimage/rpm") + if os.path.exists(db): + rpm_db = db + break + if "var" in dirs: + db = os.path.join(root, "var/lib/rpm") + if os.path.exists(db): + rpm_db = db + break + + if rpm_db is None: + L.debug("RPM database not found") + return [] + + log_file = tempfile.TemporaryFile() + rpm.setLogFile(log_file) + rpm.setVerbosity(rpm.RPMLOG_DEBUG) + rpm.addMacro("_dbpath", rpm_db) + ts = rpm.TransactionSet() + + try: + dbMatch = ts.dbMatch() + except Exception as e: + L.error("failed to open RPM database: %r", e) + return [] + + ret = [] + for h in dbMatch: + ret.append({ + "name": h["name"], + "version": h["version"] + }) + + rpm.delMacro("_dbpath") + + return ret + + +@log +def list_applications_windows(path): + """Find all applications installed on a windows distribution. + + Args: + path (str): Path to the mounted filesystem. + + Returns: + List of applications. For example: + [{'name': 'Mozilla Firefox 43.0.1 (x86 de)', 'version': '43.0.1'}, ...] + """ + software = None + + locations = [ + "WINDOWS/system32/config/software", # xp + "Windows/System32/config/SOFTWARE", # others + ] + + for location in locations: + software_path = os.path.join(path, location) + if os.path.isfile(software_path): + software = software_path + break + + if not software: + L.debug("software hive not found in %s", path) + return [] + + try: + registry = Registry.Registry(software) + except Exception as e: + L.error("failed to open registry file %s: %r", software, e) + return [] + + ret = [] + + # native applications + hive_path = "Microsoft\\Windows\\CurrentVersion\\Uninstall" + try: + key = registry.open(hive_path) + except Exception as e: + L.error("%s not found in %s: %r", hive_path, software, e) + return ret + if apps_native := _list_applications_windows_from_key(key): + ret.extend(apps_native) + + # 32-bit applications running on WOW64 emulator + # see also: http://support.microsoft.com/kb/896459 + hive_path = "Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall" + try: + key = registry.open(hive_path) + except Exception as e: + L.error("%s not found in %s: %r", hive_path, software, e) + return ret + if apps_emulator := _list_applications_windows_from_key(key): + ret.extend(apps_emulator) + + return ret + + +@log +def _list_applications_windows_from_key(key): + """Parse applications from windows registry key. + + See also: + https://docs.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key + + Args: + key (Registry.Key): Registry key. + + Returns: + List of applications. + """ + ret = [] + for k in key.subkeys(): + # name = k.name() + # name does not say much, so take the display name + name = version = "" + for v in k.values(): + if v.name() == "DisplayName": + name = v.value() + if v.name() == "DisplayVersion": + version = v.value() + # ignore applications with no display name + if name and version: + ret.append({ + "name": name, + "version": version + }) + + return ret diff --git a/tools/inspect_os.py b/tools/inspect_os.py new file mode 100644 index 0000000..ed4ee45 --- /dev/null +++ b/tools/inspect_os.py @@ -0,0 +1,143 @@ +import os +import logging +import re + +from glob import iglob +from . import log + +__all__ = ["get_linux_os_info", "get_windows_os_info"] + +L = logging.getLogger(__name__) + +try: + from Registry import Registry # type: ignore +except ModuleNotFoundError: + L.error( + "You need to install the following package:\n" + "pip3 install python-registry" + ) + raise + + +@log +def get_linux_os_info(path): + """Find and parse the os-release file. + + See also: + https://www.freedesktop.org/software/systemd/man/os-release.html + + Args: + path (str): Path to the mounted filesystem. + + Returns: + Name and version of linux distribution as a dictionary. For example: + {'name': 'CentOS Linux', 'version': '6.0 (Final)'} + """ + release_files = {} + for root, dirs, _ in os.walk(path): + if "etc" in dirs: + skip = False + for f in iglob(f"{root}/etc/*release"): + # Hack for immutable operating systems of Fedora. + if not os.path.exists(f): + skip = True + break + release_files[os.path.basename(f)] = f + if release_files and not skip: + break + + if not release_files: + L.debug("no release file found") + return {} + + L.debug("release files: %s", release_files) + + name = version = "" + if "centos-release" in release_files: + L.debug("parsing %s", release_files["centos-release"]) + # CentOS 6.* doesn't have os-release, but centos-release. + # For consistency, use always centos-release first. + with open(release_files["centos-release"]) as f: + # AlmaLinux 8.* and Rocky Linux 8.* also have centos-release. + if m := re.match(r"^(.*)\srelease\s(.*)$", f.read()): + name, version = m.groups() + elif "os-release" in release_files: + L.debug("parsing %s", release_files["os-release"]) + with open(release_files["os-release"]) as f: + kv = {} + for line in f: + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + kv[k] = v + name = kv.get("NAME", "") + version = kv.get("VERSION", kv.get("VERSION_ID", "")) + elif "system-release" in release_files: + # RedHat (RHEL) provides the redhat-release file. However, it does not + # seem to be reliable for determining which operating system it is. + # Therefore, for distributions such as Scientific Linux 6.* and + # Oracle Linux 6.*, use system-release instead. + L.debug("parsing %s", release_files["system-release"]) + with open(release_files["system-release"]) as f: + if m := re.match(r"^(.*)\srelease\s(.*)$", f.read()): + name, version = m.groups() + + if not name or not version: + return {} + + name = name.strip().strip("'\"") + version = version.strip().strip("'\"") + return {"name": name, "version": version} + + +@log +def get_windows_os_info(path): + """Find and parse the software registry. + + Args: + path (str): Path to the mounted filesystem. + + Returns: + Name and version of windows distribution as a dictionary. For example: + {'name': 'Microsoft Windows XP', 'version': '5.1'} + """ + software = None + + locations = [ + "WINDOWS/system32/config/software", # xp + "Windows/System32/config/SOFTWARE", # others + ] + + for location in locations: + software_path = os.path.join(path, location) + if os.path.isfile(software_path): + software = software_path + break + + if not software: + return {} + + try: + registry = Registry.Registry(software) + except Exception as e: + L.error("failed to open registry file %s: %r", software, e) + return {} + + hive_path = "Microsoft\\Windows NT\\CurrentVersion" + try: + key = registry.open(hive_path) + except Exception as e: + L.error("%s not found in %s: %r", hive_path, software, e) + return {} + + name = version = "" + for k in key.values(): + if k.name() == "ProductName": + name = k.value() + if k.name() == "CurrentVersion": + version = k.value() + + if not name or not version: + return {} + + return {"name": name, "version": version} -- cgit v1.2.3-55-g7522