Skip to content
Closed
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
28 changes: 25 additions & 3 deletions scripts/production-environment-governance-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,25 @@ export function createGhSubprocessEnvironment(sourceEnvironment = process.env) {
return childEnvironment;
}

/**
* Decode GitHub CLI output without allowing replacement characters to convert
* malformed remote evidence into a different, parseable JSON document.
*
* @param {Uint8Array} value Raw subprocess bytes.
* @param {string} [label="output"] Diagnostic stream label.
* @returns {string} Exact UTF-8 text.
*/
export function decodeGhOutput(value, label = "output") {
try {
return new TextDecoder("utf-8", { fatal: true }).decode(value);
} catch {
throw new Error(`GitHub CLI returned invalid UTF-8 in ${label}.`);
}
}

function runGh(args) {
const childEnvironment = createGhSubprocessEnvironment();
const completed = spawnSync("gh", args, {
encoding: "utf8",
env: childEnvironment,
maxBuffer: MAX_GH_OUTPUT_BYTES,
shell: false,
Expand All @@ -55,11 +70,18 @@ function runGh(args) {
throw new Error(`GitHub CLI could not start: ${bound(detail)}`);
}
if (completed.status !== 0) {
const rawDetail = completed.stderr || completed.stdout || `exit ${completed.status}`;
let rawDetail;
if (completed.stderr?.length) {
rawDetail = decodeGhOutput(completed.stderr, "stderr");
} else if (completed.stdout?.length) {
rawDetail = decodeGhOutput(completed.stdout, "stdout");
} else {
rawDetail = `exit ${completed.status}`;
}
const detail = redactSensitiveValue(rawDetail, [childEnvironment.GH_TOKEN]);
throw new Error(`GitHub CLI failed: ${bound(detail)}`);
}
return completed.stdout.trim();
return decodeGhOutput(completed.stdout, "stdout").trim();
}

function collectEnvironment(repository) {
Expand Down
12 changes: 12 additions & 0 deletions test/production-environment-governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import { evaluateProductionEnvironment } from "../scripts/lib/production-environment-governance.mjs";
import {
createGhSubprocessEnvironment,
decodeGhOutput,
redactSensitiveValue,
} from "../scripts/production-environment-governance-audit.mjs";

Expand Down Expand Up @@ -110,6 +111,17 @@ describe("production environment governance", () => {
});
});

it("rejects malformed UTF-8 from the GitHub CLI instead of replacement-decoding evidence", () => {
expect(() => decodeGhOutput(Uint8Array.from([0x7b, 0x22, 0xff, 0x22, 0x7d]), "stdout"))
.toThrow("GitHub CLI returned invalid UTF-8 in stdout.");
});

it("decodes valid UTF-8 GitHub CLI bytes exactly", () => {
const bytes = new TextEncoder().encode('{"name":"production"}\n');

expect(decodeGhOutput(bytes, "stdout")).toBe('{"name":"production"}\n');
});

it("redacts the explicit GitHub token before child diagnostics can be retained", () => {
const token = "read-only-github-token";
const diagnostic = `gh failed with ${token}; retry also exposed ${token}`;
Expand Down
Loading