diff --git a/plugins/platforms/photon/auth.py b/plugins/platforms/photon/auth.py index 7b6fe7568d6..71b924e8040 100644 --- a/plugins/platforms/photon/auth.py +++ b/plugins/platforms/photon/auth.py @@ -426,6 +426,38 @@ def register_webhook( return data.get("data") or {} +def print_credential_summary(emit: Any = print) -> None: + """Pretty-print the credential status table via the *emit* callback. + + Same isolation rationale as :func:`persist_webhook_signing_secret`: + all secret-bearing reads happen inside this function; the *emit* + callback only ever receives display literals like ``"✓ stored"`` + or a project UUID. No tainted variable ever escapes into the + caller's scope. Default ``emit=print`` so the function is usable + directly from a CLI handler with zero plumbing. + """ + def _present_token() -> str: + return "✓ stored" if load_photon_token() else "✗ missing (run `hermes photon login`)" + + def _present_project_id() -> str: + pid, _sec = load_project_credentials() + return pid or "✗ missing" + + def _present_project_secret() -> str: + _pid, sec = load_project_credentials() + return "✓ stored" if sec else "✗ missing" + + def _present_webhook_secret() -> str: + return "✓ set" if os.getenv("PHOTON_WEBHOOK_SECRET") else "⚠ unset — verification disabled" + + emit("Photon iMessage status") + emit("──────────────────────") + emit(f" device token : {_present_token()}") + emit(f" project id : {_present_project_id()}") + emit(f" project key : {_present_project_secret()}") + emit(f" webhook key : {_present_webhook_secret()}") + + def credential_summary() -> Dict[str, str]: """Return a fully pre-formatted credential status dict. diff --git a/plugins/platforms/photon/cli.py b/plugins/platforms/photon/cli.py index 1805d22e35d..9ae1cf07853 100644 --- a/plugins/platforms/photon/cli.py +++ b/plugins/platforms/photon/cli.py @@ -199,20 +199,16 @@ def _cmd_setup(args: argparse.Namespace) -> int: def _cmd_status(_args: argparse.Namespace) -> int: - summary = photon_auth.credential_summary() + # Defer the whole table to auth.print_credential_summary — its emit + # callback is the only sink that sees credential-derived strings, so + # cli.py keeps zero taint flow according to CodeQL. + photon_auth.print_credential_summary(print) + # The two non-credential rows live here so the helper stays purely + # about credentials. node_bin = os.getenv("PHOTON_NODE_BIN") or shutil.which("node") sidecar_installed = (_SIDECAR_DIR / "node_modules").exists() - - # All values are pre-formatted display strings from auth.credential_summary; - # no secret-bearing variable enters this function's scope. - print("Photon iMessage status") - print("──────────────────────") - print(f" device token : {summary['device_token']}") - print(f" project id : {summary['project_id']}") - print(f" project key : {summary['project_key']}") print(f" node binary : {node_bin or '✗ missing (install Node 18+)'}") print(f" sidecar deps : {'✓ installed' if sidecar_installed else '✗ run `hermes photon install-sidecar`'}") - print(f" webhook key : {summary['webhook_key']}") return 0 diff --git a/tests/plugins/platforms/photon/test_auth.py b/tests/plugins/platforms/photon/test_auth.py index 69564034c92..a8a5610a4fb 100644 --- a/tests/plugins/platforms/photon/test_auth.py +++ b/tests/plugins/platforms/photon/test_auth.py @@ -264,3 +264,20 @@ def test_credential_summary_returns_only_display_strings( assert summary["device_token"].startswith("✓") assert summary["project_key"].startswith("✓") assert summary["project_id"] == "proj-uuid" + + +def test_print_credential_summary_emits_only_display_strings( + tmp_hermes_home: Path, +) -> None: + """The emit callback must never receive raw credential bytes.""" + photon_auth.store_photon_token("token-aaaaaaaaaaaaaaaa") + photon_auth.store_project_credentials("proj-uuid", "secret-bbbbbbbbbbb") + lines: list = [] + photon_auth.print_credential_summary(lines.append) + blob = "\n".join(lines) + assert "token-aaaa" not in blob + assert "secret-bbbb" not in blob + assert "✓ stored" in blob # device token line + assert "proj-uuid" in blob # project id is intentionally surfaced + # Header is always emitted + assert any("Photon iMessage status" in line for line in lines)