From c6c2978cfbc0e43d85a5f51fa6d70e2bd7e0f5ba Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:58:49 +0000 Subject: [PATCH] Register exact inert response-loss CI workflow successors --- scripts/protected_policy_context.py | 7 + tests/fixtures/ci-response-loss-source.yml | 123 ++++++++++++++++++ tests/fixtures/sonar-response-loss-source.yml | 62 +++++++++ tests/test_protected_policy_context.py | 35 +++++ 4 files changed, 227 insertions(+) create mode 100644 tests/fixtures/ci-response-loss-source.yml create mode 100644 tests/fixtures/sonar-response-loss-source.yml diff --git a/scripts/protected_policy_context.py b/scripts/protected_policy_context.py index ca673f6..8e760d8 100644 --- a/scripts/protected_policy_context.py +++ b/scripts/protected_policy_context.py @@ -68,6 +68,12 @@ REVIEWED_BOUND_PHP_SOURCE_CI_SUCCESSORS = { CI_WORKFLOW: "363540f9e81e4842099f0cad394ce3fc2a83835ce1355ffef5b16722547699b4", } +# Exact reviewed successors add the response-loss socket controls to CI and +# their actual source to coverage. Existing workflow identities remain valid. +REVIEWED_RESPONSE_LOSS_SOURCE_CI_SUCCESSORS = { + CI_WORKFLOW: "ee4ddc9ad665490681b3f1a2676cdd4ce791d90677e2a931bdb4fea7cc9deb15", + SONAR_WORKFLOW: "4eed5d82d6e5decf47d4aab1d1d203eb1060f8892216360b9308c3e710bbcd77", +} # The successor remains inert until its separate runtime pin and manual-only # workflow integration. Keep the consumed predecessor's identity unchanged. REVIEWED_INITIAL_PROFILE_SUCCESSORS = { @@ -116,6 +122,7 @@ def reviewed_context_hashes(policy_target: str) -> dict[str, set[str]]: REVIEWED_PHP_CONFIGURATION_SOURCE_CI_SUCCESSORS, REVIEWED_APT_NO_RETRIES_SOURCE_CI_SUCCESSORS, REVIEWED_BOUND_PHP_SOURCE_CI_SUCCESSORS, + REVIEWED_RESPONSE_LOSS_SOURCE_CI_SUCCESSORS, REVIEWED_INITIAL_PROFILE_SUCCESSORS, REVIEWED_NORMALIZED_PROFILE_SUCCESSORS, REVIEWED_ALTERNATIVES_PROFILE_SUCCESSORS, REVIEWED_SELECTED_ALTERNATIVES_PROFILE_SUCCESSORS, diff --git a/tests/fixtures/ci-response-loss-source.yml b/tests/fixtures/ci-response-loss-source.yml new file mode 100644 index 0000000..945ba41 --- /dev/null +++ b/tests/fixtures/ci-response-loss-source.yml @@ -0,0 +1,123 @@ +name: Repository checks + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: repository-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + contracts: + name: Shared contract tests + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.13" + + - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python,composer,curl,git,npm,cargo + + - 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 + + - uses: durable-workflow/.github/.github/actions/setup-php-linux@411a14ac07d32073eed6ab4977f2825eba4739a7 # bound-linux-php + with: + php-version: '8.3' + ini-file: production + tools: composer:2.10.3,phpunit:11.5.57 + coverage: none + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.22.1 + cache: npm + + - uses: dtolnay/rust-toolchain@4d407b29186a635f0cc27475ef0bc0ae605a8866 # 1.86.0 + with: + toolchain: 1.86.0 + + - uses: ./.github/actions/verify-composer-trust # local + + - name: Install Python test dependencies + run: >- + python -m pip install --only-binary :all: jsonschema==4.23.0 PyYAML==6.0.2 + + - name: Test retained shared contracts + run: >- + python -m unittest + tests.test_qualification_policy + tests.test_protected_policy_context + tests.test_protected_policy_check + tests.test_protected_policy_inputs + tests.test_protected_policy_issue + tests.test_native_policy_check + tests.test_native_policy_workflow + tests.test_regression_corpus + tests.test_regression_evidence_schema + tests.test_server_codec_counterfactual_schema + tests.test_visual_evidence + tests.test_temporal_runtime + tests.test_cancellation_workflow_policy + tests.test_sdk_metrics_contract + tests.test_command_response_loss + tests.test_ci_trust + tests.test_pip_effective_trust + tests.test_cli_effective_trust + tests.test_composer_trust_contract + tests.test_composer_container_trust + tests.test_composer_container_effective_trust + tests.test_php_linux_bootstrap + tests.test_php_linux_archive + tests.test_php_linux_normalize + tests.test_php_linux_recipe + tests.test_php_linux_tools + tests.test_php_linux_alternatives + tests.test_setup_php_linux + tests.test_php_linux_qualification + tests.test_php_linux_qualification_workflow + tests.test_php_linux_configuration + + - name: Test actual Composer trust selection + run: COMPOSER_BINARY="$(command -v composer)" phpunit --configuration phpunit-composer-trust.xml --do-not-cache-result + + - name: Install Node dependencies + run: npm ci --ignore-scripts + + - name: Test browser evidence boundary + run: npm run test:visual-capture + + protected-parser-install: + name: Protected parser installer controls + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - name: Create a disposable installer runtime + run: | + python -I -m venv "$RUNNER_TEMP/protected-parser-install" + printf '%s\n' "$RUNNER_TEMP/protected-parser-install/bin" >> "$GITHUB_PATH" + - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python + - name: Exercise the actual isolated hash-locked installer + run: python -I tests/test_policy_dependency_install.py diff --git a/tests/fixtures/sonar-response-loss-source.yml b/tests/fixtures/sonar-response-loss-source.yml new file mode 100644 index 0000000..9423d38 --- /dev/null +++ b/tests/fixtures/sonar-response-loss-source.yml @@ -0,0 +1,62 @@ +name: SonarQube + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: sonar-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analysis: + name: SonarQube analysis + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python,composer,curl,git,npm,cargo + - 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 + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.3' + tools: composer:2.10.3, phpunit:11.5.57 + coverage: xdebug + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.22.1 + - uses: dtolnay/rust-toolchain@4d407b29186a635f0cc27475ef0bc0ae605a8866 # 1.86.0 + with: + toolchain: 1.86.0 + - uses: ./.github/actions/verify-composer-trust # local + - name: Produce actual Composer control coverage + run: | + mkdir -p build + COMPOSER_BINARY="$(command -v composer)" XDEBUG_MODE=coverage \ + phpunit --configuration phpunit-composer-trust.xml --do-not-cache-result \ + --coverage-clover build/composer-trust-coverage.xml + - name: Produce source coverage + run: | + python -m pip install --only-binary :all: coverage==7.10.7 jsonschema==4.23.0 PyYAML==6.0.2 + coverage run --source=scripts,conformance/cancellation/temporal,conformance/command-keys,.github/actions/bootstrap-patched-pip -m unittest tests.test_qualification_policy tests.test_protected_policy_context tests.test_protected_policy_check tests.test_protected_policy_inputs tests.test_protected_policy_issue tests.test_native_policy_check tests.test_native_policy_workflow tests.test_regression_corpus tests.test_regression_evidence_schema tests.test_server_codec_counterfactual_schema tests.test_visual_evidence tests.test_temporal_runtime tests.test_cancellation_workflow_policy tests.test_sdk_metrics_contract tests.test_command_response_loss tests.test_ci_trust tests.test_pip_effective_trust tests.test_cli_effective_trust tests.test_composer_trust_contract tests.test_composer_container_trust tests.test_composer_container_effective_trust tests.test_php_linux_bootstrap tests.test_php_linux_archive tests.test_php_linux_normalize tests.test_php_linux_recipe tests.test_php_linux_tools tests.test_php_linux_alternatives tests.test_setup_php_linux tests.test_php_linux_qualification tests.test_php_linux_qualification_workflow tests.test_php_linux_configuration + coverage xml -o coverage.xml + - uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: -Dsonar.qualitygate.wait=true -Dsonar.qualitygate.timeout=300 diff --git a/tests/test_protected_policy_context.py b/tests/test_protected_policy_context.py index 2ab3a55..ef3b752 100644 --- a/tests/test_protected_policy_context.py +++ b/tests/test_protected_policy_context.py @@ -31,6 +31,41 @@ def test_unknown_context_target_refuses_without_interpreting_a_workflow(self): with self.assertRaisesRegex(InputRefused, "context target"): context.validate_context({}, policy_target="elsewhere") + def test_response_loss_ci_successors_preserve_predecessors_and_require_exact_review(self): + predecessors = { + context.CI_WORKFLOW: "363540f9e81e4842099f0cad394ce3fc2a83835ce1355ffef5b16722547699b4", + context.SONAR_WORKFLOW: "f7fad8367df4a4e9565d97e13aa120d55038ed9c749fdfcd86e1312bfca3cced", + } + for path, previous_digest in predecessors.items(): + name = Path(path).stem + raw = (ROOT / f"tests/fixtures/{name}-response-loss-source.yml").read_bytes() + module = (b" tests.test_command_response_loss\n" if path == context.CI_WORKFLOW + else b" tests.test_command_response_loss") + self.assertEqual(1, raw.count(module)) + previous = raw.replace(module, b"", 1) + if path == context.SONAR_WORKFLOW: + self.assertEqual(1, previous.count(b",conformance/command-keys")) + previous = previous.replace(b",conformance/command-keys", b"", 1) + self.assertEqual(previous_digest, hashlib.sha256(previous).hexdigest()) + self.assertIn(previous_digest, context.reviewed_context_hashes("github-control-plane")[path]) + context.validate_context({path: previous}) + self.assertEqual(context.REVIEWED_RESPONSE_LOSS_SOURCE_CI_SUCCESSORS[path], + hashlib.sha256(raw).hexdigest()) + with self.subTest(path=path): + context.validate_context({path: raw}) + with patch.object(context, "REVIEWED_RESPONSE_LOSS_SOURCE_CI_SUCCESSORS", {}), \ + self.assertRaises(InputRefused): + context.validate_context({path: raw}) + for changed in (raw + b"\n# unreviewed\n", + raw.replace(b"tests.test_command_response_loss", b"tests.unreviewed", 1)): + self.assertNotEqual(raw, changed) + with self.assertRaises(InputRefused): + context.validate_context({path: changed}) + with self.assertRaises(InputRefused): + context.validate_context({WORKFLOW: raw}) + with self.assertRaises(InputRefused): + context.validate_context({path: raw}, policy_target="ai") + def test_bound_php_source_ci_successor_preserves_checks_and_requires_exact_review(self): path = context.CI_WORKFLOW raw = (ROOT / "tests/fixtures/ci-bound-php-source.yml").read_bytes()