diff --git a/dissect/target/plugins/child/parallels.py b/dissect/target/plugins/child/parallels.py new file mode 100644 index 000000000..c24530324 --- /dev/null +++ b/dissect/target/plugins/child/parallels.py @@ -0,0 +1,68 @@ +from pathlib import Path +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.fsutil import TargetPath +from dissect.target.helpers.record import ChildTargetRecord +from dissect.target.plugin import ChildTargetPlugin +from dissect.target.target import Target + +PARALLELS_USER_PATHS = [ + "Parallels", + "Documents/Parallels", + "Library/Group Containers/*.com.parallels.desktop.appstore/Shared/Parallels", +] + +PARALLELS_SYSTEM_PATHS = [ + "/Users/Shared/Parallels", +] + + +def find_pvms(target: Target) -> Iterator[TargetPath]: + """Finds virtual machines located in default folders on a macOS target. + + Resources: + - https://kb.parallels.com/117333 + """ + for user_details in target.user_details.all_with_home(): + for parallels_path in PARALLELS_SYSTEM_PATHS: + if (path := target.fs.path(parallels_path)).exists(): + yield from iter_vms(path) + + for parallels_path in PARALLELS_USER_PATHS: + if "*" in parallels_path: + start_path, pattern = parallels_path.split("*", 1) + for path in user_details.home_path.joinpath(start_path).rglob("*" + pattern): + yield from iter_vms(path) + else: + if (path := user_details.home_path.joinpath(parallels_path)).exists(): + yield from iter_vms(path) + + +def iter_vms(path: Path) -> Iterator[TargetPath]: + """Glob for .pvm folders in the provided folder.""" + for file in path.rglob("*.pvm"): + if file.is_dir(): + yield file + + +class ParallelsChildTargetPlugin(ChildTargetPlugin): + """Child target plugin that yields Parallels Desktop VM files.""" + + __type__ = "parallels" + + def __init__(self, target: Target): + super().__init__(target) + self.pvms = list(find_pvms(target)) + + def check_compatible(self) -> None: + if not self.pvms: + raise UnsupportedPluginError("No Parallels pvm file(s) found") + + def list_children(self) -> Iterator[ChildTargetRecord]: + for pvm in self.pvms: + yield ChildTargetRecord( + type=self.__type__, + path=pvm, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/osx/_os/dissect.plist b/tests/_data/plugins/os/unix/bsd/osx/_os/dissect.plist index a85d673c3..d6b861ad7 100644 --- a/tests/_data/plugins/os/unix/bsd/osx/_os/dissect.plist +++ b/tests/_data/plugins/os/unix/bsd/osx/_os/dissect.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b89fdb2ba4488e5afcbe22693322f93e95cdbbb85cd02c42c8bed965bdf9a08 -size 1255 +oid sha256:16b94cb1bdee735b833a56cb3e09ad23f7f55f61fb169b208c83832e9aa7d5a0 +size 1259 diff --git a/tests/conftest.py b/tests/conftest.py index 16501eaff..53b8a295f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -455,6 +455,8 @@ def target_osx_users(target_osx: Target, fs_osx: VirtualFilesystem) -> Iterator[ test = absolute_path("_data/plugins/os/unix/bsd/osx/_os/test.plist") fs_osx.map_file("/var/db/dslocal/nodes/Default/users/_test.plist", test) + fs_osx.makedirs("/Users/dissect") + yield target_osx diff --git a/tests/plugins/child/test_parallels.py b/tests/plugins/child/test_parallels.py new file mode 100644 index 000000000..2a1f17d21 --- /dev/null +++ b/tests/plugins/child/test_parallels.py @@ -0,0 +1,25 @@ +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.child.parallels import ParallelsChildTargetPlugin +from dissect.target.target import Target + + +def test_parallels_child_detection(target_osx_users: Target, fs_osx: VirtualFilesystem) -> None: + """test if we correctly find Parallels child VMs on MacOS targets.""" + + fs_osx.makedirs("Users/dissect/Parallels/Windows 11.pvm") + fs_osx.makedirs("Users/dissect/Documents/Parallels/Windows 10.pvm") + fs_osx.makedirs( + "Users/dissect/Library/Group Containers/someversionnumber.com.parallels.desktop.appstore/Shared/Parallels/Windows 8.pvm" # noqa: E501 + ) + fs_osx.makedirs("Users/Shared/Parallels/Windows 7.pvm") + + target_osx_users.add_plugin(ParallelsChildTargetPlugin) + children = list(target_osx_users.list_children()) + + assert len(children) == 4 + assert [c.path for c in children] == [ + "/Users/Shared/Parallels/Windows 7.pvm", + "/Users/dissect/Parallels/Windows 11.pvm", + "/Users/dissect/Documents/Parallels/Windows 10.pvm", + "/Users/dissect/Library/Group Containers/someversionnumber.com.parallels.desktop.appstore/Shared/Parallels/Windows 8.pvm", # noqa: E501 + ] diff --git a/tests/plugins/os/unix/bsd/osx/test__os.py b/tests/plugins/os/unix/bsd/osx/test__os.py index 3f2d20534..1a488fe33 100644 --- a/tests/plugins/os/unix/bsd/osx/test__os.py +++ b/tests/plugins/os/unix/bsd/osx/test__os.py @@ -27,7 +27,7 @@ def test_unix_bsd_osx_os(target_osx_users, fs_osx): assert dissect_user.name == "_dissect" assert dissect_user.passwd == "*" - assert dissect_user.home == "/var/empty" + assert dissect_user.home == "/Users/dissect" assert isinstance(dissect_user.home, posix_path) assert dissect_user.shell == "/usr/bin/false" assert dissect_user.source == "/var/db/dslocal/nodes/Default/users/_dissect.plist"