diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7f1ad6d00..6d016425c 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -20,6 +20,7 @@ concurrency: permissions: contents: read + pull-requests: read jobs: required-workflow-bootstrap: @@ -31,6 +32,126 @@ jobs: echo "Required OpenCode workflow materialized without checking out or executing pull-request content." + - name: Resolve immutable central policy source + id: trusted_source + env: + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + WORKFLOW_SHA: ${{ github.workflow_sha }} + WORKFLOW_REF: ${{ github.workflow_ref }} + run: | + set -euo pipefail + python3 <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + expected_repository = "ContextualWisdomLab/.github" + expected_file = ".github/workflows/opencode-review.yml" + workflow_sha = str( + job_context.get("workflow_sha") or os.environ.get("WORKFLOW_SHA") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or os.environ.get("WORKFLOW_REF") or "" + ).strip() + workflow_ref_head, separator, _ = workflow_ref.partition("@") + if not separator: + print("::error::Required workflow ref is missing its immutable ref separator.", file=sys.stderr) + raise SystemExit(1) + ref_parts = workflow_ref_head.split("/", 2) + if len(ref_parts) < 2 or not ref_parts[0] or not ref_parts[1]: + print("::error::Required workflow ref does not identify a repository.", file=sys.stderr) + raise SystemExit(1) + workflow_repository = "/".join(ref_parts[:2]) + workflow_file_path = str(job_context.get("workflow_file_path") or "").strip() + + if not workflow_file_path: + prefix = f"{expected_repository}/{expected_file}@" + if workflow_ref.startswith(prefix): + workflow_file_path = expected_file + + if workflow_repository != expected_repository: + print( + f"::error::Required workflow repository resolved to {workflow_repository}, expected {expected_repository}.", + file=sys.stderr, + ) + raise SystemExit(1) + if not re.fullmatch(r"[0-9a-fA-F]{40}", workflow_sha): + print("::error::Required workflow SHA is missing or malformed.", file=sys.stderr) + raise SystemExit(1) + if workflow_file_path != expected_file: + print("::error::Required workflow file path is missing or unexpected.", file=sys.stderr) + raise SystemExit(1) + expected_ref_prefix = f"{expected_repository}/{expected_file}@" + if not workflow_ref.startswith(expected_ref_prefix): + print("::error::Required workflow ref is missing or inconsistent.", file=sys.stderr) + raise SystemExit(1) + + print(f"repository={workflow_repository}") + print(f"sha={workflow_sha}") + print(f"workflow_file_path={workflow_file_path}") + PY + + - name: Materialize trusted central policy source + env: + GH_TOKEN: ${{ github.token }} + TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.sha }} + run: | + set -euo pipefail + if [[ ! "$TRUSTED_SOURCE_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Trusted central policy source ref must resolve to the immutable workflow commit SHA before archive materialization." + exit 1 + fi + trusted_archive="${RUNNER_TEMP}/trusted-opencode-policy-source.tar.gz" + trusted_source_dir="${GITHUB_WORKSPACE}/.cwl-required-source" + api_url="${GITHUB_API_URL:-https://api.github.com}" + mkdir -p "$trusted_source_dir" + curl -fsSL \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -o "$trusted_archive" \ + "${api_url}/repos/ContextualWisdomLab/.github/tarball/${TRUSTED_SOURCE_REF}" + tar -xzf "$trusted_archive" -C "$trusted_source_dir" --strip-components=1 + + - name: Verify immutable central policy source + env: + EXPECTED_FILE: ${{ steps.trusted_source.outputs.workflow_file_path }} + run: | + set -euo pipefail + trusted_source_dir="$GITHUB_WORKSPACE/.cwl-required-source" + if [ ! -f "$trusted_source_dir/$EXPECTED_FILE" ] || [ -L "$trusted_source_dir/$EXPECTED_FILE" ]; then + printf '::error::Required workflow source file is missing or symlinked: %s.\n' \ + "$EXPECTED_FILE" + exit 1 + fi + if [ ! -f "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ] || [ -L "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]; then + echo "::error::Trusted Pingora edge policy helper is missing or symlinked." + exit 1 + fi + + - name: Enforce Cloudflare Pingora edge policy + if: ${{ github.event_name == 'pull_request_target' }} + env: + GITHUB_TOKEN: ${{ github.token }} + TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || 0 }} + PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + EVENT_ACTION: ${{ github.event.action || 'unknown' }} + run: | + set -euo pipefail + python3 .cwl-required-source/scripts/ci/pingora_edge_policy.py \ + --repository "$TARGET_REPOSITORY" \ + --pull-request "$PULL_REQUEST_NUMBER" \ + --head-sha "$PULL_REQUEST_HEAD_SHA" \ + --event-action "$EVENT_ACTION" \ + --api-url "https://api.github.com" + coverage-source-tree: name: coverage-source-tree needs: [required-workflow-bootstrap] diff --git a/AGENTS.md b/AGENTS.md index 2df633f49..02b7f7c3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,4 +7,5 @@ Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include ( Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). +Organization edge runtimes use Cloudflare Pingora. Do not add or preserve active Nginx containers, packages, commands, service/config files, or Kubernetes Nginx ingress annotations/classes. Read [`docs/policies/PINGORA_EDGE_POLICY.md`](docs/policies/PINGORA_EDGE_POLICY.md) and ADR-0019 before changing HTTP edge, static-serving, ingress, TLS, or proxy deployment behavior. The materialization contract is also covered by [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md new file mode 100644 index 000000000..aed9a188f --- /dev/null +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -0,0 +1,67 @@ +# ADR-0019: Standardize the CWL edge runtime on Cloudflare Pingora + +- **Status:** Accepted +- **Date:** 2026-08-18 +- **Decision owners:** ContextualWisdomLab platform and product maintainers + +## Context + +CWL repositories currently carry several unrelated Nginx images, configuration +files, ingress annotations, service scripts, and host runbooks. The duplication +creates drift in headers, TLS, timeouts, WebSocket handling, non-root operation, +metrics, and security patching. It also makes every product repository an edge +runtime maintainer. + +Cloudflare Pingora supplies an Apache-2.0, Rust-based framework for HTTP/1 and +HTTP/2 proxying, TLS, gRPC/WebSocket forwarding, graceful reload, failover, and +observability. It is a framework, not a drop-in parser for Nginx configuration, +so a governed shared implementation is required. + +## Decision + +1. Pingora is the only approved CWL public HTTP reverse-proxy, load-balancer, and + static edge runtime. +2. The shared implementation pins Pingora `0.8.1`; updates use a reviewed version + bump, security advisory review, compatibility tests, and exact-current-head CI. +3. Product repositories consume versioned static/proxy artifacts and declarative + contracts. Environment deployment remains in `linux-cluster-ops`. +4. The organization required workflow rejects active Nginx runtime artifacts in + changed final files without executing pull-request code. +5. Initial migration does not use Pingora's experimental cache integration. +6. PHP workloads move to an HTTP application server or reviewed FastCGI adapter + behind Pingora before the public listener changes. + +## Consequences + +### Positive + +- One memory-safe, programmable edge framework and patch stream. +- Reusable security headers, request limits, metrics, graceful shutdown, and + connection-management behavior. +- Product repositories stop maintaining bespoke proxy configuration. +- Exact-head organization enforcement prevents regression. + +### Costs and risks + +- Nginx configuration cannot be translated mechanically; behavior must be tested. +- The organization owns Rust proxy code and its release lifecycle. +- TLS/SNI, FastCGI, caching, and advanced ingress features need explicit modules. +- A faulty shared artifact has broad blast radius, so canary, digest pinning, + rollback, and independent review are mandatory. + +## Alternatives rejected + +- **Keep Nginx with templates:** preserves configuration drift and C-runtime risk. +- **Traefik as the universal gateway:** useful off-the-shelf controller, but does + not meet the user-mandated Pingora standard and creates a second edge runtime. +- **Per-repository Pingora binaries:** duplicates security logic and fragments the + upgrade path. +- **Immediate host replacement without behavior tests:** creates unacceptable + outage and certificate risk. + +## Validation + +The policy scanner has 100% production statement and branch coverage, bounded +GitHub API evidence, path/control escaping, pagination limits, exact-head content +inspection, and fail-closed malformed-evidence tests. Product migrations require +site/proxy behavior tests and deployment-specific smoke tests before cutover. diff --git a/docs/doctoring/pingora-edge-standard.md b/docs/doctoring/pingora-edge-standard.md new file mode 100644 index 000000000..bb624a9c5 --- /dev/null +++ b/docs/doctoring/pingora-edge-standard.md @@ -0,0 +1,42 @@ +# Doctoring record: Cloudflare Pingora edge standard + +## Decision trace + +CWL standardizes public HTTP edge behavior on Cloudflare Pingora and prohibits +active Nginx runtime artifacts. Pingora is treated as a programmable framework; +shared binaries and declarative contracts prevent each product from becoming a +proxy implementation owner. + +The minimum allowed Pingora line is `0.8.x`. Versions through `0.7.0` were affected +by a critical HTTP request-smuggling flaw caused by ambiguous HTTP/1 framing; +`0.8.0` patched it. The selected `0.8.1` release additionally bounded default +HTTP/2 server limits and updated security-sensitive Rustls development +dependencies. The initial CWL implementation avoids experimental cache APIs. + +## Standards and controls + +- HTTP parsing and proxy behavior must follow the patched Pingora framing model + and RFC 9112 semantics referenced by the upstream advisory. +- The shared artifact is Apache-2.0 compatible with CWL permissive-license policy. +- Required-workflow code is bound to its immutable central SHA and never executes + pull-request content. +- Runtime evidence is bounded to one-megabyte UTF-8 regular files and a maximum of + 3,000 changed files; missing or malformed evidence fails closed. +- Exact-head product tests cover host/path routing, SPA fallback, security headers, + WebSocket/streaming, body limits, health, metrics, TLS, and graceful shutdown as + applicable. + +## APA 7th references + +Cloudflare, Inc. (2026, June 4). *Pingora 0.8.1* [Software release]. GitHub. +https://github.com/cloudflare/pingora/releases/tag/0.8.1 + +Cloudflare, Inc. (2026, March 5). *HTTP request smuggling via HTTP/1.0 and +Transfer-Encoding misparsing* (GHSA-hj7x-879w-vrp7) [Security advisory]. GitHub. +https://github.com/cloudflare/pingora/security/advisories/GHSA-hj7x-879w-vrp7 + +Cloudflare, Inc. (n.d.). *Pingora* [Computer software]. GitHub. Retrieved August +18, 2026, from https://github.com/cloudflare/pingora + +Cloudflare, Inc. (n.d.). *Pingora user guide*. GitHub. Retrieved August 18, 2026, +from https://github.com/cloudflare/pingora/tree/main/docs/user_guide diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md new file mode 100644 index 000000000..d01944480 --- /dev/null +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -0,0 +1,63 @@ +# Cloudflare Pingora Edge Runtime Policy + +## Binding rule + +ContextualWisdomLab production and test edge runtimes use **Cloudflare Pingora**. +Active Nginx containers, packages, commands, configuration files, Kubernetes +Nginx ingress annotations/classes, and host-service units are prohibited. + +This is a runtime boundary, not a vocabulary ban. Documentation, license notices, +source-level scanner tests, and migration histories may name Nginx. Pull requests +that modify a runtime candidate are evaluated against the final exact head file, +so deleting a legacy artifact is allowed while preserving it or introducing a new +one fails closed. + +## Why this is not a search-and-replace + +Pingora is a programmable Rust framework rather than an Nginx configuration +interpreter. The organization therefore maintains reusable, versioned Pingora +static-serving and proxy artifacts and gives product repositories only declarative +route/site contracts. Product repositories do not fork proxy internals. + +## Required migration contract + +1. Inventory the current listener, host/path matching, TLS ownership, static root, + upstream protocol, WebSocket/streaming behavior, body/timeout limits, headers, + health probes, metrics, and rollback path. +2. Reproduce those behaviors with the approved Pingora artifact and a versioned + route/site manifest. +3. Add behavior-level tests before deleting the old runtime artifact. +4. Pin Pingora to an exact release at or above `0.8.0`; the shared baseline is + `0.8.1`. Do not use the experimental Pingora cache integration in the initial + migration. +5. Preserve certificate data and rollback evidence, but never keep a runnable + Nginx fallback after cutover. Rollback means redeploying the prior application + release behind Pingora, not reintroducing Nginx. +6. Treat PHP/FastCGI workloads as application-runtime migrations: place an + HTTP-capable PHP application server or a reviewed FastCGI adapter behind + Pingora before cutover. Pingora must remain the public HTTP/TLS edge. + +## Ownership + +- `.github` owns the binding policy, scanner, shared contracts, and required gate. +- `linux-cluster-ops` owns environment-specific listeners, certificates, routes, + service units, backups, rollout, and host cutover. +- Each product owns its static build or upstream application behavior and tests. +- Keyverse remains the identity authority; an edge runtime never becomes the + identity system of record. + +## Enforcement and evidence + +The organization-required `required-workflow-bootstrap` job runs trusted +base-branch scanner code at the immutable required-workflow SHA. It reads bounded +changed-file metadata and final UTF-8 content through GitHub's REST API. It does +not check out or execute pull-request content and receives only read permissions. +Malformed, truncated, binary, symlink, oversized, or unavailable evidence fails +closed. + +## Exception process + +There is no standing Nginx exception. A temporary exception requires a public ADR +with an owner, exact affected asset, buyer impact, security controls, removal date, +and an approved Pingora migration PR. The central scanner remains unchanged; the +exception is implemented by completing the migration before merge. diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py new file mode 100644 index 000000000..5c04cd4c2 --- /dev/null +++ b/scripts/ci/pingora_edge_policy.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +"""Enforce the CWL Pingora-only edge runtime policy on pull-request changes. + +The checker never executes pull-request content. It reads changed-file metadata and +bounded UTF-8 file content through the GitHub REST API, then rejects active Nginx +runtime artifacts while allowing documentation, license text, and source-level +negative test fixtures. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import PurePosixPath +from typing import Callable, Mapping, Sequence +from urllib.error import HTTPError, URLError +from urllib.parse import quote, urlsplit +from urllib.request import HTTPRedirectHandler, Request, build_opener + +MAX_FILE_BYTES = 1_048_576 +MAX_RESPONSE_BYTES = 16_777_216 +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +SHA_RE = re.compile(r"^[0-9a-f]{40}$") +GITHUB_API_ORIGIN = "https://api.github.com" + +DOCUMENT_SUFFIXES = frozenset({".md", ".mdx", ".rst", ".adoc", ".txt"}) +SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) +LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) +DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) +DOCUMENTATION_ROOT_NAMES = frozenset({"readme", "changelog", "changes"}) + +RUNTIME_PATH_NAMES = frozenset({ + "dockerfile", + "containerfile", + "nginx.conf", + "nginx.service", +}) +RUNTIME_PATH_PARTS = frozenset({"deploy", "deployment", "docker", "infra", "k8s", "kubernetes", "ops"}) + +CONTENT_RULES: tuple[tuple[str, re.Pattern[str]], ...] = ( + ( + "nginx_container_image", + re.compile( + r"(?im)^\s*(?:FROM|image:)\s+(?:docker\.io/)?(?:library/)?nginx(?:[:@\s]|$)" + ), + ), + ( + "nginx_ingress_controller", + re.compile( + r"(?im)(?:nginx\.ingress\.kubernetes\.io/|" + r"kubernetes\.io/ingress\.class\s*:\s*[\"']?nginx\b|" + r"ingressClassName\s*:\s*nginx\b)" + ), + ), + ( + "nginx_runtime_command", + re.compile( + r"(?im)(?:^\s*(?:systemctl|service)\s+(?:--\S+\s+)*nginx\b|" + r"(?:CMD|ENTRYPOINT)\s*\[[^\n]*[\"']nginx[\"']|" + r"\bnginx\s+-g\s+[\"']daemon\s+off;)" + ), + ), + ( + "nginx_runtime_path", + re.compile( + r"(?i)(?:/etc/nginx(?:/|\b)|/var/(?:cache|run|log)/nginx(?:/|\b)|" + r"/usr/share/nginx(?:/|\b))" + ), + ), + ( + "nginx_package_install", + re.compile( + r"(?im)^\s*(?:RUN\s+)?(?:apk\s+add|apt(?:-get)?\s+install|" + r"dnf\s+install|yum\s+install)\b[^\n#]*\bnginx\b" + ), + ), +) + + +@dataclass(frozen=True) +class ChangedFile: + """A bounded subset of GitHub pull-request changed-file metadata.""" + + path: str + status: str + patch: str + patch_available: bool = True + + +@dataclass(frozen=True) +class Violation: + """A single policy violation suitable for GitHub annotation output.""" + + path: str + rule: str + line: int + excerpt: str + + +class PolicyError(RuntimeError): + """Raised when policy evidence cannot be collected or validated safely.""" + + +OpenJson = Callable[[str, str], object] + + +class NoRedirectHandler(HTTPRedirectHandler): + """Reject redirects so validated GitHub API requests keep one origin.""" + + def redirect_request(self, *_args: object, **_kwargs: object) -> None: + """Return no follow-up request for any HTTP redirect response.""" + return None + + +github_opener = build_opener(NoRedirectHandler()) + + +def _is_documentation_or_source_fixture(path: str) -> bool: + """Return whether *path* is prose, license text, or scanner source fixture.""" + + pure = PurePosixPath(path) + lower_name = pure.name.lower() + stem = pure.stem.lower() + is_known_documentation_path = pure.parts and ( + pure.parts[0].lower() in DOCUMENTATION_DIRECTORIES + or (len(pure.parts) == 1 and stem in DOCUMENTATION_ROOT_NAMES) + ) + if lower_name in LICENSE_NAMES or ( + is_known_documentation_path and pure.suffix.lower() in DOCUMENT_SUFFIXES + ): + return True + if pure.as_posix() == "scripts/ci/pingora_edge_policy.py": + return True + if any(part.lower() in {"test", "tests"} for part in pure.parts) and pure.suffix.lower() in SOURCE_TEST_SUFFIXES: + return True + return False + + +def _runtime_path_rule(path: str) -> str | None: + """Return a path-level violation rule for active Nginx runtime artifacts.""" + + pure = PurePosixPath(path) + lower_parts = tuple(part.lower() for part in pure.parts) + lower_name = pure.name.lower() + if lower_name in RUNTIME_PATH_NAMES and "nginx" in lower_name: + return "nginx_runtime_artifact" + if "nginx" in lower_parts and any(part in RUNTIME_PATH_PARTS for part in lower_parts): + return "nginx_runtime_artifact" + if lower_name.startswith("nginx-") and pure.suffix.lower() in {".conf", ".service", ".sh", ".yaml", ".yml"}: + return "nginx_runtime_artifact" + return None + + +def _line_number(content: str, start: int) -> int: + """Translate a character offset into a one-based line number.""" + + return content.count("\n", 0, start) + 1 + + +def scan_content(path: str, content: str) -> tuple[Violation, ...]: + """Return all Pingora policy violations found in one final file version.""" + + if _is_documentation_or_source_fixture(path): + return () + violations: list[Violation] = [] + path_rule = _runtime_path_rule(path) + if path_rule is not None: + violations.append(Violation(path, path_rule, 1, "active Nginx runtime artifact path")) + for rule, pattern in CONTENT_RULES: + for match in pattern.finditer(content): + excerpt = " ".join(match.group(0).strip().split())[:160] + violations.append(Violation(path, rule, _line_number(content, match.start()), excerpt)) + return tuple(violations) + + +def _validate_github_api_url(url: str) -> None: + """Reject policy evidence URLs outside the public GitHub REST origin.""" + + parsed = urlsplit(url) + if ( + parsed.scheme != "https" + or parsed.hostname != "api.github.com" + or parsed.port is not None + or parsed.username is not None + or parsed.password is not None + or not parsed.path.startswith("/repos/") + or parsed.fragment + ): + raise PolicyError("GitHub API policy URL is outside the approved origin") + + +def _github_open_json(url: str, token: str) -> object: + """Read one bounded GitHub REST JSON document using bearer authentication.""" + + _validate_github_api_url(url) + request = Request( # noqa: S310 - URL is validated immediately above + url, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "cwl-pingora-edge-policy/1", + }, + ) + try: + with github_opener.open(request, timeout=30) as response: + payload = response.read(MAX_RESPONSE_BYTES + 1) + except (HTTPError, URLError, TimeoutError) as exc: + raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc + if len(payload) > MAX_RESPONSE_BYTES: + raise PolicyError("GitHub API policy response exceeded the bounded response size") + try: + return json.loads(payload) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise PolicyError("GitHub API returned malformed JSON policy evidence") from exc + + +def _load_changed_files(api_url: str, repository: str, pull_request: int, token: str, opener: OpenJson) -> tuple[ChangedFile, ...]: + """Load every changed-file page while enforcing shape and pagination bounds.""" + + files: list[ChangedFile] = [] + for page in range(1, 31): + url = f"{api_url}/repos/{repository}/pulls/{pull_request}/files?per_page=100&page={page}" + payload = opener(url, token) + if not isinstance(payload, list): + raise PolicyError("GitHub changed-file evidence is not a JSON array") + for item in payload: + if not isinstance(item, Mapping): + raise PolicyError("GitHub changed-file entry is not an object") + path = item.get("filename") + status = item.get("status") + raw_patch = item.get("patch") + patch = "" if raw_patch is None else raw_patch + if ( + not isinstance(path, str) + or not path + or not isinstance(status, str) + or not isinstance(patch, str) + ): + raise PolicyError("GitHub changed-file entry has invalid bounded fields") + files.append( + ChangedFile( + path=path, + status=status, + patch=patch, + patch_available=raw_patch is not None, + ) + ) + if len(payload) < 100: + return tuple(files) + raise PolicyError("GitHub changed-file pagination exceeded 3,000 files") + + +def _load_file_content(api_url: str, repository: str, path: str, head_sha: str, token: str, opener: OpenJson) -> str: + """Load one final head file as bounded UTF-8 text from the Contents API.""" + + encoded_path = quote(path, safe="/") + url = f"{api_url}/repos/{repository}/contents/{encoded_path}?ref={head_sha}" + payload = opener(url, token) + if not isinstance(payload, Mapping): + raise PolicyError(f"GitHub content evidence for {path} is not an object") + if payload.get("type") != "file" or payload.get("encoding") != "base64": + raise PolicyError(f"GitHub content evidence for {path} is not a regular base64 file") + encoded = payload.get("content") + declared_size = payload.get("size") + if not isinstance(encoded, str) or not isinstance(declared_size, int) or declared_size < 0 or declared_size > MAX_FILE_BYTES: + raise PolicyError(f"GitHub content evidence for {path} exceeds or violates the size contract") + try: + raw = base64.b64decode("".join(encoded.split()), validate=True) + except (ValueError, TypeError) as exc: + raise PolicyError(f"GitHub content evidence for {path} is invalid base64") from exc + if len(raw) != declared_size: + raise PolicyError(f"GitHub content evidence for {path} has a size mismatch") + try: + return raw.decode("utf-8") + except UnicodeDecodeError as exc: + raise PolicyError(f"Runtime policy candidate {path} is not valid UTF-8") from exc + + +def _needs_content_scan(changed: ChangedFile) -> bool: + """Return whether a changed final file can carry an active edge runtime.""" + + if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): + return False + if not changed.patch_available: + return True + if _runtime_path_rule(changed.path) is not None: + return True + lower_path = changed.path.lower() + if PurePosixPath(lower_path).name in {"dockerfile", "containerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"}: + return True + if PurePosixPath(lower_path).suffix in {".conf", ".service", ".yaml", ".yml", ".sh"}: + return "nginx" in changed.patch.lower() or any(token in lower_path for token in ("deploy", "infra", "k8s", "docker")) + return "nginx" in changed.patch.lower() + + +def evaluate_pull_request( + *, + api_url: str, + repository: str, + pull_request: int, + head_sha: str, + event_action: str, + token: str, + opener: OpenJson = _github_open_json, +) -> tuple[Violation, ...]: + """Evaluate one pull request without checking out or executing its content.""" + + if event_action == "closed": + return () + if not REPOSITORY_RE.fullmatch(repository): + raise PolicyError("Repository identity is malformed") + if pull_request <= 0: + raise PolicyError("Pull-request number must be positive") + if not SHA_RE.fullmatch(head_sha): + raise PolicyError("Pull-request head SHA is malformed") + if not token: + raise PolicyError("GITHUB_TOKEN is required for policy evidence") + changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) + violations: list[Violation] = [] + for changed in changed_files: + if not _needs_content_scan(changed): + continue + content = _load_file_content(api_url.rstrip("/"), repository, changed.path, head_sha, token, opener) + violations.extend(scan_content(changed.path, content)) + return tuple(violations) + + +def _annotation(violation: Violation) -> str: + """Render one bounded GitHub workflow command annotation.""" + + path = violation.path.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A").replace(",", "%2C") + message = f"CWL edge policy requires Cloudflare Pingora; {violation.rule}: {violation.excerpt}" + message = message.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") + return f"::error file={path},line={violation.line}::{message}" + + +def build_parser() -> argparse.ArgumentParser: + """Build the command-line parser used by the required workflow.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repository", required=True) + parser.add_argument("--pull-request", required=True, type=int) + parser.add_argument("--head-sha", required=True) + parser.add_argument("--event-action", required=True) + parser.add_argument("--api-url", default=GITHUB_API_ORIGIN) + return parser + + +def main(argv: Sequence[str] | None = None, environ: Mapping[str, str] | None = None) -> int: + """Run the policy checker and return a process exit status.""" + + args = build_parser().parse_args(argv) + env = os.environ if environ is None else environ + try: + violations = evaluate_pull_request( + api_url=args.api_url, + repository=args.repository, + pull_request=args.pull_request, + head_sha=args.head_sha, + event_action=args.event_action, + token=env.get("GITHUB_TOKEN", ""), + ) + except PolicyError as exc: + print(f"::error::Pingora edge policy could not establish complete evidence: {exc}") + return 2 + if violations: + for violation in violations: + print(_annotation(violation)) + print(f"CWL Pingora edge policy rejected {len(violations)} active Nginx runtime artifact(s).") + return 1 + print("CWL Pingora edge policy passed: no changed active Nginx runtime artifact remains.") + return 0 + + +if __name__ == "__main__": # pragma: no cover - exercised through main() contract tests + sys.exit(main()) diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py new file mode 100644 index 000000000..d03bd0ce2 --- /dev/null +++ b/tests/test_pingora_edge_policy.py @@ -0,0 +1,395 @@ +"""Regression tests for the organization-wide Pingora edge policy.""" + +from __future__ import annotations + +import base64 +import importlib.util +import sys +from io import BytesIO +from pathlib import Path +from urllib.error import HTTPError, URLError + +import pytest + +MODULE_PATH = Path(__file__).parents[1] / "scripts" / "ci" / "pingora_edge_policy.py" +SPEC = importlib.util.spec_from_file_location("pingora_edge_policy", MODULE_PATH) +assert SPEC and SPEC.loader +policy = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = policy +SPEC.loader.exec_module(policy) + + +def encoded_file(content: str, *, size: int | None = None, kind: str = "file", encoding: str = "base64") -> dict[str, object]: + """Build one GitHub Contents API response.""" + + raw = content.encode() + return { + "type": kind, + "encoding": encoding, + "size": len(raw) if size is None else size, + "content": base64.b64encode(raw).decode(), + } + + +def test_scan_content_rejects_runtime_paths_and_every_denied_runtime_form() -> None: + """Runtime filenames and all supported active Nginx forms fail closed.""" + + content = """FROM nginx:1.27-alpine +image: nginx@sha256:abc +nginx.ingress.kubernetes.io/rewrite-target: / +kubernetes.io/ingress.class: nginx +ingressClassName: nginx +CMD [\"nginx\", \"-g\", \"daemon off;\"] +systemctl nginx restart +COPY x /etc/nginx/conf.d/default.conf +RUN apk add --no-cache nginx +""" + violations = policy.scan_content("infra/nginx/nginx.conf", content) + rules = {item.rule for item in violations} + assert rules == { + "nginx_runtime_artifact", + "nginx_container_image", + "nginx_ingress_controller", + "nginx_runtime_command", + "nginx_runtime_path", + "nginx_package_install", + } + assert all(item.line >= 1 and item.excerpt for item in violations) + + +def test_scan_content_allows_prose_license_and_source_negative_fixtures() -> None: + """Policy prose, license text, and scanner source fixtures can name Nginx.""" + + sample = "FROM nginx:latest\nnginx.ingress.kubernetes.io/foo: bar\n" + assert policy.scan_content("docs/migration.md", sample) == () + assert policy.scan_content("COPYING", sample) == () + assert policy.scan_content("scripts/ci/pingora_edge_policy.py", sample) == () + assert policy.scan_content("tests/test_policy.py", sample) == () + assert policy.scan_content("tests/negative_fixture.rs", sample) == () + + +@pytest.mark.parametrize("directory", ["testing", "contests", "assert", "my_tests"]) +def test_scan_content_does_not_treat_test_name_substrings_as_fixtures( + directory: str, +) -> None: + """Only exact test directories are fixture boundaries for active content.""" + + violations = policy.scan_content( + f"{directory}/runtime.py", + "FROM nginx:1.27-alpine\n", + ) + + assert [item.rule for item in violations] == ["nginx_container_image"] + + +def test_runtime_path_rule_covers_script_and_config_shapes() -> None: + """Active Nginx filenames are blocked without relying on their contents.""" + + assert policy._runtime_path_rule("tests/live/nginx.conf") == "nginx_runtime_artifact" + assert policy._runtime_path_rule("ops/nginx-backup.sh") == "nginx_runtime_artifact" + assert policy._runtime_path_rule("infra/nginx/default.yaml") == "nginx_runtime_artifact" + assert policy._runtime_path_rule("config/nginx.service") == "nginx_runtime_artifact" + assert policy._runtime_path_rule("docs/nginx-history.md") is None + + +def test_needs_content_scan_is_delta_bounded() -> None: + """Removed/prose files skip, while runtime candidates and changed markers scan.""" + + changed = policy.ChangedFile + assert not policy._needs_content_scan(changed("Dockerfile", "removed", "+FROM nginx")) + assert not policy._needs_content_scan(changed("README.md", "modified", "+nginx")) + assert policy._needs_content_scan(changed("Dockerfile", "modified", "-FROM nginx\n+FROM scratch")) + assert policy._needs_content_scan(changed("config/runtime.txt", "modified", "+FROM nginx")) + assert policy._needs_content_scan(changed("infra/nginx/default.yaml", "modified", "+server: edge")) + assert policy._needs_content_scan(changed("infra/deployment.yaml", "modified", "+image: app")) + assert policy._needs_content_scan(changed("config/runtime.conf", "modified", "+upstream nginx")) + assert policy._needs_content_scan( + changed("config/runtime.conf", "modified", "", patch_available=False) + ) + assert not policy._needs_content_scan(changed("config/runtime.conf", "modified", "+upstream app")) + assert policy._needs_content_scan(changed("src/runtime.go", "modified", "+exec nginx")) + assert not policy._needs_content_scan(changed("src/runtime.go", "modified", "+exec pingora")) + + +def test_untrusted_document_suffix_does_not_bypass_runtime_scan() -> None: + """A runtime-looking file cannot evade policy checks by using a prose suffix.""" + + violations = policy.scan_content("config/runtime.txt", "FROM nginx:1.27-alpine\n") + assert [item.rule for item in violations] == ["nginx_container_image"] + + +def test_evaluate_pull_request_reads_pagination_and_final_content() -> None: + """The checker uses every file page and scans final head content, not removed lines.""" + + calls: list[str] = [] + first_page = [ + {"filename": f"docs/file-{index}.md", "status": "modified", "patch": "+Nginx"} + for index in range(100) + ] + second_page = [ + {"filename": "Dockerfile", "status": "modified", "patch": "-FROM nginx\n+FROM scratch"}, + {"filename": "old/nginx.conf", "status": "removed", "patch": "-server {}"}, + {"filename": "deploy/proxy.yaml", "status": "modified"}, + ] + + def opener(url: str, token: str) -> object: + calls.append(url) + assert token == "token" + if url.endswith("page=1"): + return first_page + if url.endswith("page=2"): + return second_page + if "/contents/Dockerfile" in url: + return encoded_file("FROM scratch\n") + if "/contents/deploy/proxy.yaml" in url: + return encoded_file("image: cwl-pingora-proxy:0.1.0\n") + raise AssertionError(url) + + result = policy.evaluate_pull_request( + api_url="https://api.github.test/", + repository="ContextualWisdomLab/example", + pull_request=7, + head_sha="a" * 40, + event_action="synchronize", + token="token", + opener=opener, + ) + assert result == () + assert any("page=2" in url for url in calls) + assert all("old/nginx.conf" not in url for url in calls) + + +def test_evaluate_pull_request_reports_final_runtime_violation() -> None: + """A changed active runtime image is rejected from final head content.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/9/files" in url: + return [{"filename": "docker-compose.yml", "status": "modified", "patch": "+image: nginx"}] + return encoded_file("services:\n edge:\n image: nginx:1.27-alpine\n") + + result = policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=9, + head_sha="b" * 40, + event_action="opened", + token="token", + opener=opener, + ) + assert [item.rule for item in result] == ["nginx_container_image"] + + +def test_closed_event_skips_without_credentials_or_identity_validation() -> None: + """Closed-event cleanup remains a no-op for the required-workflow context.""" + + assert policy.evaluate_pull_request( + api_url="x", + repository="bad repo", + pull_request=0, + head_sha="bad", + event_action="closed", + token="", + opener=lambda _url, _token: pytest.fail("must not open"), + ) == () + + +@pytest.mark.parametrize( + ("repository", "pull_request", "head_sha", "token", "message"), + [ + ("bad repo", 1, "a" * 40, "x", "Repository identity"), + ("a/b", 0, "a" * 40, "x", "must be positive"), + ("a/b", 1, "bad", "x", "head SHA"), + ("a/b", 1, "a" * 40, "", "GITHUB_TOKEN"), + ], +) +def test_evaluate_pull_request_rejects_invalid_authority( + repository: str, pull_request: int, head_sha: str, token: str, message: str +) -> None: + """Malformed authority and absent credentials fail before network access.""" + + with pytest.raises(policy.PolicyError, match=message): + policy.evaluate_pull_request( + api_url="x", + repository=repository, + pull_request=pull_request, + head_sha=head_sha, + event_action="opened", + token=token, + opener=lambda _url, _token: pytest.fail("must not open"), + ) + + +@pytest.mark.parametrize( + ("payload", "message"), + [ + ({}, "not a JSON array"), + (["bad"], "entry is not an object"), + ([{"filename": "", "status": "modified", "patch": ""}], "invalid bounded fields"), + ], +) +def test_changed_file_evidence_shape_is_fail_closed(payload: object, message: str) -> None: + """Malformed changed-file API shapes fail closed.""" + + with pytest.raises(policy.PolicyError, match=message): + policy._load_changed_files("api", "a/b", 1, "x", lambda _url, _token: payload) + + +def test_changed_file_pagination_bound_is_fail_closed() -> None: + """More than 3,000 changed files cannot silently truncate policy evidence.""" + + page = [{"filename": f"f-{index}", "status": "modified", "patch": ""} for index in range(100)] + with pytest.raises(policy.PolicyError, match="3,000"): + policy._load_changed_files("api", "a/b", 1, "x", lambda _url, _token: page) + + +@pytest.mark.parametrize( + ("payload", "message"), + [ + ([], "not an object"), + ({"type": "symlink", "encoding": "base64", "size": 0, "content": ""}, "not a regular"), + ({"type": "file", "encoding": "base64", "size": policy.MAX_FILE_BYTES + 1, "content": ""}, "size contract"), + ({"type": "file", "encoding": "base64", "size": 1, "content": "!"}, "invalid base64"), + ({"type": "file", "encoding": "base64", "size": 2, "content": base64.b64encode(b"x").decode()}, "size mismatch"), + ({"type": "file", "encoding": "base64", "size": 1, "content": base64.b64encode(b"\xff").decode()}, "not valid UTF-8"), + ], +) +def test_file_content_evidence_is_fail_closed(payload: object, message: str) -> None: + """Unbounded, nonregular, corrupt, or binary runtime content is rejected.""" + + with pytest.raises(policy.PolicyError, match=message): + policy._load_file_content("api", "a/b", "x y", "a" * 40, "x", lambda url, _token: payload) + + +def test_file_content_loader_quotes_paths() -> None: + """Contents API paths are percent-encoded without losing path separators.""" + + seen: list[str] = [] + + def opener(url: str, _token: str) -> object: + seen.append(url) + return encoded_file("ok") + + assert policy._load_file_content("api", "a/b", "dir/a b.conf", "a" * 40, "x", opener) == "ok" + assert "dir/a%20b.conf" in seen[0] + + +def test_file_content_loader_accepts_wrapped_base64_content() -> None: + """GitHub's line-wrapped Contents API base64 remains valid evidence.""" + + payload = encoded_file("FROM scratch\n") + encoded = str(payload["content"]) + payload["content"] = "\n".join(encoded[index : index + 4] for index in range(0, len(encoded), 4)) + + assert policy._load_file_content( + "api", "a/b", "Dockerfile", "a" * 40, "x", lambda _url, _token: payload + ) == "FROM scratch\n" + + +class FakeResponse: + """Context-managed bounded response for direct opener tests.""" + + def __init__(self, payload: bytes): + self.payload = payload + + def __enter__(self) -> "FakeResponse": + return self + + def __exit__(self, *_args: object) -> None: + return None + + def read(self, _limit: int) -> bytes: + return self.payload + + +def test_github_open_json_accepts_valid_bounded_json(monkeypatch: pytest.MonkeyPatch) -> None: + """The default opener parses bounded GitHub JSON.""" + + monkeypatch.setattr(policy.github_opener, "open", lambda _request, timeout: FakeResponse(b'{"ok": true}')) + assert policy._github_open_json("https://api.github.com/repos/a/b", "token") == {"ok": True} + + +@pytest.mark.parametrize("exc", [URLError("dns"), TimeoutError(), HTTPError("x", 500, "bad", {}, BytesIO())]) +def test_github_open_json_sanitizes_transport_failures(monkeypatch: pytest.MonkeyPatch, exc: Exception) -> None: + """Network failures preserve only their stable class, never response text.""" + + def fail(_request: object, timeout: int) -> object: + assert timeout == 30 + raise exc + + monkeypatch.setattr(policy.github_opener, "open", fail) + with pytest.raises(policy.PolicyError, match=type(exc).__name__): + policy._github_open_json("https://api.github.com/repos/a/b", "token") + + +def test_github_open_json_rejects_oversized_and_malformed_payloads(monkeypatch: pytest.MonkeyPatch) -> None: + """REST evidence must remain one bounded valid JSON document.""" + + monkeypatch.setattr(policy.github_opener, "open", lambda _request, timeout: FakeResponse(b"x" * (policy.MAX_RESPONSE_BYTES + 1))) + with pytest.raises(policy.PolicyError, match="bounded response size"): + policy._github_open_json("https://api.github.com/repos/a/b", "token") + monkeypatch.setattr(policy.github_opener, "open", lambda _request, timeout: FakeResponse(b"not-json")) + with pytest.raises(policy.PolicyError, match="malformed JSON"): + policy._github_open_json("https://api.github.com/repos/a/b", "token") + + +@pytest.mark.parametrize( + "url", + [ + "http://api.github.com/repos/a/b", + "https://evil.example/repos/a/b", + "https://user@api.github.com/repos/a/b", + "https://api.github.com:443/repos/a/b", + "https://api.github.com/user", + "https://api.github.com/repos/a/b#fragment", + ], +) +def test_github_open_json_rejects_nonapproved_origins(url: str) -> None: + """Evidence collection cannot be redirected to attacker-controlled origins.""" + + with pytest.raises(policy.PolicyError, match="approved origin"): + policy._github_open_json(url, "token") + + +def test_github_opener_never_constructs_redirect_requests() -> None: + """The policy opener refuses redirects rather than changing API origins.""" + + assert policy.NoRedirectHandler().redirect_request(None, None, 302, "Found", {}, "https://evil.example") is None + + +def test_annotation_escapes_workflow_command_fields() -> None: + """Workflow annotations cannot be broken by path or excerpt control syntax.""" + + annotation = policy._annotation(policy.Violation("a,b%\n", "rule", 2, "bad%\ntext")) + assert annotation.startswith("::error file=a%2Cb%25%0A,line=2::") + assert "bad%25%0Atext" in annotation + + +def test_main_returns_pass_reject_and_evidence_error(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """The CLI exposes distinct success, policy rejection, and evidence-error statuses.""" + + base_args = ["--repository", "a/b", "--pull-request", "1", "--head-sha", "a" * 40, "--event-action", "opened"] + monkeypatch.setattr(policy, "evaluate_pull_request", lambda **_kwargs: ()) + assert policy.main(base_args, {"GITHUB_TOKEN": "x"}) == 0 + assert "passed" in capsys.readouterr().out + + violation = policy.Violation("Dockerfile", "nginx_container_image", 1, "FROM nginx") + monkeypatch.setattr(policy, "evaluate_pull_request", lambda **_kwargs: (violation,)) + assert policy.main(base_args, {"GITHUB_TOKEN": "x"}) == 1 + assert "rejected 1" in capsys.readouterr().out + + def evidence_error(**_kwargs: object) -> tuple[object, ...]: + raise policy.PolicyError("unavailable") + + monkeypatch.setattr(policy, "evaluate_pull_request", evidence_error) + assert policy.main(base_args, {"GITHUB_TOKEN": "x"}) == 2 + assert "complete evidence" in capsys.readouterr().out + + +def test_build_parser_uses_pinned_public_api_origin() -> None: + """The CLI defaults to the approved public GitHub API origin.""" + + parser = policy.build_parser() + args = parser.parse_args([ + "--repository", "a/b", "--pull-request", "1", "--head-sha", "a" * 40, "--event-action", "opened" + ]) + assert args.api_url == "https://api.github.com" diff --git a/tests/test_pingora_edge_workflow_contract.py b/tests/test_pingora_edge_workflow_contract.py new file mode 100644 index 000000000..82a85986a --- /dev/null +++ b/tests/test_pingora_edge_workflow_contract.py @@ -0,0 +1,46 @@ +"""Contract tests for organization-wide Pingora enforcement.""" + +from pathlib import Path + + +ROOT = Path(__file__).parents[1] +WORKFLOW = ROOT / ".github" / "workflows" / "opencode-review.yml" + + +def test_required_workflow_enforces_pingora_without_executing_pr_content() -> None: + """The trusted bootstrap must scan API evidence from immutable central code.""" + + text = WORKFLOW.read_text(encoding="utf-8") + assert "pull-requests: read" in text + assert "JOB_CONTEXT_JSON: ${{ toJSON(job) }}" in text + assert "WORKFLOW_SHA: ${{ github.workflow_sha }}" in text + assert "WORKFLOW_REF: ${{ github.workflow_ref }}" in text + assert "GITHUB_CONTEXT_JSON" not in text + assert 'job_context.get("workflow_sha") or os.environ.get("WORKFLOW_SHA")' in text + assert 'workflow_ref.partition("@")' in text + assert 'workflow_repository = "/".join(ref_parts[:2])' in text + assert 'job_context.get("workflow_repository")' not in text + assert "TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.sha }}" in text + assert "repos/ContextualWisdomLab/.github/tarball/${TRUSTED_SOURCE_REF}" in text + assert "Trusted central policy source ref must resolve to the immutable workflow commit SHA" in text + assert "actions/checkout" not in text + assert "scripts/ci/pingora_edge_policy.py" in text + assert '--api-url "https://api.github.com"' in text + assert "secrets:" not in text + + # The required workflow must never turn the untrusted PR head into a source + # checkout ref; it is only an evidence identifier passed to the API scanner. + assert "ref: ${{ github.event.pull_request.head.sha }}" not in text + assert 'TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.sha }}' in text + assert 'tarball/${TRUSTED_SOURCE_REF}' in text + + # The source archive must contain both the workflow and the policy helper, + # and symlinked replacements must fail before the helper is executed. + assert '[ ! -f "$trusted_source_dir/$EXPECTED_FILE" ]' in text + assert '[ -L "$trusted_source_dir/$EXPECTED_FILE" ]' in text + assert '[ ! -f "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]' in text + assert '[ -L "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]' in text + assert "if: ${{ github.event_name == 'pull_request_target' }}" in text + assert text.index("Verify immutable central policy source") < text.index( + "Enforce Cloudflare Pingora edge policy" + )