From 022ae345cd61d2c0f1dfcdb89c4772a255e57096 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:38:14 +0200 Subject: [PATCH 01/11] add application plugins --- dissect/target/helpers/record.py | 21 ++++++ .../target/plugins/os/unix/applications.py | 69 +++++++++++++++++++ .../plugins/os/unix/linux/debian/snap.py | 69 +++++++++++++++++++ .../target/plugins/os/unix/packagemanager.py | 2 +- .../plugins/os/windows/regf/applications.py | 41 +++++++++++ .../plugins/os/unix/linux/debian/test_snap.py | 0 tests/plugins/os/unix/test_applications.py | 0 tests/plugins/os/windows/test_applications.py | 0 8 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 dissect/target/plugins/os/unix/applications.py create mode 100644 dissect/target/plugins/os/unix/linux/debian/snap.py create mode 100644 dissect/target/plugins/os/windows/regf/applications.py create mode 100644 tests/plugins/os/unix/linux/debian/test_snap.py create mode 100644 tests/plugins/os/unix/test_applications.py create mode 100644 tests/plugins/os/windows/test_applications.py diff --git a/dissect/target/helpers/record.py b/dissect/target/helpers/record.py index a481748fe..3f83da6b9 100644 --- a/dissect/target/helpers/record.py +++ b/dissect/target/helpers/record.py @@ -179,3 +179,24 @@ def DynamicDescriptor(types): # noqa ("varint", "interface_service_order"), ], ) + + +COMMON_APPLICATION_FIELDS = [ + ("datetime", "ts_modified"), + ("datetime", "ts_installed"), + ("string", "name"), + ("string", "version"), + ("string", "author"), + ("string", "type"), + ("path", "path"), +] + +UnixApplicationRecord = TargetRecordDescriptor( + "unix/application", + COMMON_APPLICATION_FIELDS, +) + +WindowsApplicationRecord = TargetRecordDescriptor( + "windows/application", + COMMON_APPLICATION_FIELDS, +) diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py new file mode 100644 index 000000000..ccfe8360a --- /dev/null +++ b/dissect/target/plugins/os/unix/applications.py @@ -0,0 +1,69 @@ +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers import configutil +from dissect.target.helpers.fsutil import TargetPath +from dissect.target.helpers.record import UnixApplicationRecord +from dissect.target.plugin import Plugin, export +from dissect.target.target import Target + + +class UnixApplicationsPlugin(Plugin): + """Unix Applications plugin.""" + + SYSTEM_PATHS = [ + "/usr/share/applications/", + "/usr/local/share/applications/", + "/var/lib/snapd/desktop/applications/", + "/var/lib/flatpak/exports/share/applications/", + ] + + USER_PATHS = [ + ".local/share/applications/", + ] + + SYSTEM_APPS = [ + "org.gnome.", + ] + + def __init__(self, target: Target): + super().__init__(target) + self.desktop_files = list(self.find_desktop_files()) + + def find_desktop_files(self) -> Iterator[TargetPath]: + for dir in self.SYSTEM_PATHS: + for file in self.target.fs.path(dir).glob("*.desktop"): + yield file + + for user_details in self.target.user_details.all_with_home(): + for dir in self.USER_PATHS: + for file in user_details.home_path.joinpath(dir).glob("*.desktop"): + yield file + + def check_compatible(self) -> None: + if not self.desktop_files: + raise UnsupportedPluginError("No application .desktop files found") + + @export(record=UnixApplicationRecord) + def applications(self) -> Iterator[UnixApplicationRecord]: + """Yield installed Unix GUI applications from GNOME and XFCE. + + Resources: + - https://wiki.archlinux.org/title/Desktop_entries + - https://specifications.freedesktop.org/desktop-entry-spec/latest/ + - https://unix.stackexchange.com/questions/582928/where-gnome-apps-are-installed + """ + for file in self.desktop_files: + parser = configutil.parse(file, hint="ini") + config = parser.get("Desktop Entry") or {} + stat = file.lstat() + + yield UnixApplicationRecord( + ts_modified=stat.st_mtime, + ts_installed=stat.st_btime if hasattr(stat, "st_btime") else None, + name=config.get("Name"), + version=config.get("Version"), + path=config.get("Exec"), + type="system" if config.get("Icon", "")[0:10] in self.SYSTEM_APPS else "user", + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/linux/debian/snap.py b/dissect/target/plugins/os/unix/linux/debian/snap.py new file mode 100644 index 000000000..faf307b62 --- /dev/null +++ b/dissect/target/plugins/os/unix/linux/debian/snap.py @@ -0,0 +1,69 @@ +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.filesystems.squashfs import SquashFSFilesystem +from dissect.target.helpers import configutil +from dissect.target.helpers.fsutil import TargetPath +from dissect.target.helpers.record import UnixApplicationRecord +from dissect.target.plugin import Plugin, export +from dissect.target.target import Target + + +class SnapPlugin(Plugin): + """Canonical Linux Snapcraft plugin. + + Reads information from installed ``*.snap`` files found in ``/var/lib/snapd/snaps``. + Logs of the ``snapd`` daemon can be parsed using the ``journal`` or ``syslog`` plugins. + + Resources: + - https://github.com/canonical/snapcraft + - https://en.wikipedia.org/wiki/Snap_(software) + """ + + PATHS = [ + "/var/lib/snapd/snaps", + ] + + def __init__(self, target: Target): + super().__init__(target) + self.installs = list(self._find_installs()) + + def check_compatible(self) -> None: + if not configutil.HAS_YAML: + raise UnsupportedPluginError("Missing required dependency ruamel.yaml") + + if not self.installs: + raise UnsupportedPluginError("No snapd install folder(s) found") + + def _find_installs(self) -> Iterator[TargetPath]: + for str_path in self.PATHS: + if (path := self.target.fs.path(str_path)).exists(): + yield path + + @export(record=UnixApplicationRecord) + def snap(self) -> Iterator[UnixApplicationRecord]: + """Yield installed snap packages.""" + + for install_path in self.installs: + for snap in install_path.glob("*.snap"): + try: + squashfs = SquashFSFilesystem(snap.open()) + + except (ValueError, NotImplementedError) as e: + self.target.log.warning("Unable to open snap file %s", snap) + self.target.log.debug("", exc_info=e) + continue + + if not (meta := squashfs.path("meta/snap.yaml")).exists(): + self.target.log.warning("Snap %s has no meta/snap.yaml file") + continue + + meta_data = configutil.parse(meta, hint="yaml") + + yield UnixApplicationRecord( + ts_modified=meta.lstat().st_mtime, + name=meta_data.get("name"), + version=meta_data.get("version"), + path=snap, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/packagemanager.py b/dissect/target/plugins/os/unix/packagemanager.py index 3bdf34175..e368dcf62 100644 --- a/dissect/target/plugins/os/unix/packagemanager.py +++ b/dissect/target/plugins/os/unix/packagemanager.py @@ -9,7 +9,7 @@ from dissect.target.plugin import Plugin, export PackageManagerLogRecord = TargetRecordDescriptor( - "unix/log/packagemanager", + "unix/packagemanager/log", [ ("datetime", "ts"), ("string", "package_manager"), diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py new file mode 100644 index 000000000..3a1f7873e --- /dev/null +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -0,0 +1,41 @@ +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import WindowsApplicationRecord +from dissect.target.plugin import Plugin, export +from dissect.target.target import Target + + +class WindowsApplicationsPlugin(Plugin): + """Windows Applications plugin.""" + + KEY = "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" + + def __init__(self, target: Target): + super().__init__(target) + self.keys = list(self.target.registry.keys(self.KEY)) + + def check_compatible(self) -> None: + if not self.target.has_function("registry"): + raise UnsupportedPluginError("No Windows registry found") + + if not self.keys: + raise UnsupportedPluginError("No 'Uninstall' registry keys found") + + @export(record=WindowsApplicationRecord) + def applications(self) -> Iterator[WindowsApplicationRecord]: + """Yields installed applications from the Windows registry.""" + for uninstall in self.keys: + for app in uninstall.subkeys(): + values = {value.name: value.value for value in app.values()} + + yield WindowsApplicationRecord( + ts_modified=app.ts, + ts_installed=values.get("InstallDate"), + name=values.get("DisplayName"), + version=values.get("DisplayVersion"), + author=values.get("Publisher"), + type="system" if values.get("SystemComponent") else "user", + path=values.get("DisplayIcon") or values.get("InstallLocation") or values.get("InstallSource"), + _target=self.target, + ) diff --git a/tests/plugins/os/unix/linux/debian/test_snap.py b/tests/plugins/os/unix/linux/debian/test_snap.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/plugins/os/unix/test_applications.py b/tests/plugins/os/unix/test_applications.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/plugins/os/windows/test_applications.py b/tests/plugins/os/windows/test_applications.py new file mode 100644 index 000000000..e69de29bb From 0b61f8d7fc015e7313a410ad93b8d9b9f550c3b6 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:33:47 +0200 Subject: [PATCH 02/11] add tests --- .../plugins/os/unix/linux/debian/snap.py | 3 +- .../plugins/os/windows/regf/applications.py | 4 +- .../os/unix/applications/code_code.desktop | 3 + .../unix/applications/firefox_firefox.desktop | 3 + .../plugins/os/unix/applications/gimp.desktop | 3 + .../os/unix/applications/python.desktop | 3 + .../os/unix/applications/terminal.desktop | 3 + .../plugins/os/unix/applications/vlc.desktop | 3 + .../applications/vmware-workstation.desktop | 3 + .../os/unix/linux/debian/snap/firefox.snap | 3 + .../plugins/os/unix/linux/debian/test_snap.py | 36 ++++++++ tests/plugins/os/unix/test_applications.py | 82 +++++++++++++++++++ .../os/windows/regf/test_applications.py | 66 +++++++++++++++ tests/plugins/os/windows/test_applications.py | 0 14 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 tests/_data/plugins/os/unix/applications/code_code.desktop create mode 100644 tests/_data/plugins/os/unix/applications/firefox_firefox.desktop create mode 100644 tests/_data/plugins/os/unix/applications/gimp.desktop create mode 100644 tests/_data/plugins/os/unix/applications/python.desktop create mode 100644 tests/_data/plugins/os/unix/applications/terminal.desktop create mode 100644 tests/_data/plugins/os/unix/applications/vlc.desktop create mode 100644 tests/_data/plugins/os/unix/applications/vmware-workstation.desktop create mode 100644 tests/_data/plugins/os/unix/linux/debian/snap/firefox.snap create mode 100644 tests/plugins/os/windows/regf/test_applications.py delete mode 100644 tests/plugins/os/windows/test_applications.py diff --git a/dissect/target/plugins/os/unix/linux/debian/snap.py b/dissect/target/plugins/os/unix/linux/debian/snap.py index faf307b62..e3ad5dc57 100644 --- a/dissect/target/plugins/os/unix/linux/debian/snap.py +++ b/dissect/target/plugins/os/unix/linux/debian/snap.py @@ -5,7 +5,7 @@ from dissect.target.helpers import configutil from dissect.target.helpers.fsutil import TargetPath from dissect.target.helpers.record import UnixApplicationRecord -from dissect.target.plugin import Plugin, export +from dissect.target.plugin import Plugin, alias, export from dissect.target.target import Target @@ -41,6 +41,7 @@ def _find_installs(self) -> Iterator[TargetPath]: yield path @export(record=UnixApplicationRecord) + @alias("snaps") def snap(self) -> Iterator[UnixApplicationRecord]: """Yield installed snap packages.""" diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index 3a1f7873e..413b110d3 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -32,10 +32,10 @@ def applications(self) -> Iterator[WindowsApplicationRecord]: yield WindowsApplicationRecord( ts_modified=app.ts, ts_installed=values.get("InstallDate"), - name=values.get("DisplayName"), + name=values.get("DisplayName") or app.name, version=values.get("DisplayVersion"), author=values.get("Publisher"), - type="system" if values.get("SystemComponent") else "user", + type="system" if values.get("SystemComponent") or not values else "user", path=values.get("DisplayIcon") or values.get("InstallLocation") or values.get("InstallSource"), _target=self.target, ) diff --git a/tests/_data/plugins/os/unix/applications/code_code.desktop b/tests/_data/plugins/os/unix/applications/code_code.desktop new file mode 100644 index 000000000..78a27aba7 --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/code_code.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adbb1b293e5cad29e494937d82762b8838244ec55df73917441b958859ece358 +size 972 diff --git a/tests/_data/plugins/os/unix/applications/firefox_firefox.desktop b/tests/_data/plugins/os/unix/applications/firefox_firefox.desktop new file mode 100644 index 000000000..ac5639cba --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/firefox_firefox.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1048d67b7e83577a2fff2239399dcff5c0311685ada454cd7defb800d1f1dc12 +size 9461 diff --git a/tests/_data/plugins/os/unix/applications/gimp.desktop b/tests/_data/plugins/os/unix/applications/gimp.desktop new file mode 100644 index 000000000..6c2813880 --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/gimp.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f77e0c692ed6f090de885e10b9c3f8b94b77fb9a8eb30b95fa019d0b0210e247 +size 15373 diff --git a/tests/_data/plugins/os/unix/applications/python.desktop b/tests/_data/plugins/os/unix/applications/python.desktop new file mode 100644 index 000000000..958029014 --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/python.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc30300f81cb89c4f1d5c3575f539cd4c74293c4752e0d9724f7f359389d31ed +size 224 diff --git a/tests/_data/plugins/os/unix/applications/terminal.desktop b/tests/_data/plugins/os/unix/applications/terminal.desktop new file mode 100644 index 000000000..05750997e --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/terminal.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9caa775a1d5ae964e7c860c22de470a7d2b62edccb8ad1c7cacd88a1b1f3c20 +size 571 diff --git a/tests/_data/plugins/os/unix/applications/vlc.desktop b/tests/_data/plugins/os/unix/applications/vlc.desktop new file mode 100644 index 000000000..66ea7b8ea --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/vlc.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce6aea28e742fb482a755a3e2aa9bc033cbc6136716ddad615454e7ccdd561fa +size 14904 diff --git a/tests/_data/plugins/os/unix/applications/vmware-workstation.desktop b/tests/_data/plugins/os/unix/applications/vmware-workstation.desktop new file mode 100644 index 000000000..6e9eceda2 --- /dev/null +++ b/tests/_data/plugins/os/unix/applications/vmware-workstation.desktop @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05fc84c9998faf664bb08c57e4183604bf1d604d9a2cfaad8332a8a949eba2c5 +size 323 diff --git a/tests/_data/plugins/os/unix/linux/debian/snap/firefox.snap b/tests/_data/plugins/os/unix/linux/debian/snap/firefox.snap new file mode 100644 index 000000000..cfb8c6434 --- /dev/null +++ b/tests/_data/plugins/os/unix/linux/debian/snap/firefox.snap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f63e48b7e4a064a993c6a9e82d6e26e1b024192e20017706227e6e054341ac +size 20480 diff --git a/tests/plugins/os/unix/linux/debian/test_snap.py b/tests/plugins/os/unix/linux/debian/test_snap.py index e69de29bb..89c443236 100644 --- a/tests/plugins/os/unix/linux/debian/test_snap.py +++ b/tests/plugins/os/unix/linux/debian/test_snap.py @@ -0,0 +1,36 @@ +from datetime import datetime, timezone +from io import BytesIO + +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.os.unix._os import UnixPlugin +from dissect.target.plugins.os.unix.linux.debian.snap import SnapPlugin +from dissect.target.target import Target +from tests._utils import absolute_path + + +def test_snap_packages(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + """test if snap packages are discovered on unix systems""" + + fs_unix.map_file_fh("/etc/hostname", BytesIO(b"hostname")) + fs_unix.map_file( + "/var/lib/snapd/snaps/firefox_12345.snap", + absolute_path("_data/plugins/os/unix/linux/debian/snap/firefox.snap"), + ) + fs_unix.map_file( + "/var/lib/snapd/snaps/firefox_67890.snap", + absolute_path("_data/plugins/os/unix/linux/debian/snap/firefox.snap"), + ) + + target_unix_users.add_plugin(UnixPlugin) + target_unix_users.add_plugin(SnapPlugin) + + results = list(target_unix_users.snaps()) + assert len(results) == 2 + + assert results[0].hostname == "hostname" + assert results[0].ts_modified == datetime(2024, 9, 17, 13, 18, 58, tzinfo=timezone.utc) + assert results[0].name == "firefox" + assert results[0].version == "129.0.2-1" + assert results[0].author is None + assert results[0].type is None + assert results[0].path == "/var/lib/snapd/snaps/firefox_12345.snap" diff --git a/tests/plugins/os/unix/test_applications.py b/tests/plugins/os/unix/test_applications.py index e69de29bb..ae4153ae3 100644 --- a/tests/plugins/os/unix/test_applications.py +++ b/tests/plugins/os/unix/test_applications.py @@ -0,0 +1,82 @@ +from io import BytesIO + +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.os.unix._os import UnixPlugin +from dissect.target.plugins.os.unix.applications import UnixApplicationsPlugin +from dissect.target.target import Target +from tests._utils import absolute_path + + +def test_unix_applications_desktop_files(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + """test if .desktop files registering installed applications are detected correctly""" + + fs_unix.map_file_fh("/etc/hostname", BytesIO(b"hostname")) + + # system paths + fs_unix.map_file( + "/var/lib/snapd/desktop/applications/firefox_firefox.desktop", + absolute_path("_data/plugins/os/unix/applications/firefox_firefox.desktop"), + ) + fs_unix.map_file( + "/var/lib/snapd/desktop/applications/code_code.desktop", + absolute_path("_data/plugins/os/unix/applications/code_code.desktop"), + ) + fs_unix.map_file( + "/usr/share/applications/gimp.desktop", + absolute_path("_data/plugins/os/unix/applications/gimp.desktop"), + ) + fs_unix.map_file( + "/usr/local/share/applications/vmware-workstation.desktop", + absolute_path("_data/plugins/os/unix/applications/vmware-workstation.desktop"), + ) + fs_unix.map_file( + "/var/lib/flatpak/exports/share/applications/python.desktop", + absolute_path("_data/plugins/os/unix/applications/python.desktop"), + ) + + # user paths + fs_unix.map_file( + "/home/user/.local/share/applications/vlc.desktop", + absolute_path("_data/plugins/os/unix/applications/vlc.desktop"), + ) + fs_unix.map_file( + "/root/.local/share/applications/terminal.desktop", + absolute_path("_data/plugins/os/unix/applications/terminal.desktop"), + ) + + target_unix_users.add_plugin(UnixPlugin) + target_unix_users.add_plugin(UnixApplicationsPlugin) + results = sorted(list(target_unix_users.applications()), key=lambda r: r.name) + + assert len(results) == 7 + + assert results[0].ts_installed is None + assert results[0].name == "Firefox Web Browser" + assert results[0].version == "1.0" + assert results[0].author is None + assert results[0].type == "user" + assert ( + results[0].path + == "env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/firefox_firefox.desktop /snap/bin/firefox %u" + ) # noqa: E501 + assert results[0].hostname == "hostname" + + assert [r.name for r in results] == [ + "Firefox Web Browser", + "GNU Image Manipulation Program", + "Python (v3.12)", + "Terminal", + "VLC media player", + "VMware Workstation", + "Visual Studio Code", + ] + + assert [r.path for r in results] == [ + "env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/firefox_firefox.desktop /snap/bin/firefox %u", + "gimp-2.10 %U", + "/usr/bin/python3.12", + "gnome-terminal", + "/usr/bin/vlc --started-from-file %U", + "/usr/bin/vmware %U", + "env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/code_code.desktop /snap/bin/code --force-user-env %F", + ] diff --git a/tests/plugins/os/windows/regf/test_applications.py b/tests/plugins/os/windows/regf/test_applications.py new file mode 100644 index 000000000..fcb5afe8a --- /dev/null +++ b/tests/plugins/os/windows/regf/test_applications.py @@ -0,0 +1,66 @@ +from datetime import datetime, timezone + +from dissect.target.helpers.regutil import VirtualHive, VirtualKey +from dissect.target.plugins.os.windows.regf.applications import ( + WindowsApplicationsPlugin, +) +from dissect.target.target import Target + + +def test_windows_applications(target_win_users: Target, hive_hklm: VirtualHive) -> None: + """test if windows applications are detected correctly in the registry""" + + firefox_name = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Mozilla Firefox 123.0.1 (x64 nl)" + firefox_key = VirtualKey(hive_hklm, firefox_name) + firefox_key.add_value("Comments", "Mozilla Firefox 123.0.1 (x64 nl)") + firefox_key.add_value("DisplayIcon", "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0") + firefox_key.add_value("DisplayName", "Mozilla Firefox (x64 nl)") + firefox_key.add_value("DisplayVersion", "123.0.1") + firefox_key.add_value("EstimatedSize", 238271) + firefox_key.add_value("HelpLink", "https://support.mozilla.org") + firefox_key.add_value("InstallLocation", "C:\\Program Files\\Mozilla Firefox") + firefox_key.add_value("NoModify", 1) + firefox_key.add_value("NoRepair", 1) + firefox_key.add_value("Publisher", "Mozilla") + firefox_key.add_value("URLInfoAbout", "https://www.mozilla.org") + firefox_key.add_value("URLUpdateInfo", "https://www.mozilla.org/firefox/123.0.1/releasenotes") + firefox_key.add_value("UninstallString", '"C:\\Program Files\\Mozilla Firefox\\uninstall\\helper.exe"') + hive_hklm.map_key(firefox_name, firefox_key) + + chrome_name = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{47FB91DD-98F3-3C87-A963-357B14EAC7C9}" + chrome_key = VirtualKey(hive_hklm, chrome_name) + chrome_key.add_value("DisplayVersion", "122.0.6261.95") + chrome_key.add_value("InstallDate", "20240301") + chrome_key.add_value("InstallLocation", "") + chrome_key.add_value("InstallSource", "C:\\Users\\user\\Desktop\\GoogleChromeEnterpriseBundle64\\Installers\\") + chrome_key.add_value("ModifyPath", "MsiExec.exe /X{47FB91DD-98F3-3C87-A963-357B14EAC7C9}") + chrome_key.add_value("NoModify", 1) + chrome_key.add_value("Publisher", "Google LLC") + chrome_key.add_value("EstimatedSize", 113725) + chrome_key.add_value("UninstallString", "MsiExec.exe /X{47FB91DD-98F3-3C87-A963-357B14EAC7C9}") + chrome_key.add_value("VersionMajor", 70) + chrome_key.add_value("VersionMinor", 29) + chrome_key.add_value("WindowsInstaller", 1) + chrome_key.add_value("Version", 1176322143) + chrome_key.add_value("Language", 1033) + chrome_key.add_value("DisplayName", "Google Chrome") + hive_hklm.map_key(chrome_name, chrome_key) + + target_win_users.add_plugin(WindowsApplicationsPlugin) + results = sorted(list(target_win_users.applications()), key=lambda r: r.name) + + assert len(results) == 2 + + assert results[0].ts_installed == datetime(2024, 3, 1, 0, 0, 0, tzinfo=timezone.utc) + assert results[0].name == "Google Chrome" + assert results[0].version == "122.0.6261.95" + assert results[0].author == "Google LLC" + assert results[0].type == "user" + assert results[0].path == "C:\\Users\\user\\Desktop\\GoogleChromeEnterpriseBundle64\\Installers\\" + + assert results[0].ts_installed is None + assert results[0].name == "Mozilla Firefox (x64 nl)" + assert results[0].version == "123.0.1" + assert results[0].author == "Mozilla" + assert results[0].type == "user" + assert results[0].path == "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0" diff --git a/tests/plugins/os/windows/test_applications.py b/tests/plugins/os/windows/test_applications.py deleted file mode 100644 index e69de29bb..000000000 From 95a0d05be429886c8ba3f341e45889639005cba1 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:51:17 +0200 Subject: [PATCH 03/11] fix tests --- tests/plugins/os/windows/regf/test_applications.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/plugins/os/windows/regf/test_applications.py b/tests/plugins/os/windows/regf/test_applications.py index fcb5afe8a..281831543 100644 --- a/tests/plugins/os/windows/regf/test_applications.py +++ b/tests/plugins/os/windows/regf/test_applications.py @@ -58,9 +58,9 @@ def test_windows_applications(target_win_users: Target, hive_hklm: VirtualHive) assert results[0].type == "user" assert results[0].path == "C:\\Users\\user\\Desktop\\GoogleChromeEnterpriseBundle64\\Installers\\" - assert results[0].ts_installed is None - assert results[0].name == "Mozilla Firefox (x64 nl)" - assert results[0].version == "123.0.1" - assert results[0].author == "Mozilla" - assert results[0].type == "user" - assert results[0].path == "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0" + assert results[1].ts_installed is None + assert results[1].name == "Mozilla Firefox (x64 nl)" + assert results[1].version == "123.0.1" + assert results[1].author == "Mozilla" + assert results[1].type == "user" + assert results[1].path == "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0" From d509f5c6717a3ca9c3cfaf05b28a8dcab9576e63 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:08:21 +0200 Subject: [PATCH 04/11] Apply suggestions from code review Co-authored-by: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> --- dissect/target/plugins/os/unix/applications.py | 6 ++---- dissect/target/plugins/os/windows/regf/applications.py | 4 +--- tests/plugins/os/unix/linux/debian/test_snap.py | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py index ccfe8360a..c6da64360 100644 --- a/dissect/target/plugins/os/unix/applications.py +++ b/dissect/target/plugins/os/unix/applications.py @@ -22,9 +22,7 @@ class UnixApplicationsPlugin(Plugin): ".local/share/applications/", ] - SYSTEM_APPS = [ - "org.gnome.", - ] + SYSTEM_APPS = ("org.gnome.",) def __init__(self, target: Target): super().__init__(target) @@ -64,6 +62,6 @@ def applications(self) -> Iterator[UnixApplicationRecord]: name=config.get("Name"), version=config.get("Version"), path=config.get("Exec"), - type="system" if config.get("Icon", "")[0:10] in self.SYSTEM_APPS else "user", + type="system" if config.get("Icon", "").startswith(self.SYSTEM_APPS) else "user", _target=self.target, ) diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index 413b110d3..6186bb64e 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -9,11 +9,9 @@ class WindowsApplicationsPlugin(Plugin): """Windows Applications plugin.""" - KEY = "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" - def __init__(self, target: Target): super().__init__(target) - self.keys = list(self.target.registry.keys(self.KEY)) + self.keys = list(self.target.registry.keys("HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall")) def check_compatible(self) -> None: if not self.target.has_function("registry"): diff --git a/tests/plugins/os/unix/linux/debian/test_snap.py b/tests/plugins/os/unix/linux/debian/test_snap.py index 89c443236..2ae67e45e 100644 --- a/tests/plugins/os/unix/linux/debian/test_snap.py +++ b/tests/plugins/os/unix/linux/debian/test_snap.py @@ -24,6 +24,8 @@ def test_snap_packages(target_unix_users: Target, fs_unix: VirtualFilesystem) -> target_unix_users.add_plugin(UnixPlugin) target_unix_users.add_plugin(SnapPlugin) + assert target_unix_users.has_function("snap") + results = list(target_unix_users.snaps()) assert len(results) == 2 From 6692ae2b149e2f1ea528a7dea4f3e52007212ebd Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:09:27 +0200 Subject: [PATCH 05/11] Update dissect/target/plugins/os/unix/applications.py Co-authored-by: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> --- dissect/target/plugins/os/unix/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py index c6da64360..9d54f25b5 100644 --- a/dissect/target/plugins/os/unix/applications.py +++ b/dissect/target/plugins/os/unix/applications.py @@ -28,7 +28,7 @@ def __init__(self, target: Target): super().__init__(target) self.desktop_files = list(self.find_desktop_files()) - def find_desktop_files(self) -> Iterator[TargetPath]: + def _find_desktop_files(self) -> Iterator[TargetPath]: for dir in self.SYSTEM_PATHS: for file in self.target.fs.path(dir).glob("*.desktop"): yield file From f61ec049e85a695b8b0e47373a0e3732b7edaf70 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:58:54 +0200 Subject: [PATCH 06/11] implement review suggestions --- dissect/target/helpers/regutil.py | 11 +++- .../target/plugins/os/unix/applications.py | 5 +- tests/plugins/os/unix/test_applications.py | 12 +++- .../os/windows/regf/test_applications.py | 57 +++++++++++++++---- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/dissect/target/helpers/regutil.py b/dissect/target/helpers/regutil.py index ad3eec841..f58ffde32 100644 --- a/dissect/target/helpers/regutil.py +++ b/dissect/target/helpers/regutil.py @@ -296,6 +296,7 @@ def __init__(self, hive: RegistryHive, path: str, class_name: Optional[str] = No self._class_name = class_name self._values: dict[str, RegistryValue] = {} self._subkeys: dict[str, RegistryKey] = {} + self._timestamp: datetime = None self.top: RegistryKey = None super().__init__(hive=hive) @@ -325,11 +326,19 @@ def path(self) -> str: return self._path @property - def timestamp(self) -> datetime: + def timestamp(self) -> datetime | None: if self.top: return self.top.timestamp + + if self._timestamp: + return self._timestamp + return None + @timestamp.setter + def timestamp(self, ts: datetime) -> None: + self._timestamp = ts + def subkey(self, subkey: str) -> RegistryKey: try: return self._subkeys[subkey.lower()] diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py index 9d54f25b5..69da06536 100644 --- a/dissect/target/plugins/os/unix/applications.py +++ b/dissect/target/plugins/os/unix/applications.py @@ -26,7 +26,7 @@ class UnixApplicationsPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.desktop_files = list(self.find_desktop_files()) + self.desktop_files = list(self._find_desktop_files()) def _find_desktop_files(self) -> Iterator[TargetPath]: for dir in self.SYSTEM_PATHS: @@ -52,8 +52,7 @@ def applications(self) -> Iterator[UnixApplicationRecord]: - https://unix.stackexchange.com/questions/582928/where-gnome-apps-are-installed """ for file in self.desktop_files: - parser = configutil.parse(file, hint="ini") - config = parser.get("Desktop Entry") or {} + config = configutil.parse(file, hint="ini").get("Desktop Entry") or {} stat = file.lstat() yield UnixApplicationRecord( diff --git a/tests/plugins/os/unix/test_applications.py b/tests/plugins/os/unix/test_applications.py index ae4153ae3..b50f545d6 100644 --- a/tests/plugins/os/unix/test_applications.py +++ b/tests/plugins/os/unix/test_applications.py @@ -78,5 +78,15 @@ def test_unix_applications_desktop_files(target_unix_users: Target, fs_unix: Vir "gnome-terminal", "/usr/bin/vlc --started-from-file %U", "/usr/bin/vmware %U", - "env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/code_code.desktop /snap/bin/code --force-user-env %F", + "env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/code_code.desktop /snap/bin/code --force-user-env %F", # noqa: E501 + ] + + assert [r.type for r in results] == [ + "user", + "user", + "user", + "system", + "user", + "user", + "user", ] diff --git a/tests/plugins/os/windows/regf/test_applications.py b/tests/plugins/os/windows/regf/test_applications.py index 281831543..1150a76cd 100644 --- a/tests/plugins/os/windows/regf/test_applications.py +++ b/tests/plugins/os/windows/regf/test_applications.py @@ -46,21 +46,54 @@ def test_windows_applications(target_win_users: Target, hive_hklm: VirtualHive) chrome_key.add_value("DisplayName", "Google Chrome") hive_hklm.map_key(chrome_name, chrome_key) + addressbook_name = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\AddressBook" + addressbook_key = VirtualKey(hive_hklm, addressbook_name) + addressbook_key.timestamp = datetime(2024, 12, 31, 13, 37, 0, tzinfo=timezone.utc) + hive_hklm.map_key(addressbook_name, addressbook_key) + + msvc_name = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{D5D19E2F-7189-42FE-8103-92CD1FA457C2}" + msvc_key = VirtualKey(hive_hklm, msvc_name) + msvc_key.add_value("DisplayName", "Microsoft Visual C++ 2022 X64 Minimum Runtime - 14.36.32532") + msvc_key.add_value("InstallDate", "20240301") + msvc_key.add_value("DisplayVersion", "14.36.32532") + msvc_key.add_value("Publisher", "Microsoft Corporation") + msvc_key.add_value( + "InstallSource", + "C:\\ProgramData\\Package Cache\\{D5D19E2F-7189-42FE-8103-92CD1FA457C2}v14.36.32532\\packages\\vcRuntimeMinimum_amd64\\", # noqa: E501 + ) + msvc_key.add_value("SystemComponent", 1) + hive_hklm.map_key(msvc_name, msvc_key) + target_win_users.add_plugin(WindowsApplicationsPlugin) results = sorted(list(target_win_users.applications()), key=lambda r: r.name) - assert len(results) == 2 + assert len(results) == 4 - assert results[0].ts_installed == datetime(2024, 3, 1, 0, 0, 0, tzinfo=timezone.utc) - assert results[0].name == "Google Chrome" - assert results[0].version == "122.0.6261.95" - assert results[0].author == "Google LLC" - assert results[0].type == "user" - assert results[0].path == "C:\\Users\\user\\Desktop\\GoogleChromeEnterpriseBundle64\\Installers\\" + assert results[0].ts_installed is None + assert results[0].ts_modified == datetime(2024, 12, 31, 13, 37, 0, tzinfo=timezone.utc) + assert results[0].name == "AddressBook" + assert results[0].type == "system" - assert results[1].ts_installed is None - assert results[1].name == "Mozilla Firefox (x64 nl)" - assert results[1].version == "123.0.1" - assert results[1].author == "Mozilla" + assert results[1].ts_installed == datetime(2024, 3, 1, 0, 0, 0, tzinfo=timezone.utc) + assert results[1].name == "Google Chrome" + assert results[1].version == "122.0.6261.95" + assert results[1].author == "Google LLC" assert results[1].type == "user" - assert results[1].path == "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0" + assert results[1].path == "C:\\Users\\user\\Desktop\\GoogleChromeEnterpriseBundle64\\Installers\\" + + assert results[2].ts_installed == datetime(2024, 3, 1, 0, 0, 0, tzinfo=timezone.utc) + assert results[2].name == "Microsoft Visual C++ 2022 X64 Minimum Runtime - 14.36.32532" + assert results[2].version == "14.36.32532" + assert results[2].author == "Microsoft Corporation" + assert results[2].type == "system" + assert ( + results[2].path + == "C:\\ProgramData\\Package Cache\\{D5D19E2F-7189-42FE-8103-92CD1FA457C2}v14.36.32532\\packages\\vcRuntimeMinimum_amd64\\" # noqa: E501 + ) + + assert results[3].ts_installed is None + assert results[3].name == "Mozilla Firefox (x64 nl)" + assert results[3].version == "123.0.1" + assert results[3].author == "Mozilla" + assert results[3].type == "user" + assert results[3].path == "C:\\Program Files\\Mozilla Firefox\\firefox.exe,0" From f354b97c4de628a9ac9011d4ec94798ddc44ef0c Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:34:51 +0200 Subject: [PATCH 07/11] add better docstrings --- .../target/plugins/os/unix/applications.py | 12 ++++++++ .../plugins/os/unix/linux/debian/snap.py | 29 ++++++++++++------- .../plugins/os/windows/regf/applications.py | 21 +++++++++++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py index 69da06536..b2b3d100f 100644 --- a/dissect/target/plugins/os/unix/applications.py +++ b/dissect/target/plugins/os/unix/applications.py @@ -50,6 +50,18 @@ def applications(self) -> Iterator[UnixApplicationRecord]: - https://wiki.archlinux.org/title/Desktop_entries - https://specifications.freedesktop.org/desktop-entry-spec/latest/ - https://unix.stackexchange.com/questions/582928/where-gnome-apps-are-installed + + Yields ``UnixApplicationRecord``s with the following fields: + + .. code-block:: text + + ts_modified (datetime): timestamp when the installation was modified + ts_installed (datetime): timestamp when the application was installed on the system + name (string): name of the application + version (string): version of the application + author (string): author of the application + type (string): type of the application, either user or system + path (string): path to the desktop file entry of the application """ for file in self.desktop_files: config = configutil.parse(file, hint="ini").get("Desktop Entry") or {} diff --git a/dissect/target/plugins/os/unix/linux/debian/snap.py b/dissect/target/plugins/os/unix/linux/debian/snap.py index e3ad5dc57..189f3b9da 100644 --- a/dissect/target/plugins/os/unix/linux/debian/snap.py +++ b/dissect/target/plugins/os/unix/linux/debian/snap.py @@ -10,15 +10,7 @@ class SnapPlugin(Plugin): - """Canonical Linux Snapcraft plugin. - - Reads information from installed ``*.snap`` files found in ``/var/lib/snapd/snaps``. - Logs of the ``snapd`` daemon can be parsed using the ``journal`` or ``syslog`` plugins. - - Resources: - - https://github.com/canonical/snapcraft - - https://en.wikipedia.org/wiki/Snap_(software) - """ + """Canonical Linux Snapcraft plugin.""" PATHS = [ "/var/lib/snapd/snaps", @@ -43,7 +35,24 @@ def _find_installs(self) -> Iterator[TargetPath]: @export(record=UnixApplicationRecord) @alias("snaps") def snap(self) -> Iterator[UnixApplicationRecord]: - """Yield installed snap packages.""" + """Yields installed Canonical Linux Snapcraft (snaps) applications on the target system. + + Reads information from installed SquashFS ``*.snap`` files found in ``/var/lib/snapd/snaps``. + Logs of the ``snapd`` daemon can be parsed using the ``journal`` or ``syslog`` plugins. + + Resources: + - https://github.com/canonical/snapcraft + - https://en.wikipedia.org/wiki/Snap_(software) + + Yields ``UnixApplicationRecord``s with the following fields: + + .. code-block:: text + + ts_modified (datetime): timestamp when the installation was modified + name (string): name of the application + version (string): version of the application + path (string): path to the application snap file + """ for install_path in self.installs: for snap in install_path.glob("*.snap"): diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index 6186bb64e..1118e2dc8 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -22,7 +22,26 @@ def check_compatible(self) -> None: @export(record=WindowsApplicationRecord) def applications(self) -> Iterator[WindowsApplicationRecord]: - """Yields installed applications from the Windows registry.""" + """Yields currently installed applications from the Windows registry. + + Use the Windows eventlog plugin (``evtx``, ``evt``) to parse install and uninstall events + of applications and services (e.g. ``4697``, ``110707``, ``1034`` and ``11724``). + + Resources: + - https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key + + Yields ``WindowsApplicationRecord``s with the following fields: + + .. code-block:: text + + ts_modified (datetime): timestamp when the installation was modified according to the registry + ts_installed (datetime): timestamp when the application was installed according to the application + name (string): name of the application + version (string): version of the application + author (string): author of the application + type (string): type of the application, either user or system + path (string): path to the installed location or installer of the application + """ for uninstall in self.keys: for app in uninstall.subkeys(): values = {value.name: value.value for value in app.values()} From ae15c0a66d52aff742478b1c3b9b890f19c3a788 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:31:00 +0200 Subject: [PATCH 08/11] fix InstallDate regf parsing --- dissect/target/plugins/os/windows/regf/applications.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index 1118e2dc8..a16ecc16e 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Iterator from dissect.target.exceptions import UnsupportedPluginError @@ -45,10 +46,14 @@ def applications(self) -> Iterator[WindowsApplicationRecord]: for uninstall in self.keys: for app in uninstall.subkeys(): values = {value.name: value.value for value in app.values()} + install_date = None + + if install_date := values.get("InstallDate"): + install_date = datetime.strptime(install_date, "%Y%m%d") yield WindowsApplicationRecord( ts_modified=app.ts, - ts_installed=values.get("InstallDate"), + ts_installed=install_date, name=values.get("DisplayName") or app.name, version=values.get("DisplayVersion"), author=values.get("Publisher"), From 31a61edc111ec9b28c3cc14a3e67d9e9e0116894 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:32:54 +0200 Subject: [PATCH 09/11] fix rst --- dissect/target/plugins/os/unix/applications.py | 2 +- dissect/target/plugins/os/windows/regf/applications.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/applications.py b/dissect/target/plugins/os/unix/applications.py index b2b3d100f..b1331110a 100644 --- a/dissect/target/plugins/os/unix/applications.py +++ b/dissect/target/plugins/os/unix/applications.py @@ -51,7 +51,7 @@ def applications(self) -> Iterator[UnixApplicationRecord]: - https://specifications.freedesktop.org/desktop-entry-spec/latest/ - https://unix.stackexchange.com/questions/582928/where-gnome-apps-are-installed - Yields ``UnixApplicationRecord``s with the following fields: + Yields ``UnixApplicationRecord`` records with the following fields: .. code-block:: text diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index a16ecc16e..67eca70f9 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -31,7 +31,7 @@ def applications(self) -> Iterator[WindowsApplicationRecord]: Resources: - https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key - Yields ``WindowsApplicationRecord``s with the following fields: + Yields ``WindowsApplicationRecord`` records with the following fields: .. code-block:: text From f3bf5d1622780917154ec249322fa7f05522bd6a Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:33:18 +0200 Subject: [PATCH 10/11] fix tests --- dissect/target/plugins/os/unix/linux/debian/snap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/unix/linux/debian/snap.py b/dissect/target/plugins/os/unix/linux/debian/snap.py index 189f3b9da..76a7de702 100644 --- a/dissect/target/plugins/os/unix/linux/debian/snap.py +++ b/dissect/target/plugins/os/unix/linux/debian/snap.py @@ -44,7 +44,7 @@ def snap(self) -> Iterator[UnixApplicationRecord]: - https://github.com/canonical/snapcraft - https://en.wikipedia.org/wiki/Snap_(software) - Yields ``UnixApplicationRecord``s with the following fields: + Yields ``UnixApplicationRecord`` records with the following fields: .. code-block:: text From 820b10008ed7839371a8191b29fffa4d89b7de5a Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:26:52 +0100 Subject: [PATCH 11/11] Update dissect/target/plugins/os/windows/regf/applications.py Co-authored-by: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> --- dissect/target/plugins/os/windows/regf/applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/regf/applications.py b/dissect/target/plugins/os/windows/regf/applications.py index 67eca70f9..abcb1444c 100644 --- a/dissect/target/plugins/os/windows/regf/applications.py +++ b/dissect/target/plugins/os/windows/regf/applications.py @@ -46,7 +46,6 @@ def applications(self) -> Iterator[WindowsApplicationRecord]: for uninstall in self.keys: for app in uninstall.subkeys(): values = {value.name: value.value for value in app.values()} - install_date = None if install_date := values.get("InstallDate"): install_date = datetime.strptime(install_date, "%Y%m%d")