Files

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