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.
13 KiB
Development
The Mobile Verification Toolkit team welcomes contributions of new forensic modules or other contributions which help improve the software.
Local environment
MVT uses uv for dependency management. To install the project and development dependencies from the locked environment, run:
make install
Testing
MVT uses pytest for unit and integration tests. Code style consistency is maintained with ruff and mypy. All can
be run automatically with:
make check
Run these tests before making new commits or opening pull requests.
Module dependencies
Modules can require other modules to run first by declaring their classes in
dependencies. The command runner uses a stable topological ordering, so the
existing module list order is preserved wherever dependency constraints allow.
class DependentModule(MVTModule):
dependencies = (PrerequisiteModule,)
def run(self):
prerequisite_results = self.get_dependency_results(PrerequisiteModule)
Selecting a single module also runs its transitive dependencies.
A module can only depend on modules the command it runs in also has. When a
declared dependency is not among them, the command logs a warning naming the
module and the missing dependency, skips that module and everything depending
on it, and runs the rest of the analysis. Selecting such a module with
--module therefore leaves nothing to run, which the warning explains.
A cycle in the dependency graph is a programming error rather than a configuration problem: the command logs a warning and runs no modules at all.
Custom modules
MVT's module-running check-* commands can run forensic modules which are not
part of MVT. Custom modules are distributed as plugin packages. A Python
package installed next to MVT registers its modules through an entry point.
The modules then load automatically in every command they support, see
Installed module packages. mvt plugins list
shows the installed packages and where each one was installed from.
MVT can also load module files by path, with --load-module and
MVT_CUSTOM_MODULES, see
Developing modules locally. This can be used
while writing a module. Use a Python package to distribute one.
A custom module declares in supported_commands the platform and command
pairs it runs in. A module with empty supported_commands does not run and
MVT logs a warning. The nine pairs are:
("ios", "check-backup")
("ios", "check-fs")
("ios", "check-iocs")
("ios", "check-sysdiagnose")
("android", "check-backup")
("android", "check-bugreport")
("android", "check-androidqf")
("android", "check-intrusion-logs")
("android", "check-iocs")
check-iocs re-checks stored results rather than an acquisition. It matches
every <slug>.json file in the results folder to the module with that slug.
It then runs that module's check_indicators() again.
Writing a module
A module subclasses MVTModule or one of the base classes below and
implements run(). check_indicators() and serialize() are optional. The
first matches results against IOCs or detections. The second returns timeline
records.
from mvt.plugin import IOSExtraction, convert_unix_to_iso
class ExampleCustomModule(IOSExtraction):
supported_commands = (
("ios", "check-backup"),
("ios", "check-fs"),
)
slug = "example_custom_module"
def run(self):
self.results = [{"checked_at": convert_unix_to_iso(0)}]
def check_indicators(self):
pass
def serialize(self, result):
return None
The base classes are:
MVTModule: the base of every module. It providesself.results,self.alertstore,self.log,self.indicatorsandget_dependency_results(). Subclass it directly for a module which reads only the results of other modules.IOSExtraction:("ios", "check-backup")and("ios", "check-fs"). Adds_find_ios_database(), which locates a module's database in a backup or in a filesystem dump and repairs it if it is malformed. Adds_get_backup_files_from_manifest(),_get_backup_file_from_id()and_get_fs_files_from_patterns(). Adds_open_sqlite_db(), which opens a database read-only.SysdiagnoseExtraction:("ios", "check-sysdiagnose"). MVT extracts the archive and callsfrom_sysdiagnose_folder()beforerun(). The module reads files with_get_files_by_pattern()and_get_file_content().ips_fileslists the crash reports. See Check an iOS Sysdiagnose.AndroidQFModule:("android", "check-androidqf"). MVT callsfrom_dir()orfrom_zip()with the file list of the acquisition. The module reads files with_get_files_by_pattern()and_get_file_content()._get_device_timezone()returns the device timezone.AndroidBackupModule:("android", "check-backup"). MVT callsfrom_dir()orfrom_ab(). The module reads files with_get_files_by_pattern()and_get_file_content().BugReportModule:("android", "check-bugreport"). MVT callsfrom_dir()orfrom_zip(). The module reads files with_get_files_by_pattern(),_get_files_by_patterns()and_get_file_content()._get_dumpstate_file()returns the dumpstate file, and_get_file_modification_time()the modification time of a file.
The underscore-named helpers are internal to the base classes. Plugin modules
can call them. Their names and signatures can change between releases. Read
the base class in src/mvt/ios/modules or src/mvt/android/modules before
relying on one.
Depending on a built-in module
A module which post-processes records generated by one or more built-in MVT
modules must declare the source modules in dependencies. It reads their
results with get_dependency_results(). Import the class from its family
package: mvt.ios.modules.backup, mvt.ios.modules.fs,
mvt.ios.modules.mixed, mvt.android.modules.androidqf,
mvt.android.modules.backup, mvt.android.modules.bugreport or
mvt.android.modules.intrusion_logs.
from mvt.ios.modules.backup import Manifest
from mvt.plugin import MVTModule
class DependentCustomModule(MVTModule):
supported_commands = (("ios", "check-backup"),)
dependencies = (Manifest,)
def run(self):
manifest_results = self.get_dependency_results(Manifest)
self.results = [{"manifest_entries": len(manifest_results)}]
Dependencies are ordered as for the built-in modules, with custom modules appended after the built-ins, see Module dependencies. A dependency has to run in every command the module supports. Where it does not, MVT skips the module with a warning.
get_dependency_results() returns the plain dictionaries the module produced.
They are the same records it writes to <slug>.json. Typed results per module
are planned.
Importing from MVT
Import from mvt.plugin if it has what you need. The names it exports are
kept working on a best-effort basis. A change to one of them is announced in
the release notes. Anything else in mvt can be imported too, but may change
between releases without notice. The plugin interface is best effort.
mvt.plugin exports the base classes above and Command, Alert and
AlertLevel, the result types, DatabaseNotFoundError and
DatabaseCorruptedError, the timestamp converters, the settings API of
Plugin Configuration, MVT's own settings,
get_plugin_logger() and MVT_VERSION. src/mvt/plugin.py holds the list.
Read MVT's settings for values such as NETWORK_ACCESS_ALLOWED and
NETWORK_TIMEOUT. Plugin values go in the plugin's own settings file.
Installed module packages
Python packages can register modules so they load automatically in every
module-running check-* command, without --load-module or
MVT_CUSTOM_MODULES. Register an entry point in the mvt.modules group in
the package's pyproject.toml:
[project.entry-points."mvt.modules"]
mvt-plugin-example-org = "mvt_plugin_example_org:get_modules"
The entry point must resolve to an iterable of MVTModule subclasses, or to
a callable returning one:
from mvt.plugin import MVTModule
class PackagedModule(MVTModule):
supported_commands = (("ios", "check-backup"),)
def run(self):
self.results = [{"message": "packaged module ran"}]
def get_modules() -> list[type[MVTModule]]:
return [PackagedModule]
get_modules() is the package's module list, written by hand. A package which
keeps its modules in separate files imports each class there and lists it. A
module missing from the list does not load.
Installed modules follow the same rules as other custom modules: each module
must declare supported_commands, and dependencies are resolved with the
standard ordering logic. A broken entry point is skipped with a warning and
does not prevent MVT from running. As with custom commands, installed module
packages run as trusted code inside the MVT process, so install only packages
from sources you trust.
For a pipx installation of MVT, inject the package into MVT's environment:
pipx inject mvt mvt-plugin-example-org
Module packages that need their own settings, such as an API key, should store
them in a namespaced plugin configuration file
rather than in MVT's own config.yaml.
Naming module packages
Name module packages mvt-plugin-<name> (import package mvt_plugin_<name>),
and include the name of the publishing organization or author so packages from
different groups do not collide: for example, an organisation's custom modules
would be distributed as mvt-plugin-example-org with the import package
mvt_plugin_example_org.
The prefix makes module packages easy to find on PyPI and keeps their import
names from clashing with unrelated Python packages. It is a convention, not a
technical requirement: modules load through the mvt.modules entry point
regardless of what the package is called, and existing packages with other
names keep working. Note that the prefix is also not a mark of authenticity —
anyone can publish a package with any available name, so vet a module package
and its publisher before installing it, whatever it is called.
Module logging
Modules log through self.log, and MVT names the logger for where the module
came from. MVT's own modules log under their dotted path (for example
mvt.ios.modules.mixed.whatsapp). Everything external is namespaced under
mvt.ext to keep it visually distinct from built-in modules and isolated from
MVT's internal logger tree:
- Installed packages log under
mvt.ext.<package>, with themvt_plugin_prefix stripped: modules inmvt_plugin_example_orglog asmvt.ext.example_org.*. - Files loaded with
--load-moduleorMVT_CUSTOM_MODULESlog asmvt.ext.<file name>.
Outside a module class, for example in a custom command line handler, log
through get_plugin_logger(__name__). It returns a logger in the same
namespace.
Developing modules locally
While a module is being written, load it from its file. --load-module takes a
Python file, or a folder of them, on every module-running command, and can be
repeated:
mvt-ios check-backup --load-module ./example_module.py --output ./out ./backup
For a folder, MVT loads its non-hidden top-level *.py files in sorted order
and skips __init__.py. MVT_CUSTOM_MODULES names a folder to load on every
module-running command, before any --load-module path:
MVT_CUSTOM_MODULES=./custom_modules mvt-android check-bugreport ./bugreport.zip
Files loaded this way follow the same rules as packaged modules.
--list-modules reports them with the SHA-256 hash of the file in place of a
version. An editable install of the package (pip install -e .) also works:
the modules load through the entry point, and mvt plugins list shows the
package with the local origin.
Loading by path is for development. Move a module into a package once it works.
Auditing loaded modules
Because installed module packages load automatically, MVT records where every module came from:
--list-modulesgroups the available modules by source: MVT itself (with its version), each installed package (with its version and, when installed directly from a repository, the commit), and each file loaded with--load-moduleorMVT_CUSTOM_MODULES(with the SHA-256 hash of the file).- When a command runs with an
--outputfolder, thecommand.logfile records one line per module source with the source's version or hash and the list of modules loaded from it. mvt plugins listlists the installed packages, where each of them was installed from and how many modules it contributes, see Managing Plugins.
Profiling
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be take to avoid inefficient code paths as we add new modules.
MVT modules can be profiled with Python built-in cProfile by setting the MVT_PROFILE environment variable.
MVT_PROFILE=1 dev/mvt-ios check-backup test_backup
Open an issue or PR if you are encountering significant performance issues when analyzing a device with MVT.