summaryrefslogblamecommitdiffstats
path: root/tools/inspect_apps.py
blob: 5b6ec0061af76f6afa5c32036705a8c9e0871a1d (plain) (tree)
1
2
3
4
5
6
7
8
9

              
         
                          
 
                                             
                          

           
                            
                             
                               
                                





                               

    





















































                                                                          
                                 


































                                                                         


                        








                                          
                      






                                                  












                                                           


































                                                                      


                        

















                                                                         
                                                                       








                                  


































                                                                          


                        



















                                                                  































                                                                     
                                     








                                                     
             
                     
                     





                                   
               



































                                                                               
             






                                                                 
                   
                                                               
                                







                                                                            
                   
                                                                 
                                  
 
               














                                                                             
             










                                                          
                         



                                  
               
import logging
import os
import re
import rpm  # type: ignore

from Registry import Registry  # type: ignore
from . import log, subdirs

__all__ = [
    "list_applications_apk",
    "list_applications_dpkg",
    "list_applications_pacman",
    "list_applications_portage",
    "list_applications_rpm",
    "list_applications_windows"
]

L = logging.getLogger(__name__)


@log
def list_applications_apk(path):
    """Find all packages installed on the linux distribution Alpine Linux.

    See also:
    https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper

    Args:
        path (str): Path to the mounted filesystem.

    Returns:
        List of packages. For example:
        [{'name': 'musl', 'version': '1.2.3-r0'}, ...]
    """
    apk_db = None

    location = "lib/apk/db/installed"

    db = os.path.join(path, location)
    if os.path.exists(db):
        apk_db = db

    if apk_db is None:
        for dir in subdirs(path):
            new_path = os.path.join(path, dir)
            db = os.path.join(new_path, location)
            if os.path.exists(db):
                apk_db = db
                break

    if apk_db is None:
        L.debug("apk database not found")
        return []

    pkgs = []
    with open(apk_db) as f:
        name = version = ""
        for line in f:
            line = line.strip()
            if not line:
                if name and version:
                    pkgs.append({
                        "name": name,
                        "version": version
                    })
                name = version = ""
            elif line.startswith("P:"):
                name = line[2:]
            elif line.startswith("V:"):
                version = line[2:]

    return pkgs


@log
def list_applications_dpkg(path):
    """Find all packages installed on a debian-based linux distribution.

    See also:
    https://man7.org/linux/man-pages/man1/dpkg.1.html

    Args:
        path (str): Path to the mounted filesystem.

    Returns:
        List of packages. For example:
        [{'name': 'adduser', 'version': '3.118'}, ...]
    """
    dpkg_db = None

    locations = [
        "var/lib/dpkg/status",
        "lib/dpkg/status"  # separated /var partition
    ]

    for location in locations:
        db = os.path.join(path, location)
        if os.path.exists(db):
            dpkg_db = db
            break

    # Debian uses subvol=@rootfs for root filesystem for btrfs.
    # Therefore, under Debian 11.* looks like this: /@rootfs/var/lib/dpkg
    if dpkg_db is None:
        for dir in subdirs(path):
            new_path = os.path.join(path, dir)
            for location in locations:
                db = os.path.join(new_path, location)
                if os.path.exists(db):
                    dpkg_db = db
                    break
            else:
                continue
            break

    if dpkg_db is None:
        L.debug("dpkg database not found")
        return []

    pkgs = []
    with open(dpkg_db) as f:
        name = version = ""
        installed = False
        for line in f:
            line = line.strip()
            if not line:
                if name and version and installed:
                    pkgs.append({
                        "name": name,
                        "version": version
                    })
                name = version = ""
                installed = False
            elif line.startswith("Package:"):
                name = line[9:]
            elif line.startswith("Status:"):
                installed = "installed" in line[8:].split()
            elif line.startswith("Version:"):
                version = line[9:]

    return pkgs


@log
def list_applications_pacman(path):
    """Find all packages installed on a arch-based linux distribution.

    See also:
    https://wiki.archlinux.org/title/pacman

    Args:
        path (str): Path to the mounted filesystem.

    Returns:
        List of packages. For example:
        [{'name': 'python', 'version': '3.10.6-1'}, ...]
    """
    pacman_db = None

    locations = [
        "var/lib/pacman/local",
        "lib/pacman/local"  # separated /var partition
    ]

    for location in locations:
        db = os.path.join(path, location)
        if os.path.exists(db):
            pacman_db = db
            break

    # Just in case the system is using btrfs.
    if pacman_db is None:
        for dir in subdirs(path):
            new_path = os.path.join(path, dir)
            for location in locations:
                db = os.path.join(new_path, location)
                if os.path.exists(db):
                    pacman_db = db
                    break
            else:
                continue
            break

    if pacman_db is None:
        L.debug("pacman database not found")
        return []

    pkgs = []
    for dir in subdirs(pacman_db):
        desc = os.path.join(pacman_db, dir, "desc")
        kv = {}
        with open(desc) as f:
            sections = re.split(r"\n(?=%[A-Z]+%)", f.read())
            for section in sections:
                section = section.strip()
                if lines := section.split("\n"):
                    k = lines[0].strip("%")
                    n = len(lines)
                    v = "" if n < 2 else lines[1:] if n > 2 else lines[1]
                    kv[k] = v
        if (name := kv.get("NAME")) and (version := kv.get("VERSION")):
            pkgs.append({
                "name": name,
                "version": version
            })

    return pkgs


@log
def list_applications_portage(path):
    """Find all packages installed on the linux distribution Gentoo Linux.

    See also:
    https://wiki.gentoo.org/wiki/Portage

    Args:
        path (str): Path to the mounted filesystem.

    Returns:
        List of packages. For example:
        [{'name': 'sys-devel/bison', 'version': '3.8.2'}, ...]
    """
    portage_db = None

    locations = [
        "var/db/pkg",
        "db/pkg"  # separated /var partition
    ]

    for location in locations:
        db = os.path.join(path, location)
        if os.path.exists(db):
            portage_db = db
            break

    # btrfs?
    if portage_db is None:
        for dir in subdirs(path):
            new_path = os.path.join(path, dir)
            for location in locations:
                db = os.path.join(new_path, location)
                if os.path.exists(db):
                    portage_db = db
                    break
            else:
                continue
            break

    if portage_db is None:
        L.debug("portage database not found")
        return []

    pkgs = []
    for cat in subdirs(portage_db):
        for pkg in subdirs(os.path.join(portage_db, cat)):
            # https://projects.gentoo.org/pms/8/pms.html#x1-150003
            if m := re.match(r"^(.+)-(\d.*)$", pkg):
                name, version = m.groups()
                pkgs.append({
                    "name": "/".join([cat, name]),
                    "version": version
                })

    return pkgs


@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 []

    rpm.setVerbosity(rpm.RPMLOG_CRIT)
    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 []

    pkgs = []
    for h in dbMatch:
        pkgs.append({
            "name": h["name"],
            "version": h["version"]
        })

    rpm.delMacro("_dbpath")

    return pkgs


@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 []

    apps = []

    # 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 apps
    if apps_native := _list_applications_windows_from_key(key):
        apps.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 apps
    if apps_emulator := _list_applications_windows_from_key(key):
        apps.extend(apps_emulator)

    return apps


@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.
    """
    apps = []
    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:
            apps.append({
                "name": name,
                "version": version
            })

    return apps