Files
kwonta3wook 031b8ab298 feat: add Linux x86-64 MCPTrace analysis
Add a Linux task/VMA adapter and POSIX path policy checks. Share scanning and
output with Windows while preserving its existing VAD fields. Report skipped
address spaces and scan coverage, and test both adapters across host platforms.
2026-10-27 13:49:51 +09:00

163 lines
5.0 KiB
Python

import copy
import pytest
from test_core import carve, request
from mcptrace.policy import evaluate, validate_policy
def rule(operator, value, argument="/path"):
return {
"id": "R1",
"description": "Analyst-defined policy",
"severity": "medium",
"tool": "read_file",
"argument": argument,
"operator": operator,
"value": value,
}
def run_rule(value, operator="windows_path_outside", expected=None):
spec = rule(operator, expected if expected is not None else [r"C:\Cases"])
rules = validate_policy({"version": 1, "rules": [spec]})
return evaluate(carve(request(tool="read_file", arguments={"path": value})), rules)
@pytest.mark.parametrize(
"value",
[r"C:\Cases\evidence.txt", r"c:\CASES\sub\a.txt", r"C:/Cases/a.txt", r"\\?\C:\Cases\a.txt"],
)
def test_allowed_path_boundaries(value):
assert run_rule(value) == []
@pytest.mark.parametrize("value", [r"C:\Cases2\a.txt", r"C:\Cases\..\secret.txt", r"D:\Cases\a"])
def test_outside_paths_match(value):
assert run_rule(value)[0]["status"] == "policy-match"
@pytest.mark.parametrize(
"value",
[
r"C:relative.txt",
r"\rooted.txt",
r"%USERPROFILE%\a",
3,
None,
r"C:\Cases\a.txt:secret",
r"C:\Cases\a. ",
r"C:\Cases\*.txt",
],
)
def test_unresolved_inputs_are_not_marked_safe(value):
assert run_rule(value)[0]["status"] == "not-evaluated"
@pytest.mark.parametrize(
"url,matched",
[
("https://API.EXAMPLE.TEST./x", False),
("https://api.example.test.evil.test/x", True),
("https://api.example.test@evil.test/x", True),
("https://evil.test/?allowed=api.example.test", True),
("https://api.example.test:443/path", False),
],
)
def test_url_host_uses_parsed_authority(url, matched):
findings = run_rule(url, "url_host_not_in", ["api.example.test"])
assert bool(findings) == matched
def test_policy_requires_exact_tool_and_complete_request():
rules = validate_policy({"version": 1, "rules": [rule("contains", "secret")]})
assert evaluate(carve(request(tool="another_tool", arguments={"path": "secret"})), rules) == []
record = carve(request(tool="read_file", arguments={"path": "secret"}))
assert evaluate(record, rules)[0]["status"] == "policy-match"
record.state = "fragment"
assert evaluate(record, rules) == []
def test_json_pointer_and_missing_arguments():
spec = rule("equals", "secret", "/nested/a~1b/0")
rules = validate_policy({"version": 1, "rules": [spec]})
rec = carve(request(tool="read_file", arguments={"nested": {"a/b": ["secret"]}}))
assert evaluate(rec, rules)[0]["status"] == "policy-match"
rec.obj["params"]["arguments"] = {}
assert evaluate(rec, rules)[0]["status"] == "not-evaluated"
@pytest.mark.parametrize(
"change",
[
{"operator": "execute"},
{"severity": "critical"},
{"extra": "ignored?"},
{"argument": "path"},
{"value": []},
{"value": ["relative"]},
{"argument": "/bad~2pointer"},
],
)
def test_bad_policy_is_rejected(change):
spec = rule("windows_path_outside", [r"C:\Cases"])
spec.update(change)
with pytest.raises(ValueError):
validate_policy({"version": 1, "rules": [spec]})
def test_duplicate_rule_ids_rejected():
spec = rule("contains", "x")
with pytest.raises(ValueError):
validate_policy({"version": 1, "rules": [spec, copy.deepcopy(spec)]})
@pytest.mark.parametrize(
"value,matched",
[
("/srv/cases/a.txt", False),
("/srv/cases", False),
("/srv/cases/", False),
("/srv/cases/./a", False),
("/srv/cases/sub/../a", False),
("/srv/cases2/a", True),
("/srv/cases/../secret", True),
("/etc/shadow", True),
("/SRV/CASES/a", True),
(r"/srv/cases\..\secret", True),
(r"/srv/cases/a\b", False),
],
)
def test_posix_path_semantics_are_independent_of_analysis_host(value, matched):
findings = run_rule(value, "posix_path_outside", ["/srv/cases"])
assert bool(findings) == matched
if matched:
assert findings[0]["status"] == "policy-match"
@pytest.mark.parametrize(
"value",
[
"relative",
"~/cases/a",
"$HOME/a",
"/srv/$CASE/a",
"//server/share",
"/srv/cases/*.txt",
"/srv/cases/a\0b",
],
)
def test_unresolved_posix_paths_are_not_evaluated(value):
assert run_rule(value, "posix_path_outside", ["/srv/cases"])[0]["status"] == "not-evaluated"
@pytest.mark.parametrize("root", ["cases", "~/cases", "//server/share", "/$HOME", "/srv/*"])
def test_invalid_posix_allowlist_is_rejected(root):
with pytest.raises(ValueError, match="POSIX"):
validate_policy({"version": 1, "rules": [rule("posix_path_outside", [root])]})
def test_posix_root_and_multiple_allowed_roots():
assert run_rule("/etc/shadow", "posix_path_outside", ["/"]) == []
assert run_rule("/tmp/cases/a", "posix_path_outside", ["/srv/cases", "/tmp/cases"]) == []