summaryrefslogtreecommitdiffstats
path: root/src/utils/sw_inventory.py
blob: 5c93ad41d097b804d4cec6373467e7807c5f9cfb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
#
# 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

from collections import namedtuple

import hivex

from src.utils.probe import getwindowsversion, getlinuxversion


Package = namedtuple('Package', ['name', 'version'])
Package.__str__ = lambda pkg: f'{pkg.name} {pkg.version}'

WINDOWS_HIVES_PATH = '/Windows/System32/config'
WINDOWS_HIVES_SOFTWARE = f'{WINDOWS_HIVES_PATH}/SOFTWARE'
DPKG_STATUS_PATH = '/var/lib/dpkg/status'
OSRELEASE_PATH = '/etc/os-release'


def _fill_package_set(h, 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 = h.node_children(key)
    valid_childs = [h.node_get_child(key, h.node_name(child))
                    for child in childs
                    for value in h.node_values(child) if h.value_key(value) == 'DisplayVersion']
    for ch in valid_childs:
        name = h.value_string(h.node_get_value(ch, 'DisplayName'))
        value = h.node_get_value(ch, 'DisplayVersion')
        version = h.value_string(value)
        pkg = Package(name, version)
        pkg_set.add(pkg)


def _fill_package_set_1(h, pkg_set):
    """
    Looks for entries in registry path
    /Microsoft/Windows/CurrentVersion/Uninstall

    Fills the given set with Package instances for each program found.
    """
    key = h.root()
    key = h.node_get_child(key, 'Microsoft')
    key = h.node_get_child(key, 'Windows')
    key = h.node_get_child(key, 'CurrentVersion')
    key = h.node_get_child(key, 'Uninstall')
    _fill_package_set(h, key, pkg_set)


def _fill_package_set_2(h, 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.
    """
    key = h.root()
    key = h.node_get_child(key, 'Wow6432Node')
    key = h.node_get_child(key, 'Microsoft')
    key = h.node_get_child(key, 'Windows')
    key = h.node_get_child(key, 'CurrentVersion')
    key = h.node_get_child(key, 'Uninstall')
    _fill_package_set(h, key, pkg_set)


def _get_package_set_windows(hivepath):
    packages = set()
    h = hivex.Hivex(hivepath)
    _fill_package_set_1(h, packages)
    _fill_package_set_2(h, packages)
    return packages


def _get_package_set_dpkg(dpkg_status_path):
    regex_pkg = '(?:^Package: )(?P<name>.*)\n(Essential:.*\n)?(?:Status: install ok installed)'
    regex_ver = '(?:^Version: )(?P<version>.*)'
    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}'
    winreghives = f'{mountpoint}{WINDOWS_HIVES_PATH}'
    osrelease = f'{mountpoint}{OSRELEASE_PATH}'
    softwarehive = f'{mountpoint}{WINDOWS_HIVES_SOFTWARE}'
    if os.path.exists(softwarehive):
        pkgset = _get_package_set_windows(softwarehive)
        osname = getwindowsversion(winreghives)
    elif os.path.exists(dpkg_status_path):
        pkgset = _get_package_set_dpkg(dpkg_status_path)
        osname = getlinuxversion(osrelease)
    else:
        raise ValueError(f'Cannot fetch software inventory at {mountpoint}')
    # Legacy software inventory first element is the OS name
    return [osname] + list(pkgset)