Filesystems#
In Dissect, filesystems are a lot more than actual filesystems on a disk. Because if you squint hard enough, almost
anything can be a filesystem! Dissect has various real filesystem implementations, such as
dissect.ntfs or dissect.vmfs, but Dissect also supports a lot of other
things that resemble a filesystem. For example, a .tar
file, a directory on your local filesystem,
or even the API of a remote EDR agent can all be considered a type of filesystem.
The filesystem abstraction layer in dissect.target
makes working with all of these different types of filesystems
quite easy. Most of the common use-cases, such as interacting with “normal” filesystems like NTFS or XFS, are already
supported, and when you require something exotic the APIs are usually sufficient to achieve your goal. The common
use-cases are automatically handled when using the various tools in dissect.target
, and when
you can use the dissect.target.filesystem
API to achieve most other tasks. You can also choose to go one level
deeper and directly use the API from the individual Dissect libraries, such as dissect.ntfs.
Most of the APIs closely resemble the Python standard library, so they should look familiar if you’re already
comfortable with Python.
See also
Want to learn more about opening a filesystem in Python? Continue reading here.
View all available filesystem implementations at dissect.target.filesystems
.
There are some special types of “filesystem” that we will explain in a little more detail: the virtual filesystem and the root filesystem.
Virtual filesystem#
Sometimes you don’t need to write a full fledged filesystem implementation and you just need a simple skeleton of one.
For example, when you want to map a file on your own local filesystem into a dissect.target
filesystem, represent
the contents of a .zip
or .tar
file as a dissect.target
filesystem, or even just a completely
fake and virtual file or directory. There would be a lot of duplicate boilerplate to achieve these simple tasks. For this
reason dissect.target
has a VirtualFilesystem
that should make most of these
tasks easy to do. Coincidentally it’s also very useful in unit tests!
from io import BytesIO
from dissect.target.filesystem import VirtualFile, VirtualFilesystem
vfs = VirtualFilesystem()
# Create some virtual directories
vfs.makedirs("/here/be/directories")
# Map a real file (``/etc/hostname``) on your local filesystem to ``/myhostname`` in the virtual filesystem
vfs.map_file("/myhostname", "/etc/hostname")
# Map an arbitrary file-like object to ``/path/to/some/file``
vfs.map_file_fh("/path/to/some/file", BytesIO(b"content"))
# Create a virtual symlink
vfs.symlink("/path/to/some/", "dirlink")
# Obtain a Path-like object to the root and print some info
path = vfs.path()
for entry in path.rglob("*"):
print(str(entry))
print(path.joinpath("/path/to/some/file").read_text())
print(path.joinpath("/dirlink/file").read_text())
print(path.joinpath("/myhostname").read_text())
See also
View the API documentation and source of VirtualFilesystem
,
VirtualFile
and VirtualDirectory
for more
information.
Browse the dissect.target
source for more advanced usage examples of VirtualFilesystem
.
Root filesystem#
In dissect.target
we try to reconstruct the filesystem structure of a target as close as possible to how it would
look and work on a live system. This often means that we have to “mount” filesystems to some arbitrary paths, or
overlaying different filesystems on top of each other. To help with this we introduced the concept of a “root”
filesystem.
The root filesystem is a special type of virtual filesystem that allows multiple filesystems to overlay each other. It allows for complex filesystem structures, such as instances where you have a “filesystem” that only contains metadata, overlayed with one that contains file content for a select number of files. It also has some helper methods for “mounting” other filesystems into the root filesystem.
See also
View the API documentation and source of RootFilesystem
and
RootFilesystemEntry
for more information.
Writing your own#
There are a few methods of using your own filesystem implementation in dissect.target
:
Specify the path to your implementation(s) using the
DISSECT_PLUGINS
environment variable.Specify the path to your implementation(s) using the
--plugin-path
argument with the various Dissect Tools.Add a new implementation in the
dissect.target
source tree atdissect/target/filesystems
.
The last method requires you to have a source checkout and working development setup of dissect.target
.
This is the recommended method if you intend to contribute your filesystem back to the project.
See also
Read more about using your own modules in dissect.target
at Loading your own modules.
Interested in developing for Dissect? Read more at Developing for Dissect.
Regardless of which method you use, you can use the boilerplate below. Do keep in mind that the final line is only
required if you’re using either of the DISSECT_PLUGINS
or --plugin-path
options!
from typing import BinaryIO
from dissect.target.filesystem import Filesystem, FilesystemEntry
from dissect.target.helpers import fsutil
class MyFilesystem(Filesystem):
# Specify a unique filesystem identifier
__fstype__ = "myfs"
def __init__(self, fh: BinaryIO, *args, **kwargs):
# Do your initialization here, for example, initialize a parser:
# self.fs = FSParser(fh)
# Call ``super().__init__`` with the original file-like object and arguments
super().__init__(fh, *args, **kwargs)
# You can also do more complex initialization, view the NtfsFilesystem or TarFilesystem for examples
def detect(fh: BinaryIO) -> bool:
# Perform detection for your filesystem from a binary file-like object here
# For example, check a specific magic superblock value
raise NotImplementedError()
def get(self, path: str) -> MyFilesystemEntry:
# Retrieve a FilesystemEntry for the given path from your filesystem.
raise NotImplementedError()
class MyFilesystemEntry(FilesystemEntry):
# This object represents a file on your filesystem
def __init__(self, fs: Filesystem, path: str, entry: FilesystemEntry):
# Perform additional initialization here if you need it
super().__init__(fs, path, entry)
def get(self, path: str) -> MyFilesystemEntry:
# Retrieve a different filesystem entry relative to this one
# In most filesystems it's more efficient to retrieve relative file entries instead of absolute ones
raise NotImplementedError()
def open(self) -> BinaryIO:
# Open a file-like object for this filesystem entry
raise NotImplementedError()
def iterdir(self) -> Iterator[str]:
# Iterate over all directory entries and yield all file names
raise NotImplementedError()
def scandir(self) -> Iterator[MyFilesystemEntry]:
# Iteratie over all directory entries and yield all filesystem entries
raise NotImplementedError()
def is_file(self) -> bool:
# Return whether this filesystem entry is a file
raise NotImplementedError()
def is_dir(self) -> bool:
# Return whether this filesystem entry is a directory
raise NotImplementedError()
def is_symlink(self) -> bool:
# Return whether this filesystem entry is a symbolic link
raise NotImplementedError()
def readlink(self) -> str:
# Return the symbolic link target, if this is a symbolic link
raise NotImplementedError()
def stat(self) -> fsutil.stat_result:
# Return stat information of this filesystem entry
raise NotImplementedError()
def lstat(self) -> fsutil.stat_result:
# Return stat information of this filesystem entry, without resolving symlinks
raise NotImplementedError()
# This line is necessary if your filesystem is outside the ``dissect.target`` source tree!
register(__name__, MyFilesystem.__name__, internal=False)
See also
You can refer to the API documentation of the Filesystem
,
FilesystemEntry
and
fsutil.stat_result
classes for more documentation on the
methods referenced here.
If you are placing your implementation in the dissect.target
source tree, you’ll need to register your implementation.
Do this by opening dissect/target/filesystem.py
and add your implementation to the bottom by using
register()
:
register("myfilesystem", "MyFilesystem")