From 80a11b82933321c5a14a5e868dcac7d824b2646e Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:21:27 +0800 Subject: [PATCH] fix(gateway): classify passive SSH failures without sensitive output Co-Authored-By: Codex --- .github/workflows/ci.yml | 1 + .../read-only-vm-metadata-diagnostic.yml | 71 +++--- scripts/classify_gateway_ssh_failure.py | 126 +++++++++++ scripts/prepare_gateway_ssh_key.sh | 42 ++++ tests/test_gateway_ssh_failure_diagnostics.py | 211 ++++++++++++++++++ tests/test_inspect_gateway_connections.sh | 4 +- 6 files changed, 410 insertions(+), 45 deletions(-) create mode 100644 scripts/classify_gateway_ssh_failure.py create mode 100644 scripts/prepare_gateway_ssh_key.sh create mode 100644 tests/test_gateway_ssh_failure_diagnostics.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b2ddba..1f16e55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: node tests/test_parse_protected_gateway_index.cjs node tests/test_gateway_workflow_resolve.cjs python3 -m unittest discover -s tests -p 'test_match_gateway_metadata.py' + python3 -m unittest discover -s tests -p 'test_gateway_ssh_failure_diagnostics.py' docker-build: runs-on: ubuntu-latest diff --git a/.github/workflows/read-only-vm-metadata-diagnostic.yml b/.github/workflows/read-only-vm-metadata-diagnostic.yml index c59f0fa..44092f3 100644 --- a/.github/workflows/read-only-vm-metadata-diagnostic.yml +++ b/.github/workflows/read-only-vm-metadata-diagnostic.yml @@ -364,16 +364,18 @@ jobs: GCP_PROJECT_ID: ${{ steps.metadata.outputs.gcp_secret_project_id }} run: | set -euo pipefail - key_file="$(mktemp "${RUNNER_TEMP}/gateway-ssh-key.XXXXXX")" - error_file="$(mktemp "${RUNNER_TEMP}/gateway-secret-error.XXXXXX")" + key_file="$(mktemp "${RUNNER_TEMP}/gateway-ssh-key.XXXXXX" 2>/dev/null)" + error_file="$(mktemp "${RUNNER_TEMP}/gateway-secret-error.XXXXXX" 2>/dev/null)" key_handed_off=false cleanup_secret_fetch() { - rm -f "${error_file}" - if [ "${key_handed_off}" != "true" ]; then rm -f "${key_file}"; fi + rm -f "${error_file}" 2>/dev/null || true + if [ "${key_handed_off}" != "true" ]; then rm -f "${key_file}" 2>/dev/null || true; fi } trap cleanup_secret_fetch EXIT - echo "SSH_KEY_FILE=${key_file}" >> "${GITHUB_ENV}" - chmod 600 "${key_file}" + if ! chmod 600 "${key_file}" 2>/dev/null; then + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_unavailable" + exit 1 + fi if ! gcloud secrets versions access latest \ --secret="${SSH_PRIVATE_KEY_SECRET_NAME}" \ --project="${GCP_PROJECT_ID}" >"${key_file}" 2>"${error_file}"; then @@ -384,7 +386,11 @@ jobs: echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_unavailable" exit 1 fi - chmod 600 "${key_file}" + if ! SSH_KEY_FILE="${key_file}" bash scripts/prepare_gateway_ssh_key.sh >/dev/null 2>"${error_file}"; then + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 + fi + echo "SSH_KEY_FILE=${key_file}" >> "${GITHUB_ENV}" key_handed_off=true - name: Inspect Gateway connections through one IAP SSH session @@ -394,11 +400,14 @@ jobs: TARGET_INDEX: ${{ matrix.target_index }} run: | set -euo pipefail - known_hosts="$(mktemp "${RUNNER_TEMP}/gateway-known-hosts.XXXXXX")" - output_file="$(mktemp "${RUNNER_TEMP}/gateway-inspection-output.XXXXXX")" - error_file="$(mktemp "${RUNNER_TEMP}/gateway-ssh-error.XXXXXX")" - trap 'rm -f "${known_hosts}" "${output_file}" "${error_file}" "${SSH_KEY_FILE}"' EXIT - chmod 600 "${known_hosts}" "${output_file}" "${error_file}" + known_hosts="$(mktemp "${RUNNER_TEMP}/gateway-known-hosts.XXXXXX" 2>/dev/null)" + output_file="$(mktemp "${RUNNER_TEMP}/gateway-inspection-output.XXXXXX" 2>/dev/null)" + error_file="$(mktemp "${RUNNER_TEMP}/gateway-ssh-error.XXXXXX" 2>/dev/null)" + trap 'rm -f "${known_hosts}" "${output_file}" "${error_file}" "${SSH_KEY_FILE}" 2>/dev/null || true' EXIT + if ! chmod 600 "${known_hosts}" "${output_file}" "${error_file}" 2>/dev/null; then + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=inspection_unavailable" + exit 1 + fi target_alias="gateway-target-${TARGET_INDEX}" # The temporary SSH host-key record is bound to this exact IAP target alias. proxy_command="gcloud compute start-iap-tunnel '${GCE_INSTANCE_NAME}' 22 --listen-on-stdin --project='${GCP_PROJECT_ID}' --zone='${GCE_ZONE}' --quiet" @@ -419,36 +428,10 @@ jobs: else ssh_status=$? fi - python3 - "${output_file}" "${ssh_status}" <<'PY' - import re - import sys - from pathlib import Path - - lines = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines() - ssh_status = int(sys.argv[2]) - if len(lines) == 1 and re.fullmatch(r"GATEWAY_CONNECTION_INSPECTION=blocked reason=[a-z_]+", lines[0]): - print(lines[0]) - raise SystemExit(1) - patterns = { - "GATEWAY_CONNECTION_INSPECTION": r"GATEWAY_CONNECTION_INSPECTION=observed", - "GATEWAY_CONTAINER_RUNNING": r"GATEWAY_CONTAINER_RUNNING=(true|false)", - "GATEWAY_API_LISTENER": r"GATEWAY_API_LISTENER=(true|false)", - "GATEWAY_ESTABLISHED_CONNECTION_COUNT": r"GATEWAY_ESTABLISHED_CONNECTION_COUNT=[0-9]+", - "GATEWAY_CONNECTION_OBSERVED_AT_UTC": r"GATEWAY_CONNECTION_OBSERVED_AT_UTC=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", - "GATEWAY_CONNECTION_INSPECTION_LIMIT": r"GATEWAY_CONNECTION_INSPECTION_LIMIT=OBSERVATION_ONLY_NO_AUTH_OR_CONCURRENCY_ASSERTION", - } - names = [line.split("=", 1)[0] for line in lines] - if ssh_status != 0: - print("GATEWAY_CONNECTION_INSPECTION=blocked reason=ssh_unavailable") - raise SystemExit(1) - if len(lines) != len(patterns) or set(names) != set(patterns) or len(set(names)) != len(names) or any( - not re.fullmatch(patterns.get(name, ""), line) for name, line in zip(names, lines) - ): - print("GATEWAY_CONNECTION_INSPECTION=blocked reason=remote_response_invalid") - raise SystemExit(1) - for line in lines: - print(line) - PY + python3 scripts/classify_gateway_ssh_failure.py \ + --stdout-file "${output_file}" \ + --stderr-file "${error_file}" \ + --exit-code "${ssh_status}" - name: Remove temporary SSH key if: ${{ always() && inputs.inspect_connections }} @@ -456,8 +439,8 @@ jobs: SSH_KEY_FILE: ${{ env.SSH_KEY_FILE }} run: | set -euo pipefail - if [ -n "${SSH_KEY_FILE:-}" ]; then rm -f -- "${SSH_KEY_FILE}"; fi + if [ -n "${SSH_KEY_FILE:-}" ]; then rm -f -- "${SSH_KEY_FILE}" 2>/dev/null || true; fi for key_file in "${RUNNER_TEMP}"/gateway-ssh-key.*; do [ -e "${key_file}" ] || continue - rm -f -- "${key_file}" + rm -f -- "${key_file}" 2>/dev/null || true done diff --git a/scripts/classify_gateway_ssh_failure.py b/scripts/classify_gateway_ssh_failure.py new file mode 100644 index 0000000..c798819 --- /dev/null +++ b/scripts/classify_gateway_ssh_failure.py @@ -0,0 +1,126 @@ +"""Classify captured SSH/IAP errors into fixed, non-sensitive categories.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +_PATTERNS = { + "timeout": re.compile(r"(?:connection|operation|command|connect) timed out|\btimeout\b", re.I), + "key_invalid": re.compile( + r"invalid format|error in libcrypto|incorrect passphrase|bad passphrase|load key .*: invalid", + re.I, + ), + "auth_denied": re.compile( + r"permission denied \(publickey[^)]*\)|no supported authentication methods|authentication failed|too many authentication failures", + re.I, + ), + "iap_denied": re.compile( + r"(?:start-iap-tunnel|iap tunnel|iap-tunnel).{0,160}(?:4033|not authorized|permission denied|forbidden)|" + r"(?:4033|not authorized|permission denied|forbidden).{0,160}(?:start-iap-tunnel|iap tunnel|iap-tunnel)", + re.I | re.S, + ), + "iap_transport": re.compile( + r"(?:start-iap-tunnel|iap tunnel|iap-tunnel).{0,160}(?:failed to connect|connection refused|backend|unreachable|closed)|" + r"(?:failed to connect|connection refused|backend|unreachable|closed).{0,160}(?:start-iap-tunnel|iap tunnel|iap-tunnel)", + re.I | re.S, + ), +} + + +def classify_gateway_ssh_failure(message: str) -> str: + """Return a category only when the captured text identifies exactly one.""" + + matches = [name for name, pattern in _PATTERNS.items() if pattern.search(message)] + return matches[0] if len(matches) == 1 else "unknown" + + +_RESPONSE_PATTERNS = { + "GATEWAY_CONNECTION_INSPECTION": r"GATEWAY_CONNECTION_INSPECTION=observed", + "GATEWAY_CONTAINER_RUNNING": r"GATEWAY_CONTAINER_RUNNING=(true|false)", + "GATEWAY_API_LISTENER": r"GATEWAY_API_LISTENER=(true|false)", + "GATEWAY_ESTABLISHED_CONNECTION_COUNT": r"GATEWAY_ESTABLISHED_CONNECTION_COUNT=[0-9]+", + "GATEWAY_CONNECTION_OBSERVED_AT_UTC": r"GATEWAY_CONNECTION_OBSERVED_AT_UTC=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", + "GATEWAY_CONNECTION_INSPECTION_LIMIT": ( + "GATEWAY_CONNECTION_INSPECTION_LIMIT=" + "OBSERVATION_ONLY_NO_AUTH_OR_CONCURRENCY_ASSERTION" + ), +} +_HELPER_BLOCKED_REASONS = { + "configuration_invalid", + "container_inspection_failed", + "container_state_invalid", + "container_pid_invalid", + "socket_inspection_failed", + "socket_result_invalid", +} + + +def validate_gateway_ssh_response(lines: list[str]) -> str | None: + """Return safe helper output or a fixed local response error.""" + + if len(lines) == 1 and any( + lines[0] == f"GATEWAY_CONNECTION_INSPECTION=blocked reason={reason}" + for reason in _HELPER_BLOCKED_REASONS + ): + return lines[0] + names = [line.split("=", 1)[0] for line in lines] + if ( + len(lines) != len(_RESPONSE_PATTERNS) + or set(names) != set(_RESPONSE_PATTERNS) + or len(set(names)) != len(names) + or any( + not re.fullmatch(_RESPONSE_PATTERNS.get(name, ""), line) + for name, line in zip(names, lines) + ) + ): + return None + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--stdout-file", required=True, type=Path) + parser.add_argument("--stderr-file", required=True, type=Path) + parser.add_argument("--exit-code", required=True, type=int) + args = parser.parse_args(argv) + if not 0 <= args.exit_code <= 255: + print("GATEWAY_CONNECTION_INSPECTION=blocked reason=ssh_unknown exit_code=1") + return 1 + if args.exit_code == 0: + try: + lines = args.stdout_file.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + lines = [] + response = validate_gateway_ssh_response(lines) + if response is None: + print("GATEWAY_CONNECTION_INSPECTION=blocked reason=remote_response_invalid") + return 1 + print(response) + return 1 if response.startswith("GATEWAY_CONNECTION_INSPECTION=blocked reason=") else 0 + try: + lines = args.stdout_file.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + lines = [] + helper_response = validate_gateway_ssh_response(lines) + if helper_response is not None and helper_response.startswith( + "GATEWAY_CONNECTION_INSPECTION=blocked reason=" + ): + print(helper_response) + return 1 + try: + message = args.stderr_file.read_text(encoding="utf-8", errors="replace") + except OSError: + message = "" + category = "timeout" if args.exit_code == 124 else classify_gateway_ssh_failure(message) + print( + "GATEWAY_CONNECTION_INSPECTION=blocked " + f"reason=ssh_{category} exit_code={args.exit_code}" + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prepare_gateway_ssh_key.sh b/scripts/prepare_gateway_ssh_key.sh new file mode 100644 index 0000000..d433b35 --- /dev/null +++ b/scripts/prepare_gateway_ssh_key.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +key_file="${SSH_KEY_FILE:-}" +if [[ -z "${key_file}" || ! -f "${key_file}" ]]; then + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +fi + +normalized_file="" +cleanup() { + if [[ -n "${normalized_file}" ]]; then rm -f -- "${normalized_file}" 2>/dev/null || true; fi +} +trap cleanup EXIT + +normalized_file="$(mktemp "${RUNNER_TEMP:-/tmp}/gateway-ssh-key-normalized.XXXXXX" 2>/dev/null)" || { + rm -f -- "${key_file}" 2>/dev/null || true + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +} +chmod 600 "${normalized_file}" 2>/dev/null || { + rm -f -- "${key_file}" 2>/dev/null || true + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +} +if ! tr -d '\r' <"${key_file}" >"${normalized_file}" 2>/dev/null || [[ ! -s "${normalized_file}" ]]; then + rm -f -- "${key_file}" 2>/dev/null || true + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +fi +if ! ssh-keygen -y -P '' -f "${normalized_file}" >/dev/null 2>/dev/null; then + rm -f -- "${key_file}" 2>/dev/null || true + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +fi +if ! cat "${normalized_file}" >"${key_file}" 2>/dev/null || ! chmod 600 "${key_file}" 2>/dev/null; then + rm -f -- "${key_file}" + echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid" + exit 1 +fi + +echo "GATEWAY_SSH_KEY_STATUS=ready" diff --git a/tests/test_gateway_ssh_failure_diagnostics.py b/tests/test_gateway_ssh_failure_diagnostics.py new file mode 100644 index 0000000..5874cfe --- /dev/null +++ b/tests/test_gateway_ssh_failure_diagnostics.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PREPARE_KEY = ROOT / "scripts" / "prepare_gateway_ssh_key.sh" +CLASSIFIER = ROOT / "scripts" / "classify_gateway_ssh_failure.py" +WORKFLOW = ROOT / ".github" / "workflows" / "read-only-vm-metadata-diagnostic.yml" + + +def _make_key(path: Path, passphrase: str = "") -> None: + subprocess.run( + ["ssh-keygen", "-q", "-t", "ed25519", "-N", passphrase, "-f", str(path)], + check=True, + capture_output=True, + text=True, + ) + path.with_suffix(path.suffix + ".pub").unlink() + + +def _prepare_key(path: Path, runner_temp: Path) -> subprocess.CompletedProcess[str]: + env = {**os.environ, "SSH_KEY_FILE": str(path), "RUNNER_TEMP": str(runner_temp)} + return subprocess.run( + ["bash", str(PREPARE_KEY)], + check=False, + capture_output=True, + text=True, + env=env, + ) + + +def _classify( + tmp_path: Path, stderr: str, exit_code: int, stdout: str = "" +) -> subprocess.CompletedProcess[str]: + stdout_file = tmp_path / "ssh.stdout" + stderr_file = tmp_path / "ssh.stderr" + stdout_file.write_text(stdout, encoding="utf-8") + stderr_file.write_text(stderr, encoding="utf-8") + return subprocess.run( + [ + "python3", + str(CLASSIFIER), + "--stdout-file", + str(stdout_file), + "--stderr-file", + str(stderr_file), + "--exit-code", + str(exit_code), + ], + check=False, + capture_output=True, + text=True, + ) + + +class GatewaySshFailureDiagnosticsTests(unittest.TestCase): + def setUp(self) -> None: + self.temp_dir = tempfile.TemporaryDirectory() + self.tmp_path = Path(self.temp_dir.name) + + def tearDown(self) -> None: + self.temp_dir.cleanup() + + def test_crlf_key_is_normalized_and_preflighted_without_outputting_key(self) -> None: + key_file = self.tmp_path / "private-key" + _make_key(key_file) + original = key_file.read_bytes() + key_file.write_bytes(original.replace(b"\n", b"\r\n")) + + result = _prepare_key(key_file, self.tmp_path) + + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "GATEWAY_SSH_KEY_STATUS=ready\n") + self.assertEqual(result.stderr, "") + self.assertNotIn(b"\r", key_file.read_bytes()) + self.assertIn(b"PRIVATE KEY", key_file.read_bytes()) + check = subprocess.run( + ["ssh-keygen", "-y", "-P", "", "-f", str(key_file)], + check=True, + capture_output=True, + text=True, + ) + self.assertTrue(check.stdout.startswith("ssh-ed25519 ")) + self.assertNotIn("PRIVATE KEY", result.stdout + result.stderr) + + def test_invalid_and_encrypted_keys_fail_closed_and_are_removed(self) -> None: + runner_temp = self.tmp_path / "runner-temp" + runner_temp.mkdir() + invalid_key = self.tmp_path / "invalid-key" + invalid_marker = "synthetic-private-material-marker" + invalid_key.write_text( + f"-----BEGIN PRIVATE KEY-----\n{invalid_marker}\n", encoding="utf-8" + ) + + invalid_result = _prepare_key(invalid_key, runner_temp) + + self.assertNotEqual(invalid_result.returncode, 0) + self.assertEqual( + invalid_result.stdout, + "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid\n", + ) + self.assertFalse(invalid_key.exists()) + self.assertNotIn(invalid_marker, invalid_result.stdout + invalid_result.stderr) + + encrypted_key = self.tmp_path / "encrypted-key" + passphrase_marker = "synthetic-passphrase-marker" + _make_key(encrypted_key, passphrase_marker) + encrypted_result = _prepare_key(encrypted_key, runner_temp) + + self.assertNotEqual(encrypted_result.returncode, 0) + self.assertEqual( + encrypted_result.stdout, + "GATEWAY_CONNECTION_INSPECTION=blocked reason=credential_invalid\n", + ) + self.assertFalse(encrypted_key.exists()) + self.assertNotIn(passphrase_marker, encrypted_result.stdout + encrypted_result.stderr) + self.assertEqual(list(runner_temp.iterdir()), []) + + def test_ssh_stderr_maps_to_closed_categories_and_hides_raw_text(self) -> None: + examples = { + "timeout": "ssh: connect to host synthetic.invalid port 22: Connection timed out", + "key_invalid": 'Load key "synthetic-path": invalid format; hidden-key-marker', + "auth_denied": "Permission denied (publickey); hidden-auth-marker", + "iap_denied": "ERROR: gcloud.compute.start-iap-tunnel Error while connecting [4033: not authorized]", + "iap_transport": "start-iap-tunnel failed to connect to backend; hidden-endpoint-marker", + } + for category, error_text in examples.items(): + with self.subTest(category=category): + result = _classify(self.tmp_path, error_text, 255) + self.assertEqual(result.returncode, 1) + self.assertEqual( + result.stdout, + f"GATEWAY_CONNECTION_INSPECTION=blocked reason=ssh_{category} exit_code=255\n", + ) + self.assertNotIn(error_text, result.stdout + result.stderr) + self.assertNotIn("hidden-", result.stdout + result.stderr) + + def test_ambiguous_or_unrecognized_errors_fall_back_to_unknown(self) -> None: + for error_text in ( + "unrecognized provider detail hidden-provider-marker", + "Connection timed out; Permission denied (publickey)", + ): + with self.subTest(error_text=error_text): + result = _classify(self.tmp_path, error_text, 255) + self.assertEqual(result.returncode, 1) + self.assertEqual( + result.stdout, + "GATEWAY_CONNECTION_INSPECTION=blocked reason=ssh_unknown exit_code=255\n", + ) + self.assertNotIn(error_text, result.stdout + result.stderr) + + timed_out = _classify(self.tmp_path, "", 124) + self.assertEqual( + timed_out.stdout, + "GATEWAY_CONNECTION_INSPECTION=blocked reason=ssh_timeout exit_code=124\n", + ) + + def test_successful_response_and_helper_block_retain_only_fixed_fields(self) -> None: + valid = "\n".join( + ( + "GATEWAY_CONNECTION_INSPECTION=observed", + "GATEWAY_CONTAINER_RUNNING=true", + "GATEWAY_API_LISTENER=true", + "GATEWAY_ESTABLISHED_CONNECTION_COUNT=2", + "GATEWAY_CONNECTION_OBSERVED_AT_UTC=2026-10-01T00:00:00Z", + "GATEWAY_CONNECTION_INSPECTION_LIMIT=OBSERVATION_ONLY_NO_AUTH_OR_CONCURRENCY_ASSERTION", + ) + ) + result = _classify(self.tmp_path, "", 0, valid) + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, f"{valid}\n") + + helper_error = "GATEWAY_CONNECTION_INSPECTION=blocked reason=socket_inspection_failed" + result = _classify(self.tmp_path, "", 1, helper_error) + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stdout, f"{helper_error}\n") + result = _classify(self.tmp_path, "", 0, helper_error) + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stdout, f"{helper_error}\n") + + untrusted_reason = "GATEWAY_CONNECTION_INSPECTION=blocked reason=made_up_sensitive_detail" + result = _classify(self.tmp_path, "", 0, untrusted_reason) + self.assertEqual(result.returncode, 1) + self.assertEqual( + result.stdout, + "GATEWAY_CONNECTION_INSPECTION=blocked reason=remote_response_invalid\n", + ) + self.assertNotIn("sensitive", result.stdout + result.stderr) + + def test_workflow_uses_helpers_and_retains_single_bounded_ssh_path(self) -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("bash scripts/prepare_gateway_ssh_key.sh", workflow) + self.assertIn("scripts/classify_gateway_ssh_failure.py", workflow) + self.assertIn('--stdout-file "${output_file}"', workflow) + self.assertIn('--stderr-file "${error_file}"', workflow) + self.assertIn("timeout 75 ssh", workflow) + self.assertIn("ConnectionAttempts=1", workflow) + self.assertIn("gcloud compute instances describe", workflow) + self.assertIn('"${match_status}" != "match"', workflow) + self.assertIn("always() && inputs.inspect_connections", workflow) + self.assertIn("gcloud compute start-iap-tunnel", workflow) + self.assertNotIn("gcloud compute ssh", workflow) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_inspect_gateway_connections.sh b/tests/test_inspect_gateway_connections.sh index 5e899f0..d9203e9 100644 --- a/tests/test_inspect_gateway_connections.sh +++ b/tests/test_inspect_gateway_connections.sh @@ -87,6 +87,8 @@ grep -Fq 'gcloud compute start-iap-tunnel' "${workflow_file}" grep -Fq 'ss -H -tn state established sport = ":${api_port}"' "${script_file}" grep -Fq 'gcloud secrets versions access latest' "${workflow_file}" grep -Fq 'chmod 600 "${key_file}"' "${workflow_file}" +grep -Fq 'bash scripts/prepare_gateway_ssh_key.sh' "${workflow_file}" +grep -Fq 'scripts/classify_gateway_ssh_failure.py' "${workflow_file}" grep -Fq 'trap cleanup_secret_fetch EXIT' "${workflow_file}" grep -Fq 'if: ${{ always() && inputs.inspect_connections }}' "${workflow_file}" grep -Fq 'rm -f -- "${SSH_KEY_FILE}"' "${workflow_file}" @@ -96,7 +98,7 @@ test "$(grep -Fc 'NODE_OPTIONS: --require=${{ github.workspace }}/scripts/filter grep -Fq "steps.gcloud_setup_filtered.outcome != 'success'" "${workflow_file}" grep -Fq 'GCP_AUTH_ACTION_STATUS=failed' "${repo_dir}/scripts/filter_github_action_auth_logs.cjs" ! grep -Fq 'gcloud compute ssh' "${workflow_file}" -! grep -Fq 'ssh-keygen' "${workflow_file}" +grep -Fq 'ssh-keygen -y -P' "${repo_dir}/scripts/prepare_gateway_ssh_key.sh" ! grep -Fq 'docker exec' "${script_file}" ! grep -Fq 'docker logs' "${script_file}" ! grep -Fq '4001' "${script_file}"