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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
sftp-config.json
.vscode/launch.json
.env
combined.log
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# tracker-utils

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Tracker Utils",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -32,5 +32,8 @@
"redis": "3.1.1",
"ulidx": "^2.4.1",
"winston": "^3.8.2"
},
"devDependencies": {
"jest": "^30.2.0"
}
}
9 changes: 8 additions & 1 deletion src/logger/logger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const winston = require("winston");
const { maskData } = require("./mask");

const logger = (() => {
let LOGGER_ENABLED = true;
Expand Down Expand Up @@ -37,7 +38,13 @@ const logger = (() => {
};

const getLogstring = (info) => {
return `[REQ_ID: ${requestId}][USER: ${user}][USER_ID: ${userId}][LEVEL:${info.level}][MSG:${info.message}]`;
const safeMessage = maskData(info.message);
const printableMessage =
typeof safeMessage === "object"
? JSON.stringify(safeMessage)
: safeMessage;

return `[REQ_ID: ${requestId}][USER: ${user}][USER_ID: ${userId}][LEVEL:${info.level}][MSG:${printableMessage}]`;
};

const info = (msg) => {
Expand Down
148 changes: 148 additions & 0 deletions src/logger/mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Enable/Disable masking via env (default: enabled)
const ENABLE_MASKING = process.env.MASK_LOGS !== "false";

// Regex patterns for detecting emails and phone numbers
const REGEX_PATTERNS = [
{
regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
replacer: (match) => maskEmail(match)
},
{
regex: /\+?\d[\d\s\-]{8,}\d/g,
replacer: (match) => maskPhone(match)
}
Comment on lines +10 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Phone regex is overly broad — will mask non-phone numeric sequences.

The pattern \+?\d[\d\s\-]{8,}\d matches any 10+ digit sequence, which means order IDs, timestamps, transaction amounts, and other numeric identifiers will be incorrectly masked. For example, "Order 1234567890 confirmed" would have the order ID replaced with "**********".

Consider tightening the regex (e.g., requiring a leading + or parenthesized country code, or restricting match length) or switching to a key-based approach for phone masking in objects, similar to how name is handled.

🤖 Prompt for AI Agents
In `@src/logger/mask.js` around lines 6 - 9, The phone regex in the mask config
(the object with the regex and replacer) is too broad and matches any 10+ digit
sequence; replace it with a stricter approach: either narrow the regex to
require a leading '+' or parenthesized country code and limit total length
(e.g., allow country code + 7–12 digits) or remove the free-form regex and add
key-based masking for object properties named phone/phoneNumber/mobile similar
to how name is handled; update the replacer to remain "**********" and ensure
the change targets the same regex property/replacer pair so other numeric
sequences (order IDs, timestamps) are no longer masked.

];

// Generic mask used for structured fields like address
const MASK = "********";

// Masks phone number and exposes last 4 digits
const maskPhone = (phone) => {
const digits = phone.replace(/\D/g, "");

if (digits.length <= 3) return "*".repeat(digits.length);

const last4 = digits.slice(-4);
return "*".repeat(Math.max(digits.length - 4, 0)) + last4;
};

// Masks email while partially exposing user and domain
const maskEmail = (email) => {
const [user, domain] = email.split("@");
if (!domain) return "***";

const maskedUser =
user.length <= 3
? "*".repeat(user.length)
: (
user[0] +
"*".repeat(user.length - 3) +
user.slice(-2)
);

const domainParts = domain.split(".");
const mainDomain = domainParts[0] || "";

const maskedDomain =
mainDomain.length <= 3
? "***"
: (
"*".repeat(mainDomain.length - 2) +
mainDomain.slice(-2)
);

const maskedTld = "***";

return `${maskedUser}@${maskedDomain}.${maskedTld}`;
};

// Masks name by showing the first and last two characters
const maskName = (name) => {
if (!name || typeof name !== "string") return name;

return name
.split(" ")
.map(part => {
if (part.length <= 3) return "*".repeat(part.length);

return (
part[0] +
"*".repeat(part.length - 3) +
part.slice(-2)
);
})
.join(" ");
};

// Applies regex-based masking on free-form strings
const applyRegexMasking = (str) => {
let masked = str;

REGEX_PATTERNS.forEach(({ regex, replacer }) => {
masked = masked.replace(regex, replacer);
});

return masked;
};

// Recursively masks structured objects (name, region, nested fields)
const maskObject = (obj, seen = new WeakSet()) => {
if (!obj || typeof obj !== "object") return obj;
if (seen.has(obj)) return "[Circular]";
seen.add(obj);

const cloned = Array.isArray(obj) ? [...obj] : { ...obj };

Object.keys(cloned).forEach(key => {
const value = cloned[key];

if (key === "name" && typeof value === "string") {
cloned[key] = maskName(value);
return;
}

if (key === "region" && value!==null && typeof value === "object") {
const maskedRegion = maskObject(value, seen);
cloned[key] = {
...maskedRegion,
address: value.address ? MASK : value.address,
city: value.city ? MASK : value.city,
state: value.state ? MASK : value.state,
zipcode: value.zipcode ? MASK : value.zipcode
};
return;
}

if (typeof value === "string") {
cloned[key] = applyRegexMasking(value);
return;
}

if (typeof value === "object") {
cloned[key] = maskObject(value, seen);
}
});

return cloned;
};

// Entry point for masking (handles string or object)
const maskData = (data) => {

// Skip masking if disabled
if (!ENABLE_MASKING) return data;

if (!data) return data;

if (typeof data === "string") {
return applyRegexMasking(data);
}

if (typeof data === "object") {
return maskObject(data);
}

return data;
};

module.exports = { maskData };