diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 6df7d7dcac..bf078ed476 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -6034,7 +6034,7 @@ const PRODUCT_USAGE_SENSITIVE_VALUE = // Compose from the canonical scrubber in redaction.ts so this surface cannot drift from the boundary; // it already covered /root/ and /var/, and now unifies the Windows form (also accepts `C:/Users/`). const PRODUCT_USAGE_LOCAL_PATH = PUBLIC_LOCAL_PATH_SCRUB_PATTERN; -const PRODUCT_USAGE_TOKEN_VALUE = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; +const PRODUCT_USAGE_TOKEN_VALUE = /\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; const PRODUCT_USAGE_BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi; function sanitizeProductUsageMetadata(value: Record | null | undefined, actorRedactor: ProductUsageActorRedactor | null): Record { diff --git a/src/selfhost/sentry.ts b/src/selfhost/sentry.ts index 99ea4d8ab0..cf9ffda5de 100644 --- a/src/selfhost/sentry.ts +++ b/src/selfhost/sentry.ts @@ -40,6 +40,11 @@ const SECRET_VALUE = new RegExp( String.raw`gh[opsru]_[A-Za-z0-9_]{20,}`, String.raw`sk-[A-Za-z0-9_-]{20,}`, String.raw`xox[baprs]-[A-Za-z0-9-]+`, + // Gittensory's own opaque tokens (createOpaqueToken, src/auth/security.ts): gts_ is the default session-token + // prefix, orbenr_/orbsec_ are the Orb broker's enrollment id/secret (#1825) — a broker error message can quote + // these bare (no "secret"/"token"-named field for the key-based redaction above to catch), so the VALUE itself + // must be recognized here too. + String.raw`(?:gts|orbenr|orbsec)_[A-Za-z0-9_]{20,}`, String.raw`Bearer\s+[A-Za-z0-9._~+/=-]{12,}`, String.raw`-----BEGIN [^-]+ PRIVATE KEY-----[\s\S]*?-----END [^-]+ PRIVATE KEY-----`, ].join("|"), diff --git a/src/services/control-panel-roles.ts b/src/services/control-panel-roles.ts index 3063d350c7..323122c036 100644 --- a/src/services/control-panel-roles.ts +++ b/src/services/control-panel-roles.ts @@ -292,7 +292,7 @@ function isMaintainerAssociation(value: string | null | undefined): boolean { export function sanitizeRoleText(value: string): string { const redacted = value .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") - .replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "") + .replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if (/\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|payout|reward estimate|farming|private reviewability|public score estimate)\b/i.test(redacted)) return ""; return redacted.slice(0, 200); diff --git a/src/services/miner-dashboard-recommendations.ts b/src/services/miner-dashboard-recommendations.ts index 7e47d8f8bc..33b1fb5afe 100644 --- a/src/services/miner-dashboard-recommendations.ts +++ b/src/services/miner-dashboard-recommendations.ts @@ -46,7 +46,7 @@ const FORBIDDEN_PUBLIC_TEXT = // Compose the roots from the canonical PUBLIC_LOCAL_PATH_INLINE in redaction.ts (so this surface cannot drift) // while preserving this surface's own trailing class and its case-sensitive `/g` (Windows form via `[A-Z]`). const LOCAL_PATH = new RegExp(`(?:${PUBLIC_LOCAL_PATH_INLINE})[^\\s,;:)]+`, "g"); -const FORBIDDEN_TOKEN = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; +const FORBIDDEN_TOKEN = /\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; export function previousDecisionPackFromSnapshots(currentPack: ContributorDecisionPack, snapshots: SignalSnapshotRecord[]): ContributorDecisionPack | undefined { const current = asRecord(currentPack); diff --git a/src/services/weekly-value-report.ts b/src/services/weekly-value-report.ts index 8b9cbaa650..c52dadbadc 100644 --- a/src/services/weekly-value-report.ts +++ b/src/services/weekly-value-report.ts @@ -411,7 +411,7 @@ function normalizeReportDays(value: number | null | undefined): number { function sanitizeReportText(value: string): string { const redacted = value .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") - .replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_=-]{8,}/g, "") + .replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_=-]{8,}/g, "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if ( /\b(seed phrase|mnemonic|private key|raw[-\s]?trust|trust[-\s]?score|wallet|hotkey|coldkey|payout|reward(?:[-\s]?(?:estimate|prediction|claim|score|payout|risk))?|farming|private[-\s]?reviewability|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)|score[-\s]?(?:estimate|prediction|preview))\b/i.test( diff --git a/test/unit/miner-dashboard-recommendations.test.ts b/test/unit/miner-dashboard-recommendations.test.ts index 8ba2f2b5d4..cb85478c2d 100644 --- a/test/unit/miner-dashboard-recommendations.test.ts +++ b/test/unit/miner-dashboard-recommendations.test.ts @@ -340,6 +340,33 @@ describe("miner dashboard recommendation metadata", () => { expect(JSON.stringify(enriched?.rerunReasons)).not.toMatch(/\/root\/work|\/var\/log|C:\/Users\/alice|c:\\Users\\bob/); }); + // Regression (#1825): the Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), + // src/orb/broker.ts) are bare opaque tokens with no "token"/"secret"-named field to trip elsewhere — the + // rerun-reason text scrubber must recognize the orbenr_/orbsec_ shape too, not just ghp_/github_pat_/gts_. + it("redacts orbenr_/orbsec_ Orb broker tokens from rerun reasons (#1825)", () => { + const fakeEnrollId = `orbenr_${"a".repeat(20)}`; + const fakeSecret = `orbsec_${"b".repeat(20)}`; + const current = decisionPack({ + generatedAt: "2026-06-02T00:00:00.000Z", + topActions: [action()], + actionPortfolio: { + topActions: [ + { + repoFullName: "JSONbored/gittensory", + actionKind: "open_new_direct_pr", + rerunWhen: `Rerun once the broker recovers from enrollment ${fakeEnrollId} secret ${fakeSecret}.`, + }, + ], + }, + }); + + const [enriched] = buildMinerDashboardNextActions(current); + const repoStateReasons = enriched?.rerunReasons.find((group) => group.group === "repo_state")?.reasons.join(" ") ?? ""; + expect(repoStateReasons).toContain("private context"); + expect(JSON.stringify(enriched?.rerunReasons)).not.toContain(fakeEnrollId); + expect(JSON.stringify(enriched?.rerunReasons)).not.toContain(fakeSecret); + }); + it("selects the previous ready decision-pack snapshot", () => { const current = decisionPack({ generatedAt: "2026-06-02T00:00:00.000Z" }); const previous = decisionPack({ generatedAt: "2026-06-01T00:00:00.000Z" }); diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index 7d2ca7c72f..ed10809698 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -163,6 +163,13 @@ describe("sanitizeRoleText token redaction", () => { expect(sanitizeRoleText("key glpat-abcdefghij1234")).toContain(""); }); + // Regression (#1825): the Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), + // src/orb/broker.ts) must be redacted like any other opaque token when it appears bare in role text. + it("redacts orbenr_ and orbsec_ prefixed Orb broker tokens", () => { + expect(sanitizeRoleText(`enrollment orbenr_${"a".repeat(20)}`)).toContain(""); + expect(sanitizeRoleText(`secret orbsec_${"b".repeat(20)}`)).toContain(""); + }); + it("redacts Bearer authorization tokens", () => { const result = sanitizeRoleText("Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.abc"); expect(result).toBe("Authorization: Bearer "); diff --git a/test/unit/product-usage.test.ts b/test/unit/product-usage.test.ts index e3044ce7de..606a84c3ac 100644 --- a/test/unit/product-usage.test.ts +++ b/test/unit/product-usage.test.ts @@ -201,6 +201,33 @@ describe("product usage events", () => { expect(JSON.stringify(row.metadata)).not.toMatch(/\/Users|\/root\/|\/var\/|github_pat|ghp_|source code|private patch|trustScore|wallet/i); }); + // Regression (#1825): the Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), + // src/orb/broker.ts) are bare opaque tokens with no "token"/"secret"-named field to trip the key-based + // redaction above when they appear as a plain VALUE (e.g. quoted inside an error-message string embedded + // in metadata) — PRODUCT_USAGE_TOKEN_VALUE must recognize the orbenr_/orbsec_ shape too, or it survives + // into persisted telemetry verbatim. + it("redacts bare Orb broker enrollment id/secret values from persisted telemetry", async () => { + const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "fixed-test-salt" }); + const fakeEnrollId = `orbenr_${"a".repeat(64)}`; + const fakeSecret = `orbsec_${"b".repeat(64)}`; + + await recordProductUsageEvent(env, { + surface: "internal", + eventName: "command_previewed", + actor: "oktofeesh1", + metadata: { + note: `broker exchange failed for enrollment ${fakeEnrollId} secret ${fakeSecret}`, + }, + }); + + const [row] = await listProductUsageEvents(env); + expect(row).toBeDefined(); + if (!row) throw new Error("expected product usage event"); + expect(row.metadata).toMatchObject({ note: "broker exchange failed for enrollment secret " }); + expect(JSON.stringify(row.metadata)).not.toContain(fakeEnrollId); + expect(JSON.stringify(row.metadata)).not.toContain(fakeSecret); + }); + it("persists normalized role on the event row and strips private scoreability metadata", async () => { const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "fixed-test-salt" }); diff --git a/test/unit/selfhost-sentry.test.ts b/test/unit/selfhost-sentry.test.ts index 16e31ac0ef..49864fdb1c 100644 --- a/test/unit/selfhost-sentry.test.ts +++ b/test/unit/selfhost-sentry.test.ts @@ -258,6 +258,23 @@ describe("scrubEvent — redact secrets before an event leaves the box", () => { expect(ev.exception.values[0].stacktrace.frames[0].vars.safe).toBe("value"); }); + // Regression (#1825): the Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), + // src/orb/broker.ts) are bare opaque tokens with no "secret"/"token"-NAMED field for the key-based redaction + // to catch when a broker error message quotes one directly (e.g. an error string embedding the failed + // Authorization value) — the VALUE-based SECRET_VALUE pattern must recognize the orbenr_/orbsec_ shape too. + it("redacts a bare Orb enrollment id/secret value from an exception message (#1825)", () => { + const fakeEnrollId = `orbenr_${"c".repeat(64)}`; + const fakeSecret = `orbsec_${"d".repeat(64)}`; + const ev = scrubbedEvent({ + exception: { + values: [{ value: `Orb broker rejected enrollment ${fakeEnrollId} using secret ${fakeSecret}` }], + }, + }) as any; + expect(ev.exception.values[0].value).not.toContain(fakeEnrollId); + expect(ev.exception.values[0].value).not.toContain(fakeSecret); + expect(ev.exception.values[0].value).toBe("Orb broker rejected enrollment [redacted] using secret [redacted]"); + }); + it("scrubs transaction span descriptions and data before sending transaction events", () => { const queryTokenKey = fakeQueryTokenKey(); const ev = scrubbedEvent({ diff --git a/test/unit/weekly-value-report.test.ts b/test/unit/weekly-value-report.test.ts index 0969355fae..cd29834fb1 100644 --- a/test/unit/weekly-value-report.test.ts +++ b/test/unit/weekly-value-report.test.ts @@ -243,6 +243,40 @@ describe("weekly value reports", () => { expect(JSON.stringify(report)).not.toMatch(new RegExp(slackToken.slice(0, 4), "i")); }); + // Regression (#1825): the Orb broker's enrollment id/secret (createOpaqueToken("orbenr"/"orbsec"), + // src/orb/broker.ts) are bare opaque tokens that must be redacted the same way ghp_/gts_/xoxb- tokens are. + it("redacts Orb broker enrollment id/secret tokens in operator rollup dimensions (#1825)", () => { + const orbSecret = `orbsec_${"e".repeat(20)}`; + const report = buildWeeklyValueReport({ + generatedAt: "2026-06-01T12:00:00.000Z", + variant: "operator", + days: 7, + repositories: [repo("JSONbored/gittensory", true, true)], + installations: [installation(1)], + health: [health(1, "healthy")], + registry: registry([]), + scoring: scoring([]), + upstreamDrift: upstream({ status: "current", openReportCount: 0 }), + usageSummary: usageSummary({ totalEvents: 1, activeActors: 1 }), + usageRollups: [ + rollup("2026-05-31", { + totalEvents: 1, + activeActors: 1, + activeRepos: 1, + repos: [{ key: orbSecret, count: 1 }], + events: [], + surfaces: [], + commands: [], + tools: [], + }), + ], + usageRollupStatus: rollupStatus({ status: "ready" }), + }); + + expect(report.operatorDetails?.topRepos).toEqual([{ key: "", count: 1 }]); + expect(JSON.stringify(report)).not.toContain(orbSecret); + }); + it("keeps clean complete windows marked ready", () => { const report = buildWeeklyValueReport({ generatedAt: "2026-06-01T12:00:00.000Z",