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/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
- // Create .env.local if it doesn't exist
175
- const envPath = path.join(process.cwd(), '.env.local');
176
- if (!fs.existsSync(envPath)) {
177
- createEnvTemplate(envPath);
178
- console.log('✅ Created .env.local template');
179
- }
180
-
181
- console.log('\n┌─────────────────────────────────────────────────┐');
182
- console.log('│ 🚀 Next Steps: │');
183
- console.log('│ │');
184
- console.log('│ 1. Edit .env.local and set: │');
185
- console.log('│ SECURENOW_APPID=your-app-name │');
186
- console.log('│ SECURENOW_INSTANCE=http://signoz:4318 │');
187
- console.log('│ │');
188
- console.log('│ 2. Run your app: npm run dev │');
189
- console.log('│ │');
190
- console.log('│ 3. Check SigNoz for traces! │');
191
- console.log('│ │');
192
- console.log('│ 📚 Full guide: npm docs securenow │');
193
- console.log('└─────────────────────────────────────────────────┘\n');
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: getNodeAutoInstrumentations({ ...disabledMap }),
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');