mirror of
https://github.com/mvt-project/mvt.git
synced 2026-09-03 08:30:51 +02:00
Now that installed module packages load automatically, record where every module came from: - --list-modules groups the available modules by source, one line per source with the modules comma-separated: MVT itself with its version, each installed package with its version and VCS commit when recorded (PEP 610 direct_url.json), and each --load-module/MVT_CUSTOM_MODULES file with its SHA-256 hash. - Commands log one line per module source with its version or hash and the modules loaded from it, so command.log records exactly which modules ran and where they came from. - Make init_logging() idempotent: a loaded module package importing an MVT CLI module would previously add a second console handler and duplicate every console log line.
346 lines
9.8 KiB
Python
346 lines
9.8 KiB
Python
import hashlib
|
|
import importlib.metadata
|
|
import json
|
|
|
|
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 import module_loader
|
|
from mvt.common.module import MVTModule
|
|
from mvt.common.version import MVT_VERSION
|
|
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):
|
|
(tmp_path / "Manifest.db").touch()
|
|
(tmp_path / "Info.plist").touch()
|
|
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 InstalledPackageModule(MVTModule):
|
|
supported_commands = (("ios", "check-backup"),)
|
|
|
|
|
|
def get_installed_package_modules():
|
|
return [InstalledPackageModule]
|
|
|
|
|
|
def _fake_entry_points(monkeypatch, value, name="test-modules"):
|
|
entry_point = importlib.metadata.EntryPoint(
|
|
name=name, value=value, group=module_loader.MODULES_ENTRY_POINT_GROUP
|
|
)
|
|
|
|
def fake_entry_points(*, group):
|
|
assert group == module_loader.MODULES_ENTRY_POINT_GROUP
|
|
return [entry_point]
|
|
|
|
monkeypatch.setattr(
|
|
module_loader.importlib.metadata, "entry_points", fake_entry_points
|
|
)
|
|
|
|
|
|
def test_installed_module_package_loads_from_entry_point(monkeypatch):
|
|
_fake_entry_points(monkeypatch, f"{__name__}:get_installed_package_modules")
|
|
|
|
modules = module_loader.load_custom_modules()
|
|
|
|
assert modules == [InstalledPackageModule]
|
|
|
|
|
|
def test_broken_module_entry_point_is_skipped(monkeypatch, caplog):
|
|
_fake_entry_points(monkeypatch, "nonexistent_module_xyz:get_modules")
|
|
|
|
with caplog.at_level("WARNING"):
|
|
modules = module_loader.load_custom_modules()
|
|
|
|
assert modules == []
|
|
assert "Unable to load modules from entry point" in caplog.text
|
|
|
|
|
|
def test_entry_point_module_deduplicated_against_paths(monkeypatch, tmp_path):
|
|
_fake_entry_points(monkeypatch, f"{__name__}:get_installed_package_modules")
|
|
module_path = _write_custom_module(
|
|
tmp_path / "custom.py",
|
|
"PathLoadedModule",
|
|
(("ios", "check-backup"),),
|
|
)
|
|
|
|
modules = module_loader.load_custom_modules([str(module_path)])
|
|
|
|
assert [module.__name__ for module in modules] == [
|
|
"InstalledPackageModule",
|
|
"PathLoadedModule",
|
|
]
|
|
|
|
|
|
def test_list_modules_shows_module_sources(tmp_path, caplog):
|
|
module_path = _write_custom_module(
|
|
tmp_path / "custom.py",
|
|
"SourcedBackupModule",
|
|
(("ios", "check-backup"),),
|
|
)
|
|
file_sha256 = hashlib.sha256(module_path.read_bytes()).hexdigest()
|
|
custom_modules = module_loader.load_custom_modules([str(module_path)])
|
|
|
|
from mvt.ios.cmd_check_backup import CmdIOSCheckBackup
|
|
|
|
cmd = CmdIOSCheckBackup(target_path=str(tmp_path), custom_modules=custom_modules)
|
|
cmd.list_modules()
|
|
|
|
assert f" - Modules from 'mvt@{MVT_VERSION}':" in caplog.text
|
|
assert (
|
|
f" - Modules from '{module_path}' (sha256: {file_sha256}): SourcedBackupModule"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
def test_builtin_module_origin():
|
|
from mvt.ios.modules.backup import BACKUP_MODULES
|
|
|
|
origin = module_loader.get_module_origin(BACKUP_MODULES[0])
|
|
|
|
assert origin.kind == "builtin"
|
|
assert origin.name == "mvt"
|
|
assert origin.version == MVT_VERSION
|
|
|
|
|
|
def test_installed_module_origin(monkeypatch):
|
|
_fake_entry_points(monkeypatch, f"{__name__}:get_installed_package_modules")
|
|
|
|
modules = module_loader.load_custom_modules()
|
|
|
|
origin = module_loader.get_module_origin(modules[0])
|
|
assert origin.kind == "package"
|
|
assert origin.name == "test-modules"
|
|
|
|
|
|
def test_distribution_commit_read_from_direct_url():
|
|
class FakeDistribution:
|
|
def read_text(self, filename):
|
|
assert filename == "direct_url.json"
|
|
return json.dumps(
|
|
{
|
|
"url": "https://github.com/example/example-modules",
|
|
"vcs_info": {"commit_id": "abc1234", "vcs": "git"},
|
|
}
|
|
)
|
|
|
|
assert module_loader._distribution_commit(FakeDistribution()) == "abc1234"
|
|
|
|
|
|
def test_command_log_records_loaded_modules(tmp_path):
|
|
(tmp_path / "Manifest.db").touch()
|
|
(tmp_path / "Info.plist").touch()
|
|
module_path = _write_custom_module(
|
|
tmp_path / "custom.py",
|
|
"AuditedRunModule",
|
|
(("ios", "check-backup"),),
|
|
slug="audited_run_module",
|
|
)
|
|
file_sha256 = hashlib.sha256(module_path.read_bytes()).hexdigest()
|
|
output_path = tmp_path / "out"
|
|
|
|
result = CliRunner().invoke(
|
|
check_backup,
|
|
[
|
|
"--module",
|
|
"AuditedRunModule",
|
|
"--load-module",
|
|
str(module_path),
|
|
"--output",
|
|
str(output_path),
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
command_log = (output_path / "command.log").read_text(encoding="utf-8")
|
|
assert (
|
|
f"Loaded 1 check-backup modules from '{module_path}' "
|
|
f"(sha256: {file_sha256}): AuditedRunModule" in command_log
|
|
)
|
|
|
|
|
|
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"],
|
|
}
|