mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-25 15:43:41 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cf82f44e9 | ||
|
|
da33c2fdb3 | ||
|
|
1b163749b2 | ||
|
|
70efefde22 | ||
|
|
b3585e76ad | ||
|
|
1e45ead1a5 |
No files matched your search
@@ -60,6 +60,8 @@ For alternative installation options and known issues, please refer to the [docu
|
|||||||
|
|
||||||
MVT provides three commands: `mvt-ios` and `mvt-android` analyse acquisitions from devices of that platform, and `mvt` hosts what belongs to neither: `version`, `completion`, `plugins` and `download-iocs` (`version` and `download-iocs` remain available on the platform commands for now). Running `mvt` on its own shows the installed version, update notices and the available commands. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
|
MVT provides three commands: `mvt-ios` and `mvt-android` analyse acquisitions from devices of that platform, and `mvt` hosts what belongs to neither: `version`, `completion`, `plugins` and `download-iocs` (`version` and `download-iocs` remain available on the platform commands for now). Running `mvt` on its own shows the installed version, update notices and the available commands. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
|
||||||
|
|
||||||
|
Pass `--verbose` to any of the three commands, before the command name (`mvt-ios --verbose check-backup ...`), for debug output. The `--verbose` option the `check-*` commands accept after their name still works but is kept for compatibility only and will be removed in a future release.
|
||||||
|
|
||||||
### Shell completion
|
### Shell completion
|
||||||
|
|
||||||
MVT can generate a shell completion script for Bash, Zsh, and Fish which covers `mvt`, `mvt-ios` and `mvt-android`:
|
MVT can generate a shell completion script for Bash, Zsh, and Fish which covers `mvt`, `mvt-ios` and `mvt-android`:
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ mvt-android check-intrusion-logs --output /path/to/results/ /path/to/intrusion-l
|
|||||||
| `-l, --list-modules` | List the available intrusion-log modules and exit. |
|
| `-l, --list-modules` | List the available intrusion-log modules and exit. |
|
||||||
| `-m, --module NAME` | Run a single module (e.g. `DnsEvent`) instead of all of them. |
|
| `-m, --module NAME` | Run a single module (e.g. `DnsEvent`) instead of all of them. |
|
||||||
| `-t, --timezone TZ` | IANA timezone name for the device (e.g. `Europe/Paris`). When set, event timestamps are converted to the device's local time instead of UTC. |
|
| `-t, --timezone TZ` | IANA timezone name for the device (e.g. `Europe/Paris`). When set, event timestamps are converted to the device's local time instead of UTC. |
|
||||||
| `-v, --verbose` | Verbose logging. |
|
| `-v, --verbose` | Verbose logging. Kept for compatibility and to be removed in a future release: pass `--verbose` to `mvt-android` itself instead. |
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ def summarize(path):
|
|||||||
click.echo(f"Summarizing {path}")
|
click.echo(f"Summarizing {path}")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Log through `get_plugin_logger(__name__)` from `mvt.plugin`. Records logged
|
||||||
|
through `logging.getLogger(__name__)` do not reach `command.log`, and MVT's
|
||||||
|
console handler does not show them.
|
||||||
|
|
||||||
Register the object in the package's `pyproject.toml`. The entry-point name is
|
Register the object in the package's `pyproject.toml`. The entry-point name is
|
||||||
the command users invoke:
|
the command users invoke:
|
||||||
|
|
||||||
@@ -125,6 +129,64 @@ export MVT_IOS_CUSTOM_COMMANDS=./ios_commands
|
|||||||
export MVT_ANDROID_CUSTOM_COMMANDS=./android_commands
|
export MVT_ANDROID_CUSTOM_COMMANDS=./android_commands
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building a Module-Running Command
|
||||||
|
|
||||||
|
A command which runs forensic modules over an acquisition subclasses `Command`.
|
||||||
|
`Command` creates the output folder and writes `command.log`. It orders the
|
||||||
|
modules, resolves their dependencies and runs them. It writes the result files,
|
||||||
|
`alerts.json` and `info.json`. The subclass sets `platform`, `name` and
|
||||||
|
`modules`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mvt.plugin import Command, MVTModule, convert_unix_to_iso, get_plugin_logger
|
||||||
|
|
||||||
|
log = get_plugin_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APKManifest(MVTModule):
|
||||||
|
supported_commands = (("android", "check-apks"),)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.results = [{"checked_at": convert_unix_to_iso(0)}]
|
||||||
|
|
||||||
|
|
||||||
|
class CmdCheckAPKs(Command):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.platform = "android"
|
||||||
|
self.name = "check-apks"
|
||||||
|
self.modules = [APKManifest]
|
||||||
|
```
|
||||||
|
|
||||||
|
`platform` and `name` are the pair the modules declare in
|
||||||
|
`supported_commands`. `modules` lists the module classes the command runs,
|
||||||
|
imported directly. A command which runs modules of another plugin depends on
|
||||||
|
that package in `pyproject.toml` and imports them the same way. `init()`,
|
||||||
|
`module_init(module)` and `finish()` are optional hooks. `run()` calls them
|
||||||
|
before the run, before each module and after the run.
|
||||||
|
|
||||||
|
Wrap the command in a Click command with an `--output` option. Run it, then
|
||||||
|
print the alert summary:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("check-apks")
|
||||||
|
@click.option("--output", "-o", type=click.Path(exists=False))
|
||||||
|
@click.argument("TARGET_PATH", type=click.Path(exists=True))
|
||||||
|
def cli(output, target_path):
|
||||||
|
cmd = CmdCheckAPKs(target_path=target_path, results_path=output)
|
||||||
|
log.info("Checking APK files at path: %s", target_path)
|
||||||
|
cmd.run()
|
||||||
|
cmd.show_alerts_brief()
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--verbose` option of `mvt`, `mvt-ios` and `mvt-android` applies to the
|
||||||
|
command. The command defines no `--verbose` option of its own. The pair a
|
||||||
|
plugin command adds is not listed anywhere in MVT. Name it in the plugin's
|
||||||
|
README.
|
||||||
|
|
||||||
## Naming and Errors
|
## Naming and Errors
|
||||||
|
|
||||||
Built-in MVT commands cannot be replaced. External command names must also be
|
Built-in MVT commands cannot be replaced. External command names must also be
|
||||||
|
|||||||
+158
-56
@@ -48,51 +48,21 @@ configuration problem: the command logs a warning and runs no modules at all.
|
|||||||
|
|
||||||
## Custom modules
|
## Custom modules
|
||||||
|
|
||||||
Module-running `check-*` commands can load custom modules from Python files that
|
MVT's module-running `check-*` commands can run forensic modules which are not
|
||||||
are not installed as part of MVT. Load one file with:
|
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](#installed-module-packages). `mvt plugins list`
|
||||||
|
shows the installed packages and where each one was installed from.
|
||||||
|
|
||||||
```bash
|
MVT can also load module files by path, with `--load-module` and
|
||||||
mvt-ios check-backup --load-module ./example_module.py --output ./out ./backup
|
`MVT_CUSTOM_MODULES`, see
|
||||||
```
|
[Developing modules locally](#developing-modules-locally). This can be used
|
||||||
|
while writing a module. Use a Python package to distribute one.
|
||||||
|
|
||||||
You can also load a folder. MVT loads non-hidden top-level `*.py` files in
|
A custom module declares in `supported_commands` the platform and command
|
||||||
sorted order and skips `__init__.py`:
|
pairs it runs in. A module with empty `supported_commands` does not run and
|
||||||
|
MVT logs a warning. The nine pairs are:
|
||||||
```bash
|
|
||||||
mvt-ios check-fs --load-module ./custom_modules ./filesystem-dump
|
|
||||||
```
|
|
||||||
|
|
||||||
Set `MVT_CUSTOM_MODULES` to load a folder for every module-running command. This
|
|
||||||
folder is loaded before any `--load-module` path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MVT_CUSTOM_MODULES=./custom_modules mvt-android check-bugreport ./bugreport.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
Custom modules are normal `MVTModule` subclasses:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from mvt.common.module import MVTModule
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleCustomModule(MVTModule):
|
|
||||||
supported_commands = (("ios", "check-backup"), ("ios", "check-fs"))
|
|
||||||
slug = "example_custom_module"
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.results = [{"message": "custom module ran"}]
|
|
||||||
|
|
||||||
def check_indicators(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def serialize(self, result):
|
|
||||||
return None
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `supported_commands` to declare the platform/command pairs a module
|
|
||||||
supports. Empty `supported_commands` means the module will not run and MVT logs
|
|
||||||
a warning. This explicit declaration is required for every command. Supported
|
|
||||||
pairs are:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
("ios", "check-backup")
|
("ios", "check-backup")
|
||||||
@@ -106,13 +76,86 @@ pairs are:
|
|||||||
("android", "check-iocs")
|
("android", "check-iocs")
|
||||||
```
|
```
|
||||||
|
|
||||||
Custom modules can depend on existing MVT module classes. Dependencies are
|
`check-iocs` re-checks stored results rather than an acquisition. It matches
|
||||||
resolved with the same ordering logic as built-in modules, and custom modules
|
every `<slug>.json` file in the results folder to the module with that slug.
|
||||||
are appended after built-ins before ordering:
|
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.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mvt.common.module import MVTModule
|
from mvt.plugin import IOSExtraction, convert_unix_to_iso
|
||||||
from mvt.ios.modules.backup.manifest import Manifest
|
|
||||||
|
|
||||||
|
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 provides `self.results`,
|
||||||
|
`self.alertstore`, `self.log`, `self.indicators` and
|
||||||
|
`get_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 calls `from_sysdiagnose_folder()` before `run()`. The module
|
||||||
|
reads files with `_get_files_by_pattern()` and `_get_file_content()`.
|
||||||
|
`ips_files` lists the crash reports. See
|
||||||
|
[Check an iOS Sysdiagnose](../ios/sysdiagnose.md).
|
||||||
|
- `AndroidQFModule`: `("android", "check-androidqf")`. MVT calls `from_dir()`
|
||||||
|
or `from_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 calls `from_dir()`
|
||||||
|
or `from_ab()`. The module reads files with `_get_files_by_pattern()` and
|
||||||
|
`_get_file_content()`.
|
||||||
|
- `BugReportModule`: `("android", "check-bugreport")`. MVT calls `from_dir()`
|
||||||
|
or `from_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`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mvt.ios.modules.backup import Manifest
|
||||||
|
from mvt.plugin import MVTModule
|
||||||
|
|
||||||
|
|
||||||
class DependentCustomModule(MVTModule):
|
class DependentCustomModule(MVTModule):
|
||||||
@@ -124,6 +167,30 @@ class DependentCustomModule(MVTModule):
|
|||||||
self.results = [{"manifest_entries": len(manifest_results)}]
|
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](#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](plugin_configuration.md), 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
|
## Installed module packages
|
||||||
|
|
||||||
Python packages can register modules so they load automatically in every
|
Python packages can register modules so they load automatically in every
|
||||||
@@ -133,14 +200,14 @@ the package's `pyproject.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[project.entry-points."mvt.modules"]
|
[project.entry-points."mvt.modules"]
|
||||||
mvt-plugin-amnesty-custom = "mvt_plugin_amnesty_custom:get_modules"
|
mvt-plugin-example-org = "mvt_plugin_example_org:get_modules"
|
||||||
```
|
```
|
||||||
|
|
||||||
The entry point must resolve to an iterable of `MVTModule` subclasses, or to
|
The entry point must resolve to an iterable of `MVTModule` subclasses, or to
|
||||||
a callable returning one:
|
a callable returning one:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mvt.common.module import MVTModule
|
from mvt.plugin import MVTModule
|
||||||
|
|
||||||
|
|
||||||
class PackagedModule(MVTModule):
|
class PackagedModule(MVTModule):
|
||||||
@@ -154,6 +221,10 @@ def get_modules() -> list[type[MVTModule]]:
|
|||||||
return [PackagedModule]
|
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
|
Installed modules follow the same rules as other custom modules: each module
|
||||||
must declare `supported_commands`, and dependencies are resolved with the
|
must declare `supported_commands`, and dependencies are resolved with the
|
||||||
standard ordering logic. A broken entry point is skipped with a warning and
|
standard ordering logic. A broken entry point is skipped with a warning and
|
||||||
@@ -164,7 +235,7 @@ from sources you trust.
|
|||||||
For a `pipx` installation of MVT, inject the package into MVT's environment:
|
For a `pipx` installation of MVT, inject the package into MVT's environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pipx inject mvt mvt-plugin-amnesty-custom
|
pipx inject mvt mvt-plugin-example-org
|
||||||
```
|
```
|
||||||
|
|
||||||
Module packages that need their own settings, such as an API key, should store
|
Module packages that need their own settings, such as an API key, should store
|
||||||
@@ -175,9 +246,9 @@ rather than in MVT's own `config.yaml`.
|
|||||||
|
|
||||||
Name module packages `mvt-plugin-<name>` (import package `mvt_plugin_<name>`),
|
Name module packages `mvt-plugin-<name>` (import package `mvt_plugin_<name>`),
|
||||||
and include the name of the publishing organization or author so packages from
|
and include the name of the publishing organization or author so packages from
|
||||||
different groups do not collide: for example, Amnesty International's custom
|
different groups do not collide: for example, an organisation's custom modules
|
||||||
modules would be distributed as `mvt-plugin-amnesty-custom` with the import
|
would be distributed as `mvt-plugin-example-org` with the import package
|
||||||
package `mvt_plugin_amnesty_custom`.
|
`mvt_plugin_example_org`.
|
||||||
|
|
||||||
The prefix makes module packages easy to find on PyPI and keeps their import
|
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
|
names from clashing with unrelated Python packages. It is a convention, not a
|
||||||
@@ -196,11 +267,42 @@ came from. MVT's own modules log under their dotted path (for example
|
|||||||
MVT's internal logger tree:
|
MVT's internal logger tree:
|
||||||
|
|
||||||
- Installed packages log under `mvt.ext.<package>`, with the `mvt_plugin_`
|
- Installed packages log under `mvt.ext.<package>`, with the `mvt_plugin_`
|
||||||
prefix stripped: modules in `mvt_plugin_amnesty_custom` log as
|
prefix stripped: modules in `mvt_plugin_example_org` log as
|
||||||
`mvt.ext.amnesty_custom.*`.
|
`mvt.ext.example_org.*`.
|
||||||
- Files loaded with `--load-module` or `MVT_CUSTOM_MODULES` log as
|
- Files loaded with `--load-module` or `MVT_CUSTOM_MODULES` log as
|
||||||
`mvt.ext.<file name>`.
|
`mvt.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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
## Auditing loaded modules
|
||||||
|
|
||||||
Because installed module packages load automatically, MVT records where every
|
Because installed module packages load automatically, MVT records where every
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ configuration:
|
|||||||
|
|
||||||
The exact parent folder follows the platform convention used for MVT's
|
The exact parent folder follows the platform convention used for MVT's
|
||||||
`config.yaml` (for example `~/Library/Application Support/mvt` on macOS). Use
|
`config.yaml` (for example `~/Library/Application Support/mvt` on macOS). Use
|
||||||
`mvt.common.plugin_config.plugin_config_path()` instead of building the path by
|
`plugin_config_path()` from `mvt.plugin` instead of building the path by hand.
|
||||||
hand.
|
|
||||||
|
|
||||||
Plugin names must be lowercase and may only contain letters, digits and dashes,
|
Plugin names must be lowercase and may only contain letters, digits and dashes,
|
||||||
matching the `mvt-plugin-<name>` package naming convention. MVT creates the
|
matching the `mvt-plugin-<name>` package naming convention. MVT creates the
|
||||||
@@ -38,9 +37,8 @@ leaves a partially written settings file behind.
|
|||||||
|
|
||||||
Everything else a plugin keeps on disk, such as a cache, a downloaded artifact
|
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()`
|
or synchronization state, belongs in the folder returned by the `data_folder()`
|
||||||
class method of the plugin's settings class, or by
|
class method of the plugin's settings class, or by `plugin_data_folder()` from
|
||||||
`mvt.common.plugin_config.plugin_data_folder()` called with the plugin name if
|
`mvt.plugin`, called with the plugin name if the plugin has no settings class:
|
||||||
the plugin has no settings class:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.local/share/mvt/plugin-data/<plugin name>/ # Linux
|
~/.local/share/mvt/plugin-data/<plugin name>/ # Linux
|
||||||
@@ -76,7 +74,7 @@ defaults:
|
|||||||
```python
|
```python
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.common.plugin_config import MVTPluginSettings
|
from mvt.plugin import MVTPluginSettings
|
||||||
|
|
||||||
|
|
||||||
class ExamplePluginSettings(MVTPluginSettings):
|
class ExamplePluginSettings(MVTPluginSettings):
|
||||||
@@ -94,12 +92,15 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from mvt.plugin import plugin_env_prefix
|
||||||
|
|
||||||
|
|
||||||
def sync():
|
def sync():
|
||||||
settings = ExamplePluginSettings.load()
|
settings = ExamplePluginSettings.load()
|
||||||
if not settings.API_KEY:
|
if not settings.API_KEY:
|
||||||
|
prefix = plugin_env_prefix(settings.plugin_name)
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
"No API key configured. Set MVT_PLUGIN_EXAMPLE_PLUGIN_API_KEY or "
|
f"No API key configured. Set {prefix}API_KEY or "
|
||||||
"run 'example-plugin configure'."
|
"run 'example-plugin configure'."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +108,9 @@ def sync():
|
|||||||
settings.save()
|
settings.save()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The message builds the variable name with `plugin_env_prefix()`, see
|
||||||
|
[Environment Variables](#environment-variables).
|
||||||
|
|
||||||
A missing settings file is not an error: the plugin then runs on the field
|
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
|
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`.
|
values that differ from the defaults, and it never touches MVT's `config.yaml`.
|
||||||
@@ -129,6 +133,10 @@ export MVT_PLUGIN_EXAMPLE_PLUGIN_API_KEY=...
|
|||||||
export MVT_PLUGIN_EXAMPLE_PLUGIN_MAX_RESULTS=50
|
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:
|
Settings resolve in this order, from highest to lowest priority:
|
||||||
|
|
||||||
1. Arguments passed to the settings class directly, such as
|
1. Arguments passed to the settings class directly, such as
|
||||||
|
|||||||
+18
-14
@@ -1,30 +1,32 @@
|
|||||||
# Check an iOS Sysdiagnose
|
# Check an iOS Sysdiagnose
|
||||||
|
|
||||||
`mvt-ios check-sysdiagnose` prepares an iOS sysdiagnose archive for analysis by
|
`mvt-ios check-sysdiagnose` prepares an iOS sysdiagnose archive for analysis by
|
||||||
custom MVT modules. MVT does not include built-in sysdiagnose modules. You must
|
custom MVT modules. MVT does not include built-in sysdiagnose modules. The
|
||||||
load at least one custom module that explicitly supports this command.
|
command runs the modules of the installed
|
||||||
|
[plugin packages](../development/index.md#installed-module-packages) which
|
||||||
|
declare support for it. Install at least one such package first.
|
||||||
|
|
||||||
The command accepts either an extracted sysdiagnose directory or the original
|
The command accepts either an extracted sysdiagnose directory or the original
|
||||||
gzip-compressed tar archive.
|
gzip-compressed tar archive.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvt-ios check-sysdiagnose \
|
mvt-ios check-sysdiagnose --output ./results \
|
||||||
--load-module ./sysdiagnose_modules.py \
|
|
||||||
--output ./results \
|
|
||||||
./sysdiagnose_2024.01.02_03-04-05+0200.tar.gz
|
./sysdiagnose_2024.01.02_03-04-05+0200.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--hashes` to include hashes for analyzed files in `info.json`, and
|
Use `--hashes` to include hashes for analyzed files in `info.json`, and
|
||||||
`--list-modules` to display the eligible custom modules without running them.
|
`--list-modules` to display the eligible modules without running them.
|
||||||
|
|
||||||
## Writing a custom module
|
## Writing a custom module
|
||||||
|
|
||||||
Extend `SysdiagnoseExtraction` to access the archive contents consistently for
|
Extend `SysdiagnoseExtraction` from `mvt.plugin`, see
|
||||||
both directory and tar inputs. Each module must declare the command explicitly
|
[Writing a module](../development/index.md#writing-a-module). The module reads
|
||||||
in `supported_commands`.
|
the archive the same way whether MVT was given a folder or a tar archive. It
|
||||||
|
declares the command in `supported_commands`. While writing one,
|
||||||
|
[load it from its file](../development/index.md#developing-modules-locally).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mvt.ios.modules.sysdiagnose import SysdiagnoseExtraction
|
from mvt.plugin import SysdiagnoseExtraction
|
||||||
|
|
||||||
|
|
||||||
class ExampleSysdiagnoseModule(SysdiagnoseExtraction):
|
class ExampleSysdiagnoseModule(SysdiagnoseExtraction):
|
||||||
@@ -44,7 +46,9 @@ class ExampleSysdiagnoseModule(SysdiagnoseExtraction):
|
|||||||
return None
|
return None
|
||||||
```
|
```
|
||||||
|
|
||||||
The base class provides `from_sysdiagnose_folder()` and
|
MVT extracts a tar archive first. It calls `from_sysdiagnose_folder()` on each
|
||||||
`from_sysdiagnose_tar()` setup hooks, as well as protected file lookup, file
|
module before `run()`. `ips_files` lists the IPS crash reports.
|
||||||
reading, and timezone extraction helpers. IPS crash-report metadata is exposed
|
|
||||||
on `ips_files`.
|
`_get_files_by_pattern()` and `_get_file_content()` are internal helpers of the
|
||||||
|
base class. Use them to read the archive. Their names and signatures can change
|
||||||
|
between releases. See `src/mvt/ios/modules/sysdiagnose/base.py`.
|
||||||
+20
-16
@@ -36,6 +36,7 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_OUTPUT,
|
HELP_MSG_OUTPUT,
|
||||||
HELP_MSG_STIX2,
|
HELP_MSG_STIX2,
|
||||||
HELP_MSG_VERBOSE,
|
HELP_MSG_VERBOSE,
|
||||||
|
HELP_MSG_VERBOSE_COMMAND,
|
||||||
HELP_MSG_VERSION,
|
HELP_MSG_VERSION,
|
||||||
HELP_MSG_VIRUS_TOTAL,
|
HELP_MSG_VIRUS_TOTAL,
|
||||||
)
|
)
|
||||||
@@ -48,11 +49,8 @@ from .cmd_check_androidqf import CmdAndroidCheckAndroidQF
|
|||||||
from .cmd_check_backup import CmdAndroidCheckBackup
|
from .cmd_check_backup import CmdAndroidCheckBackup
|
||||||
from .cmd_check_bugreport import CmdAndroidCheckBugreport
|
from .cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||||
from .cmd_check_intrusion_logs import CmdAndroidCheckIntrusionLogs
|
from .cmd_check_intrusion_logs import CmdAndroidCheckIntrusionLogs
|
||||||
from .modules.intrusion_logs import INTRUSION_LOGS_MODULES
|
from .command_modules import ANDROID_CHECK_IOCS_MODULES
|
||||||
from .modules.androidqf import ANDROIDQF_MODULES
|
|
||||||
from .modules.backup import BACKUP_MODULES
|
|
||||||
from .modules.backup.helpers import cli_load_android_backup_password
|
from .modules.backup.helpers import cli_load_android_backup_password
|
||||||
from .modules.bugreport import BUGREPORT_MODULES
|
|
||||||
|
|
||||||
init_logging()
|
init_logging()
|
||||||
log = logging.getLogger("mvt")
|
log = logging.getLogger("mvt")
|
||||||
@@ -70,6 +68,11 @@ def _get_disable_flags(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_verbose(ctx):
|
||||||
|
"""Return whether --verbose was passed to the CLI itself."""
|
||||||
|
return bool(ctx.obj and ctx.obj.get("verbose", False))
|
||||||
|
|
||||||
|
|
||||||
def _load_custom_modules(load_module):
|
def _load_custom_modules(load_module):
|
||||||
try:
|
try:
|
||||||
return load_custom_modules(load_module)
|
return load_custom_modules(load_module)
|
||||||
@@ -90,11 +93,14 @@ def _load_custom_modules(load_module):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
)
|
)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, disable_update_check, disable_indicator_update_check):
|
def cli(ctx, disable_update_check, disable_indicator_update_check, verbose):
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj["disable_version_check"] = disable_update_check
|
ctx.obj["disable_version_check"] = disable_update_check
|
||||||
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
||||||
|
ctx.obj["verbose"] = verbose
|
||||||
|
set_verbose_logging(verbose)
|
||||||
logo(
|
logo(
|
||||||
disable_version_check=disable_update_check,
|
disable_version_check=disable_update_check,
|
||||||
disable_indicator_check=disable_indicator_update_check,
|
disable_indicator_check=disable_indicator_update_check,
|
||||||
@@ -145,7 +151,7 @@ def check_adb(ctx):
|
|||||||
default=[],
|
default=[],
|
||||||
help=HELP_MSG_LOAD_MODULE,
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
)
|
)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
|
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_bugreport(
|
def check_bugreport(
|
||||||
@@ -158,7 +164,7 @@ def check_bugreport(
|
|||||||
verbose,
|
verbose,
|
||||||
bugreport_path,
|
bugreport_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
# Always generate hashes as bug reports are small.
|
# Always generate hashes as bug reports are small.
|
||||||
cmd = CmdAndroidCheckBugreport(
|
cmd = CmdAndroidCheckBugreport(
|
||||||
@@ -213,7 +219,7 @@ def check_bugreport(
|
|||||||
)
|
)
|
||||||
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
||||||
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_backup(
|
def check_backup(
|
||||||
@@ -227,7 +233,7 @@ def check_backup(
|
|||||||
verbose,
|
verbose,
|
||||||
backup_path,
|
backup_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
# Always generate hashes as backups are generally small.
|
# Always generate hashes as backups are generally small.
|
||||||
@@ -287,7 +293,7 @@ def check_backup(
|
|||||||
)
|
)
|
||||||
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
||||||
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
|
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_androidqf(
|
def check_androidqf(
|
||||||
@@ -305,7 +311,7 @@ def check_androidqf(
|
|||||||
verbose,
|
verbose,
|
||||||
androidqf_path,
|
androidqf_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
cmd = CmdAndroidCheckAndroidQF(
|
cmd = CmdAndroidCheckAndroidQF(
|
||||||
@@ -373,7 +379,7 @@ def check_androidqf(
|
|||||||
"time instead of UTC."
|
"time instead of UTC."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("LOGS_PATH", type=click.Path(exists=True))
|
@click.argument("LOGS_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_intrusion_logs(
|
def check_intrusion_logs(
|
||||||
@@ -387,7 +393,7 @@ def check_intrusion_logs(
|
|||||||
verbose,
|
verbose,
|
||||||
logs_path,
|
logs_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
module_options = {}
|
module_options = {}
|
||||||
@@ -450,9 +456,7 @@ def check_iocs(ctx, iocs, list_modules, module, load_module, folder):
|
|||||||
custom_modules=custom_modules,
|
custom_modules=custom_modules,
|
||||||
platform="android",
|
platform="android",
|
||||||
)
|
)
|
||||||
cmd.modules = (
|
cmd.modules = ANDROID_CHECK_IOCS_MODULES
|
||||||
BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES + INTRUSION_LOGS_MODULES
|
|
||||||
)
|
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
cmd.list_modules()
|
cmd.list_modules()
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
"""Module lists an mvt-android command composes from more than one family.
|
||||||
|
|
||||||
|
Commands whose modules are one family read that family directly. check-iocs
|
||||||
|
re-checks stored results, so it has to know every module that could have
|
||||||
|
written one, and both the CLI and any other code needing that answer share
|
||||||
|
the list from here rather than each concatenating their own.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
from .modules.androidqf import ANDROIDQF_MODULES
|
||||||
|
from .modules.backup import BACKUP_MODULES
|
||||||
|
from .modules.bugreport import BUGREPORT_MODULES
|
||||||
|
from .modules.intrusion_logs import INTRUSION_LOGS_MODULES
|
||||||
|
|
||||||
|
ANDROID_CHECK_IOCS_MODULES: list[type[MVTModule]] = (
|
||||||
|
BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES + INTRUSION_LOGS_MODULES
|
||||||
|
)
|
||||||
+6
-2
@@ -17,11 +17,12 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
HELP_MSG_DISABLE_UPDATE_CHECK,
|
HELP_MSG_DISABLE_UPDATE_CHECK,
|
||||||
HELP_MSG_STIX2,
|
HELP_MSG_STIX2,
|
||||||
|
HELP_MSG_VERBOSE,
|
||||||
HELP_MSG_VERSION,
|
HELP_MSG_VERSION,
|
||||||
)
|
)
|
||||||
from mvt.common.logo import logo
|
from mvt.common.logo import logo
|
||||||
from mvt.common.updates import IndicatorsUpdates
|
from mvt.common.updates import IndicatorsUpdates
|
||||||
from mvt.common.utils import init_logging
|
from mvt.common.utils import init_logging, set_verbose_logging
|
||||||
|
|
||||||
init_logging()
|
init_logging()
|
||||||
|
|
||||||
@@ -41,8 +42,9 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
)
|
)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, disable_update_check, disable_indicator_update_check):
|
def cli(ctx, disable_update_check, disable_indicator_update_check, verbose):
|
||||||
"""Mobile Verification Toolkit.
|
"""Mobile Verification Toolkit.
|
||||||
|
|
||||||
mvt-ios and mvt-android run the forensic analysis of an acquisition: each
|
mvt-ios and mvt-android run the forensic analysis of an acquisition: each
|
||||||
@@ -53,6 +55,8 @@ def cli(ctx, disable_update_check, disable_indicator_update_check):
|
|||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj["disable_version_check"] = disable_update_check
|
ctx.obj["disable_version_check"] = disable_update_check
|
||||||
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
||||||
|
ctx.obj["verbose"] = verbose
|
||||||
|
set_verbose_logging(verbose)
|
||||||
if ctx.invoked_subcommand != "completion":
|
if ctx.invoked_subcommand != "completion":
|
||||||
logo(
|
logo(
|
||||||
disable_version_check=disable_update_check,
|
disable_version_check=disable_update_check,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from typing import Iterable
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from .module_loader import CUSTOM_COMMAND_MODULE_PREFIX
|
||||||
|
|
||||||
IOS_CLI_PLUGIN_GROUP = "mvt.ios.cli_plugins"
|
IOS_CLI_PLUGIN_GROUP = "mvt.ios.cli_plugins"
|
||||||
ANDROID_CLI_PLUGIN_GROUP = "mvt.android.cli_plugins"
|
ANDROID_CLI_PLUGIN_GROUP = "mvt.android.cli_plugins"
|
||||||
# Commands in this group are registered on the platform-neutral mvt command only.
|
# Commands in this group are registered on the platform-neutral mvt command only.
|
||||||
@@ -57,7 +59,7 @@ class BrokenPluginCommand(click.Command):
|
|||||||
|
|
||||||
def _module_name_for_path(path: Path) -> str:
|
def _module_name_for_path(path: Path) -> str:
|
||||||
digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:16]
|
digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:16]
|
||||||
return f"_mvt_custom_command_{path.stem}_{digest}"
|
return f"{CUSTOM_COMMAND_MODULE_PREFIX}{path.stem}_{digest}"
|
||||||
|
|
||||||
|
|
||||||
def _iter_command_files(path: Path) -> Iterable[Path]:
|
def _iter_command_files(path: Path) -> Iterable[Path]:
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ HELP_MSG_LOAD_MODULE = (
|
|||||||
HELP_MSG_NONINTERACTIVE = "Don't ask interactive questions during processing"
|
HELP_MSG_NONINTERACTIVE = "Don't ask interactive questions during processing"
|
||||||
HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
|
HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
|
||||||
HELP_MSG_VERBOSE = "Verbose mode"
|
HELP_MSG_VERBOSE = "Verbose mode"
|
||||||
|
HELP_MSG_VERBOSE_COMMAND = (
|
||||||
|
"Verbose mode (kept for compatibility, pass --verbose before the command "
|
||||||
|
"name instead)"
|
||||||
|
)
|
||||||
HELP_MSG_CHECK_IOCS = "Compare stored JSON results to provided indicators"
|
HELP_MSG_CHECK_IOCS = "Compare stored JSON results to provided indicators"
|
||||||
HELP_MSG_STIX2 = "Download public STIX2 indicators"
|
HELP_MSG_STIX2 = "Download public STIX2 indicators"
|
||||||
HELP_MSG_DISABLE_UPDATE_CHECK = "Disable MVT version update check"
|
HELP_MSG_DISABLE_UPDATE_CHECK = "Disable MVT version update check"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
@@ -26,6 +27,9 @@ EXTERNAL_LOGGER_NAMESPACE = "mvt.ext"
|
|||||||
PLUGIN_PACKAGE_PREFIX = "mvt_plugin_"
|
PLUGIN_PACKAGE_PREFIX = "mvt_plugin_"
|
||||||
_ORIGIN_ATTRIBUTE = "_mvt_module_origin"
|
_ORIGIN_ATTRIBUTE = "_mvt_module_origin"
|
||||||
_PATH_MODULE_PREFIX = "_mvt_custom_module_"
|
_PATH_MODULE_PREFIX = "_mvt_custom_module_"
|
||||||
|
# Shared with cli_plugins, which names a loaded command file this way.
|
||||||
|
CUSTOM_COMMAND_MODULE_PREFIX = "_mvt_custom_command_"
|
||||||
|
_LOADED_FILE_DIGEST = re.compile(r"_[0-9a-f]{16}$")
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,6 +70,25 @@ def _module_name_for_path(path: Path) -> str:
|
|||||||
return f"{_PATH_MODULE_PREFIX}{path.stem}_{digest}"
|
return f"{_PATH_MODULE_PREFIX}{path.stem}_{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_builtin_logger_name(name: str) -> bool:
|
||||||
|
return name == "mvt" or name.startswith("mvt.")
|
||||||
|
|
||||||
|
|
||||||
|
def _loaded_file_stem(name: str, prefix: str) -> str:
|
||||||
|
"""Recover a loaded file's name from the import name MVT gave it."""
|
||||||
|
return _LOADED_FILE_DIGEST.sub("", name[len(prefix) :])
|
||||||
|
|
||||||
|
|
||||||
|
def _external_logger_name(name: str) -> str:
|
||||||
|
"""Return the "mvt.ext" logger name external code logs under."""
|
||||||
|
top_level, separator, rest = name.partition(".")
|
||||||
|
if top_level.startswith(PLUGIN_PACKAGE_PREFIX) and len(top_level) > len(
|
||||||
|
PLUGIN_PACKAGE_PREFIX
|
||||||
|
):
|
||||||
|
name = top_level[len(PLUGIN_PACKAGE_PREFIX) :] + separator + rest
|
||||||
|
return f"{EXTERNAL_LOGGER_NAMESPACE}.{name}"
|
||||||
|
|
||||||
|
|
||||||
def get_module_logger(module_class: type[MVTModule]) -> logging.Logger:
|
def get_module_logger(module_class: type[MVTModule]) -> logging.Logger:
|
||||||
"""Return the logger a module's records should be emitted through.
|
"""Return the logger a module's records should be emitted through.
|
||||||
|
|
||||||
@@ -80,19 +103,35 @@ def get_module_logger(module_class: type[MVTModule]) -> logging.Logger:
|
|||||||
"mvt_plugin_<name>" naming convention log under "mvt.ext.<name>".
|
"mvt_plugin_<name>" naming convention log under "mvt.ext.<name>".
|
||||||
"""
|
"""
|
||||||
name = module_class.__module__
|
name = module_class.__module__
|
||||||
if name == "mvt" or name.startswith("mvt."):
|
if _is_builtin_logger_name(name):
|
||||||
return logging.getLogger(name)
|
return logging.getLogger(name)
|
||||||
|
|
||||||
if name.startswith(_PATH_MODULE_PREFIX):
|
if name.startswith(_PATH_MODULE_PREFIX):
|
||||||
name = Path(get_module_origin(module_class).name).stem
|
file_name = Path(get_module_origin(module_class).name).stem
|
||||||
else:
|
return logging.getLogger(f"{EXTERNAL_LOGGER_NAMESPACE}.{file_name}")
|
||||||
top_level, separator, rest = name.partition(".")
|
|
||||||
if top_level.startswith(PLUGIN_PACKAGE_PREFIX) and len(top_level) > len(
|
|
||||||
PLUGIN_PACKAGE_PREFIX
|
|
||||||
):
|
|
||||||
name = top_level[len(PLUGIN_PACKAGE_PREFIX) :] + separator + rest
|
|
||||||
|
|
||||||
return logging.getLogger(f"{EXTERNAL_LOGGER_NAMESPACE}.{name}")
|
return logging.getLogger(_external_logger_name(name))
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_logger(name: str) -> logging.Logger:
|
||||||
|
"""Return a general logger for use in custom MVT plugins.
|
||||||
|
|
||||||
|
Call it with ``__name__``. The logger sits under "mvt.ext". That is
|
||||||
|
where get_module_logger() puts module classes. A file loaded with
|
||||||
|
--load-module or --load-command is named after the file.
|
||||||
|
"""
|
||||||
|
if _is_builtin_logger_name(name):
|
||||||
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
# A file loaded with --load-module or --load-command is imported under a
|
||||||
|
# mangled name. Log it under the file it came from. get_module_logger()
|
||||||
|
# does the same for the module classes such a file defines.
|
||||||
|
for prefix in (_PATH_MODULE_PREFIX, CUSTOM_COMMAND_MODULE_PREFIX):
|
||||||
|
if name.startswith(prefix):
|
||||||
|
stem = _loaded_file_stem(name, prefix)
|
||||||
|
return logging.getLogger(f"{EXTERNAL_LOGGER_NAMESPACE}.{stem}")
|
||||||
|
|
||||||
|
return logging.getLogger(_external_logger_name(name))
|
||||||
|
|
||||||
|
|
||||||
def _iter_module_files(path: Path) -> Iterable[Path]:
|
def _iter_module_files(path: Path) -> Iterable[Path]:
|
||||||
|
|||||||
+11
-5
@@ -256,12 +256,18 @@ def init_logging(verbose: bool = False):
|
|||||||
|
|
||||||
|
|
||||||
def set_verbose_logging(verbose: bool = False):
|
def set_verbose_logging(verbose: bool = False):
|
||||||
|
"""Raise or lower the verbosity of MVT's console output.
|
||||||
|
|
||||||
|
Only MVT's own console handler is adjusted, wherever it sits in the list.
|
||||||
|
The file handler a command attaches to its output folder keeps recording
|
||||||
|
everything, so the command.log of a run does not depend on how the run was
|
||||||
|
invoked, and a handler attached to the "mvt" logger by anything else is
|
||||||
|
left alone.
|
||||||
|
"""
|
||||||
log = logging.getLogger("mvt")
|
log = logging.getLogger("mvt")
|
||||||
handler = log.handlers[0]
|
for handler in log.handlers:
|
||||||
if verbose:
|
if isinstance(handler, MVTLogHandler):
|
||||||
handler.setLevel(logging.DEBUG)
|
handler.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||||
else:
|
|
||||||
handler.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def exec_or_profile(module, globals, locals):
|
def exec_or_profile(module, globals, locals):
|
||||||
|
|||||||
+18
-11
@@ -38,6 +38,7 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_LOAD_MODULE,
|
HELP_MSG_LOAD_MODULE,
|
||||||
HELP_MSG_MODULE,
|
HELP_MSG_MODULE,
|
||||||
HELP_MSG_VERBOSE,
|
HELP_MSG_VERBOSE,
|
||||||
|
HELP_MSG_VERBOSE_COMMAND,
|
||||||
HELP_MSG_CHECK_FS,
|
HELP_MSG_CHECK_FS,
|
||||||
HELP_MSG_CHECK_IOCS,
|
HELP_MSG_CHECK_IOCS,
|
||||||
HELP_MSG_STIX2,
|
HELP_MSG_STIX2,
|
||||||
@@ -52,9 +53,7 @@ from .cmd_check_backup import CmdIOSCheckBackup
|
|||||||
from .cmd_check_fs import CmdIOSCheckFS
|
from .cmd_check_fs import CmdIOSCheckFS
|
||||||
from .cmd_check_sysdiagnose import CmdIOSCheckSysdiagnose
|
from .cmd_check_sysdiagnose import CmdIOSCheckSysdiagnose
|
||||||
from .decrypt import DecryptBackup
|
from .decrypt import DecryptBackup
|
||||||
from .modules.backup import BACKUP_MODULES
|
from .command_modules import IOS_CHECK_IOCS_MODULES
|
||||||
from .modules.fs import FS_MODULES
|
|
||||||
from .modules.mixed import MIXED_MODULES
|
|
||||||
|
|
||||||
init_logging()
|
init_logging()
|
||||||
log = logging.getLogger("mvt")
|
log = logging.getLogger("mvt")
|
||||||
@@ -74,6 +73,11 @@ def _get_disable_flags(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_verbose(ctx):
|
||||||
|
"""Return whether --verbose was passed to the CLI itself."""
|
||||||
|
return bool(ctx.obj and ctx.obj.get("verbose", False))
|
||||||
|
|
||||||
|
|
||||||
def _load_custom_modules(load_module):
|
def _load_custom_modules(load_module):
|
||||||
try:
|
try:
|
||||||
return load_custom_modules(load_module)
|
return load_custom_modules(load_module)
|
||||||
@@ -94,11 +98,14 @@ def _load_custom_modules(load_module):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
)
|
)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, disable_update_check, disable_indicator_update_check):
|
def cli(ctx, disable_update_check, disable_indicator_update_check, verbose):
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj["disable_version_check"] = disable_update_check
|
ctx.obj["disable_version_check"] = disable_update_check
|
||||||
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
||||||
|
ctx.obj["verbose"] = verbose
|
||||||
|
set_verbose_logging(verbose)
|
||||||
logo(
|
logo(
|
||||||
disable_version_check=disable_update_check,
|
disable_version_check=disable_update_check,
|
||||||
disable_indicator_check=disable_indicator_update_check,
|
disable_indicator_check=disable_indicator_update_check,
|
||||||
@@ -254,7 +261,7 @@ def extract_key(password, key_file, backup_path):
|
|||||||
help=HELP_MSG_LOAD_MODULE,
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
)
|
)
|
||||||
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_backup(
|
def check_backup(
|
||||||
@@ -269,7 +276,7 @@ def check_backup(
|
|||||||
verbose,
|
verbose,
|
||||||
backup_path,
|
backup_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
module_options = {"fast_mode": fast}
|
module_options = {"fast_mode": fast}
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
@@ -323,7 +330,7 @@ def check_backup(
|
|||||||
help=HELP_MSG_LOAD_MODULE,
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
)
|
)
|
||||||
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("DUMP_PATH", type=click.Path(exists=True))
|
@click.argument("DUMP_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_fs(
|
def check_fs(
|
||||||
@@ -338,7 +345,7 @@ def check_fs(
|
|||||||
verbose,
|
verbose,
|
||||||
dump_path,
|
dump_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
module_options = {"fast_mode": fast}
|
module_options = {"fast_mode": fast}
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
@@ -392,7 +399,7 @@ def check_fs(
|
|||||||
help=HELP_MSG_LOAD_MODULE,
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
)
|
)
|
||||||
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
|
||||||
@click.argument("SYSDIAGNOSE_PATH", type=click.Path(exists=True))
|
@click.argument("SYSDIAGNOSE_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_sysdiagnose(
|
def check_sysdiagnose(
|
||||||
@@ -406,7 +413,7 @@ def check_sysdiagnose(
|
|||||||
verbose,
|
verbose,
|
||||||
sysdiagnose_path,
|
sysdiagnose_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose or _get_verbose(ctx))
|
||||||
custom_modules = _load_custom_modules(load_module)
|
custom_modules = _load_custom_modules(load_module)
|
||||||
cmd = CmdIOSCheckSysdiagnose(
|
cmd = CmdIOSCheckSysdiagnose(
|
||||||
target_path=sysdiagnose_path,
|
target_path=sysdiagnose_path,
|
||||||
@@ -470,7 +477,7 @@ def check_iocs(ctx, iocs, list_modules, module, load_module, folder):
|
|||||||
custom_modules=custom_modules,
|
custom_modules=custom_modules,
|
||||||
platform="ios",
|
platform="ios",
|
||||||
)
|
)
|
||||||
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
cmd.modules = IOS_CHECK_IOCS_MODULES
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
cmd.list_modules()
|
cmd.list_modules()
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
"""Module lists an mvt-ios command composes from more than one family.
|
||||||
|
|
||||||
|
Commands whose modules are one family read that family directly. check-iocs
|
||||||
|
re-checks stored results, so it has to know every module that could have
|
||||||
|
written one, and both the CLI and any other code needing that answer share
|
||||||
|
the list from here rather than each concatenating their own.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
from .modules.backup import BACKUP_MODULES
|
||||||
|
from .modules.fs import FS_MODULES
|
||||||
|
from .modules.mixed import MIXED_MODULES
|
||||||
|
|
||||||
|
IOS_CHECK_IOCS_MODULES: list[type[MVTModule]] = (
|
||||||
|
BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
||||||
|
)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
"""Stable functions and modules which plugins can import.
|
||||||
|
|
||||||
|
Anything else in mvt can be imported too, and may change
|
||||||
|
between releases without notice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from mvt.android.modules.androidqf.base import AndroidQFModule
|
||||||
|
from mvt.android.modules.backup.base import BackupModule as AndroidBackupModule
|
||||||
|
from mvt.android.modules.bugreport.base import BugReportModule
|
||||||
|
from mvt.common.alerts import Alert, AlertLevel
|
||||||
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.config import settings
|
||||||
|
from mvt.common.module import DatabaseCorruptedError, DatabaseNotFoundError, MVTModule
|
||||||
|
from mvt.common.module_loader import get_plugin_logger
|
||||||
|
from mvt.common.module_types import (
|
||||||
|
ModuleAtomicResult,
|
||||||
|
ModuleResults,
|
||||||
|
ModuleSerializedResult,
|
||||||
|
)
|
||||||
|
from mvt.common.plugin_config import (
|
||||||
|
MVTPluginSettings,
|
||||||
|
PluginConfigLoadError,
|
||||||
|
plugin_config_path,
|
||||||
|
plugin_data_folder,
|
||||||
|
plugin_env_prefix,
|
||||||
|
)
|
||||||
|
from mvt.common.utils import (
|
||||||
|
convert_chrometime_to_datetime,
|
||||||
|
convert_datetime_to_iso,
|
||||||
|
convert_mactime_to_datetime,
|
||||||
|
convert_mactime_to_iso,
|
||||||
|
convert_unix_to_iso,
|
||||||
|
convert_unix_to_utc_datetime,
|
||||||
|
)
|
||||||
|
from mvt.common.version import MVT_VERSION
|
||||||
|
from mvt.ios.modules.base import IOSExtraction
|
||||||
|
from mvt.ios.modules.sysdiagnose.base import SysdiagnoseExtraction
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Classes a plugin subclasses.
|
||||||
|
"MVTModule",
|
||||||
|
"IOSExtraction",
|
||||||
|
"SysdiagnoseExtraction",
|
||||||
|
"AndroidQFModule",
|
||||||
|
"AndroidBackupModule",
|
||||||
|
"BugReportModule",
|
||||||
|
"Command",
|
||||||
|
# Results and alerts.
|
||||||
|
"ModuleAtomicResult",
|
||||||
|
"ModuleResults",
|
||||||
|
"ModuleSerializedResult",
|
||||||
|
"Alert",
|
||||||
|
"AlertLevel",
|
||||||
|
# Errors a module raises.
|
||||||
|
"DatabaseNotFoundError",
|
||||||
|
"DatabaseCorruptedError",
|
||||||
|
# Settings.
|
||||||
|
"settings",
|
||||||
|
"MVTPluginSettings",
|
||||||
|
"PluginConfigLoadError",
|
||||||
|
"plugin_config_path",
|
||||||
|
"plugin_data_folder",
|
||||||
|
"plugin_env_prefix",
|
||||||
|
# Logging.
|
||||||
|
"get_plugin_logger",
|
||||||
|
# Timestamps.
|
||||||
|
"convert_chrometime_to_datetime",
|
||||||
|
"convert_datetime_to_iso",
|
||||||
|
"convert_mactime_to_datetime",
|
||||||
|
"convert_mactime_to_iso",
|
||||||
|
"convert_unix_to_iso",
|
||||||
|
"convert_unix_to_utc_datetime",
|
||||||
|
# MVT's version.
|
||||||
|
"MVT_VERSION",
|
||||||
|
]
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from mvt.android.cli import cli as android_cli
|
||||||
|
from mvt.android.command_modules import ANDROID_CHECK_IOCS_MODULES
|
||||||
|
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
from mvt.ios.cli import cli as ios_cli
|
||||||
|
from mvt.ios.command_modules import IOS_CHECK_IOCS_MODULES
|
||||||
|
|
||||||
|
# Keep the banner of the group callback from checking for updates online.
|
||||||
|
OFFLINE = ["--disable-update-check", "--disable-indicator-update-check"]
|
||||||
|
|
||||||
|
|
||||||
|
class CustomResultsModule(MVTModule):
|
||||||
|
"""A custom module which declares the check-iocs pair of both platforms."""
|
||||||
|
|
||||||
|
slug = "custom_results"
|
||||||
|
supported_commands = (
|
||||||
|
("ios", "check-backup"),
|
||||||
|
("ios", "check-iocs"),
|
||||||
|
("android", "check-iocs"),
|
||||||
|
)
|
||||||
|
|
||||||
|
checked: list = []
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
self.checked.append(list(self.results))
|
||||||
|
|
||||||
|
|
||||||
|
class BackupOnlyModule(MVTModule):
|
||||||
|
"""A custom module which does not declare check-iocs."""
|
||||||
|
|
||||||
|
slug = "backup_only"
|
||||||
|
supported_commands = (("ios", "check-backup"),)
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
raise AssertionError("must not be re-checked")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"platform, builtin_modules",
|
||||||
|
[("ios", IOS_CHECK_IOCS_MODULES), ("android", ANDROID_CHECK_IOCS_MODULES)],
|
||||||
|
)
|
||||||
|
def test_check_iocs_rechecks_the_stored_results_of_custom_modules(
|
||||||
|
platform, builtin_modules, tmp_path, caplog
|
||||||
|
):
|
||||||
|
# check-iocs matches every <slug>.json in the results folder to the module
|
||||||
|
# with that slug, custom modules included, and runs its check_indicators()
|
||||||
|
# again over the stored results.
|
||||||
|
results = [{"domain": "example.org"}]
|
||||||
|
(tmp_path / "custom_results.json").write_text(json.dumps(results))
|
||||||
|
(tmp_path / "backup_only.json").write_text(json.dumps(results))
|
||||||
|
CustomResultsModule.checked.clear()
|
||||||
|
|
||||||
|
cmd = CmdCheckIOCS(
|
||||||
|
target_path=str(tmp_path),
|
||||||
|
custom_modules=[CustomResultsModule, BackupOnlyModule],
|
||||||
|
platform=platform,
|
||||||
|
)
|
||||||
|
cmd.modules = builtin_modules
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
assert CustomResultsModule.checked == [results]
|
||||||
|
assert (
|
||||||
|
'Loading results from "custom_results.json" with module CustomResultsModule'
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
# A module declaring only check-backup is not part of check-iocs.
|
||||||
|
assert "backup_only.json" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_iocs_lists_custom_modules_declaring_the_command(caplog):
|
||||||
|
cmd = CmdCheckIOCS(
|
||||||
|
custom_modules=[CustomResultsModule, BackupOnlyModule],
|
||||||
|
platform="ios",
|
||||||
|
)
|
||||||
|
cmd.modules = IOS_CHECK_IOCS_MODULES
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
cmd.list_modules()
|
||||||
|
|
||||||
|
assert "CustomResultsModule" in caplog.text
|
||||||
|
assert "BackupOnlyModule" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
LOADED_MODULE = '''
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
|
||||||
|
class LoadedResultsModule(MVTModule):
|
||||||
|
"""A module loaded from a file with --load-module."""
|
||||||
|
|
||||||
|
slug = "loaded_results"
|
||||||
|
supported_commands = (("ios", "check-iocs"), ("android", "check-iocs"))
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
self.log.warning("loaded module checked %d results", len(self.results))
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("cli", [ios_cli, android_cli], ids=["mvt-ios", "mvt-android"])
|
||||||
|
def test_check_iocs_loads_custom_modules_from_a_file_on_each_cli(cli, tmp_path, caplog):
|
||||||
|
module_path = tmp_path / "loaded_module.py"
|
||||||
|
module_path.write_text(LOADED_MODULE)
|
||||||
|
results_folder = tmp_path / "results"
|
||||||
|
results_folder.mkdir()
|
||||||
|
(results_folder / "loaded_results.json").write_text(json.dumps([{"a": 1}]))
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
*OFFLINE,
|
||||||
|
"check-iocs",
|
||||||
|
"--load-module",
|
||||||
|
str(module_path),
|
||||||
|
str(results_folder),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert "loaded module checked 1 results" in caplog.text
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
from mvt.android.command_modules import ANDROID_CHECK_IOCS_MODULES
|
||||||
|
from mvt.android.modules.androidqf import ANDROIDQF_MODULES
|
||||||
|
from mvt.android.modules.backup import BACKUP_MODULES as ANDROID_BACKUP_MODULES
|
||||||
|
from mvt.android.modules.bugreport import BUGREPORT_MODULES
|
||||||
|
from mvt.android.modules.intrusion_logs import INTRUSION_LOGS_MODULES
|
||||||
|
from mvt.ios.command_modules import IOS_CHECK_IOCS_MODULES
|
||||||
|
from mvt.ios.modules.backup import BACKUP_MODULES as IOS_BACKUP_MODULES
|
||||||
|
from mvt.ios.modules.fs import FS_MODULES
|
||||||
|
from mvt.ios.modules.mixed import MIXED_MODULES
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_check_iocs_lists_are_the_families_of_their_platform():
|
||||||
|
# The CLI reads these same lists, so nothing composing one elsewhere can
|
||||||
|
# drift from what the command runs. This pins what the lists are composed
|
||||||
|
# of.
|
||||||
|
assert IOS_CHECK_IOCS_MODULES == IOS_BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
||||||
|
assert ANDROID_CHECK_IOCS_MODULES == (
|
||||||
|
ANDROID_BACKUP_MODULES
|
||||||
|
+ BUGREPORT_MODULES
|
||||||
|
+ ANDROIDQF_MODULES
|
||||||
|
+ INTRUSION_LOGS_MODULES
|
||||||
|
)
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from mvt.common.cli_plugins import _module_name_for_path as _command_module_name
|
||||||
from mvt.common.module import MVTModule
|
from mvt.common.module import MVTModule
|
||||||
from mvt.common.module_loader import (
|
from mvt.common.module_loader import (
|
||||||
CustomModuleLoadError,
|
CustomModuleLoadError,
|
||||||
|
_module_name_for_path,
|
||||||
get_module_logger,
|
get_module_logger,
|
||||||
|
get_plugin_logger,
|
||||||
load_custom_modules,
|
load_custom_modules,
|
||||||
load_custom_modules_from_path,
|
load_custom_modules_from_path,
|
||||||
module_supports_command,
|
module_supports_command,
|
||||||
@@ -168,9 +173,9 @@ def test_get_module_logger_strips_the_plugin_package_prefix():
|
|||||||
class PluginModule(MVTModule):
|
class PluginModule(MVTModule):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
PluginModule.__module__ = "mvt_plugin_amnesty_custom.ios.custom"
|
PluginModule.__module__ = "mvt_plugin_example_org.ios.custom"
|
||||||
|
|
||||||
assert get_module_logger(PluginModule).name == "mvt.ext.amnesty_custom.ios.custom"
|
assert get_module_logger(PluginModule).name == "mvt.ext.example_org.ios.custom"
|
||||||
|
|
||||||
|
|
||||||
def test_get_module_logger_only_strips_the_prefix_from_the_top_level():
|
def test_get_module_logger_only_strips_the_prefix_from_the_top_level():
|
||||||
@@ -187,3 +192,25 @@ def test_get_module_logger_names_path_modules_after_their_file(tmp_path):
|
|||||||
module = load_custom_modules_from_path(str(module_path))[0]
|
module = load_custom_modules_from_path(str(module_path))[0]
|
||||||
|
|
||||||
assert get_module_logger(module).name == "mvt.ext.my_custom_module"
|
assert get_module_logger(module).name == "mvt.ext.my_custom_module"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_plugin_logger_uses_the_same_namespace_as_modules():
|
||||||
|
assert (
|
||||||
|
get_plugin_logger("mvt_plugin_example_org.commands.summarize").name
|
||||||
|
== "mvt.ext.example_org.commands.summarize"
|
||||||
|
)
|
||||||
|
assert get_plugin_logger("example_plugin.cli").name == "mvt.ext.example_plugin.cli"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_plugin_logger_keeps_builtin_names():
|
||||||
|
assert get_plugin_logger("mvt.ios.cli").name == "mvt.ios.cli"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_plugin_logger_names_loaded_files_after_the_file():
|
||||||
|
# A file loaded with --load-command or --load-module is imported under a
|
||||||
|
# mangled name. The log names the file instead.
|
||||||
|
command_name = _command_module_name(Path("/tmp/case_summary.py"))
|
||||||
|
module_name = _module_name_for_path(Path("/tmp/my_custom_module.py"))
|
||||||
|
|
||||||
|
assert get_plugin_logger(command_name).name == "mvt.ext.case_summary"
|
||||||
|
assert get_plugin_logger(module_name).name == "mvt.ext.my_custom_module"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
import mvt.plugin
|
||||||
|
from mvt.android.modules.backup.base import BackupModule
|
||||||
|
from mvt.common.config import settings
|
||||||
|
|
||||||
|
from ..plugin_fixtures import run_isolated_python
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_exported_names_are_the_public_names():
|
||||||
|
public = {name for name in vars(mvt.plugin) if not name.startswith("_")}
|
||||||
|
|
||||||
|
assert public == set(mvt.plugin.__all__)
|
||||||
|
assert mvt.plugin.settings is settings
|
||||||
|
assert mvt.plugin.AndroidBackupModule is BackupModule
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_surface_imports_before_anything_else_of_mvt(tmp_path):
|
||||||
|
# A plugin can import the surface as its first import of MVT. The
|
||||||
|
# subprocess gets a temporary home because importing MVT writes its
|
||||||
|
# configuration file.
|
||||||
|
result = run_isolated_python(
|
||||||
|
"from mvt.plugin import IOSExtraction, MVT_VERSION, settings\n"
|
||||||
|
"assert MVT_VERSION\n"
|
||||||
|
"assert settings.NETWORK_TIMEOUT > 0\n"
|
||||||
|
"assert IOSExtraction.__name__ == 'IOSExtraction'\n",
|
||||||
|
home=tmp_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stderr == ""
|
||||||
@@ -18,6 +18,7 @@ from mvt.common.utils import (
|
|||||||
generate_hashes_from_path,
|
generate_hashes_from_path,
|
||||||
get_sha256_from_file_path,
|
get_sha256_from_file_path,
|
||||||
init_logging,
|
init_logging,
|
||||||
|
set_verbose_logging,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils import get_artifact_folder
|
from ..utils import get_artifact_folder
|
||||||
@@ -122,3 +123,29 @@ class TestInitLogging:
|
|||||||
sum(isinstance(handler, MVTLogHandler) for handler in log.handlers)
|
sum(isinstance(handler, MVTLogHandler) for handler in log.handlers)
|
||||||
== handler_count
|
== handler_count
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_verbose_logging_finds_the_console_handler_among_others(self):
|
||||||
|
# Something else may have attached a handler to the "mvt" logger
|
||||||
|
# before MVT did, so the console handler is not always the first.
|
||||||
|
log = logging.getLogger("mvt")
|
||||||
|
init_logging()
|
||||||
|
foreign_handler = logging.NullHandler()
|
||||||
|
foreign_handler.setLevel(logging.CRITICAL)
|
||||||
|
log.handlers.insert(0, foreign_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
set_verbose_logging(True)
|
||||||
|
console_handlers = [
|
||||||
|
handler
|
||||||
|
for handler in log.handlers
|
||||||
|
if isinstance(handler, MVTLogHandler)
|
||||||
|
]
|
||||||
|
assert console_handlers
|
||||||
|
assert all(handler.level == logging.DEBUG for handler in console_handlers)
|
||||||
|
assert foreign_handler.level == logging.CRITICAL
|
||||||
|
|
||||||
|
set_verbose_logging(False)
|
||||||
|
assert all(handler.level == logging.INFO for handler in console_handlers)
|
||||||
|
assert foreign_handler.level == logging.CRITICAL
|
||||||
|
finally:
|
||||||
|
log.handlers.remove(foreign_handler)
|
||||||
@@ -73,6 +73,7 @@ def run_isolated_python(
|
|||||||
}
|
}
|
||||||
isolated_environment["HOME"] = str(home)
|
isolated_environment["HOME"] = str(home)
|
||||||
isolated_environment["XDG_CONFIG_HOME"] = str(home / "config")
|
isolated_environment["XDG_CONFIG_HOME"] = str(home / "config")
|
||||||
|
isolated_environment["XDG_DATA_HOME"] = str(home / "data")
|
||||||
if site_path is not None:
|
if site_path is not None:
|
||||||
isolated_environment["PYTHONPATH"] = str(site_path)
|
isolated_environment["PYTHONPATH"] = str(site_path)
|
||||||
isolated_environment.update(environment)
|
isolated_environment.update(environment)
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from mvt.android.cli import cli as android_cli
|
||||||
|
from mvt.cli import cli as mvt_cli
|
||||||
|
from mvt.common.log import MVTLogHandler
|
||||||
|
from mvt.common.utils import set_verbose_logging
|
||||||
|
from mvt.ios.cli import cli as ios_cli
|
||||||
|
|
||||||
|
# Keep the banner of the group callback from checking for updates online.
|
||||||
|
OFFLINE = ["--disable-update-check", "--disable-indicator-update-check"]
|
||||||
|
|
||||||
|
PROGRAMS = {"mvt": mvt_cli, "mvt-ios": ios_cli, "mvt-android": android_cli}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reset_console_level():
|
||||||
|
"""Leave the console handler at its default level after every test."""
|
||||||
|
yield
|
||||||
|
set_verbose_logging(False)
|
||||||
|
|
||||||
|
|
||||||
|
def _console_level():
|
||||||
|
"""Return the level of MVT's own console log handler."""
|
||||||
|
for handler in logging.getLogger("mvt").handlers:
|
||||||
|
if isinstance(handler, MVTLogHandler):
|
||||||
|
return handler.level
|
||||||
|
raise AssertionError("MVT has no console log handler")
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerboseOnTheCommands:
|
||||||
|
@pytest.mark.parametrize("program", sorted(PROGRAMS))
|
||||||
|
def test_verbose_before_the_command_name_turns_on_debug(self, program):
|
||||||
|
cli = PROGRAMS[program]
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli, [*OFFLINE, "--verbose", "version"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("program", sorted(PROGRAMS))
|
||||||
|
def test_a_run_without_verbose_goes_back_to_info(self, program):
|
||||||
|
cli = PROGRAMS[program]
|
||||||
|
CliRunner().invoke(cli, [*OFFLINE, "--verbose", "version"])
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli, [*OFFLINE, "version"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.INFO
|
||||||
|
|
||||||
|
def test_mvt_verbose_without_a_command_prints_the_help(self):
|
||||||
|
result = CliRunner().invoke(mvt_cli, [*OFFLINE, "--verbose"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Usage:" in result.output
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerboseOnTheCheckCommands:
|
||||||
|
def test_ios_command_default_does_not_undo_the_cli_choice(self, tmp_path):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
ios_cli,
|
||||||
|
[*OFFLINE, "--verbose", "check-backup", "--list-modules", str(tmp_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
def test_ios_verbose_after_the_command_name_still_works(self, tmp_path):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
ios_cli,
|
||||||
|
[*OFFLINE, "check-backup", "--verbose", "--list-modules", str(tmp_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
def test_android_command_default_does_not_undo_the_cli_choice(self, tmp_path):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
android_cli,
|
||||||
|
[*OFFLINE, "--verbose", "check-bugreport", "--list-modules", str(tmp_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
def test_android_verbose_after_the_command_name_still_works(self, tmp_path):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
android_cli,
|
||||||
|
[*OFFLINE, "check-bugreport", "--verbose", "--list-modules", str(tmp_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert _console_level() == logging.DEBUG
|
||||||
|
|
||||||
|
def test_the_command_option_says_it_is_kept_for_compatibility(self):
|
||||||
|
result = CliRunner().invoke(ios_cli, [*OFFLINE, "check-backup", "--help"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "kept for compatibility" in result.output
|
||||||
Reference in new issue
Block a user