From efe109279c8ab6a02dfc54ad6ac9431f07ee75c0 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:57:44 +0000 Subject: [PATCH 1/3] Bind Waterline PHP 8.2 build runtime source --- qualification/composer-container-trust.json | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/qualification/composer-container-trust.json b/qualification/composer-container-trust.json index 71faf1a..90fb812 100644 --- a/qualification/composer-container-trust.json +++ b/qualification/composer-container-trust.json @@ -128,6 +128,68 @@ "mode": 420 } } + }, + "waterline-php82-build": { + "target": "waterline", + "workflows": { + "php.yml": [ + "build" + ] + }, + "commands": [ + "bash \"$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh\" \"8.2\"" + ], + "environment": [ + "WATERLINE_PHP_CI_IMAGE", + "WATERLINE_PHP_CI_ROOT", + "WATERLINE_PHP_CI_STATE" + ], + "files": { + "docker/php-ci/Dockerfile": { + "sha256": "81d23fbbee7e4ee97ca14df9d17982aa664335b94499c5019662259fe0387eaa", + "mode": 384 + }, + "docker/php-ci/Dockerfile.dockerignore": { + "sha256": "a26d7f1f5ae2cac900e5e32bfaa0f08bd7a8435847095990c9bfe09d0edbf264", + "mode": 384 + }, + "docker/php-ci/verify-php-trust.php": { + "sha256": "529c282e7422202bf25565431a6391de388f86dfe5bfd6c75cf21daa56ffa75e", + "mode": 384 + }, + "docker/brotli/install-vendor.sh": { + "sha256": "d7eb8b2a3e0af1c3072a56bcb81632b22f061f211d8fe5e57ee901c8bf94c92f", + "mode": 384 + }, + "docker/brotli/verify-runtime.php": { + "sha256": "580cc465e349f3872eca592355294e7320fe7f1a4560ff5d4c83d5900921c00e", + "mode": 384 + }, + "docker/brotli/verify-system-brotli.c": { + "sha256": "3353927d206aaf5e63157e46be715c6cbee46bac777e29f8f9e25c4c1f656667", + "mode": 384 + }, + "docker/brotli/version": { + "sha256": "e064953e075ddb99fb7b48ad29e215f2dfa075812c127eb1d6ab680e9920fba2", + "mode": 384 + }, + "scripts/ci/php-container-images.json": { + "sha256": "3314ecfd6c32641af1d5475f8e57f4421979f67e05db1f98c79ef7b08d41b944", + "mode": 384 + }, + "scripts/ci/run-php-container.sh": { + "sha256": "456f9716edbfaa8429edba92f958c4e1b3551bcbfd849a722764ae47daffb8fd", + "mode": 448 + }, + "scripts/ci/setup-php-container.sh": { + "sha256": "5064d48df69940cdf609be7bccd82ee71624d1017a8486a29028c765bb311f13", + "mode": 448 + }, + "tests/ci/test_php_container_tls.py": { + "sha256": "7e365ff0f27c4efea0f7cf8ec3778f48fb1fe03e80a5b5b0c9500a6935f3d767", + "mode": 384 + } + } } } } From e33a73012e40c2564de753301f21963975074c0a Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 12:18:38 +0000 Subject: [PATCH 2/3] Require Waterline Composer trust even when guards are deleted --- qualification/policy.json | 1 + tests/fixtures/waterline-php82-build.yml | 60 ++++++++++++++++++++++++ tests/test_composer_trust_contract.py | 45 ++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tests/fixtures/waterline-php82-build.yml diff --git a/qualification/policy.json b/qualification/policy.json index 441a1c2..4a7293e 100644 --- a/qualification/policy.json +++ b/qualification/policy.json @@ -336,6 +336,7 @@ ] }, "waterline": { + "composer_trust_required": true, "branch": "main", "repository": "waterline", "workflows": [ diff --git a/tests/fixtures/waterline-php82-build.yml b/tests/fixtures/waterline-php82-build.yml new file mode 100644 index 0000000..04f0d03 --- /dev/null +++ b/tests/fixtures/waterline-php82-build.yml @@ -0,0 +1,60 @@ +# Proposed caller for the Waterline PHP 8.2 runtime admission contract. +jobs: + build: + needs: + - classify + - frontend + if: ${{ needs.classify.outputs.qualification-class == 'complete' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + with: + fetch-depth: 10 + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 + with: + python-version: '3.13' + - name: Select authenticated dependency trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 + with: + clients: pip,python,composer,curl,git + - name: Disable APT download retries (CVE-2026-109518) + run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries + >/dev/null + - name: Check out the fixed APT prerequisite + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + with: + repository: durable-workflow/.github + ref: 18783be6bef737cc778e4c7282a8797e39daa61a + path: .central-apt-source + persist-credentials: false + - name: Install the bound PHP 8.2 build runtime + run: bash "$GITHUB_WORKSPACE/scripts/ci/setup-php-container.sh" "8.2" + - name: Verify Composer configuration before requests + uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 + - name: Validate Composer manifest + run: composer validate --strict --no-check-all + - name: Cache Composer packages + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name + == github.repository }} + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 + with: + path: vendor + key: ${{ github.event_name }}-${{ runner.os }}-php-8.2-${{ hashFiles('composer.json') + }} + restore-keys: '${{ github.event_name }}-${{ runner.os }}-php-8.2- + + ' + - name: Verify Composer after cache restoration + uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 + - name: Install maintained qualification dependencies + run: composer install --prefer-dist --no-progress --no-interaction + - name: Verify Composer before the dependency audit + uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 + - name: Audit qualification dependencies + run: composer audit --no-interaction + - name: Verify Composer before Workbench scaffolding + uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 + - name: Build Laravel 12 Workbench scaffold + run: composer build diff --git a/tests/test_composer_trust_contract.py b/tests/test_composer_trust_contract.py index 2798528..f51192a 100644 --- a/tests/test_composer_trust_contract.py +++ b/tests/test_composer_trust_contract.py @@ -355,6 +355,51 @@ def test_an_unregistered_setup_or_another_action_at_that_pin_refuses(self): self.check(steps) +class WaterlineBuildMandatoryTrustContractTest(unittest.TestCase): + def setUp(self): + repository = Path(__file__).resolve().parents[1] + self.policy = json.loads((repository / "qualification/policy.json").read_text()) + self.document = yaml.safe_load((repository / "tests/fixtures/waterline-php82-build.yml").read_text()) + self.catalog = json.loads((repository / "qualification/composer-container-trust.json").read_text()) + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + # Source hashing is covered independently. Bind harmless local bytes so + # this regression tests the real target policy and proposed caller offline. + recipe = self.catalog["runtime_setups"]["waterline-php82-build"] + for name, expected in recipe["files"].items(): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("# reviewed runtime fixture\n") + path.chmod(expected["mode"]) + expected["sha256"] = hashlib.sha256(path.read_bytes()).hexdigest() + patch = mock.patch("scripts.qualification_policy._composer_container_catalog", return_value=self.catalog) + patch.start() + self.addCleanup(patch.stop) + + def check(self, document): + return verify_composer_network_sinks( + self.policy, "waterline", {"php.yml": yaml.safe_dump(document)}, self.root, + ) + + def test_proposed_build_enumerates_all_guarded_requests(self): + self.assertEqual(len(self.check(self.document)), 4) + + def test_deleting_every_live_verifier_cannot_disable_enumeration(self): + document = deepcopy(self.document) + steps = document["jobs"]["build"]["steps"] + document["jobs"]["build"]["steps"] = [ + step for step in steps if "verify-composer-trust@" not in step.get("uses", "") + ] + with self.assertRaises(PolicyError): + self.check(document) + + def test_guardless_build_cannot_opt_out_before_loading_the_catalog(self): + document = {"jobs": {"build": {"steps": [{"run": "composer install --no-interaction"}]}}} + with self.assertRaises(PolicyError): + self.check(document) + + class ComposerRuntimeSetupContractTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() From 5cfb5bf4dedf3ec1ff4901d8bd69c832da57a1bf Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 12:47:55 +0000 Subject: [PATCH 3/3] Put the expected sink count first in the Waterline assertion --- tests/test_composer_trust_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_composer_trust_contract.py b/tests/test_composer_trust_contract.py index 8d3e1cc..280e65c 100644 --- a/tests/test_composer_trust_contract.py +++ b/tests/test_composer_trust_contract.py @@ -397,7 +397,7 @@ def check(self, document): ) def test_proposed_build_enumerates_all_guarded_requests(self): - self.assertEqual(len(self.check(self.document)), 4) + self.assertEqual(4, len(self.check(self.document))) def test_deleting_every_live_verifier_cannot_disable_enumeration(self): document = deepcopy(self.document)