Skip to content
Open
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: 2 additions & 0 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ esbuild
"keytar",
// TypeScript is a peer dependency and should not be bundled
"typescript",
// pino-pretty uses worker threads and must be external
"pino-pretty",
],
})
.catch((e) => {
Expand Down
5 changes: 5 additions & 0 deletions require-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
* CommonJS module loader.
*/
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

globalThis.require = createRequire(import.meta.url);
globalThis.__filename = fileURLToPath(import.meta.url);
globalThis.__dirname = dirname(globalThis.__filename);
24 changes: 19 additions & 5 deletions src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createActionsCommand } from "./cli/commands/actions";
import { createApplicationCommand } from "./cli/commands/application";
import { createWebhookCommand } from "./cli/commands/webhook";
import { validateEnvironment } from "./core/environment";
import { setDebugMode } from "./services/logger";
import { setVerbosityLevel } from "./services/logger";

/**
* Traditional CLI entry point using commander.js
Expand All @@ -26,7 +26,9 @@ export function createCliProgram(): Command {
"-e, --env <environment>",
"Set the target environment for commands (ote, prod)",
)
.option("--debug", "Enable debug logging for HTTP requests and responses")
.option("-v, --verbose", "Enable verbose output (use -vv for full details)", (_, prev) => prev + 1, 0)
.option("--info", "Enable basic verbose output (same as -v)")
.option("--debug", "Enable full verbose output (same as -vv)")
.addHelpText(
"after",
`
Expand All @@ -40,13 +42,25 @@ Example Usage:
$ godaddy webhook events`,
);

// Global pre-action hook to validate environment option and set debug mode
// Global pre-action hook to validate environment option and set verbosity level
program.hook("preAction", async (thisCommand, actionCommand) => {
const options = thisCommand.opts();

// Enable debug logging if --debug flag is set
let verbosity = options.verbose || 0;
if (options.info) {
verbosity = Math.max(verbosity, 1);
}
if (options.debug) {
setDebugMode(true);
verbosity = Math.max(verbosity, 2);
}

if (verbosity > 0) {
setVerbosityLevel(verbosity);
if (verbosity === 1) {
console.error(" (verbose output enabled)");
} else {
console.error(" (verbose output enabled: full details)");
}
}

if (options.env) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getApiUrl,
getClientId,
} from "./environment";
import { loggedFetch } from "../services/logger";

const KEYCHAIN_SERVICE = "godaddy-cli";
const PORT = 7443;
Expand Down Expand Up @@ -90,7 +91,7 @@ export async function authLogin(): Promise<CmdResult<AuthResult>> {
);
}

const tokenResponse = await fetch(oauthTokenUrl, {
const tokenResponse = await loggedFetch(oauthTokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down
119 changes: 99 additions & 20 deletions src/services/logger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import pino from "pino";
import pinoPretty from "pino-pretty";

// Global debug state
let isDebugEnabled = false;
// Global verbosity level: 0 = none, 1 = basic, 2 = full
let verbosityLevel = 0;

// Configure logger based on environment and debug flag
// Configure logger based on environment and verbosity level
const createLogger = () => {
const isDev = process.env.NODE_ENV === "development";
const level = isDebugEnabled ? "debug" : "info";
const level = verbosityLevel > 0 ? "debug" : "info";

const redact = {
paths: [
Expand All @@ -18,19 +19,19 @@
censor: "[REDACTED]",
};

if (isDev || isDebugEnabled) {
return pino({
level,
redact,
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "HH:MM:ss",
ignore: "pid,hostname",
},
},
if (isDev || verbosityLevel > 0) {
const prettyStream = pinoPretty({
colorize: true,
translateTime: "HH:MM:ss",
ignore: "pid,hostname",
});
return pino(
{
level,
redact,
},
prettyStream,
);
}

return pino({
Expand All @@ -41,8 +42,8 @@

let logger = createLogger();

export const setDebugMode = (enabled: boolean) => {
isDebugEnabled = enabled;
export const setVerbosityLevel = (level: number) => {
verbosityLevel = level;
logger = createLogger();
};

Expand All @@ -55,17 +56,19 @@
headers?: Record<string, string>;
body?: unknown;
}) => {
if (isDebugEnabled) {
if (verbosityLevel >= 2) {
logger.debug(
{
type: "http_request",
method: options.method,
url: options.url,
headers: options.headers,
body: options.body,

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
a call to getPassword
as clear text.
},
`→ ${options.method} ${options.url}`,

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to oauthTokenUrl
as clear text.
);
} else if (verbosityLevel === 1) {
logger.debug(`→ ${options.method} ${options.url}`);

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to oauthTokenUrl
as clear text.

Copilot Autofix

AI 4 days ago

In general: avoid logging full URLs that may contain sensitive information (query strings, path segments) and avoid logging any token/credential endpoints at low verbosity. Instead, log only high‑level, non‑sensitive details (HTTP method, status code, maybe a truncated or sanitized URL) and reserve detailed information for higher verbosity with explicit redaction.

Best targeted fix here without changing existing behavior more than necessary:

  • For verbosityLevel === 1, stop including the full options.url in the log messages in logHttpRequest and logHttpResponse. We can still log the method and, for responses, the status and duration, which preserves the intent of “basic” verbosity while eliminating the sensitive URL from that path.
  • For verbosityLevel >= 2, we already log a structured object including url, and that is where advanced debugging is expected; we leave that as‑is since the reported sink is the string interpolation at line 71, not the structured object.

Concretely:

  • In src/services/logger.ts:
    • Change logHttpRequest so that the branch verbosityLevel === 1 logs only the HTTP method (or a generic message) instead of → ${options.method} ${options.url}.
    • Change logHttpResponse so that the branch verbosityLevel === 1 logs only the status, method, and optional duration, but not options.url.

No new imports or helpers are required; we only adjust the log message strings.

Suggested changeset 1
src/services/logger.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/logger.ts b/src/services/logger.ts
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -68,7 +68,8 @@
 			`→ ${options.method} ${options.url}`,
 		);
 	} else if (verbosityLevel === 1) {
-		logger.debug(`→ ${options.method} ${options.url}`);
+		// Avoid logging full URL at basic verbosity to prevent leaking sensitive data
+		logger.debug(`→ ${options.method}`);
 	}
 };
 
@@ -98,8 +99,9 @@
 			}`,
 		);
 	} else if (verbosityLevel === 1) {
+		// Avoid logging full URL at basic verbosity to prevent leaking sensitive data
 		logger.debug(
-			`← ${options.status} ${options.method} ${options.url} ${
+			`← ${options.status} ${options.method} ${
 				options.duration ? `(${options.duration}ms)` : ""
 			}`,
 		);
EOF
@@ -68,7 +68,8 @@
`→ ${options.method} ${options.url}`,
);
} else if (verbosityLevel === 1) {
logger.debug(`→ ${options.method} ${options.url}`);
// Avoid logging full URL at basic verbosity to prevent leaking sensitive data
logger.debug(`→ ${options.method}`);
}
};

@@ -98,8 +99,9 @@
}`,
);
} else if (verbosityLevel === 1) {
// Avoid logging full URL at basic verbosity to prevent leaking sensitive data
logger.debug(
`← ${options.status} ${options.method} ${options.url} ${
`← ${options.status} ${options.method} ${
options.duration ? `(${options.duration}ms)` : ""
}`,
);
Copilot is powered by AI and may make mistakes. Always verify output.
}
};

Expand All @@ -78,21 +81,97 @@
body?: unknown;
duration?: number;
}) => {
if (isDebugEnabled) {
if (verbosityLevel >= 2) {
logger.debug(
{
type: "http_response",
method: options.method,
url: options.url,
status: options.status,
statusText: options.statusText,
headers: options.headers,
body: options.body,
duration: options.duration,
},

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to oauthTokenUrl
as clear text.
`← ${options.status} ${options.method} ${options.url} ${
options.duration ? `(${options.duration}ms)` : ""
}`,

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to oauthTokenUrl
as clear text.
);
} else if (verbosityLevel === 1) {
logger.debug(
`← ${options.status} ${options.method} ${options.url} ${
options.duration ? `(${options.duration}ms)` : ""
}`,
Comment on lines +102 to +104

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This logs sensitive data returned by
an access to oauthTokenUrl
as clear text.

Copilot Autofix

AI 4 days ago

In general, to fix clear-text logging of sensitive information you must either (1) avoid logging sensitive values at all, or (2) ensure they are consistently redacted before being written to logs. In this code, the problem arises because logHttpResponse always includes the full options.url in the log message string; for calls like the OAuth token exchange, that URL is considered sensitive by the analysis and should not be logged verbatim.

The minimal, behavior-preserving fix is to treat URLs as potentially sensitive and avoid logging them in the human-readable message string for responses (and, ideally, for requests as well), while still keeping them in the structured log object for high-verbosity, where operators can decide whether such logs are acceptable. Specifically:

  • Leave the structured object fields (url, headers, body, etc.) as-is for verbosity level ≥ 2, since that’s already subject to redaction logic and is clearly marked as debug-level logging.
  • Change the string passed as the second argument to logger.debug in logHttpResponse (both branches: verbosityLevel >= 2 and verbosityLevel === 1) so that it no longer includes ${options.url}.
  • Optionally, for symmetry and defense in depth, also remove the URL from the logHttpRequest message string, although CodeQL only flagged the response path.

Concretely, in src/services/logger.ts:

  • In logHttpResponse, replace:
    • The debug message template at lines 96–98 with one that omits ${options.url} but keeps status, method, and duration.
    • The debug message template at lines 101–104 similarly.
      No new methods or imports are required; we only adjust the formatting strings.

Suggested changeset 1
src/services/logger.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/logger.ts b/src/services/logger.ts
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -93,13 +93,13 @@
 				body: options.body,
 				duration: options.duration,
 			},
-			`← ${options.status} ${options.method} ${options.url} ${
+			`← ${options.status} ${options.method} ${
 				options.duration ? `(${options.duration}ms)` : ""
 			}`,
 		);
 	} else if (verbosityLevel === 1) {
 		logger.debug(
-			`← ${options.status} ${options.method} ${options.url} ${
+			`← ${options.status} ${options.method} ${
 				options.duration ? `(${options.duration}ms)` : ""
 			}`,
 		);
EOF
@@ -93,13 +93,13 @@
body: options.body,
duration: options.duration,
},
`← ${options.status} ${options.method} ${options.url} ${
`← ${options.status} ${options.method} ${
options.duration ? `(${options.duration}ms)` : ""
}`,
);
} else if (verbosityLevel === 1) {
logger.debug(
`← ${options.status} ${options.method} ${options.url} ${
`← ${options.status} ${options.method} ${
options.duration ? `(${options.duration}ms)` : ""
}`,
);
Copilot is powered by AI and may make mistakes. Always verify output.
);
}
};

const redactSensitiveFields = (obj: unknown): unknown => {
if (typeof obj !== "object" || obj === null) {
return obj;
}

if (Array.isArray(obj)) {
return obj.map(redactSensitiveFields);
}

const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "access_token" || key === "accessToken") {
result[key] = "[REDACTED]";
} else if (typeof value === "object" && value !== null) {
result[key] = redactSensitiveFields(value);
} else {
result[key] = value;
}
}
return result;
};

export const loggedFetch = async (
url: string,
init?: RequestInit,
): Promise<Response> => {
const method = init?.method ?? "(unknown)";
const headers = init?.headers;
const body = init?.body;

logHttpRequest({
method,
url,
headers: headers as Record<string, string>,
body:
body instanceof URLSearchParams
? Object.fromEntries(body.entries())
: body,
});

const startTime = Date.now();
const response = await fetch(url, init);
const duration = Date.now() - startTime;

let responseBody: unknown;
if (verbosityLevel >= 2 && response.ok) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
const clonedResponse = response.clone();
try {
const jsonBody = await clonedResponse.json();
responseBody = redactSensitiveFields(jsonBody);
} catch {
responseBody = undefined;
}
}
}

logHttpResponse({
method,
url,
status: response.status,
statusText: response.statusText,
body: responseBody,
duration,
});

return response;
};
Loading