From e76d8be9023d5dfb344890cf7250e7f17ee65c04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:04:02 +0900 Subject: [PATCH 1/5] test(security): require least-authority gh subprocess env --- ...hourly-commercial-readiness-script.test.ts | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/hourly-commercial-readiness-script.test.ts b/test/hourly-commercial-readiness-script.test.ts index 6da89078d..6d4b275c6 100644 --- a/test/hourly-commercial-readiness-script.test.ts +++ b/test/hourly-commercial-readiness-script.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { + createGhSubprocessEnvironment, flattenArrayPages, hasActiveNoemaReviewRun, latestCheckRunsBySuite, @@ -242,11 +243,42 @@ describe("hourly commercial-readiness GitHub adapter", () => { ).toBe(false); }); + it("passes only explicit GitHub CLI authority into child processes", () => { + expect(createGhSubprocessEnvironment({ + PATH: "/trusted/bin", + GH_TOKEN: "read-only-maintainer-token", + GITHUB_TOKEN: "ambient-workflow-token", + NVIDIA_NIM_API_KEY: "model-secret", + NOEMA_MAINTAINER_APP_PRIVATE_KEY: "maintainer-private-key", + NOEMA_REVIEWER_APP_PRIVATE_KEY: "reviewer-private-key", + NOEMA_REVIEWER_LOGIN: "reviewer[bot]", + CLOUDFLARE_API_TOKEN: "cloudflare-secret", + HTTPS_PROXY: "http://proxy.invalid", + HTTP_PROXY: "http://proxy.invalid", + ALL_PROXY: "socks5://proxy.invalid", + HOME: "/credential-bearing-home", + NODE_OPTIONS: "--require /tmp/preload.cjs", + NOEMA_MAINTENANCE_ENABLED: "true", + })).toEqual({ + GH_HOST: "github.com", + NO_COLOR: "1", + PATH: "/trusted/bin", + GH_TOKEN: "read-only-maintainer-token", + }); + + expect(createGhSubprocessEnvironment({})).toEqual({ + GH_HOST: "github.com", + NO_COLOR: "1", + }); + }); + it("uses shell-free complete pagination and exact-head write contracts", () => { const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8"); expect(script).toContain('spawnSync("gh"'); expect(script).toContain("shell: false"); + expect(script).toContain("env: createGhSubprocessEnvironment()"); + expect(script).not.toContain("env: process.env"); expect(script).toContain('"--paginate", "--slurp"'); expect(script).toContain("pulls?state=open&per_page=100"); expect(script).toContain("check-runs?filter=all&per_page=100"); @@ -265,7 +297,7 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(script).toContain("live?.head?.repo?.full_name !== repository"); }); - it("writes a bounded report and post-action queue outputs without exposing tokens", () => { + it("writes a bounded report and post-action queue outputs without embedding ambient token names", () => { const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8"); expect(script).toContain("open_pull_request_count="); @@ -273,7 +305,6 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(script).toContain("report_path="); expect(script).toContain("remainingOpenPullRequestCount"); expect(script).toContain("MAX_ERROR_CHARS"); - expect(script).not.toContain("GH_TOKEN"); expect(script).not.toContain("GITHUB_TOKEN"); }); From 05d6a0ae0f2c9d59d2557679c25db0ddbcdfa822 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:06:29 +0900 Subject: [PATCH 2/5] fix(security): isolate hourly gh subprocess authority --- scripts/hourly-commercial-readiness.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/hourly-commercial-readiness.mjs b/scripts/hourly-commercial-readiness.mjs index 821aa6628..2880ffde8 100755 --- a/scripts/hourly-commercial-readiness.mjs +++ b/scripts/hourly-commercial-readiness.mjs @@ -29,9 +29,24 @@ function bound(value, limit = MAX_REPORT_DETAIL_CHARS) { return text.length <= limit ? text : `${text.slice(0, limit)}…`; } +export function createGhSubprocessEnvironment(sourceEnvironment = process.env) { + const childEnvironment = { + GH_HOST: "github.com", + NO_COLOR: "1", + }; + if (typeof sourceEnvironment.PATH === "string" && sourceEnvironment.PATH.length > 0) { + childEnvironment.PATH = sourceEnvironment.PATH; + } + if (typeof sourceEnvironment.GH_TOKEN === "string" && sourceEnvironment.GH_TOKEN.length > 0) { + childEnvironment.GH_TOKEN = sourceEnvironment.GH_TOKEN; + } + return childEnvironment; +} + function runGh(args, { input } = {}) { const completed = spawnSync("gh", args, { encoding: "utf8", + env: createGhSubprocessEnvironment(), input, maxBuffer: MAX_GH_OUTPUT_BYTES, shell: false, From 55a11db6efa3f004bedbf0b9929edbd14eef7094 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:18:13 +0900 Subject: [PATCH 3/5] test(security): require maintainer token diagnostic redaction --- ...hourly-commercial-readiness-script.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/hourly-commercial-readiness-script.test.ts b/test/hourly-commercial-readiness-script.test.ts index 6d4b275c6..573d81270 100644 --- a/test/hourly-commercial-readiness-script.test.ts +++ b/test/hourly-commercial-readiness-script.test.ts @@ -7,6 +7,7 @@ import { latestCheckRunsBySuite, latestReviewStates, parseNoemaReviewDecision, + redactSensitiveValue, } from "../scripts/hourly-commercial-readiness.mjs"; const repository = "ContextualWisdomLab/noema"; @@ -247,6 +248,8 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(createGhSubprocessEnvironment({ PATH: "/trusted/bin", GH_TOKEN: "read-only-maintainer-token", + GH_HOST: "evil.example", + NO_COLOR: "0", GITHUB_TOKEN: "ambient-workflow-token", NVIDIA_NIM_API_KEY: "model-secret", NOEMA_MAINTAINER_APP_PRIVATE_KEY: "maintainer-private-key", @@ -272,13 +275,25 @@ describe("hourly commercial-readiness GitHub adapter", () => { }); }); + it("redacts an explicit maintainer token before child diagnostics can reach retained outputs", () => { + const token = "read-only-maintainer-token"; + const detail = `gh failed with ${token}; retry also exposed ${token}`; + + expect(redactSensitiveValue(detail, [token])).toBe( + "gh failed with [REDACTED]; retry also exposed [REDACTED]", + ); + expect(redactSensitiveValue(detail, ["", null, undefined, token])).not.toContain(token); + expect(redactSensitiveValue("safe diagnostic", [])).toBe("safe diagnostic"); + }); + it("uses shell-free complete pagination and exact-head write contracts", () => { const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8"); expect(script).toContain('spawnSync("gh"'); expect(script).toContain("shell: false"); - expect(script).toContain("env: createGhSubprocessEnvironment()"); + expect(script).toContain("env: childEnvironment"); expect(script).not.toContain("env: process.env"); + expect(script).toContain("redactSensitiveValue(detail, [childEnvironment.GH_TOKEN])"); expect(script).toContain('"--paginate", "--slurp"'); expect(script).toContain("pulls?state=open&per_page=100"); expect(script).toContain("check-runs?filter=all&per_page=100"); @@ -297,7 +312,7 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(script).toContain("live?.head?.repo?.full_name !== repository"); }); - it("writes a bounded report and post-action queue outputs without embedding ambient token names", () => { + it("writes a bounded report and post-action queue outputs without ambient credential payloads", () => { const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8"); expect(script).toContain("open_pull_request_count="); @@ -306,6 +321,7 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(script).toContain("remainingOpenPullRequestCount"); expect(script).toContain("MAX_ERROR_CHARS"); expect(script).not.toContain("GITHUB_TOKEN"); + expect(script).not.toContain("read-only-maintainer-token"); }); it("documents the operator contract and buyer-visible governance boundaries", () => { From 1ef50c7dde4568371db1dfe60d7560622c82f227 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:19:40 +0900 Subject: [PATCH 4/5] fix(security): redact maintainer token from gh diagnostics --- scripts/hourly-commercial-readiness.mjs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/hourly-commercial-readiness.mjs b/scripts/hourly-commercial-readiness.mjs index 2880ffde8..bbde521f4 100755 --- a/scripts/hourly-commercial-readiness.mjs +++ b/scripts/hourly-commercial-readiness.mjs @@ -29,6 +29,17 @@ function bound(value, limit = MAX_REPORT_DETAIL_CHARS) { return text.length <= limit ? text : `${text.slice(0, limit)}…`; } +export function redactSensitiveValue(value, sensitiveValues = []) { + let redacted = String(value ?? ""); + for (const sensitiveValue of Array.isArray(sensitiveValues) ? sensitiveValues : []) { + if (typeof sensitiveValue !== "string" || sensitiveValue.length === 0) { + continue; + } + redacted = redacted.split(sensitiveValue).join("[REDACTED]"); + } + return redacted; +} + export function createGhSubprocessEnvironment(sourceEnvironment = process.env) { const childEnvironment = { GH_HOST: "github.com", @@ -44,18 +55,21 @@ export function createGhSubprocessEnvironment(sourceEnvironment = process.env) { } function runGh(args, { input } = {}) { + const childEnvironment = createGhSubprocessEnvironment(); const completed = spawnSync("gh", args, { encoding: "utf8", - env: createGhSubprocessEnvironment(), + env: childEnvironment, input, maxBuffer: MAX_GH_OUTPUT_BYTES, shell: false, }); if (completed.error) { - throw new Error(`GitHub CLI could not start: ${bound(completed.error.message, MAX_ERROR_CHARS)}`); + const detail = redactSensitiveValue(completed.error.message, [childEnvironment.GH_TOKEN]); + throw new Error(`GitHub CLI could not start: ${bound(detail, MAX_ERROR_CHARS)}`); } if (completed.status !== 0) { - const detail = completed.stderr || completed.stdout || `exit ${completed.status}`; + const rawDetail = completed.stderr || completed.stdout || `exit ${completed.status}`; + const detail = redactSensitiveValue(rawDetail, [childEnvironment.GH_TOKEN]); throw new Error(`GitHub CLI failed: ${bound(detail, MAX_ERROR_CHARS)}`); } return completed.stdout.trim(); From 11618bbe27b40fcc06a1b2786d421b080acb08a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 07:23:53 +0900 Subject: [PATCH 5/5] test(security): bind gh diagnostic redaction contracts --- test/hourly-commercial-readiness-script.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/hourly-commercial-readiness-script.test.ts b/test/hourly-commercial-readiness-script.test.ts index 573d81270..9602dda19 100644 --- a/test/hourly-commercial-readiness-script.test.ts +++ b/test/hourly-commercial-readiness-script.test.ts @@ -293,7 +293,10 @@ describe("hourly commercial-readiness GitHub adapter", () => { expect(script).toContain("shell: false"); expect(script).toContain("env: childEnvironment"); expect(script).not.toContain("env: process.env"); - expect(script).toContain("redactSensitiveValue(detail, [childEnvironment.GH_TOKEN])"); + expect(script).toContain( + "redactSensitiveValue(completed.error.message, [childEnvironment.GH_TOKEN])", + ); + expect(script).toContain("redactSensitiveValue(rawDetail, [childEnvironment.GH_TOKEN])"); expect(script).toContain('"--paginate", "--slurp"'); expect(script).toContain("pulls?state=open&per_page=100"); expect(script).toContain("check-runs?filter=all&per_page=100");