Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | null | undefined, actorRedactor: ProductUsageActorRedactor | null): Record<string, JsonValue> {
Expand Down
5 changes: 5 additions & 0 deletions src/selfhost/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("|"),
Expand Down
2 changes: 1 addition & 1 deletion src/services/control-panel-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<redacted-path>")
.replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer <redacted-token>");
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 "<redacted>";
return redacted.slice(0, 200);
Expand Down
2 changes: 1 addition & 1 deletion src/services/miner-dashboard-recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/services/weekly-value-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<redacted-path>")
.replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\b(?:ghp_|github_pat_|gts_|orbenr_|orbsec_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer <redacted-token>");
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(
Expand Down
27 changes: 27 additions & 0 deletions test/unit/miner-dashboard-recommendations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down
7 changes: 7 additions & 0 deletions test/unit/policy-sanitizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ describe("sanitizeRoleText token redaction", () => {
expect(sanitizeRoleText("key glpat-abcdefghij1234")).toContain("<redacted-token>");
});

// 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("<redacted-token>");
expect(sanitizeRoleText(`secret orbsec_${"b".repeat(20)}`)).toContain("<redacted-token>");
});

it("redacts Bearer authorization tokens", () => {
const result = sanitizeRoleText("Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.abc");
expect(result).toBe("Authorization: Bearer <redacted-token>");
Expand Down
27 changes: 27 additions & 0 deletions test/unit/product-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <redacted-token> secret <redacted-token>" });
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" });

Expand Down
17 changes: 17 additions & 0 deletions test/unit/selfhost-sentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
34 changes: 34 additions & 0 deletions test/unit/weekly-value-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<redacted-token>", 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",
Expand Down
Loading