From 4f7d99e2dd4422f3d81cf5b709581be963f5818a Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Fri, 25 Sep 2026 17:53:39 -0500 Subject: [PATCH 1/5] fix(deploy): make readiness waits configurable (default 90s) (#24) The staging boot, rollback-restore and production health waits each polled /healthz for only 20s (40 x 0.5s). A cold uvicorn boot on a loaded FORGE exceeds that: it aborted the d8ad9fd security release at staging, and the same budget can report a healthy rollback as failed or roll back a good production release. - CERTFORGE_READY_TIMEOUT_S (default 90) drives all three waits; a healthy service still returns on the first successful probe. - On a staging readiness failure, print the /healthz probe result and keep service.log under $STATE_ROOT/deploy-logs/ before the EXIT trap removes the scratch directory. Claude-Session: https://claude.ai/code/session_0152mN1wMwj9vE2YmV2xZF4F Co-authored-by: echoomegaprime <314902331+echoomegaprime@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- deploy/deploy_forge.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index c280297..a266eba 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -14,6 +14,11 @@ EXPECTED_COMMIT_SHA="${CERTFORGE_EXPECTED_COMMIT_SHA:-}" RELEASE_ROOT="${CERTFORGE_RELEASE_ROOT:-/home/forge/echo-certification-forge-releases}" CURRENT_LINK="${CERTFORGE_CURRENT_LINK:-/home/forge/echo-certification-forge-current}" STATE_ROOT="${CERTFORGE_STATE_ROOT:-/home/forge/echo-certification-forge/var}" +# Readiness waits poll /healthz every 0.5s and return on the first healthy probe. +# 20s proved too short for a cold uvicorn boot on a loaded FORGE (staging abort, +# false rollback-restore failure, needless production rollback). +READY_TIMEOUT_S="${CERTFORGE_READY_TIMEOUT_S:-90}" +READY_POLLS=$(( READY_TIMEOUT_S * 2 )) ADAPTER_DIR="${ECHO_CERTFORGE_PROD_ADAPTER_DIR:-$STATE_ROOT/p5}" ADAPTER_MODE="${CERTFORGE_ADAPTER_MODE:-required}" TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-7dc98e0e95e6dd2c000ec069a8c46c4d1d49a4fe869ad4eae25e059d103644f4}" @@ -221,10 +226,12 @@ ECHO_CERTFORGE_API_KEY_PEPPER="$STAGING_PEPPER" \ STAGING_PID=$! ready=0 -for _ in $(seq 1 40); do +for _ in $(seq 1 "$READY_POLLS"); do kill -0 "$STAGING_PID" 2>/dev/null || { echo "!! staging process exited before readiness" - tail -20 "$STAGING_ROOT/service.log" + mkdir -p "$STATE_ROOT/deploy-logs" && + cp -f "$STAGING_ROOT/service.log" "$STATE_ROOT/deploy-logs/staging-$RELEASE_ID.log" 2>/dev/null || true + tail -40 "$STAGING_ROOT/service.log" exit 1 } curl -sf "http://127.0.0.1:$STAGING_PORT/healthz" >/dev/null 2>&1 && { @@ -240,7 +247,12 @@ for _ in $(seq 1 40); do done if [ "$ready" != 1 ]; then echo "!! staging never became healthy" - tail -20 "$STAGING_ROOT/service.log" + echo " waited ${READY_TIMEOUT_S}s; set CERTFORGE_READY_TIMEOUT_S to adjust" + curl -sS -m 5 -o /dev/null -w " /healthz probe: HTTP %{http_code}\n" \ + "http://127.0.0.1:$STAGING_PORT/healthz" 2>&1 || true + mkdir -p "$STATE_ROOT/deploy-logs" && + cp -f "$STAGING_ROOT/service.log" "$STATE_ROOT/deploy-logs/staging-$RELEASE_ID.log" 2>/dev/null || true + tail -40 "$STAGING_ROOT/service.log" exit 1 fi @@ -383,7 +395,7 @@ rollback_production() { if [ "$PREV_ACTIVE" = "active" ]; then sudo systemctl start "$SERVICE.service" || rollback_status=1 restored=0 - for _ in $(seq 1 40); do + for _ in $(seq 1 "$READY_POLLS"); do service_owns_port "$PROD_PORT" && curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && { restored=1 @@ -585,7 +597,7 @@ sudo systemctl enable "$DISPATCH_SERVICE.service" echo "== [8/9] production health + live-smoke ==" ready=0 -for _ in $(seq 1 40); do +for _ in $(seq 1 "$READY_POLLS"); do service_owns_port "$PROD_PORT" && curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && { ready=1 From d8d1b362af79e1141b0eeb896310c3cb9b60d84f Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sat, 3 Oct 2026 08:36:39 -0500 Subject: [PATCH 2/5] Bound deployment readiness probes and validate timeout configuration --- deploy/README.md | 22 ++++ deploy/deploy_forge.sh | 27 ++-- deploy/readiness.sh | 40 ++++++ tests/test_deploy_gate.py | 2 +- tests/test_deploy_readiness.py | 225 +++++++++++++++++++++++++++++++++ 5 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 deploy/README.md create mode 100644 deploy/readiness.sh create mode 100644 tests/test_deploy_readiness.py diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..ca18a25 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,22 @@ +# Deployment readiness + +`deploy_forge.sh` uses `readiness.sh` for staging, production, and rollback +health polling. Keep the two files together when invoking the deployment script. + +`CERTFORGE_READY_TIMEOUT_S` defaults to `90` and accepts a decimal integer from +`1` through `1800`, without leading zeroes. Invalid values fail before locking, +fetching, or changing deployment state. Each wait has a fresh elapsed-time +deadline; failed HTTP probes consume that budget. Each connection/response is +limited to the smaller of two seconds and the remaining budget. A healthy probe +returns immediately. Shell clock granularity and the polling pause can add a +subsecond tolerance; service-manager and listener-ownership commands are separate +from the HTTP budget. + +Staging failures retain the service log under +`$STATE_ROOT/deploy-logs/staging-.log`. Listener ownership, signed +readiness verification, live-smoke gates, and rollback requirements still apply. + +A green source-validation run does not prove current production identity or +provide signed production E2E evidence. Every changed candidate needs fresh +exact-SHA certification. Historical smoke reports and old certified tags must not +be reused as certification for a new commit. diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index 4bd94b9..01bf226 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -20,11 +20,11 @@ CURRENT_LINK="${CERTFORGE_CURRENT_LINK:-/home/forge/echo-certification-forge-cur STATE_ROOT="${CERTFORGE_STATE_ROOT:-/home/forge/echo-certification-forge/var}" PRODUCTION_E2E_ATTESTATION_DIR="${ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION_DIR:-$STATE_ROOT/production-e2e/attestations}" PRODUCTION_E2E_TRUSTED_KEYS="${ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS:-$STATE_ROOT/production-e2e/trusted-public-keys}" -# Readiness waits poll /healthz every 0.5s and return on the first healthy probe. +# Readiness waits bound /healthz probes and return on the first healthy probe. # 20s proved too short for a cold uvicorn boot on a loaded FORGE (staging abort, # false rollback-restore failure, needless production rollback). -READY_TIMEOUT_S="${CERTFORGE_READY_TIMEOUT_S:-90}" -READY_POLLS=$(( READY_TIMEOUT_S * 2 )) +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/readiness.sh" +configure_readiness_timeout "${CERTFORGE_READY_TIMEOUT_S:-90}" ADAPTER_DIR="${ECHO_CERTFORGE_PROD_ADAPTER_DIR:-$STATE_ROOT/p5}" ADAPTER_MODE="${CERTFORGE_ADAPTER_MODE:-required}" TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-965106b00917268d556b325719f26f5096e6c3746551658ffecb9fd4a95ec342}" @@ -264,7 +264,8 @@ ECHO_CERTFORGE_API_KEY_PEPPER="$STAGING_PEPPER" \ STAGING_PID=$! ready=0 -for _ in $(seq 1 "$READY_POLLS"); do +readiness_start +while readiness_pending; do kill -0 "$STAGING_PID" 2>/dev/null || { echo "!! staging process exited before readiness" mkdir -p "$STATE_ROOT/deploy-logs" && @@ -272,7 +273,7 @@ for _ in $(seq 1 "$READY_POLLS"); do tail -40 "$STAGING_ROOT/service.log" exit 1 } - curl -sf "http://127.0.0.1:$STAGING_PORT/healthz" >/dev/null 2>&1 && { + readiness_probe "$STAGING_PORT" >/dev/null 2>&1 && { if ss -H -ltnp "sport = :$STAGING_PORT" | grep -q "pid=$STAGING_PID,"; then ready=1 else @@ -281,7 +282,7 @@ for _ in $(seq 1 "$READY_POLLS"); do fi break } - sleep 0.5 + readiness_pause done if [ "$ready" != 1 ]; then echo "!! staging never became healthy" @@ -433,13 +434,14 @@ rollback_production() { if [ "$PREV_ACTIVE" = "active" ]; then sudo systemctl start "$SERVICE.service" || rollback_status=1 restored=0 - for _ in $(seq 1 "$READY_POLLS"); do + readiness_start + while readiness_pending; do service_owns_port "$PROD_PORT" && - curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && { + readiness_probe "$PROD_PORT" >/dev/null 2>&1 && { restored=1 break } - sleep 0.5 + readiness_pause done [ "$restored" = 1 ] || rollback_status=1 else @@ -638,13 +640,14 @@ sudo systemctl enable "$DISPATCH_SERVICE.service" echo "== [8/9] production health + live-smoke ==" ready=0 -for _ in $(seq 1 "$READY_POLLS"); do +readiness_start +while readiness_pending; do service_owns_port "$PROD_PORT" && - curl -sf "http://127.0.0.1:$PROD_PORT/healthz" >/dev/null 2>&1 && { + readiness_probe "$PROD_PORT" >/dev/null 2>&1 && { ready=1 break } - sleep 0.5 + readiness_pause done if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$DB_PATH" \ ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \ diff --git a/deploy/readiness.sh b/deploy/readiness.sh new file mode 100644 index 0000000..9fec7ef --- /dev/null +++ b/deploy/readiness.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Sourced by deploy_forge.sh; defining these helpers has no side effects. + +configure_readiness_timeout() { + local value="$1" + # Validate before arithmetic: Bash otherwise accepts expressions and overflows. + if [[ ! "$value" =~ ^[1-9][0-9]{0,3}$ ]] || (( value > 1800 )); then + echo "!! CERTFORGE_READY_TIMEOUT_S must be an integer between 1 and 1800" >&2 + return 1 + fi + READY_TIMEOUT_S="$value" +} + +readiness_start() { + READY_DEADLINE=$(( SECONDS + READY_TIMEOUT_S )) +} + +readiness_pending() { + (( SECONDS < READY_DEADLINE )) +} + +readiness_probe() { + local port="$1" + local remaining=$(( READY_DEADLINE - SECONDS )) + (( remaining > 0 )) || return 1 + # Each probe consumes the existing deadline, never a fresh total budget. + # Small individual probes allow the caller to recheck process ownership. + local probe_timeout="$remaining" + (( probe_timeout <= 2 )) || probe_timeout=2 + curl -sf "http://127.0.0.1:$port/healthz" \ + --connect-timeout "$probe_timeout" --max-time "$probe_timeout" +} + +readiness_pause() { + # SECONDS has one-second resolution; the polling pause adds at most 0.5s. + # Callers retain their process ownership checks outside these HTTP helpers. + if readiness_pending; then + sleep 0.5 + fi +} diff --git a/tests/test_deploy_gate.py b/tests/test_deploy_gate.py index 2b9009e..b9d7acd 100644 --- a/tests/test_deploy_gate.py +++ b/tests/test_deploy_gate.py @@ -145,7 +145,7 @@ def test_deploy_gate_rejects_dead_staging_process() -> None: ) process_check = DEPLOY_SCRIPT.index('kill -0 "$STAGING_PID"') health_check = DEPLOY_SCRIPT.index( - 'curl -sf "http://127.0.0.1:$STAGING_PORT/healthz"' + 'readiness_probe "$STAGING_PORT"' ) ownership_check = DEPLOY_SCRIPT.index( 'grep -q "pid=$STAGING_PID,"' diff --git a/tests/test_deploy_readiness.py b/tests/test_deploy_readiness.py new file mode 100644 index 0000000..e442dc9 --- /dev/null +++ b/tests/test_deploy_readiness.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import time +from pathlib import Path + +import pytest + + +HELPER = Path(__file__).resolve().parents[1] / "deploy" / "readiness.sh" + + +def test_deploy_validates_before_side_effects_and_bounds_all_health_waits() -> None: + script = HELPER.with_name("deploy_forge.sh").read_text(encoding="utf-8") + source = script.index('/readiness.sh"') + configure = script.index('configure_readiness_timeout "${CERTFORGE_READY_TIMEOUT_S:-90}"') + lock = script.index('exec 9>"$LOCK_FILE"') + fetch = script.index('git "${GITC[@]}" fetch') + assert source < configure < lock < fetch + waits = re.findall( + r"^[ \t]*readiness_start\n[ \t]*while readiness_pending; do\n(.*?)^[ \t]*done$", + script, + flags=re.MULTILINE | re.DOTALL, + ) + assert len(waits) == 3 + assert sum('readiness_probe "$STAGING_PORT"' in wait for wait in waits) == 1 + assert sum('readiness_probe "$PROD_PORT"' in wait for wait in waits) == 2 + assert all("readiness_pause" in wait for wait in waits) + assert "READY_POLLS" not in script + + +def _bash() -> str: + # Windows' System32/bash.exe may only be an unconfigured WSL launcher. + if os.name == "nt": + git_bash = Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Git/bin/bash.exe" + if git_bash.is_file(): + return str(git_bash) + executable = shutil.which("bash") + if executable is None: + pytest.skip("Bash is required for deploy readiness behavior tests") + return executable + + +def _run(body: str, *args: str, timeout: float = 5) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + _bash(), "--noprofile", "--norc", "-c", + 'set -euo pipefail\nsource "$1"\nshift\n' + body, + "readiness-test", HELPER.as_posix(), *args, + ], + text=True, + capture_output=True, + timeout=timeout, + check=False, + ) + + +@pytest.mark.parametrize("value", ["1", "90", "1800"]) +def test_readiness_accepts_supported_timeout(value: str) -> None: + result = _run('configure_readiness_timeout "$1"\nprintf "%s" "$READY_TIMEOUT_S"', value) + assert result.returncode == 0, result.stderr + assert result.stdout == value + + +@pytest.mark.parametrize( + "value", + ["", "0", "-1", "1801", "090", "08", "1.5", "1+2", " 90", "90 ", "x", "9" * 40], +) +def test_readiness_rejects_invalid_configuration_before_continuing(value: str) -> None: + result = _run( + 'configure_readiness_timeout "$1"\nprintf "UNEXPECTED SIDE EFFECT"', value + ) + assert result.returncode != 0 + assert result.stdout == "" + assert "must be an integer between 1 and 1800" in result.stderr + + +def test_readiness_returns_on_first_success_without_polling_delay() -> None: + result = _run( + """ +configure_readiness_timeout 90 +curl() { printf 'healthy'; } +sleep() { printf 'UNEXPECTED DELAY'; return 1; } +readiness_start +while readiness_pending; do + if readiness_probe 8311; then + exit 0 + fi + readiness_pause +done +exit 1 +""" + ) + assert result.returncode == 0, result.stderr + assert result.stdout == "healthy" + + +def test_readiness_allows_cold_start_beyond_old_twenty_second_budget() -> None: + result = _run( + """ +configure_readiness_timeout 90 +SECONDS=100 +attempts=0 +curl() { + attempts=$(( attempts + 1 )) + SECONDS=$(( SECONDS + 1 )) + (( attempts == 25 )) +} +sleep() { :; } +readiness_start +while readiness_pending; do + if readiness_probe 8311; then + printf '%s' "$attempts" + exit 0 + fi + readiness_pause +done +exit 1 +""" + ) + assert result.returncode == 0, result.stderr + assert result.stdout == "25" + + +def test_failed_probe_time_consumes_total_deadline() -> None: + result = _run( + """ +configure_readiness_timeout 3 +SECONDS=100 +attempts=0 +curl() { + attempts=$(( attempts + 1 )) + SECONDS=$(( SECONDS + 1 )) + return 22 +} +sleep() { :; } +readiness_start +while readiness_pending; do + if readiness_probe 8311; then + exit 1 + fi + readiness_pause +done +printf '%s' "$attempts" +""" + ) + assert result.returncode == 0, result.stderr + assert result.stdout == "3" + + +@pytest.mark.parametrize("remaining, expected", [(90, "2"), (2, "2"), (1, "1")]) +def test_probe_bounds_connection_and_response_to_remaining_budget( + remaining: int, expected: str +) -> None: + result = _run( + """ +configure_readiness_timeout "$1" +SECONDS=100 +curl() { printf '%s\n' "$@"; } +readiness_start +readiness_probe 8311 +""", + str(remaining), + ) + assert result.returncode == 0, result.stderr + assert result.stdout.splitlines() == [ + "-sf", "http://127.0.0.1:8311/healthz", + "--connect-timeout", expected, "--max-time", expected, + ] + + +def test_expired_budget_neither_probes_nor_sleeps() -> None: + result = _run( + """ +configure_readiness_timeout 1 +SECONDS=100 +curl() { printf 'UNEXPECTED PROBE'; } +sleep() { printf 'UNEXPECTED DELAY'; } +readiness_start +SECONDS=$(( SECONDS + 1 )) +if readiness_pending || readiness_probe 8311; then + exit 1 +fi +readiness_pause +""" + ) + assert result.returncode == 0, result.stderr + assert result.stdout == "" + + +def test_stalled_probe_uses_finite_response_timeout() -> None: + started = time.monotonic() + result = _run( + """ +configure_readiness_timeout 1 +curl() { + local max_time='' + while (( $# )); do + if [[ "$1" == --max-time ]]; then + max_time="$2" + shift + fi + shift + done + [[ "$max_time" == 1 ]] || return 99 + # Simulate curl accepting a connection but receiving no response. No network. + sleep "$max_time" + return 28 +} +readiness_start +if readiness_probe 8311; then + exit 1 +else + status=$? +fi +printf '%s' "$status" +""" + ) + elapsed = time.monotonic() - started + assert result.returncode == 0, result.stderr + assert result.stdout == "28" + assert 0.8 <= elapsed < 5 From 413e47e72a344d52a289585f44dd037beb429920 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sat, 3 Oct 2026 08:41:20 -0500 Subject: [PATCH 3/5] Require patched cryptography for package deployments --- pyproject.toml | 2 +- tests/test_release_pins.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3f1a10e..0d44c78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.12" license = {text = "Proprietary"} authors = [{name = "ECHO-OMEGA-PRIME", email = "bobbymcwilliams@echo-op.com"}] dependencies = [ - "cryptography>=44.0.0", + "cryptography>=50.0.0", "fastapi>=0.115.0", "pydantic>=2.10.0", "uvicorn>=0.34.0", diff --git a/tests/test_release_pins.py b/tests/test_release_pins.py index 152dc36..831ff73 100644 --- a/tests/test_release_pins.py +++ b/tests/test_release_pins.py @@ -7,8 +7,11 @@ from __future__ import annotations import re +import tomllib from pathlib import Path +from packaging.requirements import Requirement + ROOT = Path(__file__).resolve().parents[1] PATCHED_BASE = "sha256:4c47124a8391cb7a9f571164147d154777cf012a4ece5f86097130d7a4478111" VULNERABLE_BASE = "sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df" @@ -53,6 +56,17 @@ def test_dispatcher_wants_but_does_not_require_the_api_service() -> None: assert "After=network.target $SERVICE.service" in unit +def test_package_install_rejects_vulnerable_cryptography_versions() -> None: + # The deployment venv installs pyproject.toml, independently of the image lock. + dependencies = tomllib.loads(_text("pyproject.toml"))["project"]["dependencies"] + requirement = next( + item for item in map(Requirement, dependencies) if item.name == "cryptography" + ) + for vulnerable in ("44.0.0", "49.0.0", "49.0.1"): + assert not requirement.specifier.contains(vulnerable) + assert requirement.specifier.contains("50.0.0") + + def test_source_fetch_credential_helper_is_configurable_with_safe_default() -> None: script = _text("deploy/deploy_forge.sh") assert ('GIT_CREDENTIAL_HELPER="${CERTFORGE_GIT_CREDENTIAL_HELPER:-store --file=' From 81356ae2e31fe271a932ebfec44bde86f415a9ab Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sat, 3 Oct 2026 08:43:05 -0500 Subject: [PATCH 4/5] Keep certification journey independent of packaging test dependencies --- tests/test_package_dependency_policy.py | 17 +++++++++++++++++ tests/test_release_pins.py | 14 -------------- 2 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 tests/test_package_dependency_policy.py diff --git a/tests/test_package_dependency_policy.py b/tests/test_package_dependency_policy.py new file mode 100644 index 0000000..bfe83e1 --- /dev/null +++ b/tests/test_package_dependency_policy.py @@ -0,0 +1,17 @@ +"""Package-install policy, separate from the stdlib-only certification journey.""" +from pathlib import Path +import tomllib + +from packaging.requirements import Requirement + + +def test_package_install_rejects_vulnerable_cryptography_versions() -> None: + # The deployment venv installs pyproject.toml, independently of the image lock. + manifest = Path(__file__).resolve().parents[1] / "pyproject.toml" + dependencies = tomllib.loads(manifest.read_text(encoding="utf-8"))["project"]["dependencies"] + requirement = next( + item for item in map(Requirement, dependencies) if item.name == "cryptography" + ) + for vulnerable in ("44.0.0", "49.0.0", "49.0.1"): + assert not requirement.specifier.contains(vulnerable) + assert requirement.specifier.contains("50.0.0") diff --git a/tests/test_release_pins.py b/tests/test_release_pins.py index 831ff73..152dc36 100644 --- a/tests/test_release_pins.py +++ b/tests/test_release_pins.py @@ -7,11 +7,8 @@ from __future__ import annotations import re -import tomllib from pathlib import Path -from packaging.requirements import Requirement - ROOT = Path(__file__).resolve().parents[1] PATCHED_BASE = "sha256:4c47124a8391cb7a9f571164147d154777cf012a4ece5f86097130d7a4478111" VULNERABLE_BASE = "sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df" @@ -56,17 +53,6 @@ def test_dispatcher_wants_but_does_not_require_the_api_service() -> None: assert "After=network.target $SERVICE.service" in unit -def test_package_install_rejects_vulnerable_cryptography_versions() -> None: - # The deployment venv installs pyproject.toml, independently of the image lock. - dependencies = tomllib.loads(_text("pyproject.toml"))["project"]["dependencies"] - requirement = next( - item for item in map(Requirement, dependencies) if item.name == "cryptography" - ) - for vulnerable in ("44.0.0", "49.0.0", "49.0.1"): - assert not requirement.specifier.contains(vulnerable) - assert requirement.specifier.contains("50.0.0") - - def test_source_fetch_credential_helper_is_configurable_with_safe_default() -> None: script = _text("deploy/deploy_forge.sh") assert ('GIT_CREDENTIAL_HELPER="${CERTFORGE_GIT_CREDENTIAL_HELPER:-store --file=' From 424fc76fa09d9a83fcbe0d2ec12dc62ef97859c9 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sat, 3 Oct 2026 08:51:39 -0500 Subject: [PATCH 5/5] Publish authentic candidate CI and CodeQL using supported Actions --- .github/workflows/ci.yml | 19 +++++++++++----- .github/workflows/codeql.yml | 42 ++++++++++++++++++++++++++++++++++++ deploy/README.md | 8 +++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01578a4..bc4b3d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,13 +13,22 @@ permissions: jobs: test: - runs-on: ubuntu-latest + # Release Sentinel requires the authentic Actions check named CI. + name: CI + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: Check out exact source - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Verify candidate identity + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" - name: Configure Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.13" cache: pip @@ -37,9 +46,9 @@ jobs: run: python scripts/p7_acceptance.py - name: Upload verification evidence if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: certification-forge-evidence-${{ github.sha }} + name: certification-forge-evidence-${{ github.event.pull_request.head.sha || github.sha }} path: artifacts/ if-no-files-found: error retention-days: 30 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..446e95b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: certification-forge-codeql + +"on": + push: + branches: + - main + - feat/p5-adapter-execution + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + analyze: + name: CodeQL + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + security-events: write + steps: + - name: Check out exact source + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Verify candidate identity + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - name: Initialize Python analysis + uses: github/codeql-action/init@v4 + with: + languages: python + build-mode: none + - name: Analyze and upload results + uses: github/codeql-action/analyze@v4 + with: + category: /language:python + ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/deploy/README.md b/deploy/README.md index ca18a25..3c1d713 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -16,6 +16,14 @@ Staging failures retain the service log under `$STATE_ROOT/deploy-logs/staging-.log`. Listener ownership, signed readiness verification, live-smoke gates, and rollback requirements still apply. +# Source validation and release evidence + +The `CI` Actions job runs the full deterministic suite with coverage and P1, P6, +and P7 verification. The `CodeQL` job performs Python security analysis and uploads +its results. Both check out the immutable PR head (or event SHA on push), verify +that checkout, and bind their output to it. Their names match Release Sentinel's +required authentic checks; neither is a substitute status or a release verdict. + A green source-validation run does not prove current production identity or provide signed production E2E evidence. Every changed candidate needs fresh exact-SHA certification. Historical smoke reports and old certified tags must not