Publish curated ITWorx_MCP_Hub source from 2aa63ba527a98b38b4d28f2a1bb85b1b1dbe7f6d
This commit is contained in:
1 parent
49627c7a7c
commit
378ae83d30
22 files changed
+4055
-81
No files matched your search
+3
@@ -22,3 +22,6 @@ INSPECTOR_BIND_IP=127.0.0.1
|
||||
COMPOSE_PROFILES=observability
|
||||
DEMO_TOKEN_REVOCATIONS_DIR_HOST=/tmp
|
||||
APPOPS_BUILD_DATE=2026-10-01T18:00:00Z
|
||||
APPOPS_SOURCE_TREE=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
APPOPS_BUILD_ID=aaaaaaaaaaaa
|
||||
SOURCE_DATE_EPOCH=1790888863
|
||||
@@ -259,16 +259,43 @@ class ComposeContractTests(unittest.TestCase):
|
||||
arguments.get("BUILD_DATE"),
|
||||
"${APPOPS_BUILD_DATE:?APPOPS_BUILD_DATE is required}",
|
||||
)
|
||||
for argument, value in {
|
||||
"GIT_SOURCE_TREE": "${APPOPS_SOURCE_TREE:?APPOPS_SOURCE_TREE is required}",
|
||||
"IMAGE_VERSION": "${APPOPS_BUILD_ID:?APPOPS_BUILD_ID is required}",
|
||||
"SOURCE_DATE_EPOCH": "${SOURCE_DATE_EPOCH:?SOURCE_DATE_EPOCH is required}",
|
||||
}.items():
|
||||
self.assertEqual(arguments.get(argument), value, name)
|
||||
original_arguments = self.document["services"][name]["build"].get("args", {})
|
||||
for key, value in original_arguments.items():
|
||||
self.assertEqual(arguments.get(key), value)
|
||||
|
||||
def test_build_dockerfiles_emit_controller_tree_version_and_epoch_labels(self) -> None:
|
||||
dockerfiles = (
|
||||
"infrastructure/images/python-service/Dockerfile",
|
||||
"infrastructure/images/docker-read-broker/Dockerfile",
|
||||
"infrastructure/images/observability/Dockerfile",
|
||||
)
|
||||
expected_labels = {
|
||||
"org.opencontainers.image.source-tree": "GIT_SOURCE_TREE",
|
||||
"org.opencontainers.image.version": "IMAGE_VERSION",
|
||||
"org.opencontainers.image.source-date-epoch": "SOURCE_DATE_EPOCH",
|
||||
}
|
||||
for relative_path in dockerfiles:
|
||||
text = (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
with self.subTest(dockerfile=relative_path):
|
||||
for label, argument in expected_labels.items():
|
||||
self.assertIn(f"ARG {argument}", text)
|
||||
self.assertIn(f'{label}="${{{argument}}}"', text)
|
||||
|
||||
def test_real_autodeploy_preserves_intake_digest_and_owned_build_revision(self) -> None:
|
||||
docker = shutil.which("docker")
|
||||
if docker is None:
|
||||
self.skipTest("docker CLI is not available in this environment")
|
||||
revision = "a" * 40
|
||||
built_at = "2026-10-01T18:00:00Z"
|
||||
source_tree = "b" * 40
|
||||
image_version = revision[:12]
|
||||
source_date_epoch = "1790888863"
|
||||
with tempfile.TemporaryDirectory() as secrets_dir:
|
||||
for name in self.document["secrets"]:
|
||||
(Path(secrets_dir) / name).write_text("test-fixture-secret\n", encoding="utf-8")
|
||||
@@ -301,6 +328,9 @@ class ComposeContractTests(unittest.TestCase):
|
||||
"CONNECTOR_INGESTION_IMAGE": f"registry.example.test/intake@sha256:{'b' * 64}",
|
||||
"GITEA_COMMIT_SHA": revision,
|
||||
"APPOPS_BUILD_DATE": built_at,
|
||||
"APPOPS_SOURCE_TREE": source_tree,
|
||||
"APPOPS_BUILD_ID": image_version,
|
||||
"SOURCE_DATE_EPOCH": source_date_epoch,
|
||||
},
|
||||
)
|
||||
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||
@@ -316,6 +346,9 @@ class ComposeContractTests(unittest.TestCase):
|
||||
continue
|
||||
self.assertEqual(service["build"]["args"]["GIT_REVISION"], revision, name)
|
||||
self.assertEqual(service["build"]["args"]["BUILD_DATE"], built_at, name)
|
||||
self.assertEqual(service["build"]["args"]["GIT_SOURCE_TREE"], source_tree, name)
|
||||
self.assertEqual(service["build"]["args"]["IMAGE_VERSION"], image_version, name)
|
||||
self.assertEqual(service["build"]["args"]["SOURCE_DATE_EPOCH"], source_date_epoch, name)
|
||||
self.assertTrue(service["image"].endswith(f":{revision}"), name)
|
||||
|
||||
def test_unraid_folder_view_groups_the_complete_production_stack(self) -> None:
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
# The upstream TOML fixture retains exact source formatting.
|
||||
# ruff: noqa: E501
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import contextforge_candidate_recipe as recipe # noqa: E402 - script path added above
|
||||
|
||||
LOCK_DIR = ROOT / "infrastructure" / "images" / "contextforge" / "candidate"
|
||||
|
||||
|
||||
def _hash(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _containerfile_fixture() -> bytes:
|
||||
return (
|
||||
b"FROM base\n"
|
||||
b'RUN if [ "$ENABLE_RUST" = "true" ]; then \\\n dnf upgrade -y; fi\n'
|
||||
b"RUN set -euo pipefail \\\n && dnf install -y python3.12\n"
|
||||
b"RUN microdnf upgrade -y \\\n && microdnf install -y python3.12\n"
|
||||
b'RUN if [ "$ENABLE_FIPS" = "true" ]; then \\\n microdnf install -y crypto-policies; fi\n'
|
||||
b"COPY pyproject.toml /app/\n"
|
||||
+ recipe.INSTALL_START.encode("utf-8")
|
||||
+ b"\n && /app/.venv/bin/uv pip install unpinned\n"
|
||||
+ recipe.INSTALL_END.encode("utf-8")
|
||||
+ b"COPY source /app\n"
|
||||
)
|
||||
|
||||
|
||||
def _source_fixture(tmp_path: Path, containerfile: bytes) -> tuple[Path, dict[str, str]]:
|
||||
source = tmp_path / "upstream"
|
||||
source.mkdir()
|
||||
contents = {
|
||||
"Containerfile": containerfile,
|
||||
"pyproject.toml": b"[project]\nname='fixture'\n",
|
||||
"uv.lock": b"version = 1\n",
|
||||
}
|
||||
expected = {name: _hash(value) for name, value in contents.items()}
|
||||
for name, value in contents.items():
|
||||
(source / name).write_bytes(value)
|
||||
return source, expected
|
||||
|
||||
|
||||
def _copy_candidate_locks(destination: Path) -> dict[str, str]:
|
||||
destination.mkdir(parents=True)
|
||||
requirements: dict[str, str] = {}
|
||||
for name in ("production-requirements.lock", "build-tools.lock"):
|
||||
data = (LOCK_DIR / name).read_bytes()
|
||||
(destination / name).write_bytes(data)
|
||||
requirements[name] = _hash(data)
|
||||
return requirements
|
||||
|
||||
|
||||
def test_candidate_lock_exports_are_hash_complete_and_exclude_build_tools() -> None:
|
||||
manifest = json.loads((LOCK_DIR / "locks.json").read_text(encoding="utf-8"))
|
||||
production = (LOCK_DIR / "production-requirements.lock").read_text(encoding="utf-8")
|
||||
build_tools = (LOCK_DIR / "build-tools.lock").read_text(encoding="utf-8")
|
||||
|
||||
assert manifest["upstreamCommit"] == recipe.UPSTREAM_COMMIT
|
||||
assert manifest["uvVersion"] == recipe.UV_VERSION == "0.12.10"
|
||||
assert manifest["globalExcludeNewerUtc"] == "2026-09-22T00:00:00Z"
|
||||
assert manifest["freshnessOverrideUtc"] == "2026-10-02T00:00:00Z"
|
||||
assert manifest["extras"] == list(recipe.EXTRAS)
|
||||
assert manifest["constraints"] == list(recipe.CONSTRAINTS)
|
||||
for name in ("production-requirements.lock", "build-tools.lock"):
|
||||
assert _hash((LOCK_DIR / name).read_bytes()) == manifest["requirements"][name]
|
||||
|
||||
for requirement in recipe.CONSTRAINTS:
|
||||
package, version = requirement.split("==", 1)
|
||||
assert re.search(rf"(?im)^{re.escape(package)}=={re.escape(version)}\s*\\", production)
|
||||
for requirement in recipe.BUILD_TOOLS:
|
||||
package, version = requirement.split("==", 1)
|
||||
assert re.search(rf"(?im)^{re.escape(package)}=={re.escape(version)}\s*\\", build_tools)
|
||||
|
||||
package_blocks = re.split(r"(?m)(?=^[A-Za-z0-9_.-]+==)", production)
|
||||
package_blocks = [
|
||||
block for block in package_blocks if re.match(r"(?m)^[A-Za-z0-9_.-]+==", block)
|
||||
]
|
||||
assert len(package_blocks) > 100
|
||||
assert all(re.search(r"--hash=sha256:[0-9a-f]{64}", block) for block in package_blocks)
|
||||
assert not re.search(r"(?im)^(pip|setuptools|wheel|uv)==", production)
|
||||
build_blocks = re.split(r"(?m)(?=^[A-Za-z0-9_.-]+==)", build_tools)
|
||||
build_blocks = [block for block in build_blocks if re.match(r"(?m)^[A-Za-z0-9_.-]+==", block)]
|
||||
assert all(re.search(r"--hash=sha256:[0-9a-f]{64}", block) for block in build_blocks)
|
||||
assert "pip==26.2.1" in build_tools
|
||||
|
||||
|
||||
def test_candidate_pyproject_freezes_relative_freshness_and_preserves_project_metadata() -> None:
|
||||
source = """[tool.uv]
|
||||
exclude-newer = "10 days"
|
||||
|
||||
[tool.uv.exclude-newer-package]
|
||||
known-safe-package = "2026-07-01T00:00:00Z"
|
||||
|
||||
[tool.uv.sources]
|
||||
compliance-reference-server = { path = "mcp-servers/python/compliance_reference_server", editable = true }
|
||||
|
||||
# Project table follows the local source table.
|
||||
[project]
|
||||
name = "contextforge"
|
||||
dependencies = ["httpx>=0.27"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8"]
|
||||
"""
|
||||
|
||||
generated = recipe.candidate_pyproject(source)
|
||||
import tomllib
|
||||
|
||||
parsed = tomllib.loads(generated)
|
||||
assert parsed["project"] == {
|
||||
"name": "contextforge",
|
||||
"dependencies": ["httpx>=0.27"],
|
||||
}
|
||||
assert "sources" not in parsed["tool"]["uv"]
|
||||
assert parsed["tool"]["uv"]["exclude-newer"] == "2026-09-22T00:00:00Z"
|
||||
assert parsed["tool"]["uv"]["constraint-dependencies"] == list(recipe.CONSTRAINTS)
|
||||
assert parsed["tool"]["uv"]["exclude-newer-package"]["known-safe-package"] == (
|
||||
"2026-07-01T00:00:00Z"
|
||||
)
|
||||
assert parsed["tool"]["uv"]["exclude-newer-package"]["pyjwt"] == "2026-10-02T00:00:00Z"
|
||||
assert parsed["dependency-groups"]["build-tools"] == list(recipe.BUILD_TOOLS)
|
||||
assert "dev" not in parsed["dependency-groups"]
|
||||
|
||||
|
||||
def test_candidate_transform_is_exact_and_uses_hash_locked_runtime_install() -> None:
|
||||
original = _containerfile_fixture()
|
||||
transformed = recipe.transform_containerfile(original, expected_hash=_hash(original)).decode()
|
||||
|
||||
assert transformed.count("COPY contextforge-candidate-production-requirements.txt") == 1
|
||||
assert transformed.count("COPY contextforge-candidate-build-tools.txt") == 1
|
||||
assert (
|
||||
"--require-hashes --only-binary=:all: -r /tmp/contextforge-production-requirements.txt"
|
||||
in transformed
|
||||
)
|
||||
assert (
|
||||
"--require-hashes --only-binary=:all: -r /tmp/contextforge-build-tools.txt" in transformed
|
||||
)
|
||||
assert (
|
||||
'--no-deps --no-build-isolation ".[redis,postgres,observability,granian,plugins,llmchat]"'
|
||||
in transformed
|
||||
)
|
||||
assert "uv pip install unpinned" not in transformed
|
||||
assert "COPY source /app" in transformed
|
||||
assert not re.search(r"\b(?:dnf|microdnf) (?:upgrade|install)\b", transformed)
|
||||
assert 'test "$ENABLE_RUST" = "false"' in transformed
|
||||
assert 'test "$ENABLE_FIPS" = "false"' in transformed
|
||||
assert 'test "$ENABLE_PROFILING" = "false"' in transformed
|
||||
assert 'test "$(id -u app):$(id -g app)" = "10001:10001"' in transformed
|
||||
|
||||
|
||||
def test_candidate_os_freeze_rejects_missing_or_additional_transactions() -> None:
|
||||
original = _containerfile_fixture().decode()
|
||||
for changed in (
|
||||
original.replace("dnf upgrade -y", "echo changed", 1),
|
||||
original + "\nRUN dnf install -y unexpected\n",
|
||||
):
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="RPM transaction count"):
|
||||
recipe.freeze_os_layers(changed)
|
||||
|
||||
|
||||
def test_candidate_transform_rejects_source_drift_and_unexpected_anchor_counts() -> None:
|
||||
original = _containerfile_fixture()
|
||||
digest = _hash(original)
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="Containerfile hash mismatch"):
|
||||
recipe.transform_containerfile(original + b"# drift\n", expected_hash=digest)
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="anchor count changed"):
|
||||
recipe.transform_containerfile(
|
||||
original.replace(b"COPY pyproject.toml /app/\n", b""),
|
||||
expected_hash=_hash(original.replace(b"COPY pyproject.toml /app/\n", b"")),
|
||||
)
|
||||
|
||||
|
||||
def test_source_verification_rejects_missing_and_tampered_pinned_inputs(tmp_path: Path) -> None:
|
||||
source, expected = _source_fixture(tmp_path, _containerfile_fixture())
|
||||
assert recipe.verify_upstream_source(source, expected) == {
|
||||
name: (source / name).read_bytes() for name in expected
|
||||
}
|
||||
(source / "uv.lock").write_bytes(b"changed lock")
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="uv.lock hash mismatch"):
|
||||
recipe.verify_upstream_source(source, expected)
|
||||
(source / "uv.lock").unlink()
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="missing: uv.lock"):
|
||||
recipe.verify_upstream_source(source, expected)
|
||||
|
||||
|
||||
def test_prepare_copies_verified_locks_and_fails_before_writing_on_missing_lock(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
source, source_hashes = _source_fixture(tmp_path, _containerfile_fixture())
|
||||
lock_dir = tmp_path / "candidate-locks"
|
||||
lock_hashes = _copy_candidate_locks(lock_dir)
|
||||
manifest = recipe._make_manifest(
|
||||
source_hashes=source_hashes,
|
||||
production_hash=lock_hashes["production-requirements.lock"],
|
||||
build_tools_hash=lock_hashes["build-tools.lock"],
|
||||
)
|
||||
(lock_dir / "locks.json").write_bytes(recipe._canonical_json(manifest))
|
||||
monkeypatch.setattr(recipe, "SOURCE_HASHES", source_hashes)
|
||||
monkeypatch.setattr(recipe, "LOCK_DIR", lock_dir)
|
||||
monkeypatch.setattr(recipe, "LOCK_MANIFEST", lock_dir / "locks.json")
|
||||
|
||||
recipe.prepare_source(source)
|
||||
assert (source / "Containerfile.itworx-candidate").exists()
|
||||
for name, target in recipe.LOCK_OUTPUT_NAMES.items():
|
||||
assert (source / target).read_bytes() == (lock_dir / name).read_bytes()
|
||||
|
||||
(source / "Containerfile.itworx-candidate").unlink()
|
||||
(lock_dir / "build-tools.lock").unlink()
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="missing: build-tools.lock"):
|
||||
recipe.prepare_source(source)
|
||||
assert not (source / "Containerfile.itworx-candidate").exists()
|
||||
|
||||
|
||||
def test_candidate_build_args_pin_every_base_and_disable_optional_builds() -> None:
|
||||
args = recipe.candidate_build_args()
|
||||
assert set(args) == {
|
||||
"UBI_BASE",
|
||||
"NODEJS_IMAGE",
|
||||
"UBI_MINIMAL",
|
||||
"WHEELS_REF",
|
||||
"PYTHON_VERSION",
|
||||
"ENABLE_RUST",
|
||||
"ENABLE_RUST_MCP_RMCP",
|
||||
"ENABLE_PROFILING",
|
||||
"ENABLE_FIPS",
|
||||
}
|
||||
assert all("@sha256:" in args[key] for key in ("UBI_BASE", "NODEJS_IMAGE", "UBI_MINIMAL"))
|
||||
assert args["UBI_BASE"] == (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-builder@"
|
||||
"sha256:e23a733abd28376eb3328676a3ab1e6d153122228110de0c881bb5f3c38a4483"
|
||||
)
|
||||
assert args["UBI_MINIMAL"] == (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@"
|
||||
"sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236"
|
||||
)
|
||||
assert args["NODEJS_IMAGE"] == (
|
||||
"registry.access.redhat.com/ubi10/nodejs-24@"
|
||||
"sha256:0f110e68682f4a49188ba4db786eb4f207b119420913fc76bb5ba9512ffbef06"
|
||||
)
|
||||
assert args["WHEELS_REF"] == args["UBI_MINIMAL"]
|
||||
assert args["PYTHON_VERSION"] == "3.12"
|
||||
assert (
|
||||
args["ENABLE_RUST"] == args["ENABLE_RUST_MCP_RMCP"] == args["ENABLE_PROFILING"] == "false"
|
||||
)
|
||||
assert args["ENABLE_FIPS"] == "false"
|
||||
|
||||
|
||||
def test_frozen_base_manifest_binds_published_digests_and_rpm_inventories() -> None:
|
||||
manifest = json.loads((LOCK_DIR / "locks.json").read_text(encoding="utf-8"))
|
||||
bases = {item["arg"]: item for item in manifest["baseImages"]}
|
||||
|
||||
assert set(bases) == {"UBI_BASE", "NODEJS_IMAGE", "UBI_MINIMAL"}
|
||||
assert bases["UBI_BASE"] == {
|
||||
"arg": "UBI_BASE",
|
||||
"sourceTag": (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-builder:"
|
||||
"sha256-c6bb70a7a340ed1047e7cb934aa55b086ee68a5eb9ba6b2ad2e6825c7184a3e8"
|
||||
),
|
||||
"reference": (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-builder@"
|
||||
"sha256:e23a733abd28376eb3328676a3ab1e6d153122228110de0c881bb5f3c38a4483"
|
||||
),
|
||||
"parentReference": (
|
||||
"registry.access.redhat.com/ubi10@"
|
||||
"sha256:1e32d829a4de713a835d702d3bc05f0c448a08590796281b7dec181912e9aeb8"
|
||||
),
|
||||
"imageId": "sha256:c6bb70a7a340ed1047e7cb934aa55b086ee68a5eb9ba6b2ad2e6825c7184a3e8",
|
||||
"rpmInventorySha256": "6a8c0d4f66fc5a38008541ad15614711e7f536cd18c70c02d2eb0691552b88b0",
|
||||
"platform": "linux/amd64",
|
||||
"childManifestDigest": (
|
||||
"sha256:e23a733abd28376eb3328676a3ab1e6d153122228110de0c881bb5f3c38a4483"
|
||||
),
|
||||
}
|
||||
assert bases["UBI_MINIMAL"] == {
|
||||
"arg": "UBI_MINIMAL",
|
||||
"sourceTag": (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime:"
|
||||
"sha256-fd94e2643dfc0401790d0528d3740bb2f0c04709e743f6c774a4e8a38606e40b"
|
||||
),
|
||||
"reference": (
|
||||
"gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@"
|
||||
"sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236"
|
||||
),
|
||||
"parentReference": (
|
||||
"registry.access.redhat.com/ubi10/ubi-minimal@"
|
||||
"sha256:afbfb5c18c628796fed3ad3122dcf096e0b2dec9dad3fbabe8620de06bc6f078"
|
||||
),
|
||||
"imageId": "sha256:fd94e2643dfc0401790d0528d3740bb2f0c04709e743f6c774a4e8a38606e40b",
|
||||
"rpmInventorySha256": "a74d60ecb43d1aa561874305d1a9c62ac35c99a14c1dc275ba1656f14df7d034",
|
||||
"platform": "linux/amd64",
|
||||
"childManifestDigest": (
|
||||
"sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236"
|
||||
),
|
||||
}
|
||||
assert bases["NODEJS_IMAGE"]["reference"] == (
|
||||
"registry.access.redhat.com/ubi10/nodejs-24@"
|
||||
"sha256:0f110e68682f4a49188ba4db786eb4f207b119420913fc76bb5ba9512ffbef06"
|
||||
)
|
||||
assert "imageId" not in bases["NODEJS_IMAGE"]
|
||||
|
||||
|
||||
def test_frozen_base_reference_drift_fails_before_candidate_build(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
original = recipe.BASE_IMAGES[0]
|
||||
changed = dict(original)
|
||||
changed["reference"] = changed["reference"].replace("e23a733", "f23a733", 1)
|
||||
monkeypatch.setattr(recipe, "BASE_IMAGES", (changed, *recipe.BASE_IMAGES[1:]))
|
||||
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="reviewed contract"):
|
||||
recipe._read_lock_manifest()
|
||||
|
||||
|
||||
def test_frozen_base_evidence_matches_candidate_pins_and_repository_inputs() -> None:
|
||||
evidence_path = ROOT / "evidence" / "WP-400" / "CF_FROZEN_BASES.json"
|
||||
evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
|
||||
manifest = json.loads((LOCK_DIR / "locks.json").read_text(encoding="utf-8"))
|
||||
bases = {item["arg"]: item for item in manifest["baseImages"]}
|
||||
|
||||
assert evidence["schema"] == "itworx.contextforge-frozen-os-bases-evidence/v1"
|
||||
assert evidence["status"] == "passed"
|
||||
assert evidence["finalContextForgeCandidateBuilt"] is False
|
||||
assert evidence["sourceInputs"]["upstreamCommit"] == manifest["upstreamCommit"]
|
||||
assert evidence["sourceInputs"]["ContainerfileSha256"] == manifest["upstreamFiles"][
|
||||
"Containerfile"
|
||||
]
|
||||
assert evidence["sourceInputs"]["pyprojectTomlSha256"] == manifest["upstreamFiles"][
|
||||
"pyproject.toml"
|
||||
]
|
||||
assert evidence["sourceInputs"]["uvLockSha256"] == manifest["upstreamFiles"]["uv.lock"]
|
||||
assert set(evidence["hubInputsSha256"]) == {
|
||||
"scripts/contextforge_candidate_recipe.py",
|
||||
"scripts/build_contextforge_compat_image.sh",
|
||||
"infrastructure/images/contextforge/candidate/locks.json",
|
||||
"infrastructure/images/contextforge/candidate/production-requirements.lock",
|
||||
"infrastructure/images/contextforge/candidate/build-tools.lock",
|
||||
"infrastructure/images/contextforge/os-bases/Dockerfile.builder",
|
||||
"infrastructure/images/contextforge/os-bases/Dockerfile.runtime",
|
||||
"infrastructure/compose/docker-compose.autodeploy.yml",
|
||||
}
|
||||
assert evidence["hubInputsSha256"] == {
|
||||
name: _hash((ROOT / name).read_bytes()) for name in evidence["hubInputsSha256"]
|
||||
}
|
||||
for role, arg in (("builder", "UBI_BASE"), ("runtime", "UBI_MINIMAL")):
|
||||
published = evidence["publishedBases"][role]
|
||||
locked = bases[arg]
|
||||
assert published["reference"] == locked["reference"]
|
||||
assert published["imageId"] == locked["imageId"]
|
||||
assert published["rpmInventorySha256"] == locked["rpmInventorySha256"]
|
||||
assert published["platform"] == locked["platform"] == "linux/amd64"
|
||||
assert published["publication"]["pulledByDigest"] is True
|
||||
assert published["publication"]["manifestConfigImageIdMatches"] is True
|
||||
assert published["publication"]["rpmInventoryMatchesAfterPull"] is True
|
||||
assert published["rpmInventory"]["exactMatch"] is True
|
||||
assert published["scan"]["secretFindings"] == 0
|
||||
assert published["scan"]["vulnerabilityFindingRecords"] == 0
|
||||
|
||||
|
||||
def test_candidate_manifest_rejects_changed_lock_bytes(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
lock_dir = tmp_path / "locks"
|
||||
lock_hashes = _copy_candidate_locks(lock_dir)
|
||||
manifest = recipe._make_manifest(
|
||||
source_hashes=recipe.SOURCE_HASHES,
|
||||
production_hash=lock_hashes["production-requirements.lock"],
|
||||
build_tools_hash=lock_hashes["build-tools.lock"],
|
||||
)
|
||||
(lock_dir / "locks.json").write_bytes(recipe._canonical_json(manifest))
|
||||
(lock_dir / "production-requirements.lock").write_bytes(b"changed")
|
||||
monkeypatch.setattr(recipe, "LOCK_DIR", lock_dir)
|
||||
monkeypatch.setattr(recipe, "LOCK_MANIFEST", lock_dir / "locks.json")
|
||||
with pytest.raises(recipe.CandidateRecipeError, match="lock hash mismatch"):
|
||||
recipe._read_lock_manifest()
|
||||
@@ -1,9 +1,15 @@
|
||||
# Shell fixture lines preserve exact mocked build arguments and OCI values.
|
||||
# ruff: noqa: E501
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -12,6 +18,9 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "infrastructure" / "images" / "contextforge" / "compat-image.env"
|
||||
CONTRACT = ROOT / "infrastructure" / "images" / "contextforge" / "runtime-contract.json"
|
||||
BUILDER = ROOT / "scripts" / "build_contextforge_compat_image.sh"
|
||||
SECURITY_REQUIREMENTS = (
|
||||
ROOT / "infrastructure" / "images" / "contextforge" / "security-requirements.txt"
|
||||
)
|
||||
|
||||
EXPECTED = {
|
||||
"CONTEXTFORGE_UPSTREAM_COMMIT": "1e58a0434a78bbdbe1e975abeb0bed1043ff14c1",
|
||||
@@ -73,18 +82,18 @@ def test_contextforge_compat_builder_guards_source_and_runtime() -> None:
|
||||
assert 'CONTRACT="$ROOT/infrastructure/images/contextforge/runtime-contract.json"' in script
|
||||
assert 'ENTRYPOINT="$ROOT/infrastructure/images/contextforge/entrypoint.sh"' in script
|
||||
assert 'RUNTIME_VERIFIER="$ROOT/scripts/contextforge_runtime_contract.py"' in script
|
||||
assert 'https://github.com/IBM/mcp-context-forge.git' in script
|
||||
assert "https://github.com/IBM/mcp-context-forge.git" in script
|
||||
assert 'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"' in script
|
||||
assert 'git -C "$SRC" checkout --detach FETCH_HEAD' in script
|
||||
assert 'git -C "$SRC" rev-parse HEAD' in script
|
||||
assert 'REVISION_PREFIX="${CONTEXTFORGE_ALEMBIC_HEAD}_"' in script
|
||||
assert 'MCP_REQUIREMENT="mcp==${CONTEXTFORGE_MCP_SDK}"' in script
|
||||
assert "from mcp import McpError" in script
|
||||
assert 'alembic -c mcpgateway/alembic.ini heads' in script
|
||||
assert 'CONTEXTFORGE_ALEMBIC_HEAD' in script
|
||||
assert 'contextforge-runtime-contract.json' in script
|
||||
assert 'contextforge-entrypoint.sh' in script
|
||||
assert 'contextforge-runtime-contract.py' in script
|
||||
assert "alembic -c mcpgateway/alembic.ini heads" in script
|
||||
assert "CONTEXTFORGE_ALEMBIC_HEAD" in script
|
||||
assert "contextforge-runtime-contract.json" in script
|
||||
assert "contextforge-entrypoint.sh" in script
|
||||
assert "contextforge-runtime-contract.py" in script
|
||||
for label in (
|
||||
"io.itworx.contextforge.contract-sha256",
|
||||
"io.itworx.contextforge.entrypoint-sha256",
|
||||
@@ -92,12 +101,68 @@ def test_contextforge_compat_builder_guards_source_and_runtime() -> None:
|
||||
"io.itworx.contextforge.source-commit",
|
||||
"io.itworx.contextforge.alembic-head",
|
||||
"io.itworx.contextforge.mcp-sdk",
|
||||
"io.itworx.contextforge.security-requirements-sha256",
|
||||
):
|
||||
assert label in script
|
||||
assert 'docker image inspect -f' in script
|
||||
assert "docker image inspect -f" in script
|
||||
assert 'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"' in script
|
||||
|
||||
|
||||
def test_contextforge_security_wheels_match_the_reviewed_hub_lock() -> None:
|
||||
lock = tomllib.loads((ROOT / "uv.lock").read_text(encoding="utf-8"))
|
||||
packages = {package["name"]: package for package in lock["package"]}
|
||||
requirements = SECURITY_REQUIREMENTS.read_text(encoding="utf-8").splitlines()
|
||||
assert len(requirements) == 2
|
||||
names = set()
|
||||
for requirement in requirements:
|
||||
match = re.fullmatch(
|
||||
r"([A-Za-z0-9]+)==([0-9]+\.[0-9]+\.[0-9]+) --hash=sha256:([0-9a-f]{64})", requirement
|
||||
)
|
||||
assert match is not None, (
|
||||
"Security inputs must pin a wheel version and SHA-256 without extra flags."
|
||||
)
|
||||
name, version, digest = match.groups()
|
||||
normalized = name.lower()
|
||||
assert normalized not in names
|
||||
names.add(normalized)
|
||||
package = packages[normalized]
|
||||
assert version == package["version"]
|
||||
assert f"sha256:{digest}" in {wheel["hash"] for wheel in package["wheels"]}
|
||||
assert names == {"pyjwt", "urllib3"}
|
||||
|
||||
|
||||
def test_missing_security_inputs_stop_builder_before_any_external_command(tmp_path: Path) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
scripts = tmp_path / "scripts"
|
||||
scripts.mkdir()
|
||||
builder = scripts / BUILDER.name
|
||||
builder.write_text(BUILDER.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
inputs = tmp_path / "infrastructure" / "images" / "contextforge"
|
||||
inputs.mkdir(parents=True)
|
||||
for name in ("compat-image.env", "runtime-contract.json", "entrypoint.sh"):
|
||||
(inputs / name).write_text("fixture", encoding="utf-8")
|
||||
(scripts / "contextforge_runtime_contract.py").write_text("fixture", encoding="utf-8")
|
||||
bin_directory = tmp_path / "bin"
|
||||
bin_directory.mkdir()
|
||||
marker = tmp_path / "external-command-ran"
|
||||
for name in ("git", "docker"):
|
||||
executable = bin_directory / name
|
||||
executable.write_text(f"#!/bin/sh\ntouch '{marker}'\nexit 99\n", encoding="utf-8")
|
||||
executable.chmod(0o755)
|
||||
env = {**os.environ, "PATH": f"{bin_directory}:{os.environ['PATH']}"}
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder)],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 66
|
||||
assert "security-requirements.txt" in completed.stderr
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_contextforge_compat_builder_has_valid_shell_syntax() -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("WSL bash cannot read a Windows path for syntax validation")
|
||||
@@ -110,3 +175,392 @@ def test_contextforge_compat_builder_has_valid_shell_syntax() -> None:
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
|
||||
|
||||
def _candidate_builder_fixture(tmp_path: Path) -> tuple[Path, dict[str, str], Path, Path]:
|
||||
root = tmp_path / "hub"
|
||||
for relative in (
|
||||
"scripts/build_contextforge_compat_image.sh",
|
||||
"scripts/contextforge_candidate_recipe.py",
|
||||
"scripts/contextforge_runtime_contract.py",
|
||||
"infrastructure/images/contextforge/compat-image.env",
|
||||
"infrastructure/images/contextforge/runtime-contract.json",
|
||||
"infrastructure/images/contextforge/entrypoint.sh",
|
||||
"infrastructure/images/contextforge/security-requirements.txt",
|
||||
):
|
||||
target = root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / relative, target)
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
log = tmp_path / "external-calls.log"
|
||||
layer_capture = tmp_path / "candidate-layer.Containerfile"
|
||||
git = bin_dir / "git"
|
||||
git.write_text(
|
||||
"#!/bin/sh\n"
|
||||
'echo "git $*" >> "$CALL_LOG"\n'
|
||||
'if [ "$1" = "-C" ]; then\n'
|
||||
' src="$2"; action="$3"\n'
|
||||
' if [ "$src" = "$SOURCE_ROOT" ]; then\n'
|
||||
' case "$action:$4:$5" in\n'
|
||||
' rev-parse:HEAD:) printf "%s\\n" "$GIT_HEAD" ;;\n'
|
||||
' rev-parse:HEAD*:) printf "%s\\n" "$GIT_TREE" ;;\n'
|
||||
' show:-s:--format=%ct) printf "%s\\n" "$GIT_EPOCH" ;;\n'
|
||||
' show:-s:--format=%cI) printf "%s\\n" "$GIT_COMMIT_DATE" ;;\n'
|
||||
' status:*:*) printf "%s" "${GIT_DIRTY:-}" ;;\n'
|
||||
" esac\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
' case "$action" in\n'
|
||||
' fetch|checkout) mkdir -p "$src/mcpgateway/alembic/versions"; '
|
||||
' printf "revision = \\"12d4a0c7789c\\"\\n" > '
|
||||
'"$src/mcpgateway/alembic/versions/12d4a0c7789c_fixture.py"; '
|
||||
' printf "FROM python:3.12\\n" > "$src/Containerfile" ;;\n'
|
||||
' rev-parse) printf "1e58a0434a78bbdbe1e975abeb0bed1043ff14c1\\n" ;;\n'
|
||||
" esac\n"
|
||||
"fi\n"
|
||||
"exit 0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
git.chmod(0o755)
|
||||
real_python = shutil.which("python3")
|
||||
assert real_python is not None
|
||||
python3 = bin_dir / "python3"
|
||||
python3.write_text(
|
||||
"#!/bin/sh\n"
|
||||
'case "$1" in\n'
|
||||
" */contextforge_candidate_recipe.py)\n"
|
||||
' case "$2" in\n'
|
||||
' prepare) printf "candidate recipe\\n" > "$4/Containerfile.itworx-candidate" ;;\n'
|
||||
" build-args)\n"
|
||||
' printf "%s\\n" \\\n'
|
||||
' "UBI_BASE=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-builder@sha256:e23a733abd28376eb3328676a3ab1e6d153122228110de0c881bb5f3c38a4483" \\\n'
|
||||
' "NODEJS_IMAGE=registry.access.redhat.com/ubi10/nodejs-24@sha256:0f110e68682f4a49188ba4db786eb4f207b119420913fc76bb5ba9512ffbef06" \\\n'
|
||||
' "UBI_MINIMAL=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236" \\\n'
|
||||
' "WHEELS_REF=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236" \\\n'
|
||||
' "PYTHON_VERSION=3.12" "ENABLE_RUST=false" "ENABLE_RUST_MCP_RMCP=false" "ENABLE_PROFILING=false" "ENABLE_FIPS=false" ;;\n'
|
||||
" esac\n"
|
||||
" exit 0 ;;\n"
|
||||
"esac\n"
|
||||
f'exec "{real_python}" "$@"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
python3.chmod(0o755)
|
||||
docker = bin_dir / "docker"
|
||||
docker.write_text(
|
||||
"#!/bin/sh\n"
|
||||
'echo "docker $*" >> "$CALL_LOG"\n'
|
||||
'if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then\n'
|
||||
' if [ "$3" != "-f" ]; then\n'
|
||||
' [ "${COLLISION_TAG:-}" = "$3" ] && exit 0\n'
|
||||
' printf "Error response from daemon: No such image: %s\\n" "$3" >&2\n'
|
||||
" exit 1\n"
|
||||
" fi\n"
|
||||
' case "$4" in\n'
|
||||
' *Config.User*) printf "10001:10001\\n" ;;\n'
|
||||
' *contract-sha256*) printf "%s\\n" "$CONTRACT_SHA" ;;\n'
|
||||
' *entrypoint-sha256*) printf "%s\\n" "$ENTRYPOINT_SHA" ;;\n'
|
||||
' *verifier-sha256*) printf "%s\\n" "$VERIFIER_SHA" ;;\n'
|
||||
' *source-commit*) printf "1e58a0434a78bbdbe1e975abeb0bed1043ff14c1\\n" ;;\n'
|
||||
' *alembic-head*) printf "12d4a0c7789c\\n" ;;\n'
|
||||
' *mcp-sdk*) printf "1.30.0\\n" ;;\n'
|
||||
' *security-requirements-sha256*) printf "%s\\n" "$SECURITY_SHA" ;;\n'
|
||||
' *org.opencontainers.image.source-tree*) printf "%s\\n" "$APPOPS_SOURCE_TREE" ;;\n'
|
||||
' *org.opencontainers.image.revision*) printf "%s\\n" "$APPOPS_SOURCE_REVISION" ;;\n'
|
||||
' *org.opencontainers.image.version*) printf "%s\\n" "$APPOPS_BUILD_ID" ;;\n'
|
||||
' *org.opencontainers.image.source-date-epoch*) printf "%s\\n" "$SOURCE_DATE_EPOCH" ;;\n'
|
||||
' *org.opencontainers.image.created*) printf "%s\\n" "$APPOPS_BUILD_DATE" ;;\n'
|
||||
' *org.opencontainers.image.source*) printf "%s\\n" "$OCI_SOURCE" ;;\n'
|
||||
" esac\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
'if [ "$1" = "build" ]; then\n'
|
||||
' while [ "$#" -gt 0 ]; do\n'
|
||||
' if [ "$1" = "-f" ]; then layer="$2"; shift 2; else shift; fi\n'
|
||||
" done\n"
|
||||
' case "$layer" in *Containerfile.compat) cp "$layer" "$LAYER_CAPTURE" ;; esac\n'
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
'if [ "$1" = "run" ]; then\n'
|
||||
' case "$*" in\n'
|
||||
' *"alembic -c mcpgateway/alembic.ini heads"*) printf "12d4a0c7789c (head)\\n" ;;\n'
|
||||
' *"contextforge-runtime-contract.json"*) printf "%s\\n" "$CONTRACT_SHA" ;;\n'
|
||||
' *"contextforge-entrypoint.sh"*) printf "%s\\n" "$ENTRYPOINT_SHA" ;;\n'
|
||||
' *"contextforge-runtime-contract.py"*) printf "%s\\n" "$VERIFIER_SHA" ;;\n'
|
||||
' *"contextforge-security-requirements.txt"*) printf "%s\\n" "$SECURITY_SHA" ;;\n'
|
||||
" esac\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"exit 0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
docker.chmod(0o755)
|
||||
contract_sha = hashlib.sha256(
|
||||
(root / "infrastructure/images/contextforge/runtime-contract.json").read_bytes()
|
||||
).hexdigest()
|
||||
entrypoint_sha = hashlib.sha256(
|
||||
(root / "infrastructure/images/contextforge/entrypoint.sh").read_bytes()
|
||||
).hexdigest()
|
||||
verifier_sha = hashlib.sha256(
|
||||
(root / "scripts/contextforge_runtime_contract.py").read_bytes()
|
||||
).hexdigest()
|
||||
security_sha = hashlib.sha256(
|
||||
(root / "infrastructure/images/contextforge/security-requirements.txt").read_bytes()
|
||||
).hexdigest()
|
||||
environment = {
|
||||
**os.environ,
|
||||
"PATH": f"{bin_dir}:{os.environ['PATH']}",
|
||||
"CALL_LOG": str(log),
|
||||
"LAYER_CAPTURE": str(layer_capture),
|
||||
"SOURCE_ROOT": str(root),
|
||||
"CONTRACT_SHA": contract_sha,
|
||||
"ENTRYPOINT_SHA": entrypoint_sha,
|
||||
"VERIFIER_SHA": verifier_sha,
|
||||
"SECURITY_SHA": security_sha,
|
||||
"OCI_SOURCE": "https://gitea.example.com/jens/itworx-mcp-hub",
|
||||
"GIT_HEAD": "a" * 40,
|
||||
"GIT_TREE": "b" * 40,
|
||||
"GIT_EPOCH": "1790964000",
|
||||
"GIT_COMMIT_DATE": "2026-09-30T00:00:00+00:00",
|
||||
"APPOPS_SOURCE_REPOSITORY": "jens/itworx_mcp_hub",
|
||||
"APPOPS_SOURCE_REVISION": "a" * 40,
|
||||
"APPOPS_SOURCE_TREE": "b" * 40,
|
||||
"APPOPS_BUILD_ID": "a" * 12,
|
||||
"APPOPS_BUILD_DATE": "2026-10-02T18:00:00.123Z",
|
||||
"SOURCE_DATE_EPOCH": "1790964000",
|
||||
"GITEA_REPOSITORY": "jens/itworx_mcp_hub",
|
||||
"GITEA_COMMIT_SHA": "a" * 40,
|
||||
}
|
||||
return root / "scripts/build_contextforge_compat_image.sh", environment, log, layer_capture
|
||||
|
||||
|
||||
def test_candidate_mode_requires_frozen_metadata_before_git_or_docker(tmp_path: Path) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
env.pop("APPOPS_SOURCE_TREE")
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "APPOPS_SOURCE_TREE" in completed.stderr
|
||||
assert not log.exists() or log.read_text(encoding="utf-8") == ""
|
||||
|
||||
|
||||
def test_candidate_mode_rejects_unreviewed_build_variants_before_git_or_docker(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[
|
||||
shutil.which("bash") or "/bin/bash",
|
||||
str(builder),
|
||||
"--release-candidate",
|
||||
"--enable-fips",
|
||||
],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 64
|
||||
assert "supported mode" in completed.stderr
|
||||
assert not log.exists() or log.read_text(encoding="utf-8") == ""
|
||||
|
||||
|
||||
def test_candidate_mode_rejects_invalid_calendar_date_before_git_or_docker(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
env["APPOPS_BUILD_DATE"] = "2026-02-30T18:00:00.123Z"
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "valid UTC calendar timestamp" in completed.stderr
|
||||
assert not log.exists() or log.read_text(encoding="utf-8") == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("collision", ["final", "upstream", "staged"])
|
||||
def test_candidate_mode_rejects_existing_revision_tags_after_source_validation_before_build(
|
||||
tmp_path: Path, collision: str
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
revision = env["APPOPS_SOURCE_REVISION"]
|
||||
suffix = {"final": "", "upstream": "-upstream", "staged": "-staged"}[collision]
|
||||
env["COLLISION_TAG"] = f"itworx-mcp-hub/contextforge-schema:{revision}{suffix}"
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "already exists" in completed.stderr
|
||||
calls = log.read_text(encoding="utf-8").splitlines()
|
||||
assert any("docker image inspect" in call for call in calls)
|
||||
assert any(call.startswith("git ") for call in calls)
|
||||
assert not any("docker build" in call or "docker tag" in call for call in calls)
|
||||
|
||||
|
||||
def test_candidate_mode_uses_only_revision_scoped_tags_and_frozen_oci_metadata(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, layer_capture = _candidate_builder_fixture(tmp_path)
|
||||
static = EXPECTED["CONTEXTFORGE_LOCAL_IMAGE"]
|
||||
revision = env["APPOPS_SOURCE_REVISION"]
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
calls = log.read_text(encoding="utf-8").splitlines()
|
||||
tags = [call for call in calls if call.startswith("docker tag ")]
|
||||
assert tags == [
|
||||
f"docker tag itworx-mcp-hub/contextforge-schema:{revision}-staged "
|
||||
f"itworx-mcp-hub/contextforge-schema:{revision}"
|
||||
]
|
||||
assert any(
|
||||
f"-t itworx-mcp-hub/contextforge-schema:{revision}-upstream " in call for call in calls
|
||||
)
|
||||
assert any(
|
||||
f"-t itworx-mcp-hub/contextforge-schema:{revision}-staged " in call for call in calls
|
||||
)
|
||||
candidate_build = next(
|
||||
call
|
||||
for call in calls
|
||||
if "docker build" in call and "Containerfile.itworx-candidate" in call
|
||||
)
|
||||
assert "--platform linux/amd64" in candidate_build
|
||||
assert (
|
||||
"UBI_BASE=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-builder@"
|
||||
"sha256:e23a733abd28376eb3328676a3ab1e6d153122228110de0c881bb5f3c38a4483"
|
||||
in candidate_build
|
||||
)
|
||||
assert (
|
||||
"NODEJS_IMAGE=registry.access.redhat.com/ubi10/nodejs-24@sha256:0f110e686"
|
||||
in candidate_build
|
||||
)
|
||||
assert (
|
||||
"UBI_MINIMAL=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@"
|
||||
"sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236"
|
||||
in candidate_build
|
||||
)
|
||||
assert (
|
||||
"WHEELS_REF=gitea.example.com/jens/itworx-mcp-hub/contextforge-os-runtime@"
|
||||
"sha256:9b23d8152f253b10e47ce077bb9c5f8c3c370f3e0d5533f8a4a0e986ff176236"
|
||||
in candidate_build
|
||||
)
|
||||
assert "ENABLE_RUST=false" in candidate_build
|
||||
assert "ENABLE_RUST_MCP_RMCP=false" in candidate_build
|
||||
assert "ENABLE_PROFILING=false" in candidate_build
|
||||
assert "ENABLE_FIPS=false" in candidate_build
|
||||
assert not any(static in call for call in calls)
|
||||
assert not any(call.startswith("docker rmi ") for call in calls)
|
||||
assert "NOT APPROVED OR DEPLOYED" in completed.stdout
|
||||
layer = layer_capture.read_text(encoding="utf-8")
|
||||
assert "-m pip" not in layer
|
||||
assert "import importlib.util" in layer
|
||||
assert "find_spec" in layer
|
||||
assert (
|
||||
'org.opencontainers.image.source="https://gitea.example.com/jens/itworx-mcp-hub"' in layer
|
||||
)
|
||||
assert f'org.opencontainers.image.revision="{revision}"' in layer
|
||||
assert f'org.opencontainers.image.source-tree="{env["APPOPS_SOURCE_TREE"]}"' in layer
|
||||
assert f'org.opencontainers.image.version="{env["APPOPS_BUILD_ID"]}"' in layer
|
||||
assert f'org.opencontainers.image.created="{env["APPOPS_BUILD_DATE"]}"' in layer
|
||||
assert f'org.opencontainers.image.source-date-epoch="{env["SOURCE_DATE_EPOCH"]}"' in layer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dirty", [" M scripts/contextforge_runtime_contract.py\n", "?? unexpected.py\n"]
|
||||
)
|
||||
def test_candidate_mode_rejects_a_dirty_source_checkout_before_fetch_or_build(
|
||||
tmp_path: Path,
|
||||
dirty: str,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
env["GIT_DIRTY"] = dirty
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "source checkout has changes" in completed.stderr
|
||||
calls = log.read_text(encoding="utf-8").splitlines()
|
||||
assert not any("git init" in call or "git fetch" in call for call in calls)
|
||||
assert not any(call.startswith("docker ") for call in calls)
|
||||
assert not any("docker build" in call or "docker tag" in call for call in calls)
|
||||
|
||||
|
||||
def test_candidate_mode_rejects_source_tree_mismatch_before_fetch_or_build(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
env["APPOPS_SOURCE_TREE"] = "c" * 40
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "source tree differs" in completed.stderr
|
||||
calls = log.read_text(encoding="utf-8").splitlines()
|
||||
assert any("rev-parse" in call for call in calls)
|
||||
assert not any("git init" in call or "git fetch" in call for call in calls)
|
||||
assert not any(call.startswith("docker ") for call in calls)
|
||||
assert not any("docker build" in call or "docker tag" in call for call in calls)
|
||||
|
||||
|
||||
def test_candidate_mode_rejects_revision_mismatch_with_checked_out_head(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
if os.name == "nt":
|
||||
pytest.skip("The Linux builder executes in the required Linux validation job")
|
||||
builder, env, log, _layer = _candidate_builder_fixture(tmp_path)
|
||||
env["APPOPS_SOURCE_REVISION"] = "c" * 40
|
||||
env["GITEA_COMMIT_SHA"] = "c" * 40
|
||||
env["APPOPS_BUILD_ID"] = "c" * 12
|
||||
completed = subprocess.run( # noqa: S603
|
||||
[shutil.which("bash") or "/bin/bash", str(builder), "--release-candidate"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode != 0
|
||||
assert "checked-out Hub revision differs" in completed.stderr
|
||||
calls = log.read_text(encoding="utf-8").splitlines()
|
||||
assert any("rev-parse" in call for call in calls)
|
||||
assert not any("git init" in call or "git fetch" in call for call in calls)
|
||||
assert not any(call.startswith("docker ") for call in calls)
|
||||
assert not any("docker build" in call or "docker tag" in call for call in calls)
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from scripts.promotion_preflight import (
|
||||
CONTEXTFORGE_PRODUCTION_IMAGE,
|
||||
IMAGE_ENVIRONMENT,
|
||||
@@ -14,15 +16,23 @@ ENV_EXAMPLE = ROOT / "infrastructure" / "compose" / ".env.example"
|
||||
INPUT_MAP = ROOT / "infrastructure" / "compose" / "production-input-map.json"
|
||||
PROD_COMPOSE = ROOT / "infrastructure" / "compose" / "docker-compose.prod.yml"
|
||||
CONTRACT = ROOT / "infrastructure" / "images" / "contextforge" / "runtime-contract.json"
|
||||
AUTODEPLOY = ROOT / "infrastructure" / "compose" / "docker-compose.autodeploy.yml"
|
||||
|
||||
|
||||
def test_governed_contextforge_candidate_uses_the_exact_release_revision() -> None:
|
||||
document = yaml.safe_load(AUTODEPLOY.read_text(encoding="utf-8"))
|
||||
service = document["services"]["contextforge"]
|
||||
assert service["image"] == (
|
||||
"itworx-mcp-hub/contextforge-schema:${GITEA_COMMIT_SHA:?GITEA_COMMIT_SHA is required}"
|
||||
)
|
||||
assert service["pull_policy"] == "never"
|
||||
assert not service.get("build")
|
||||
|
||||
|
||||
def test_contextforge_uses_the_verified_local_compatibility_image() -> None:
|
||||
assert "contextforge" not in IMAGE_ENVIRONMENT
|
||||
assert "CONTEXTFORGE_IMAGE" not in REQUIRED_ENV
|
||||
assert (
|
||||
CONTEXTFORGE_PRODUCTION_IMAGE
|
||||
== "itworx-mcp-hub/contextforge-schema:12d4a0c7789c"
|
||||
)
|
||||
assert CONTEXTFORGE_PRODUCTION_IMAGE == "itworx-mcp-hub/contextforge-schema:12d4a0c7789c"
|
||||
|
||||
env_example = ENV_EXAMPLE.read_text(encoding="utf-8")
|
||||
assert "CONTEXTFORGE_IMAGE=" not in env_example
|
||||
@@ -42,8 +52,5 @@ def test_contextforge_contract_digest_is_repository_owned_and_exact() -> None:
|
||||
expected = hashlib.sha256(CONTRACT.read_bytes()).hexdigest()
|
||||
production = PROD_COMPOSE.read_text(encoding="utf-8")
|
||||
|
||||
assert (
|
||||
f'ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256: "{expected}"'
|
||||
in production
|
||||
)
|
||||
assert f'ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256: "{expected}"' in production
|
||||
assert "${CONTEXTFORGE_CONTRACT_SHA256" not in production
|
||||
@@ -255,7 +255,10 @@ class IngestionCatalogComposeTests(unittest.TestCase):
|
||||
**os.environ,
|
||||
"SECRETS_DIR": str(ROOT / "tests/fixtures"),
|
||||
"CONNECTOR_INGESTION_IMAGE": digest,
|
||||
"GITEA_COMMIT_SHA": "test-sha",
|
||||
"GITEA_COMMIT_SHA": "a" * 40,
|
||||
"APPOPS_SOURCE_TREE": "b" * 40,
|
||||
"APPOPS_BUILD_ID": "a" * 12,
|
||||
"SOURCE_DATE_EPOCH": "1790888863",
|
||||
},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.check_cve_exceptions import REQUIRED_FIELDS, check
|
||||
from scripts.draft_cve_exceptions import main as draft_exceptions_main
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TODAY = date(2026, 8, 15)
|
||||
@@ -45,6 +47,13 @@ def _exception(**overrides) -> dict:
|
||||
return base
|
||||
|
||||
|
||||
def _image_entry(image_id: str, tag: str | None = None) -> dict[str, str]:
|
||||
entry = {"imageId": image_id}
|
||||
if tag is not None:
|
||||
entry["tag"] = tag
|
||||
return entry
|
||||
|
||||
|
||||
def test_real_wp220_registry_has_every_required_field() -> None:
|
||||
"""The actual drafted registry this packet ships must be schema-complete."""
|
||||
doc = json.loads(
|
||||
@@ -122,7 +131,7 @@ def test_changed_digest_fails() -> None:
|
||||
exceptions = {
|
||||
"exceptions": [_exception(affectedImageDigests={"control-api": "sha256:" + "a" * 64})]
|
||||
}
|
||||
current_digests = {"control-api": "sha256:" + "b" * 64}
|
||||
current_digests = {"control-api": _image_entry("sha256:" + "b" * 64)}
|
||||
errors = check(scan, exceptions, current_digests, TODAY)
|
||||
assert any("DIGEST CHANGED" in e for e in errors)
|
||||
|
||||
@@ -132,11 +141,258 @@ def test_matching_digest_passes() -> None:
|
||||
exceptions = {
|
||||
"exceptions": [_exception(affectedImageDigests={"control-api": "sha256:" + "a" * 64})]
|
||||
}
|
||||
current_digests = {"control-api": "sha256:" + "a" * 64}
|
||||
current_digests = {"control-api": _image_entry("sha256:" + "a" * 64)}
|
||||
errors = check(scan, exceptions, current_digests, TODAY)
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_missing_current_service_for_affected_image_fails() -> None:
|
||||
scan = _scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"affectedImages": ["local/control-api:dev", "local/worker:dev"],
|
||||
}
|
||||
)
|
||||
exceptions = {
|
||||
"exceptions": [
|
||||
_exception(
|
||||
affectedImageDigests={
|
||||
"control-api": "sha256:" + "a" * 64,
|
||||
"worker": "sha256:" + "b" * 64,
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
current = {"control-api": _image_entry("sha256:" + "a" * 64, "local/control-api:dev")}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any(
|
||||
"AFFECTED IMAGE UNBOUND" in error and "local/worker:dev" in error for error in errors
|
||||
)
|
||||
|
||||
|
||||
def test_missing_exception_digest_binding_fails_closed() -> None:
|
||||
scan = _scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"affectedImages": ["local/control-api:dev"],
|
||||
}
|
||||
)
|
||||
exceptions = {
|
||||
"exceptions": [
|
||||
_exception(
|
||||
affectedImageDigests={
|
||||
"unrelated": "sha256:" + "c" * 64,
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
current = {"control-api": _image_entry("sha256:" + "a" * 64, "local/control-api:dev")}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("IMAGE BINDING MISSING" in error and "control-api" in error for error in errors)
|
||||
|
||||
|
||||
def test_empty_exception_digest_binding_fails_closed() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {"exceptions": [_exception(affectedImageDigests={"control-api": ""})]}
|
||||
current = {"control-api": _image_entry("sha256:" + "a" * 64)}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("IMAGE BINDING MISSING" in error and "control-api" in error for error in errors)
|
||||
|
||||
|
||||
def test_malformed_recorded_digest_fails_closed() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {"exceptions": [_exception(affectedImageDigests={"control-api": "sha256:abc"})]}
|
||||
current = {"control-api": _image_entry("sha256:" + "a" * 64)}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("IMAGE BINDING INVALID" in error and "control-api" in error for error in errors)
|
||||
|
||||
|
||||
def test_missing_or_malformed_current_image_id_fails_even_with_digest() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {"exceptions": [_exception()]}
|
||||
current = {"control-api": {"digest": "sha256:" + "a" * 64}}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("CURRENT IMAGE ID INVALID" in error and "control-api" in error for error in errors)
|
||||
|
||||
|
||||
def test_malformed_current_image_id_fails_even_with_digest() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {"exceptions": [_exception()]}
|
||||
current = {
|
||||
"control-api": {
|
||||
"imageId": "sha256:not-a-full-image-id",
|
||||
"digest": "sha256:" + "a" * 64,
|
||||
}
|
||||
}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("CURRENT IMAGE ID INVALID" in error for error in errors)
|
||||
|
||||
|
||||
def test_empty_current_image_inventory_fails_when_supplied() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
|
||||
errors = check(scan, {"exceptions": [_exception()]}, {}, TODAY)
|
||||
|
||||
assert any("IMAGE INVENTORY INVALID" in error for error in errors)
|
||||
|
||||
|
||||
def test_scan_installed_version_must_match_reviewed_version() -> None:
|
||||
scan = _scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": "2.0",
|
||||
}
|
||||
)
|
||||
|
||||
errors = check(scan, {"exceptions": [_exception(installedVersion="1.0")]}, None, TODAY)
|
||||
|
||||
assert any("VERSION MISMATCH" in error and "2.0" in error for error in errors)
|
||||
|
||||
|
||||
def test_conflicting_duplicate_exceptions_fail_instead_of_overwriting() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
approved = _exception()
|
||||
conflicting = _exception(
|
||||
approvalStatus="pending", riskOwner="", approver="", compensatingControls=""
|
||||
)
|
||||
|
||||
errors = check(scan, {"exceptions": [approved, conflicting]}, None, TODAY)
|
||||
|
||||
assert any("CONFLICTING DUPLICATE" in error for error in errors)
|
||||
assert any("UNAPPROVED" in error for error in errors)
|
||||
|
||||
|
||||
def test_scoped_approval_binds_only_affected_services() -> None:
|
||||
scan = _scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": "1.0",
|
||||
"affectedImages": ["local/worker:dev"],
|
||||
}
|
||||
)
|
||||
exceptions = {"exceptions": [_exception(affectedImageDigests={"worker": "sha256:" + "b" * 64})]}
|
||||
current = {
|
||||
"control-api": _image_entry("sha256:" + "c" * 64, "local/control-api:dev"),
|
||||
"worker": _image_entry("sha256:" + "b" * 64, "local/worker:dev"),
|
||||
}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_legacy_scan_without_affected_images_uses_full_inventory() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {
|
||||
"exceptions": [_exception(affectedImageDigests={"control-api": "sha256:" + "a" * 64})]
|
||||
}
|
||||
current = {
|
||||
"control-api": _image_entry("sha256:" + "a" * 64),
|
||||
"worker": _image_entry("sha256:" + "b" * 64),
|
||||
}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("IMAGE BINDING MISSING" in error and "worker" in error for error in errors)
|
||||
|
||||
|
||||
def test_legacy_scan_rejects_inventory_missing_a_reviewed_service() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {
|
||||
"exceptions": [
|
||||
_exception(
|
||||
affectedImageDigests={
|
||||
"control-api": "sha256:" + "a" * 64,
|
||||
"worker": "sha256:" + "b" * 64,
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
current = {"control-api": _image_entry("sha256:" + "a" * 64)}
|
||||
|
||||
errors = check(scan, exceptions, current, TODAY)
|
||||
|
||||
assert any("IMAGE INVENTORY SCOPE CHANGED" in error for error in errors)
|
||||
|
||||
|
||||
def test_draft_is_pending_and_scopes_digests_to_affected_images(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
scan_path = tmp_path / "scan.json"
|
||||
image_path = tmp_path / "images.json"
|
||||
output_path = tmp_path / "draft.json"
|
||||
scan_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
**_scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": "1.0",
|
||||
"affectedImages": ["local/worker:dev"],
|
||||
}
|
||||
),
|
||||
"scannedAt": "2026-08-15T00:00:00Z",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
image_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"images": {
|
||||
"control-api": _image_entry("sha256:" + "a" * 64, "local/control-api:dev"),
|
||||
"worker": _image_entry("sha256:" + "b" * 64, "local/worker:dev"),
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"draft_cve_exceptions.py",
|
||||
"--scan-summary",
|
||||
str(scan_path),
|
||||
"--image-digests",
|
||||
str(image_path),
|
||||
"--evidence-reference",
|
||||
"evidence/WP-200/test.md",
|
||||
"--output",
|
||||
str(output_path),
|
||||
],
|
||||
)
|
||||
|
||||
assert draft_exceptions_main() == 0
|
||||
drafted = json.loads(output_path.read_text(encoding="utf-8"))["exceptions"]
|
||||
|
||||
assert len(drafted) == 1
|
||||
assert drafted[0]["approvalStatus"] == "pending"
|
||||
assert drafted[0]["affectedImageDigests"] == {"worker": "sha256:" + "b" * 64}
|
||||
|
||||
|
||||
def test_malformed_exception_missing_required_field_fails() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
broken = _exception()
|
||||
@@ -158,3 +414,103 @@ def test_multiple_cves_in_one_package_row_each_need_their_own_exception() -> Non
|
||||
errors = check(scan, exceptions, None, TODAY)
|
||||
assert not any("CVE-2026-1" in e for e in errors)
|
||||
assert any("CVE-2026-2" in e and "UNAPPROVED" in e for e in errors)
|
||||
|
||||
|
||||
def test_distinct_installed_versions_are_reviewed_separately_for_scoped_images() -> None:
|
||||
scan = _scan(
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": "1.0",
|
||||
"affectedImages": ["local/web:dev"],
|
||||
},
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": "2.0",
|
||||
"affectedImages": ["local/postgres:dev"],
|
||||
},
|
||||
)
|
||||
exceptions = {
|
||||
"exceptions": [
|
||||
_exception(affectedImageDigests={"web": "sha256:" + "a" * 64}),
|
||||
_exception(
|
||||
id="pkg-CVE-2026-1-v2",
|
||||
installedVersion="2.0",
|
||||
affectedImageDigests={"postgres": "sha256:" + "b" * 64},
|
||||
),
|
||||
]
|
||||
}
|
||||
images = {
|
||||
"web": _image_entry("sha256:" + "a" * 64, "local/web:dev"),
|
||||
"postgres": _image_entry("sha256:" + "b" * 64, "local/postgres:dev"),
|
||||
}
|
||||
assert check(scan, exceptions, images, TODAY) == []
|
||||
exceptions["exceptions"].pop()
|
||||
assert any("VERSION MISMATCH" in error for error in check(scan, exceptions, images, TODAY))
|
||||
|
||||
|
||||
def test_versionless_legacy_scan_rejects_ambiguous_version_reviews() -> None:
|
||||
scan = _scan({"package": "pkg", "cves": ["CVE-2026-1"], "severity": "HIGH"})
|
||||
exceptions = {"exceptions": [_exception(), _exception(id="v2", installedVersion="2.0")]}
|
||||
assert any("AMBIGUOUS VERSION" in error for error in check(scan, exceptions, None, TODAY))
|
||||
|
||||
|
||||
def test_draft_distinct_versions_keeps_unique_pending_image_bound_reviews(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
scan = _scan(
|
||||
*[
|
||||
{
|
||||
"package": "pkg",
|
||||
"cves": ["CVE-2026-1"],
|
||||
"severity": "HIGH",
|
||||
"installedVersion": version,
|
||||
"affectedImages": [f"local/{service}:dev"],
|
||||
}
|
||||
for version, service in [("1.0", "web"), ("2.0", "postgres")]
|
||||
]
|
||||
)
|
||||
scans, images, output = (
|
||||
tmp_path / "scan.json",
|
||||
tmp_path / "images.json",
|
||||
tmp_path / "draft.json",
|
||||
)
|
||||
scans.write_text(json.dumps(scan), encoding="utf-8")
|
||||
images.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"images": {
|
||||
"web": _image_entry("sha256:" + "a" * 64, "local/web:dev"),
|
||||
"postgres": _image_entry("sha256:" + "b" * 64, "local/postgres:dev"),
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"draft",
|
||||
"--scan-summary",
|
||||
str(scans),
|
||||
"--image-digests",
|
||||
str(images),
|
||||
"--output",
|
||||
str(output),
|
||||
"--evidence-reference",
|
||||
"fixture",
|
||||
],
|
||||
)
|
||||
assert draft_exceptions_main() == 0
|
||||
drafted = json.loads(output.read_text(encoding="utf-8"))["exceptions"]
|
||||
assert len(drafted) == 2 and len({entry["id"] for entry in drafted}) == 2
|
||||
assert all(
|
||||
entry["approvalStatus"] == "pending" and not entry["riskOwner"] and not entry["approver"]
|
||||
for entry in drafted
|
||||
)
|
||||
assert drafted[0]["affectedImageDigests"] == {"web": "sha256:" + "a" * 64}
|
||||
assert drafted[1]["affectedImageDigests"] == {"postgres": "sha256:" + "b" * 64}
|
||||
Reference in new issue
Block a user