summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tools/inspect_apps.py180
-rw-r--r--tools/inspect_os.py143
2 files changed, 323 insertions, 0 deletions
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}