mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
Chain signed installer downloads, verification and execution in the English, German and Spanish landing pages so a failed trust step cannot run stale or unverified bytes. Keep the pinned signer, namespace and release URLs, preserve real installer exits, and correct the existing plan and paired-app retirement guidance. Exercise the copyable Bash gate with offline command stubs and run those documentation regressions in Public docs CI. Contract-Neutral: Public documentation and its secret-free CI validation only; no runtime, entitlement, release-selection or subsystem contract delta. Change-source: pulse-maintainer
188 lines
7.6 KiB
Python
188 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the public landing pages' signed-install gate without network access."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def read_repo_text(relative_path: str) -> str:
|
|
return (ROOT / relative_path).read_text(encoding="utf-8")
|
|
|
|
|
|
ENTRYPOINTS = (
|
|
"README.md",
|
|
"docs/i18n/de/README.md",
|
|
"docs/i18n/es/README.md",
|
|
)
|
|
SIGNER = (
|
|
'pulse-installer namespaces="pulse-install" ssh-ed25519 '
|
|
"AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
|
|
)
|
|
|
|
|
|
def installer_commands(text: str) -> str:
|
|
for block in re.findall(r"```bash\n(.*?)\n```", text, flags=re.DOTALL):
|
|
if "ssh-keygen -Y verify" not in block:
|
|
continue
|
|
lines = block.splitlines()
|
|
for index, line in enumerate(lines):
|
|
if line.startswith("bash install.sh --version "):
|
|
# Only exercise the download -> verification -> execution gate,
|
|
# never any subsequent file-cleanup recipe.
|
|
return "\n".join(lines[: index + 1]) + "\n"
|
|
raise AssertionError("signed server-installer command block is missing")
|
|
|
|
|
|
STUB = """#!/usr/bin/python3
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
name = Path(sys.argv[0]).name
|
|
trace = Path(os.environ['TRACE'])
|
|
events = [json.loads(line) for line in trace.read_text().splitlines()] if trace.exists() else []
|
|
event = {'command': name, 'argv': sys.argv[1:]}
|
|
if name == 'ssh-keygen':
|
|
event['allowed_signers'] = Path(sys.argv[sys.argv.index('-f') + 1]).read_text()
|
|
with trace.open('a') as out:
|
|
out.write(json.dumps(event) + '\\n')
|
|
if name == 'curl':
|
|
attempt = 1 + sum(event['command'] == 'curl' for event in events)
|
|
code = int(os.environ.get('CURL_' + str(attempt), '0'))
|
|
if code == 0:
|
|
# Public dummy bytes only. The installer itself is never executed.
|
|
Path(sys.argv[-1].rsplit('/', 1)[-1]).write_text('downloaded fixture\\n')
|
|
elif name == 'ssh-keygen':
|
|
sys.stdin.read()
|
|
code = int(os.environ.get('VERIFY', '0'))
|
|
elif name == 'bash':
|
|
code = int(os.environ.get('INSTALL', '0'))
|
|
else:
|
|
raise AssertionError('unexpected stub command')
|
|
sys.exit(code)
|
|
"""
|
|
|
|
|
|
def exercise_install(commands: str, exits: dict[str, str]) -> tuple[int, list[dict]]:
|
|
with tempfile.TemporaryDirectory(prefix="pulse-readme-gate-") as directory:
|
|
root = Path(directory)
|
|
stubs = root / "bin"
|
|
stubs.mkdir()
|
|
for name in ("curl", "ssh-keygen", "bash"):
|
|
stub = stubs / name
|
|
stub.write_text(STUB, encoding="utf-8")
|
|
stub.chmod(0o700)
|
|
# A failed download must not execute files left by an earlier attempt.
|
|
(root / "install.sh").write_text("stale installer fixture\n", encoding="utf-8")
|
|
(root / "install.sh.sshsig").write_text("stale signature fixture\n", encoding="utf-8")
|
|
trace = root / "trace.jsonl"
|
|
completed = subprocess.run(
|
|
["/bin/bash", "-c", commands],
|
|
cwd=root,
|
|
env={"PATH": f"{stubs}:/usr/bin:/bin", "TRACE": str(trace), **exits},
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
check=False,
|
|
)
|
|
if completed.stderr:
|
|
raise AssertionError(completed.stderr)
|
|
events = [json.loads(line) for line in trace.read_text().splitlines()]
|
|
return completed.returncode, events
|
|
|
|
|
|
class ReadmeEntrypointsTest(unittest.TestCase):
|
|
def test_signed_install_stops_at_each_failed_trust_step(self) -> None:
|
|
cases = (
|
|
({"CURL_1": "22"}, 22, ["curl"]),
|
|
({"CURL_2": "23"}, 23, ["curl", "curl"]),
|
|
({"VERIFY": "1"}, 1, ["curl", "curl", "ssh-keygen"]),
|
|
)
|
|
for relative_path in ENTRYPOINTS:
|
|
commands = installer_commands(read_repo_text(relative_path))
|
|
for exits, expected_exit, expected_steps in cases:
|
|
with self.subTest(page=relative_path, exits=exits):
|
|
code, events = exercise_install(commands, exits)
|
|
self.assertEqual([event["command"] for event in events], expected_steps)
|
|
self.assertEqual(code, expected_exit)
|
|
|
|
def test_verified_install_keeps_pinned_version_and_signer(self) -> None:
|
|
for relative_path in ENTRYPOINTS:
|
|
with self.subTest(page=relative_path):
|
|
commands = installer_commands(read_repo_text(relative_path))
|
|
self.assertIn(SIGNER, commands)
|
|
code, events = exercise_install(commands, {})
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(
|
|
[event["command"] for event in events],
|
|
["curl", "curl", "ssh-keygen", "bash"],
|
|
)
|
|
self.assertEqual(
|
|
[event["argv"][-1] for event in events[:2]],
|
|
[
|
|
"https://github.com/rcourtman/Pulse/releases/download/vX.Y.Z/install.sh",
|
|
"https://github.com/rcourtman/Pulse/releases/download/vX.Y.Z/install.sh.sshsig",
|
|
],
|
|
)
|
|
self.assertTrue(all(event["argv"][0] == "-fsSLO" for event in events[:2]))
|
|
self.assertEqual(events[2]["allowed_signers"], SIGNER + "\n")
|
|
verify = events[2]["argv"]
|
|
for option, value in (
|
|
("-I", "pulse-installer"),
|
|
("-n", "pulse-install"),
|
|
("-s", "install.sh.sshsig"),
|
|
):
|
|
self.assertEqual(verify[verify.index(option) + 1], value)
|
|
self.assertEqual(events[3]["argv"], ["install.sh", "--version", "vX.Y.Z"])
|
|
|
|
def test_installer_failure_remains_nonzero(self) -> None:
|
|
for relative_path in ENTRYPOINTS:
|
|
with self.subTest(page=relative_path):
|
|
code, events = exercise_install(
|
|
installer_commands(read_repo_text(relative_path)), {"INSTALL": "17"}
|
|
)
|
|
self.assertEqual(code, 17)
|
|
self.assertEqual(events[-1]["command"], "bash")
|
|
|
|
def test_entrypoint_plans_do_not_offer_retired_relay(self) -> None:
|
|
expected = {
|
|
"README.md": (
|
|
"31 March 2027", "Relay is no longer sold", "current price",
|
|
"for as long as their subscription continues", "own VPN or tunnel",
|
|
"ntfy, Gotify or Pushover", "not the web UI",
|
|
),
|
|
"docs/i18n/de/README.md": (
|
|
"31. März 2027", "Relay wird nicht mehr verkauft", "bisherigen Preis",
|
|
"solange ihr Abonnement weiterläuft", "eigenes VPN oder einen Tunnel",
|
|
"ntfy, Gotify oder Pushover", "nicht mit der Weboberfläche",
|
|
),
|
|
"docs/i18n/es/README.md": (
|
|
"31 de marzo de 2027", "Relay ya no se vende", "precio actual",
|
|
"mientras continúe su suscripción", "tu propia VPN o un túnel",
|
|
"ntfy, Gotify o Pushover", "no la interfaz web",
|
|
),
|
|
}
|
|
stale_claims = (
|
|
"**Relay** — Community plus", "**Pro** — Relay plus",
|
|
"Relay ergänzt sicheren Remote-Zugriff", "Relay añade acceso remoto seguro",
|
|
)
|
|
for relative_path, fragments in expected.items():
|
|
with self.subTest(page=relative_path):
|
|
prose = " ".join(read_repo_text(relative_path).split())
|
|
for fragment in fragments:
|
|
self.assertIn(fragment, prose)
|
|
for claim in stale_claims:
|
|
self.assertNotIn(claim, prose)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|