Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve exported plugin docstrings and annotations #725

Merged
merged 24 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion dissect/target/filesystems/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class ConfigurationEntry(FilesystemEntry):
Behaves like a ``directory`` when :attr:`parser_items` is a :class:`.ConfigurationParser` or a ``dict``.
Behaves like a ``file`` otherwise.

Attributes:
Args:
parser_items: A dict-like object containing all configuration entries and values.
In most cases this is either a :class:`.ConfigurationParser` or ``dict``.
Otherwise, its the entry's value
Expand Down
10 changes: 5 additions & 5 deletions dissect/target/helpers/compat/path_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ class _DissectScandirIterator:

The _DissectScandirIterator provides a context manager, so scandir can be called as:

```
with scandir(path) as it:
for entry in it
print(entry.name)
```
.. code-block:: python

with scandir(path) as it:
for entry in it
print(entry.name)

similar to os.scandir() behaviour since Python 3.6.
"""
Expand Down
60 changes: 31 additions & 29 deletions dissect/target/helpers/configutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ def _update_dictionary(current: dict[str, Any], key: str, value: Any) -> None:


class PeekableIterator:
"""Source gotten from:
https://more-itertools.readthedocs.io/en/stable/_modules/more_itertools/more.html#peekable
"""
# https://more-itertools.readthedocs.io/en/stable/_modules/more_itertools/more.html#peekable

def __init__(self, iterable):
self._iterator = iter(iterable)
Expand All @@ -98,9 +96,6 @@ def peek(self):
class ConfigurationParser:
"""A configuration parser where you can configure certain aspects of the parsing mechanism.

Attributes:
parsed_data: The resulting dictionary after parsing.

Args:
collapse: A ``bool`` or an ``Iterator``:
If ``True``: it will collapse all the resulting dictionary values.
Expand Down Expand Up @@ -195,6 +190,8 @@ class Default(ConfigurationParser):

This parser splits only on the first ``separator`` it finds:

.. code-block::

key<separator>value -> {"key": "value"}

key<separator>value\n
Expand Down Expand Up @@ -316,7 +313,7 @@ def parse_file(self, fh: io.BytesIO) -> None:


class Xml(ConfigurationParser):
"""Parses an XML file. Ignores any constructor parameters passed from ``ConfigurationParser`."""
"""Parses an XML file. Ignores any constructor parameters passed from ``ConfigurationParser``."""

def _tree(self, tree: ElementTree, root: bool = False) -> dict:
"""Very simple but robust xml -> dict implementation, see comments."""
Expand Down Expand Up @@ -395,8 +392,9 @@ class ListUnwrapper:
def unwrap(data: Union[dict, list]) -> Union[dict, list]:
"""Transforms a list with dictionaries to a dictionary.

The order of the list is preserved. If no dictionary is found,
the list remains untouched:
The order of the list is preserved. If no dictionary is found, the list remains untouched:

.. code-block::

["value1", "value2"] -> ["value1", "value2"]

Expand Down Expand Up @@ -622,6 +620,8 @@ class Indentation(Default):

The parser parses this as the following:

.. code-block::

key value
key2 value2
-> {"key value": {"key2": "value2"}}
Expand All @@ -644,7 +644,7 @@ def _change_scope(
Args:
manager: A :class:`ScopeManager` that contains the logic to ``push`` and ``pop`` scopes. And keeps state.
line: The line to be parsed.
key: The key that should be updated during a :method:`ScopeManager.push``.
key: The key that should be updated during a :method:`ScopeManager.push`.
next_line: The next line to be parsed.

Returns:
Expand Down Expand Up @@ -694,26 +694,28 @@ class SystemD(Indentation):
"""A :class:`ConfigurationParser` that specifically parses systemd configuration files.

Examples:
>>> systemd_data = textwrap.dedent(
'''
[Section1]
Key=Value
[Section2]
Key2=Value 2\\
Value 2 continued
'''
)
>>> parser = SystemD(io.StringIO(systemd_data))
>>> parser.parser_items
{
"Section1": {
"Key": "Value
},
"Section2": {
"Key2": "Value2 Value 2 continued
}
}

.. code-block::

>>> systemd_data = textwrap.dedent(
'''
[Section1]
Key=Value
[Section2]
Key2=Value 2\\
Value 2 continued
'''
)
>>> parser = SystemD(io.StringIO(systemd_data))
>>> parser.parser_items
{
"Section1": {
"Key": "Value
},
"Section2": {
"Key2": "Value2 Value 2 continued
}
}
"""

def _change_scope(
Expand Down
2 changes: 2 additions & 0 deletions dissect/target/helpers/cyber.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@


class Color(Enum):
"""Cyber colors."""

BLACK = 30
RED = 31
GREEN = 32
Expand Down
2 changes: 1 addition & 1 deletion dissect/target/helpers/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_real_func_obj(func: Callable) -> Tuple[Type, Callable]:
return (klass, func)


def get_docstring(obj: Any, placeholder=NO_DOCS) -> str:
def get_docstring(obj: Any, placeholder: str = NO_DOCS) -> str:
"""Get object's docstring or a placeholder if no docstring found"""
# Use of `inspect.cleandoc()` is preferred to `textwrap.dedent()` here
# because many multi-line docstrings in the codebase
Expand Down
2 changes: 2 additions & 0 deletions dissect/target/helpers/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@


class KeyType(Enum):
"""Valid key types."""

RAW = "raw"
PASSPHRASE = "passphrase"
RECOVERY_KEY = "recovery_key"
Expand Down
3 changes: 2 additions & 1 deletion dissect/target/helpers/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from dissect.target.filesystem import Filesystem, FilesystemEntry

HAS_FUSE3 = False
if feature_enabled(Feature.BETA):
from fuse3 import FuseOSError, Operations
from fuse3.c_fuse import fuse_config_p, fuse_conn_info_p
Expand All @@ -20,6 +19,8 @@
fuse_config_p = c_void_p
fuse_conn_info_p = c_void_p

HAS_FUSE3 = False

Check warning on line 22 in dissect/target/helpers/mount.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/helpers/mount.py#L22

Added line #L22 was not covered by tests


log = logging.getLogger(__name__)

Expand Down
28 changes: 15 additions & 13 deletions dissect/target/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Dissect plugin system.

See dissect/target/plugins/general/example.py for an example plugin.
See ``dissect/target/plugins/general/example.py`` for an example plugin.
"""

from __future__ import annotations
Expand Down Expand Up @@ -76,18 +76,19 @@
Supported keyword arguments:
property (bool): Whether this export should be regarded as a property.
Properties are implicitly cached.

cache (bool): Whether the result of this function should be cached.

record (RecordDescriptor): The :class:`flow.record.RecordDescriptor` for the records that this function yields.
If the records are dynamically made, use DynamicRecord instead.
output (str): The output type of this function. Can be one of:

output (str): The output type of this function. Must be one of:
- default: Single return value
- record: Yields records. Implicit when record argument is given.
- yield: Yields printable values.
- none: No return value. Plugin is responsible for output formatting and should return ``None``.

The ``export`` decorator adds some additional private attributes to an exported method or property:

- ``__output__``: The output type to expect for this function, this is the same as ``output``.
- ``__record__``: The type of record to expect, this value is the same as ``record``.
- ``__exported__``: set to ``True`` to indicate the method or property is exported.
Expand Down Expand Up @@ -173,7 +174,7 @@
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``::
call your plugin with ``test.example``.

A ``Plugin`` class has the following private class attributes:

Expand Down Expand Up @@ -430,15 +431,13 @@
"""Register a plugin, and put related data inside :attr:`PLUGINS`.

This function uses the following private attributes that are set using decorators:

- ``__exported__``: Set in :func:`export`.
- ``__internal__``: Set in :func:`internal`.
- ``__exported__``: Set in :func:`export`.
- ``__internal__``: Set in :func:`internal`.

Additionally, ``register`` sets the following private attributes on the `plugincls`:

- ``__plugin__``: Always set to ``True``.
- ``__functions__``: A list of all the methods and properties that are ``__internal__`` or ``__exported__``.
- ``__exports__``: A list of all the methods or properties that were explicitly exported.
- ``__plugin__``: Always set to ``True``.
- ``__functions__``: A list of all the methods and properties that are ``__internal__`` or ``__exported__``.
- ``__exports__``: A list of all the methods or properties that were explicitly exported.

Args:
plugincls: A plugin class to register.
Expand Down Expand Up @@ -1138,6 +1137,9 @@
method_name: str
plugin_desc: PluginDescriptor = field(hash=False)

def __repr__(self) -> str:
return self.path

Check warning on line 1141 in dissect/target/plugin.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugin.py#L1141

Added line #L1141 was not covered by tests


def plugin_function_index(target: Optional[Target]) -> tuple[dict[str, PluginDescriptor], set[str]]:
"""Returns an index-list for plugins.
Expand Down Expand Up @@ -1292,7 +1294,7 @@
path=index_name,
class_object=loaded_plugin_object,
method_name=method_name,
output_type=getattr(fobject, "__output__", "text"),
output_type=getattr(fobject, "__output__", "none"),
plugin_desc=func,
)
)
Expand Down Expand Up @@ -1337,7 +1339,7 @@
path=f"{description['module']}.{funcname}",
class_object=loaded_plugin_object,
method_name=funcname,
output_type=getattr(fobject, "__output__", "text"),
output_type=getattr(fobject, "__output__", "none"),
plugin_desc=description,
)
)
Expand Down
2 changes: 2 additions & 0 deletions dissect/target/plugins/apps/av/mcafee.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@


class McAfeePlugin(Plugin):
"""McAfee antivirus plugin."""

__namespace__ = "mcafee"

DIRS = [
Expand Down
2 changes: 2 additions & 0 deletions dissect/target/plugins/apps/av/sophos.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@


class SophosPlugin(Plugin):
"""Sophos antivirus plugin."""

__namespace__ = "sophos"

LOG_SOPHOS_HOME = "sysvol/ProgramData/Sophos/Clean/Logs/Clean.log"
Expand Down
2 changes: 2 additions & 0 deletions dissect/target/plugins/apps/av/trendmicro.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@


class TrendMicroPlugin(Plugin):
"""TrendMicro antivirus plugin."""

__namespace__ = "trendmicro"

LOG_FOLDER = "sysvol/Program Files (x86)/Trend Micro/Security Agent"
Expand Down
33 changes: 27 additions & 6 deletions dissect/target/plugins/apps/browser/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,15 @@ def remove_padding(decrypted: bytes) -> bytes:


def decrypt_v10(encrypted_password: bytes) -> str:
"""Decrypt a version 10 encrypted password.

Args:
encrypted_password: The encrypted password bytes.

Returns:
Decrypted password string.
"""

if not HAS_CRYPTO:
raise ValueError("Missing pycryptodome dependency for AES operation")

Expand All @@ -625,12 +634,24 @@ def decrypt_v10(encrypted_password: bytes) -> str:


def decrypt_v10_2(encrypted_password: bytes, key: bytes) -> str:
"""
struct chrome_pass {
byte signature[3] = 'v10';
byte iv[12];
byte ciphertext[EOF];
}
"""Decrypt a version 10 type 2 password.

JSCU-CNI marked this conversation as resolved.
Show resolved Hide resolved
References:

.. code-block::

struct chrome_pass {
byte signature[3] = 'v10';
byte iv[12];
byte ciphertext[EOF];
}

Args:
encrypted_password: The encrypted password bytes.
key: The encryption key.

Returns:
Decrypted password string.
"""

if not HAS_CRYPTO:
Expand Down
8 changes: 6 additions & 2 deletions dissect/target/plugins/apps/shell/powershell.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Iterator

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
from dissect.target.helpers.record import create_extended_descriptor
Expand All @@ -14,6 +16,8 @@


class PowerShellHistoryPlugin(Plugin):
"""Windows PowerShell history plugin."""

PATHS = [
"AppData/Roaming/Microsoft/Windows/PowerShell/psreadline",
".local/share/powershell/PSReadLine",
Expand All @@ -35,10 +39,10 @@ def check_compatible(self) -> None:
raise UnsupportedPluginError("No ConsoleHost_history.txt files found")

@export(record=ConsoleHostHistoryRecord)
def powershell_history(self):
def powershell_history(self) -> Iterator[ConsoleHostHistoryRecord]:
"""Return PowerShell command history for all users.

The PowerShell ConsoleHost_history.txt file contains information about the commands executed with PowerShell in
The PowerShell ``ConsoleHost_history.txt`` file contains information about the commands executed with PowerShell in
a terminal. No data is recorded from terminal-less PowerShell sessions. Commands are saved to disk after the process has completed.
PSReadLine does not save commands containing 'password', 'asplaintext', 'token', 'apikey' or 'secret'.

Expand Down
2 changes: 1 addition & 1 deletion dissect/target/plugins/apps/shell/wget.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def hsts(self) -> Iterator[WgetHstsRecord]:
- https://gitlab.com/gnuwget/wget/-/blob/master/src/hsts.c
- https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security

Yields ``WgetHstsRecord``s with the following fields:
Yields ``WgetHstsRecord`` records with the following fields:

.. code-block:: text

Expand Down
2 changes: 2 additions & 0 deletions dissect/target/plugins/apps/ssh/openssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def find_sshd_directory(target: Target) -> TargetPath:


class OpenSSHPlugin(SSHPlugin):
"""OpenSSH plugin."""

__namespace__ = "openssh"

SSHD_DIRECTORIES = ["/sysvol/ProgramData/ssh", "/etc/ssh"]
Expand Down
Loading
Loading