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
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
153 changes: 153 additions & 0 deletions src/logger/mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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)
}
];

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

// Keys that contain IDs — not PII, skip regex masking to avoid false positives
const SKIP_MASK_KEYS = new Set(['_id', 'id', 'org_id', 'user_id', 'campaign_id', 'publisher_id']);

// 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 => {
if (SKIP_MASK_KEYS.has(key)) return;

const value = cloned[key];

if ((key === "name" || key === "user") && 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 };
Loading