mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
- Implemented MonitorUtil for managing monitor secrets and populating them in monitor steps and tests. - Created StackTraceParser to parse and structure stack traces from various programming languages. - Developed SyslogParser to handle and parse syslog messages in both RFC 5424 and RFC 3164 formats.
271 lines
7.2 KiB
TypeScript
271 lines
7.2 KiB
TypeScript
import LogScrubRule from "Common/Models/DatabaseModels/LogScrubRule";
|
|
import DatabaseService from "Common/Server/Services/DatabaseService";
|
|
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
|
import LIMIT_MAX from "Common/Types/Database/LimitMax";
|
|
import ObjectID from "Common/Types/ObjectID";
|
|
import { JSONObject } from "Common/Types/JSON";
|
|
import LogScrubAction from "Common/Types/Log/LogScrubAction";
|
|
import LogScrubPatternType from "Common/Types/Log/LogScrubPatternType";
|
|
import crypto from "crypto";
|
|
|
|
interface CacheEntry {
|
|
rules: Array<LogScrubRule>;
|
|
compiledPatterns: Array<CompiledRule>;
|
|
loadedAt: number;
|
|
}
|
|
|
|
interface CompiledRule {
|
|
rule: LogScrubRule;
|
|
regex: RegExp;
|
|
}
|
|
|
|
const CACHE_TTL_MS: number = 60 * 1000; // 60 seconds
|
|
|
|
const scrubRuleCache: Map<string, CacheEntry> = new Map();
|
|
|
|
// Built-in PII detection patterns
|
|
const BUILT_IN_PATTERNS: Record<string, RegExp> = {
|
|
[LogScrubPatternType.Email]:
|
|
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
[LogScrubPatternType.CreditCard]: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
|
|
[LogScrubPatternType.SSN]: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
[LogScrubPatternType.PhoneNumber]:
|
|
/\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
[LogScrubPatternType.IPAddress]: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
};
|
|
|
|
export class LogScrubRuleService {
|
|
public static async loadScrubRules(
|
|
projectId: ObjectID,
|
|
): Promise<Array<CompiledRule>> {
|
|
const cacheKey: string = projectId.toString();
|
|
const cached: CacheEntry | undefined = scrubRuleCache.get(cacheKey);
|
|
|
|
if (cached && Date.now() - cached.loadedAt < CACHE_TTL_MS) {
|
|
return cached.compiledPatterns;
|
|
}
|
|
|
|
const service: DatabaseService<LogScrubRule> =
|
|
new DatabaseService<LogScrubRule>(LogScrubRule);
|
|
|
|
const rules: Array<LogScrubRule> = await service.findBy({
|
|
query: {
|
|
projectId: projectId,
|
|
isEnabled: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
sort: {
|
|
sortOrder: SortOrder.Ascending,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
name: true,
|
|
patternType: true,
|
|
customRegex: true,
|
|
scrubAction: true,
|
|
fieldsToScrub: true,
|
|
sortOrder: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// Pre-compile regex patterns for performance
|
|
const compiledPatterns: Array<CompiledRule> = [];
|
|
|
|
for (const rule of rules) {
|
|
const regex: RegExp | null = this.getRegexForPattern(
|
|
rule.patternType as string,
|
|
rule.customRegex as string | undefined,
|
|
);
|
|
|
|
if (regex) {
|
|
compiledPatterns.push({ rule, regex });
|
|
}
|
|
}
|
|
|
|
scrubRuleCache.set(cacheKey, {
|
|
rules,
|
|
compiledPatterns,
|
|
loadedAt: Date.now(),
|
|
});
|
|
|
|
return compiledPatterns;
|
|
}
|
|
|
|
private static getRegexForPattern(
|
|
patternType: string,
|
|
customRegex?: string,
|
|
): RegExp | null {
|
|
if (patternType === LogScrubPatternType.Custom) {
|
|
if (!customRegex) {
|
|
return null;
|
|
}
|
|
try {
|
|
return new RegExp(customRegex, "g");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const builtIn: RegExp | undefined = BUILT_IN_PATTERNS[patternType];
|
|
if (builtIn) {
|
|
// Return a new instance so lastIndex is independent per use
|
|
return new RegExp(builtIn.source, builtIn.flags);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static applyScrubAction(
|
|
match: string,
|
|
action: string,
|
|
patternType: string,
|
|
): string {
|
|
switch (action) {
|
|
case LogScrubAction.Redact:
|
|
return "[REDACTED]";
|
|
|
|
case LogScrubAction.Hash: {
|
|
const hash: string = crypto
|
|
.createHash("sha256")
|
|
.update(match)
|
|
.digest("hex")
|
|
.substring(0, 8);
|
|
return `[HASHED:${hash}]`;
|
|
}
|
|
|
|
case LogScrubAction.Mask:
|
|
return this.maskValue(match, patternType);
|
|
|
|
default:
|
|
return "[REDACTED]";
|
|
}
|
|
}
|
|
|
|
private static maskValue(value: string, patternType: string): string {
|
|
switch (patternType) {
|
|
case LogScrubPatternType.Email: {
|
|
const atIndex: number = value.indexOf("@");
|
|
if (atIndex > 0) {
|
|
const dotIndex: number = value.lastIndexOf(".");
|
|
if (dotIndex > atIndex) {
|
|
return value[0] + "***@***" + value.substring(dotIndex);
|
|
}
|
|
}
|
|
return "***@***.***";
|
|
}
|
|
|
|
case LogScrubPatternType.CreditCard: {
|
|
// Show last 4 digits only
|
|
const digits: string = value.replace(/[-\s]/g, "");
|
|
if (digits.length >= 4) {
|
|
return "****-****-****-" + digits.substring(digits.length - 4);
|
|
}
|
|
return "****-****-****-****";
|
|
}
|
|
|
|
case LogScrubPatternType.SSN:
|
|
return "***-**-" + value.substring(value.length - 4);
|
|
|
|
case LogScrubPatternType.PhoneNumber: {
|
|
// Show last 4 digits only
|
|
const phoneDigits: string = value.replace(/[^0-9]/g, "");
|
|
if (phoneDigits.length >= 4) {
|
|
return "***-***-" + phoneDigits.substring(phoneDigits.length - 4);
|
|
}
|
|
return "***-***-****";
|
|
}
|
|
|
|
case LogScrubPatternType.IPAddress:
|
|
return "***.***.***.***";
|
|
|
|
default: {
|
|
// Generic masking: keep first and last char, mask middle
|
|
if (value.length <= 2) {
|
|
return "***";
|
|
}
|
|
return (
|
|
value[0] +
|
|
"*".repeat(Math.max(value.length - 2, 3)) +
|
|
value[value.length - 1]!
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static scrubString(
|
|
value: string,
|
|
compiledRules: Array<CompiledRule>,
|
|
): string {
|
|
let result: string = value;
|
|
|
|
for (const { rule, regex } of compiledRules) {
|
|
// Reset lastIndex for global regex
|
|
regex.lastIndex = 0;
|
|
|
|
const action: string =
|
|
(rule.scrubAction as string) || LogScrubAction.Redact;
|
|
const patternType: string = (rule.patternType as string) || "";
|
|
|
|
result = result.replace(regex, (match: string) => {
|
|
return this.applyScrubAction(match, action, patternType);
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static scrubLog(
|
|
logRow: JSONObject,
|
|
compiledRules: Array<CompiledRule>,
|
|
): JSONObject {
|
|
if (compiledRules.length === 0) {
|
|
return logRow;
|
|
}
|
|
|
|
for (const { rule } of compiledRules) {
|
|
const fieldsToScrub: string = (rule.fieldsToScrub as string) || "both";
|
|
|
|
// Filter compiled rules to just this one for per-rule scrubbing
|
|
const singleRule: Array<CompiledRule> = compiledRules.filter(
|
|
(cr: CompiledRule) => {
|
|
return cr.rule === rule;
|
|
},
|
|
);
|
|
|
|
// Scrub body
|
|
if (
|
|
(fieldsToScrub === "body" || fieldsToScrub === "both") &&
|
|
typeof logRow["body"] === "string"
|
|
) {
|
|
logRow["body"] = this.scrubString(logRow["body"] as string, singleRule);
|
|
}
|
|
|
|
// Scrub attributes
|
|
if (
|
|
(fieldsToScrub === "attributes" || fieldsToScrub === "both") &&
|
|
logRow["attributes"] &&
|
|
typeof logRow["attributes"] === "object"
|
|
) {
|
|
const attributes: JSONObject = logRow["attributes"] as JSONObject;
|
|
|
|
for (const key of Object.keys(attributes)) {
|
|
if (typeof attributes[key] === "string") {
|
|
attributes[key] = this.scrubString(
|
|
attributes[key] as string,
|
|
singleRule,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return logRow;
|
|
}
|
|
}
|
|
|
|
export default LogScrubRuleService;
|