Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ jobs:
bash tests/test_workflow_shared_config.sh
bash tests/test_docker_compose_ports.sh
bash tests/test_read_only_vm_metadata_diagnostic.sh
bash tests/test_inspect_gateway_connections.sh
node tests/test_filter_github_action_auth_logs.cjs
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'

docker-build:
Expand Down
234 changes: 229 additions & 5 deletions .github/workflows/read-only-vm-metadata-diagnostic.yml

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions scripts/filter_github_action_auth_logs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

const rawStdoutWrite = process.stdout.write.bind(process.stdout);
const rawStderrWrite = process.stderr.write.bind(process.stderr);
let stdoutRemainder = '';

function callbackFrom(encoding, callback) {
if (typeof encoding === 'function') return encoding;
return typeof callback === 'function' ? callback : null;
}

function acceptedMaskCommands(value, flush = false) {
stdoutRemainder += value;
let output = '';
while (true) {
const newline = stdoutRemainder.indexOf('\n');
if (newline < 0) break;
const line = stdoutRemainder.slice(0, newline + 1);
stdoutRemainder = stdoutRemainder.slice(newline + 1);
if (line.startsWith('::add-mask::')) output += line;
}
if (flush && stdoutRemainder.startsWith('::add-mask::')) output += `${stdoutRemainder}\n`;
if (flush) stdoutRemainder = '';
return output;
}

process.stdout.write = function filteredStdoutWrite(chunk, encoding, callback) {
const done = callbackFrom(encoding, callback);
const text = Buffer.isBuffer(chunk) || chunk instanceof Uint8Array
? Buffer.from(chunk).toString(typeof encoding === 'string' ? encoding : 'utf8')
: String(chunk);
const allowed = acceptedMaskCommands(text);
if (!allowed) {
if (done) process.nextTick(done);
return true;
}
return rawStdoutWrite(allowed, 'utf8', done || undefined);
};

process.stderr.write = function filteredStderrWrite(chunk, encoding, callback) {
const done = callbackFrom(encoding, callback);
if (done) process.nextTick(done);
return true;
};

process.on('uncaughtException', () => {
if (process.exitCode === undefined || process.exitCode === 0) process.exitCode = 1;
});

process.on('unhandledRejection', () => {
if (process.exitCode === undefined || process.exitCode === 0) process.exitCode = 1;
});

process.on('exit', (code) => {
const pendingMask = acceptedMaskCommands('', true);
if (pendingMask) rawStdoutWrite(pendingMask);
if (code !== 0) rawStderrWrite('GCP_AUTH_ACTION_STATUS=failed\n');
});
66 changes: 66 additions & 0 deletions scripts/inspect_gateway_connections.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

container_name="${1:-}"
api_port="${2:-}"

if [[ ! "${container_name}" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || [[ ! "${api_port}" =~ ^[0-9]{1,5}$ ]] || (( api_port < 1 || api_port > 65535 )); then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=configuration_invalid"
exit 1
fi

container_state=""
if ! container_state="$(sudo -n docker inspect --format '{{.State.Running}} {{.State.Pid}}' "${container_name}" 2>/dev/null)"; then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=container_inspection_failed"
exit 1
fi

running="${container_state%% *}"
container_pid="${container_state#* }"
if [[ "${running}" != "true" && "${running}" != "false" ]] || [[ ! "${container_pid}" =~ ^[0-9]+$ ]]; then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=container_state_invalid"
exit 1
fi
if [[ "${running}" == "true" ]] && (( container_pid <= 0 )); then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=container_pid_invalid"
exit 1
fi

listener_output=""
if [[ "${running}" == "true" ]] && ! listener_output="$(sudo -n nsenter -t "${container_pid}" -n ss -H -ltn sport = ":${api_port}" 2>/dev/null)"; then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=socket_inspection_failed"
exit 1
fi

established_output=""
if [[ "${running}" == "true" ]] && ! established_output="$(sudo -n nsenter -t "${container_pid}" -n ss -H -tn state established sport = ":${api_port}" 2>/dev/null)"; then
echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=socket_inspection_failed"
exit 1
fi

listener_count=0
established_count=0
if [[ "${running}" == "true" ]]; then
if [[ -n "${listener_output}" ]]; then
listener_count="$(printf '%s\n' "${listener_output}" | wc -l | tr -d '[:space:]')"
fi
if [[ -n "${established_output}" ]]; then
established_count="$(printf '%s\n' "${established_output}" | wc -l | tr -d '[:space:]')"
fi
fi

case "${listener_count}:${established_count}" in
*[!0-9:]* | :* | *:) echo "GATEWAY_CONNECTION_INSPECTION=blocked reason=socket_result_invalid"; exit 1 ;;
esac

observed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "GATEWAY_CONNECTION_INSPECTION=observed"
echo "GATEWAY_CONTAINER_RUNNING=${running}"
if (( listener_count > 0 )); then
echo "GATEWAY_API_LISTENER=true"
else
echo "GATEWAY_API_LISTENER=false"
fi
echo "GATEWAY_ESTABLISHED_CONNECTION_COUNT=${established_count}"
echo "GATEWAY_CONNECTION_OBSERVED_AT_UTC=${observed_at}"
echo "GATEWAY_CONNECTION_INSPECTION_LIMIT=OBSERVATION_ONLY_NO_AUTH_OR_CONCURRENCY_ASSERTION"
22 changes: 22 additions & 0 deletions scripts/parse_protected_gateway_index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

function parseProtectedGatewayIndex(raw) {
if (typeof raw !== 'string' || !/^\s*\{\s*"target_index"\s*:\s*(0|[1-3])\s*\}\s*$/.test(raw)) {
return undefined;
}

let payload;
try {
payload = JSON.parse(raw);
} catch {
return undefined;
}
if (!payload || Array.isArray(payload) || Object.keys(payload).length !== 1 ||
!Object.hasOwn(payload, 'target_index') || !Number.isInteger(payload.target_index) ||
payload.target_index < 0 || payload.target_index > 3) {
return undefined;
}
return payload.target_index;
}

module.exports = { parseProtectedGatewayIndex };
76 changes: 76 additions & 0 deletions tests/test_filter_github_action_auth_logs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const repoRoot = path.resolve(__dirname, '..');
const preload = path.join(repoRoot, 'scripts', 'filter_github_action_auth_logs.cjs');
const syntheticCode = [
"process.stdout.write('raw sdk diagnostic synthetic-token\\n');",
"process.stdout.write('::add-mask::synthetic-token\\n');",
"process.stdout.write('::error::raw sdk setup failure synthetic-token\\n');",
"console.error('raw sdk error synthetic-token');",
].join('\n');

function run(code, nodeOptions, extraEnv = {}) {
const env = { ...process.env };
if (nodeOptions === undefined) delete env.NODE_OPTIONS;
else env.NODE_OPTIONS = nodeOptions;
Object.assign(env, extraEnv);
return spawnSync(process.execPath, ['-e', code], { encoding: 'utf8', env });
}

const defaultResult = run(`${syntheticCode}\nprocess.exitCode = 0;`);
assert.equal(defaultResult.status, 0);
assert.match(defaultResult.stdout, /raw sdk diagnostic synthetic-token/);
assert.match(defaultResult.stdout, /::add-mask::synthetic-token/);
assert.match(defaultResult.stderr, /raw sdk error synthetic-token/);

const filteredSuccess = run(`${syntheticCode}\nprocess.exitCode = 0;`, `--require=${preload}`);
assert.equal(filteredSuccess.status, 0);
assert.equal(filteredSuccess.stdout, '::add-mask::synthetic-token\n');
assert.equal(filteredSuccess.stderr, '');

const filteredFailure = run(`${syntheticCode}\nprocess.exitCode = 17;`, `--require=${preload}`);
assert.equal(filteredFailure.status, 17);
assert.equal(filteredFailure.stdout, '::add-mask::synthetic-token\n');
assert.equal(filteredFailure.stderr, 'GCP_AUTH_ACTION_STATUS=failed\n');
assert.doesNotMatch(filteredFailure.stderr, /raw sdk|synthetic-token/);

const filteredThrow = run("throw new Error('synthetic-token');", `--require=${preload}`);
assert.notEqual(filteredThrow.status, 0);
assert.equal(filteredThrow.stdout, '');
assert.equal(filteredThrow.stderr, 'GCP_AUTH_ACTION_STATUS=failed\n');

const filteredRejection = run("Promise.reject(new Error('synthetic-rejection-token'));", `--require=${preload}`);
assert.notEqual(filteredRejection.status, 0);
assert.equal(filteredRejection.stdout, '');
assert.equal(filteredRejection.stderr, 'GCP_AUTH_ACTION_STATUS=failed\n');

const fileCommandDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gateway-auth-filter-'));
const envFile = path.join(fileCommandDir, 'env');
const outputFile = path.join(fileCommandDir, 'output');
const pathFile = path.join(fileCommandDir, 'path');
for (const filename of [envFile, outputFile, pathFile]) fs.writeFileSync(filename, '');
const fileCommands = run([
"const fs = require('node:fs');",
"fs.appendFileSync(process.env.GITHUB_ENV, 'FILTER_TEST_ENV=present\\n');",
"fs.appendFileSync(process.env.GITHUB_OUTPUT, 'filter_test_output=present\\n');",
"fs.appendFileSync(process.env.GITHUB_PATH, '/tmp/filter-test-bin\\n');",
"process.stdout.write('ordinary tool setup log\\n');",
].join('\n'), `--require=${preload}`, {
GITHUB_ENV: envFile,
GITHUB_OUTPUT: outputFile,
GITHUB_PATH: pathFile,
});
assert.equal(fileCommands.status, 0);
assert.equal(fileCommands.stdout, '');
assert.equal(fs.readFileSync(envFile, 'utf8'), 'FILTER_TEST_ENV=present\n');
assert.equal(fs.readFileSync(outputFile, 'utf8'), 'filter_test_output=present\n');
assert.equal(fs.readFileSync(pathFile, 'utf8'), '/tmp/filter-test-bin\n');
fs.rmSync(fileCommandDir, { recursive: true, force: true });

console.log('PASS: auth action log filter preserves add-mask, fixed failure status, and default behavior');
102 changes: 102 additions & 0 deletions tests/test_gateway_workflow_resolve.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const repoRoot = path.resolve(__dirname, '..');
const workflowPath = path.join(repoRoot, '.github', 'workflows', 'read-only-vm-metadata-diagnostic.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
const resolveBlock = workflow.slice(workflow.indexOf('name: Resolve one gateway target'));
const scriptStart = resolveBlock.indexOf('script: |');
assert.notEqual(scriptStart, -1);
const scriptLines = resolveBlock.slice(scriptStart + 'script: |'.length).replace(/^\n/, '').split('\n');
const resolveScriptLines = [];
for (const line of scriptLines) {
if (line.trim() && !line.startsWith(' ')) break;
resolveScriptLines.push(line.startsWith(' ') ? line.slice(12) : '');
}
const resolveScript = resolveScriptLines.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/);
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 \}\}/);
assert.doesNotMatch(ordinarySetup, /NODE_OPTIONS/);
const inspectedSetup = workflow.slice(workflow.indexOf('- name: Set up gcloud for passive inspection\n'), workflow.indexOf('- name: Stop if protected cloud access is unavailable\n'));
assert.match(inspectedSetup, /if: \$\{\{ inputs\.inspect_connections && steps\.auth_filtered\.outcome == 'success' \}\}/);
assert.match(inspectedSetup, /NODE_OPTIONS: --require=\$\{\{ github\.workspace \}\}\/scripts\/filter_github_action_auth_logs\.cjs/);
assert.match(workflow, /steps\.gcloud_setup_filtered\.outcome != 'success'/);

const targets = Array.from({ length: 4 }, (_, index) => ({
name: `gateway-${index}`,
gcp_project_id: `mock-project-${index}`,
gcp_workload_identity_provider: `mock-provider-${index}`,
gcp_workload_identity_service_account: `mock-service-${index}`,
gce_instance_name: `mock-gateway-${index}`,
gce_zone: 'us-central1-a',
gce_user: 'mock-user',
ssh_private_key_secret_name: 'mock-ssh-key',
container_name: `mock-container-${index}`,
mode: index === 2 ? 'live' : 'paper',
}));

function runResolve(envValues) {
const originalEnv = { ...process.env };
for (const key of [
'GITHUB_WORKSPACE', 'MATCH_CURRENT_GATEWAY', 'INSPECT_CONNECTIONS', 'MATCH_TARGETS_JSON',
'MATCHED_INDEX', 'LEGACY_TARGETS_JSON', 'SELECTED_TARGET',
]) delete process.env[key];
Object.assign(process.env, { GITHUB_WORKSPACE: repoRoot }, envValues);
const result = { outputs: {}, failure: null };
const core = {
setOutput(name, value) { result.outputs[name] = value; },
setFailed(message) { result.failure = message; },
};
try {
new Function('core', 'require', resolveScript)(core, require);
} finally {
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) delete process.env[key];
}
Object.assign(process.env, originalEnv);
}
return result;
}

const legacy = runResolve({
MATCH_CURRENT_GATEWAY: 'false',
INSPECT_CONNECTIONS: 'false',
LEGACY_TARGETS_JSON: JSON.stringify(Object.fromEntries(targets.map(({ name, ...target }) => [name, target]))),
SELECTED_TARGET: 'gateway-2',
});
assert.equal(legacy.failure, null);
const legacyMatrix = JSON.parse(legacy.outputs.matrix).include;
assert.equal(legacyMatrix.length, 1);
assert.equal(legacyMatrix[0].target_index, 2);
assert.match(legacyMatrix[0].target_digest, /^[a-f0-9]{64}$/);

const matchOnly = runResolve({
MATCH_CURRENT_GATEWAY: 'true',
INSPECT_CONNECTIONS: 'false',
MATCH_TARGETS_JSON: JSON.stringify(targets),
});
assert.equal(matchOnly.failure, null);
assert.deepEqual(JSON.parse(matchOnly.outputs.matrix).include.map((item) => item.target_index), [0, 1, 2, 3]);

const inspect = runResolve({
MATCH_CURRENT_GATEWAY: 'true',
INSPECT_CONNECTIONS: 'true',
MATCH_TARGETS_JSON: JSON.stringify(targets),
MATCHED_INDEX: '{"target_index":2}',
});
assert.equal(inspect.failure, null);
assert.deepEqual(JSON.parse(inspect.outputs.matrix).include, [{ target_index: 2, inspect_connections: true }]);

const badBinding = runResolve({
MATCH_CURRENT_GATEWAY: 'false',
INSPECT_CONNECTIONS: 'true',
LEGACY_TARGETS_JSON: JSON.stringify(targets),
});
assert.equal(badBinding.failure, 'Connection inspection requires protected current-gateway matching');

console.log('PASS: actual workflow resolve step loads parser and preserves legacy/match/inspect routing');
Loading
Loading