diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index fe15a11..9e93705 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -36,43 +36,42 @@ jobs: steps: - name: Resolve gateway targets id: resolve - env: - TARGETS_JSON: ${{ vars.IB_GATEWAY_TARGETS_JSON }} - SELECTED_TARGET: ${{ github.event.inputs.target || 'all' }} - run: | - import json, os, sys - - raw = os.environ.get("TARGETS_JSON", "").strip() - if not raw: - raise SystemExit("IB_GATEWAY_TARGETS_JSON is required") + uses: actions/github-script@v8 + with: + script: | + const raw = ${{ toJSON(vars.IB_GATEWAY_TARGETS_JSON) }}; + if (!raw || !raw.trim()) { + core.setFailed("IB_GATEWAY_TARGETS_JSON is required"); + return; + } - loaded = json.loads(raw) - targets = [] - if isinstance(loaded, dict): - for name, config in loaded.items(): - cfg = dict(config) - cfg["name"] = name - targets.append(cfg) - elif isinstance(loaded, list): - targets = loaded - else: - raise SystemExit("IB_GATEWAY_TARGETS_JSON must be an object or list") + const loaded = JSON.parse(raw); + const targets = Array.isArray(loaded) + ? loaded + : Object.entries(loaded).map(([name, config]) => ({...config, name})); + if (!Array.isArray(targets)) { + core.setFailed("IB_GATEWAY_TARGETS_JSON must be an object or list"); + return; + } - selected_name = os.environ.get("SELECTED_TARGET", "all") - if selected_name == "all": - selected = targets - else: - selected = [t for t in targets if t["name"] == selected_name] - if not selected: - known = ", ".join(t["name"] for t in targets) - raise SystemExit(f"Unknown target {selected_name!r}. Known: {known}") + const selectedName = ${{ toJSON(github.event.inputs.target || 'all') }}; + const selected = selectedName === "all" + ? targets + : targets.filter((target) => target.name === selectedName); + if (!selected.length) { + core.setFailed("Unknown gateway target; choose one of the configured targets"); + return; + } - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("matrix=" + json.dumps({"target": selected}, separators=(",", ":")) + "\n") - print("Targets: " + ", ".join(t["name"] for t in selected)) - shell: python3 + const maskedTarget = (name) => { + const digits = String(name).replace(/\D/g, ""); + return digits.length >= 4 ? `U***${digits.slice(-4)}` : ""; + }; + core.info(`Targets: ${selected.map((target) => maskedTarget(target.name)).join(", ")}`); + core.setOutput("matrix", JSON.stringify({target: selected})); capture: + name: Capture gateway screen needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest @@ -84,27 +83,43 @@ jobs: contents: read id-token: write env: - TARGET_NAME: ${{ matrix.target.name }} - GCP_PROJECT_ID: ${{ matrix.target.gcp_project_id }} - GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ matrix.target.gcp_workload_identity_provider }} - GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ matrix.target.gcp_workload_identity_service_account }} - GCE_USER: ${{ matrix.target.gce_user }} - GCE_INSTANCE_NAME: ${{ matrix.target.gce_instance_name }} - GCE_ZONE: ${{ matrix.target.gce_zone }} - SSH_PRIVATE_KEY_SECRET_NAME: ${{ matrix.target.ssh_private_key_secret_name }} SCREEN_ACTION: ${{ github.event.inputs.screen_action }} WAIT_FOR_SECOND_FACTOR: ${{ github.event.inputs.wait_for_second_factor }} steps: + - name: Mask target metadata + uses: actions/github-script@v8 + with: + script: | + const metadata = { + TARGET_NAME: ${{ toJSON(matrix.target.name) }}, + GCP_PROJECT_ID: ${{ toJSON(matrix.target.gcp_project_id) }}, + GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + GCE_USER: ${{ toJSON(matrix.target.gce_user) }}, + GCE_INSTANCE_NAME: ${{ toJSON(matrix.target.gce_instance_name) }}, + GCE_ZONE: ${{ toJSON(matrix.target.gce_zone) }}, + DEPLOY_PATH: ${{ toJSON(matrix.target.deploy_path) }}, + SSH_PRIVATE_KEY_SECRET_NAME: ${{ toJSON(matrix.target.ssh_private_key_secret_name) }}, + IB_GATEWAY_MODE: ${{ toJSON(matrix.target.mode) }}, + IB_GATEWAY_CONTAINER_NAME: ${{ toJSON(matrix.target.container_name) }}, + IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ toJSON(matrix.target.compose_service_name) }}, + IB_GATEWAY_UNIT_SUFFIX: ${{ toJSON(matrix.target.unit_suffix || '') }} + }; + for (const [name, value] of Object.entries(metadata)) { + if (value) core.setSecret(String(value)); + core.exportVariable(name, value || ""); + } + - name: Authenticate to Google Cloud uses: google-github-actions/auth@v3 with: - workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ matrix.target.gcp_workload_identity_provider }} + service_account: ${{ matrix.target.gcp_workload_identity_service_account }} - name: Set up gcloud uses: google-github-actions/setup-gcloud@v3 with: - project_id: ${{ env.GCP_PROJECT_ID }} + project_id: ${{ matrix.target.gcp_project_id }} version: '>= 416.0.0' - name: Prepare SSH key diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index f53d128..9e403ef 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -25,43 +25,42 @@ jobs: steps: - name: Resolve gateway targets id: resolve - env: - TARGETS_JSON: ${{ vars.IB_GATEWAY_TARGETS_JSON }} - SELECTED_TARGET: ${{ github.event.inputs.target || 'all' }} - run: | - import json, os, sys - - raw = os.environ.get("TARGETS_JSON", "").strip() - if not raw: - raise SystemExit("IB_GATEWAY_TARGETS_JSON is required") - - loaded = json.loads(raw) - targets = [] - if isinstance(loaded, dict): - for name, config in loaded.items(): - cfg = dict(config) - cfg["name"] = name - targets.append(cfg) - elif isinstance(loaded, list): - targets = loaded - else: - raise SystemExit("IB_GATEWAY_TARGETS_JSON must be an object or list") + uses: actions/github-script@v8 + with: + script: | + const raw = ${{ toJSON(vars.IB_GATEWAY_TARGETS_JSON) }}; + if (!raw || !raw.trim()) { + core.setFailed("IB_GATEWAY_TARGETS_JSON is required"); + return; + } - selected_name = os.environ.get("SELECTED_TARGET", "all") - if selected_name == "all": - selected = targets - else: - selected = [t for t in targets if t["name"] == selected_name] - if not selected: - known = ", ".join(t["name"] for t in targets) - raise SystemExit(f"Unknown target {selected_name!r}. Known: {known}") + const loaded = JSON.parse(raw); + const targets = Array.isArray(loaded) + ? loaded + : Object.entries(loaded).map(([name, config]) => ({...config, name})); + if (!Array.isArray(targets)) { + core.setFailed("IB_GATEWAY_TARGETS_JSON must be an object or list"); + return; + } - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("matrix=" + json.dumps({"target": selected}, separators=(",", ":")) + "\n") - print("Targets: " + ", ".join(t["name"] for t in selected)) - shell: python3 + const selectedName = ${{ toJSON(github.event.inputs.target || 'all') }}; + const selected = selectedName === "all" + ? targets + : targets.filter((target) => target.name === selectedName); + if (!selected.length) { + core.setFailed("Unknown gateway target; choose one of the configured targets"); + return; + } + + const maskedTarget = (name) => { + const digits = String(name).replace(/\D/g, ""); + return digits.length >= 4 ? `U***${digits.slice(-4)}` : ""; + }; + core.info(`Targets: ${selected.map((target) => maskedTarget(target.name)).join(", ")}`); + core.setOutput("matrix", JSON.stringify({target: selected})); diagnose: + name: Diagnose gateway target needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest @@ -72,28 +71,42 @@ jobs: permissions: contents: read id-token: write - env: - TARGET_NAME: ${{ matrix.target.name }} - GCP_PROJECT_ID: ${{ matrix.target.gcp_project_id }} - GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ matrix.target.gcp_workload_identity_provider }} - GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ matrix.target.gcp_workload_identity_service_account }} - GCE_USER: ${{ matrix.target.gce_user }} - GCE_INSTANCE_NAME: ${{ matrix.target.gce_instance_name }} - GCE_ZONE: ${{ matrix.target.gce_zone }} - DEPLOY_PATH: ${{ matrix.target.deploy_path }} - SSH_PRIVATE_KEY_SECRET_NAME: ${{ matrix.target.ssh_private_key_secret_name }} steps: + - name: Mask target metadata + uses: actions/github-script@v8 + with: + script: | + const metadata = { + TARGET_NAME: ${{ toJSON(matrix.target.name) }}, + GCP_PROJECT_ID: ${{ toJSON(matrix.target.gcp_project_id) }}, + GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + GCE_USER: ${{ toJSON(matrix.target.gce_user) }}, + GCE_INSTANCE_NAME: ${{ toJSON(matrix.target.gce_instance_name) }}, + GCE_ZONE: ${{ toJSON(matrix.target.gce_zone) }}, + DEPLOY_PATH: ${{ toJSON(matrix.target.deploy_path) }}, + SSH_PRIVATE_KEY_SECRET_NAME: ${{ toJSON(matrix.target.ssh_private_key_secret_name) }}, + IB_GATEWAY_MODE: ${{ toJSON(matrix.target.mode) }}, + IB_GATEWAY_CONTAINER_NAME: ${{ toJSON(matrix.target.container_name) }}, + IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ toJSON(matrix.target.compose_service_name) }}, + IB_GATEWAY_UNIT_SUFFIX: ${{ toJSON(matrix.target.unit_suffix || '') }} + }; + for (const [name, value] of Object.entries(metadata)) { + if (value) core.setSecret(String(value)); + core.exportVariable(name, value || ""); + } + - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@v3 with: - workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ matrix.target.gcp_workload_identity_provider }} + service_account: ${{ matrix.target.gcp_workload_identity_service_account }} - name: Set up gcloud uses: google-github-actions/setup-gcloud@v3 with: - project_id: ${{ env.GCP_PROJECT_ID }} + project_id: ${{ matrix.target.gcp_project_id }} version: '>= 416.0.0' - name: Prepare SSH key @@ -217,4 +230,53 @@ jobs: deploy_path_quoted="$(printf '%q' "${DEPLOY_PATH}")" REMOTE_DIAG_COMMAND="${REMOTE_DIAG_COMMAND/__DEPLOY_PATH__/${deploy_path_quoted}}" - gcloud compute ssh "${REMOTE_TARGET}" "${SSH_FLAGS[@]}" --command "${REMOTE_DIAG_COMMAND}" + redact_diagnostics() { + python3 -c "$(cat <<'PY' + import ipaddress + import re + import sys + + account_pattern = re.compile(r"(?i)\b(DU|U|F)(\d{4,})\b") + email_pattern = re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.IGNORECASE) + ipv4_pattern = re.compile(r"(?" + + for line in sys.stdin: + line = account_pattern.sub( + lambda match: f"{match.group(1).upper()}***{match.group(2)[-4:]}", + line, + ) + line = email_pattern.sub("", line) + line = ipv4_pattern.sub("", line) + line = ipv6_candidate_pattern.sub(redact_ipv6_candidate, line) + line = sensitive_assignment_pattern.sub(r"\1\2", line) + line = security_code_pattern.sub(r"\1\2", line) + sys.stdout.write(line) + PY + )" + } + + # Keep SSH/authentication failures visible even though output is filtered. + set -o pipefail + gcloud compute ssh "${REMOTE_TARGET}" "${SSH_FLAGS[@]}" --command "${REMOTE_DIAG_COMMAND}" 2>&1 \ + | redact_diagnostics diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index d37297c..922301f 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -32,36 +32,42 @@ jobs: steps: - name: Resolve gateway targets id: resolve - env: - TARGETS_JSON: ${{ vars.IB_GATEWAY_TARGETS_JSON }} - SELECTED_TARGET: ${{ github.event.inputs.target || 'all' }} - run: | - import json, os, sys - raw = os.environ.get("TARGETS_JSON", "").strip() - if not raw: raise SystemExit("IB_GATEWAY_TARGETS_JSON is required") - loaded = json.loads(raw) - targets = [] - if isinstance(loaded, dict): - for name, config in loaded.items(): - cfg = dict(config); cfg["name"] = name; targets.append(cfg) - elif isinstance(loaded, list): - targets = loaded - else: - raise SystemExit("IB_GATEWAY_TARGETS_JSON must be an object or list") - selected_name = os.environ.get("SELECTED_TARGET", "all") - if selected_name == "all": - selected = targets - else: - selected = [t for t in targets if t["name"] == selected_name] - if not selected: - known = ", ".join(t["name"] for t in targets) - raise SystemExit(f"Unknown target {selected_name!r}. Known: {known}") - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write("matrix=" + json.dumps({"target": selected}, separators=(",", ":")) + "\n") - print("Targets: " + ", ".join(t["name"] for t in selected)) - shell: python3 + uses: actions/github-script@v8 + with: + script: | + const raw = ${{ toJSON(vars.IB_GATEWAY_TARGETS_JSON) }}; + if (!raw || !raw.trim()) { + core.setFailed("IB_GATEWAY_TARGETS_JSON is required"); + return; + } + + const loaded = JSON.parse(raw); + const targets = Array.isArray(loaded) + ? loaded + : Object.entries(loaded).map(([name, config]) => ({...config, name})); + if (!Array.isArray(targets)) { + core.setFailed("IB_GATEWAY_TARGETS_JSON must be an object or list"); + return; + } + + const selectedName = ${{ toJSON(github.event.inputs.target || 'all') }}; + const selected = selectedName === "all" + ? targets + : targets.filter((target) => target.name === selectedName); + if (!selected.length) { + core.setFailed("Unknown gateway target; choose one of the configured targets"); + return; + } + + const maskedTarget = (name) => { + const digits = String(name).replace(/\D/g, ""); + return digits.length >= 4 ? `U***${digits.slice(-4)}` : ""; + }; + core.info(`Targets: ${selected.map((target) => maskedTarget(target.name)).join(", ")}`); + core.setOutput("matrix", JSON.stringify({target: selected})); maintenance: + name: Maintain gateway target needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest @@ -73,31 +79,42 @@ jobs: contents: read id-token: write env: - TARGET_NAME: ${{ matrix.target.name }} - GCP_PROJECT_ID: ${{ matrix.target.gcp_project_id }} - GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ matrix.target.gcp_workload_identity_provider }} - GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ matrix.target.gcp_workload_identity_service_account }} - GCE_USER: ${{ matrix.target.gce_user }} - GCE_INSTANCE_NAME: ${{ matrix.target.gce_instance_name }} - GCE_ZONE: ${{ matrix.target.gce_zone }} - DEPLOY_PATH: ${{ matrix.target.deploy_path }} - IB_GATEWAY_MODE: ${{ matrix.target.mode }} - IB_GATEWAY_CONTAINER_NAME: ${{ matrix.target.container_name }} - IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ matrix.target.compose_service_name }} - IB_GATEWAY_UNIT_SUFFIX: ${{ matrix.target.unit_suffix || '' }} - SSH_PRIVATE_KEY_SECRET_NAME: ${{ matrix.target.ssh_private_key_secret_name }} MAINTENANCE_ACTION: ${{ github.event.inputs.action }} steps: + - name: Mask target metadata + uses: actions/github-script@v8 + with: + script: | + const metadata = { + TARGET_NAME: ${{ toJSON(matrix.target.name) }}, + GCP_PROJECT_ID: ${{ toJSON(matrix.target.gcp_project_id) }}, + GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + GCE_USER: ${{ toJSON(matrix.target.gce_user) }}, + GCE_INSTANCE_NAME: ${{ toJSON(matrix.target.gce_instance_name) }}, + GCE_ZONE: ${{ toJSON(matrix.target.gce_zone) }}, + DEPLOY_PATH: ${{ toJSON(matrix.target.deploy_path) }}, + SSH_PRIVATE_KEY_SECRET_NAME: ${{ toJSON(matrix.target.ssh_private_key_secret_name) }}, + IB_GATEWAY_MODE: ${{ toJSON(matrix.target.mode) }}, + IB_GATEWAY_CONTAINER_NAME: ${{ toJSON(matrix.target.container_name) }}, + IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ toJSON(matrix.target.compose_service_name) }}, + IB_GATEWAY_UNIT_SUFFIX: ${{ toJSON(matrix.target.unit_suffix || '') }} + }; + for (const [name, value] of Object.entries(metadata)) { + if (value) core.setSecret(String(value)); + core.exportVariable(name, value || ""); + } + - name: Authenticate to Google Cloud uses: google-github-actions/auth@v3 with: - workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + workload_identity_provider: ${{ matrix.target.gcp_workload_identity_provider }} + service_account: ${{ matrix.target.gcp_workload_identity_service_account }} - name: Set up gcloud uses: google-github-actions/setup-gcloud@v3 with: - project_id: ${{ env.GCP_PROJECT_ID }} + project_id: ${{ matrix.target.gcp_project_id }} version: '>= 416.0.0' - name: Prepare SSH key diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index b9a8cfb..7a26f80 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -4,6 +4,7 @@ set -euo pipefail repo_dir="$(cd "$(dirname "$0")/.." && pwd)" workflow_file="$repo_dir/.github/workflows/main.yml" maintenance_workflow_file="$repo_dir/.github/workflows/remote-maintenance.yml" +diagnose_workflow_file="$repo_dir/.github/workflows/diagnose.yml" grep -Fq 'target:' "$workflow_file" grep -Fq 'IB_GATEWAY_TARGETS_JSON' "$workflow_file" @@ -146,12 +147,90 @@ done grep -Fq 'stop-gateway' "$maintenance_workflow_file" grep -Fq 'restart-gateway' "$maintenance_workflow_file" grep -Fq 'status' "$maintenance_workflow_file" -grep -Fq 'DEPLOY_PATH: ${{ matrix.target.deploy_path }}' "$maintenance_workflow_file" -grep -Fq 'IB_GATEWAY_MODE: ${{ matrix.target.mode }}' "$maintenance_workflow_file" -grep -Fq 'IB_GATEWAY_CONTAINER_NAME: ${{ matrix.target.container_name }}' "$maintenance_workflow_file" -grep -Fq 'IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ matrix.target.compose_service_name }}' "$maintenance_workflow_file" +grep -Fq 'DEPLOY_PATH: ${{ toJSON(matrix.target.deploy_path) }}' "$maintenance_workflow_file" +grep -Fq 'IB_GATEWAY_MODE: ${{ toJSON(matrix.target.mode) }}' "$maintenance_workflow_file" +grep -Fq 'IB_GATEWAY_CONTAINER_NAME: ${{ toJSON(matrix.target.container_name) }}' "$maintenance_workflow_file" +grep -Fq 'IB_GATEWAY_COMPOSE_SERVICE_NAME: ${{ toJSON(matrix.target.compose_service_name) }}' "$maintenance_workflow_file" grep -Fq 'sudo systemctl disable --now' "$maintenance_workflow_file" grep -Fq 'sudo docker update --restart=no "${container_name}"' "$maintenance_workflow_file" grep -Fq 'sudo docker compose down' "$maintenance_workflow_file" grep -Fq 'sudo docker compose up -d --no-build "${compose_service_name}"' "$maintenance_workflow_file" grep -Fq 'bash ./scripts/install_gateway_health_watcher.sh __IB_GATEWAY_MODE__' "$maintenance_workflow_file" + +! grep -Fxq ' shell: python3' "$diagnose_workflow_file" +grep -Fq 'redact_diagnostics()' "$diagnose_workflow_file" +grep -Fq 'sensitive_assignment_pattern.sub(r"\1\2", line)' "$diagnose_workflow_file" +grep -Fq 'set -o pipefail' "$diagnose_workflow_file" +grep -Fq '| redact_diagnostics' "$diagnose_workflow_file" +! grep -Fq 'actions/checkout' "$diagnose_workflow_file" +! grep -R -Fxq ' shell: python3' "$repo_dir/.github/workflows" + +python3 - "$diagnose_workflow_file" <<'PY' +from pathlib import Path +import subprocess +import sys + +workflow = Path(sys.argv[1]).read_text(encoding="utf-8") +start = " python3 -c \"$(cat <<'PY'\n" +end = "\n PY\n )\"\n" +code = workflow.split(start, 1)[1].split(end, 1)[0] +code = "\n".join(line.removeprefix(" ") for line in code.splitlines()) +sample = ( + "account=U12345678 host=10.20.30.40 user@example.com\n" + "network=2001:db8::1 scoped=fe80::1%eth0 punctuation=10.20.30.40.\n" + "paper_account=DU7654321 advisor_account=F9876543\n" + "TOTP_SECRET=JBSWY3DPEHPK3PXP\n" + "Authorization: Bearer header.payload.signature\n" + 'json={"access_token":"json-secret","other":1}\n' + '"cookie": "session-secret; second=value"\n' + "Security code: 123456\n" +) +result = subprocess.run( + [sys.executable, "-c", code], + input=sample, + capture_output=True, + check=True, + text=True, +) +assert "account=U***5678 host= " in result.stdout +assert "network= scoped= punctuation=." in result.stdout +assert "paper_account=DU***4321 advisor_account=F***6543" in result.stdout +assert "TOTP_SECRET=" in result.stdout +assert "Authorization: " in result.stdout +assert 'json={"access_token":' in result.stdout +assert '"cookie": ' in result.stdout +assert "Security code: " in result.stdout +assert "\n\n" not in result.stdout +for sensitive in ( + "U12345678", + "DU7654321", + "F9876543", + "10.20.30.40", + "2001:db8::1", + "fe80::1%eth0", + "user@example.com", + "JBSWY3DPEHPK3PXP", + "header.payload.signature", + "json-secret", + "session-secret", + "123456", +): + assert sensitive not in result.stdout +PY + +for resolver_workflow in "$repo_dir/.github/workflows/diagnose.yml" \ + "$repo_dir/.github/workflows/capture-screen.yml" \ + "$repo_dir/.github/workflows/remote-maintenance.yml" +do + grep -Fq 'const raw = ${{ toJSON(vars.IB_GATEWAY_TARGETS_JSON) }};' "$resolver_workflow" + grep -Fq 'return digits.length >= 4 ? `U***${digits.slice(-4)}` : "";' "$resolver_workflow" + grep -Fq 'if (value) core.setSecret(String(value));' "$resolver_workflow" + grep -Fq 'core.setFailed("Unknown gateway target; choose one of the configured targets")' "$resolver_workflow" + ! grep -Fq 'Unknown gateway target: ${selectedName}' "$resolver_workflow" + ! grep -Fq 'TARGETS_JSON: ${{ vars.IB_GATEWAY_TARGETS_JSON }}' "$resolver_workflow" + ! grep -Fq 'print("Targets: " + ", ".join(t["name"] for t in selected))' "$resolver_workflow" +done + +grep -Fq 'name: Diagnose gateway target' "$repo_dir/.github/workflows/diagnose.yml" +grep -Fq 'name: Capture gateway screen' "$repo_dir/.github/workflows/capture-screen.yml" +grep -Fq 'name: Maintain gateway target' "$repo_dir/.github/workflows/remote-maintenance.yml"