mirror of
https://github.com/mvt-project/mvt.git
synced 2026-09-03 00:21:07 +02:00
* Add plugin update checking Report available updates to the installed MVT plugin packages in the startup banner, for plugins installed from a package index and for plugins installed directly from a repository. Repository installs pinned to a commit or a tag are never reported as outdated. MVT only prints the command which upgrades a plugin. Installing the update stays a deliberate choice of the analyst. The check runs at most once every twelve hours, and in between prints the findings of the latest check which still apply to what is installed. Nothing about the check can interrupt a running command: the parts of the suggested command come from package metadata and are quoted for the shell, the repository query refuses to prompt for credentials and never passes metadata as a git option, and a corrupt or stale cache is discarded rather than trusted. * Add a plugins command to list installed plugins and check updates Add a "plugins" command to the platform-neutral mvt command. "plugins list" shows every installed plugin package with its version, where it was installed from, how many forensic modules it contributes and which commands it adds. "plugins check-updates" checks for updates immediately, without waiting for the automatic check, and prints the command which upgrades a plugin instead of installing anything. It lives on mvt only. The packages it lists extend mvt-ios and mvt-android too, but auditing them is not the job of a command which analyses one platform, and the two platform CLIs should not carry commands which are not about an acquisition. The command is registered as a built-in, before any external command, so that an installed package cannot replace this audit surface.
237 lines
7.1 KiB
Python
237 lines
7.1 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from mvt.android.cli import cli as android_cli
|
|
from mvt.cli import cli as mvt_cli
|
|
from mvt.common.cli_plugins import (
|
|
ANDROID_CLI_PLUGIN_GROUP,
|
|
IOS_CLI_PLUGIN_GROUP,
|
|
NEUTRAL_CLI_PLUGIN_GROUP,
|
|
)
|
|
from mvt.common.cmd_plugins import plugins
|
|
from mvt.common.module import MVTModule
|
|
from mvt.common.module_loader import MODULES_ENTRY_POINT_GROUP
|
|
from mvt.common.updates import PluginUpdates
|
|
from mvt.ios.cli import cli as ios_cli
|
|
|
|
|
|
class ExampleModule(MVTModule):
|
|
pass
|
|
|
|
|
|
class AnotherModule(MVTModule):
|
|
pass
|
|
|
|
|
|
class FakeDistribution:
|
|
def __init__(self, name, version="1.0.0", direct_url=None):
|
|
self.name = name
|
|
self.version = version
|
|
self.direct_url = direct_url
|
|
|
|
def read_text(self, file_name):
|
|
if file_name == "direct_url.json" and self.direct_url is not None:
|
|
return json.dumps(self.direct_url)
|
|
return None
|
|
|
|
|
|
def _entry_point(name, distribution, modules=None, exception=None):
|
|
def load():
|
|
if exception is not None:
|
|
raise exception
|
|
return modules
|
|
|
|
return SimpleNamespace(
|
|
name=name, value="example_plugin:modules", dist=distribution, load=load
|
|
)
|
|
|
|
|
|
def _run(command, arguments):
|
|
# Keep rich from wrapping the table while its content is being asserted.
|
|
return CliRunner().invoke(command, arguments, env={"COLUMNS": "200"})
|
|
|
|
|
|
def _table_rows(output):
|
|
"""Return the content of the table rows, without the header and the box."""
|
|
return [
|
|
[cell.strip() for cell in line.strip().strip("│").split("│")]
|
|
for line in output.splitlines()
|
|
if "│" in line
|
|
]
|
|
|
|
|
|
def _table_header(output):
|
|
for line in output.splitlines():
|
|
if "┃" in line:
|
|
return [cell.strip() for cell in line.strip().strip("┃").split("┃")]
|
|
return []
|
|
|
|
|
|
def _install(monkeypatch, distributions, entry_points):
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.installed_plugin_distributions",
|
|
lambda: distributions,
|
|
)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.importlib.metadata.entry_points",
|
|
lambda *, group: entry_points.get(group, []),
|
|
)
|
|
|
|
|
|
def test_plugins_is_a_builtin_command_of_the_mvt_cli_only():
|
|
assert mvt_cli.commands["plugins"] is plugins
|
|
assert "plugins" not in ios_cli.commands
|
|
assert "plugins" not in android_cli.commands
|
|
|
|
|
|
def test_list_shows_what_every_plugin_contributes(monkeypatch):
|
|
index_plugin = FakeDistribution("example-plugin", version="1.2.0")
|
|
repository_plugin = FakeDistribution(
|
|
"repository-plugin",
|
|
version="0.1.0",
|
|
direct_url={
|
|
"url": "https://example.org/plugin.git",
|
|
"vcs_info": {"vcs": "git", "commit_id": "b" * 40},
|
|
},
|
|
)
|
|
local_plugin = FakeDistribution(
|
|
"local-plugin",
|
|
direct_url={"url": "file:///plugins", "dir_info": {"editable": True}},
|
|
)
|
|
_install(
|
|
monkeypatch,
|
|
[index_plugin, local_plugin, repository_plugin],
|
|
{
|
|
MODULES_ENTRY_POINT_GROUP: [
|
|
_entry_point(
|
|
"example", index_plugin, modules=[ExampleModule, AnotherModule]
|
|
),
|
|
_entry_point("local", local_plugin, modules=lambda: [ExampleModule]),
|
|
],
|
|
IOS_CLI_PLUGIN_GROUP: [_entry_point("summarize", repository_plugin)],
|
|
ANDROID_CLI_PLUGIN_GROUP: [_entry_point("triage", local_plugin)],
|
|
NEUTRAL_CLI_PLUGIN_GROUP: [_entry_point("report", repository_plugin)],
|
|
},
|
|
)
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
# Plugins are listed by name, with the modules and the commands each of
|
|
# them contributes.
|
|
assert _table_header(result.output) == [
|
|
"Name",
|
|
"Version",
|
|
"Origin",
|
|
"Modules",
|
|
"Commands",
|
|
]
|
|
assert _table_rows(result.output) == [
|
|
["example-plugin", "1.2.0", "pypi", "2", "-"],
|
|
["local-plugin", "1.0.0", "local", "1", "triage"],
|
|
["repository-plugin", "0.1.0", "git+bbbbbbbb", "0", "report, summarize"],
|
|
]
|
|
|
|
|
|
def test_list_reports_a_broken_module_entry_point(monkeypatch):
|
|
plugin = FakeDistribution("broken-plugin")
|
|
_install(
|
|
monkeypatch,
|
|
[plugin],
|
|
{
|
|
MODULES_ENTRY_POINT_GROUP: [
|
|
_entry_point(
|
|
"broken", plugin, exception=ImportError("missing dependency")
|
|
)
|
|
]
|
|
},
|
|
)
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert _table_rows(result.output) == [
|
|
["broken-plugin", "1.0.0", "pypi", "error", "-"]
|
|
]
|
|
|
|
|
|
def test_list_without_plugins(monkeypatch):
|
|
_install(monkeypatch, [], {})
|
|
|
|
result = _run(plugins, ["list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output == "No MVT plugins are installed.\n"
|
|
|
|
|
|
def test_check_updates_prints_the_findings_and_ignores_the_throttle(monkeypatch):
|
|
findings = [
|
|
{
|
|
"name": "example-plugin",
|
|
"installed": "1.0.0",
|
|
"latest": "1.2.0",
|
|
"origin": "pypi",
|
|
"upgrade_command": "pip install -U example-plugin",
|
|
}
|
|
]
|
|
_install(monkeypatch, [FakeDistribution("example-plugin")], {})
|
|
monkeypatch.setattr(PluginUpdates, "check", lambda self: findings)
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"should_check",
|
|
lambda self: pytest.fail("an explicit check must not be throttled"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Plugin updates available:" in result.output
|
|
assert "example-plugin 1.0.0 → 1.2.0" in result.output
|
|
assert "Upgrade with: pip install -U example-plugin" in result.output
|
|
assert "MVT does not install plugin updates." in result.output
|
|
|
|
|
|
def test_check_updates_without_available_updates(monkeypatch):
|
|
_install(monkeypatch, [FakeDistribution("example-plugin")], {})
|
|
monkeypatch.setattr(PluginUpdates, "check", lambda self: [])
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "All plugins are up to date." in result.output
|
|
|
|
|
|
def test_check_updates_without_plugins(monkeypatch):
|
|
_install(monkeypatch, [], {})
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"check",
|
|
lambda self: pytest.fail("nothing must be checked without plugins"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No MVT plugins are installed." in result.output
|
|
|
|
|
|
def test_check_updates_without_network_access(monkeypatch):
|
|
monkeypatch.setattr("mvt.common.cmd_plugins.settings.NETWORK_ACCESS_ALLOWED", False)
|
|
monkeypatch.setattr(
|
|
"mvt.common.cmd_plugins.installed_plugin_distributions",
|
|
lambda: pytest.fail("plugins must not be listed without network access"),
|
|
)
|
|
monkeypatch.setattr(
|
|
PluginUpdates,
|
|
"check",
|
|
lambda self: pytest.fail("nothing must be checked without network access"),
|
|
)
|
|
|
|
result = _run(plugins, ["check-updates"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Network access is disabled" in result.output
|