278 lines
9.7 KiB
Python
278 lines
9.7 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from config import CONFIG
|
|
from pipeline.plugins import discover_plugins, scan_plugins, TRUST_BLOCKED, TRUST_TRUSTED, TRUST_UNTRUSTED, TRUST_UNSIGNED
|
|
|
|
|
|
def _write_plugin(tmp_path: Path, plugin_id: str, content: str, signature: str = "") -> Path:
|
|
plugin_dir = tmp_path / "plugins"
|
|
plugin_dir.mkdir()
|
|
py_file = plugin_dir / "example_plugin.py"
|
|
py_file.write_text(content, encoding="utf-8")
|
|
|
|
digest = hashlib.sha256(py_file.read_bytes()).hexdigest()
|
|
manifest = {
|
|
"id": plugin_id,
|
|
"name": "Test Plugin",
|
|
"version": "1.0.0",
|
|
"author": "tests",
|
|
"min_doctor_version": "0.0.0",
|
|
"sha256": digest,
|
|
}
|
|
if signature:
|
|
manifest["signature"] = signature
|
|
manifest["signature_alg"] = "hmac-sha256"
|
|
(plugin_dir / "example_plugin.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
return plugin_dir
|
|
|
|
|
|
def _sign_plugin(py_file: Path, key: str) -> str:
|
|
return hmac.new(key.encode("utf-8"), py_file.read_bytes(), hashlib.sha256).hexdigest()
|
|
|
|
|
|
def test_plugins_disabled_returns_empty(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.test",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
)
|
|
|
|
original_enabled = CONFIG.enable_community_plugins
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
try:
|
|
CONFIG.enable_community_plugins = False
|
|
CONFIG.plugin_allowlist = ["community.test"]
|
|
assert discover_plugins(plugin_dir) == []
|
|
finally:
|
|
CONFIG.enable_community_plugins = original_enabled
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
|
|
|
|
def test_allowlisted_plugin_loads_and_is_trusted(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.test",
|
|
"def register_matchers(traceback):\n return ('ok', {'matched_pattern_id': 'x'})\n",
|
|
)
|
|
|
|
original_enabled = CONFIG.enable_community_plugins
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
try:
|
|
CONFIG.enable_community_plugins = True
|
|
CONFIG.plugin_allowlist = ["community.test"]
|
|
plugins = discover_plugins(plugin_dir)
|
|
assert len(plugins) == 1
|
|
assert getattr(plugins[0], "__plugin_trust__", None) == "trusted"
|
|
finally:
|
|
CONFIG.enable_community_plugins = original_enabled
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
|
|
|
|
def test_oversized_plugin_is_rejected(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.test",
|
|
"x = 'a' * 100\n\ndef register_matchers(traceback):\n return None\n",
|
|
)
|
|
|
|
big_file = next(plugin_dir.glob("*.py"))
|
|
big_file.write_bytes(b"a" * 2000)
|
|
|
|
manifest_path = big_file.with_suffix(".json")
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
manifest["sha256"] = hashlib.sha256(big_file.read_bytes()).hexdigest()
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
original_enabled = CONFIG.enable_community_plugins
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_max_size = CONFIG.plugin_max_file_size_bytes
|
|
try:
|
|
CONFIG.enable_community_plugins = True
|
|
CONFIG.plugin_allowlist = ["community.test"]
|
|
CONFIG.plugin_max_file_size_bytes = 1024
|
|
assert discover_plugins(plugin_dir) == []
|
|
finally:
|
|
CONFIG.enable_community_plugins = original_enabled
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_max_file_size_bytes = original_max_size
|
|
|
|
|
|
def test_symlink_escape_is_rejected(tmp_path: Path):
|
|
outside_dir = tmp_path / "outside"
|
|
outside_dir.mkdir()
|
|
outside_file = outside_dir / "evil.py"
|
|
outside_file.write_text("def register_matchers(traceback):\n return None\n", encoding="utf-8")
|
|
|
|
plugin_dir = tmp_path / "plugins"
|
|
plugin_dir.mkdir()
|
|
symlink_file = plugin_dir / "evil.py"
|
|
|
|
try:
|
|
symlink_file.symlink_to(outside_file)
|
|
except (OSError, NotImplementedError):
|
|
pytest.skip("Symlinks not supported in this environment")
|
|
|
|
digest = hashlib.sha256(outside_file.read_bytes()).hexdigest()
|
|
manifest = {
|
|
"id": "community.evil",
|
|
"name": "Evil Plugin",
|
|
"version": "1.0.0",
|
|
"author": "tests",
|
|
"min_doctor_version": "0.0.0",
|
|
"sha256": digest,
|
|
}
|
|
symlink_file.with_suffix(".json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
original_enabled = CONFIG.enable_community_plugins
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
try:
|
|
CONFIG.enable_community_plugins = True
|
|
CONFIG.plugin_allowlist = ["community.evil"]
|
|
assert discover_plugins(plugin_dir) == []
|
|
finally:
|
|
CONFIG.enable_community_plugins = original_enabled
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
|
|
|
|
def test_scan_plugins_reports_unsigned_without_manifest(tmp_path: Path):
|
|
plugin_dir = tmp_path / "plugins"
|
|
plugin_dir.mkdir()
|
|
(plugin_dir / "example_plugin.py").write_text(
|
|
"def register_matchers(traceback):\n return None\n", encoding="utf-8"
|
|
)
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_blocklist = list(CONFIG.plugin_blocklist)
|
|
try:
|
|
CONFIG.plugin_allowlist = ["community.test"]
|
|
CONFIG.plugin_blocklist = []
|
|
report = scan_plugins(plugin_dir)
|
|
assert len(report) == 1
|
|
assert report[0]["trust"] == TRUST_UNSIGNED
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_blocklist = original_blocklist
|
|
|
|
|
|
def test_scan_plugins_reports_untrusted_when_not_allowlisted(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.test",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
)
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_blocklist = list(CONFIG.plugin_blocklist)
|
|
try:
|
|
CONFIG.plugin_allowlist = []
|
|
CONFIG.plugin_blocklist = []
|
|
report = scan_plugins(plugin_dir)
|
|
assert len(report) == 1
|
|
assert report[0]["trust"] == TRUST_UNTRUSTED
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_blocklist = original_blocklist
|
|
|
|
|
|
def test_scan_plugins_reports_blocked_when_blocklisted(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.bad",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
)
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_blocklist = list(CONFIG.plugin_blocklist)
|
|
try:
|
|
CONFIG.plugin_allowlist = ["community.bad"]
|
|
CONFIG.plugin_blocklist = ["community.bad"]
|
|
report = scan_plugins(plugin_dir)
|
|
assert len(report) == 1
|
|
assert report[0]["trust"] == TRUST_BLOCKED
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_blocklist = original_blocklist
|
|
|
|
|
|
def test_signature_required_missing_is_unsigned(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.sig",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
)
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_required = CONFIG.plugin_signature_required
|
|
original_key = CONFIG.plugin_signature_key
|
|
try:
|
|
CONFIG.plugin_allowlist = ["community.sig"]
|
|
CONFIG.plugin_signature_required = True
|
|
CONFIG.plugin_signature_key = "secret"
|
|
report = scan_plugins(plugin_dir)
|
|
assert report[0]["trust"] == TRUST_UNSIGNED
|
|
assert report[0]["reason"] == "signature_missing"
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_signature_required = original_required
|
|
CONFIG.plugin_signature_key = original_key
|
|
|
|
|
|
def test_signature_required_invalid_is_untrusted(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.sig",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
signature="bad-signature",
|
|
)
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_required = CONFIG.plugin_signature_required
|
|
original_key = CONFIG.plugin_signature_key
|
|
try:
|
|
CONFIG.plugin_allowlist = ["community.sig"]
|
|
CONFIG.plugin_signature_required = True
|
|
CONFIG.plugin_signature_key = "secret"
|
|
report = scan_plugins(plugin_dir)
|
|
assert report[0]["trust"] == TRUST_UNTRUSTED
|
|
assert report[0]["reason"] == "signature_invalid"
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_signature_required = original_required
|
|
CONFIG.plugin_signature_key = original_key
|
|
|
|
|
|
def test_signature_required_valid_is_trusted(tmp_path: Path):
|
|
plugin_dir = _write_plugin(
|
|
tmp_path,
|
|
"community.sig",
|
|
"def register_matchers(traceback):\n return None\n",
|
|
)
|
|
py_file = plugin_dir / "example_plugin.py"
|
|
|
|
signature = _sign_plugin(py_file, "secret")
|
|
manifest_path = plugin_dir / "example_plugin.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
manifest["signature"] = signature
|
|
manifest["signature_alg"] = "hmac-sha256"
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
original_allowlist = list(CONFIG.plugin_allowlist)
|
|
original_required = CONFIG.plugin_signature_required
|
|
original_key = CONFIG.plugin_signature_key
|
|
try:
|
|
CONFIG.plugin_allowlist = ["community.sig"]
|
|
CONFIG.plugin_signature_required = True
|
|
CONFIG.plugin_signature_key = "secret"
|
|
report = scan_plugins(plugin_dir)
|
|
assert report[0]["trust"] == TRUST_TRUSTED
|
|
finally:
|
|
CONFIG.plugin_allowlist = original_allowlist
|
|
CONFIG.plugin_signature_required = original_required
|
|
CONFIG.plugin_signature_key = original_key
|