diff --git a/package.json b/package.json index ade9ab5..61c069d 100644 --- a/package.json +++ b/package.json @@ -85,12 +85,14 @@ "enum": [ "debug", "info", + "warn", "error", "off" ], "markdownEnumDescriptions": [ - "Log debug, info, and errors", - "Log info and errors", + "Log debug, info, warnings, and errors", + "Log info, warnings, and errors", + "Log warnings and errors", "Log errors only", "Disable logging" ], diff --git a/src/configuration.ts b/src/configuration.ts index 720870e..1ce5802 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1002,6 +1002,7 @@ export class Configuration { ), }, }; + logger.debug("Environment:", env); // Log the extension's user configuration settings. diff --git a/src/extensionData.ts b/src/extensionData.ts index 3d9305e..dbdeac4 100644 --- a/src/extensionData.ts +++ b/src/extensionData.ts @@ -350,10 +350,12 @@ export class ExtensionData { /** * Get all extension discovery paths. + * Used for logging the paths to the output channel. * * @returns {ReadonlyMap} A read-only Map containing all extension discovery paths. */ public getAllExtensionDiscoveryPaths(): ReadonlyMap { - return this.extensionDiscoveryPaths; + // Return a new Map to prevent external mutation of the internal state. + return new Map(this.extensionDiscoveryPaths); } } diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index 6f174fb..4291e79 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -44,6 +44,7 @@ export type LanguageId = string; export const logLevels = { debug: "debug", info: "info", + warn: "warn", error: "error", off: "off", } as const; diff --git a/src/logger.ts b/src/logger.ts index 2a86931..accf73d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,3 +1,4 @@ +import * as os from "node:os"; import {OutputChannel, window} from "vscode"; import {LogLevel, logLevels} from "./interfaces/utils"; @@ -26,6 +27,14 @@ class Logger { */ private logLevel: LogLevel = "debug"; + /** + * Whether the user has already been warned that username redaction is unavailable, + * so the warning is only emitted once per session. + * + * @type {boolean} + */ + private hasWarnedAboutRedactionFailure: boolean = false; + /*********** * Methods * ***********/ @@ -121,6 +130,17 @@ class Logger { } } + /** + * Sends a warning log to the output channel. + * + * @param {string} message The message to be logged. + */ + public warn(message: string): void { + if (this.shouldLog("warn")) { + this.logMessage("WARN", message); + } + } + /** * Send an important message to the output channel. * This is a special log level that is always emitted regardless of the log level, @@ -150,8 +170,9 @@ class Logger { private shouldLog(requiredLevel: LogLevel): boolean { // Numeric weights used for level comparison. const levelWeight: Record = { - debug: 3, // Emits debug, info, and error logs - the most verbose level. - info: 2, // Emits info and error logs. + debug: 4, // Emits debug, info, warn, and error logs - the most verbose level. + info: 3, // Emits info, warn, and error logs. + warn: 2, // Emits warn and error logs. error: 1, // Emits error logs only. off: 0, // Disables all logs, except for the special "important" logs that are always emitted. }; @@ -208,7 +229,7 @@ class Logger { data = lines.join(",\n"); } - return data; + return this.redactUsername(data); } /** @@ -232,6 +253,51 @@ class Logger { return value; } + + /** + * Redact the OS username from a string, replacing it with ``, to avoid leaking + * it into debug logs that could be shared. + * + * @param {string} text The text to redact. + * @returns {string} The text with OS username replaced with ``. + */ + private redactUsername(text: string): string { + let username: string; + + // Get the current OS username using Node's userInfo() method which is + // cross-platform compatible. It can throw an error in sandboxed/remote environments where + // the username can't be determined. So catch any errors and return the original text + // if we can't get the username. + try { + username = os.userInfo().username; + } catch { + this.warnOnRedactionFailure(); + return text; + } + + // If the username is empty, return the original text. + if (!username) { + this.warnOnRedactionFailure(); + return text; + } + + // Escape special characters in the username. + const escapedUsername = username.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // Replace any occurrence of the username in the text with "", case-insensitively, and return it. + return text.replace(new RegExp(escapedUsername, "gi"), ""); + } + + /** + * Warn once per session that debug logs may not have the username redacted, + * so users don't unknowingly share it in a bug report. + */ + private warnOnRedactionFailure(): void { + if (this.hasWarnedAboutRedactionFailure) { + return; + } + this.hasWarnedAboutRedactionFailure = true; + this.warn("Could not determine OS username; debug logs may not be redacted before sharing."); + } } export const logger = new Logger();