diff --git a/packages/opencode/src/tool/auditor.ts b/packages/opencode/src/tool/auditor.ts new file mode 100644 index 000000000000..a31155abcd39 --- /dev/null +++ b/packages/opencode/src/tool/auditor.ts @@ -0,0 +1,37 @@ +/** + * ShellAuditor — Shell environment credential leak auditor + * + * Redacts credential env vars before shell execution. + */ + +const CREDENTIAL_KEYS: RegExp[] = [ + /^(AWS_ACCESS_KEY|AWS_SECRET|AWS_SESSION_TOKEN)$/i, + /^(GITHUB_TOKEN|GH_TOKEN|GH_ENTERPRISE_TOKEN)$/i, + /^(DOCKER_PASSWORD|DOCKER_TOKEN)$/i, + /^(NPM_TOKEN|NPM_AUTH_TOKEN)$/i, + /^(PYPI_TOKEN|PYPI_PASSWORD)$/i, + /^(ANTHROPIC_API_KEY|OPENAI_API_KEY|COHERE_API_KEY)$/i, + /^(DEEPSEEK_API_KEY|HUOSHAN_API_KEY)$/i, + /.*(_PASSWORD|_SECRET|_TOKEN|_KEY)$/i, +] + +export interface AuditResult { + redacted: string[] + warning?: string +} + +export function shellAudit(env: Record): AuditResult { + const redacted: string[] = [] + for (const key of Object.keys(env)) { + for (const pattern of CREDENTIAL_KEYS) { + if (pattern.test(key) && env[key] && env[key].length > 8) { + redacted.push(key) + env[key] = "[REDACTED]" + } + } + } + return { + redacted, + warning: redacted.length ? `Redacted credential env vars: ${redacted.join(", ")}` : undefined, + } +} diff --git a/packages/opencode/test/tool/auditor.test.ts b/packages/opencode/test/tool/auditor.test.ts new file mode 100644 index 000000000000..cf61543e6d79 --- /dev/null +++ b/packages/opencode/test/tool/auditor.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test" +import { shellAudit } from "../../src/tool/auditor" + +describe("shellAudit", () => { + test("redacts DEEPSEEK_API_KEY", () => { + const env = { PATH: "/usr/bin", DEEPSEEK_API_KEY: "sk-1234567890abcdef" } + const r = shellAudit(env) + expect(r.redacted).toContain("DEEPSEEK_API_KEY") + expect(env.DEEPSEEK_API_KEY).toBe("[REDACTED]") + expect(env.PATH).toBe("/usr/bin") + }) + test("redacts GITHUB_TOKEN", () => { + const env = { GITHUB_TOKEN: "ghp_1234567890abcdef" } + shellAudit(env) + expect(env.GITHUB_TOKEN).toBe("[REDACTED]") + }) + test("does not redact short values", () => { + const env = { TEST_KEY: "short" } + shellAudit(env) + expect(env.TEST_KEY).toBe("short") + }) + test("safe env vars untouched", () => { + const env = { PATH: "/bin", HOME: "/home", USER: "test" } + shellAudit(env) + expect(env.PATH).toBe("/bin") + expect(env.HOME).toBe("/home") + }) + test("warning generated for redactions", () => { + const env = { GITHUB_TOKEN: "ghp_1234567890ab" } + const r = shellAudit(env) + expect(r.warning).toContain("GITHUB_TOKEN") + }) + test("no warning when clean", () => { + const env = { PATH: "/bin" } + const r = shellAudit(env) + expect(r.warning).toBeUndefined() + }) +})