securenow 4.0.2 → 4.0.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/BODY-CAPTURE-QUICKSTART.md +147 -0
- package/CUSTOMER-GUIDE.md +19 -0
- package/NEXTJS-GUIDE.md +10 -0
- package/NEXTJS-QUICKSTART.md +1 -1
- package/REDACTION-EXAMPLES.md +481 -0
- package/REQUEST-BODY-CAPTURE.md +575 -0
- package/nextjs.js +229 -2
- package/package.json +5 -2
- package/tracing.js +154 -1
package/nextjs.js
CHANGED
|
@@ -25,6 +25,176 @@ const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k
|
|
|
25
25
|
|
|
26
26
|
let isRegistered = false;
|
|
27
27
|
|
|
28
|
+
// Default sensitive fields to redact from request bodies
|
|
29
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
30
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
31
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
32
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Redact sensitive fields from an object
|
|
37
|
+
*/
|
|
38
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
39
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
40
|
+
|
|
41
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
42
|
+
|
|
43
|
+
for (const key in redacted) {
|
|
44
|
+
const lowerKey = key.toLowerCase();
|
|
45
|
+
|
|
46
|
+
// Check if field is sensitive
|
|
47
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
48
|
+
redacted[key] = '[REDACTED]';
|
|
49
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
50
|
+
// Recursively redact nested objects
|
|
51
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return redacted;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Redact sensitive data from GraphQL query strings
|
|
60
|
+
*/
|
|
61
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
62
|
+
if (!query || typeof query !== 'string') return query;
|
|
63
|
+
|
|
64
|
+
let redacted = query;
|
|
65
|
+
|
|
66
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
67
|
+
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
68
|
+
sensitiveFields.forEach(field => {
|
|
69
|
+
// Match field: "value" or field: 'value' or field:"value" (with optional spaces)
|
|
70
|
+
const patterns = [
|
|
71
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
72
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
patterns.forEach(pattern => {
|
|
76
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
77
|
+
if (suffix) {
|
|
78
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
79
|
+
} else {
|
|
80
|
+
return `${prefix}[REDACTED]`;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return redacted;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse and capture request body safely
|
|
91
|
+
*/
|
|
92
|
+
async function captureRequestBody(request, maxSize = 10240) {
|
|
93
|
+
try {
|
|
94
|
+
const contentType = request.headers['content-type'] || '';
|
|
95
|
+
let body = '';
|
|
96
|
+
|
|
97
|
+
// Collect body chunks
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let size = 0;
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
request.on('data', (chunk) => {
|
|
103
|
+
size += chunk.length;
|
|
104
|
+
if (size <= maxSize) {
|
|
105
|
+
chunks.push(chunk);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
request.on('end', () => {
|
|
110
|
+
if (size > maxSize) {
|
|
111
|
+
resolve({
|
|
112
|
+
captured: false,
|
|
113
|
+
reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
|
|
114
|
+
size
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
body = Buffer.concat(chunks).toString('utf8');
|
|
120
|
+
|
|
121
|
+
// Parse based on content type
|
|
122
|
+
if (contentType.includes('application/json')) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(body);
|
|
125
|
+
resolve({
|
|
126
|
+
captured: true,
|
|
127
|
+
type: 'json',
|
|
128
|
+
body: parsed,
|
|
129
|
+
size
|
|
130
|
+
});
|
|
131
|
+
} catch (e) {
|
|
132
|
+
resolve({
|
|
133
|
+
captured: true,
|
|
134
|
+
type: 'json',
|
|
135
|
+
body: body.substring(0, 1000),
|
|
136
|
+
parseError: true,
|
|
137
|
+
size
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} else if (contentType.includes('application/graphql')) {
|
|
141
|
+
// GraphQL queries need redaction too!
|
|
142
|
+
resolve({
|
|
143
|
+
captured: true,
|
|
144
|
+
type: 'graphql',
|
|
145
|
+
body: body, // Will be redacted later
|
|
146
|
+
size
|
|
147
|
+
});
|
|
148
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
149
|
+
// Multipart is NOT captured (files can be huge)
|
|
150
|
+
resolve({
|
|
151
|
+
captured: false,
|
|
152
|
+
type: 'multipart',
|
|
153
|
+
reason: 'Multipart data not captured (file uploads)',
|
|
154
|
+
size
|
|
155
|
+
});
|
|
156
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
157
|
+
try {
|
|
158
|
+
const params = new URLSearchParams(body);
|
|
159
|
+
const parsed = Object.fromEntries(params);
|
|
160
|
+
resolve({
|
|
161
|
+
captured: true,
|
|
162
|
+
type: 'form',
|
|
163
|
+
body: parsed,
|
|
164
|
+
size
|
|
165
|
+
});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
resolve({
|
|
168
|
+
captured: true,
|
|
169
|
+
type: 'form',
|
|
170
|
+
body: body.substring(0, 1000),
|
|
171
|
+
size
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
resolve({
|
|
176
|
+
captured: true,
|
|
177
|
+
type: 'text',
|
|
178
|
+
body: body.substring(0, 1000),
|
|
179
|
+
size
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
request.on('error', () => {
|
|
185
|
+
resolve({ captured: false, reason: 'Stream error' });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Timeout after 100ms
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
resolve({ captured: false, reason: 'Timeout' });
|
|
191
|
+
}, 100);
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { captured: false, reason: error.message };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
28
198
|
/**
|
|
29
199
|
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
30
200
|
* @param {Object} options - Optional configuration
|
|
@@ -86,6 +256,14 @@ function registerSecureNow(options = {}) {
|
|
|
86
256
|
|
|
87
257
|
console.log('[securenow] 🚀 Next.js App → service.name=%s', serviceName);
|
|
88
258
|
|
|
259
|
+
// -------- Body Capture Configuration --------
|
|
260
|
+
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
261
|
+
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
|
|
262
|
+
options.captureBody === true;
|
|
263
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
264
|
+
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
265
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
266
|
+
|
|
89
267
|
// -------- Use @vercel/otel with enhanced configuration --------
|
|
90
268
|
const { registerOTel } = require('@vercel/otel');
|
|
91
269
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
@@ -98,11 +276,11 @@ function registerSecureNow(options = {}) {
|
|
|
98
276
|
'vercel.region': process.env.VERCEL_REGION || undefined,
|
|
99
277
|
},
|
|
100
278
|
instrumentations: [
|
|
101
|
-
// Add HTTP instrumentation with request hooks to capture IP and
|
|
279
|
+
// Add HTTP instrumentation with request hooks to capture IP, headers, and body
|
|
102
280
|
new HttpInstrumentation({
|
|
103
281
|
requireParentforOutgoingSpans: false,
|
|
104
282
|
requireParentforIncomingSpans: false,
|
|
105
|
-
requestHook: (span, request) => {
|
|
283
|
+
requestHook: async (span, request) => {
|
|
106
284
|
try {
|
|
107
285
|
// Capture client IP from various headers
|
|
108
286
|
const headers = request.headers || {};
|
|
@@ -140,6 +318,50 @@ function registerSecureNow(options = {}) {
|
|
|
140
318
|
if (headers['cf-ipcountry']) {
|
|
141
319
|
span.setAttribute('http.geo.country', headers['cf-ipcountry']);
|
|
142
320
|
}
|
|
321
|
+
|
|
322
|
+
// -------- Capture Request Body --------
|
|
323
|
+
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
324
|
+
const contentType = headers['content-type'] || '';
|
|
325
|
+
|
|
326
|
+
// Only capture JSON, GraphQL, and form data (not large files)
|
|
327
|
+
if (contentType.includes('application/json') ||
|
|
328
|
+
contentType.includes('application/graphql') ||
|
|
329
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
330
|
+
|
|
331
|
+
const bodyResult = await captureRequestBody(request, maxBodySize);
|
|
332
|
+
|
|
333
|
+
if (bodyResult.captured) {
|
|
334
|
+
let redactedBody;
|
|
335
|
+
|
|
336
|
+
// Redact based on type
|
|
337
|
+
if (bodyResult.type === 'graphql') {
|
|
338
|
+
// GraphQL: redact query string
|
|
339
|
+
redactedBody = redactGraphQLQuery(bodyResult.body, allSensitiveFields);
|
|
340
|
+
} else if (typeof bodyResult.body === 'object') {
|
|
341
|
+
// JSON/Form: redact object properties
|
|
342
|
+
redactedBody = redactSensitiveData(bodyResult.body, allSensitiveFields);
|
|
343
|
+
} else {
|
|
344
|
+
// Plain text: basic redaction
|
|
345
|
+
redactedBody = bodyResult.body;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
span.setAttributes({
|
|
349
|
+
'http.request.body': typeof redactedBody === 'string'
|
|
350
|
+
? redactedBody.substring(0, maxBodySize)
|
|
351
|
+
: JSON.stringify(redactedBody).substring(0, maxBodySize),
|
|
352
|
+
'http.request.body.type': bodyResult.type,
|
|
353
|
+
'http.request.body.size': bodyResult.size,
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
span.setAttribute('http.request.body.capture_failed', bodyResult.reason || 'unknown');
|
|
357
|
+
}
|
|
358
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
359
|
+
// Multipart is NOT captured at all
|
|
360
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
361
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
362
|
+
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
143
365
|
} catch (error) {
|
|
144
366
|
// Silently fail to not break the request
|
|
145
367
|
console.debug('[securenow] Failed to capture request metadata:', error.message);
|
|
@@ -180,6 +402,11 @@ function registerSecureNow(options = {}) {
|
|
|
180
402
|
isRegistered = true;
|
|
181
403
|
console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
|
|
182
404
|
console.log('[securenow] 📊 Auto-capturing: IP, User-Agent, Headers, Geographic data');
|
|
405
|
+
if (captureBody) {
|
|
406
|
+
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
407
|
+
} else {
|
|
408
|
+
console.log('[securenow] 📝 Request body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
|
|
409
|
+
}
|
|
183
410
|
|
|
184
411
|
// Optional test span
|
|
185
412
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js and Next.js - Send traces to SigNoz or any OTLP backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
"NEXTJS-QUICKSTART.md",
|
|
55
55
|
"CUSTOMER-GUIDE.md",
|
|
56
56
|
"AUTO-SETUP.md",
|
|
57
|
-
"AUTOMATIC-IP-CAPTURE.md"
|
|
57
|
+
"AUTOMATIC-IP-CAPTURE.md",
|
|
58
|
+
"REQUEST-BODY-CAPTURE.md",
|
|
59
|
+
"BODY-CAPTURE-QUICKSTART.md",
|
|
60
|
+
"REDACTION-EXAMPLES.md"
|
|
58
61
|
],
|
|
59
62
|
"dependencies": {
|
|
60
63
|
"@opentelemetry/api": "1.7.0",
|
package/tracing.js
CHANGED
|
@@ -37,6 +37,64 @@ const parseHeaders = str => {
|
|
|
37
37
|
return out;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
// Default sensitive fields to redact from request bodies
|
|
41
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
42
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
43
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
44
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Redact sensitive fields from an object
|
|
49
|
+
*/
|
|
50
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
51
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
52
|
+
|
|
53
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
54
|
+
|
|
55
|
+
for (const key in redacted) {
|
|
56
|
+
const lowerKey = key.toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
59
|
+
redacted[key] = '[REDACTED]';
|
|
60
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
61
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return redacted;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Redact sensitive data from GraphQL query strings
|
|
70
|
+
*/
|
|
71
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
72
|
+
if (!query || typeof query !== 'string') return query;
|
|
73
|
+
|
|
74
|
+
let redacted = query;
|
|
75
|
+
|
|
76
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
|
+
sensitiveFields.forEach(field => {
|
|
78
|
+
// Match patterns: field: "value" or field: 'value' or field:"value"
|
|
79
|
+
const patterns = [
|
|
80
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
81
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
patterns.forEach(pattern => {
|
|
85
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
86
|
+
if (suffix) {
|
|
87
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
88
|
+
} else {
|
|
89
|
+
return `${prefix}[REDACTED]`;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return redacted;
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
// -------- diagnostics --------
|
|
41
99
|
(() => {
|
|
42
100
|
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
@@ -97,11 +155,103 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
97
155
|
disabledMap[n] = { enabled: false };
|
|
98
156
|
}
|
|
99
157
|
|
|
158
|
+
// -------- Body Capture Configuration --------
|
|
159
|
+
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
160
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
161
|
+
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
162
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
163
|
+
|
|
164
|
+
// Configure HTTP instrumentation with body capture
|
|
165
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
166
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
167
|
+
requestHook: (span, request) => {
|
|
168
|
+
try {
|
|
169
|
+
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
170
|
+
const contentType = request.headers['content-type'] || '';
|
|
171
|
+
|
|
172
|
+
if (contentType.includes('application/json') ||
|
|
173
|
+
contentType.includes('application/graphql') ||
|
|
174
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
175
|
+
|
|
176
|
+
let body = '';
|
|
177
|
+
const chunks = [];
|
|
178
|
+
let size = 0;
|
|
179
|
+
|
|
180
|
+
request.on('data', (chunk) => {
|
|
181
|
+
size += chunk.length;
|
|
182
|
+
if (size <= maxBodySize) {
|
|
183
|
+
chunks.push(chunk);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
request.on('end', () => {
|
|
188
|
+
if (size <= maxBodySize && chunks.length > 0) {
|
|
189
|
+
body = Buffer.concat(chunks).toString('utf8');
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let redacted;
|
|
193
|
+
|
|
194
|
+
if (contentType.includes('application/graphql')) {
|
|
195
|
+
// GraphQL: redact query string
|
|
196
|
+
redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
197
|
+
span.setAttributes({
|
|
198
|
+
'http.request.body': redacted.substring(0, maxBodySize),
|
|
199
|
+
'http.request.body.type': 'graphql',
|
|
200
|
+
'http.request.body.size': size,
|
|
201
|
+
});
|
|
202
|
+
} else if (contentType.includes('application/json')) {
|
|
203
|
+
// JSON: parse and redact object
|
|
204
|
+
const parsed = JSON.parse(body);
|
|
205
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
206
|
+
span.setAttributes({
|
|
207
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
208
|
+
'http.request.body.type': 'json',
|
|
209
|
+
'http.request.body.size': size,
|
|
210
|
+
});
|
|
211
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
212
|
+
// Form: parse and redact
|
|
213
|
+
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
214
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
215
|
+
span.setAttributes({
|
|
216
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
217
|
+
'http.request.body.type': 'form',
|
|
218
|
+
'http.request.body.size': size,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Parse error: capture as-is (truncated)
|
|
223
|
+
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
224
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
225
|
+
}
|
|
226
|
+
} else if (size > maxBodySize) {
|
|
227
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
228
|
+
span.setAttribute('http.request.body.size', size);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
232
|
+
// Multipart is NOT captured
|
|
233
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
234
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
235
|
+
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Silently fail
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
100
244
|
// -------- SDK --------
|
|
101
245
|
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
102
246
|
const sdk = new NodeSDK({
|
|
103
247
|
traceExporter,
|
|
104
|
-
instrumentations:
|
|
248
|
+
instrumentations: [
|
|
249
|
+
httpInstrumentation,
|
|
250
|
+
...getNodeAutoInstrumentations({
|
|
251
|
+
...disabledMap,
|
|
252
|
+
'@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
|
|
253
|
+
}),
|
|
254
|
+
],
|
|
105
255
|
resource: new Resource({
|
|
106
256
|
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
107
257
|
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
@@ -115,6 +265,9 @@ const sdk = new NodeSDK({
|
|
|
115
265
|
try {
|
|
116
266
|
await Promise.resolve(sdk.start?.());
|
|
117
267
|
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
268
|
+
if (captureBody) {
|
|
269
|
+
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
270
|
+
}
|
|
118
271
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
119
272
|
const api = require('@opentelemetry/api');
|
|
120
273
|
const tracer = api.trace.getTracer('securenow-smoke');
|