forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogRedaction.js
More file actions
126 lines (107 loc) · 4.13 KB
/
Copy pathlogRedaction.js
File metadata and controls
126 lines (107 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
'use strict';
const winston = require('winston');
/**
* Log redaction.
*
* Winston's JSON formatter already JSON-escapes string values, so the risk
* here is NOT structurally corrupting log lines. The risks are:
* 1. Correlation/secret leakage: sensitive values flowing into every log line.
* 2. Volume: extremely long values (e.g. an oversized X-Request-ID) bloating
* every downstream log line.
*
* This format redacts two ways:
* - Key-based: any object key containing apikey/privatekey/secret/token/
* authorization triggers redaction of its value.
* - Pattern-based: every string value (regardless of key) is scanned for
* webhook secret shapes (`whsec_…`, partially revealed as `whsec_****` for
* operator debuggability) and for credentials embedded in URLs
* (`?token=…`, `?secret=…`, `?key=…`), which are replaced in place.
*
* Arrays are walked as first-class nodes: a plain array of secret-shaped
* strings (not wrapped in an object) is now redacted, not just arrays of
* objects with sensitive keys.
*/
const SENSITIVE_KEYS = ['apikey', 'privatekey', 'secret', 'token', 'authorization'];
// Matches webhook secrets (whsec_ + hex). Safe to match broadly: the prefix is
// distinctive and only ever precedes a secret in this codebase.
const WHSEC_RE = /whsec_[0-9a-f]+/gi;
// Matches token=/secret=/key= query parameters embedded in any string value,
// capturing the parameter name so we can preserve it and only redact the value.
const QUERY_SECRET_RE = /([?&](?:token|secret|key)=)([^&#\s]+)/gi;
// Normalize a key for matching: treat `_`/`-` as nothing so `api_key`,
// `ApiKey`, `PRIVATE_KEY` all match their tokens.
function normKey(key) {
return String(key).toLowerCase().replace(/[_-]/g, '');
}
function isSensitiveKey(key) {
const n = normKey(key);
return SENSITIVE_KEYS.some((k) => n.includes(k));
}
function redactWhsec(value) {
return value.replace(WHSEC_RE, 'whsec_****');
}
function redactQuerySecrets(value) {
return value.replace(QUERY_SECRET_RE, '$1[REDACTED]');
}
// Scan a string value for secret *shapes* regardless of the key it sits under.
function scanString(value) {
let out = redactWhsec(value);
out = redactQuerySecrets(out);
return out;
}
function redactValue(value, key) {
if (typeof value !== 'string') return '[REDACTED]';
if (normKey(key).includes('secret') && value.startsWith('whsec_')) {
return 'whsec_****';
}
return '[REDACTED]';
}
// A sensitive-keyed value may itself be a nested object/array (e.g. `secrets`
// is an array of secret-shaped strings). Recurse into it so each leaf is
// redacted by shape/key rather than blanket-replacing the whole structure with
// `[REDACTED]` — which would also lose the `whsec_****` partial reveal.
function redactSensitiveValue(value, key, seen) {
if (typeof value === 'string') return redactValue(value, key);
if (Array.isArray(value)) {
return value.map((el) => {
if (typeof el === 'string') return redactValue(el, key);
if (el && typeof el === 'object') return redact(el, seen);
return el;
});
}
if (value && typeof value === 'object') return redact(value, seen);
return '[REDACTED]';
}
function redact(node, seen) {
if (!node || typeof node !== 'object') return node;
if (seen.has(node)) return node;
seen.add(node);
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i += 1) {
const el = node[i];
if (typeof el === 'string') {
node[i] = scanString(el);
} else if (el && typeof el === 'object') {
redact(el, seen);
}
}
return node;
}
for (const key of Object.keys(node)) {
const val = node[key];
if (isSensitiveKey(key)) {
node[key] = redactSensitiveValue(val, key, seen);
} else if (typeof val === 'string') {
node[key] = scanString(val);
} else if (val && typeof val === 'object') {
redact(val, seen);
}
}
return node;
}
function redactInfo(info) {
// Track visited objects to avoid infinite recursion on circular structures.
return redact(info, new Set());
}
const redactFormat = winston.format(redactInfo);
module.exports = { redactInfo, redactFormat, SENSITIVE_KEYS };