Skip to content

Commit 07ac0ee

Browse files
authored
chore: use redact by default (#624)
1 parent d2fa04c commit 07ac0ee

File tree

4 files changed

+100
-7
lines changed

4 files changed

+100
-7
lines changed

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export class Server {
201201

202202
if (command === "start") {
203203
event.properties.startup_time_ms = commandDuration;
204-
event.properties.read_only_mode = this.userConfig.readOnly || false;
204+
event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
205205
event.properties.disabled_tools = this.userConfig.disabledTools || [];
206206
event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
207207
}

src/telemetry/telemetry.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { EventCache } from "./eventCache.js";
88
import { detectContainerEnv } from "../helpers/container.js";
99
import type { DeviceId } from "../helpers/deviceId.js";
1010
import { EventEmitter } from "events";
11+
import { redact } from "mongodb-redact";
1112

1213
type EventResult = {
1314
success: boolean;
@@ -79,7 +80,7 @@ export class Telemetry {
7980
const [deviceIdValue, containerEnv] = await this.setupPromise;
8081

8182
this.commonProperties.device_id = deviceIdValue;
82-
this.commonProperties.is_container_env = containerEnv;
83+
this.commonProperties.is_container_env = containerEnv ? "true" : "false";
8384

8485
this.isBufferingEvents = false;
8586
}
@@ -123,7 +124,6 @@ export class Telemetry {
123124
this.events.emit("events-skipped");
124125
return;
125126
}
126-
127127
// Don't wait for events to be sent - we should not block regular server
128128
// operations on telemetry
129129
void this.emit(events);
@@ -213,14 +213,18 @@ export class Telemetry {
213213
}
214214

215215
/**
216-
* Attempts to send events through the provided API client
216+
* Attempts to send events through the provided API client.
217+
* Events are redacted before being sent to ensure no sensitive data is transmitted
217218
*/
218219
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
219220
try {
220221
await client.sendEvents(
221222
events.map((event) => ({
222223
...event,
223-
properties: { ...this.getCommonProperties(), ...event.properties },
224+
properties: {
225+
...redact(this.getCommonProperties(), this.session.keychain.allSecrets),
226+
...redact(event.properties, this.session.keychain.allSecrets),
227+
},
224228
}))
225229
);
226230
return { success: true };

src/telemetry/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export type ServerEventProperties = {
4343
reason?: string;
4444
startup_time_ms?: number;
4545
runtime_duration_ms?: number;
46-
read_only_mode?: boolean;
46+
read_only_mode?: TelemetryBoolSet;
4747
disabled_tools?: string[];
4848
confirmation_required_tools?: string[];
4949
};
@@ -97,7 +97,7 @@ export type CommonProperties = {
9797
/**
9898
* A boolean indicating whether the server is running in a container environment.
9999
*/
100-
is_container_env?: boolean;
100+
is_container_env?: TelemetryBoolSet;
101101

102102
/**
103103
* The version of the MCP client as reported by the client on session establishment.

tests/unit/telemetry.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { NullLogger } from "../../tests/utils/index.js";
99
import type { MockedFunction } from "vitest";
1010
import type { DeviceId } from "../../src/helpers/deviceId.js";
1111
import { expectDefined } from "../integration/helpers.js";
12+
import { Keychain } from "../../src/common/keychain.js";
1213

1314
// Mock the ApiClient to avoid real API calls
1415
vi.mock("../../src/common/atlas/apiClient.js");
@@ -140,6 +141,7 @@ describe("Telemetry", () => {
140141
close: vi.fn().mockResolvedValue(undefined),
141142
setAgentRunner: vi.fn().mockResolvedValue(undefined),
142143
logger: new NullLogger(),
144+
keychain: new Keychain(),
143145
} as unknown as Session;
144146

145147
telemetry = Telemetry.create(session, config, mockDeviceId, {
@@ -345,4 +347,91 @@ describe("Telemetry", () => {
345347
verifyMockCalls();
346348
});
347349
});
350+
351+
describe("when secrets are registered", () => {
352+
describe("comprehensive redaction coverage", () => {
353+
it("should redact sensitive data from CommonStaticProperties", async () => {
354+
session.keychain.register("secret-server-version", "password");
355+
session.keychain.register("secret-server-name", "password");
356+
session.keychain.register("secret-password", "password");
357+
session.keychain.register("secret-key", "password");
358+
session.keychain.register("secret-token", "password");
359+
session.keychain.register("secret-password-version", "password");
360+
361+
// Simulates sensitive data across random properties
362+
const sensitiveStaticProps = {
363+
mcp_server_version: "secret-server-version",
364+
mcp_server_name: "secret-server-name",
365+
platform: "linux-secret-password",
366+
arch: "x64-secret-key",
367+
os_type: "linux-secret-token",
368+
os_version: "secret-password-version",
369+
};
370+
371+
telemetry = Telemetry.create(session, config, mockDeviceId, {
372+
eventCache: mockEventCache as unknown as EventCache,
373+
commonProperties: sensitiveStaticProps,
374+
});
375+
376+
await telemetry.setupPromise;
377+
378+
telemetry.emitEvents([createTestEvent()]);
379+
380+
const calls = mockApiClient.sendEvents.mock.calls;
381+
expect(calls).toHaveLength(1);
382+
383+
// get event properties
384+
const sentEvent = calls[0]?.[0][0] as { properties: Record<string, unknown> };
385+
expectDefined(sentEvent);
386+
387+
const eventProps = sentEvent.properties;
388+
expect(eventProps.mcp_server_version).toBe("<password>");
389+
expect(eventProps.mcp_server_name).toBe("<password>");
390+
expect(eventProps.platform).toBe("linux-<password>");
391+
expect(eventProps.arch).toBe("x64-<password>");
392+
expect(eventProps.os_type).toBe("linux-<password>");
393+
expect(eventProps.os_version).toBe("<password>-version");
394+
});
395+
396+
it("should redact sensitive data from CommonProperties", () => {
397+
// register the common properties as sensitive data
398+
session.keychain.register("test-device-id", "password");
399+
session.keychain.register(session.sessionId, "password");
400+
401+
telemetry.emitEvents([createTestEvent()]);
402+
403+
const calls = mockApiClient.sendEvents.mock.calls;
404+
expect(calls).toHaveLength(1);
405+
406+
// get event properties
407+
const sentEvent = calls[0]?.[0][0] as { properties: Record<string, unknown> };
408+
expectDefined(sentEvent);
409+
410+
const eventProps = sentEvent.properties;
411+
412+
expect(eventProps.device_id).toBe("<password>");
413+
expect(eventProps.session_id).toBe("<password>");
414+
});
415+
416+
it("should redact sensitive data that is added to events", () => {
417+
session.keychain.register("test-device-id", "password");
418+
session.keychain.register(session.sessionId, "password");
419+
session.keychain.register("test-component", "password");
420+
421+
telemetry.emitEvents([createTestEvent()]);
422+
423+
const calls = mockApiClient.sendEvents.mock.calls;
424+
expect(calls).toHaveLength(1);
425+
426+
// get event properties
427+
const sentEvent = calls[0]?.[0][0] as { properties: Record<string, unknown> };
428+
expectDefined(sentEvent);
429+
430+
const eventProps = sentEvent.properties;
431+
expect(eventProps.device_id).toBe("<password>");
432+
expect(eventProps.session_id).toBe("<password>");
433+
expect(eventProps.component).toBe("<password>");
434+
});
435+
});
436+
});
348437
});

0 commit comments

Comments
 (0)