From e613732e9b9672cf135c5b8e0ff3b555ecd2f96a Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:49:18 +0800 Subject: [PATCH 01/11] fix(ops): restore redacted gateway diagnostics Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 2 +- .github/workflows/ci.yml | 1 + .github/workflows/diagnose.yml | 14 ++++++++-- .github/workflows/remote-maintenance.yml | 2 +- scripts/redact_gateway_diagnostics.py | 35 ++++++++++++++++++++++++ tests/test_redact_gateway_diagnostics.sh | 19 +++++++++++++ tests/test_workflow_shared_config.sh | 6 ++++ 7 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 scripts/redact_gateway_diagnostics.py create mode 100644 tests/test_redact_gateway_diagnostics.sh diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index fe15a11..bc770da 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -70,7 +70,7 @@ jobs: 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 + shell: python3 {0} capture: needs: resolve diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79014cc..11b4482 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,4 @@ jobs: bash tests/test_wait_for_ib_gateway_ready.sh bash tests/test_workflow_shared_config.sh bash tests/test_docker_compose_ports.sh + bash tests/test_redact_gateway_diagnostics.sh diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index f53d128..a27e282 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -58,8 +58,12 @@ jobs: 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 + def masked_target(name): + digits = "".join(char for char in str(name) if char.isdigit()) + return f"U***{digits[-4:]}" if len(digits) >= 4 else "" + + print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) + shell: python3 {0} diagnose: needs: resolve @@ -83,6 +87,9 @@ jobs: DEPLOY_PATH: ${{ matrix.target.deploy_path }} SSH_PRIVATE_KEY_SECRET_NAME: ${{ matrix.target.ssh_private_key_secret_name }} steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@v3 @@ -217,4 +224,5 @@ 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}" + gcloud compute ssh "${REMOTE_TARGET}" "${SSH_FLAGS[@]}" --command "${REMOTE_DIAG_COMMAND}" 2>&1 \ + | python3 scripts/redact_gateway_diagnostics.py diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index d37297c..d4d508f 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -59,7 +59,7 @@ jobs: 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 + shell: python3 {0} maintenance: needs: resolve diff --git a/scripts/redact_gateway_diagnostics.py b/scripts/redact_gateway_diagnostics.py new file mode 100644 index 0000000..a8d8791 --- /dev/null +++ b/scripts/redact_gateway_diagnostics.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Redact account and credential-like values from gateway diagnostics.""" + +from __future__ import annotations + +import re +import sys + + +ACCOUNT_PATTERN = re.compile(r"(?i)\bu(\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"(? str: + line = ACCOUNT_PATTERN.sub(lambda match: f"U***{match.group(1)[-4:]}", line) + line = EMAIL_PATTERN.sub("", line) + line = IPV4_PATTERN.sub("", line) + line = SENSITIVE_ASSIGNMENT_PATTERN.sub(r"\1\2", line) + return SECURITY_CODE_PATTERN.sub(r"\1", line) + + +def main() -> int: + for line in sys.stdin: + sys.stdout.write(redact_line(line)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_redact_gateway_diagnostics.sh b/tests/test_redact_gateway_diagnostics.sh new file mode 100644 index 0000000..70227c0 --- /dev/null +++ b/tests/test_redact_gateway_diagnostics.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_dir="$(cd "$(dirname "$0")/.." && pwd)" + +actual="$({ + printf '%s\n' 'account=U12345678 host=10.20.30.40 user@example.com' + printf '%s\n' 'TOTP_SECRET=JBSWY3DPEHPK3PXP' + printf '%s\n' 'Security code: 123456' +} | python3 "$repo_dir/scripts/redact_gateway_diagnostics.py")" + +grep -Fq 'account=U***5678 host= ' <<<"$actual" +grep -Fq 'TOTP_SECRET=' <<<"$actual" +grep -Fq 'Security code: ' <<<"$actual" +! grep -Fq 'U12345678' <<<"$actual" +! grep -Fq '10.20.30.40' <<<"$actual" +! grep -Fq 'user@example.com' <<<"$actual" +! grep -Fq 'JBSWY3DPEHPK3PXP' <<<"$actual" +! grep -Fq '123456' <<<"$actual" diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index b9a8cfb..20b261a 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" @@ -155,3 +156,8 @@ grep -Fq 'sudo docker update --restart=no "${container_name}"' "$maintenance_wor 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 -Fq 'shell: python3 {0}' "$diagnose_workflow_file" +! grep -Fxq ' shell: python3' "$diagnose_workflow_file" +grep -Fq '| python3 scripts/redact_gateway_diagnostics.py' "$diagnose_workflow_file" +! grep -R -Fxq ' shell: python3' "$repo_dir/.github/workflows" From 95571cb9470aabe6ec686af886cb859538232bde Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:53:35 +0800 Subject: [PATCH 02/11] fix(security): inline privileged log redaction Co-Authored-By: Codex --- .github/workflows/ci.yml | 1 - .github/workflows/diagnose.yml | 29 +++++++++++++--- scripts/redact_gateway_diagnostics.py | 35 ------------------- tests/test_redact_gateway_diagnostics.sh | 19 ----------- tests/test_workflow_shared_config.sh | 43 +++++++++++++++++++++++- 5 files changed, 67 insertions(+), 60 deletions(-) delete mode 100644 scripts/redact_gateway_diagnostics.py delete mode 100644 tests/test_redact_gateway_diagnostics.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11b4482..79014cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,4 +23,3 @@ jobs: bash tests/test_wait_for_ib_gateway_ready.sh bash tests/test_workflow_shared_config.sh bash tests/test_docker_compose_ports.sh - bash tests/test_redact_gateway_diagnostics.sh diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index a27e282..1dfd204 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -87,9 +87,6 @@ jobs: DEPLOY_PATH: ${{ matrix.target.deploy_path }} SSH_PRIVATE_KEY_SECRET_NAME: ${{ matrix.target.ssh_private_key_secret_name }} steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@v3 @@ -224,5 +221,29 @@ jobs: deploy_path_quoted="$(printf '%q' "${DEPLOY_PATH}")" REMOTE_DIAG_COMMAND="${REMOTE_DIAG_COMMAND/__DEPLOY_PATH__/${deploy_path_quoted}}" + redact_diagnostics() { + python3 -c ' + import re + import sys + + account_pattern = re.compile(r"(?i)\bu(\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"(?", line) + line = ipv4_pattern.sub("", line) + line = sensitive_assignment_pattern.sub(r"\1\2\n", line) + line = security_code_pattern.sub(r"\1\n", line) + sys.stdout.write(line) + ' + } + gcloud compute ssh "${REMOTE_TARGET}" "${SSH_FLAGS[@]}" --command "${REMOTE_DIAG_COMMAND}" 2>&1 \ - | python3 scripts/redact_gateway_diagnostics.py + | redact_diagnostics diff --git a/scripts/redact_gateway_diagnostics.py b/scripts/redact_gateway_diagnostics.py deleted file mode 100644 index a8d8791..0000000 --- a/scripts/redact_gateway_diagnostics.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""Redact account and credential-like values from gateway diagnostics.""" - -from __future__ import annotations - -import re -import sys - - -ACCOUNT_PATTERN = re.compile(r"(?i)\bu(\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"(? str: - line = ACCOUNT_PATTERN.sub(lambda match: f"U***{match.group(1)[-4:]}", line) - line = EMAIL_PATTERN.sub("", line) - line = IPV4_PATTERN.sub("", line) - line = SENSITIVE_ASSIGNMENT_PATTERN.sub(r"\1\2", line) - return SECURITY_CODE_PATTERN.sub(r"\1", line) - - -def main() -> int: - for line in sys.stdin: - sys.stdout.write(redact_line(line)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_redact_gateway_diagnostics.sh b/tests/test_redact_gateway_diagnostics.sh deleted file mode 100644 index 70227c0..0000000 --- a/tests/test_redact_gateway_diagnostics.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_dir="$(cd "$(dirname "$0")/.." && pwd)" - -actual="$({ - printf '%s\n' 'account=U12345678 host=10.20.30.40 user@example.com' - printf '%s\n' 'TOTP_SECRET=JBSWY3DPEHPK3PXP' - printf '%s\n' 'Security code: 123456' -} | python3 "$repo_dir/scripts/redact_gateway_diagnostics.py")" - -grep -Fq 'account=U***5678 host= ' <<<"$actual" -grep -Fq 'TOTP_SECRET=' <<<"$actual" -grep -Fq 'Security code: ' <<<"$actual" -! grep -Fq 'U12345678' <<<"$actual" -! grep -Fq '10.20.30.40' <<<"$actual" -! grep -Fq 'user@example.com' <<<"$actual" -! grep -Fq 'JBSWY3DPEHPK3PXP' <<<"$actual" -! grep -Fq '123456' <<<"$actual" diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 20b261a..4da4095 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -159,5 +159,46 @@ grep -Fq 'bash ./scripts/install_gateway_health_watcher.sh __IB_GATEWAY_MODE__' grep -Fq 'shell: python3 {0}' "$diagnose_workflow_file" ! grep -Fxq ' shell: python3' "$diagnose_workflow_file" -grep -Fq '| python3 scripts/redact_gateway_diagnostics.py' "$diagnose_workflow_file" +grep -Fq 'redact_diagnostics()' "$diagnose_workflow_file" +grep -Fq 'sensitive_assignment_pattern.sub(r"\1\2\n", line)' "$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 '\n" +end = "\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" + "TOTP_SECRET=JBSWY3DPEHPK3PXP\n" + "Authorization: Bearer header.payload.signature\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 "TOTP_SECRET=" in result.stdout +assert "Authorization: " in result.stdout +assert "Security code: " in result.stdout +for sensitive in ( + "U12345678", + "10.20.30.40", + "user@example.com", + "JBSWY3DPEHPK3PXP", + "header.payload.signature", + "123456", +): + assert sensitive not in result.stdout +PY From 9ec2ab0d9eb700884a64816cbd97b02f3885581e Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:55:01 +0800 Subject: [PATCH 03/11] fix(security): redact IB paper account IDs Co-Authored-By: Codex --- .github/workflows/diagnose.yml | 7 +++++-- tests/test_workflow_shared_config.sh | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index 1dfd204..e1f25bf 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -226,7 +226,7 @@ jobs: import re import sys - account_pattern = re.compile(r"(?i)\bu(\d{4,})\b") + 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"(?", line) line = ipv4_pattern.sub("", line) line = sensitive_assignment_pattern.sub(r"\1\2\n", line) diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 4da4095..a01c33d 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -177,6 +177,7 @@ 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" + "paper_account=DU7654321 advisor_account=F9876543\n" "TOTP_SECRET=JBSWY3DPEHPK3PXP\n" "Authorization: Bearer header.payload.signature\n" "Security code: 123456\n" @@ -189,11 +190,14 @@ result = subprocess.run( text=True, ) assert "account=U***5678 host= " 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 "Security code: " in result.stdout for sensitive in ( "U12345678", + "DU7654321", + "F9876543", "10.20.30.40", "user@example.com", "JBSWY3DPEHPK3PXP", From 398deb36870d08212477c89b138a3e42bc955e77 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:56:05 +0800 Subject: [PATCH 04/11] fix(ops): preserve diagnostic SSH failures Co-Authored-By: Codex --- .github/workflows/diagnose.yml | 2 ++ tests/test_workflow_shared_config.sh | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index e1f25bf..3b84b81 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -248,5 +248,7 @@ jobs: ' } + # 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/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index a01c33d..9da14ca 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -161,6 +161,7 @@ grep -Fq 'shell: python3 {0}' "$diagnose_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\n", 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" From a478bf20c9487ab4e3205aa3b0e3a9c51e5448eb Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:58:39 +0800 Subject: [PATCH 05/11] fix(ops): left-align inline redactor source Co-Authored-By: Codex --- .github/workflows/diagnose.yml | 5 +++-- tests/test_workflow_shared_config.sh | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index 3b84b81..7272446 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -222,7 +222,7 @@ jobs: REMOTE_DIAG_COMMAND="${REMOTE_DIAG_COMMAND/__DEPLOY_PATH__/${deploy_path_quoted}}" redact_diagnostics() { - python3 -c ' + python3 -c "$(cat <<'PY' import re import sys @@ -245,7 +245,8 @@ jobs: line = sensitive_assignment_pattern.sub(r"\1\2\n", line) line = security_code_pattern.sub(r"\1\n", line) sys.stdout.write(line) - ' + PY + )" } # Keep SSH/authentication failures visible even though output is filtered. diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 9da14ca..6fea14d 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -172,8 +172,8 @@ import subprocess import sys workflow = Path(sys.argv[1]).read_text(encoding="utf-8") -start = " python3 -c '\n" -end = "\n '\n" +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 = ( From 3d8f441619c4315582620f917d52119be693a8e1 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:05:56 +0800 Subject: [PATCH 06/11] fix(security): cover diagnostic metadata formats Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 6 +++++- .github/workflows/diagnose.yml | 17 ++++++++++++++++- .github/workflows/remote-maintenance.yml | 6 +++++- tests/test_workflow_shared_config.sh | 12 ++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index bc770da..7ef09a6 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -69,7 +69,11 @@ jobs: 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)) + def masked_target(name): + digits = "".join(char for char in str(name) if char.isdigit()) + return f"U***{digits[-4:]}" if len(digits) >= 4 else "" + + print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) shell: python3 {0} capture: diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index 7272446..b5ea242 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -223,18 +223,32 @@ jobs: 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:]}", @@ -242,6 +256,7 @@ jobs: ) 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\n", line) line = security_code_pattern.sub(r"\1\n", line) sys.stdout.write(line) diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index d4d508f..adcca5a 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -58,7 +58,11 @@ jobs: 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)) + def masked_target(name): + digits = "".join(char for char in str(name) if char.isdigit()) + return f"U***{digits[-4:]}" if len(digits) >= 4 else "" + + print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) shell: python3 {0} maintenance: diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 6fea14d..5714296 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -178,6 +178,7 @@ 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" @@ -191,6 +192,7 @@ result = subprocess.run( 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 @@ -200,6 +202,8 @@ for sensitive in ( "DU7654321", "F9876543", "10.20.30.40", + "2001:db8::1", + "fe80::1%eth0", "user@example.com", "JBSWY3DPEHPK3PXP", "header.payload.signature", @@ -207,3 +211,11 @@ for sensitive in ( ): 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 'return f"U***{digits[-4:]}" if len(digits) >= 4 else ""' "$resolver_workflow" + ! grep -Fq 'print("Targets: " + ", ".join(t["name"] for t in selected))' "$resolver_workflow" +done From cba197294c1303e4b7d1be62d218ce4b8f111cee Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:09:59 +0800 Subject: [PATCH 07/11] fix(security): mask manual workflow target metadata Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 84 ++++++++++++---------- .github/workflows/diagnose.yml | 90 ++++++++++++++---------- .github/workflows/remote-maintenance.yml | 83 +++++++++++++--------- tests/test_workflow_shared_config.sh | 6 +- 4 files changed, 154 insertions(+), 109 deletions(-) diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index 7ef09a6..2ca9470 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -36,45 +36,39 @@ 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") - def masked_target(name): - digits = "".join(char for char in str(name) if char.isdigit()) - return f"U***{digits[-4:]}" if len(digits) >= 4 else "" + 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: ${selectedName}`); + return; + } - print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) - shell: python3 {0} + 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: needs: resolve @@ -99,6 +93,24 @@ jobs: 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: | + for (const value of [ + ${{ toJSON(matrix.target.name) }}, + ${{ toJSON(matrix.target.gcp_project_id) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + ${{ toJSON(matrix.target.gce_user) }}, + ${{ toJSON(matrix.target.gce_instance_name) }}, + ${{ toJSON(matrix.target.gce_zone) }}, + ${{ toJSON(matrix.target.deploy_path) }}, + ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} + ]) { + if (value) core.setSecret(String(value)); + } + - name: Authenticate to Google Cloud uses: google-github-actions/auth@v3 with: diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index b5ea242..3be5a19 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -25,45 +25,39 @@ 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") - def masked_target(name): - digits = "".join(char for char in str(name) if char.isdigit()) - return f"U***{digits[-4:]}" if len(digits) >= 4 else "" - - print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) - shell: python3 {0} + 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: ${selectedName}`); + 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: needs: resolve @@ -87,6 +81,24 @@ jobs: 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: | + for (const value of [ + ${{ toJSON(matrix.target.name) }}, + ${{ toJSON(matrix.target.gcp_project_id) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + ${{ toJSON(matrix.target.gce_user) }}, + ${{ toJSON(matrix.target.gce_instance_name) }}, + ${{ toJSON(matrix.target.gce_zone) }}, + ${{ toJSON(matrix.target.deploy_path) }}, + ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} + ]) { + if (value) core.setSecret(String(value)); + } + - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@v3 diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index adcca5a..90f27d5 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -32,38 +32,39 @@ 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") - def masked_target(name): - digits = "".join(char for char in str(name) if char.isdigit()) - return f"U***{digits[-4:]}" if len(digits) >= 4 else "" - - print("Targets: " + ", ".join(masked_target(t["name"]) for t in selected)) - shell: python3 {0} + 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: ${selectedName}`); + 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: needs: resolve @@ -92,6 +93,24 @@ jobs: 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: | + for (const value of [ + ${{ toJSON(matrix.target.name) }}, + ${{ toJSON(matrix.target.gcp_project_id) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, + ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, + ${{ toJSON(matrix.target.gce_user) }}, + ${{ toJSON(matrix.target.gce_instance_name) }}, + ${{ toJSON(matrix.target.gce_zone) }}, + ${{ toJSON(matrix.target.deploy_path) }}, + ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} + ]) { + if (value) core.setSecret(String(value)); + } + - name: Authenticate to Google Cloud uses: google-github-actions/auth@v3 with: diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 5714296..9533add 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -157,7 +157,6 @@ 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 -Fq 'shell: python3 {0}' "$diagnose_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\n", line)' "$diagnose_workflow_file" @@ -216,6 +215,9 @@ 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 'return f"U***{digits[-4:]}" if len(digits) >= 4 else ""' "$resolver_workflow" + 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 'TARGETS_JSON: ${{ vars.IB_GATEWAY_TARGETS_JSON }}' "$resolver_workflow" ! grep -Fq 'print("Targets: " + ", ".join(t["name"] for t in selected))' "$resolver_workflow" done From 90a74c7d4d634a3aed4716258d7451c67e8157b4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:11:16 +0800 Subject: [PATCH 08/11] fix(security): keep target config out of workflow logs Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 42 ++++++++++----------- .github/workflows/diagnose.yml | 44 ++++++++++------------ .github/workflows/remote-maintenance.yml | 47 ++++++++++-------------- tests/test_workflow_shared_config.sh | 8 ++-- 4 files changed, 64 insertions(+), 77 deletions(-) diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index 2ca9470..58b39c5 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -82,14 +82,6 @@ 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: @@ -97,30 +89,36 @@ jobs: uses: actions/github-script@v8 with: script: | - for (const value of [ - ${{ toJSON(matrix.target.name) }}, - ${{ toJSON(matrix.target.gcp_project_id) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, - ${{ toJSON(matrix.target.gce_user) }}, - ${{ toJSON(matrix.target.gce_instance_name) }}, - ${{ toJSON(matrix.target.gce_zone) }}, - ${{ toJSON(matrix.target.deploy_path) }}, - ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} - ]) { + 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 3be5a19..bd85ddb 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -70,46 +70,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: | - for (const value of [ - ${{ toJSON(matrix.target.name) }}, - ${{ toJSON(matrix.target.gcp_project_id) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, - ${{ toJSON(matrix.target.gce_user) }}, - ${{ toJSON(matrix.target.gce_instance_name) }}, - ${{ toJSON(matrix.target.gce_zone) }}, - ${{ toJSON(matrix.target.deploy_path) }}, - ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} - ]) { + 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 diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index 90f27d5..adb50ed 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -78,49 +78,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: | - for (const value of [ - ${{ toJSON(matrix.target.name) }}, - ${{ toJSON(matrix.target.gcp_project_id) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_provider) }}, - ${{ toJSON(matrix.target.gcp_workload_identity_service_account) }}, - ${{ toJSON(matrix.target.gce_user) }}, - ${{ toJSON(matrix.target.gce_instance_name) }}, - ${{ toJSON(matrix.target.gce_zone) }}, - ${{ toJSON(matrix.target.deploy_path) }}, - ${{ toJSON(matrix.target.ssh_private_key_secret_name) }} - ]) { + 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 9533add..4772b62 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -147,10 +147,10 @@ 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" From 612070fe8ff505e27782b7178aa6ff7d3b920aba Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:11:44 +0800 Subject: [PATCH 09/11] fix(security): hide matrix target identifiers Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 1 + .github/workflows/diagnose.yml | 1 + .github/workflows/remote-maintenance.yml | 1 + tests/test_workflow_shared_config.sh | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index 58b39c5..aba5916 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -71,6 +71,7 @@ jobs: core.setOutput("matrix", JSON.stringify({target: selected})); capture: + name: Capture gateway screen needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index bd85ddb..2bc7bca 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -60,6 +60,7 @@ jobs: core.setOutput("matrix", JSON.stringify({target: selected})); diagnose: + name: Diagnose gateway target needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index adb50ed..a584a98 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -67,6 +67,7 @@ jobs: core.setOutput("matrix", JSON.stringify({target: selected})); maintenance: + name: Maintain gateway target needs: resolve if: needs.resolve.outputs.matrix != '{"target":[]}' runs-on: ubuntu-latest diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 4772b62..ed57396 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -221,3 +221,7 @@ do ! 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" From 192f6321c21bdb6f1411008520fc35b5d9f55063 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:16:06 +0800 Subject: [PATCH 10/11] fix(security): redact structured credential fields Co-Authored-By: Codex --- .github/workflows/diagnose.yml | 13 ++++++++----- tests/test_workflow_shared_config.sh | 9 ++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index 2bc7bca..068a5e0 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -244,10 +244,13 @@ jobs: r"(?:%[0-9a-z_.-]+)?(?![0-9a-f:])" ) sensitive_assignment_pattern = re.compile( - r"(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|COOKIE|AUTHORIZATION)[A-Z0-9_]*)" - r"(\s*[:=]\s*).*$" + r"(?i)(?", line) line = ipv4_pattern.sub("", line) line = ipv6_candidate_pattern.sub(redact_ipv6_candidate, line) - line = sensitive_assignment_pattern.sub(r"\1\2\n", line) - line = security_code_pattern.sub(r"\1\n", line) + line = sensitive_assignment_pattern.sub(r"\1\2", line) + line = security_code_pattern.sub(r"\1\2", line) sys.stdout.write(line) PY )" diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index ed57396..0d13ae5 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -159,7 +159,7 @@ grep -Fq 'bash ./scripts/install_gateway_health_watcher.sh __IB_GATEWAY_MODE__' ! grep -Fxq ' shell: python3' "$diagnose_workflow_file" grep -Fq 'redact_diagnostics()' "$diagnose_workflow_file" -grep -Fq 'sensitive_assignment_pattern.sub(r"\1\2\n", line)' "$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" @@ -181,6 +181,8 @@ sample = ( "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( @@ -195,7 +197,10 @@ 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", @@ -206,6 +211,8 @@ for sensitive in ( "user@example.com", "JBSWY3DPEHPK3PXP", "header.payload.signature", + "json-secret", + "session-secret", "123456", ): assert sensitive not in result.stdout From d2bfdd7e616e6c35f57bb17d7f02950755d1060c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:16:50 +0800 Subject: [PATCH 11/11] fix(security): hide unknown target input Co-Authored-By: Codex --- .github/workflows/capture-screen.yml | 2 +- .github/workflows/diagnose.yml | 2 +- .github/workflows/remote-maintenance.yml | 2 +- tests/test_workflow_shared_config.sh | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/capture-screen.yml b/.github/workflows/capture-screen.yml index aba5916..9e93705 100644 --- a/.github/workflows/capture-screen.yml +++ b/.github/workflows/capture-screen.yml @@ -59,7 +59,7 @@ jobs: ? targets : targets.filter((target) => target.name === selectedName); if (!selected.length) { - core.setFailed(`Unknown gateway target: ${selectedName}`); + core.setFailed("Unknown gateway target; choose one of the configured targets"); return; } diff --git a/.github/workflows/diagnose.yml b/.github/workflows/diagnose.yml index 068a5e0..9e403ef 100644 --- a/.github/workflows/diagnose.yml +++ b/.github/workflows/diagnose.yml @@ -48,7 +48,7 @@ jobs: ? targets : targets.filter((target) => target.name === selectedName); if (!selected.length) { - core.setFailed(`Unknown gateway target: ${selectedName}`); + core.setFailed("Unknown gateway target; choose one of the configured targets"); return; } diff --git a/.github/workflows/remote-maintenance.yml b/.github/workflows/remote-maintenance.yml index a584a98..922301f 100644 --- a/.github/workflows/remote-maintenance.yml +++ b/.github/workflows/remote-maintenance.yml @@ -55,7 +55,7 @@ jobs: ? targets : targets.filter((target) => target.name === selectedName); if (!selected.length) { - core.setFailed(`Unknown gateway target: ${selectedName}`); + core.setFailed("Unknown gateway target; choose one of the configured targets"); return; } diff --git a/tests/test_workflow_shared_config.sh b/tests/test_workflow_shared_config.sh index 0d13ae5..7a26f80 100644 --- a/tests/test_workflow_shared_config.sh +++ b/tests/test_workflow_shared_config.sh @@ -225,6 +225,8 @@ 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