Skip to content

Commit 2689176

Browse files
authored
Add custom module loading (#816)
1 parent 638937e commit 2689176

18 files changed

Lines changed: 793 additions & 10 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ For alternative installation options and known issues, please refer to the [docu
6060

6161
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
6262

63+
Module-running `check-*` commands can load custom Python modules with
64+
`--load-module PATH` or from a folder set in `MVT_CUSTOM_MODULES`. See the
65+
[development documentation](https://docs.mvt.re/en/latest/development/) for
66+
details.
67+
6368

6469
## License
6570

docs/development.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,82 @@ Selecting a single module also runs its transitive dependencies. If a dependency
3939
is unavailable or the dependency graph contains a cycle, the command logs a
4040
warning and does not run any modules.
4141

42+
## Custom modules
43+
44+
Module-running `check-*` commands can load custom modules from Python files that
45+
are not installed as part of MVT. Load one file with:
46+
47+
```bash
48+
mvt-ios check-backup --load-module ./example_module.py --output ./out ./backup
49+
```
50+
51+
You can also load a folder. MVT loads non-hidden top-level `*.py` files in
52+
sorted order and skips `__init__.py`:
53+
54+
```bash
55+
mvt-ios check-fs --load-module ./custom_modules ./filesystem-dump
56+
```
57+
58+
Set `MVT_CUSTOM_MODULES` to load a folder for every module-running command. This
59+
folder is loaded before any `--load-module` path:
60+
61+
```bash
62+
MVT_CUSTOM_MODULES=./custom_modules mvt-android check-bugreport ./bugreport.zip
63+
```
64+
65+
Custom modules are normal `MVTModule` subclasses:
66+
67+
```python
68+
from mvt.common.module import MVTModule
69+
70+
71+
class ExampleCustomModule(MVTModule):
72+
supported_commands = (("ios", "check-backup"), ("ios", "check-fs"))
73+
slug = "example_custom_module"
74+
75+
def run(self):
76+
self.results = [{"message": "custom module ran"}]
77+
78+
def check_indicators(self):
79+
pass
80+
81+
def serialize(self, result):
82+
return None
83+
```
84+
85+
Use `supported_commands` to restrict a module to specific platform/command
86+
pairs. Missing or empty `supported_commands` means the module is available to
87+
all commands, which keeps older modules compatible. Supported pairs are:
88+
89+
```python
90+
("ios", "check-backup")
91+
("ios", "check-fs")
92+
("ios", "check-iocs")
93+
("android", "check-backup")
94+
("android", "check-bugreport")
95+
("android", "check-androidqf")
96+
("android", "check-intrusion-logs")
97+
("android", "check-iocs")
98+
```
99+
100+
Custom modules can depend on existing MVT module classes. Dependencies are
101+
resolved with the same ordering logic as built-in modules, and custom modules
102+
are appended after built-ins before ordering:
103+
104+
```python
105+
from mvt.common.module import MVTModule
106+
from mvt.ios.modules.backup.manifest import Manifest
107+
108+
109+
class DependentCustomModule(MVTModule):
110+
supported_commands = (("ios", "check-backup"),)
111+
dependencies = (Manifest,)
112+
113+
def run(self):
114+
manifest_results = self.get_dependency_results(Manifest)
115+
self.results = [{"manifest_entries": len(manifest_results)}]
116+
```
117+
42118
## Profiling
43119

44120
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be

src/mvt/android/cli.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
HELP_MSG_HASHES,
2323
HELP_MSG_IOC,
2424
HELP_MSG_LIST_MODULES,
25+
HELP_MSG_LOAD_MODULE,
2526
HELP_MSG_MODULE,
2627
HELP_MSG_NONINTERACTIVE,
2728
HELP_MSG_OUTPUT,
@@ -30,6 +31,7 @@
3031
HELP_MSG_VERSION,
3132
)
3233
from mvt.common.logo import logo
34+
from mvt.common.module_loader import CustomModuleLoadError, load_custom_modules
3335
from mvt.common.updates import IndicatorsUpdates
3436
from mvt.common.utils import init_logging, set_verbose_logging
3537

@@ -59,6 +61,13 @@ def _get_disable_flags(ctx):
5961
)
6062

6163

64+
def _load_custom_modules(load_module):
65+
try:
66+
return load_custom_modules(load_module)
67+
except CustomModuleLoadError as exc:
68+
raise click.ClickException(str(exc)) from exc
69+
70+
6271
# ==============================================================================
6372
# Main
6473
# ==============================================================================
@@ -119,11 +128,28 @@ def check_adb(ctx):
119128
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
120129
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
121130
@click.option("--module", "-m", help=HELP_MSG_MODULE)
131+
@click.option(
132+
"--load-module",
133+
type=click.Path(exists=True),
134+
multiple=True,
135+
default=[],
136+
help=HELP_MSG_LOAD_MODULE,
137+
)
122138
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
123139
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
124140
@click.pass_context
125-
def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_path):
141+
def check_bugreport(
142+
ctx,
143+
iocs,
144+
output,
145+
list_modules,
146+
module,
147+
load_module,
148+
verbose,
149+
bugreport_path,
150+
):
126151
set_verbose_logging(verbose)
152+
custom_modules = _load_custom_modules(load_module)
127153
# Always generate hashes as bug reports are small.
128154
cmd = CmdAndroidCheckBugreport(
129155
target_path=bugreport_path,
@@ -133,6 +159,7 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
133159
hashes=True,
134160
disable_version_check=_get_disable_flags(ctx)[0],
135161
disable_indicator_check=_get_disable_flags(ctx)[1],
162+
custom_modules=custom_modules,
136163
)
137164

138165
if list_modules:
@@ -164,6 +191,13 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
164191
)
165192
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
166193
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
194+
@click.option(
195+
"--load-module",
196+
type=click.Path(exists=True),
197+
multiple=True,
198+
default=[],
199+
help=HELP_MSG_LOAD_MODULE,
200+
)
167201
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
168202
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
169203
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@@ -174,12 +208,14 @@ def check_backup(
174208
iocs,
175209
output,
176210
list_modules,
211+
load_module,
177212
non_interactive,
178213
backup_password,
179214
verbose,
180215
backup_path,
181216
):
182217
set_verbose_logging(verbose)
218+
custom_modules = _load_custom_modules(load_module)
183219

184220
# Always generate hashes as backups are generally small.
185221
cmd = CmdAndroidCheckBackup(
@@ -193,6 +229,7 @@ def check_backup(
193229
},
194230
disable_version_check=_get_disable_flags(ctx)[0],
195231
disable_indicator_check=_get_disable_flags(ctx)[1],
232+
custom_modules=custom_modules,
196233
)
197234

198235
if list_modules:
@@ -223,6 +260,13 @@ def check_backup(
223260
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
224261
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
225262
@click.option("--module", "-m", help=HELP_MSG_MODULE)
263+
@click.option(
264+
"--load-module",
265+
type=click.Path(exists=True),
266+
multiple=True,
267+
default=[],
268+
help=HELP_MSG_LOAD_MODULE,
269+
)
226270
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
227271
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
228272
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@@ -235,13 +279,15 @@ def check_androidqf(
235279
output,
236280
list_modules,
237281
module,
282+
load_module,
238283
hashes,
239284
non_interactive,
240285
backup_password,
241286
verbose,
242287
androidqf_path,
243288
):
244289
set_verbose_logging(verbose)
290+
custom_modules = _load_custom_modules(load_module)
245291

246292
cmd = CmdAndroidCheckAndroidQF(
247293
target_path=androidqf_path,
@@ -255,6 +301,7 @@ def check_androidqf(
255301
},
256302
disable_version_check=_get_disable_flags(ctx)[0],
257303
disable_indicator_check=_get_disable_flags(ctx)[1],
304+
custom_modules=custom_modules,
258305
)
259306

260307
if list_modules:
@@ -288,6 +335,13 @@ def check_androidqf(
288335
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
289336
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
290337
@click.option("--module", "-m", help=HELP_MSG_MODULE)
338+
@click.option(
339+
"--load-module",
340+
type=click.Path(exists=True),
341+
multiple=True,
342+
default=[],
343+
help=HELP_MSG_LOAD_MODULE,
344+
)
291345
@click.option(
292346
"--timezone",
293347
"-t",
@@ -307,11 +361,13 @@ def check_intrusion_logs(
307361
output,
308362
list_modules,
309363
module,
364+
load_module,
310365
timezone,
311366
verbose,
312367
logs_path,
313368
):
314369
set_verbose_logging(verbose)
370+
custom_modules = _load_custom_modules(load_module)
315371

316372
module_options = {}
317373
if timezone:
@@ -325,6 +381,7 @@ def check_intrusion_logs(
325381
module_options=module_options if module_options else None,
326382
disable_version_check=_get_disable_flags(ctx)[0],
327383
disable_indicator_check=_get_disable_flags(ctx)[1],
384+
custom_modules=custom_modules,
328385
)
329386

330387
if list_modules:
@@ -352,15 +409,25 @@ def check_intrusion_logs(
352409
)
353410
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
354411
@click.option("--module", "-m", help=HELP_MSG_MODULE)
412+
@click.option(
413+
"--load-module",
414+
type=click.Path(exists=True),
415+
multiple=True,
416+
default=[],
417+
help=HELP_MSG_LOAD_MODULE,
418+
)
355419
@click.argument("FOLDER", type=click.Path(exists=True))
356420
@click.pass_context
357-
def check_iocs(ctx, iocs, list_modules, module, folder):
421+
def check_iocs(ctx, iocs, list_modules, module, load_module, folder):
422+
custom_modules = _load_custom_modules(load_module)
358423
cmd = CmdCheckIOCS(
359424
target_path=folder,
360425
ioc_files=iocs,
361426
module_name=module,
362427
disable_version_check=_get_disable_flags(ctx)[0],
363428
disable_indicator_check=_get_disable_flags(ctx)[1],
429+
custom_modules=custom_modules,
430+
platform="android",
364431
)
365432
cmd.modules = (
366433
BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES + INTRUSION_LOGS_MODULES

src/mvt/android/cmd_check_androidqf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
1818
from mvt.common.command import Command
1919
from mvt.common.indicators import Indicators
20+
from mvt.common.module import MVTModule
2021

2122
from .modules.androidqf import ANDROIDQF_MODULES
2223
from .modules.androidqf.base import AndroidQFModule
@@ -50,6 +51,7 @@ def __init__(
5051
sub_command: Optional[bool] = False,
5152
disable_version_check: bool = False,
5253
disable_indicator_check: bool = False,
54+
custom_modules: Optional[list[type[MVTModule]]] = None,
5355
) -> None:
5456
super().__init__(
5557
target_path=target_path,
@@ -64,8 +66,10 @@ def __init__(
6466
log=log,
6567
disable_version_check=disable_version_check,
6668
disable_indicator_check=disable_indicator_check,
69+
custom_modules=custom_modules,
6770
)
6871

72+
self.platform = "android"
6973
self.name = "check-androidqf"
7074
self.modules = ANDROIDQF_MODULES
7175

@@ -210,6 +214,7 @@ def run_bugreport_cmd(self) -> bool:
210214
module_options=self.module_options,
211215
hashes=self.hashes,
212216
sub_command=True,
217+
custom_modules=self.custom_modules,
213218
)
214219
cmd.from_zip(bugreport)
215220
cmd.run()
@@ -239,6 +244,7 @@ def run_backup_cmd(self) -> bool:
239244
module_options=self.module_options,
240245
hashes=self.hashes,
241246
sub_command=True,
247+
custom_modules=self.custom_modules,
242248
)
243249
try:
244250
cmd.from_ab(backup)
@@ -318,6 +324,7 @@ def run_intrusion_logs_cmd(self) -> bool:
318324
module_options=adv_module_options,
319325
hashes=self.hashes,
320326
sub_command=True,
327+
custom_modules=self.custom_modules,
321328
)
322329
cmd.run()
323330

src/mvt/android/cmd_check_backup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from mvt.common.command import Command
2323
from mvt.common.indicators import Indicators
24+
from mvt.common.module import MVTModule
2425

2526
from .modules.backup import BACKUP_MODULES
2627

@@ -45,6 +46,7 @@ def __init__(
4546
sub_command: Optional[bool] = False,
4647
disable_version_check: bool = False,
4748
disable_indicator_check: bool = False,
49+
custom_modules: Optional[list[type[MVTModule]]] = None,
4850
) -> None:
4951
super().__init__(
5052
target_path=target_path,
@@ -59,8 +61,10 @@ def __init__(
5961
log=log,
6062
disable_version_check=disable_version_check,
6163
disable_indicator_check=disable_indicator_check,
64+
custom_modules=custom_modules,
6265
)
6366

67+
self.platform = "android"
6468
self.name = "check-backup"
6569
self.modules = BACKUP_MODULES
6670

0 commit comments

Comments
 (0)