Skip to content

Configure default sensitive fields #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 10, 2025
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,15 @@ To get the logs of all events for the specific `correlationId` across multiple s
### Constructor

```javascript
const logger = new Logger(serviceName, applicationName, correlationId);
const logger = new Logger(serviceName, applicationName, options);
```

- **serviceName** [string, mandatory]: Added to each log output
- **applicationName** [string, mandatory]: Defines the Namespace for metrics
- **correlationId** [string, optional]: A new UUIDv4 is generated when not defined. Added to each log output.
- **options** [object | string | null, optional]: Configuration options
- **correlationId** [string, optional]: A new UUIDv4 is generated when not defined. Added to each log output.
- **additionalSensitiveAttributes** [string[], optional]: Add new sensitive attributes to the pre-defined defaults
- **overrideSensitiveAttributes** [string[], optional]: Completely override the default sensitive attributes

### setCorrelationId

Expand Down Expand Up @@ -414,3 +417,47 @@ functions:
applicationLogLevel: WARN
systemLogLevel: INFO
```

### Sensitive Attributes

The logger automatically masks sensitive values in your logs. By default, it masks common sensitive fields like passwords, tokens, and API keys. You can customize this behavior in two ways:

1. **Add to Defaults**: Use `additionalSensitiveAttributes` to add new fields to the default list:
```javascript
const logger = new Logger("myService", "myApp", {
additionalSensitiveAttributes: ["customSecret", "apiToken"]
});
```

2. **Override Defaults**: Use `overrideSensitiveAttributes` to completely replace the default list:
```javascript
const logger = new Logger("myService", "myApp", {
overrideSensitiveAttributes: ["onlyThese", "willBeMasked"]
});
```

You can also specify sensitive attributes per log call:
```javascript
// Add to defaults
logger.info("message", payload, context, ["customSecret"]);

// Or use the object syntax
logger.info("message", payload, context, {
additionalSensitiveAttributes: ["customSecret"]
});

// Or override completely
logger.info("message", payload, context, {
overrideSensitiveAttributes: ["onlyThese", "willBeMasked"]
});
```

The default sensitive attributes are:
- password
- userid
- token
- secret
- key
- x-api-key
- bearer
- authorization
82 changes: 59 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type {
JSONObject,
JSONValue,
ErrorLogAttributes,
LoggerOptions,
LogOptions,
} from "./types";

const LOG_EVENT = env.get("SG_LOGGER_LOG_EVENT").default("true").asBool();
Expand All @@ -36,24 +38,54 @@ const LOG_LEVEL = env

class Logger {
static METRIC_UNITS = MetricUnitList;
private static readonly DEFAULT_SENSITIVE_ATTRIBUTES: StringArray = [
"password",
"userid",
"token",
"secret",
"key",
"x-api-key",
"bearer",
"authorization",
];

private serviceName: string;
private correlationId: string;
private resetCorrelationId: boolean;
private applicationName: string;
private persistentContext: JSONObject;
private console: Console;
private defaultSensitiveAttributes: StringArray;

constructor(serviceName: string, applicationName: string, correlationId: string | null = null) {
constructor(serviceName: string, applicationName: string, options: LoggerOptions | string | null = {}) {
this.serviceName = serviceName;
this.correlationId = correlationId ? correlationId : randomUUID();
this.resetCorrelationId = correlationId ? false : true;
this.applicationName = applicationName;
this.persistentContext = {};
this.console =
env.get("AWS_LAMBDA_LOG_FORMAT").asString() === "JSON"
? new Console((process.stdout, process.stderr))
: console;

// Normalize input options
let inputOptions: LoggerOptions;
if (typeof options === 'string' || options === null) {
inputOptions = { correlationId: options };
} else {
inputOptions = options;
}

// Set correlation ID
this.correlationId = inputOptions.correlationId ? inputOptions.correlationId : randomUUID();
this.resetCorrelationId = inputOptions.correlationId ? false : true;

// Initialize default sensitive attributes
if (inputOptions.overrideSensitiveAttributes) {
this.defaultSensitiveAttributes = inputOptions.overrideSensitiveAttributes;
} else if (inputOptions.additionalSensitiveAttributes) {
this.defaultSensitiveAttributes = [...Logger.DEFAULT_SENSITIVE_ATTRIBUTES, ...inputOptions.additionalSensitiveAttributes];
} else {
this.defaultSensitiveAttributes = [...Logger.DEFAULT_SENSITIVE_ATTRIBUTES];
}
}

getLogLevel(level: Level): number {
Expand All @@ -72,34 +104,38 @@ class Logger {
message: string = "",
payload: JSONValue | Error = {},
context: JSONObject = {},
sensitiveAttributes: StringArray = []
options: StringArray | LogOptions = []
): void {
if (this.getLogLevel(level) < this.getLogLevel(LOG_LEVEL)) {
return;
}

try {
// Default sensitive attributes
const defaultSensitiveAttributes: StringArray = [
"password",
"userid",
"token",
"secret",
"key",
"x-api-key",
"bearer",
"authorization",
];

const arrayToLowerCase = (array: StringArray): StringArray => {
if (Array.isArray(array)) {
return array.filter((el) => typeof el === "string").map((el) => el.toLowerCase());
}
return [];
};

// Merge default sensitive attributes with custom ones
const attributesToMask = new Set([...defaultSensitiveAttributes, ...arrayToLowerCase(sensitiveAttributes)]);
// Normalize input options
let inputOptions: LogOptions;
if (Array.isArray(options)) {
inputOptions = { additionalSensitiveAttributes: options };
} else {
inputOptions = options;
}

// Handle sensitive attributes
let attributesToMask: Set<string>;
if (inputOptions.overrideSensitiveAttributes) {
attributesToMask = new Set(arrayToLowerCase(inputOptions.overrideSensitiveAttributes));
} else {
attributesToMask = new Set([
...arrayToLowerCase(this.defaultSensitiveAttributes),
...arrayToLowerCase(inputOptions.additionalSensitiveAttributes || [])
]);
}

// Mask sensitive attributes, remove null
const maskSensitiveAttributes = (key: string, value: JSONValue): JSONValue | string | undefined => {
Expand Down Expand Up @@ -243,14 +279,14 @@ class Logger {
default:
break;
}
} catch {}
} catch { }
}

info(
message: string = "",
payload: JSONValue = {},
context: JSONObject = {},
sensitiveAttributes: StringArray = []
sensitiveAttributes: StringArray | LogOptions = []
): void {
this.log("info", message, payload, context, sensitiveAttributes);
}
Expand All @@ -259,7 +295,7 @@ class Logger {
message: string = "",
payload: JSONValue = {},
context: JSONObject = {},
sensitiveAttributes: StringArray = []
sensitiveAttributes: StringArray | LogOptions = []
): void {
this.log("debug", message, payload, context, sensitiveAttributes);
}
Expand All @@ -268,7 +304,7 @@ class Logger {
message: string = "",
payload: JSONValue = {},
context: JSONObject = {},
sensitiveAttributes: StringArray = []
sensitiveAttributes: StringArray | LogOptions = []
): void {
this.log("warn", message, payload, context, sensitiveAttributes);
}
Expand All @@ -277,7 +313,7 @@ class Logger {
message: string = "",
payload: JSONValue | Error = {},
context: JSONObject = {},
sensitiveAttributes: StringArray = []
sensitiveAttributes: StringArray | LogOptions = []
): void {
this.log("error", message, payload, context, sensitiveAttributes);
}
Expand Down
11 changes: 11 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ type MetricDefinition = {
Unit: MetricUnit;
};

interface LogOptions {
additionalSensitiveAttributes?: StringArray;
overrideSensitiveAttributes?: StringArray;
}

interface LoggerOptions extends LogOptions {
correlationId?: string | null;
}

export type {
Level,
StringArray,
Expand All @@ -60,4 +69,6 @@ export type {
JSONObject,
JSONValue,
ErrorLogAttributes,
LogOptions,
LoggerOptions,
};
72 changes: 72 additions & 0 deletions test/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,76 @@ describe("Log Outputs", () => {
'{"level":"ERROR","service":"testService","correlationId":"testId","message":"Message"}'
);
});

test("Additional Sensitive Attributes in Constructor", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp", {
correlationId: "testId",
additionalSensitiveAttributes: ["newSecret", "newKey"]
});
logger.info("Message", { newSecret: "secret", newKey: "key", password: "pass" });
expect(console.info).toHaveBeenCalledWith(
'{"level":"INFO","service":"testService","correlationId":"testId","message":"Message","payload":{"newSecret":"****","newKey":"****","password":"****"}}'
);
});

test("Override Sensitive Attributes in Constructor", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp", {
overrideSensitiveAttributes: ["onlyThis", "willBeMasked"]
});
logger.info("Message", { onlyThis: "secret", willBeMasked: "key", password: "pass" });
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(
/{"level":"INFO","service":"testService","correlationId":"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}","message":"Message","payload":{"onlyThis":"\*\*\*\*","willBeMasked":"\*\*\*\*","password":"pass"}}/
)
);
});

test("Additional Sensitive Attributes in Log Call", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp");
logger.info("Message", { customSecret: "secret", password: "pass" }, {}, {
additionalSensitiveAttributes: ["customSecret"]
});
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(
/{"level":"INFO","service":"testService","correlationId":"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}","message":"Message","payload":{"customSecret":"\*\*\*\*","password":"\*\*\*\*"}}/
)
);
});

test("Override Sensitive Attributes in Log Call", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp");
logger.info("Message", { customSecret: "secret", password: "pass" }, {}, {
overrideSensitiveAttributes: ["customSecret"]
});
expect(console.info).toHaveBeenCalledWith(
expect.stringContaining('{"level":"INFO","service":"testService","correlationId":"')
);
expect(console.info).toHaveBeenCalledWith(
expect.stringContaining('","message":"Message","payload":{"customSecret":"****","password":"pass"}}')
);
});

test("Backward Compatibility with String Array in Log Call", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp");
logger.info("Message", { customSecret: "secret", password: "pass" }, {}, ["customSecret"]);
expect(console.info).toHaveBeenCalledWith(
expect.stringMatching(
/{"level":"INFO","service":"testService","correlationId":"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}","message":"Message","payload":{"customSecret":"\*\*\*\*","password":"\*\*\*\*"}}/
)
);
});

test("Backward Compatibility with String in Constructor", () => {
const { Logger } = require("../src/index");
const logger = new Logger("testService", "testApp", "testId");
logger.info("Message", { password: "pass" });
expect(console.info).toHaveBeenCalledWith(
'{"level":"INFO","service":"testService","correlationId":"testId","message":"Message","payload":{"password":"****"}}'
);
});
});