diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index 23dd6a7..1cb94b5 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `3237e4d8aec312d63d13b4e80664422a7520b5f9`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `b5607d7532f496660c4a3cbe8db3bc3414a7b36b`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 9893933..3bc33b8 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "3237e4d8aec312d63d13b4e80664422a7520b5f9", + "sourceRevision": "b5607d7532f496660c4a3cbe8db3bc3414a7b36b", "files": [ { "path": ".dockerignore", @@ -1999,8 +1999,8 @@ }, { "path": "infrastructure/compose/docker-compose.yml", - "sha256": "7dbf120b195e491e48047a863dd353362d6933e60e0777af5e38d7d0b8f627fb", - "bytes": 27883 + "sha256": "1d935403bdde612195cb9d12ca938ceb10a77f14d79a6dc32379e235fbcfffd9", + "bytes": 28021 }, { "path": "infrastructure/database/README.md", @@ -3144,8 +3144,8 @@ }, { "path": "tests/test_compose.py", - "sha256": "f0a31081471405ff796c29de800a013c107f3c4a55970e05cf7d85ea09a69ae7", - "bytes": 24213 + "sha256": "f8ac6e9d377eefb2c6a07858248d85272b5a6f87fb89b8c75876114f1418314d", + "bytes": 24902 }, { "path": "tests/test_contextforge.py", @@ -3469,8 +3469,8 @@ }, { "path": "tests/test_release_images_workflow.py", - "sha256": "64a7398d772caa7ffe7590fb473efa3fdeaa244dae62b52d4b9024ab3503d87a", - "bytes": 3130 + "sha256": "96fffbc3375f34de139d56abe8ed86eba48ba1bdd3f8dff0339b3c3b266ee48b", + "bytes": 3116 }, { "path": "tests/test_release_scanner_asset.py", @@ -3489,8 +3489,13 @@ }, { "path": "tests/test_security_base_preparation.py", - "sha256": "817a39215877dd9125cb51edeaf08ec95be453abff2341ca52264e29d7061a36", - "bytes": 8209 + "sha256": "9a2234dcdc60958841cc5904b78468a151f59c154f2c388cbb0840d9e887a2f7", + "bytes": 8191 + }, + { + "path": "tests/test_security_base_publication_pins.py", + "sha256": "9299fa268b82f2c7193b8a9ede7032c13f07f97ab9f504ffc82b60460769f831", + "bytes": 3141 }, { "path": "tests/test_security_pcre_inventory.py", diff --git a/infrastructure/compose/docker-compose.yml b/infrastructure/compose/docker-compose.yml index a269782..d016fdb 100644 --- a/infrastructure/compose/docker-compose.yml +++ b/infrastructure/compose/docker-compose.yml @@ -29,7 +29,7 @@ x-third-party: &third-party services: postgres: <<: *third-party - image: gitea.example.com/jens/itworx-mcp-hub/postgres:wp400-gosu-v2-20261002-a94e0b4d2029@sha256:a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73 + image: gitea.example.com/jens/itworx-mcp-hub/postgres:3237e4d8aec312d63d13b4e80664422a7520b5f9-security-run-23280-attempt-1@sha256:4e471418b9cf0805ed3c5cce716fb13ac96aa1fcabb6751fd4386b1db679e176 entrypoint: ["sh", "/opt/itworx/postgres-entrypoint.sh"] command: ["postgres"] environment: @@ -319,7 +319,7 @@ services: ingestion-clamd: <<: *third-party profiles: [knowledge-intake] - image: clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303 + image: gitea.example.com/jens/itworx-mcp-hub/ingestion-clamd:3237e4d8aec312d63d13b4e80664422a7520b5f9-security-run-23280-attempt-1@sha256:eeca204bb48b30e78cf103bff00a33babb45e7c289747d7469396551803a8d23 cap_drop: ["ALL"] # /init must chown its signature volume, prepare log files, and drop to clamav. cap_add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"] diff --git a/tests/test_compose.py b/tests/test_compose.py index 85114a8..a057a27 100644 --- a/tests/test_compose.py +++ b/tests/test_compose.py @@ -128,6 +128,12 @@ class ComposeContractTests(unittest.TestCase): encoding="utf-8" ) ) + security_publication = json.loads( + (ROOT / "evidence/WP-400/SECURITY_BASE_PUBLICATION.json").read_text(encoding="utf-8") + ) + self.assertEqual(security_publication["schema"], "itworx.security-base-preparation/v1") + self.assertFalse(security_publication["candidateEmitted"]) + self.assertEqual(security_publication["reportCount"], 5) for name in ("postgres", "redis"): with self.subTest(service=name): image_evidence = evidence["images"][name] @@ -135,18 +141,22 @@ class ComposeContractTests(unittest.TestCase): candidate_proof = publication_proof["candidates"][name] self.assert_database_candidate_pin( name, - self.document["services"][name]["image"], - image_evidence, - publication, - candidate_proof, - ) - self.assert_database_candidate_pin( - name, - integration["services"][name]["image"], + image_ref, image_evidence, publication, candidate_proof, ) + if name == "postgres": + prepared = next( + row + for row in security_publication["preparedImages"] + if row["service"] == name + ) + self.assertEqual(prepared["parentRef"], image_ref) + self.assertTrue(prepared["runtimeVerified"]) + self.assertTrue(prepared["vulnerabilityAndSecretVerified"]) + self.assertRegex(prepared["imageId"], r"^sha256:[0-9a-f]{64}$") + image_ref = prepared["imageRef"] self.assertEqual(image_ref, self.document["services"][name]["image"]) self.assertEqual(image_ref, integration["services"][name]["image"]) diff --git a/tests/test_release_images_workflow.py b/tests/test_release_images_workflow.py index 3c96292..59595e6 100644 --- a/tests/test_release_images_workflow.py +++ b/tests/test_release_images_workflow.py @@ -60,12 +60,10 @@ def test_release_credentials_only_reach_the_bounded_producer() -> None: command = steps[1]["run"] assert "set -euo pipefail" in command assert "set -x" not in command and "docker login" not in command - assert ( - "python3 scripts/build_mcp_hub_release_images.py --prepare-security-bases " - '--revision "$GITHUB_SHA"' in command - ) + assert 'python3 scripts/build_mcp_hub_release_images.py --revision "$GITHUB_SHA"' in command + assert "--prepare-security-bases" not in command assert '--run-id "$GITHUB_RUN_ID"' in command - assert '--output "${RUNNER_TEMP}/hub-release-security-bases-${GITHUB_SHA}"' in command + assert '--output "${RUNNER_TEMP}/hub-release-candidate-${GITHUB_SHA}"' in command def test_release_artifact_is_pinned_uncompressed_and_cannot_overwrite() -> None: @@ -73,8 +71,8 @@ def test_release_artifact_is_pinned_uncompressed_and_cannot_overwrite() -> None: assert upload["uses"] == UPLOAD assert "if" not in upload assert upload["with"] == { - "name": "hub-release-security-bases-${{ github.sha }}", - "path": "${{ runner.temp }}/hub-release-security-bases-${{ github.sha }}/*", + "name": "hub-release-candidate-${{ github.sha }}", + "path": "${{ runner.temp }}/hub-release-candidate-${{ github.sha }}/*", "if-no-files-found": "error", "retention-days": 90, "compression-level": 0, diff --git a/tests/test_security_base_preparation.py b/tests/test_security_base_preparation.py index fff3832..294e7ca 100644 --- a/tests/test_security_base_preparation.py +++ b/tests/test_security_base_preparation.py @@ -136,7 +136,7 @@ def test_only_verified_scanned_images_publish_and_no_candidate_is_emitted( built[reference] = config def inspect(reference: str, *args: object, **kwargs: object) -> dict: - if reference in {value[0] for value in producer.EXTERNAL_IMAGES.values()}: + if reference in set(preparation.PARENT_IMAGES.values()): return copy.deepcopy(parent) result = copy.deepcopy(built[reference.split("@", 1)[0]]) if "@" in reference and failure == "pull": diff --git a/tests/test_security_base_publication_pins.py b/tests/test_security_base_publication_pins.py new file mode 100644 index 0000000..bb8a9e6 --- /dev/null +++ b/tests/test_security_base_publication_pins.py @@ -0,0 +1,60 @@ +import hashlib +import json +import subprocess +from pathlib import Path + +import yaml + +from scripts import build_mcp_hub_release_images as producer +from scripts import prepare_release_security_bases as preparation + +ROOT = Path(__file__).resolve().parents[1] + + +def test_preparation_recipe_parents_do_not_follow_the_new_production_pins() -> None: + receipt = json.loads((ROOT / "evidence/WP-400/SECURITY_BASE_PUBLICATION.json").read_text()) + for row in receipt["preparedImages"]: + parent = preparation.PARENT_IMAGES[row["service"]] + assert parent == row["parentRef"] + assert parent != producer.EXTERNAL_IMAGES[row["service"]][0] + recipe = ( + ROOT / "infrastructure/images/security-bases" / row["service"] / "Dockerfile" + ).read_text() + assert f"\nFROM {parent}\n" in "\n" + recipe + + +def test_phase_b_pins_are_the_exact_runtime_verified_publication() -> None: + receipt = json.loads((ROOT / "evidence/WP-400/SECURITY_BASE_PUBLICATION.json").read_text()) + assert receipt["schema"] == "itworx.security-base-preparation/v1" + assert receipt["revision"] == "3237e4d8aec312d63d13b4e80664422a7520b5f9" + assert receipt["runId"] == 23280 and receipt["runAttempt"] == 1 + assert receipt["candidateEmitted"] is False and receipt["reportCount"] == 5 + services = yaml.safe_load((ROOT / "infrastructure/compose/docker-compose.yml").read_text())[ + "services" + ] + rows = receipt["preparedImages"] + assert {row["service"] for row in rows} == {"postgres", "ingestion-clamd"} + for row in rows: + assert row["runtimeVerified"] is True and row["vulnerabilityAndSecretVerified"] is True + assert producer.EXTERNAL_IMAGES[row["service"]] == (row["imageRef"], row["archiveRef"]) + assert services[row["service"]]["image"] == row["imageRef"] + assert row["archiveRef"] == row["imageRef"].split("@", 1)[0] + assert row["archiveRef"] not in {"postgres:18.6", "clamav/clamav:1.4.6"} + for path, identities in row["sourceInputs"].items(): + assert path in { + "infrastructure/images/security-bases/postgres/Dockerfile", + "evidence/WP-400/database-image-candidate-20261003/recipes/postgres/verify-gosu-metadata.sh", + "infrastructure/images/security-bases/ingestion-clamd/Dockerfile", + "infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh", + } + # The recipes remain byte-identical; HEAD also works in depth-one native checkouts. + blob = subprocess.check_output(["git", "show", "HEAD:" + path], cwd=ROOT) # noqa: S603,S607 - fixed reviewed recipe blob paths + assert hashlib.sha256(blob).hexdigest() == identities["sourceSha256"] + assert ( + hashlib.sha256(blob.replace(b"\r\n", b"\n")).hexdigest() + == identities["materializedSha256"] + ) + integration = yaml.safe_load( + (ROOT / "infrastructure/compose/docker-compose.wp050-integration.yml").read_text() + ) + assert integration["services"]["postgres"]["image"] == producer.EXTERNAL_IMAGES["postgres"][0]