mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-31 18:24:10 +01:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57179a62c | ||
|
|
2689176c0e | ||
|
|
f95f7b1283 |
No files matched your search
@@ -60,6 +60,28 @@ For alternative installation options and known issues, please refer to the [docu
|
|||||||
|
|
||||||
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
|
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
|
||||||
|
|
||||||
|
### Shell completion
|
||||||
|
|
||||||
|
MVT can generate shell completion scripts for Bash, Zsh, and Fish:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvt-ios completion
|
||||||
|
mvt-android completion
|
||||||
|
```
|
||||||
|
|
||||||
|
The commands print setup instructions by default. To generate a completion script directly, pass the shell name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvt-ios completion bash
|
||||||
|
mvt-android completion zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
MVT only writes completion files or shell configuration when `--install` is passed. See the [command completion documentation](https://docs.mvt.re/en/latest/command_completion/) for details.
|
||||||
|
Module-running `check-*` commands can load custom Python modules with
|
||||||
|
`--load-module PATH` or from a folder set in `MVT_CUSTOM_MODULES`. See the
|
||||||
|
[development documentation](https://docs.mvt.re/en/latest/development/) for
|
||||||
|
details.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+33
-10
@@ -4,7 +4,7 @@ MVT utilizes the [Click](https://click.palletsprojects.com/en/stable/) library f
|
|||||||
|
|
||||||
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
|
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
|
||||||
|
|
||||||
To enable it, you need to manually register a special function with your shell, which varies depending on the shell you are using.
|
To enable it, you need to register a completion script with your shell, which varies depending on the shell you are using.
|
||||||
|
|
||||||
The following describes how to generate the command completion scripts and add them to your shell configuration.
|
The following describes how to generate the command completion scripts and add them to your shell configuration.
|
||||||
|
|
||||||
@@ -13,31 +13,54 @@ The following describes how to generate the command completion scripts and add t
|
|||||||
### For Bash
|
### For Bash
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generates bash completion scripts
|
# Generate bash completion scripts
|
||||||
echo "$(_MVT_IOS_COMPLETE=bash_source mvt-ios)" > ~/.mvt-ios-complete.bash &&
|
mvt-ios completion bash > ~/.mvt-ios-complete.bash
|
||||||
echo "$(_MVT_ANDROID_COMPLETE=bash_source mvt-android)" > ~/.mvt-android-complete.bash
|
mvt-android completion bash > ~/.mvt-android-complete.bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the following to `~/.bashrc`:
|
Add the following to `~/.bashrc`:
|
||||||
```bash
|
```bash
|
||||||
# source mvt completion scripts
|
# source mvt completion scripts
|
||||||
. ~/.mvt-ios-complete.bash && . ~/.mvt-android-complete.bash
|
[ -f ~/.mvt-ios-complete.bash ] && . ~/.mvt-ios-complete.bash
|
||||||
|
[ -f ~/.mvt-android-complete.bash ] && . ~/.mvt-android-complete.bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Zsh
|
### For Zsh
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generates zsh completion scripts
|
# Generate zsh completion scripts
|
||||||
echo "$(_MVT_IOS_COMPLETE=zsh_source mvt-ios)" > ~/.mvt-ios-complete.zsh &&
|
mvt-ios completion zsh > ~/.mvt-ios-complete.zsh
|
||||||
echo "$(_MVT_ANDROID_COMPLETE=zsh_source mvt-android)" > ~/.mvt-android-complete.zsh
|
mvt-android completion zsh > ~/.mvt-android-complete.zsh
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the following to `~/.zshrc`:
|
Add the following to `~/.zshrc`:
|
||||||
```bash
|
```bash
|
||||||
# source mvt completion scripts
|
# source mvt completion scripts
|
||||||
. ~/.mvt-ios-complete.zsh && . ~/.mvt-android-complete.zsh
|
[ -f ~/.mvt-ios-complete.zsh ] && . ~/.mvt-ios-complete.zsh
|
||||||
|
[ -f ~/.mvt-android-complete.zsh ] && . ~/.mvt-android-complete.zsh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### For Fish
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate fish completion scripts
|
||||||
|
mkdir -p ~/.config/fish/completions
|
||||||
|
mvt-ios completion fish > ~/.config/fish/completions/mvt-ios.fish
|
||||||
|
mvt-android completion fish > ~/.config/fish/completions/mvt-android.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
Fish loads completion files from `~/.config/fish/completions` automatically.
|
||||||
|
|
||||||
|
### Automatic Installation
|
||||||
|
|
||||||
|
MVT can write the completion file and update the relevant shell configuration for Bash and Zsh when you pass `--install`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvt-ios completion bash --install
|
||||||
|
mvt-android completion bash --install
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `bash` with `zsh` or `fish` as needed. For Fish, `--install` writes the completion file into `~/.config/fish/completions`.
|
||||||
|
|
||||||
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).
|
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +39,82 @@ Selecting a single module also runs its transitive dependencies. If a dependency
|
|||||||
is unavailable or the dependency graph contains a cycle, the command logs a
|
is unavailable or the dependency graph contains a cycle, the command logs a
|
||||||
warning and does not run any modules.
|
warning and does not run any modules.
|
||||||
|
|
||||||
|
## Custom modules
|
||||||
|
|
||||||
|
Module-running `check-*` commands can load custom modules from Python files that
|
||||||
|
are not installed as part of MVT. Load one file with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvt-ios check-backup --load-module ./example_module.py --output ./out ./backup
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also load a folder. MVT loads non-hidden top-level `*.py` files in
|
||||||
|
sorted order and skips `__init__.py`:
|
||||||
|
|
||||||
|
```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 restrict a module to specific platform/command
|
||||||
|
pairs. Missing or empty `supported_commands` means the module is available to
|
||||||
|
all commands, which keeps older modules compatible. Supported pairs are:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("ios", "check-backup")
|
||||||
|
("ios", "check-fs")
|
||||||
|
("ios", "check-iocs")
|
||||||
|
("android", "check-backup")
|
||||||
|
("android", "check-bugreport")
|
||||||
|
("android", "check-androidqf")
|
||||||
|
("android", "check-intrusion-logs")
|
||||||
|
("android", "check-iocs")
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom modules can depend on existing MVT module classes. Dependencies are
|
||||||
|
resolved with the same ordering logic as built-in modules, and custom modules
|
||||||
|
are appended after built-ins before ordering:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
from mvt.ios.modules.backup.manifest import Manifest
|
||||||
|
|
||||||
|
|
||||||
|
class DependentCustomModule(MVTModule):
|
||||||
|
supported_commands = (("ios", "check-backup"),)
|
||||||
|
dependencies = (Manifest,)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
manifest_results = self.get_dependency_results(Manifest)
|
||||||
|
self.results = [{"manifest_entries": len(manifest_results)}]
|
||||||
|
```
|
||||||
|
|
||||||
## Profiling
|
## Profiling
|
||||||
|
|
||||||
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be
|
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be
|
||||||
|
|||||||
+111
-2
@@ -8,6 +8,12 @@ import logging
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||||
|
from mvt.common.completion import (
|
||||||
|
SUPPORTED_SHELLS,
|
||||||
|
completion_instructions,
|
||||||
|
generate_completion_script,
|
||||||
|
install_completion_script,
|
||||||
|
)
|
||||||
from mvt.common.help import (
|
from mvt.common.help import (
|
||||||
HELP_MSG_ANDROID_BACKUP_PASSWORD,
|
HELP_MSG_ANDROID_BACKUP_PASSWORD,
|
||||||
HELP_MSG_CHECK_ADB_REMOVED,
|
HELP_MSG_CHECK_ADB_REMOVED,
|
||||||
@@ -17,11 +23,13 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_CHECK_BUGREPORT,
|
HELP_MSG_CHECK_BUGREPORT,
|
||||||
HELP_MSG_CHECK_IOCS,
|
HELP_MSG_CHECK_IOCS,
|
||||||
HELP_MSG_CHECK_INTRUSION_LOGS,
|
HELP_MSG_CHECK_INTRUSION_LOGS,
|
||||||
|
HELP_MSG_COMPLETION,
|
||||||
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
HELP_MSG_DISABLE_UPDATE_CHECK,
|
HELP_MSG_DISABLE_UPDATE_CHECK,
|
||||||
HELP_MSG_HASHES,
|
HELP_MSG_HASHES,
|
||||||
HELP_MSG_IOC,
|
HELP_MSG_IOC,
|
||||||
HELP_MSG_LIST_MODULES,
|
HELP_MSG_LIST_MODULES,
|
||||||
|
HELP_MSG_LOAD_MODULE,
|
||||||
HELP_MSG_MODULE,
|
HELP_MSG_MODULE,
|
||||||
HELP_MSG_NONINTERACTIVE,
|
HELP_MSG_NONINTERACTIVE,
|
||||||
HELP_MSG_OUTPUT,
|
HELP_MSG_OUTPUT,
|
||||||
@@ -30,6 +38,7 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_VERSION,
|
HELP_MSG_VERSION,
|
||||||
)
|
)
|
||||||
from mvt.common.logo import logo
|
from mvt.common.logo import logo
|
||||||
|
from mvt.common.module_loader import CustomModuleLoadError, load_custom_modules
|
||||||
from mvt.common.updates import IndicatorsUpdates
|
from mvt.common.updates import IndicatorsUpdates
|
||||||
from mvt.common.utils import init_logging, set_verbose_logging
|
from mvt.common.utils import init_logging, set_verbose_logging
|
||||||
|
|
||||||
@@ -59,6 +68,13 @@ def _get_disable_flags(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_custom_modules(load_module):
|
||||||
|
try:
|
||||||
|
return load_custom_modules(load_module)
|
||||||
|
except CustomModuleLoadError as exc:
|
||||||
|
raise click.ClickException(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Main
|
# Main
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -76,6 +92,7 @@ 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
|
||||||
|
if ctx.invoked_subcommand != "completion":
|
||||||
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,
|
||||||
@@ -90,6 +107,40 @@ def version():
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Command: completion
|
||||||
|
# ==============================================================================
|
||||||
|
@cli.command("completion", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_COMPLETION)
|
||||||
|
@click.argument("shell", required=False, type=click.Choice(SUPPORTED_SHELLS))
|
||||||
|
@click.option(
|
||||||
|
"--install",
|
||||||
|
is_flag=True,
|
||||||
|
help="Write completion files and update shell configuration.",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def completion(ctx, shell, install):
|
||||||
|
program_name = "mvt-android"
|
||||||
|
|
||||||
|
if shell is None:
|
||||||
|
if install:
|
||||||
|
raise click.UsageError("A shell is required when using --install.")
|
||||||
|
click.echo(completion_instructions(program_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
root_cli = ctx.find_root().command
|
||||||
|
|
||||||
|
if install:
|
||||||
|
script_path = install_completion_script(root_cli, program_name, shell)
|
||||||
|
click.echo(f"Installed {shell} completion to {script_path}")
|
||||||
|
if shell in ("bash", "zsh"):
|
||||||
|
click.echo(f"Updated ~/.{shell}rc")
|
||||||
|
else:
|
||||||
|
click.echo("Fish loads completion files automatically.")
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(generate_completion_script(root_cli, program_name, shell))
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Command: check-adb (removed)
|
# Command: check-adb (removed)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -119,11 +170,28 @@ def check_adb(ctx):
|
|||||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
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)
|
||||||
@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(ctx, iocs, output, list_modules, module, verbose, bugreport_path):
|
def check_bugreport(
|
||||||
|
ctx,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
list_modules,
|
||||||
|
module,
|
||||||
|
load_module,
|
||||||
|
verbose,
|
||||||
|
bugreport_path,
|
||||||
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
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(
|
||||||
target_path=bugreport_path,
|
target_path=bugreport_path,
|
||||||
@@ -133,6 +201,7 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
|
|||||||
hashes=True,
|
hashes=True,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -164,6 +233,13 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
|
|||||||
)
|
)
|
||||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
|
)
|
||||||
@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)
|
||||||
@@ -174,12 +250,14 @@ def check_backup(
|
|||||||
iocs,
|
iocs,
|
||||||
output,
|
output,
|
||||||
list_modules,
|
list_modules,
|
||||||
|
load_module,
|
||||||
non_interactive,
|
non_interactive,
|
||||||
backup_password,
|
backup_password,
|
||||||
verbose,
|
verbose,
|
||||||
backup_path,
|
backup_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
# Always generate hashes as backups are generally small.
|
# Always generate hashes as backups are generally small.
|
||||||
cmd = CmdAndroidCheckBackup(
|
cmd = CmdAndroidCheckBackup(
|
||||||
@@ -193,6 +271,7 @@ def check_backup(
|
|||||||
},
|
},
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -223,6 +302,13 @@ def check_backup(
|
|||||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
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("--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)
|
||||||
@@ -235,6 +321,7 @@ def check_androidqf(
|
|||||||
output,
|
output,
|
||||||
list_modules,
|
list_modules,
|
||||||
module,
|
module,
|
||||||
|
load_module,
|
||||||
hashes,
|
hashes,
|
||||||
non_interactive,
|
non_interactive,
|
||||||
backup_password,
|
backup_password,
|
||||||
@@ -242,6 +329,7 @@ def check_androidqf(
|
|||||||
androidqf_path,
|
androidqf_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
cmd = CmdAndroidCheckAndroidQF(
|
cmd = CmdAndroidCheckAndroidQF(
|
||||||
target_path=androidqf_path,
|
target_path=androidqf_path,
|
||||||
@@ -255,6 +343,7 @@ def check_androidqf(
|
|||||||
},
|
},
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -288,6 +377,13 @@ def check_androidqf(
|
|||||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--timezone",
|
"--timezone",
|
||||||
"-t",
|
"-t",
|
||||||
@@ -307,11 +403,13 @@ def check_intrusion_logs(
|
|||||||
output,
|
output,
|
||||||
list_modules,
|
list_modules,
|
||||||
module,
|
module,
|
||||||
|
load_module,
|
||||||
timezone,
|
timezone,
|
||||||
verbose,
|
verbose,
|
||||||
logs_path,
|
logs_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
module_options = {}
|
module_options = {}
|
||||||
if timezone:
|
if timezone:
|
||||||
@@ -325,6 +423,7 @@ def check_intrusion_logs(
|
|||||||
module_options=module_options if module_options else None,
|
module_options=module_options if module_options else None,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -352,15 +451,25 @@ def check_intrusion_logs(
|
|||||||
)
|
)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
|
)
|
||||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
def check_iocs(ctx, iocs, list_modules, module, load_module, folder):
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
cmd = CmdCheckIOCS(
|
cmd = CmdCheckIOCS(
|
||||||
target_path=folder,
|
target_path=folder,
|
||||||
ioc_files=iocs,
|
ioc_files=iocs,
|
||||||
module_name=module,
|
module_name=module,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
|
platform="android",
|
||||||
)
|
)
|
||||||
cmd.modules = (
|
cmd.modules = (
|
||||||
BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES + INTRUSION_LOGS_MODULES
|
BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES + INTRUSION_LOGS_MODULES
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from mvt.android.cmd_check_backup import CmdAndroidCheckBackup, InvalidAndroidBa
|
|||||||
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
|
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.androidqf import ANDROIDQF_MODULES
|
from .modules.androidqf import ANDROIDQF_MODULES
|
||||||
from .modules.androidqf.base import AndroidQFModule
|
from .modules.androidqf.base import AndroidQFModule
|
||||||
@@ -50,6 +51,7 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
sub_command: Optional[bool] = False,
|
sub_command: Optional[bool] = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -64,8 +66,10 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "android"
|
||||||
self.name = "check-androidqf"
|
self.name = "check-androidqf"
|
||||||
self.modules = ANDROIDQF_MODULES
|
self.modules = ANDROIDQF_MODULES
|
||||||
|
|
||||||
@@ -210,6 +214,7 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
module_options=self.module_options,
|
module_options=self.module_options,
|
||||||
hashes=self.hashes,
|
hashes=self.hashes,
|
||||||
sub_command=True,
|
sub_command=True,
|
||||||
|
custom_modules=self.custom_modules,
|
||||||
)
|
)
|
||||||
cmd.from_zip(bugreport)
|
cmd.from_zip(bugreport)
|
||||||
cmd.run()
|
cmd.run()
|
||||||
@@ -239,6 +244,7 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
module_options=self.module_options,
|
module_options=self.module_options,
|
||||||
hashes=self.hashes,
|
hashes=self.hashes,
|
||||||
sub_command=True,
|
sub_command=True,
|
||||||
|
custom_modules=self.custom_modules,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
cmd.from_ab(backup)
|
cmd.from_ab(backup)
|
||||||
@@ -318,6 +324,7 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
module_options=adv_module_options,
|
module_options=adv_module_options,
|
||||||
hashes=self.hashes,
|
hashes=self.hashes,
|
||||||
sub_command=True,
|
sub_command=True,
|
||||||
|
custom_modules=self.custom_modules,
|
||||||
)
|
)
|
||||||
cmd.run()
|
cmd.run()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from mvt.android.parsers.backup import (
|
|||||||
)
|
)
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.backup import BACKUP_MODULES
|
from .modules.backup import BACKUP_MODULES
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
sub_command: Optional[bool] = False,
|
sub_command: Optional[bool] = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -59,8 +61,10 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "android"
|
||||||
self.name = "check-backup"
|
self.name = "check-backup"
|
||||||
self.modules = BACKUP_MODULES
|
self.modules = BACKUP_MODULES
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from zipfile import ZipFile
|
|||||||
from mvt.android.modules.bugreport.base import BugReportModule
|
from mvt.android.modules.bugreport.base import BugReportModule
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.bugreport import BUGREPORT_MODULES
|
from .modules.bugreport import BUGREPORT_MODULES
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
sub_command: Optional[bool] = False,
|
sub_command: Optional[bool] = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -46,8 +48,10 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "android"
|
||||||
self.name = "check-bugreport"
|
self.name = "check-bugreport"
|
||||||
self.modules = BUGREPORT_MODULES
|
self.modules = BUGREPORT_MODULES
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.intrusion_logs import (
|
from .modules.intrusion_logs import (
|
||||||
INTRUSION_LOGS_MODULES,
|
INTRUSION_LOGS_MODULES,
|
||||||
@@ -35,6 +36,7 @@ class CmdAndroidCheckIntrusionLogs(Command):
|
|||||||
sub_command: Optional[bool] = False,
|
sub_command: Optional[bool] = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -49,8 +51,10 @@ class CmdAndroidCheckIntrusionLogs(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "android"
|
||||||
self.name = "check-intrusion-logs"
|
self.name = "check-intrusion-logs"
|
||||||
self.modules = INTRUSION_LOGS_MODULES
|
self.modules = INTRUSION_LOGS_MODULES
|
||||||
self._all_events: dict[str, list[dict]] = {}
|
self._all_events: dict[str, list[dict]] = {}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import os
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
from mvt.common.utils import exec_or_profile
|
from mvt.common.utils import exec_or_profile
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -26,6 +27,8 @@ class CmdCheckIOCS(Command):
|
|||||||
sub_command: Optional[bool] = False,
|
sub_command: Optional[bool] = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
|
platform: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -39,14 +42,16 @@ class CmdCheckIOCS(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = platform
|
||||||
self.name = "check-iocs"
|
self.name = "check-iocs"
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
assert self.target_path is not None
|
assert self.target_path is not None
|
||||||
all_modules = []
|
all_modules = []
|
||||||
for entry in self.modules:
|
for entry in self._available_modules():
|
||||||
if entry not in all_modules:
|
if entry not in all_modules:
|
||||||
all_modules.append(entry)
|
all_modules.append(entry)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .alerts import AlertLevel, AlertStore
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
from .indicators import Indicators
|
from .indicators import Indicators
|
||||||
from .module import EncryptedBackupError, MVTModule, run_module, save_timeline
|
from .module import EncryptedBackupError, MVTModule, run_module, save_timeline
|
||||||
|
from .module_loader import module_supports_command
|
||||||
from .module_types import ModuleTimeline
|
from .module_types import ModuleTimeline
|
||||||
from .utils import (
|
from .utils import (
|
||||||
CustomJSONEncoder,
|
CustomJSONEncoder,
|
||||||
@@ -44,9 +45,12 @@ class Command:
|
|||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = ""
|
self.name = ""
|
||||||
|
self.platform = ""
|
||||||
self.modules: list[type[MVTModule]] = []
|
self.modules: list[type[MVTModule]] = []
|
||||||
|
self.custom_modules = custom_modules if custom_modules else []
|
||||||
|
|
||||||
self.target_path = target_path
|
self.target_path = target_path
|
||||||
self.results_path = results_path
|
self.results_path = results_path
|
||||||
@@ -199,9 +203,24 @@ class Command:
|
|||||||
|
|
||||||
def list_modules(self) -> None:
|
def list_modules(self) -> None:
|
||||||
self.log.info("Following is the list of available %s modules:", self.name)
|
self.log.info("Following is the list of available %s modules:", self.name)
|
||||||
for module in self.modules:
|
for module in self._available_modules():
|
||||||
self.log.info(" - %s", module.__name__)
|
self.log.info(" - %s", module.__name__)
|
||||||
|
|
||||||
|
def _available_modules(self) -> list[type[MVTModule]]:
|
||||||
|
modules = list(self.modules)
|
||||||
|
modules.extend(
|
||||||
|
module
|
||||||
|
for module in self.custom_modules
|
||||||
|
if module_supports_command(module, self.platform, self.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
deduplicated = []
|
||||||
|
for module in modules:
|
||||||
|
if module not in deduplicated:
|
||||||
|
deduplicated.append(module)
|
||||||
|
|
||||||
|
return deduplicated
|
||||||
|
|
||||||
def init(self) -> None:
|
def init(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -262,14 +281,15 @@ class Command:
|
|||||||
|
|
||||||
def _ordered_modules(self) -> Optional[list[type[MVTModule]]]:
|
def _ordered_modules(self) -> Optional[list[type[MVTModule]]]:
|
||||||
"""Return enabled modules in stable topological order."""
|
"""Return enabled modules in stable topological order."""
|
||||||
module_indexes = {module: index for index, module in enumerate(self.modules)}
|
modules = self._available_modules()
|
||||||
|
module_indexes = {module: index for index, module in enumerate(modules)}
|
||||||
|
|
||||||
if self.module_name:
|
if self.module_name:
|
||||||
selected = [
|
selected = [
|
||||||
module for module in self.modules if module.__name__ == self.module_name
|
module for module in modules if module.__name__ == self.module_name
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
selected = [module for module in self.modules if module.enabled]
|
selected = [module for module in modules if module.enabled]
|
||||||
|
|
||||||
required = set(selected)
|
required = set(selected)
|
||||||
pending = list(selected)
|
pending = list(selected)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 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 pathlib import Path
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
import click
|
||||||
|
from click.shell_completion import get_completion_class
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORTED_SHELLS = ("bash", "zsh", "fish")
|
||||||
|
|
||||||
|
|
||||||
|
def completion_instructions(program_name: str) -> str:
|
||||||
|
return f"""Shell completion for {program_name}
|
||||||
|
|
||||||
|
Print a completion script:
|
||||||
|
{program_name} completion bash > ~/.{program_name}-complete.bash
|
||||||
|
{program_name} completion zsh > ~/.{program_name}-complete.zsh
|
||||||
|
mkdir -p ~/.config/fish/completions
|
||||||
|
{program_name} completion fish > ~/.config/fish/completions/{program_name}.fish
|
||||||
|
|
||||||
|
Load the generated Bash script from ~/.bashrc:
|
||||||
|
[ -f ~/.{program_name}-complete.bash ] && . ~/.{program_name}-complete.bash
|
||||||
|
|
||||||
|
Load the generated Zsh script from ~/.zshrc:
|
||||||
|
[ -f ~/.{program_name}-complete.zsh ] && . ~/.{program_name}-complete.zsh
|
||||||
|
|
||||||
|
Fish loads completion files from ~/.config/fish/completions automatically.
|
||||||
|
|
||||||
|
To write these files and update Bash/Zsh shell configuration automatically:
|
||||||
|
{program_name} completion bash --install
|
||||||
|
{program_name} completion zsh --install
|
||||||
|
{program_name} completion fish --install
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_completion_script(cli: click.Command, program_name: str, shell: str) -> str:
|
||||||
|
completion_class = get_completion_class(shell)
|
||||||
|
if completion_class is None:
|
||||||
|
raise click.ClickException(f"Unsupported shell: {shell}")
|
||||||
|
|
||||||
|
complete_var = f"_{program_name.upper().replace('-', '_')}_COMPLETE"
|
||||||
|
return completion_class(cli, {}, program_name, complete_var).source()
|
||||||
|
|
||||||
|
|
||||||
|
def install_completion_script(
|
||||||
|
cli: click.Command,
|
||||||
|
program_name: str,
|
||||||
|
shell: str,
|
||||||
|
) -> Path:
|
||||||
|
script = generate_completion_script(cli, program_name, shell)
|
||||||
|
script_path = _completion_script_path(program_name, shell)
|
||||||
|
script_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
script_path.write_text(script, encoding="utf-8")
|
||||||
|
|
||||||
|
if shell in ("bash", "zsh"):
|
||||||
|
_install_shell_source_line(program_name, shell, script_path)
|
||||||
|
|
||||||
|
return script_path
|
||||||
|
|
||||||
|
|
||||||
|
def _completion_script_path(program_name: str, shell: str) -> Path:
|
||||||
|
home = Path.home()
|
||||||
|
|
||||||
|
if shell == "fish":
|
||||||
|
return home / ".config" / "fish" / "completions" / f"{program_name}.fish"
|
||||||
|
|
||||||
|
return home / f".{program_name}-complete.{shell}"
|
||||||
|
|
||||||
|
|
||||||
|
def _install_shell_source_line(program_name: str, shell: str, script_path: Path) -> None:
|
||||||
|
shell_config_path = Path.home() / f".{shell}rc"
|
||||||
|
source_line = (
|
||||||
|
f"[ -f {shlex.quote(str(script_path))} ] && "
|
||||||
|
f". {shlex.quote(str(script_path))}"
|
||||||
|
)
|
||||||
|
block = (
|
||||||
|
f"# MVT shell completion for {program_name}\n"
|
||||||
|
f"{source_line}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if shell_config_path.exists():
|
||||||
|
shell_config = shell_config_path.read_text(encoding="utf-8")
|
||||||
|
if source_line in shell_config:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
shell_config = ""
|
||||||
|
|
||||||
|
separator = "" if not shell_config or shell_config.endswith("\n") else "\n"
|
||||||
|
with shell_config_path.open("a", encoding="utf-8") as handle:
|
||||||
|
handle.write(f"{separator}{block}")
|
||||||
@@ -10,6 +10,10 @@ HELP_MSG_IOC = "Path to indicators file (can be invoked multiple time)"
|
|||||||
HELP_MSG_FAST = "Avoid running time/resource consuming features"
|
HELP_MSG_FAST = "Avoid running time/resource consuming features"
|
||||||
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
|
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
|
||||||
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
|
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
|
||||||
|
HELP_MSG_LOAD_MODULE = (
|
||||||
|
"Load custom MVT module(s) from a Python file or folder "
|
||||||
|
"(can be invoked multiple times)"
|
||||||
|
)
|
||||||
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"
|
||||||
@@ -17,6 +21,7 @@ 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"
|
||||||
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK = "Disable indicators update check"
|
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK = "Disable indicators update check"
|
||||||
|
HELP_MSG_COMPLETION = "Generate or install shell completion"
|
||||||
|
|
||||||
# IOS Specific
|
# IOS Specific
|
||||||
HELP_MSG_DECRYPT_BACKUP = "Decrypt an encrypted iTunes backup"
|
HELP_MSG_DECRYPT_BACKUP = "Decrypt an encrypted iTunes backup"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class MVTModule:
|
|||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
slug: Optional[str] = None
|
slug: Optional[str] = None
|
||||||
dependencies: Sequence[type["MVTModule"]] = ()
|
dependencies: Sequence[type["MVTModule"]] = ()
|
||||||
|
supported_commands: Sequence[tuple[str, str]] = ()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 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 hashlib
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from .module import MVTModule
|
||||||
|
|
||||||
|
MVT_CUSTOM_MODULES_ENV = "MVT_CUSTOM_MODULES"
|
||||||
|
|
||||||
|
|
||||||
|
class CustomModuleLoadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _module_name_for_path(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:16]
|
||||||
|
return f"_mvt_custom_module_{path.stem}_{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_module_files(path: Path) -> Iterable[Path]:
|
||||||
|
if path.is_file():
|
||||||
|
if path.suffix != ".py":
|
||||||
|
raise CustomModuleLoadError(f"Custom module file is not a Python file: {path}")
|
||||||
|
yield path
|
||||||
|
return
|
||||||
|
|
||||||
|
if path.is_dir():
|
||||||
|
for child in sorted(path.iterdir()):
|
||||||
|
if child.name.startswith("."):
|
||||||
|
continue
|
||||||
|
if child.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
if child.is_file() and child.suffix == ".py":
|
||||||
|
yield child
|
||||||
|
return
|
||||||
|
|
||||||
|
raise CustomModuleLoadError(f"Custom module path does not exist: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_python_file(path: Path) -> ModuleType:
|
||||||
|
module_name = _module_name_for_path(path)
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise CustomModuleLoadError(f"Unable to load custom module file: {path}")
|
||||||
|
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[module_name] = module
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
except Exception as exc:
|
||||||
|
raise CustomModuleLoadError(f"Unable to import custom module {path}: {exc}") from exc
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def discover_mvt_modules(module: ModuleType) -> list[type[MVTModule]]:
|
||||||
|
modules = []
|
||||||
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
|
if obj is MVTModule:
|
||||||
|
continue
|
||||||
|
if obj.__module__ != module.__name__:
|
||||||
|
continue
|
||||||
|
if not issubclass(obj, MVTModule):
|
||||||
|
continue
|
||||||
|
modules.append(obj)
|
||||||
|
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def load_custom_modules_from_path(path: str) -> list[type[MVTModule]]:
|
||||||
|
custom_modules: list[type[MVTModule]] = []
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
resolved_path = Path(path).expanduser().resolve()
|
||||||
|
|
||||||
|
for module_file in _iter_module_files(resolved_path):
|
||||||
|
loaded_module = _load_python_file(module_file)
|
||||||
|
for module_class in discover_mvt_modules(loaded_module):
|
||||||
|
key = (str(module_file), module_class.__qualname__)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
custom_modules.append(module_class)
|
||||||
|
|
||||||
|
return custom_modules
|
||||||
|
|
||||||
|
|
||||||
|
def load_custom_modules(paths: Optional[Iterable[str]] = None) -> list[type[MVTModule]]:
|
||||||
|
search_paths: list[str] = []
|
||||||
|
env_path = os.environ.get(MVT_CUSTOM_MODULES_ENV)
|
||||||
|
if env_path:
|
||||||
|
search_paths.append(env_path)
|
||||||
|
if paths:
|
||||||
|
search_paths.extend(paths)
|
||||||
|
|
||||||
|
custom_modules: list[type[MVTModule]] = []
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
for path in search_paths:
|
||||||
|
for module_class in load_custom_modules_from_path(path):
|
||||||
|
source = Path(inspect.getfile(module_class)).resolve()
|
||||||
|
key = (str(source), module_class.__qualname__)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
custom_modules.append(module_class)
|
||||||
|
|
||||||
|
return custom_modules
|
||||||
|
|
||||||
|
|
||||||
|
def module_supports_command(
|
||||||
|
module_class: type[MVTModule],
|
||||||
|
platform: str,
|
||||||
|
command: str,
|
||||||
|
) -> bool:
|
||||||
|
supported_commands = getattr(module_class, "supported_commands", None)
|
||||||
|
if not supported_commands:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return (platform, command) in {tuple(entry) for entry in supported_commands}
|
||||||
+102
-3
@@ -11,6 +11,12 @@ import click
|
|||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||||
|
from mvt.common.completion import (
|
||||||
|
SUPPORTED_SHELLS,
|
||||||
|
completion_instructions,
|
||||||
|
generate_completion_script,
|
||||||
|
install_completion_script,
|
||||||
|
)
|
||||||
from mvt.common.logo import logo
|
from mvt.common.logo import logo
|
||||||
from mvt.common.options import MutuallyExclusiveOption
|
from mvt.common.options import MutuallyExclusiveOption
|
||||||
from mvt.common.updates import IndicatorsUpdates
|
from mvt.common.updates import IndicatorsUpdates
|
||||||
@@ -31,6 +37,7 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_OUTPUT,
|
HELP_MSG_OUTPUT,
|
||||||
HELP_MSG_FAST,
|
HELP_MSG_FAST,
|
||||||
HELP_MSG_LIST_MODULES,
|
HELP_MSG_LIST_MODULES,
|
||||||
|
HELP_MSG_LOAD_MODULE,
|
||||||
HELP_MSG_MODULE,
|
HELP_MSG_MODULE,
|
||||||
HELP_MSG_VERBOSE,
|
HELP_MSG_VERBOSE,
|
||||||
HELP_MSG_CHECK_FS,
|
HELP_MSG_CHECK_FS,
|
||||||
@@ -39,7 +46,9 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_CHECK_IOS_BACKUP,
|
HELP_MSG_CHECK_IOS_BACKUP,
|
||||||
HELP_MSG_DISABLE_UPDATE_CHECK,
|
HELP_MSG_DISABLE_UPDATE_CHECK,
|
||||||
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
|
HELP_MSG_COMPLETION,
|
||||||
)
|
)
|
||||||
|
from mvt.common.module_loader import CustomModuleLoadError, load_custom_modules
|
||||||
from .cmd_check_backup import CmdIOSCheckBackup
|
from .cmd_check_backup import CmdIOSCheckBackup
|
||||||
from .cmd_check_fs import CmdIOSCheckFS
|
from .cmd_check_fs import CmdIOSCheckFS
|
||||||
from .decrypt import DecryptBackup
|
from .decrypt import DecryptBackup
|
||||||
@@ -65,6 +74,13 @@ def _get_disable_flags(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_custom_modules(load_module):
|
||||||
|
try:
|
||||||
|
return load_custom_modules(load_module)
|
||||||
|
except CustomModuleLoadError as exc:
|
||||||
|
raise click.ClickException(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Main
|
# Main
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -82,6 +98,7 @@ 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
|
||||||
|
if ctx.invoked_subcommand != "completion":
|
||||||
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,
|
||||||
@@ -96,6 +113,40 @@ def version():
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Command: completion
|
||||||
|
# ==============================================================================
|
||||||
|
@cli.command("completion", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_COMPLETION)
|
||||||
|
@click.argument("shell", required=False, type=click.Choice(SUPPORTED_SHELLS))
|
||||||
|
@click.option(
|
||||||
|
"--install",
|
||||||
|
is_flag=True,
|
||||||
|
help="Write completion files and update shell configuration.",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def completion(ctx, shell, install):
|
||||||
|
program_name = "mvt-ios"
|
||||||
|
|
||||||
|
if shell is None:
|
||||||
|
if install:
|
||||||
|
raise click.UsageError("A shell is required when using --install.")
|
||||||
|
click.echo(completion_instructions(program_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
root_cli = ctx.find_root().command
|
||||||
|
|
||||||
|
if install:
|
||||||
|
script_path = install_completion_script(root_cli, program_name, shell)
|
||||||
|
click.echo(f"Installed {shell} completion to {script_path}")
|
||||||
|
if shell in ("bash", "zsh"):
|
||||||
|
click.echo(f"Updated ~/.{shell}rc")
|
||||||
|
else:
|
||||||
|
click.echo("Fish loads completion files automatically.")
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(generate_completion_script(root_cli, program_name, shell))
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Command: decrypt-backup
|
# Command: decrypt-backup
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -229,15 +280,32 @@ def extract_key(password, key_file, backup_path):
|
|||||||
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
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)
|
||||||
@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(
|
||||||
ctx, iocs, output, fast, list_modules, module, hashes, verbose, backup_path
|
ctx,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
fast,
|
||||||
|
list_modules,
|
||||||
|
module,
|
||||||
|
load_module,
|
||||||
|
hashes,
|
||||||
|
verbose,
|
||||||
|
backup_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
module_options = {"fast_mode": fast}
|
module_options = {"fast_mode": fast}
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
cmd = CmdIOSCheckBackup(
|
cmd = CmdIOSCheckBackup(
|
||||||
target_path=backup_path,
|
target_path=backup_path,
|
||||||
@@ -248,6 +316,7 @@ def check_backup(
|
|||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -277,13 +346,32 @@ def check_backup(
|
|||||||
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
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)
|
||||||
@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(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dump_path):
|
def check_fs(
|
||||||
|
ctx,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
fast,
|
||||||
|
list_modules,
|
||||||
|
module,
|
||||||
|
load_module,
|
||||||
|
hashes,
|
||||||
|
verbose,
|
||||||
|
dump_path,
|
||||||
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
module_options = {"fast_mode": fast}
|
module_options = {"fast_mode": fast}
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
|
|
||||||
cmd = CmdIOSCheckFS(
|
cmd = CmdIOSCheckFS(
|
||||||
target_path=dump_path,
|
target_path=dump_path,
|
||||||
@@ -294,6 +382,7 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
|
|||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -321,15 +410,25 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
|
|||||||
)
|
)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option(
|
||||||
|
"--load-module",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
multiple=True,
|
||||||
|
default=[],
|
||||||
|
help=HELP_MSG_LOAD_MODULE,
|
||||||
|
)
|
||||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
def check_iocs(ctx, iocs, list_modules, module, load_module, folder):
|
||||||
|
custom_modules = _load_custom_modules(load_module)
|
||||||
cmd = CmdCheckIOCS(
|
cmd = CmdCheckIOCS(
|
||||||
target_path=folder,
|
target_path=folder,
|
||||||
ioc_files=iocs,
|
ioc_files=iocs,
|
||||||
module_name=module,
|
module_name=module,
|
||||||
disable_version_check=_get_disable_flags(ctx)[0],
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
custom_modules=custom_modules,
|
||||||
|
platform="ios",
|
||||||
)
|
)
|
||||||
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.backup import BACKUP_MODULES
|
from .modules.backup import BACKUP_MODULES
|
||||||
from .modules.mixed import MIXED_MODULES
|
from .modules.mixed import MIXED_MODULES
|
||||||
@@ -29,6 +30,7 @@ class CmdIOSCheckBackup(Command):
|
|||||||
sub_command: bool = False,
|
sub_command: bool = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -43,8 +45,10 @@ class CmdIOSCheckBackup(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "ios"
|
||||||
self.name = "check-backup"
|
self.name = "check-backup"
|
||||||
self.modules = BACKUP_MODULES + MIXED_MODULES
|
self.modules = BACKUP_MODULES + MIXED_MODULES
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
from .modules.fs import FS_MODULES
|
from .modules.fs import FS_MODULES
|
||||||
from .modules.mixed import MIXED_MODULES
|
from .modules.mixed import MIXED_MODULES
|
||||||
@@ -29,6 +30,7 @@ class CmdIOSCheckFS(Command):
|
|||||||
sub_command: bool = False,
|
sub_command: bool = False,
|
||||||
disable_version_check: bool = False,
|
disable_version_check: bool = False,
|
||||||
disable_indicator_check: bool = False,
|
disable_indicator_check: bool = False,
|
||||||
|
custom_modules: Optional[list[type[MVTModule]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -42,8 +44,10 @@ class CmdIOSCheckFS(Command):
|
|||||||
log=log,
|
log=log,
|
||||||
disable_version_check=disable_version_check,
|
disable_version_check=disable_version_check,
|
||||||
disable_indicator_check=disable_indicator_check,
|
disable_indicator_check=disable_indicator_check,
|
||||||
|
custom_modules=custom_modules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.platform = "ios"
|
||||||
self.name = "check-fs"
|
self.name = "check-fs"
|
||||||
self.modules = FS_MODULES + MIXED_MODULES
|
self.modules = FS_MODULES + MIXED_MODULES
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ class IndependentModule(RecordingModule):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CustomIOSBackupModule(RecordingModule):
|
||||||
|
supported_commands = (("ios", "check-backup"),)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomIOSFSModule(RecordingModule):
|
||||||
|
supported_commands = (("ios", "check-fs"),)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomDependsOnBuiltin(RecordingModule):
|
||||||
|
supported_commands = (("ios", "check-backup"),)
|
||||||
|
dependencies = (FirstModule,)
|
||||||
|
|
||||||
|
|
||||||
class RecordingCommand(Command):
|
class RecordingCommand(Command):
|
||||||
def init(self):
|
def init(self):
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
@@ -132,3 +145,46 @@ class TestCommand:
|
|||||||
assert RecordingModule.run_order == []
|
assert RecordingModule.run_order == []
|
||||||
assert not hasattr(cmd, "initialized")
|
assert not hasattr(cmd, "initialized")
|
||||||
assert "depends on unavailable module UnavailableModule" in caplog.text
|
assert "depends on unavailable module UnavailableModule" in caplog.text
|
||||||
|
|
||||||
|
def test_custom_modules_are_filtered_before_ordering(self):
|
||||||
|
cmd = RecordingCommand()
|
||||||
|
cmd.platform = "ios"
|
||||||
|
cmd.name = "check-backup"
|
||||||
|
cmd.modules = [FirstModule]
|
||||||
|
cmd.custom_modules = [CustomIOSBackupModule, CustomIOSFSModule]
|
||||||
|
|
||||||
|
assert [module.__name__ for module in cmd._ordered_modules()] == [
|
||||||
|
"FirstModule",
|
||||||
|
"CustomIOSBackupModule",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_selected_custom_module_runs(self):
|
||||||
|
cmd = RecordingCommand(module_name="CustomIOSBackupModule")
|
||||||
|
cmd.platform = "ios"
|
||||||
|
cmd.name = "check-backup"
|
||||||
|
cmd.custom_modules = [CustomIOSBackupModule]
|
||||||
|
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
assert RecordingModule.run_order == ["CustomIOSBackupModule"]
|
||||||
|
|
||||||
|
def test_selected_unsupported_custom_module_does_not_run(self):
|
||||||
|
cmd = RecordingCommand(module_name="CustomIOSFSModule")
|
||||||
|
cmd.platform = "ios"
|
||||||
|
cmd.name = "check-backup"
|
||||||
|
cmd.custom_modules = [CustomIOSFSModule]
|
||||||
|
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
assert RecordingModule.run_order == []
|
||||||
|
|
||||||
|
def test_custom_module_dependencies_use_topological_order(self):
|
||||||
|
cmd = RecordingCommand(module_name="CustomDependsOnBuiltin")
|
||||||
|
cmd.platform = "ios"
|
||||||
|
cmd.name = "check-backup"
|
||||||
|
cmd.modules = [SecondModule, FirstModule]
|
||||||
|
cmd.custom_modules = [CustomDependsOnBuiltin]
|
||||||
|
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
assert RecordingModule.run_order == ["FirstModule", "CustomDependsOnBuiltin"]
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
from mvt.common.module_loader import (
|
||||||
|
CustomModuleLoadError,
|
||||||
|
load_custom_modules,
|
||||||
|
load_custom_modules_from_path,
|
||||||
|
module_supports_command,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_TEMPLATE = """
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
|
||||||
|
class {name}(MVTModule):
|
||||||
|
supported_commands = {supported_commands!r}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_indicators(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serialize(self, result):
|
||||||
|
return None
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _write_module(path, name, supported_commands=()):
|
||||||
|
path.write_text(
|
||||||
|
MODULE_TEMPLATE.format(
|
||||||
|
name=name,
|
||||||
|
supported_commands=supported_commands,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_from_python_file(tmp_path):
|
||||||
|
module_path = _write_module(tmp_path / "custom.py", "FileModule")
|
||||||
|
|
||||||
|
modules = load_custom_modules_from_path(str(module_path))
|
||||||
|
|
||||||
|
assert [module.__name__ for module in modules] == ["FileModule"]
|
||||||
|
assert issubclass(modules[0], MVTModule)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_from_folder_in_sorted_order(tmp_path):
|
||||||
|
_write_module(tmp_path / "b_module.py", "BModule")
|
||||||
|
_write_module(tmp_path / "a_module.py", "AModule")
|
||||||
|
_write_module(tmp_path / ".hidden.py", "HiddenModule")
|
||||||
|
_write_module(tmp_path / "__init__.py", "InitModule")
|
||||||
|
nested = tmp_path / "nested"
|
||||||
|
nested.mkdir()
|
||||||
|
_write_module(nested / "nested_module.py", "NestedModule")
|
||||||
|
|
||||||
|
modules = load_custom_modules_from_path(str(tmp_path))
|
||||||
|
|
||||||
|
assert [module.__name__ for module in modules] == ["AModule", "BModule"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_discovery_ignores_imported_base_and_unrelated_classes(tmp_path):
|
||||||
|
module_path = tmp_path / "custom.py"
|
||||||
|
module_path.write_text(
|
||||||
|
"""
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
|
||||||
|
class Unrelated:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveredModule(MVTModule):
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_indicators(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serialize(self, result):
|
||||||
|
return None
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
modules = load_custom_modules_from_path(str(module_path))
|
||||||
|
|
||||||
|
assert [module.__name__ for module in modules] == ["DiscoveredModule"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_deduplicates_same_class(tmp_path):
|
||||||
|
module_path = _write_module(tmp_path / "custom.py", "DuplicateModule")
|
||||||
|
|
||||||
|
modules = load_custom_modules([str(module_path), str(module_path)])
|
||||||
|
|
||||||
|
assert [module.__name__ for module in modules] == ["DuplicateModule"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_raises_for_missing_path(tmp_path):
|
||||||
|
with pytest.raises(CustomModuleLoadError, match="does not exist"):
|
||||||
|
load_custom_modules_from_path(str(tmp_path / "missing.py"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_raises_for_import_error(tmp_path):
|
||||||
|
module_path = tmp_path / "broken.py"
|
||||||
|
module_path.write_text("raise RuntimeError('broken import')", encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(CustomModuleLoadError, match="broken import"):
|
||||||
|
load_custom_modules_from_path(str(module_path))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_modules_loads_env_folder_first(tmp_path, monkeypatch):
|
||||||
|
env_folder = tmp_path / "env"
|
||||||
|
env_folder.mkdir()
|
||||||
|
cli_folder = tmp_path / "cli"
|
||||||
|
cli_folder.mkdir()
|
||||||
|
_write_module(env_folder / "env_module.py", "EnvModule")
|
||||||
|
_write_module(cli_folder / "cli_module.py", "CliModule")
|
||||||
|
monkeypatch.setenv("MVT_CUSTOM_MODULES", str(env_folder))
|
||||||
|
|
||||||
|
modules = load_custom_modules([str(cli_folder)])
|
||||||
|
|
||||||
|
assert [module.__name__ for module in modules] == ["EnvModule", "CliModule"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_supports_command_defaults_to_all_commands(tmp_path):
|
||||||
|
module_path = _write_module(tmp_path / "custom.py", "DefaultModule")
|
||||||
|
module = load_custom_modules_from_path(str(module_path))[0]
|
||||||
|
|
||||||
|
assert module_supports_command(module, "ios", "check-backup")
|
||||||
|
assert module_supports_command(module, "android", "check-bugreport")
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_supports_command_honors_supported_commands(tmp_path):
|
||||||
|
module_path = _write_module(
|
||||||
|
tmp_path / "custom.py",
|
||||||
|
"SpecificModule",
|
||||||
|
(("ios", "check-backup"),),
|
||||||
|
)
|
||||||
|
module = load_custom_modules_from_path(str(module_path))[0]
|
||||||
|
|
||||||
|
assert module_supports_command(module, "ios", "check-backup")
|
||||||
|
assert not module_supports_command(module, "ios", "check-fs")
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 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 click.testing import CliRunner
|
||||||
|
|
||||||
|
from mvt.android.cli import cli as android_cli
|
||||||
|
from mvt.ios.cli import cli as ios_cli
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompletionCommand:
|
||||||
|
def test_completion_prints_instructions_by_default(self):
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(ios_cli, ["completion"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Shell completion for mvt-ios" in result.output
|
||||||
|
assert "mvt-ios completion bash > ~/.mvt-ios-complete.bash" in result.output
|
||||||
|
assert "Mobile Verification Toolkit" not in result.output
|
||||||
|
|
||||||
|
def test_completion_prints_bash_script(self):
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(ios_cli, ["completion", "bash"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "_MVT_IOS_COMPLETE=bash_complete" in result.output
|
||||||
|
assert "complete -o nosort" in result.output
|
||||||
|
assert "mvt-ios" in result.output
|
||||||
|
assert "Mobile Verification Toolkit" not in result.output
|
||||||
|
|
||||||
|
def test_completion_prints_fish_script(self):
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(android_cli, ["completion", "fish"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "_MVT_ANDROID_COMPLETE=fish_complete" in result.output
|
||||||
|
assert "complete --no-files --command mvt-android" in result.output
|
||||||
|
assert "Mobile Verification Toolkit" not in result.output
|
||||||
|
|
||||||
|
def test_completion_install_updates_bashrc_once(self, tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(ios_cli, ["completion", "bash", "--install"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
script_path = tmp_path / ".mvt-ios-complete.bash"
|
||||||
|
bashrc_path = tmp_path / ".bashrc"
|
||||||
|
assert script_path.exists()
|
||||||
|
assert "_MVT_IOS_COMPLETE=bash_complete" in script_path.read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
bashrc = bashrc_path.read_text(encoding="utf-8")
|
||||||
|
assert "[ -f" in bashrc
|
||||||
|
assert ".mvt-ios-complete.bash" in bashrc
|
||||||
|
|
||||||
|
result = runner.invoke(ios_cli, ["completion", "bash", "--install"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert bashrc_path.read_text(encoding="utf-8") == bashrc
|
||||||
|
|
||||||
|
def test_completion_install_fish_does_not_update_shell_rc(
|
||||||
|
self, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(android_cli, ["completion", "fish", "--install"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
script_path = (
|
||||||
|
tmp_path / ".config" / "fish" / "completions" / "mvt-android.fish"
|
||||||
|
)
|
||||||
|
assert script_path.exists()
|
||||||
|
assert "_MVT_ANDROID_COMPLETE=fish_complete" in script_path.read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert not (tmp_path / ".fishrc").exists()
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from mvt.android.cli import check_bugreport
|
||||||
|
from mvt.android.cmd_check_androidqf import CmdAndroidCheckAndroidQF
|
||||||
|
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup
|
||||||
|
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||||
|
from mvt.android.cmd_check_intrusion_logs import CmdAndroidCheckIntrusionLogs
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
from mvt.ios.cli import check_backup, check_fs
|
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_MODULE = """
|
||||||
|
from mvt.common.module import MVTModule
|
||||||
|
|
||||||
|
|
||||||
|
class {name}(MVTModule):
|
||||||
|
supported_commands = {supported_commands!r}
|
||||||
|
slug = "{slug}"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.results = [{{"message": "custom module ran"}}]
|
||||||
|
|
||||||
|
def check_indicators(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serialize(self, result):
|
||||||
|
return None
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _write_custom_module(path, name, supported_commands, slug=None):
|
||||||
|
path.write_text(
|
||||||
|
CUSTOM_MODULE.format(
|
||||||
|
name=name,
|
||||||
|
supported_commands=supported_commands,
|
||||||
|
slug=slug or name.lower(),
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_module_appears_only_for_supported_cli_command(tmp_path):
|
||||||
|
module_path = _write_custom_module(
|
||||||
|
tmp_path / "custom.py",
|
||||||
|
"IOSBackupOnlyModule",
|
||||||
|
(("ios", "check-backup"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
backup_result = CliRunner().invoke(
|
||||||
|
check_backup,
|
||||||
|
["--list-modules", "--load-module", str(module_path), str(tmp_path)],
|
||||||
|
)
|
||||||
|
fs_result = CliRunner().invoke(
|
||||||
|
check_fs,
|
||||||
|
["--list-modules", "--load-module", str(module_path), str(tmp_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert backup_result.exit_code == 0
|
||||||
|
assert "IOSBackupOnlyModule" in backup_result.output
|
||||||
|
assert fs_result.exit_code == 0
|
||||||
|
assert "IOSBackupOnlyModule" not in fs_result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_option_runs_supported_custom_module(tmp_path):
|
||||||
|
module_path = _write_custom_module(
|
||||||
|
tmp_path / "custom.py",
|
||||||
|
"CustomRunModule",
|
||||||
|
(("ios", "check-backup"),),
|
||||||
|
slug="custom_run_module",
|
||||||
|
)
|
||||||
|
output_path = tmp_path / "out"
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
check_backup,
|
||||||
|
[
|
||||||
|
"--module",
|
||||||
|
"CustomRunModule",
|
||||||
|
"--load-module",
|
||||||
|
str(module_path),
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
str(tmp_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_path / "custom_run_module.json").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_modules_load_from_environment_without_cli_flag(tmp_path, monkeypatch):
|
||||||
|
custom_modules_path = tmp_path / "custom_modules"
|
||||||
|
custom_modules_path.mkdir()
|
||||||
|
_write_custom_module(
|
||||||
|
custom_modules_path / "env_module.py",
|
||||||
|
"EnvBugreportModule",
|
||||||
|
(("android", "check-bugreport"),),
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("MVT_CUSTOM_MODULES", str(custom_modules_path))
|
||||||
|
|
||||||
|
result = CliRunner().invoke(check_bugreport, ["--list-modules", str(tmp_path)])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "EnvBugreportModule" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
class NestedBugreportModule(MVTModule):
|
||||||
|
supported_commands = (("android", "check-bugreport"),)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedBackupModule(MVTModule):
|
||||||
|
supported_commands = (("android", "check-backup"),)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedIntrusionLogsModule(MVTModule):
|
||||||
|
supported_commands = (("android", "check-intrusion-logs"),)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedAndroidQFModule(MVTModule):
|
||||||
|
supported_commands = (("android", "check-androidqf"),)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyZip:
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_androidqf_propagates_custom_modules_to_nested_commands(tmp_path, monkeypatch):
|
||||||
|
records = {}
|
||||||
|
custom_modules = [
|
||||||
|
NestedBugreportModule,
|
||||||
|
NestedBackupModule,
|
||||||
|
NestedIntrusionLogsModule,
|
||||||
|
NestedAndroidQFModule,
|
||||||
|
]
|
||||||
|
cmd = CmdAndroidCheckAndroidQF(
|
||||||
|
target_path=str(tmp_path),
|
||||||
|
custom_modules=custom_modules,
|
||||||
|
)
|
||||||
|
|
||||||
|
def record_available(name):
|
||||||
|
def _record(command):
|
||||||
|
records[name] = [
|
||||||
|
module.__name__
|
||||||
|
for module in command._available_modules()
|
||||||
|
if module.__name__.startswith("Nested")
|
||||||
|
]
|
||||||
|
|
||||||
|
return _record
|
||||||
|
|
||||||
|
monkeypatch.setattr(cmd, "load_bugreport", lambda: DummyZip())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CmdAndroidCheckBugreport,
|
||||||
|
"from_zip",
|
||||||
|
lambda self, bugreport: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CmdAndroidCheckBugreport,
|
||||||
|
"run",
|
||||||
|
record_available("bugreport"),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(cmd, "load_backup", lambda: b"")
|
||||||
|
monkeypatch.setattr(CmdAndroidCheckBackup, "from_ab", lambda self, backup: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CmdAndroidCheckBackup,
|
||||||
|
"run",
|
||||||
|
record_available("backup"),
|
||||||
|
)
|
||||||
|
|
||||||
|
intrusion_logs_path = tmp_path / "intrusion_logs"
|
||||||
|
intrusion_logs_path.mkdir()
|
||||||
|
setattr(cmd, "_CmdAndroidCheckAndroidQF__format", "dir")
|
||||||
|
setattr(
|
||||||
|
cmd,
|
||||||
|
"_CmdAndroidCheckAndroidQF__files",
|
||||||
|
["androidqf/intrusion_logs/security.txt"],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cmd, "_read_device_timezone", lambda: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
CmdAndroidCheckIntrusionLogs,
|
||||||
|
"run",
|
||||||
|
record_available("intrusion_logs"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cmd.run_bugreport_cmd()
|
||||||
|
assert cmd.run_backup_cmd()
|
||||||
|
assert cmd.run_intrusion_logs_cmd()
|
||||||
|
assert records == {
|
||||||
|
"bugreport": ["NestedBugreportModule"],
|
||||||
|
"backup": ["NestedBackupModule"],
|
||||||
|
"intrusion_logs": ["NestedIntrusionLogsModule"],
|
||||||
|
}
|
||||||
Reference in new issue
Block a user