Skip to content

Commit

Permalink
Improve exported plugin docstrings and annotations (#725)
Browse files Browse the repository at this point in the history
* add style guide conformity tests for exported plugins
* fix docstrings and annotations for various plugins
  • Loading branch information
JSCU-CNI authored Oct 24, 2024
1 parent 3f93c40 commit 34ce6a7
Show file tree
Hide file tree
Showing 118 changed files with 620 additions and 330 deletions.
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


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 @@ def export(*args, **kwargs) -> Callable:
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 Plugin:
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 @@ def register(plugincls: Type[Plugin]) -> None:
"""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 @@ class PluginFunction:
method_name: str
plugin_desc: PluginDescriptor = field(hash=False)

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


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 @@ def find_plugin_functions(
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 @@ def find_plugin_functions(
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.
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

0 comments on commit 34ce6a7

Please sign in to comment.