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
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ import { multiSigSubmissionService } from "./services/multiSigSubmissionService"
import { apiKeyMiddleware } from "./middleware/apiKeyMiddleware";
import { rateLimitMiddleware } from "./middleware/rateLimitMiddleware";
import { validateEnv } from "./utils/envValidator";
import { enableGlobalLogMasking } from "./utils/logMasker";
import { hourlyAverageService } from "./services/hourlyAverageService";

// Load environment variables
dotenv.config();

// Enable log masking to prevent sensitive data leaks
enableGlobalLogMasking();

// [OPS] Implement "Environment Variable" Check on Start
validateEnv();

Expand Down
179 changes: 179 additions & 0 deletions src/utils/logMasker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Log Masking Utility
* Scrubs sensitive data (secrets, API keys, passwords, etc.) from log output
* to prevent leaking confidential information to stdout/stderr.
*/

// Patterns for detecting sensitive values
const SENSITIVE_PATTERNS = [
// Environment variable names that contain sensitive data
/\b(SECRET|PASSWORD|TOKEN|KEY|CREDENTIAL|PRIVATE|API_KEY|APIKEY|AUTH|PK)\b/gi,

// Stellar secret keys (start with 'S' and are 56 characters base32, typically A-Z and 2-7)
/\bS[A-Z2-7]{48,56}\b/g,

// Ethereum-style private keys (64 hex chars or 66 with 0x prefix)
/\b(0x)?[a-fA-F0-9]{64}\b/g,

// Common Bearer tokens
/Bearer\s+[A-Za-z0-9._-]+/gi,

// Database connection strings with passwords
/(:\/\/[^:]+:)([^@]+)(@)/g,

// AWS-style access keys (AKIA followed by 16 alphanumeric chars)
/AKIA[0-9A-Z]{16}/g,
];

/**
* Masks sensitive values in a string by replacing them with [REDACTED]
* @param input - The string to mask
* @returns The masked string with sensitive data replaced
*/
export function maskSensitiveData(input: string): string {
if (!input || typeof input !== "string") {
return input;
}

let masked = input;

// Apply each pattern
for (const pattern of SENSITIVE_PATTERNS) {
masked = masked.replace(pattern, (match) => {
// For database connection strings, preserve the connection type
if (match.includes("://")) {
return match.replace(/(:\/\/[^:]+:)([^@]+)(@)/, "$1[REDACTED]$3");
}
// For Bearer tokens, preserve the scheme
if (match.toLowerCase().startsWith("bearer")) {
return "Bearer [REDACTED]";
}
// For other matches, just redact
return "[REDACTED]";
});
}

return masked;
}

/**
* Masks sensitive data in an object (recursively)
* @param obj - The object to mask
* @returns A new object with sensitive values masked
*/
export function maskSensitiveObject(
obj: Record<string, any>,
): Record<string, any> {
if (!obj || typeof obj !== "object") {
return obj;
}

const masked: Record<string, any> = {};

for (const [key, value] of Object.entries(obj)) {
// Check if the key name suggests sensitive data
if (/secret|password|token|key|credential|private|api|auth/i.test(key)) {
masked[key] = "[REDACTED]";
} else if (typeof value === "string") {
masked[key] = maskSensitiveData(value);
} else if (Array.isArray(value)) {
masked[key] = value.map((item) =>
typeof item === "string" ? maskSensitiveData(item) : item,
);
} else if (typeof value === "object" && value !== null) {
masked[key] = maskSensitiveObject(value);
} else {
masked[key] = value;
}
}

return masked;
}

/**
* Creates a masked console object that automatically scrubs logs
* Replace console.log, console.error, etc. with these versions
*/
export const maskedConsole = {
log: (...args: any[]): void => {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
console.log(...maskedArgs);
},

error: (...args: any[]): void => {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
console.error(...maskedArgs);
},

warn: (...args: any[]): void => {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
console.warn(...maskedArgs);
},

info: (...args: any[]): void => {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
console.info(...maskedArgs);
},

debug: (...args: any[]): void => {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
console.debug(...maskedArgs);
},
};

/**
* Intercepts all console methods and applies masking
* Call this once at application startup to enable global log masking
*/
export function enableGlobalLogMasking(): void {
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
const originalInfo = console.info;
const originalDebug = console.debug;

console.log = function (...args: any[]): void {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
originalLog(...maskedArgs);
};

console.error = function (...args: any[]): void {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
originalError(...maskedArgs);
};

console.warn = function (...args: any[]): void {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
originalWarn(...maskedArgs);
};

console.info = function (...args: any[]): void {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
originalInfo(...maskedArgs);
};

console.debug = function (...args: any[]): void {
const maskedArgs = args.map((arg) =>
typeof arg === "string" ? maskSensitiveData(arg) : arg,
);
originalDebug(...maskedArgs);
};
}
118 changes: 118 additions & 0 deletions test/logMasker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { maskSensitiveData, maskSensitiveObject } from "../src/utils/logMasker";

function assert(condition: boolean, message: string): void {
if (!condition) {
throw new Error(message);
}
}

async function run(): Promise<void> {
// Test 1: Mask Stellar secret key (48-56 characters starting with S, base32)
const stellarSecret =
"SBCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV";
const maskedSecret = maskSensitiveData(stellarSecret);
assert(
maskedSecret.includes("[REDACTED]"),
"Stellar secret key should be masked",
);

// Test 2: Mask values with PASSWORD keyword
const passwordMsg = "User password is: myverysecurepassword123";
const maskedPasswordMsg = maskSensitiveData(passwordMsg);
assert(
maskedPasswordMsg.includes("[REDACTED]"),
"PASSWORD keyword should trigger masking",
);

// Test 3: Mask database connection string with password
const dbUrl = "postgresql://username:mypassword@localhost:5432/mydb";
const maskedDbUrl = maskSensitiveData(dbUrl);
assert(
maskedDbUrl.includes("[REDACTED]"),
"Database password should be masked",
);
assert(maskedDbUrl.includes("username"), "Username should not be masked");
assert(
!maskedDbUrl.includes("mypassword"),
"Password should not appear in output",
);

// Test 4: Mask Bearer token
const bearerToken = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
const maskedBearer = maskSensitiveData(bearerToken);
assert(maskedBearer.includes("[REDACTED]"), "Bearer token should be masked");
assert(
maskedBearer.startsWith("Bearer"),
"Bearer prefix should be preserved",
);

// Test 5: Mask object with sensitive keys
const sensitiveObj = {
username: "john",
password: "supersecret",
api_key: "sk_test_12345",
data: {
token: "abcdef123456",
value: 100,
},
};
const maskedObj = maskSensitiveObject(sensitiveObj);
assert(
maskedObj.password === "[REDACTED]",
"password field should be redacted",
);
assert(
maskedObj.api_key === "[REDACTED]",
"api_key field should be redacted",
);
assert(
maskedObj.data.token === "[REDACTED]",
"nested token field should be redacted",
);
assert(
maskedObj.username === "john",
"non-sensitive fields should not be masked",
);
assert(maskedObj.data.value === 100, "numeric values should not be affected");

// Test 6: Mask AWS access key
const awsKey = "AKIAIOSFODNN7EXAMPLE";
const maskedAwsKey = maskSensitiveData(awsKey);
assert(
maskedAwsKey.includes("[REDACTED]"),
"AWS access key should be masked",
);

// Test 7: Mask SECRET keyword
const secretMsg = "Loaded secret key from vault";
const maskedSecretMsg = maskSensitiveData(secretMsg);
assert(
maskedSecretMsg.includes("[REDACTED]"),
"SECRET keyword should trigger masking",
);

// Test 8: Non-sensitive strings should pass through unchanged
const normalMsg = "Processing request for user john_doe";
const maskedNormal = maskSensitiveData(normalMsg);
assert(
maskedNormal === normalMsg,
"Non-sensitive strings should not be masked",
);

// Test 9: Empty and null inputs
assert(
maskSensitiveData("") === "",
"Empty string should return empty string",
);
assert(
maskSensitiveData(null as any) === null,
"Null input should return null",
);

console.log("✅ All log masking tests passed");
}

run().catch((error) => {
console.error("❌ Test failed:", error.message);
process.exit(1);
});
Loading