securenow 5.8.1 → 5.10.0
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/cli/auth.js +1 -1
- package/cli/config.js +6 -0
- package/console-instrumentation.js +6 -0
- package/free-trial-banner.js +7 -1
- package/nextjs-auto-capture.js +1 -9
- package/nextjs-middleware.js +8 -3
- package/nextjs-wrapper.js +1 -1
- package/nextjs.js +27 -130
- package/package.json +1 -1
- package/tracing.js +19 -14
- package/web-vite.mjs +26 -18
package/cli/auth.js
CHANGED
|
@@ -10,7 +10,7 @@ function openBrowser(url) {
|
|
|
10
10
|
try {
|
|
11
11
|
const platform = process.platform;
|
|
12
12
|
if (platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
|
|
13
|
-
else if (platform === 'win32') execFileSync('
|
|
13
|
+
else if (platform === 'win32') execFileSync('rundll32', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore' });
|
|
14
14
|
else execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
15
15
|
return true;
|
|
16
16
|
} catch {
|
package/cli/config.js
CHANGED
|
@@ -32,6 +32,12 @@ function loadJSON(filepath) {
|
|
|
32
32
|
function saveJSON(filepath, data) {
|
|
33
33
|
ensureDir();
|
|
34
34
|
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
try {
|
|
37
|
+
const { execFileSync } = require('child_process');
|
|
38
|
+
execFileSync('icacls', [filepath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`], { stdio: 'ignore' });
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function loadConfig() {
|
|
@@ -36,6 +36,12 @@ if (!logger) {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (console.__securenow_patched) {
|
|
40
|
+
console.warn('[securenow] Console already instrumented by tracing.js — skipping to avoid duplicate logs.');
|
|
41
|
+
module.exports = {};
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
// Store original console methods
|
|
40
46
|
const originalConsole = {
|
|
41
47
|
log: console.log,
|
package/free-trial-banner.js
CHANGED
|
@@ -107,7 +107,8 @@ function patchHttpForBanner() {
|
|
|
107
107
|
if (res._snIsHtml === undefined) {
|
|
108
108
|
var ct = res.getHeader('content-type');
|
|
109
109
|
var ce = res.getHeader('content-encoding');
|
|
110
|
-
|
|
110
|
+
var csp = res.getHeader('content-security-policy');
|
|
111
|
+
res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce && !csp);
|
|
111
112
|
}
|
|
112
113
|
if (!res._snIsHtml) {
|
|
113
114
|
res._snBannerDone = true;
|
|
@@ -136,6 +137,11 @@ function patchHttpForBanner() {
|
|
|
136
137
|
if (modified !== chunk) {
|
|
137
138
|
var enc = typeof encoding === 'function' ? 'utf8' : encoding;
|
|
138
139
|
var callback = typeof encoding === 'function' ? encoding : cb;
|
|
140
|
+
try {
|
|
141
|
+
if (this.getHeader('content-length')) {
|
|
142
|
+
this.setHeader('content-length', Buffer.byteLength(modified));
|
|
143
|
+
}
|
|
144
|
+
} catch (_) { /* headers already sent */ }
|
|
139
145
|
return _origWrite.call(this, modified, enc, callback);
|
|
140
146
|
}
|
|
141
147
|
} catch (_) { /* never break the app */ }
|
package/nextjs-auto-capture.js
CHANGED
|
@@ -33,7 +33,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
33
33
|
|
|
34
34
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
35
35
|
|
|
36
|
-
for (const key
|
|
36
|
+
for (const key of Object.keys(redacted)) {
|
|
37
37
|
const lowerKey = key.toLowerCase();
|
|
38
38
|
|
|
39
39
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -139,7 +139,6 @@ function patchNextRequest() {
|
|
|
139
139
|
|
|
140
140
|
const originalText = Request.prototype.text;
|
|
141
141
|
const originalJson = Request.prototype.json;
|
|
142
|
-
const originalFormData = Request.prototype.formData;
|
|
143
142
|
|
|
144
143
|
// Patch text() to cache result
|
|
145
144
|
Request.prototype.text = async function() {
|
|
@@ -171,13 +170,6 @@ function patchNextRequest() {
|
|
|
171
170
|
return JSON.parse(text);
|
|
172
171
|
};
|
|
173
172
|
|
|
174
|
-
// Patch formData() to cache and capture
|
|
175
|
-
Request.prototype.formData = async function() {
|
|
176
|
-
const text = await this.text();
|
|
177
|
-
const params = new URLSearchParams(text);
|
|
178
|
-
return params;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
173
|
console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
|
|
182
174
|
}
|
|
183
175
|
|
package/nextjs-middleware.js
CHANGED
|
@@ -27,12 +27,16 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
27
27
|
/**
|
|
28
28
|
* Redact sensitive fields from an object
|
|
29
29
|
*/
|
|
30
|
+
function escapeRegex(str) {
|
|
31
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
31
35
|
if (!obj || typeof obj !== 'object') return obj;
|
|
32
36
|
|
|
33
37
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
38
|
|
|
35
|
-
for (const key
|
|
39
|
+
for (const key of Object.keys(redacted)) {
|
|
36
40
|
const lowerKey = key.toLowerCase();
|
|
37
41
|
|
|
38
42
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -54,9 +58,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
54
58
|
let redacted = query;
|
|
55
59
|
|
|
56
60
|
sensitiveFields.forEach(field => {
|
|
61
|
+
const escaped = escapeRegex(field);
|
|
57
62
|
const patterns = [
|
|
58
|
-
new RegExp(`(${
|
|
59
|
-
new RegExp(`(${
|
|
63
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
64
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
60
65
|
];
|
|
61
66
|
|
|
62
67
|
patterns.forEach(pattern => {
|
package/nextjs-wrapper.js
CHANGED
|
@@ -32,7 +32,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
32
32
|
|
|
33
33
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
34
|
|
|
35
|
-
for (const key
|
|
35
|
+
for (const key of Object.keys(redacted)) {
|
|
36
36
|
const lowerKey = key.toLowerCase();
|
|
37
37
|
|
|
38
38
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
package/nextjs.js
CHANGED
|
@@ -68,10 +68,9 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
68
68
|
|
|
69
69
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
70
70
|
|
|
71
|
-
for (const key
|
|
71
|
+
for (const key of Object.keys(redacted)) {
|
|
72
72
|
const lowerKey = key.toLowerCase();
|
|
73
73
|
|
|
74
|
-
// Check if field is sensitive
|
|
75
74
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
76
75
|
redacted[key] = '[REDACTED]';
|
|
77
76
|
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
@@ -83,6 +82,10 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
83
82
|
return redacted;
|
|
84
83
|
}
|
|
85
84
|
|
|
85
|
+
function escapeRegex(str) {
|
|
86
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
}
|
|
88
|
+
|
|
86
89
|
/**
|
|
87
90
|
* Redact sensitive data from GraphQL query strings
|
|
88
91
|
*/
|
|
@@ -94,10 +97,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
94
97
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
95
98
|
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
96
99
|
sensitiveFields.forEach(field => {
|
|
97
|
-
|
|
100
|
+
const escaped = escapeRegex(field);
|
|
98
101
|
const patterns = [
|
|
99
|
-
new RegExp(`(${
|
|
100
|
-
new RegExp(`(${
|
|
102
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
103
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
101
104
|
];
|
|
102
105
|
|
|
103
106
|
patterns.forEach(pattern => {
|
|
@@ -114,115 +117,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
114
117
|
return redacted;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
/**
|
|
118
|
-
* Parse and capture request body safely
|
|
119
|
-
*/
|
|
120
|
-
async function captureRequestBody(request, maxSize = 10240) {
|
|
121
|
-
try {
|
|
122
|
-
const contentType = request.headers['content-type'] || '';
|
|
123
|
-
let body = '';
|
|
124
|
-
|
|
125
|
-
// Collect body chunks
|
|
126
|
-
const chunks = [];
|
|
127
|
-
let size = 0;
|
|
128
|
-
|
|
129
|
-
return new Promise((resolve) => {
|
|
130
|
-
request.on('data', (chunk) => {
|
|
131
|
-
size += chunk.length;
|
|
132
|
-
if (size <= maxSize) {
|
|
133
|
-
chunks.push(chunk);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
request.on('end', () => {
|
|
138
|
-
if (size > maxSize) {
|
|
139
|
-
resolve({
|
|
140
|
-
captured: false,
|
|
141
|
-
reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
|
|
142
|
-
size
|
|
143
|
-
});
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
body = Buffer.concat(chunks).toString('utf8');
|
|
148
|
-
|
|
149
|
-
// Parse based on content type
|
|
150
|
-
if (contentType.includes('application/json')) {
|
|
151
|
-
try {
|
|
152
|
-
const parsed = JSON.parse(body);
|
|
153
|
-
resolve({
|
|
154
|
-
captured: true,
|
|
155
|
-
type: 'json',
|
|
156
|
-
body: parsed,
|
|
157
|
-
size
|
|
158
|
-
});
|
|
159
|
-
} catch (e) {
|
|
160
|
-
resolve({
|
|
161
|
-
captured: true,
|
|
162
|
-
type: 'json',
|
|
163
|
-
body: body.substring(0, 1000),
|
|
164
|
-
parseError: true,
|
|
165
|
-
size
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
} else if (contentType.includes('application/graphql')) {
|
|
169
|
-
// GraphQL queries need redaction too!
|
|
170
|
-
resolve({
|
|
171
|
-
captured: true,
|
|
172
|
-
type: 'graphql',
|
|
173
|
-
body: body, // Will be redacted later
|
|
174
|
-
size
|
|
175
|
-
});
|
|
176
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
177
|
-
// Multipart is NOT captured (files can be huge)
|
|
178
|
-
resolve({
|
|
179
|
-
captured: false,
|
|
180
|
-
type: 'multipart',
|
|
181
|
-
reason: 'Multipart data not captured (file uploads)',
|
|
182
|
-
size
|
|
183
|
-
});
|
|
184
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
185
|
-
try {
|
|
186
|
-
const params = new URLSearchParams(body);
|
|
187
|
-
const parsed = Object.fromEntries(params);
|
|
188
|
-
resolve({
|
|
189
|
-
captured: true,
|
|
190
|
-
type: 'form',
|
|
191
|
-
body: parsed,
|
|
192
|
-
size
|
|
193
|
-
});
|
|
194
|
-
} catch (e) {
|
|
195
|
-
resolve({
|
|
196
|
-
captured: true,
|
|
197
|
-
type: 'form',
|
|
198
|
-
body: body.substring(0, 1000),
|
|
199
|
-
size
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
resolve({
|
|
204
|
-
captured: true,
|
|
205
|
-
type: 'text',
|
|
206
|
-
body: body.substring(0, 1000),
|
|
207
|
-
size
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
request.on('error', () => {
|
|
213
|
-
resolve({ captured: false, reason: 'Stream error' });
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Timeout after 100ms
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
resolve({ captured: false, reason: 'Timeout' });
|
|
219
|
-
}, 100);
|
|
220
|
-
});
|
|
221
|
-
} catch (error) {
|
|
222
|
-
return { captured: false, reason: error.message };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
120
|
/**
|
|
227
121
|
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
228
122
|
* @param {Object} options - Optional configuration
|
|
@@ -281,10 +175,9 @@ function registerSecureNow(options = {}) {
|
|
|
281
175
|
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
282
176
|
const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
283
177
|
|
|
284
|
-
|
|
285
|
-
process.env.
|
|
286
|
-
process.env.
|
|
287
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
178
|
+
if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
|
|
179
|
+
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
180
|
+
if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
288
181
|
|
|
289
182
|
console.log('[securenow] 🚀 Next.js App → service.name=%s', serviceName);
|
|
290
183
|
|
|
@@ -292,7 +185,7 @@ function registerSecureNow(options = {}) {
|
|
|
292
185
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
293
186
|
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
|
|
294
187
|
options.captureBody === true;
|
|
295
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') ||
|
|
188
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
296
189
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
297
190
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
298
191
|
|
|
@@ -326,14 +219,19 @@ function registerSecureNow(options = {}) {
|
|
|
326
219
|
const clientIp = headers['x-client-ip'];
|
|
327
220
|
const socketIp = request.socket?.remoteAddress;
|
|
328
221
|
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
222
|
+
const PRIVATE_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
|
|
223
|
+
const isProxied = socketIp && PRIVATE_RE.test(socketIp);
|
|
224
|
+
let primaryIp = socketIp || 'unknown';
|
|
225
|
+
if (isProxied) {
|
|
226
|
+
if (forwardedFor) {
|
|
227
|
+
const chain = forwardedFor.split(',').map(s => s.trim()).filter(Boolean);
|
|
228
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
229
|
+
if (!PRIVATE_RE.test(chain[i])) { primaryIp = chain[i]; break; }
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
primaryIp = realIp || cfConnectingIp || clientIp || primaryIp;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
337
235
|
|
|
338
236
|
// ======== PROTOCOL & CONNECTION ========
|
|
339
237
|
const scheme = headers['x-forwarded-proto'] ||
|
|
@@ -588,10 +486,9 @@ function registerSecureNow(options = {}) {
|
|
|
588
486
|
const start = Date.now();
|
|
589
487
|
const method = req.method;
|
|
590
488
|
const url = req.url;
|
|
591
|
-
const reqCtx = otelContext.active();
|
|
592
|
-
const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
|
|
593
|
-
|
|
594
489
|
res.on('finish', () => {
|
|
490
|
+
const reqCtx = otelContext.active();
|
|
491
|
+
const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
|
|
595
492
|
const duration = Date.now() - start;
|
|
596
493
|
const status = res.statusCode;
|
|
597
494
|
const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -53,6 +53,10 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
53
53
|
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
+
function escapeRegex(str) {
|
|
57
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
/**
|
|
57
61
|
* Redact sensitive fields from an object
|
|
58
62
|
*/
|
|
@@ -61,7 +65,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
61
65
|
|
|
62
66
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
63
67
|
|
|
64
|
-
for (const key
|
|
68
|
+
for (const key of Object.keys(redacted)) {
|
|
65
69
|
const lowerKey = key.toLowerCase();
|
|
66
70
|
|
|
67
71
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -84,10 +88,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
84
88
|
|
|
85
89
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
86
90
|
sensitiveFields.forEach(field => {
|
|
87
|
-
|
|
91
|
+
const escaped = escapeRegex(field);
|
|
88
92
|
const patterns = [
|
|
89
|
-
new RegExp(`(${
|
|
90
|
-
new RegExp(`(${
|
|
93
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
94
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
91
95
|
];
|
|
92
96
|
|
|
93
97
|
patterns.forEach(pattern => {
|
|
@@ -118,7 +122,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
118
122
|
const boundary = extractBoundary(contentType);
|
|
119
123
|
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
|
|
120
124
|
|
|
121
|
-
const result = { fields:
|
|
125
|
+
const result = { fields: Object.create(null), files: [] };
|
|
122
126
|
let totalSize = 0;
|
|
123
127
|
let buf = Buffer.alloc(0);
|
|
124
128
|
|
|
@@ -139,7 +143,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
139
143
|
let textVal = '';
|
|
140
144
|
|
|
141
145
|
function flushPart() {
|
|
142
|
-
if (!fldName) return;
|
|
146
|
+
if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
|
|
143
147
|
if (isFile) {
|
|
144
148
|
result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
|
|
145
149
|
} else {
|
|
@@ -316,7 +320,7 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
316
320
|
|
|
317
321
|
// -------- Body Capture Configuration --------
|
|
318
322
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
319
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') ||
|
|
323
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
320
324
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
321
325
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
322
326
|
|
|
@@ -328,7 +332,7 @@ const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || S
|
|
|
328
332
|
// This prevents end-users from spoofing their IP via custom headers.
|
|
329
333
|
const os = require('os');
|
|
330
334
|
const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
|
|
331
|
-
const PRIVATE_IP_RE = /^(127\.|::1
|
|
335
|
+
const PRIVATE_IP_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
|
|
332
336
|
const trustedProxyCsv = (env('SECURENOW_TRUSTED_PROXIES') || '').trim();
|
|
333
337
|
const trustedProxySet = trustedProxyCsv ? new Set(trustedProxyCsv.split(',').map(s => s.trim()).filter(Boolean)) : null;
|
|
334
338
|
|
|
@@ -366,7 +370,7 @@ function resolveClientIp(request) {
|
|
|
366
370
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
367
371
|
if (!isFromTrustedProxy(chain[i])) return chain[i];
|
|
368
372
|
}
|
|
369
|
-
return
|
|
373
|
+
return socketIp;
|
|
370
374
|
}
|
|
371
375
|
const headerIp = request.headers['x-real-ip'];
|
|
372
376
|
if (headerIp) return headerIp;
|
|
@@ -390,12 +394,12 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
390
394
|
span.setAttribute('http.client_ip', clientIp);
|
|
391
395
|
}
|
|
392
396
|
|
|
393
|
-
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
397
|
+
if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
394
398
|
const contentType = request.headers['content-type'] || '';
|
|
395
399
|
|
|
396
|
-
if (contentType.includes('application/json') ||
|
|
400
|
+
if (captureBody && (contentType.includes('application/json') ||
|
|
397
401
|
contentType.includes('application/graphql') ||
|
|
398
|
-
contentType.includes('application/x-www-form-urlencoded')) {
|
|
402
|
+
contentType.includes('application/x-www-form-urlencoded'))) {
|
|
399
403
|
|
|
400
404
|
let body = '';
|
|
401
405
|
const chunks = [];
|
|
@@ -443,9 +447,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
443
447
|
});
|
|
444
448
|
}
|
|
445
449
|
} catch (e) {
|
|
446
|
-
|
|
447
|
-
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
450
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
448
451
|
span.setAttribute('http.request.body.parse_error', true);
|
|
452
|
+
span.setAttribute('http.request.body.size', size);
|
|
449
453
|
}
|
|
450
454
|
} else if (size > maxBodySize) {
|
|
451
455
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
@@ -541,6 +545,7 @@ if (loggingEnabled) {
|
|
|
541
545
|
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
542
546
|
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
543
547
|
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
548
|
+
console.__securenow_patched = true;
|
|
544
549
|
}
|
|
545
550
|
|
|
546
551
|
// -------- SDK --------
|
package/web-vite.mjs
CHANGED
|
@@ -55,27 +55,36 @@ const baseName = rawBase || null;
|
|
|
55
55
|
const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
|
|
56
56
|
const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
|
|
57
57
|
|
|
58
|
-
// Simple UUID v4 (no crypto dependency needed)
|
|
59
58
|
function uuidv4(): string {
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
60
|
+
return crypto.randomUUID();
|
|
61
|
+
}
|
|
62
|
+
const bytes = new Uint8Array(16);
|
|
63
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
64
|
+
crypto.getRandomValues(bytes);
|
|
65
|
+
} else {
|
|
66
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
67
|
+
}
|
|
68
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
69
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
70
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
71
|
+
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
let serviceName: string;
|
|
75
|
+
let disabled = false;
|
|
65
76
|
if (baseName) {
|
|
66
77
|
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
67
78
|
} else {
|
|
68
79
|
if (strict) {
|
|
69
80
|
console.error('[securenow/web-vite] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing and SECURENOW_STRICT=1. Tracing disabled.');
|
|
70
|
-
// Do not start tracing
|
|
71
81
|
// @ts-expect-error
|
|
72
82
|
window.__SECURENOW_DISABLED__ = true;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
disabled = true;
|
|
84
|
+
serviceName = 'disabled';
|
|
85
|
+
} else {
|
|
86
|
+
serviceName = `securenow-free-${uuidv4()}`;
|
|
77
87
|
}
|
|
78
|
-
serviceName = `securenow-free-${uuidv4()}`;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
const instancePrefix = baseName || 'securenow';
|
|
@@ -99,7 +108,7 @@ try {
|
|
|
99
108
|
let started = false;
|
|
100
109
|
|
|
101
110
|
export function startSecurenowWeb() {
|
|
102
|
-
if (started) return;
|
|
111
|
+
if (started || disabled) return;
|
|
103
112
|
started = true;
|
|
104
113
|
|
|
105
114
|
const exporter = new OTLPTraceExporter({
|
|
@@ -124,20 +133,21 @@ export function startSecurenowWeb() {
|
|
|
124
133
|
new DocumentLoadInstrumentation(),
|
|
125
134
|
new UserInteractionInstrumentation(),
|
|
126
135
|
new FetchInstrumentation({
|
|
127
|
-
propagateTraceHeaderCorsUrls: [
|
|
136
|
+
propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
|
|
128
137
|
ignoreUrls: [/\/vite\/hmr/, /^chrome-extension:\/\//, /sockjs/],
|
|
129
138
|
}),
|
|
130
139
|
new XMLHttpRequestInstrumentation({
|
|
131
|
-
propagateTraceHeaderCorsUrls: [
|
|
140
|
+
propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
|
|
132
141
|
}),
|
|
133
142
|
],
|
|
134
143
|
});
|
|
135
144
|
|
|
136
145
|
// Optional smoke span (same flag name)
|
|
137
146
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
import('@opentelemetry/api').then(api => {
|
|
148
|
+
const tracer = api.trace.getTracer('securenow-smoke');
|
|
149
|
+
const span = tracer.startSpan('securenow.startup.smoke.web'); span.end();
|
|
150
|
+
}).catch(() => {});
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
// eslint-disable-next-line no-console
|
|
@@ -223,9 +233,7 @@ try {
|
|
|
223
233
|
startSecurenowWeb();
|
|
224
234
|
injectFreeTrialBanner();
|
|
225
235
|
} catch (e: any) {
|
|
226
|
-
|
|
227
|
-
console.error('[securenow/web-vite] failed to start:', e);
|
|
228
|
-
}
|
|
236
|
+
console.error('[securenow/web-vite] failed to start:', e);
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
export default startSecurenowWeb;
|