diff --git a/scripts/workflow-registry-audit.mjs b/scripts/workflow-registry-audit.mjs index 92e9da706..b861786e6 100644 --- a/scripts/workflow-registry-audit.mjs +++ b/scripts/workflow-registry-audit.mjs @@ -389,6 +389,7 @@ function sanitizeDiagnosticDetail(error) { /\b((?:GH|GITHUB|ACCESS|AUTH|ID|REFRESH)?_?TOKEN)\s*=\s*[^\s,;]+/gi, `$1=${REDACTED}`, ) + .replace(/\bgithub_pat_[A-Za-z0-9_]+\b/g, REDACTED) .replace(/\bgh[pousr]_[A-Za-z0-9_]+\b/g, REDACTED) .slice(0, MAX_DIAGNOSTIC_DETAIL_LENGTH); } @@ -523,4 +524,4 @@ export async function collectWorkflowRegistryAudit(input) { } return result; -} \ No newline at end of file +} diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index a9375da78..9d3579736 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -40,6 +40,7 @@ function boundedDiagnostic(value) { const cleaned = String(value ?? "") .replace(/[\u0000-\u001f\u007f]/g, "") .replace(/\bbearer\s+\S+/gi, "Bearer [REDACTED]") + .replace(/\bgithub_pat_[A-Za-z0-9_]+\b/g, "[REDACTED]") .replace(/\bgh[pousr]_[A-Za-z0-9_]+\b/g, "[REDACTED]") .trim(); return cleaned.length <= 2_048 ? cleaned : `${cleaned.slice(0, 2_048)}…`; diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index 3d79669ed..ba18afddd 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -156,4 +156,32 @@ describe("live workflow-registry collector", () => { http_status: 403, })); }); + + it("redacts fine-grained GitHub tokens from collection failures", async () => { + const leakedToken = "github_pat_11EXAMPLE_secretmaterial"; + const ghJson = async (endpoint: string) => { + if (endpoint === "repos/ContextualWisdomLab/noema/branches/main") { + return { commit: { sha: mainSha } }; + } + if (endpoint.startsWith("repos/ContextualWisdomLab/noema/git/trees/")) { + return { truncated: false, tree: [] }; + } + if (endpoint.includes("/actions/workflows?")) { + throw Object.assign(new Error(`HTTP 403 token=${leakedToken}`), { status: 403 }); + } + throw new Error(`unexpected endpoint ${endpoint}`); + }; + + const result = await collectLiveWorkflowRegistryAudit({ + repository: "ContextualWisdomLab/noema", + defaultBranch: "main", + ghJson, + now: () => observedAt, + }); + + const detail = String(result.failures[0]?.detail ?? ""); + expect(result.status).toBe("FAIL"); + expect(detail).toContain("[REDACTED]"); + expect(detail).not.toContain(leakedToken); + }); });