Initial Commit
This commit is contained in:
131
packages/observability/monitor/logger/src/redaction/redaction.ts
Normal file
131
packages/observability/monitor/logger/src/redaction/redaction.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { isSensitive } from "@suaveplan/error/redact";
|
||||
import type {
|
||||
PathRedactionRule,
|
||||
PatternRedactionRule,
|
||||
RedactionOptions,
|
||||
RedactionRule,
|
||||
} from "./redaction.types.js";
|
||||
|
||||
const REDACTED = "[REDACTED]";
|
||||
const DEFAULT_MAX_DEPTH = 10;
|
||||
|
||||
function pathMatches(rulePath: string, keyPath: string): boolean {
|
||||
const ruleSegments = rulePath.split(".");
|
||||
const keySegments = keyPath.split(".");
|
||||
if (ruleSegments.length !== keySegments.length) return false;
|
||||
for (let i = 0; i < ruleSegments.length; i++) {
|
||||
if (ruleSegments[i] !== "*" && ruleSegments[i] !== keySegments[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldRedactPath(
|
||||
rules: PathRedactionRule[],
|
||||
keyPath: string
|
||||
): boolean {
|
||||
return rules.some((r) => pathMatches(r.path, keyPath));
|
||||
}
|
||||
|
||||
function shouldRedactPattern(
|
||||
rules: PatternRedactionRule[],
|
||||
key: string
|
||||
): boolean {
|
||||
return rules.some((r) => r.pattern.test(key));
|
||||
}
|
||||
|
||||
function redactValue(
|
||||
value: unknown,
|
||||
pathRules: PathRedactionRule[],
|
||||
patternRules: PatternRedactionRule[],
|
||||
label: string,
|
||||
currentPath: string,
|
||||
depth: number,
|
||||
maxDepth: number
|
||||
): unknown {
|
||||
if (isSensitive(value)) {
|
||||
return label;
|
||||
}
|
||||
|
||||
if (
|
||||
shouldRedactPath(pathRules, currentPath) ||
|
||||
(currentPath !== "" &&
|
||||
shouldRedactPattern(patternRules, currentPath.split(".").at(-1)!))
|
||||
) {
|
||||
return label;
|
||||
}
|
||||
|
||||
if (depth >= maxDepth || value === null || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, i) =>
|
||||
redactValue(
|
||||
item,
|
||||
pathRules,
|
||||
patternRules,
|
||||
label,
|
||||
`${currentPath}[${i}]`,
|
||||
depth + 1,
|
||||
maxDepth
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
const childPath = currentPath === "" ? k : `${currentPath}.${k}`;
|
||||
out[k] = redactValue(
|
||||
v,
|
||||
pathRules,
|
||||
patternRules,
|
||||
label,
|
||||
childPath,
|
||||
depth + 1,
|
||||
maxDepth
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export class RedactionEngine {
|
||||
private readonly pathRules: PathRedactionRule[];
|
||||
private readonly patternRules: PatternRedactionRule[];
|
||||
private readonly label: string;
|
||||
private readonly maxDepth: number;
|
||||
|
||||
public constructor(options: RedactionOptions = {}) {
|
||||
const rules: RedactionRule[] = options.rules ?? [];
|
||||
this.pathRules = rules.filter(
|
||||
(r): r is PathRedactionRule => r.kind === "path"
|
||||
);
|
||||
this.patternRules = rules.filter(
|
||||
(r): r is PatternRedactionRule => r.kind === "pattern"
|
||||
);
|
||||
this.label = options.redactedLabel ?? REDACTED;
|
||||
this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
||||
}
|
||||
|
||||
public redact(ctx: Record<string, unknown>): Record<string, unknown> {
|
||||
return redactValue(
|
||||
ctx,
|
||||
this.pathRules,
|
||||
this.patternRules,
|
||||
this.label,
|
||||
"",
|
||||
0,
|
||||
this.maxDepth
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyRedaction(
|
||||
ctx: Record<string, unknown>,
|
||||
options: RedactionOptions = {}
|
||||
): Record<string, unknown> {
|
||||
return new RedactionEngine(options).redact(ctx);
|
||||
}
|
||||
|
||||
export type { RedactionOptions, RedactionRule };
|
||||
Reference in New Issue
Block a user