From 67ac2c7a41828954b93200e0b3daee12ef16c562 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:39:58 +0800 Subject: [PATCH] fix(diagnostic): inspect one protected remaining gateway Co-Authored-By: Codex --- .../read-only-vm-metadata-diagnostic.yml | 41 +++++++++- tests/test_gateway_workflow_resolve.cjs | 78 ++++++++++++++++++- .../test_read_only_vm_metadata_diagnostic.sh | 6 ++ 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/.github/workflows/read-only-vm-metadata-diagnostic.yml b/.github/workflows/read-only-vm-metadata-diagnostic.yml index 590d397..867308b 100644 --- a/.github/workflows/read-only-vm-metadata-diagnostic.yml +++ b/.github/workflows/read-only-vm-metadata-diagnostic.yml @@ -32,6 +32,10 @@ on: required: true default: false type: boolean + remaining_gateway_index: + description: Optionally inspect one protected remaining target by inventory index 0 through 3 + required: false + type: string concurrency: group: read-only-gateway-vm-metadata-${{ inputs.match_current_gateway && 'match-current' || inputs.target }} @@ -58,6 +62,7 @@ jobs: INSPECT_SSH_POLICY: ${{ inputs.inspect_ssh_policy }} INSPECT_SSH_KEY_BINDING: ${{ inputs.inspect_ssh_key_binding }} REMAINING_GATEWAYS: ${{ inputs.remaining_gateways }} + REMAINING_GATEWAY_INDEX: ${{ inputs.remaining_gateway_index }} MATCH_TARGETS_JSON: ${{ inputs.match_current_gateway && secrets.IB_GATEWAY_TARGETS_JSON || '' }} MATCHED_INDEX: ${{ (inputs.inspect_connections || inputs.inspect_ssh_policy || inputs.remaining_gateways) && secrets.IB_GATEWAY_MATCHED_INDEX || '' }} LEGACY_TARGETS_JSON: ${{ !inputs.match_current_gateway && vars.IB_GATEWAY_TARGETS_JSON || '' }} @@ -71,6 +76,13 @@ jobs: const inspectSshPolicy = process.env.INSPECT_SSH_POLICY === "true"; const inspectSshKeyBinding = process.env.INSPECT_SSH_KEY_BINDING === "true"; const remainingGateways = process.env.REMAINING_GATEWAYS === "true"; + const remainingGatewayIndex = process.env.REMAINING_GATEWAY_INDEX || ""; + if (remainingGatewayIndex && (!/^[0-3]$/.test(remainingGatewayIndex) + || !matchMode || !remainingGateways || !inspectConnections + || inspectSshPolicy || inspectSshKeyBinding)) { + core.setFailed("Selected remaining gateway index requires protected passive connection inspection"); + return; + } if ((inspectConnections || inspectSshPolicy || inspectSshKeyBinding) && !matchMode) { core.setFailed("Passive inspection requires protected current-gateway matching"); return; @@ -133,7 +145,19 @@ jobs: const remainingTargets = targets .map((target, target_index) => ({ target, target_index })) .filter(({ target_index }) => target_index !== matchedIndex); - const invalidTarget = remainingTargets.some(({ target }) => { + const requestedIndex = remainingGatewayIndex === "" ? undefined : Number(remainingGatewayIndex); + if (requestedIndex !== undefined && requestedIndex === matchedIndex) { + core.setFailed("Selected gateway index is not an eligible remaining target"); + return; + } + const selectedTargets = requestedIndex === undefined + ? remainingTargets + : remainingTargets.filter(({ target_index }) => target_index === requestedIndex); + if (requestedIndex !== undefined && selectedTargets.length !== 1) { + core.setFailed("Selected gateway index is not an eligible remaining target"); + return; + } + const invalidTarget = selectedTargets.some(({ target }) => { if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(target.gcp_project_id) || !/^[a-z]([-a-z0-9]*[a-z0-9])?$/.test(target.gce_instance_name) || !/^[a-z]+(?:-[a-z]+)*[0-9]+-[a-z]$/.test(target.gce_zone)) return true; @@ -151,7 +175,7 @@ jobs: : "Protected remaining target metadata configuration is incomplete"); return; } - core.setOutput("matrix", JSON.stringify({include: remainingTargets.map(({ target_index }) => ({ + core.setOutput("matrix", JSON.stringify({include: selectedTargets.map(({ target_index }) => ({ target_index, inspect_connections: inspectConnections, inspect_ssh_policy: false, @@ -223,6 +247,7 @@ jobs: INSPECT_SSH_POLICY: ${{ inputs.inspect_ssh_policy }} INSPECT_SSH_KEY_BINDING: ${{ inputs.inspect_ssh_key_binding }} REMAINING_GATEWAYS: ${{ inputs.remaining_gateways }} + REMAINING_GATEWAY_INDEX: ${{ inputs.remaining_gateway_index }} MATCH_TARGETS_JSON: ${{ inputs.match_current_gateway && secrets.IB_GATEWAY_TARGETS_JSON || '' }} MATCHED_INDEX: ${{ (inputs.inspect_connections || inputs.inspect_ssh_policy || inputs.remaining_gateways) && secrets.IB_GATEWAY_MATCHED_INDEX || '' }} LEGACY_TARGETS_JSON: ${{ !inputs.match_current_gateway && vars.IB_GATEWAY_TARGETS_JSON || '' }} @@ -238,6 +263,13 @@ jobs: const inspectSshPolicy = process.env.INSPECT_SSH_POLICY === "true"; const inspectSshKeyBinding = process.env.INSPECT_SSH_KEY_BINDING === "true"; const remainingGateways = process.env.REMAINING_GATEWAYS === "true"; + const remainingGatewayIndex = process.env.REMAINING_GATEWAY_INDEX || ""; + if (remainingGatewayIndex && (!/^[0-3]$/.test(remainingGatewayIndex) + || !matchMode || !remainingGateways || !inspectConnections + || inspectSshPolicy || inspectSshKeyBinding)) { + core.setFailed("Selected remaining gateway index requires protected passive connection inspection"); + return; + } if (remainingGateways && (!matchMode || inspectSshPolicy || inspectSshKeyBinding)) { core.setFailed("Remaining gateway inspection requires protected metadata or passive-connection mode"); return; @@ -291,6 +323,11 @@ jobs: core.setFailed("Resolved gateway target is not an eligible remaining target"); return; } + if (remainingGatewayIndex !== "" && (Number(remainingGatewayIndex) === matchedIndex + || targetIndex !== Number(remainingGatewayIndex))) { + core.setFailed("Resolved gateway target does not match selected remaining index"); + return; + } target = targets[targetIndex]; if (remainingGateways && (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(String(target?.gcp_project_id || "")) || diff --git a/tests/test_gateway_workflow_resolve.cjs b/tests/test_gateway_workflow_resolve.cjs index 8c66da3..031d979 100644 --- a/tests/test_gateway_workflow_resolve.cjs +++ b/tests/test_gateway_workflow_resolve.cjs @@ -28,6 +28,8 @@ for (const line of metadataBlock.slice(metadataScriptStart + 'script: |'.length) const metadataScript = metadataScriptLines.join('\n'); assert.ok(workflow.includes(' resolve:\n runs-on: ubuntu-latest\n permissions:\n contents: read\n')); assert.match(resolveBlock, /uses: actions\/checkout@v6\n\s+with:\n\s+persist-credentials: false/); +assert.match(workflow, /remaining_gateway_index:[\s\S]*?required: false[\s\S]*?type: string/); +assert.match(workflow, /Selected remaining gateway index requires protected passive connection inspection/); const ordinarySetup = workflow.slice(workflow.indexOf('- name: Set up gcloud\n'), workflow.indexOf('- name: Set up gcloud for passive inspection\n')); assert.match(ordinarySetup, /if: \$\{\{ !inputs\.inspect_connections && !inputs\.inspect_ssh_policy && !inputs\.remaining_gateways \}\}/); assert.doesNotMatch(ordinarySetup, /NODE_OPTIONS/); @@ -53,7 +55,7 @@ function runResolve(envValues) { const originalEnv = { ...process.env }; for (const key of [ 'GITHUB_WORKSPACE', 'MATCH_CURRENT_GATEWAY', 'INSPECT_CONNECTIONS', 'INSPECT_SSH_POLICY', 'MATCH_TARGETS_JSON', - 'INSPECT_SSH_KEY_BINDING', 'REMAINING_GATEWAYS', 'MATCHED_INDEX', 'LEGACY_TARGETS_JSON', 'SELECTED_TARGET', + 'INSPECT_SSH_KEY_BINDING', 'REMAINING_GATEWAYS', 'REMAINING_GATEWAY_INDEX', 'MATCHED_INDEX', 'LEGACY_TARGETS_JSON', 'SELECTED_TARGET', ]) delete process.env[key]; Object.assign(process.env, { GITHUB_WORKSPACE: repoRoot }, envValues); const result = { outputs: {}, failure: null }; @@ -76,7 +78,7 @@ function runMetadataResolve(envValues) { const originalEnv = { ...process.env }; for (const key of [ 'GITHUB_WORKSPACE', 'MATCH_CURRENT_GATEWAY', 'INSPECT_CONNECTIONS', 'INSPECT_SSH_POLICY', - 'INSPECT_SSH_KEY_BINDING', 'REMAINING_GATEWAYS', 'MATCH_TARGETS_JSON', 'MATCHED_INDEX', + 'INSPECT_SSH_KEY_BINDING', 'REMAINING_GATEWAYS', 'REMAINING_GATEWAY_INDEX', 'MATCH_TARGETS_JSON', 'MATCHED_INDEX', 'LEGACY_TARGETS_JSON', 'SELECTED_TARGET', 'TARGET_INDEX', 'TARGET_DIGEST', ]) delete process.env[key]; Object.assign(process.env, { GITHUB_WORKSPACE: repoRoot }, envValues); @@ -175,6 +177,61 @@ assert.deepEqual(JSON.parse(remainingConnections.outputs.matrix).include, [ { target_index: 3, inspect_connections: true, inspect_ssh_policy: false }, ]); +const selectedRemainingConnection = runResolve({ + MATCH_CURRENT_GATEWAY: 'true', + INSPECT_CONNECTIONS: 'true', + INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', + REMAINING_GATEWAYS: 'true', + REMAINING_GATEWAY_INDEX: '1', + MATCH_TARGETS_JSON: JSON.stringify(targets), + MATCHED_INDEX: '{"target_index":2}', +}); +assert.equal(selectedRemainingConnection.failure, null); +assert.deepEqual(JSON.parse(selectedRemainingConnection.outputs.matrix).include, [ + { target_index: 1, inspect_connections: true, inspect_ssh_policy: false }, +]); + +for (const invalidIndex of ['00', '4', '-1', '1.0', ' bad ']) { + const rejected = runResolve({ + MATCH_CURRENT_GATEWAY: 'true', + INSPECT_CONNECTIONS: 'true', + INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', + REMAINING_GATEWAYS: 'true', + REMAINING_GATEWAY_INDEX: invalidIndex, + MATCH_TARGETS_JSON: JSON.stringify(targets), + MATCHED_INDEX: '{"target_index":2}', + }); + assert.equal(rejected.failure, 'Selected remaining gateway index requires protected passive connection inspection'); + assert.equal(rejected.outputs.matrix, undefined); +} + +for (const invalidMode of [ + { REMAINING_GATEWAYS: 'true', MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'false' }, + { REMAINING_GATEWAYS: 'false', MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true' }, + { REMAINING_GATEWAYS: 'true', MATCH_CURRENT_GATEWAY: 'false', INSPECT_CONNECTIONS: 'true' }, +]) { + const rejected = runResolve({ + ...invalidMode, + INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', + REMAINING_GATEWAY_INDEX: '1', + MATCH_TARGETS_JSON: JSON.stringify(targets), + MATCHED_INDEX: '{"target_index":2}', + }); + assert.equal(rejected.failure, 'Selected remaining gateway index requires protected passive connection inspection'); + assert.equal(rejected.outputs.matrix, undefined); +} + +const selectedPrimary = runResolve({ + MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true', INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', REMAINING_GATEWAYS: 'true', REMAINING_GATEWAY_INDEX: '2', + MATCH_TARGETS_JSON: JSON.stringify(targets), MATCHED_INDEX: '{"target_index":2}', +}); +assert.equal(selectedPrimary.failure, 'Selected gateway index is not an eligible remaining target'); +assert.equal(selectedPrimary.outputs.matrix, undefined); + const missingRemainingConnectionConfig = runResolve({ MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true', @@ -217,6 +274,7 @@ const combinedDiagnose = runMetadataResolve({ INSPECT_CONNECTIONS: 'true', INSPECT_SSH_POLICY: 'false', REMAINING_GATEWAYS: 'true', + REMAINING_GATEWAY_INDEX: '1', MATCH_TARGETS_JSON: JSON.stringify(targets), MATCHED_INDEX: '{"target_index":2}', TARGET_INDEX: '1', @@ -229,6 +287,22 @@ assert.equal(combinedDiagnose.outputs.gcp_secret_project_id, 'mock-project-1'); assert.equal(combinedDiagnose.outputs.gateway_ip_expected_host, undefined); assert.ok(combinedDiagnose.secrets.includes('mock-ssh-key')); +const rehydratedWrongIndex = runMetadataResolve({ + MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true', INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', REMAINING_GATEWAYS: 'true', REMAINING_GATEWAY_INDEX: '1', + MATCH_TARGETS_JSON: JSON.stringify(targets), MATCHED_INDEX: '{"target_index":2}', TARGET_INDEX: '0', +}); +assert.equal(rehydratedWrongIndex.failure, 'Resolved gateway target does not match selected remaining index'); +assert.equal(rehydratedWrongIndex.exports.GCE_USER, undefined); + +const rehydratedMalformedIndex = runMetadataResolve({ + MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true', INSPECT_SSH_POLICY: 'false', + INSPECT_SSH_KEY_BINDING: 'false', REMAINING_GATEWAYS: 'true', REMAINING_GATEWAY_INDEX: '01', + MATCH_TARGETS_JSON: JSON.stringify(targets), MATCHED_INDEX: '{"target_index":2}', TARGET_INDEX: '1', +}); +assert.equal(rehydratedMalformedIndex.failure, 'Selected remaining gateway index requires protected passive connection inspection'); +assert.equal(rehydratedMalformedIndex.exports.GCE_USER, undefined); + const combinedMatchedDiagnose = runMetadataResolve({ MATCH_CURRENT_GATEWAY: 'true', INSPECT_CONNECTIONS: 'true', diff --git a/tests/test_read_only_vm_metadata_diagnostic.sh b/tests/test_read_only_vm_metadata_diagnostic.sh index db9af36..e93e6e1 100755 --- a/tests/test_read_only_vm_metadata_diagnostic.sh +++ b/tests/test_read_only_vm_metadata_diagnostic.sh @@ -9,6 +9,7 @@ grep -Fq 'name: Read-Only Gateway VM Metadata Diagnostic' "$workflow_file" grep -Fq 'workflow_dispatch:' "$workflow_file" grep -Fq 'match_current_gateway:' "$workflow_file" grep -Fq 'remaining_gateways:' "$workflow_file" +grep -Fq 'remaining_gateway_index:' "$workflow_file" grep -Fq 'inspect_ssh_policy:' "$workflow_file" grep -A3 -Fq 'match_current_gateway:' "$workflow_file" grep -Fq 'default: false' "$workflow_file" @@ -55,6 +56,11 @@ grep -Fq 'GATEWAY_VM_DIAGNOSTIC_TARGET_INDEX=' "$workflow_file" grep -Fq 'inputs.remaining_gateways' "$workflow_file" grep -Fq 'scripts/match_gateway_metadata.py' "$workflow_file" grep -Fq 'targets.map((_, target_index) => ({target_index}))' "$workflow_file" +grep -Fq 'Selected remaining gateway index requires protected passive connection inspection' "$workflow_file" +grep -Fq 'Selected gateway index is not an eligible remaining target' "$workflow_file" +grep -Fq 'Resolved gateway target does not match selected remaining index' "$workflow_file" +grep -Fq 'REMAINING_GATEWAY_INDEX: ${{ inputs.remaining_gateway_index }}' "$workflow_file" +test "$(grep -Fc 'REMAINING_GATEWAY_INDEX: ${{ inputs.remaining_gateway_index }}' "$workflow_file")" -eq 2 test "$(grep -Fc 'gcloud compute instances describe' "$workflow_file")" -eq 2 grep -Fq 'gcloud compute project-info describe' "$workflow_file" grep -Fq 'scripts/parse_gateway_ssh_policy.py' "$workflow_file"