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
19 changes: 19 additions & 0 deletions test/production-environment-governance-utf8.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";

import { decodeGhOutput } from "../scripts/production-environment-governance-audit.mjs";

describe("production environment governance GitHub CLI UTF-8 boundary", () => {
it("rejects malformed UTF-8 instead of replacement-decoding production 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 bytes exactly", () => {
const bytes = new TextEncoder().encode('{"name":"production"}\n');
expect(decodeGhOutput(bytes, "stdout")).toBe('{"name":"production"}\n');
});
});
Loading