securenow 4.0.2 → 4.0.5
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/AUTO-BODY-CAPTURE.md +409 -0
- package/BODY-CAPTURE-FIX.md +258 -0
- package/BODY-CAPTURE-QUICKSTART.md +147 -0
- package/CUSTOMER-GUIDE.md +23 -0
- package/EASIEST-SETUP.md +339 -0
- package/FINAL-SOLUTION.md +332 -0
- package/NEXTJS-BODY-CAPTURE-COMPARISON.md +320 -0
- package/NEXTJS-BODY-CAPTURE.md +368 -0
- package/NEXTJS-GUIDE.md +10 -0
- package/NEXTJS-QUICKSTART.md +1 -1
- package/NEXTJS-WRAPPER-APPROACH.md +411 -0
- package/QUICKSTART-BODY-CAPTURE.md +287 -0
- package/REDACTION-EXAMPLES.md +481 -0
- package/REQUEST-BODY-CAPTURE.md +575 -0
- package/SOLUTION-SUMMARY.md +309 -0
- package/cli.js +1 -1
- package/examples/instrumentation-with-auto-capture.ts +38 -0
- package/examples/nextjs-api-route-with-body-capture.ts +51 -0
- package/examples/nextjs-middleware.js +34 -0
- package/examples/nextjs-middleware.ts +34 -0
- package/nextjs-auto-capture.js +204 -0
- package/nextjs-middleware.js +178 -0
- package/nextjs-wrapper.js +155 -0
- package/nextjs.js +204 -14
- package/package.json +20 -2
- package/postinstall.js +117 -22
- package/tracing.js +154 -1
package/postinstall.js
CHANGED
|
@@ -80,6 +80,64 @@ export function register() {
|
|
|
80
80
|
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
81
81
|
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
82
82
|
* OTEL_LOG_LEVEL=info
|
|
83
|
+
*
|
|
84
|
+
* Optional: Enable request body capture
|
|
85
|
+
* SECURENOW_CAPTURE_BODY=1
|
|
86
|
+
* (Also create middleware.ts to activate - run: npx securenow init)
|
|
87
|
+
*/
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create TypeScript middleware file
|
|
94
|
+
function createTsMiddleware(targetPath) {
|
|
95
|
+
const content = `// SecureNow Middleware - Automatic Request Body Capture
|
|
96
|
+
// This enables capturing JSON, GraphQL, and Form request bodies
|
|
97
|
+
// with automatic sensitive field redaction
|
|
98
|
+
|
|
99
|
+
export { middleware } from 'securenow/nextjs-middleware';
|
|
100
|
+
|
|
101
|
+
export const config = {
|
|
102
|
+
matcher: '/api/:path*', // Apply to all API routes
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Bodies are captured with:
|
|
107
|
+
* - Automatic redaction of passwords, tokens, cards, etc.
|
|
108
|
+
* - Size limits (configurable via SECURENOW_MAX_BODY_SIZE)
|
|
109
|
+
* - JSON, GraphQL, Form data support
|
|
110
|
+
*
|
|
111
|
+
* Configure in .env.local:
|
|
112
|
+
* SECURENOW_MAX_BODY_SIZE=20480
|
|
113
|
+
* SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
114
|
+
*/
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create JavaScript middleware file
|
|
121
|
+
function createJsMiddleware(targetPath) {
|
|
122
|
+
const content = `// SecureNow Middleware - Automatic Request Body Capture
|
|
123
|
+
// This enables capturing JSON, GraphQL, and Form request bodies
|
|
124
|
+
// with automatic sensitive field redaction
|
|
125
|
+
|
|
126
|
+
export { middleware } from 'securenow/nextjs-middleware';
|
|
127
|
+
|
|
128
|
+
export const config = {
|
|
129
|
+
matcher: '/api/:path*', // Apply to all API routes
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Bodies are captured with:
|
|
134
|
+
* - Automatic redaction of passwords, tokens, cards, etc.
|
|
135
|
+
* - Size limits (configurable via SECURENOW_MAX_BODY_SIZE)
|
|
136
|
+
* - JSON, GraphQL, Form data support
|
|
137
|
+
*
|
|
138
|
+
* Configure in .env.local:
|
|
139
|
+
* SECURENOW_MAX_BODY_SIZE=20480
|
|
140
|
+
* SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
83
141
|
*/
|
|
84
142
|
`;
|
|
85
143
|
|
|
@@ -101,6 +159,11 @@ SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
|
101
159
|
|
|
102
160
|
# Optional: Log level (debug|info|warn|error)
|
|
103
161
|
# OTEL_LOG_LEVEL=info
|
|
162
|
+
|
|
163
|
+
# Optional: Enable request body capture (requires middleware.ts)
|
|
164
|
+
# SECURENOW_CAPTURE_BODY=1
|
|
165
|
+
# SECURENOW_MAX_BODY_SIZE=10240
|
|
166
|
+
# SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
104
167
|
`;
|
|
105
168
|
|
|
106
169
|
fs.writeFileSync(targetPath, content, 'utf8');
|
|
@@ -171,33 +234,65 @@ async function setup() {
|
|
|
171
234
|
|
|
172
235
|
console.log(`\n✅ Created ${srcExists ? 'src/' : ''}${fileName}`);
|
|
173
236
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
// Ask about middleware for body capture
|
|
238
|
+
rl.question('\nWould you like to enable request body capture? (y/N) ', (middlewareAnswer) => {
|
|
239
|
+
const shouldCreateMiddleware = middlewareAnswer && (middlewareAnswer.toLowerCase() === 'y' || middlewareAnswer.toLowerCase() === 'yes');
|
|
240
|
+
|
|
241
|
+
if (shouldCreateMiddleware) {
|
|
242
|
+
try {
|
|
243
|
+
const middlewareName = useTypeScript ? 'middleware.ts' : 'middleware.js';
|
|
244
|
+
const middlewarePath = srcExists
|
|
245
|
+
? path.join(process.cwd(), 'src', middlewareName)
|
|
246
|
+
: path.join(process.cwd(), middlewareName);
|
|
247
|
+
|
|
248
|
+
if (useTypeScript) {
|
|
249
|
+
createTsMiddleware(middlewarePath);
|
|
250
|
+
} else {
|
|
251
|
+
createJsMiddleware(middlewarePath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`✅ Created ${srcExists ? 'src/' : ''}${middlewareName}`);
|
|
255
|
+
console.log(' → Captures JSON, GraphQL, Form bodies with auto-redaction');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`⚠️ Could not create middleware: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create .env.local if it doesn't exist
|
|
262
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
263
|
+
if (!fs.existsSync(envPath)) {
|
|
264
|
+
createEnvTemplate(envPath);
|
|
265
|
+
console.log('✅ Created .env.local template');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log('\n┌─────────────────────────────────────────────────┐');
|
|
269
|
+
console.log('│ 🚀 Next Steps: │');
|
|
270
|
+
console.log('│ │');
|
|
271
|
+
console.log('│ 1. Edit .env.local and set: │');
|
|
272
|
+
console.log('│ SECURENOW_APPID=your-app-name │');
|
|
273
|
+
console.log('│ SECURENOW_INSTANCE=http://signoz:4318 │');
|
|
274
|
+
if (shouldCreateMiddleware) {
|
|
275
|
+
console.log('│ SECURENOW_CAPTURE_BODY=1 │');
|
|
276
|
+
}
|
|
277
|
+
console.log('│ │');
|
|
278
|
+
console.log('│ 2. Run your app: npm run dev │');
|
|
279
|
+
console.log('│ │');
|
|
280
|
+
console.log('│ 3. Check SigNoz for traces! │');
|
|
281
|
+
console.log('│ │');
|
|
282
|
+
if (shouldCreateMiddleware) {
|
|
283
|
+
console.log('│ 📝 Body capture enabled with auto-redaction │');
|
|
284
|
+
}
|
|
285
|
+
console.log('│ 📚 Full guide: npm docs securenow │');
|
|
286
|
+
console.log('└─────────────────────────────────────────────────┘\n');
|
|
287
|
+
|
|
288
|
+
rl.close();
|
|
289
|
+
});
|
|
194
290
|
|
|
195
291
|
} catch (error) {
|
|
196
292
|
console.error('\n❌ Failed to create instrumentation file:', error.message);
|
|
197
293
|
console.log('💡 You can create it manually or run: npx securenow init');
|
|
294
|
+
rl.close();
|
|
198
295
|
}
|
|
199
|
-
|
|
200
|
-
rl.close();
|
|
201
296
|
});
|
|
202
297
|
}
|
|
203
298
|
|
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');
|