Skip to content

Commit

Permalink
fix: remove tokens in unhandled rejection log
Browse files Browse the repository at this point in the history
  • Loading branch information
KuznetsovRoman committed Jan 29, 2025
1 parent a1324da commit 62abee9
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 7 deletions.
8 changes: 4 additions & 4 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from "node:path";
import util from "node:util";
import { Command } from "@gemini-testing/commander";

import defaults from "../config/defaults";
Expand All @@ -8,6 +7,7 @@ import { Testplane } from "../testplane";
import pkg from "../../package.json";
import logger from "../utils/logger";
import { shouldIgnoreUnhandledRejection } from "../utils/errors";
import { utilInspectSafe } from "../utils/secret-replacer";
import { withCommonCliOptions, collectCliValues, handleRequires } from "../utils/cli";
import { CliCommands } from "./constants";

Expand All @@ -16,7 +16,7 @@ export type TestplaneRunOpts = { cliName?: string };
let testplane: Testplane;

process.on("uncaughtException", err => {
logger.error(util.inspect(err));
logger.error(utilInspectSafe(err));
process.exit(1);
});

Expand All @@ -28,8 +28,8 @@ process.on("unhandledRejection", (reason, p) => {

const error = [
`Unhandled Rejection in testplane:master:${process.pid}:`,
`Promise: ${util.inspect(p)}`,
`Reason: ${util.inspect(reason)}`,
`Promise: ${utilInspectSafe(p)}`,
`Reason: ${utilInspectSafe(reason)}`,
].join("\n");

if (testplane) {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/processor.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use strict";

const _ = require("lodash");
const util = require("util");
const { WORKER_UNHANDLED_REJECTION } = require("../constants/process-messages");
const logger = require("./logger");
const ipc = require("./ipc");
const { shouldIgnoreUnhandledRejection } = require("./errors");
const { utilInspectSafe } = require("./secret-replacer");

process.on("unhandledRejection", (reason, p) => {
if (shouldIgnoreUnhandledRejection(reason)) {
Expand All @@ -15,8 +15,8 @@ process.on("unhandledRejection", (reason, p) => {

const error = [
`Unhandled Rejection in testplane:worker:${process.pid}:`,
`Promise: ${util.inspect(p)}`,
`Reason: ${util.inspect(reason)}`,
`Promise: ${utilInspectSafe(p)}`,
`Reason: ${utilInspectSafe(reason)}`,
].join("\n");

ipc.emit(WORKER_UNHANDLED_REJECTION, { error });
Expand Down
55 changes: 55 additions & 0 deletions src/utils/secret-replacer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { inspect } from "node:util";

const secretPatterns = {
BEARER_TOKEN: /Bearer [A-Za-z0-9-._~+/]{30,}/,
OAUTH_KEY: /OAuth [A-Za-z0-9-._~+/]{30,}/,
OAUTH_TOKEN: /oauth_token=[A-Za-z0-9-._~+/]{30,}/,
OAUTH_ACCESS_TOKEN: /access_token=[A-Za-z0-9-._~+/]{30,}/,
JWT_TOKEN: /ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/,
AWS_ACCESS_KEY: /AKIA[A-Z0-9]{16}/,
GOOGLE_CLOUD_SECRET_KEY: /AIza[a-zA-Z0-9-_]{35}/,
STRIPE_LIVE_API_KEY: /sk_live_[a-zA-Z0-9]{24}/,
STRIPE_TEST_API_KEY: /sk_test_[a-zA-Z0-9]{24}/,
GITHUB_PAGES_ACCESS_TOKEN: /ghp_[a-zA-Z0-9]{36}/,
SLACK_API_TOKEN: /xox[baprs]-[a-zA-Z0-9]{12,}/,
REFRESH_TOKEN: /refresh_token_[a-zA-Z0-9-_]{32,}/,
SESSION_ID: /sess_[a-zA-Z0-9-_]{22,}/,
} as const;

export const objectReplaceSecrets = <T>(obj: T): T => {
if (!obj) {
return obj;
}

if (typeof obj === "string") {
let result = obj as string;

for (const pattern in secretPatterns) {
result = result.replace(secretPatterns[pattern as keyof typeof secretPatterns], `<${pattern}>`);
}

return result as T;
}

if (typeof obj !== "object") {
return obj;
}

if (Array.isArray(obj)) {
return obj.map(item => objectReplaceSecrets(item)) as T;
}

const clone = Object.create(obj) as T;

for (const key of Object.getOwnPropertyNames(obj)) {
clone[key as keyof T] = objectReplaceSecrets(obj[key as keyof T]);
}

for (const key of Object.getOwnPropertySymbols(obj)) {
clone[key as keyof T] = objectReplaceSecrets(obj[key as keyof T]);
}

return clone;
};

export const utilInspectSafe = <T>(obj: T): string => inspect(objectReplaceSecrets(obj));
124 changes: 124 additions & 0 deletions test/src/utils/secret-replacer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { objectReplaceSecrets } from "../../../src/utils/secret-replacer";

describe("objectReplaceSecrets", () => {
describe("String Inputs", () => {
it("should replace OAuth Key patterns", () => {
const input = "OAuth abcdefghijklmnopqrstuvwxyz012345";

const result = objectReplaceSecrets(input);

assert.equal(result, "<OAUTH_KEY>");
});

it("should replace multiple patterns in a single string", () => {
const input = "Bearer abcdefghijklmnopqrstuvwxyz012345 OAuth xyz0123456789abcdefghijklmnopqrstuvwxyz012";

const result = objectReplaceSecrets(input);

assert.equal(result, "<BEARER_TOKEN> <OAUTH_KEY>");
});
});

describe("Object Inputs", () => {
it("should recursively replace secrets in nested objects", () => {
const input = {
token: "Bearer abcdefghijklmnopqrstuvwxyz012345",
user: {
apiKey: "sk_live_abcdefghijklmnopqrstuvwx",
metadata: {
refreshToken: "refresh_token_abcdefghijklmnopqrstuvwxyz012345",
},
},
};

const result = objectReplaceSecrets(input);

assert.deepEqual(result, {
token: "<BEARER_TOKEN>",
user: {
apiKey: "<STRIPE_LIVE_API_KEY>",
metadata: {
refreshToken: "<REFRESH_TOKEN>",
},
},
});
});

it("should handle objects with mixed data types", () => {
const input = {
id: 123,
secret: "AKIAIOSFODNN7EXAMPLE",
isActive: true,
details: {
token: "Bearer abcdefghijklmnopqrstuvwxyz012345",
},
};

const result = objectReplaceSecrets(input);

assert.deepEqual(result, {
id: 123,
secret: "<AWS_ACCESS_KEY>",
isActive: true,
details: {
token: "<BEARER_TOKEN>",
},
});
});
});

describe("Array Inputs", () => {
it("should replace secrets in an array of strings", () => {
const input = ["Bearer abcdefghijklmnopqrstuvwxyz012345", "OAuth xyz0123456789abcdefghijklmnopqrstuvwxyz"];

const result = objectReplaceSecrets(input);

assert.deepEqual(result, ["<BEARER_TOKEN>", "<OAUTH_KEY>"]);
});

it("should recursively replace secrets in an array of objects", () => {
const input = [
{ token: "Bearer abcdefghijklmnopqrstuvwxyz012345" },
{ apiKey: "sk_live_abcdefghijklmnopqrstuvwx" },
];

const result = objectReplaceSecrets(input);

assert.deepEqual(result, [{ token: "<BEARER_TOKEN>" }, { apiKey: "<STRIPE_LIVE_API_KEY>" }]);
});
});

describe("Edge Cases", () => {
it("should return null for null input", () => {
const input = null;
const result = objectReplaceSecrets(input);
assert.isNull(result);
});

it("should return undefined for undefined input", () => {
const input = undefined;
const result = objectReplaceSecrets(input);
assert.isUndefined(result);
});

it("should return non-string primitives unchanged", () => {
const input = 123;
const result = objectReplaceSecrets(input);
assert.equal(result, 123);

const input2 = true;
const result2 = objectReplaceSecrets(input2);
assert.equal(result2, true);
});

it("should handle empty strings and objects", () => {
const input1 = "";
const result1 = objectReplaceSecrets(input1);
assert.equal(result1, "");

const input2 = {};
const result2 = objectReplaceSecrets(input2);
assert.deepEqual(result2, {});
});
});
});

0 comments on commit 62abee9

Please sign in to comment.