Plugins#

The functionality that dissect.target provides can be separated into two categories:

  • Interaction with target primitives, such as:

    • Disks

    • Volumes

    • Filesystems

  • Interaction with high level target attributes and artefacts, such as:

    • Basic OS information (hostname, domain, version, users, etc…)

    • OS specific forensic artefacts (Event logs, bash history, registry artefacts, etc…)

    • Generic forensic artefacts (filesystem artefacts, browser history, etc…)

The former is what dissect.target provides as core functionality. High level interaction is provided by a plugin system. This is also how you interact with a target using target-query.

On the technical side, a plugin is a Python class that exports a few of its methods to execute on a target. To learn how to write your own plugin, skip ahead to Writing your own.

Type of plugins#

There are a couple of plugin types that you should know about. The main difference between them is how they can be used.

OS plugins#

OS plugins are very important in dissect.target and OS detection is an important step in the initialisation of a target. OS plugins are the first layer between the target primitives and the rest of the plugins. They are responsible for mounting filesystems to their correct location in the Root filesystem and for performing any additional OS initialisation steps. Additionally, they provide basic OS specific information, such as the hostname, version, and users of a target.

Let’s compare the initialisation of WindowsPlugin and ESXiPlugin. The WindowsPlugin detects the system volume, mounts the system volume to sysvol, and the rest of the volumes to the correct drive letters. There is not much to the initialisation of a Windows target. The ESXiPlugin on the other hand, goes through an elaborate process of rebuilding the ESXi root filesystem from .tar files, parsing configuration, mounting volumes, and creating symlinks according to complex rules. The end result is that from that point onward, any other plugin can interact with an ESXi targets’ filesystem as if it was a live system because it was accurately reconstructed.

Internal plugins#

Internal plugins provide functionality for (surprise!) internal use. This means that they are unavailable through target-query and only callable from within Python.

These plugins generally provide functionality that aids other plugins or makes plugin development easier. For example, consider a plugin that provides environment variable expansion, or one that allows you to calculate UTC timestamps from the local system time zone information.

One very important internal plugin that warrants its own documentation is the Windows registry plugin.

Windows Registry#

The Windows registry is a vital part of the Windows operating system, but also vital to the field of digital forensics. It contains a lot of interesting forensic artefacts, in addition to a lot of important information that is necessary to correctly interpret a Windows target. For example, configured log file locations, which codepage is in use, what time zone is configured, which drive letter belongs to which volume, and much more.

Because the registry is so important, there’s a special internal plugin in dissect.target to make interacting with it a breeze. Among other things, it has support for different sources of registry data, such as:

  • Binary regf hives

  • Hives imported from .reg files

  • Virtual hives

It also supports multiple dimensions of the same hive (active, REGBACK or replayed hives, multiple ControlSet keys). All the hives are mapped in a similar structure as Windows does, meaning registry paths like HKLM\SOFTWARE or HKEY_CURRENT_USER work just as you expect them to.

Let’s look at this last concept in a bit more detail and take HKEY_CURRENT_USER as an example. On a live Windows system, things like HKEY_CURRENT_USER and CurrentControlSet map to a single key. This makes sense, because on a live system you only have one current user, and only one current control set. However, when performing digital forensics, you want as much data as possible. Maybe a setting was changed in an older control set and isn’t visible anymore in the “current” control set? Maybe a persistency method exists in a REGBACK version of a hive but no longer in the active hive? To make working with this concept easier from an analysis and plugin development point of view, we allowed a registry key or value to exist multiple times.

HKEY_CURRENT_USER is really just a little bit of syntactic sugar around this concept. This allows for easy analysis and plugin development, since you don’t have to iterate user hives yourself, and can just use HKCU instead. Underwater it will iterate over all the user hives for you, and simply return all of the keys that exist within those hives. So, a query for HKCU\Software\Microsoft\Windows\CurrentVersion\Run returns all the possible run key entries of all the users that have registry hives.

Let’s assume we have a fully initialised Windows target on variable t. Here are some examples of how you would interact programmatically with the registry of the target:

# Access a single key
t.registry.key("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")

# Access a single subkey
t.registry.subkey("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList", "S-1-5-18")

# Access a single value
t.registry.value("HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion", "CSDVersion")

# Iterate over all possible keys (recommended)
for key in t.registry.keys("HKCU\\Software\\Microsoft\\Office"):
    for subkey in key.subkeys():
        print(subkey.subkeys())

There is also the target-reg utility to easily interact with the registry of a Windows target from the command line, which essentially is a small utility around the registry plugin!

See also

For more information, please refer to the documentation of RegistryPlugin.

Artefact plugins#

Most other plugins in dissect.target are regular plugins, or plugins that parse artefacts. For convenience we’ll call these artefact plugins. These types of plugins are the ones you generally interact with when using target-query and generally parse some piece of data from a target and return some kind of output.

Namespace plugins#

Sometimes it makes sense to “namespace” your plugin. For example, the Windows System Resource Usage (SRU) database has multiple tables that are of interest. We want to make a distinction between the different tables and thus have separate plugin functions for parsing and returning those. However, we also want an easy option to parse all of the SRU tables at once.

This is where namespace plugins are useful. It allows us to nicely separate the different table parsing functions into different plugins such as sru.network_data and sru.application, while simultaneously allowing us to execute all of the namespace functions by simply calling sru.

Caching#

During investigations it might be useful to cache plugin output for performance reasons. dissect.target has some primitive file-based caching to speed up future executions of the same plugin. To configure caching, place a file called .targetcfg.py in the same directory as your target files with the following content:

CACHE_DIR = "/path/to/cache/directory"

Having this file in the same directory as your targets will cause the cache for those targets to be written to the given directory. You can also have different configurations for different targets, as the parent directories are traversed to find .targetcfg.py:

/t/
├── domain_a
│   ├── EXAMPLE02.tar       # Uses the .targetcfg.py from /t/domain_a
│   └── .targetcfg.py
├── domain_b
│   ├── EXAMPLE03.vma       # Uses the .targetcfg.py from /t/domain_b
│   └── .targetcfg.py
├── domain_c
│   └── EXAMPLE04.qcow2     # Uses the .targetcfg.py from /t/
├── EXAMPLE01.E01           # Uses the .targetcfg.py from /t/
└── .targetcfg.py

You can influence caching behaviour with the --no-cache, --only-read-cache and --rewrite-cache arguments in target-query or by setting the IGNORE_CACHE, ONLY_READ_CACHE, or REWRITE_CACHE environment variables to either 0 or 1.

Writing your own#

Writing your own plugin is pretty easy. There are a few methods of using your own plugin in dissect.target:

  • Specify the path to your plugin(s) using the DISSECT_PLUGINS environment variable.

  • Specify the path to your plugin(s) using the --plugin-path argument with the various Dissect Tools.

  • Add a new plugin in the dissect.target source tree at dissect/target/plugins.

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 plugin back to the project.

See also

Read more about using your own modules in dissect.target at Loading your own modules.

Either way, you’ll need to write your plugin. Here’s an example which explains a lot of concepts:

from typing import Iterator

from dissect.target.helpers.descriptor_extensions import (
    RegistryRecordDescriptorExtension,
    UserRecordDescriptorExtension,
)
from dissect.target.helpers.record import (
    TargetRecordDescriptor,
    create_extended_descriptor,
)
from dissect.target.plugin import Plugin, arg, export, internal

ExampleRecordRecord = TargetRecordDescriptor(
    "example/descriptor",
    [
        ("string", "field_a"),
        ("string", "field_b"),
    ],
)

ExampleUserRegistryRecord = create_extended_descriptor(
    [RegistryRecordDescriptorExtension, UserRecordDescriptorExtension]
)(
    "example/registry/user",
    [
        ("datetime", "ts"),
    ],
)


class ExamplePlugin(Plugin):
    """Example plugin.

    This plugin serves as an example for new plugins. Use it as a guideline.

    Docstrings are used in help messages of plugins. Make sure to document
    your plugin and plugin functions. Use Google docstring format:

    https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html

    Plugins can optionally be namespaced by specifying the ``__namespace__``
    class attribute. Namespacing results in your plugin needing to be prefixed
    with this namespace when being called. For example, if your plugin has
    specified ``test`` as namespace and a function called ``example``, you must
    call your plugin with ``test.example``::

        __namespace__ = "test"

    The ``__init__`` takes the target as only argument. Perform additional
    initialization here if necessary::

        def __init__(self, target):
            super().__init__(target)
    """

    # IMPORTANT: Remove these attributes when using this as boilerplate for your own plugin!
    __findable__ = False

    def check_compatible(self) -> None:
        """Perform a compatibility check with the target.

        This function should return ``None`` if the plugin is compatible with
        the current target (``self.target``). For example, check if a certain
        file exists.
        Otherwise it should raise an ``UnsupportedPluginError``.

        Raises:
            UnsupportedPluginError: If the plugin could not be loaded.
        """
        pass

    @export
    @arg("--flag", action="store_true", help="optional example flag")
    def example(self, flag: bool = False) -> str:
        """Example plugin function.

        Docstrings are used in help messages of plugins. Make sure to document
        your plugin and plugin functions. The first line must be a brief one
        sentence description of the plugin function.

        The ``@export`` decorator supports multiple arguments:
            property (bool): Whether this function should act like a property.
                Properties are implicitly cached.
            record (RecordDescriptor): The record descriptor this function yield,
                if any. If dynamic, use :class:`~dissect.target.helpers.record.DynamicDescriptor`.
            output (str): The output type of this function. Can be one of:
                - default: Single return value.
                - record: Yields records. Implicit when record argument is given.
                - yield: Yields printable values.
                - none: No return value.

        Command line arguments can be added using the ``@arg`` decorator. Arguments
        to this decorator are directly forwarded to the ``add_argument`` function
        of `argparse <https://docs.python.org/library/argparse.html>`_.
        Resulting arguments are passed to the function using kwargs.
        The keyword argument name must match the argparse argument name.
        """
        return f"Example plugin. Flag argument: {flag!r}"

    @export(record=ExampleRecordRecord)
    def example_record(self) -> Iterator[ExampleRecordRecord]:
        """Example plugin that generates records.

        To create a new plugin function that yields records, you must define a record descriptor
        and pass it to ``@export``. This will implicitly mark the output type as ``record``.
        """
        yield ExampleRecordRecord(
            field_a="example",
            field_b="record",
            _target=self.target,
        )

    @export(record=ExampleUserRegistryRecord)
    def example_user_registry_record(self) -> Iterator[ExampleUserRegistryRecord]:
        """Example plugin that generates records with registry key and user information.

        To include registry or user information in a record, you must create a new record descriptor using
        :func:`~dissect.target.helpers.record.create_extended_descriptor` with
        :class:`~dissect.target.helpers.descriptor_extensions.RegistryRecordDescriptorExtension` and/or
        :class:`~dissect.target.helpers.descriptor_extensions.UserRecordDescriptorExtension as extensions.
        """
        for key in self.target.registry.keys("HKCU\\SOFTWARE"):
            user = self.target.registry.get_user(key)
            yield ExampleUserRegistryRecord(
                ts=key.ts,
                _key=key,
                _user=user,
                _target=self.target,
            )

    @export(output="yield")
    def example_yield(self) -> Iterator[str]:
        """Example plugin that yields text lines.

        Setting ``output="yield"`` is useful for creating generators of text, such as human-readable timelines.
        """
        for i in range(10):
            yield f"Example line {i}"

    @export(output="none")
    def example_none(self) -> None:
        """Example plugin with no return value.

        Setting ``output="none"`` means you don't return a value. This is useful when you want to print something
        on your own, such as verbose information.
        """
        print("Example output with no return value.")

    @internal
    def example_internal(self) -> str:
        """Example internal plugin.

        Use the ``@internal`` plugin to mark your plugin as internal and hide it from the plugin overview.
        """
        return "Example internal plugin."

Output types#

Plugins can have a couple types of outputs:

  • default

    • Basic Python types such as int, str, list, etc. Mostly useful for simple or internal plugins.

  • record

    • Records are the recommended output for most artefact plugins.

  • yield

    • Yield basic Python types, such as a generator of str of a human-readable filesystem timeline.

  • none

    • No output or returns None, useful when the plugin prints something on its own (such as the plugin that lists which plugins are available).

Records#

Records are the main data format used by plugins to output information. If you don’t yet know what records are, read a short explanation in the overview. The important thing to know in the context of writing your own plugin is that you need to write a record descriptor that describes what fields your record has.

The following field types are available:

  • boolean

  • bytes

  • datetime

  • digest

  • dynamic

  • filesize

  • float

  • net.ipaddress

  • net.ipnetwork

  • record

  • string

  • uint16

  • uint32

  • unix_file_mode

  • uri

  • varint

You can also create a list of any of these types by specifying the type as type[], so for example string[] for a list of strings.

Within dissect.target, there are a couple of helpers available for working with records. Most notable is the TargetRecordDescriptor.

Historically, we’d manually add a couple of common fields to each record, such as the hostname or domain of a target. Sometimes errors or typos snuck in, so we standardized this by introducing the TargetRecordDescriptor. You can already see its usage in the example plugin above, but the basic idea is that instead of manually passing the hostname and domain, you pass a target as _target=target keyword argument, and the TargetRecordDescriptor will take care of the rest.

There are two additional so-called record extensions: RegistryRecordDescriptorExtension and UserRecordDescriptorExtension. The use of these is also demonstrated in the example plugin above.