securenow 8.11.1 → 8.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +3 -1
- package/README.md +2 -0
- package/SKILL-API.md +3 -1
- package/SKILL-CLI.md +625 -632
- package/TELEMETRY-SQL.md +6 -1
- package/cidr.js +83 -83
- package/cli/client.js +102 -102
- package/cli/diagnostics.js +4 -0
- package/cli/firewall.js +141 -141
- package/cli/fp.js +638 -638
- package/cli/monitor.js +398 -398
- package/cli/run.js +132 -132
- package/cli/security.js +1866 -1856
- package/cli/ui.js +374 -374
- package/cli.js +923 -922
- package/firewall-cloud.js +202 -202
- package/firewall-iptables.js +139 -139
- package/firewall-only.js +46 -46
- package/firewall-tcp.js +74 -74
- package/firewall.js +1470 -1470
- package/legacy-semconv.js +4 -3
- package/mcp/catalog.js +2358 -2346
- package/nextjs-auto-capture.d.ts +29 -29
- package/nextjs-auto-capture.js +280 -274
- package/nextjs-middleware.d.ts +57 -57
- package/nextjs-middleware.js +274 -268
- package/nextjs-webpack-config.js +79 -79
- package/nextjs-wrapper.d.ts +95 -95
- package/nextjs-wrapper.js +240 -234
- package/nextjs.js +19 -12
- package/nuxt-server-plugin.mjs +11 -5
- package/nuxt.mjs +74 -74
- package/otp-redaction.js +99 -0
- package/package.json +3 -2
- package/register.d.ts +42 -42
- package/register.js +44 -44
- package/resolve-ip.js +13 -13
- package/telemetry-schema.json +3 -1
- package/tracing.js +860 -855
package/nextjs-wrapper.js
CHANGED
|
@@ -1,234 +1,240 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Next.js API Route Wrapper for Body Capture
|
|
3
|
-
*
|
|
4
|
-
* This approach is NON-INVASIVE and runs INSIDE your handler,
|
|
5
|
-
* so it never blocks or interferes with middleware or routing.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
*
|
|
9
|
-
* import { withSecureNow } from 'securenow/nextjs-wrapper';
|
|
10
|
-
*
|
|
11
|
-
* export const POST = withSecureNow(async (request) => {
|
|
12
|
-
* // Your handler code - request.body is available as parsed JSON
|
|
13
|
-
* const data = await request.json();
|
|
14
|
-
* return Response.json({ success: true });
|
|
15
|
-
* });
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const { trace } = require('@opentelemetry/api');
|
|
19
|
-
const appConfig = require('./app-config');
|
|
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
|
-
{ name: '
|
|
57
|
-
//
|
|
58
|
-
{ name: '
|
|
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
|
-
redacted[key] =
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
span.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js API Route Wrapper for Body Capture
|
|
3
|
+
*
|
|
4
|
+
* This approach is NON-INVASIVE and runs INSIDE your handler,
|
|
5
|
+
* so it never blocks or interferes with middleware or routing.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* import { withSecureNow } from 'securenow/nextjs-wrapper';
|
|
10
|
+
*
|
|
11
|
+
* export const POST = withSecureNow(async (request) => {
|
|
12
|
+
* // Your handler code - request.body is available as parsed JSON
|
|
13
|
+
* const data = await request.json();
|
|
14
|
+
* return Response.json({ success: true });
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { trace } = require('@opentelemetry/api');
|
|
19
|
+
const appConfig = require('./app-config');
|
|
20
|
+
|
|
21
|
+
const { OTP_SENSITIVE_FIELDS, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery } = require('./otp-redaction');
|
|
22
|
+
|
|
23
|
+
// Default sensitive fields to redact from request bodies.
|
|
24
|
+
// OTP/code aliases use exact normalized matching; other fields are matched
|
|
25
|
+
// substring-wise against lowercased keys (see redactSensitiveData),
|
|
26
|
+
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
27
|
+
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
28
|
+
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
29
|
+
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
30
|
+
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
31
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
32
|
+
...OTP_SENSITIVE_FIELDS,
|
|
33
|
+
// credentials / auth
|
|
34
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
35
|
+
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
36
|
+
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
37
|
+
'cookie', 'set-cookie',
|
|
38
|
+
// financial
|
|
39
|
+
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
40
|
+
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
41
|
+
// PII
|
|
42
|
+
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
43
|
+
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
44
|
+
'passport', 'license',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
48
|
+
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
49
|
+
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
50
|
+
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
51
|
+
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
|
+
// the actual request/response stream. Compiled once at module load.
|
|
53
|
+
const VALUE_REDACTORS = [
|
|
54
|
+
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
|
+
// match arbitrary dotted tokens.
|
|
56
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
57
|
+
// Bearer/Basic auth header value embedded in a body string.
|
|
58
|
+
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
|
+
// Stripe-style / SecureNow live+test API keys.
|
|
60
|
+
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
+
// Email addresses (bounded local/domain parts).
|
|
62
|
+
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
63
|
+
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
|
+
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
+
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
69
|
+
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
70
|
+
|
|
71
|
+
function luhnValid(digits) {
|
|
72
|
+
let sum = 0;
|
|
73
|
+
let alt = false;
|
|
74
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
75
|
+
let d = digits.charCodeAt(i) - 48;
|
|
76
|
+
if (d < 0 || d > 9) return false;
|
|
77
|
+
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
78
|
+
sum += d;
|
|
79
|
+
alt = !alt;
|
|
80
|
+
}
|
|
81
|
+
return sum % 10 === 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
86
|
+
* Returns the input unchanged when nothing matches (cheap common case).
|
|
87
|
+
*/
|
|
88
|
+
function redactSensitiveValue(value) {
|
|
89
|
+
if (typeof value !== 'string' || value.length === 0) return value;
|
|
90
|
+
// Credential assignments in GraphQL/query strings need field-aware
|
|
91
|
+
// redaction even though a short numeric OTP has no unique value shape.
|
|
92
|
+
value = redactOtpQuery(redactOtpGraphQL(value));
|
|
93
|
+
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
|
+
let out = value;
|
|
95
|
+
for (const r of VALUE_REDACTORS) {
|
|
96
|
+
r.re.lastIndex = 0;
|
|
97
|
+
if (r.luhn) {
|
|
98
|
+
out = out.replace(r.re, (m) => {
|
|
99
|
+
const digits = m.replace(/[ -]/g, '');
|
|
100
|
+
if (digits.length < 13 || digits.length > 19) return m;
|
|
101
|
+
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
out = out.replace(r.re, '[REDACTED]');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Redact sensitive fields from an object
|
|
112
|
+
*/
|
|
113
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
114
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
115
|
+
|
|
116
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
117
|
+
|
|
118
|
+
for (const key of Object.keys(redacted)) {
|
|
119
|
+
|
|
120
|
+
if (matchesSensitiveField(key, sensitiveFields)) {
|
|
121
|
+
redacted[key] = '[REDACTED]';
|
|
122
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
123
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
124
|
+
} else if (typeof redacted[key] === 'string') {
|
|
125
|
+
// Second layer: even if the key name looks benign, scrub values that
|
|
126
|
+
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
127
|
+
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return redacted;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Capture body from Request object (clone to avoid consuming)
|
|
136
|
+
*/
|
|
137
|
+
async function captureRequestBody(request) {
|
|
138
|
+
const captureBody = appConfig.boolConfig('capture.body', true);
|
|
139
|
+
|
|
140
|
+
if (!captureBody) return;
|
|
141
|
+
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) return;
|
|
142
|
+
|
|
143
|
+
const span = trace.getActiveSpan();
|
|
144
|
+
if (!span) return;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const contentType = request.headers.get('content-type') || '';
|
|
148
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
149
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
150
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
151
|
+
|
|
152
|
+
// Only for supported types
|
|
153
|
+
if (!contentType.includes('application/json') &&
|
|
154
|
+
!contentType.includes('application/graphql') &&
|
|
155
|
+
!contentType.includes('application/x-www-form-urlencoded')) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clone to avoid consuming the original
|
|
160
|
+
const cloned = request.clone();
|
|
161
|
+
const bodyText = await cloned.text();
|
|
162
|
+
|
|
163
|
+
if (bodyText.length > maxBodySize) {
|
|
164
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
165
|
+
span.setAttribute('http.request.body.size', bodyText.length);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Parse and redact based on type
|
|
170
|
+
let redacted;
|
|
171
|
+
if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(bodyText);
|
|
174
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
175
|
+
span.setAttributes({
|
|
176
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
177
|
+
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
178
|
+
'http.request.body.size': bodyText.length,
|
|
179
|
+
});
|
|
180
|
+
} catch (e) {
|
|
181
|
+
span.setAttribute('http.request.body', '[INVALID JSON]');
|
|
182
|
+
}
|
|
183
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
184
|
+
const params = new URLSearchParams(bodyText);
|
|
185
|
+
const parsed = Object.fromEntries(params);
|
|
186
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
187
|
+
span.setAttributes({
|
|
188
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
189
|
+
'http.request.body.type': 'form',
|
|
190
|
+
'http.request.body.size': bodyText.length,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// Silently fail - never block the request
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Wrap a Next.js API route handler to capture body
|
|
200
|
+
* This is OPTIONAL and NON-INVASIVE - only use on routes where you want body capture
|
|
201
|
+
*/
|
|
202
|
+
function withSecureNow(handler) {
|
|
203
|
+
return async function wrappedHandler(request, context) {
|
|
204
|
+
// Capture body asynchronously (doesn't block handler)
|
|
205
|
+
captureRequestBody(request).catch(() => {
|
|
206
|
+
// Ignore errors silently
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Call original handler immediately - no blocking!
|
|
210
|
+
return handler(request, context);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Alternative: Auto-capture wrapper that tries to capture AFTER handler runs
|
|
216
|
+
* This is even safer as it never interferes with the handler logic
|
|
217
|
+
*/
|
|
218
|
+
function withSecureNowAsync(handler) {
|
|
219
|
+
return async function wrappedHandler(request, context) {
|
|
220
|
+
// Try to capture body in background (non-blocking)
|
|
221
|
+
const capturePromise = captureRequestBody(request);
|
|
222
|
+
|
|
223
|
+
// Run handler
|
|
224
|
+
const response = await handler(request, context);
|
|
225
|
+
|
|
226
|
+
// Wait for capture to finish (but don't fail if it doesn't)
|
|
227
|
+
await capturePromise.catch(() => {});
|
|
228
|
+
|
|
229
|
+
return response;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
withSecureNow,
|
|
235
|
+
withSecureNowAsync,
|
|
236
|
+
captureRequestBody,
|
|
237
|
+
redactSensitiveData,
|
|
238
|
+
DEFAULT_SENSITIVE_FIELDS,
|
|
239
|
+
};
|
|
240
|
+
|
package/nextjs.js
CHANGED
|
@@ -59,14 +59,18 @@ function createResource(attributes) {
|
|
|
59
59
|
throw new Error('Unsupported @opentelemetry/resources version');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const { OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery, withOtpRedactionExporter } = require('./otp-redaction');
|
|
63
|
+
|
|
62
64
|
// Default sensitive fields to redact from request bodies.
|
|
63
|
-
//
|
|
65
|
+
// OTP/code aliases use exact normalized matching; other fields are matched
|
|
66
|
+
// substring-wise against lowercased keys (see redactSensitiveData),
|
|
64
67
|
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
65
68
|
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
66
69
|
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
67
70
|
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
68
71
|
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
69
72
|
const DEFAULT_SENSITIVE_FIELDS = [
|
|
73
|
+
...OTP_SENSITIVE_FIELDS,
|
|
70
74
|
// credentials / auth
|
|
71
75
|
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
72
76
|
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
@@ -124,7 +128,10 @@ function luhnValid(digits) {
|
|
|
124
128
|
*/
|
|
125
129
|
function redactSensitiveValue(value) {
|
|
126
130
|
if (typeof value !== 'string' || value.length === 0) return value;
|
|
127
|
-
|
|
131
|
+
// Credential assignments in GraphQL/query strings need field-aware
|
|
132
|
+
// redaction even though a short numeric OTP has no unique value shape.
|
|
133
|
+
value = redactOtpQuery(redactOtpGraphQL(value));
|
|
134
|
+
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
128
135
|
let out = value;
|
|
129
136
|
for (const r of VALUE_REDACTORS) {
|
|
130
137
|
r.re.lastIndex = 0;
|
|
@@ -150,9 +157,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
150
157
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
151
158
|
|
|
152
159
|
for (const key of Object.keys(redacted)) {
|
|
153
|
-
const lowerKey = key.toLowerCase();
|
|
154
160
|
|
|
155
|
-
if (
|
|
161
|
+
if (matchesSensitiveField(key, sensitiveFields)) {
|
|
156
162
|
redacted[key] = '[REDACTED]';
|
|
157
163
|
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
158
164
|
// Recursively redact nested objects
|
|
@@ -181,7 +187,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
181
187
|
|
|
182
188
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
183
189
|
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
184
|
-
sensitiveFields.forEach(field => {
|
|
190
|
+
sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
|
|
185
191
|
const escaped = escapeRegex(field);
|
|
186
192
|
const patterns = [
|
|
187
193
|
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
@@ -201,7 +207,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
201
207
|
|
|
202
208
|
// Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
|
|
203
209
|
// body — catches values the key-name pass above can't see.
|
|
204
|
-
redacted = redactSensitiveValue(redacted);
|
|
210
|
+
redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
|
|
205
211
|
|
|
206
212
|
return redacted;
|
|
207
213
|
}
|
|
@@ -320,7 +326,7 @@ function registerSecureNow(options = {}) {
|
|
|
320
326
|
|
|
321
327
|
// ======== REQUEST METADATA ========
|
|
322
328
|
const userAgent = headers['user-agent'] || '';
|
|
323
|
-
const referer = headers['referer'] || headers['referrer'] || '';
|
|
329
|
+
const referer = redactOtpQuery(headers['referer'] || headers['referrer'] || '');
|
|
324
330
|
const accept = headers['accept'] || '';
|
|
325
331
|
const acceptLanguage = headers['accept-language'] || '';
|
|
326
332
|
const acceptEncoding = headers['accept-encoding'] || '';
|
|
@@ -329,7 +335,7 @@ function registerSecureNow(options = {}) {
|
|
|
329
335
|
const origin = headers['origin'] || '';
|
|
330
336
|
|
|
331
337
|
// ======== PROXY & LOAD BALANCER ========
|
|
332
|
-
const originalUri = headers['x-original-uri'] || '';
|
|
338
|
+
const originalUri = redactOtpQuery(headers['x-original-uri'] || '');
|
|
333
339
|
const originalMethod = headers['x-original-method'] || '';
|
|
334
340
|
const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
|
|
335
341
|
|
|
@@ -528,6 +534,7 @@ function registerSecureNow(options = {}) {
|
|
|
528
534
|
const { registerOTel } = require('@vercel/otel');
|
|
529
535
|
|
|
530
536
|
registerOTel({
|
|
537
|
+
traceExporter: withOtpRedactionExporter(new (require('@opentelemetry/exporter-trace-otlp-http').OTLPTraceExporter)({ url: tracesUrl, headers })),
|
|
531
538
|
serviceName: serviceName,
|
|
532
539
|
attributes: {
|
|
533
540
|
'deployment.environment': deploymentEnvironment,
|
|
@@ -558,10 +565,10 @@ function registerSecureNow(options = {}) {
|
|
|
558
565
|
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
559
566
|
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
560
567
|
|
|
561
|
-
const traceExporter = new OTLPTraceExporter({
|
|
568
|
+
const traceExporter = withOtpRedactionExporter(new OTLPTraceExporter({
|
|
562
569
|
url: tracesUrl,
|
|
563
|
-
|
|
564
|
-
});
|
|
570
|
+
headers
|
|
571
|
+
}));
|
|
565
572
|
|
|
566
573
|
const sdk = new NodeSDK({
|
|
567
574
|
...nodeSdkTelemetryOptions,
|
|
@@ -643,7 +650,7 @@ function registerSecureNow(options = {}) {
|
|
|
643
650
|
if (event === 'request' && req && res) {
|
|
644
651
|
const start = Date.now();
|
|
645
652
|
const method = req.method;
|
|
646
|
-
const url = req.url;
|
|
653
|
+
const url = redactOtpQuery(req.url);
|
|
647
654
|
res.on('finish', () => {
|
|
648
655
|
const reqCtx = otelContext.active();
|
|
649
656
|
const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
|