951 lines
37 KiB
Python
951 lines
37 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import types
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
BROKER_DIR = ROOT / "ops" / "gitea-deploy-broker"
|
|
|
|
|
|
@pytest.fixture
|
|
def modules(monkeypatch):
|
|
if "fcntl" not in sys.modules:
|
|
shim = types.ModuleType("fcntl")
|
|
shim.LOCK_EX, shim.LOCK_UN, shim.flock = 2, 8, lambda *_args: None
|
|
monkeypatch.setitem(sys.modules, "fcntl", shim)
|
|
monkeypatch.syspath_prepend(str(BROKER_DIR))
|
|
sys.modules.pop("deploy", None)
|
|
sys.modules.pop("prepare_mcp_hub_candidate", None)
|
|
controller = importlib.import_module("deploy")
|
|
utility = importlib.import_module("prepare_mcp_hub_candidate")
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
yield controller, utility
|
|
sys.modules.pop("prepare_mcp_hub_candidate", None)
|
|
sys.modules.pop("deploy", None)
|
|
|
|
|
|
@pytest.fixture
|
|
def harness(modules, tmp_path, monkeypatch):
|
|
controller, utility = modules
|
|
revision = "a" * 40
|
|
epoch = "1790888863"
|
|
now = (
|
|
datetime.now(timezone.utc)
|
|
.isoformat(timespec="milliseconds")
|
|
.replace("+00:00", "Z")
|
|
)
|
|
metadata = {
|
|
"id": revision[:12],
|
|
"repository": controller.MCP_HUB_REPOSITORY,
|
|
"revision": revision,
|
|
"tree": "b" * 40,
|
|
"treeDigest": hashlib.sha256(("git-tree:" + "b" * 40).encode()).hexdigest(),
|
|
"builtAt": now,
|
|
}
|
|
repo = tmp_path / "hub.git"
|
|
project = {
|
|
"branch": "main",
|
|
"slug": controller.MCP_HUB_PROJECT,
|
|
"compose_project": controller.MCP_HUB_PROJECT,
|
|
"containers": list(controller.MCP_HUB_CONTAINERS),
|
|
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
|
|
"_repository_path": str(repo),
|
|
"repo_name": "itworx_mcp_hub",
|
|
}
|
|
state = types.SimpleNamespace(
|
|
source=tmp_path / "releases" / revision,
|
|
compose_marker="baseline",
|
|
clock=now,
|
|
ingestion_digest=None,
|
|
images={},
|
|
mutations=[],
|
|
inspect_calls=[],
|
|
)
|
|
state.source.mkdir(parents=True)
|
|
(state.source / "scripts").mkdir()
|
|
draft = state.source / "scripts" / "draft_cve_exceptions.py"
|
|
draft.write_text(
|
|
"import argparse, json\n"
|
|
"from datetime import date, timedelta\n"
|
|
"from pathlib import Path\n"
|
|
"p=argparse.ArgumentParser()\n"
|
|
"p.add_argument('--scan-summary', required=True)\n"
|
|
"p.add_argument('--image-digests', required=True)\n"
|
|
"p.add_argument('--evidence-reference', required=True)\n"
|
|
"p.add_argument('--output', required=True)\n"
|
|
"a=p.parse_args()\n"
|
|
"scan=json.loads(Path(a.scan_summary).read_text())\n"
|
|
"digest=json.loads(Path(a.image_digests).read_text())['images']\n"
|
|
"exceptions=[]\n"
|
|
"for row in scan['unresolvedCriticalOrHigh']:\n"
|
|
" refs=set(row['affectedImages'])\n"
|
|
" affected={s:v['imageId'] for s,v in digest.items() if v['tag'] in refs}\n"
|
|
" for cve in row['cves']:\n"
|
|
" exceptions.append({'id':row['package']+'-'+cve,'package':row['package'],'cve':cve,\n"
|
|
" 'installedVersion':row['installedVersion'],'severity':row['severity'],\n"
|
|
" 'approvalStatus':'pending','riskOwner':'','approver':'','affectedImageDigests':affected,\n"
|
|
" 'evidenceReference':a.evidence_reference})\n"
|
|
"Path(a.output).write_text(json.dumps({'schemaVersion':'1.0','exceptions':exceptions}))\n"
|
|
"",
|
|
encoding="utf-8",
|
|
)
|
|
raw_project = dict(project)
|
|
monkeypatch.setattr(controller, "STATE_ROOT", tmp_path)
|
|
monkeypatch.setattr(controller, "MCP_HUB_RELEASES_ROOT", tmp_path / "releases")
|
|
monkeypatch.setattr(controller, "_mcp_hub_require_trusted_state_root", lambda: None)
|
|
monkeypatch.setattr(controller, "_mcp_hub_require_no_posix_acl", lambda _path: None)
|
|
monkeypatch.setattr(controller, "_root_owned", lambda _path: True)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"load_projects",
|
|
lambda: {controller.MCP_HUB_REPOSITORY: raw_project},
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "effective_mcp_hub_project", lambda repository, value: dict(value)
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "repository_path", lambda _repository, _project: repo
|
|
)
|
|
monkeypatch.setattr(controller, "output", lambda _args: revision)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"validate_commit",
|
|
lambda _repo, branch, commit: commit if branch == "main" else "",
|
|
)
|
|
def deployment_metadata(_repo, _revision, _repository, *, built_at=None):
|
|
result = dict(metadata)
|
|
result["builtAt"] = built_at or state.clock
|
|
datetime.fromisoformat(result["builtAt"].replace("Z", "+00:00"))
|
|
return result
|
|
|
|
monkeypatch.setattr(controller, "deployment_metadata", deployment_metadata)
|
|
monkeypatch.setattr(controller, "mcp_hub_source_date_epoch", lambda *_args: epoch)
|
|
|
|
def stage(_project, _repo, commit):
|
|
assert commit == revision
|
|
return state.source
|
|
|
|
monkeypatch.setattr(controller, "mcp_hub_stage_release_source", stage)
|
|
monkeypatch.setattr(
|
|
controller, "exact_source_matches_revision", lambda *_args: True
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "_mcp_hub_verify_release_tree_metadata", lambda *_args: None
|
|
)
|
|
monkeypatch.setattr(controller, "mcp_hub_validate_secret_files", lambda _model: 21)
|
|
|
|
def rendered(project_arg, commit, repository):
|
|
assert commit == revision
|
|
assert repository == controller.MCP_HUB_REPOSITORY
|
|
services = {}
|
|
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
|
|
if service in controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES:
|
|
local = controller.MCP_HUB_LOCAL_BUILD_IMAGE_REPOSITORIES.get(service)
|
|
if service == "connector-ingestion":
|
|
reference = (
|
|
f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]}@sha256:{state.ingestion_digest}"
|
|
if state.ingestion_digest
|
|
else ""
|
|
)
|
|
else:
|
|
reference = (
|
|
f"{local}:{revision[:12]}"
|
|
if local
|
|
else f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]}@sha256:{index:064x}"
|
|
)
|
|
elif service == "contextforge":
|
|
reference = f"itworx-mcp-hub/contextforge-schema:{revision}"
|
|
elif service == "ingestion-clamd":
|
|
reference = controller.MCP_HUB_INTAKE_CLAMAV_IMAGE
|
|
elif service == "postgres":
|
|
reference = controller.MCP_HUB_POSTGRES_IMAGE
|
|
elif service == "redis":
|
|
reference = controller.MCP_HUB_REDIS_IMAGE
|
|
else:
|
|
reference = f"fixture/{service}@sha256:{index:064x}"
|
|
services[service] = {"image": reference}
|
|
return {"services": services, "testMarker": state.compose_marker}
|
|
|
|
monkeypatch.setattr(controller, "mcp_hub_compose_model", rendered)
|
|
|
|
def inspect(reference):
|
|
state.inspect_calls.append(reference)
|
|
return state.images.get(reference)
|
|
|
|
monkeypatch.setattr(controller, "inspect_image", inspect)
|
|
monkeypatch.setattr(
|
|
controller, "run", lambda *_args, **_kwargs: state.mutations.append("run")
|
|
)
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
|
|
utility.freeze_candidate(controller.MCP_HUB_REPOSITORY, revision)
|
|
freeze_dir = tmp_path / "mcp-hub-source-freezes" / revision
|
|
freeze_doc = json.loads((freeze_dir / "freeze.json").read_text())
|
|
# Publication happens after freeze and advances the wall clock. The plan
|
|
# uses the immutable timestamp and the now-available registry digest.
|
|
state.clock = "2026-10-04T02:03:04.567Z"
|
|
state.ingestion_digest = "f" * 64
|
|
plan = utility.plan_candidate(controller.MCP_HUB_REPOSITORY, revision)
|
|
candidate_dir = tmp_path / "mcp-hub-release-candidates" / revision
|
|
plan_doc = json.loads((candidate_dir / "build.json").read_text())
|
|
model = rendered(project, revision, controller.MCP_HUB_REPOSITORY)
|
|
labels = {
|
|
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
|
|
"org.opencontainers.image.revision": revision,
|
|
"org.opencontainers.image.source-tree": metadata["tree"],
|
|
"org.opencontainers.image.version": metadata["id"],
|
|
"org.opencontainers.image.created": metadata["builtAt"],
|
|
"org.opencontainers.image.source-date-epoch": epoch,
|
|
}
|
|
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
|
|
reference = model["services"][service]["image"]
|
|
image_id = "sha256:" + f"{index:064x}"
|
|
config = {"User": "10001:10001"}
|
|
if (
|
|
service in controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES
|
|
or service == "contextforge"
|
|
):
|
|
config["Labels"] = dict(labels)
|
|
state.images[reference] = {"Id": image_id, "Config": config}
|
|
|
|
def write_input(name: str, value: object) -> Path:
|
|
path = candidate_dir / name
|
|
data = (
|
|
json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n"
|
|
).encode()
|
|
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
with os.fdopen(descriptor, "wb") as handle:
|
|
handle.write(data)
|
|
path.chmod(0o600)
|
|
return path
|
|
|
|
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
|
|
image_id = "sha256:" + f"{index:064x}"
|
|
vuln = {
|
|
"SchemaVersion": 2,
|
|
"Trivy": {"Version": "0.73.0"},
|
|
"CreatedAt": now,
|
|
"Metadata": {
|
|
"ImageID": image_id,
|
|
"ImageConfig": {"architecture": "amd64", "os": "linux"},
|
|
},
|
|
"Results": [
|
|
{
|
|
"Class": "os-pkgs",
|
|
"Packages": [{"Name": "fixture", "Version": "1"}],
|
|
"Vulnerabilities": [],
|
|
}
|
|
],
|
|
}
|
|
secret = {
|
|
"ImageID": image_id,
|
|
"CreatedAt": now,
|
|
"TrivyVersion": "0.73.0",
|
|
"FindingCount": 0,
|
|
"SensitiveFindingDetailsPersisted": False,
|
|
"Results": [{"FindingCount": 0}],
|
|
}
|
|
write_input(f"vulnerability-{service}.json", vuln)
|
|
write_input(f"secret-{service}.json", secret)
|
|
write_input("trivy-db-metadata.json", {"UpdatedAt": now, "DownloadedAt": now})
|
|
|
|
# The reports must match the IDs in the final inspect inventory. Keep a
|
|
# convenient writer for intentional drift cases in each test.
|
|
monkeypatch.setattr(utility, "_test_write_input", write_input, raising=False)
|
|
yield types.SimpleNamespace(
|
|
controller=controller,
|
|
utility=utility,
|
|
revision=revision,
|
|
epoch=epoch,
|
|
metadata=metadata,
|
|
project=project,
|
|
state=state,
|
|
candidate_dir=candidate_dir,
|
|
model=model,
|
|
write_input=write_input,
|
|
now=now,
|
|
plan=plan,
|
|
plan_doc=plan_doc,
|
|
freeze_dir=freeze_dir,
|
|
freeze_doc=freeze_doc,
|
|
)
|
|
|
|
|
|
def test_plan_freezes_protected_metadata_without_candidate_or_approval(harness):
|
|
h = harness
|
|
document = json.loads((h.candidate_dir / "build.json").read_text())
|
|
assert document["schema"] == "itworx.mcp-hub-build-plan/v1"
|
|
assert document["repository"] == h.controller.MCP_HUB_REPOSITORY
|
|
assert document["revision"] == h.revision
|
|
assert document["build"] == h.metadata
|
|
assert document["build"]["builtAt"] == h.freeze_doc["build"]["builtAt"]
|
|
assert document["build"]["builtAt"] != h.state.clock
|
|
assert document["sourceDateEpoch"] == h.epoch
|
|
assert len(document["composeSha256"]) == 64
|
|
assert h.state.ingestion_digest in str(h.model)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
assert not (h.candidate_dir / "approval.json").exists()
|
|
assert h.state.mutations == []
|
|
|
|
|
|
def _remove_candidate_and_freeze(h):
|
|
shutil.rmtree(h.candidate_dir, ignore_errors=True)
|
|
shutil.rmtree(h.freeze_dir.parent, ignore_errors=True)
|
|
|
|
|
|
def _prepare_freeze_dir(h):
|
|
h.freeze_dir.parent.mkdir(mode=0o700)
|
|
h.freeze_dir.mkdir(mode=0o700)
|
|
|
|
|
|
def test_plan_requires_existing_freeze_before_any_candidate_write(harness):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
with pytest.raises(
|
|
h.controller.DeployError,
|
|
match="run freeze, publish the fixed ingestion image, then run plan",
|
|
):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_freeze_is_non_replayable(harness):
|
|
h = harness
|
|
original = (h.freeze_dir / "freeze.json").read_bytes()
|
|
with pytest.raises(h.controller.DeployError, match="never overwritten|already exists"):
|
|
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert (h.freeze_dir / "freeze.json").read_bytes() == original
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mutation", "message"),
|
|
[
|
|
("revision", "freeze record.*source identity"),
|
|
("tree", "freeze record.*source identity"),
|
|
("calendar", "freeze record.*source identity"),
|
|
("epoch", "freeze record.*source epoch"),
|
|
("duplicate", "unavailable or invalid|duplicate"),
|
|
("partial", "unavailable or invalid|partial"),
|
|
],
|
|
)
|
|
def test_plan_rejects_invalid_freeze_records_without_candidate_writes(
|
|
harness, mutation, message
|
|
):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
_prepare_freeze_dir(h)
|
|
record = dict(h.freeze_doc)
|
|
if mutation == "revision":
|
|
record["revision"] = "c" * 40
|
|
payload = json.dumps(record).encode()
|
|
elif mutation == "tree":
|
|
record["build"] = dict(record["build"])
|
|
record["build"]["tree"] = "d" * 40
|
|
payload = json.dumps(record).encode()
|
|
elif mutation == "calendar":
|
|
record["build"] = dict(record["build"])
|
|
record["build"]["builtAt"] = "2026-99-99T00:00:00.000Z"
|
|
payload = json.dumps(record).encode()
|
|
elif mutation == "epoch":
|
|
record["sourceDateEpoch"] = "1790888864"
|
|
payload = json.dumps(record).encode()
|
|
elif mutation == "duplicate":
|
|
payload = b'{"schema":"duplicate","schema":"duplicate"}'
|
|
else:
|
|
payload = b'{"schema":'
|
|
(h.freeze_dir / "freeze.json").write_bytes(payload)
|
|
(h.freeze_dir / "freeze.json").chmod(0o600)
|
|
with pytest.raises(h.controller.DeployError, match=message):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_plan_rejects_symlink_freeze_file_without_candidate_writes(harness, tmp_path):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
_prepare_freeze_dir(h)
|
|
outside = tmp_path / "outside.json"
|
|
outside.write_text(json.dumps(h.freeze_doc))
|
|
try:
|
|
(h.freeze_dir / "freeze.json").symlink_to(outside)
|
|
except OSError:
|
|
pytest.skip("symlink creation is unavailable on this filesystem")
|
|
with pytest.raises(
|
|
h.controller.DeployError,
|
|
match="unavailable or invalid|unsafe|not a regular file",
|
|
):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_plan_rejects_fifo_freeze_record_without_blocking(harness):
|
|
if os.name == "nt" or not hasattr(os, "mkfifo"):
|
|
pytest.skip("FIFO files are a POSIX filesystem contract")
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
_prepare_freeze_dir(h)
|
|
os.mkfifo(h.freeze_dir / "freeze.json", 0o600)
|
|
with pytest.raises(h.controller.DeployError, match="not a regular file"):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_plan_rejects_unsafe_freeze_directory_mode_without_candidate_writes(harness):
|
|
h = harness
|
|
if os.name == "nt":
|
|
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
|
|
_remove_candidate_and_freeze(h)
|
|
_prepare_freeze_dir(h)
|
|
(h.freeze_dir / "freeze.json").write_text(json.dumps(h.freeze_doc))
|
|
(h.freeze_dir / "freeze.json").chmod(0o600)
|
|
h.freeze_dir.chmod(0o755)
|
|
with pytest.raises(h.controller.DeployError, match="private|root-owned|ACL"):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fault", ["file_mode", "owner", "dir_owner", "acl", "dir_acl"]
|
|
)
|
|
def test_plan_rejects_unsafe_freeze_file_metadata_without_candidate_writes(
|
|
harness, monkeypatch, fault
|
|
):
|
|
h = harness
|
|
if fault == "file_mode" and os.name == "nt":
|
|
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
|
|
_remove_candidate_and_freeze(h)
|
|
_prepare_freeze_dir(h)
|
|
freeze_file = h.freeze_dir / "freeze.json"
|
|
freeze_file.write_text(json.dumps(h.freeze_doc))
|
|
freeze_file.chmod(0o600)
|
|
if fault == "file_mode":
|
|
freeze_file.chmod(0o644)
|
|
match = "unsafe"
|
|
elif fault in {"owner", "dir_owner"}:
|
|
original = h.controller._root_owned
|
|
denied_path = h.freeze_dir if fault == "dir_owner" else freeze_file
|
|
monkeypatch.setattr(
|
|
h.controller,
|
|
"_root_owned",
|
|
lambda path: False if Path(path) == denied_path else original(path),
|
|
)
|
|
match = "root-owned|unsafe|owner"
|
|
else:
|
|
original = h.controller._mcp_hub_require_no_posix_acl
|
|
|
|
def reject_freeze_acl(path):
|
|
if Path(path) == (h.freeze_dir if fault == "dir_acl" else freeze_file):
|
|
raise h.controller.DeployError("freeze ACL denied")
|
|
original(path)
|
|
|
|
monkeypatch.setattr(h.controller, "_mcp_hub_require_no_posix_acl", reject_freeze_acl)
|
|
match = "ACL"
|
|
with pytest.raises(h.controller.DeployError, match=match):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_freeze_rejects_nonexact_local_main_head_without_writes(harness, monkeypatch):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
monkeypatch.setattr(h.controller, "output", lambda _args: "c" * 40)
|
|
with pytest.raises(h.controller.DeployError, match="exact protected main head"):
|
|
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.freeze_dir.parent.exists()
|
|
|
|
|
|
def test_freeze_rejects_staged_source_mismatch_without_freeze_record(harness, monkeypatch):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
monkeypatch.setattr(h.controller, "exact_source_matches_revision", lambda *_args: False)
|
|
with pytest.raises(h.controller.DeployError, match="Staged MCP Hub source differs"):
|
|
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.freeze_dir.parent.exists()
|
|
|
|
|
|
def test_freeze_rejects_invalid_epoch_before_staging_or_freeze_record(harness, monkeypatch):
|
|
h = harness
|
|
_remove_candidate_and_freeze(h)
|
|
monkeypatch.setattr(h.controller, "mcp_hub_source_date_epoch", lambda *_args: "invalid")
|
|
monkeypatch.setattr(
|
|
h.controller,
|
|
"mcp_hub_stage_release_source",
|
|
lambda *_args: pytest.fail("invalid epoch must fail before source staging"),
|
|
)
|
|
with pytest.raises(h.controller.DeployError, match="source epoch"):
|
|
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.freeze_dir.parent.exists()
|
|
|
|
|
|
def test_plan_rejects_staged_source_drift_before_candidate_writes(harness, monkeypatch):
|
|
h = harness
|
|
shutil.rmtree(h.candidate_dir)
|
|
monkeypatch.setattr(h.controller, "exact_source_matches_revision", lambda *_args: False)
|
|
with pytest.raises(h.controller.DeployError, match="Staged MCP Hub source differs"):
|
|
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not h.candidate_dir.exists()
|
|
|
|
|
|
def test_freeze_wrong_repository_and_nonroot_fail_before_writes(modules, monkeypatch, tmp_path):
|
|
controller, utility = modules
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
monkeypatch.setattr(controller, "STATE_ROOT", tmp_path)
|
|
with pytest.raises(controller.DeployError, match="fixed MCP Hub repository"):
|
|
utility.freeze_candidate("someone/else", "a" * 40)
|
|
assert list(tmp_path.iterdir()) == []
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: False)
|
|
with pytest.raises(controller.DeployError, match="host root"):
|
|
utility.freeze_candidate(controller.MCP_HUB_REPOSITORY, "a" * 40)
|
|
assert list(tmp_path.iterdir()) == []
|
|
def test_plan_candidate_freezes_the_selected_workflow_creation_timestamp(
|
|
harness, monkeypatch
|
|
):
|
|
h = harness
|
|
revision = h.revision
|
|
built_at = "2026-10-03T12:34:56.789Z"
|
|
observed = []
|
|
metadata = {
|
|
"id": revision[:12],
|
|
"repository": h.controller.MCP_HUB_REPOSITORY,
|
|
"revision": revision,
|
|
"tree": "d" * 40,
|
|
"treeDigest": hashlib.sha256(("git-tree:" + "d" * 40).encode()).hexdigest(),
|
|
"builtAt": built_at,
|
|
}
|
|
monkeypatch.setattr(h.controller, "output", lambda _args: revision)
|
|
monkeypatch.setattr(h.controller, "validate_commit", lambda _repo, _branch, commit: commit)
|
|
|
|
def deployment_metadata(_repo, supplied_revision, repository, *, built_at=None):
|
|
observed.append(built_at)
|
|
assert supplied_revision == revision
|
|
assert repository == h.controller.MCP_HUB_REPOSITORY
|
|
return dict(metadata)
|
|
|
|
monkeypatch.setattr(h.controller, "deployment_metadata", deployment_metadata)
|
|
monkeypatch.setattr(h.controller, "mcp_hub_stage_release_source", lambda *_args: h.state.source)
|
|
monkeypatch.setattr(h.controller, "exact_source_matches_revision", lambda *_args: True)
|
|
compose_calls = []
|
|
|
|
def fixed_compose_modeler(project, revision, repository):
|
|
compose_calls.append((project, revision, repository))
|
|
return {
|
|
"services": {
|
|
service: {"image": f"fixture/{service}:candidate"}
|
|
for service in h.controller.MCP_HUB_SERVICES
|
|
}
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
h.controller,
|
|
"mcp_hub_compose_model",
|
|
lambda *_args: pytest.fail("plan used the ambient Compose renderer"),
|
|
)
|
|
|
|
staging = (
|
|
h.controller.STATE_ROOT
|
|
/ "mcp-hub-release-imports"
|
|
/ revision
|
|
/ ("1" * 32)
|
|
/ revision
|
|
)
|
|
h.utility.plan_candidate(
|
|
h.controller.MCP_HUB_REPOSITORY,
|
|
revision,
|
|
built_at=built_at,
|
|
staging_directory=staging,
|
|
compose_modeler=fixed_compose_modeler,
|
|
)
|
|
|
|
plan = json.loads(
|
|
(staging / "build.json").read_text()
|
|
)
|
|
assert observed == [built_at, built_at]
|
|
assert plan["build"]["builtAt"] == built_at
|
|
assert len(compose_calls) == 1
|
|
|
|
|
|
def test_candidate_preparation_rejects_staging_path_outside_private_import_anchor(
|
|
harness,
|
|
):
|
|
h = harness
|
|
revision = h.revision
|
|
staging = h.controller.STATE_ROOT / "outside-imports" / revision
|
|
with pytest.raises(h.controller.DeployError, match="private anchor|staging path"):
|
|
h.utility.plan_candidate(
|
|
h.controller.MCP_HUB_REPOSITORY,
|
|
revision,
|
|
built_at="2026-10-03T12:34:56.789Z",
|
|
staging_directory=staging,
|
|
)
|
|
assert not staging.exists()
|
|
|
|
|
|
def test_candidate_preparation_rejects_symlinked_staging_destination(harness):
|
|
h = harness
|
|
revision = h.revision
|
|
anchor = h.controller.STATE_ROOT / "mcp-hub-release-imports" / revision
|
|
token = anchor / ("2" * 32)
|
|
token.mkdir(parents=True, mode=0o700)
|
|
target = h.controller.STATE_ROOT / "untrusted-target"
|
|
target.mkdir(mode=0o700)
|
|
staging = token / revision
|
|
try:
|
|
staging.symlink_to(target, target_is_directory=True)
|
|
except OSError:
|
|
pytest.skip("symlink creation is unavailable on this filesystem")
|
|
with pytest.raises(h.controller.DeployError):
|
|
h.utility.plan_candidate(
|
|
h.controller.MCP_HUB_REPOSITORY,
|
|
revision,
|
|
built_at="2026-10-03T12:34:56.789Z",
|
|
staging_directory=staging,
|
|
)
|
|
|
|
|
|
def test_candidate_preparation_rejects_nonprivate_staging_parent(harness):
|
|
if os.name == "nt":
|
|
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
|
|
h = harness
|
|
revision = h.revision
|
|
anchor = h.controller.STATE_ROOT / "mcp-hub-release-imports" / revision
|
|
token = anchor / ("3" * 32)
|
|
token.mkdir(parents=True, mode=0o700)
|
|
token.chmod(0o755)
|
|
staging = token / revision
|
|
with pytest.raises(h.controller.DeployError, match="private|safe|canonical"):
|
|
h.utility.plan_candidate(
|
|
h.controller.MCP_HUB_REPOSITORY,
|
|
revision,
|
|
built_at="2026-10-03T12:34:56.789Z",
|
|
staging_directory=staging,
|
|
)
|
|
|
|
|
|
def test_wrong_repository_and_nonroot_fail_before_controller_access(
|
|
modules, monkeypatch
|
|
):
|
|
controller, utility = modules
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
monkeypatch.setattr(
|
|
controller, "load_projects", lambda: pytest.fail("must reject repo first")
|
|
)
|
|
with pytest.raises(controller.DeployError, match="fixed MCP Hub repository"):
|
|
utility.plan_candidate("someone/else", "a" * 40)
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: False)
|
|
with pytest.raises(controller.DeployError, match="root"):
|
|
utility.plan_candidate(controller.MCP_HUB_REPOSITORY, "a" * 40)
|
|
|
|
|
|
def test_non_exact_main_head_is_denied_before_staging(modules, monkeypatch, tmp_path):
|
|
controller, utility = modules
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
project = {
|
|
"branch": "main",
|
|
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
|
|
}
|
|
monkeypatch.setattr(
|
|
controller, "load_projects", lambda: {controller.MCP_HUB_REPOSITORY: project}
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "effective_mcp_hub_project", lambda _repo, value: value
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "repository_path", lambda *_args: tmp_path / "hub.git"
|
|
)
|
|
monkeypatch.setattr(controller, "output", lambda _args: "c" * 40)
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"mcp_hub_stage_release_source",
|
|
lambda *_args: pytest.fail("must not stage"),
|
|
)
|
|
with pytest.raises(controller.DeployError, match="exact protected main head"):
|
|
utility.plan_candidate(controller.MCP_HUB_REPOSITORY, "a" * 40)
|
|
|
|
|
|
def test_capture_emits_candidate_and_pending_only_envelope_without_apply_calls(
|
|
harness, monkeypatch
|
|
):
|
|
h = harness
|
|
calls = []
|
|
original_run = h.utility.subprocess.run
|
|
|
|
def observe_draft(command, **kwargs):
|
|
calls.append((command, kwargs))
|
|
return original_run(command, **kwargs)
|
|
|
|
monkeypatch.setattr(h.utility.subprocess, "run", observe_draft)
|
|
result = h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
candidate = json.loads((h.candidate_dir / "candidate.json").read_text())
|
|
exceptions = json.loads((h.candidate_dir / "exceptions.json").read_text())
|
|
digests = json.loads((h.candidate_dir / "image-digests.json").read_text())
|
|
assert result["status"] == "pending_review"
|
|
assert set(candidate["images"]) == set(h.controller.MCP_HUB_SERVICES)
|
|
assert candidate["images"]["connector-ingestion"]["imageRef"].endswith(
|
|
"@sha256:" + h.state.ingestion_digest
|
|
)
|
|
assert candidate["build"]["builtAt"] == h.freeze_doc["build"]["builtAt"]
|
|
assert digests["images"]["web"]["tag"] == candidate["images"]["web"]["imageRef"]
|
|
assert digests["images"]["web"]["imageId"] == candidate["images"]["web"]["imageId"]
|
|
candidate_bytes = (h.candidate_dir / "candidate.json").read_bytes()
|
|
exceptions_bytes = (h.candidate_dir / "exceptions.json").read_bytes()
|
|
assert result["candidateSha256"] == hashlib.sha256(candidate_bytes).hexdigest()
|
|
assert result["exceptionsSha256"] == hashlib.sha256(exceptions_bytes).hexdigest()
|
|
assert not (h.candidate_dir / "approval.json").exists()
|
|
assert all(item["approvalStatus"] == "pending" for item in exceptions["exceptions"])
|
|
assert h.state.mutations == []
|
|
assert len(h.state.inspect_calls) == 17
|
|
assert len(calls) == 1
|
|
assert (
|
|
Path(calls[0][0][1]) == h.state.source / "scripts" / "draft_cve_exceptions.py"
|
|
)
|
|
assert calls[0][1]["stdout"] is subprocess.DEVNULL
|
|
assert calls[0][1]["stderr"] is subprocess.DEVNULL
|
|
assert set(calls[0][1]["env"]) == {
|
|
"PATH",
|
|
"PYTHONDONTWRITEBYTECODE",
|
|
"PYTHONIOENCODING",
|
|
}
|
|
|
|
|
|
def test_capture_can_use_the_importers_fixed_image_inspector(harness, monkeypatch):
|
|
h = harness
|
|
inspected = []
|
|
|
|
def fixed_inspector(reference):
|
|
inspected.append(reference)
|
|
return h.state.images.get(reference)
|
|
|
|
monkeypatch.setattr(
|
|
h.controller,
|
|
"inspect_image",
|
|
lambda _reference: pytest.fail("capture used the ambient Docker context"),
|
|
)
|
|
monkeypatch.setattr(
|
|
h.controller,
|
|
"mcp_hub_compose_model",
|
|
lambda *_args: pytest.fail("capture used the ambient Compose renderer"),
|
|
)
|
|
result = h.utility.capture_candidate(
|
|
h.controller.MCP_HUB_REPOSITORY,
|
|
h.revision,
|
|
image_inspector=fixed_inspector,
|
|
compose_modeler=lambda *_args: h.model,
|
|
)
|
|
assert result["status"] == "pending_review"
|
|
assert len(inspected) == 17
|
|
assert set(inspected) == {
|
|
row["image"] for row in h.model["services"].values()
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", ["label", "root"])
|
|
def test_capture_rejects_contextforge_provenance_drift_without_candidate(
|
|
harness, mutation
|
|
):
|
|
h = harness
|
|
reference = h.model["services"]["contextforge"]["image"]
|
|
config = h.state.images[reference]["Config"]
|
|
if mutation == "label":
|
|
config["Labels"]["org.opencontainers.image.source-tree"] = "wrong"
|
|
else:
|
|
config["User"] = "0:10001"
|
|
with pytest.raises(h.controller.DeployError, match="provenance|non-root runtime"):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
assert not (h.candidate_dir / "approval.json").exists()
|
|
assert h.state.mutations == []
|
|
|
|
|
|
def test_pending_high_findings_are_drafted_from_the_exact_staged_source(harness):
|
|
h = harness
|
|
report_path = h.candidate_dir / "vulnerability-web.json"
|
|
report = json.loads(report_path.read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "libfixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1.2.3",
|
|
"Severity": "HIGH",
|
|
}
|
|
]
|
|
report_path.unlink()
|
|
h.write_input(report_path.name, report)
|
|
result = h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
candidate = json.loads((h.candidate_dir / "candidate.json").read_text())
|
|
summary = json.loads((h.candidate_dir / "scan-summary.json").read_text())
|
|
exceptions = json.loads((h.candidate_dir / "exceptions.json").read_text())[
|
|
"exceptions"
|
|
]
|
|
row = summary["unresolvedCriticalOrHigh"][0]
|
|
assert row["affectedImages"] == [candidate["images"]["web"]["imageRef"]]
|
|
assert exceptions[0]["approvalStatus"] == "pending"
|
|
assert exceptions[0]["package"] == "libfixture"
|
|
assert exceptions[0]["cve"] == "CVE-2026-12345"
|
|
assert exceptions[0]["affectedImageDigests"] == {
|
|
"web": candidate["images"]["web"]["imageId"]
|
|
}
|
|
assert result["status"] == "pending_review"
|
|
assert not h.state.mutations
|
|
|
|
|
|
def test_findings_for_same_cve_at_distinct_versions_remain_distinct(harness):
|
|
h = harness
|
|
for service, version in (("web", "1.2.3"), ("worker", "4.5.6")):
|
|
path = h.candidate_dir / f"vulnerability-{service}.json"
|
|
report = json.loads(path.read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "libfixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": version,
|
|
"Severity": "HIGH",
|
|
}
|
|
]
|
|
path.unlink()
|
|
h.write_input(path.name, report)
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
summary = json.loads((h.candidate_dir / "scan-summary.json").read_text())
|
|
assert {row["installedVersion"] for row in summary["unresolvedCriticalOrHigh"]} == {
|
|
"1.2.3",
|
|
"4.5.6",
|
|
}
|
|
|
|
|
|
def test_same_package_cve_version_with_conflicting_severity_fails_closed(harness):
|
|
h = harness
|
|
for service, severity in (("web", "HIGH"), ("worker", "CRITICAL")):
|
|
path = h.candidate_dir / f"vulnerability-{service}.json"
|
|
report = json.loads(path.read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "libfixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1.2.3",
|
|
"Severity": severity,
|
|
}
|
|
]
|
|
path.unlink()
|
|
h.write_input(path.name, report)
|
|
with pytest.raises(h.controller.DeployError, match="conflicting scan severities"):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
|
|
|
|
def test_draft_for_all_images_is_rejected_when_finding_scope_is_narrower(harness):
|
|
h = harness
|
|
path = h.candidate_dir / "vulnerability-web.json"
|
|
report = json.loads(path.read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "libfixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1.2.3",
|
|
"Severity": "HIGH",
|
|
}
|
|
]
|
|
path.unlink()
|
|
h.write_input(path.name, report)
|
|
draft_script = h.state.source / "scripts" / "draft_cve_exceptions.py"
|
|
draft_script.write_text(
|
|
draft_script.read_text().replace("if v['tag'] in refs", "if True"),
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(h.controller.DeployError, match="pending-only document"):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
|
|
|
|
def test_missing_scan_report_fails_without_publishing_candidate(harness):
|
|
h = harness
|
|
(h.candidate_dir / "secret-web.json").unlink()
|
|
with pytest.raises(h.controller.DeployError):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
assert not (h.candidate_dir / "approval.json").exists()
|
|
assert h.state.mutations == []
|
|
|
|
|
|
def test_frozen_compose_drift_and_image_identity_drift_fail_closed(harness):
|
|
h = harness
|
|
h.state.compose_marker = "changed"
|
|
with pytest.raises(h.controller.DeployError, match="Compose"):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
h.state.compose_marker = "baseline"
|
|
web_ref = h.model["services"]["web"]["image"]
|
|
h.state.images[web_ref]["Id"] = "sha256:" + "f" * 64
|
|
with pytest.raises(h.controller.DeployError):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
|
|
|
|
def test_fixable_critical_or_high_finding_is_not_captured(harness):
|
|
h = harness
|
|
path = h.candidate_dir / "vulnerability-web.json"
|
|
report = json.loads(path.read_text())
|
|
report["Results"][0]["Vulnerabilities"] = [
|
|
{
|
|
"PkgName": "libfixture",
|
|
"VulnerabilityID": "CVE-2026-12345",
|
|
"InstalledVersion": "1.2.3",
|
|
"FixedVersion": "1.2.4",
|
|
"Severity": "CRITICAL",
|
|
}
|
|
]
|
|
path.unlink()
|
|
h.write_input(path.name, report)
|
|
with pytest.raises(h.controller.DeployError, match="fixable"):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert not (h.candidate_dir / "candidate.json").exists()
|
|
|
|
|
|
def test_capture_never_overwrites_existing_records(harness):
|
|
h = harness
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
original = (h.candidate_dir / "candidate.json").read_bytes()
|
|
with pytest.raises(h.controller.DeployError):
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
assert (h.candidate_dir / "candidate.json").read_bytes() == original
|
|
assert h.state.mutations == []
|
|
|
|
|
|
def test_capture_preserves_full_supported_trivy_database_metadata(harness):
|
|
h = harness
|
|
path = h.candidate_dir / "trivy-db-metadata.json"
|
|
path.unlink()
|
|
metadata = {
|
|
"UpdatedAt": h.now,
|
|
"DownloadedAt": h.now,
|
|
"Version": 2,
|
|
"NextUpdate": h.now,
|
|
}
|
|
h.write_input(path.name, metadata)
|
|
h.utility.capture_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
|
|
candidate = json.loads((h.candidate_dir / "candidate.json").read_text())
|
|
assert (
|
|
candidate["databaseMetadataSha256"]
|
|
== hashlib.sha256(path.read_bytes()).hexdigest()
|
|
)
|
|
assert json.loads(path.read_text()) == metadata
|
|
|
|
|
|
def test_cli_requires_explicit_fixed_target_and_subcommand(modules, monkeypatch):
|
|
_controller, utility = modules
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: False)
|
|
with pytest.raises(SystemExit):
|
|
utility.main([])
|
|
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
|
|
assert (
|
|
utility.main(["plan", "--repository", "other/repo", "--revision", "a" * 40])
|
|
== 2
|
|
)
|