Files
mvt/docs/development/plugin_configuration.md
T
Donncha Ó Cearbhaill 0b5b3f2d7c Add the mvt.plugin import surface (#901)
* Add the mvt.plugin import surface

mvt.plugin re-exports the names a plugin needs from MVT under one import
path. It holds the module base classes and Command, the alert and result
types, the database errors a module raises, the timestamp converters, the
plugin settings API, MVT's settings, get_plugin_logger() and MVT_VERSION.

The names it exports are kept working on a best-effort basis. Changes to
them are announced in the release notes. Anything else in mvt can still be
imported, and may change between releases without notice.

get_plugin_logger(__name__) returns a logger under mvt.ext for plugin code
outside a module class. Its records then reach the console and the
command.log file of a run. A file loaded with --load-module or
--load-command is named after the file.

* Document how to write MVT plugins

The custom modules page now leads with plugin packages. Loading module
files with --load-module and MVT_CUSTOM_MODULES moves to a section on
developing a module locally.

A new "Writing a module" section shows a module which subclasses
IOSExtraction. It lists each base class, the command pair it serves and the
helpers it provides. "Depending on a built-in module" says to import a
built-in class from its family package.

"Importing from MVT" says what mvt.plugin exports and what importing from
it means. The custom commands page shows a Command subclass which lists its
own modules. The sysdiagnose and plugin configuration pages import from
mvt.plugin.
2026-08-27 14:47:16 +02:00

5.6 KiB

Plugin Configuration

Plugin packages that add custom CLI commands or modules often need to store their own settings, such as an API key, a server URL or the timestamp of the last synchronization. MVT provides a namespaced settings base class so each plugin keeps its configuration in its own file, and a data folder for anything else a plugin needs to keep on disk.

!!! warning

Do not write plugin settings to MVT's own `config.yaml`. MVT rewrites that
file with the settings it knows about every time it starts, so any other
section is deleted.

Where Settings Are Stored

Each plugin gets one YAML file in a plugins folder next to MVT's own configuration:

~/.config/mvt/plugins/<plugin name>.yaml

The exact parent folder follows the platform convention used for MVT's config.yaml (for example ~/Library/Application Support/mvt on macOS). Use plugin_config_path() from mvt.plugin instead of building the path by hand.

Plugin names must be lowercase and may only contain letters, digits and dashes, matching the mvt-plugin-<name> package naming convention. MVT creates the plugins folder with 0700 permissions and writes the settings files with 0600 permissions, because they commonly hold credentials. Files are written through a temporary file and moved into place, so an interrupted save never leaves a partially written settings file behind.

Plugin Data Folder

Everything else a plugin keeps on disk, such as a cache, a downloaded artifact or synchronization state, belongs in the folder returned by the data_folder() class method of the plugin's settings class, or by plugin_data_folder() from mvt.plugin, called with the plugin name if the plugin has no settings class:

~/.local/share/mvt/plugin-data/<plugin name>/                # Linux
~/Library/Application Support/mvt/plugin-data/<plugin name>/ # macOS

The folder sits beside MVT's own data, such as the downloaded indicators. It is created if it is missing, with 0700 permissions. Asking for it again returns the same path and leaves the contents alone, so a plugin can ask for it every time it needs the folder. ExamplePluginSettings below is the settings class defined in the next section:

import os


def cache_path() -> str:
    folder = ExamplePluginSettings.data_folder()
    return os.path.join(folder, "virustotal_lookups_cache.json")

A plugin which has no settings class calls plugin_data_folder("example-plugin") instead.

Do not fall back on a path of your own such as ~/.cache/example-plugin: it is a Linux-only convention, and MVT will not create it for you.

Defining Plugin Settings

Subclass MVTPluginSettings, set plugin_name and declare typed fields with defaults:

from typing import Optional

from mvt.plugin import MVTPluginSettings


class ExamplePluginSettings(MVTPluginSettings):
    plugin_name = "example-plugin"

    API_KEY: Optional[str] = None
    MAX_RESULTS: int = 25
    LAST_SYNC: Optional[str] = None

load() returns the current settings and save() writes them back:

from datetime import datetime, timezone

import click

from mvt.plugin import plugin_env_prefix


def sync():
    settings = ExamplePluginSettings.load()
    if not settings.API_KEY:
        prefix = plugin_env_prefix(settings.plugin_name)
        raise click.ClickException(
            f"No API key configured. Set {prefix}API_KEY or "
            "run 'example-plugin configure'."
        )

    settings.LAST_SYNC = datetime.now(timezone.utc).isoformat()
    settings.save()

The message builds the variable name with plugin_env_prefix(), see Environment Variables.

A missing settings file is not an error: the plugin then runs on the field defaults and on whatever the environment provides. save() only persists the values that differ from the defaults, and it never touches MVT's config.yaml. A settings file that cannot be parsed, or that does not hold a mapping of setting names to values, raises a PluginConfigLoadError naming the file.

Every subclass that sets its own plugin_name gets its own file and its own environment namespace. A subclass that does not redefine plugin_name inherits it, and therefore shares the file and the environment variables of its parent class.

Environment Variables

Every field can also be set with an environment variable. The prefix is MVT_PLUGIN_, followed by the plugin name upper-cased with dashes replaced by underscores, followed by the field name. For the example above:

export MVT_PLUGIN_EXAMPLE_PLUGIN_API_KEY=...
export MVT_PLUGIN_EXAMPLE_PLUGIN_MAX_RESULTS=50

Do not repeat the plugin name in a field name: a plugin named example-scanner with a SCANNER_API_KEY field asks the user for MVT_PLUGIN_EXAMPLE_SCANNER_SCANNER_API_KEY. Name the field API_KEY.

Settings resolve in this order, from highest to lowest priority:

  1. Arguments passed to the settings class directly, such as ExamplePluginSettings(API_KEY="...")
  2. Environment variables
  3. The plugin's YAML file
  4. The field defaults declared on the settings class

!!! tip

On shared or multi-user machines, prefer passing API keys through
environment variables rather than saving them to the plugin file. `save()`
skips every value that the environment currently supplies, so a credential
provided that way is not copied into the settings file when a plugin saves
an unrelated setting.

Unknown keys in a plugin's YAML file are ignored, so a settings file written by a newer version of a plugin does not break an older one.