Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions dissect/target/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,42 @@ def map(self, target: Target) -> None:
raise NotImplementedError


class MiddlewareLoader(Loader):
"""A base class for preparing arbitrary data to be used by other :class:`Loader`s.

Instead of mapping data directly to a :class:`Target <dissect.target.target.Target>`, loaders of this type
prepare data in some way and make it available for other :class:`Loader`s to use.

Subclasses should implement the :method:`detect` method like any other loader, and return a path to the prepared
data in the :method:`prepare` method . The loading mechanism will then use that path to find other loaders to map
the prepared data into the target.

Feels like forever since I've heard the term "middleware", I'm bringing it back baby!
"""

def __init__(self, path: Path, *, fallbacks: list[type[Loader]] | None = None, **kwargs):
super().__init__(path, **kwargs)
# This will be the loader that successfully mapped the prepared path
self.loader = None

@staticmethod
def detect(path: Path) -> bool:
raise NotImplementedError

def prepare(self, target: Target) -> Path:
raise NotImplementedError

def map(self, target: Target) -> None:
path = self.prepare(target)

if (loader := find_loader(path, fallbacks=[DirLoader, RawLoader])) is not None:
ldr = loader(path)
ldr.map(target)

# Store a reference to the loader if we successfully mapped
self.loader = ldr


def register(module_name: str, class_name: str, internal: bool = True) -> None:
"""Registers a ``Loader`` class inside ``LOADERS``.

Expand Down
36 changes: 15 additions & 21 deletions dissect/target/loaders/vbk.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from dissect.target.exceptions import LoaderError
from dissect.target.filesystem import VirtualFilesystem
from dissect.target.filesystems.vbk import VbkFilesystem
from dissect.target.loader import Loader, find_loader
from dissect.target.loaders.raw import RawLoader
from dissect.target.loader import MiddlewareLoader

if TYPE_CHECKING:
from pathlib import Path
Expand All @@ -19,7 +18,7 @@
RE_RAW_DISK = re.compile(r"(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})|(?:DEV__.+)")


class VbkLoader(Loader):
class VbkLoader(MiddlewareLoader):
"""Load Veaam Backup (VBK) files.

References:
Expand All @@ -35,7 +34,7 @@ def __init__(self, path: Path, **kwargs):
def detect(path: Path) -> bool:
return path.suffix.lower() == ".vbk"

def map(self, target: Target) -> None:
def prepare(self, target: Target) -> Path:
# We haven't really researched any of the VBK metadata yet, so just try some common formats
root = self.vbkfs.path("/")
if (base := next(root.glob("*"), None)) is None:
Expand All @@ -51,24 +50,19 @@ def map(self, target: Target) -> None:

candidates.append(root.joinpath("+".join(map(str, disks))))

# Try to find a loader
for candidate in candidates:
if candidate.suffix.lower() == ".vmcx":
# For VMCX files we need to massage the file layout a bit
vfs = VirtualFilesystem()
vfs.map_file_entry(candidate.name, candidate)
# We should only have one candidate at this point
if len(candidates) > 1:
Comment on lines +53 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only change that does not make sense to me right now. Why are we not iterating candidates and loading the first "valid" one anymore?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while since I looked at VBK and I don't have access to any data anymore, so I might just be forgetting something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This current setup doesn't really allow it because we can only return a single path, and I can't really think of a nicer way to allow that way, except for changing prepare to be an iterator instead of simply returning a path. I can maybe imagine a few more usecases where that could be useful, so maybe it's worth changing to that. Perhaps the reviewer can weigh in too.

But if I understand the logic in the VBK loader correctly, the candidates list should contain paths only if the globs for either vmx or vmcx succeeded, and otherwise the multiloader path. I don't expect there to be a valid way to create a VBK that contains both a vmx and a vmcx file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allright, makes sense to me.

raise LoaderError("Unsupported VBK structure, use `-L raw` to manually inspect the VBK")

for entry in chain(base.glob("Ide*/*"), base.glob("Scsi*/*")):
vfs.map_file_entry(entry.name, entry)
candidate = candidates[0]
if candidate.suffix.lower() == ".vmcx":
# For VMCX files we need to massage the file layout a bit
vfs = VirtualFilesystem()
vfs.map_file_entry(candidate.name, candidate)

candidate = vfs.path(candidate.name)
for entry in chain(base.glob("Ide*/*"), base.glob("Scsi*/*")):
vfs.map_file_entry(entry.name, entry)

if (loader := find_loader(candidate, fallbacks=[RawLoader])) is not None:
ldr = loader(candidate)
ldr.map(target)
candidate = vfs.path(candidate.name)

# Store a reference to the loader if we successfully mapped
self.loader = ldr
break
else:
raise LoaderError("Unsupported VBK structure, use `-L raw` to manually inspect the VBK")
return candidate
Loading