diff --git a/dissect/target/loaders/dir.py b/dissect/target/loaders/dir.py index a86a540b0..22a55e549 100644 --- a/dissect/target/loaders/dir.py +++ b/dissect/target/loaders/dir.py @@ -1,9 +1,11 @@ from __future__ import annotations import zipfile +from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING +from dissect.target.filesystem import LayerFilesystem from dissect.target.filesystems.dir import DirectoryFilesystem from dissect.target.filesystems.zip import ZipFilesystem from dissect.target.helpers import loaderutil @@ -48,6 +50,7 @@ def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str, alt_separator = "\\" case_sensitive = False + drive_letter_map = defaultdict(list) for path in dirs: drive_letter = None if isinstance(path, tuple): @@ -59,13 +62,28 @@ def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str, dfs = ZipFilesystem(path.root.fp, path.at, alt_separator=alt_separator, case_sensitive=case_sensitive) else: dfs = DirectoryFilesystem(path, alt_separator=alt_separator, case_sensitive=case_sensitive) - target.filesystems.add(dfs) - if os_type == OperatingSystem.WINDOWS: - loaderutil.add_virtual_ntfs_filesystem(target, dfs, **kwargs) + drive_letter_map[drive_letter].append(dfs) + + fs_to_add = [] + for drive_letter, dfs in drive_letter_map.items(): + if drive_letter is not None: + if len(dfs) > 1: + vfs = LayerFilesystem() + for fs in dfs: + vfs.append_fs_layer(fs) + else: + vfs = dfs[0] - if drive_letter is not None: - target.fs.mount(drive_letter.lower() + ":", dfs) + fs_to_add.append(vfs) + target.fs.mount(drive_letter.lower() + ":", vfs) + else: + fs_to_add.extend(dfs) + + for fs in fs_to_add: + target.filesystems.add(fs) + if os_type == OperatingSystem.WINDOWS: + loaderutil.add_virtual_ntfs_filesystem(target, fs, **kwargs) def find_and_map_dirs(target: Target, path: Path, **kwargs) -> None: diff --git a/dissect/target/loaders/velociraptor.py b/dissect/target/loaders/velociraptor.py index c570337f6..68737cf7f 100644 --- a/dissect/target/loaders/velociraptor.py +++ b/dissect/target/loaders/velociraptor.py @@ -61,6 +61,10 @@ def extract_drive_letter(name: str) -> Optional[str]: if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"): return name[10].lower() + # X: in URL encoding + if len(name) == 4 and name.endswith("%3A"): + return name[0].lower() + class VelociraptorLoader(DirLoader): """Load Rapid7 Velociraptor forensic image files. @@ -71,10 +75,7 @@ class VelociraptorLoader(DirLoader): {"Generic.Collectors.File":{"Root":"/","collectionSpec":"Glob\\netc/**\\nvar/log/**"}} Generic.Collectors.File (Windows) and Windows.KapeFiles.Targets (Windows) uses the accessors mft, ntfs, lazy_ntfs, - ntfs_vss and auto. The loader only supports a collection where a single accessor is used, which can be forced by - using the following configuration:: - - {"Windows.KapeFiles.Targets":{"VSSAnalysisAge":"1000","_SANS_Triage":"Y"}} + ntfs_vss and auto. The loader supports a collection where multiple accessors were used. References: - https://www.rapid7.com/products/velociraptor/ diff --git a/tests/loaders/test_velociraptor.py b/tests/loaders/test_velociraptor.py index bf2318401..3b1291e19 100644 --- a/tests/loaders/test_velociraptor.py +++ b/tests/loaders/test_velociraptor.py @@ -43,11 +43,19 @@ def create_root(sub_dir: str, tmp_path: Path) -> Path: @pytest.mark.parametrize( - "sub_dir", - ["mft", "ntfs", "ntfs_vss", "lazy_ntfs", "auto"], + "sub_dir, other_dir", + [ + ("mft", "auto"), + ("ntfs", "auto"), + ("ntfs_vss", "auto"), + ("lazy_ntfs", "auto"), + ("auto", "ntfs"), + ], ) -def test_velociraptor_loader_windows_ntfs(sub_dir: str, target_bare: Target, tmp_path: Path) -> None: +def test_windows_ntfs(sub_dir: str, other_dir: str, target_bare: Target, tmp_path: Path) -> None: root = create_root(sub_dir, tmp_path) + root.joinpath(f"uploads/{other_dir}/C%3A").mkdir(parents=True, exist_ok=True) + root.joinpath(f"uploads/{other_dir}/C%3A/other.txt").write_text("my first file") assert VelociraptorLoader.detect(root) is True @@ -65,13 +73,14 @@ def test_velociraptor_loader_windows_ntfs(sub_dir: str, target_bare: Target, tmp assert usnjrnl_records == 2 assert len(target_bare.filesystems) == 4 assert target_bare.fs.path("sysvol/C-DRIVE.txt").exists() + assert target_bare.fs.path("sysvol/other.txt").read_text() == "my first file" @pytest.mark.parametrize( "sub_dir", ["mft", "ntfs", "ntfs_vss", "lazy_ntfs", "auto"], ) -def test_velociraptor_loader_windows_ntfs_zip(sub_dir: str, target_bare: Target, tmp_path: Path) -> None: +def test_windows_ntfs_zip(sub_dir: str, target_bare: Target, tmp_path: Path) -> None: create_root(sub_dir, tmp_path) shutil.make_archive(tmp_path.joinpath("test_ntfs"), "zip", tmp_path) @@ -106,7 +115,7 @@ def test_velociraptor_loader_windows_ntfs_zip(sub_dir: str, target_bare: Target, (["uploads/auto/Library", "uploads/auto/Applications"]), ], ) -def test_dir_loader_unix(paths: list[str], target_bare: Target, tmp_path: Path) -> None: +def test_unix(paths: list[str], target_bare: Target, tmp_path: Path) -> None: root = tmp_path mkdirs(root, paths) @@ -131,7 +140,7 @@ def test_dir_loader_unix(paths: list[str], target_bare: Target, tmp_path: Path) (["uploads/auto/Library", "uploads/auto/Applications"]), ], ) -def test_dir_loader_unix_zip(paths: list[str], target_bare: Target, tmp_path: Path) -> None: +def test_unix_zip(paths: list[str], target_bare: Target, tmp_path: Path) -> None: root = tmp_path mkdirs(root, paths)