securenow 7.7.11 → 7.7.12

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.
@@ -97,7 +97,8 @@ var BANNER_SCRIPT =
97
97
  */
98
98
  function patchHttpForBanner() {
99
99
  try {
100
- var http = require('http');
100
+ var nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
101
+ var http = nodeRequire('node:http');
101
102
  var _origWrite = http.ServerResponse.prototype.write;
102
103
  var _origEnd = http.ServerResponse.prototype.end;
103
104
 
package/nextjs.js CHANGED
@@ -34,6 +34,15 @@ const env = appConfig.env;
34
34
 
35
35
  let isRegistered = false;
36
36
 
37
+ function requireRuntimeModule(name) {
38
+ const nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
39
+ return nodeRequire(name);
40
+ }
41
+
42
+ function requireNodeBuiltin(name) {
43
+ return requireRuntimeModule(name);
44
+ }
45
+
37
46
  function createResource(attributes) {
38
47
  if (typeof otelResources.resourceFromAttributes === 'function') {
39
48
  return otelResources.resourceFromAttributes(attributes);
@@ -501,7 +510,7 @@ function registerSecureNow(options = {}) {
501
510
 
502
511
  // Auto-log every incoming HTTP request/response
503
512
  try {
504
- const http = require('http');
513
+ const http = requireNodeBuiltin('node:http');
505
514
  const originalEmit = http.Server.prototype.emit;
506
515
  http.Server.prototype.emit = function (event, req, res) {
507
516
  if (event === 'request' && req && res) {
@@ -549,8 +558,8 @@ function registerSecureNow(options = {}) {
549
558
  } catch (_) {}
550
559
 
551
560
  // Graceful shutdown for logs
552
- process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
553
- process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
561
+ process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
562
+ process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
554
563
  } catch (e) {
555
564
  console.warn('[securenow] ⚠️ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
556
565
  }
@@ -563,7 +572,7 @@ function registerSecureNow(options = {}) {
563
572
 
564
573
  // Free trial banner (optional — may not be bundled in standalone builds)
565
574
  try {
566
- const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
575
+ const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
567
576
  if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
568
577
  patchHttpForBanner();
569
578
  }
@@ -607,7 +616,7 @@ function registerSecureNow(options = {}) {
607
616
  const firewallOptions = appConfig.resolveFirewallOptions();
608
617
  if (firewallOptions.apiKey && firewallOptions.enabled) {
609
618
  try {
610
- require('./firewall').init({
619
+ requireRuntimeModule('./firewall').init({
611
620
  apiKey: firewallOptions.apiKey,
612
621
  appKey: firewallOptions.appKey,
613
622
  environment: deploymentEnvironment || firewallOptions.environment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.11",
3
+ "version": "7.7.12",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
package/tracing.js CHANGED
@@ -124,9 +124,9 @@ function extractBoundary(contentType) {
124
124
  return match ? (match[1] || match[2]) : null;
125
125
  }
126
126
 
127
- function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
127
+ function createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete) {
128
128
  const boundary = extractBoundary(contentType);
129
- if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
129
+ if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return null; }
130
130
 
131
131
  const result = { fields: Object.create(null), files: [] };
132
132
  let totalSize = 0;
@@ -219,13 +219,14 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
219
219
  }
220
220
  }
221
221
 
222
- request.on('data', (chunk) => {
223
- totalSize += chunk.length;
224
- buf = Buffer.concat([buf, chunk]);
222
+ function onData(chunk) {
223
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
224
+ totalSize += data.length;
225
+ buf = Buffer.concat([buf, data]);
225
226
  drain();
226
- });
227
+ }
227
228
 
228
- request.on('end', () => {
229
+ function onEnd() {
229
230
  try {
230
231
  if (!inHeaders && fldName) {
231
232
  bodyBytes += buf.length;
@@ -236,7 +237,16 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
236
237
  } catch (e) {
237
238
  onComplete({ error: 'PARSE_ERROR' });
238
239
  }
239
- });
240
+ }
241
+
242
+ return { onData, onEnd };
243
+ }
244
+
245
+ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
246
+ const collector = createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete);
247
+ if (!collector) return;
248
+ request.on('data', collector.onData);
249
+ request.on('end', collector.onEnd);
240
250
  }
241
251
 
242
252
  // -------- ESM detection --------
@@ -342,6 +352,133 @@ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveField
342
352
 
343
353
  const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
344
354
 
355
+ const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
356
+
357
+ function installRequestBodyObserver(span, request, contentType) {
358
+ if (!request || request[BODY_CAPTURE_PATCH]) return;
359
+
360
+ const normalizedContentType = String(contentType || '').toLowerCase();
361
+ const isStructuredBody = captureBody && (
362
+ normalizedContentType.includes('application/json') ||
363
+ normalizedContentType.includes('application/graphql') ||
364
+ normalizedContentType.includes('application/x-www-form-urlencoded')
365
+ );
366
+ const isMultipartBody = normalizedContentType.includes('multipart/form-data');
367
+
368
+ if (!isStructuredBody && !isMultipartBody) return;
369
+
370
+ if (isMultipartBody && !captureMultipart) {
371
+ span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
372
+ span.setAttribute('http.request.body.type', 'multipart');
373
+ span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
374
+ return;
375
+ }
376
+
377
+ let chunks = [];
378
+ let size = 0;
379
+ let structuredDone = false;
380
+
381
+ const multipartCollector = isMultipartBody && captureMultipart
382
+ ? createMultipartMetaCollector(normalizedContentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
383
+ try {
384
+ if (error === 'BOUNDARY_NOT_FOUND') {
385
+ span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
386
+ span.setAttribute('http.request.body.type', 'multipart');
387
+ return;
388
+ }
389
+ if (error) {
390
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
391
+ span.setAttribute('http.request.body.type', 'multipart');
392
+ span.setAttribute('http.request.body.parse_error', true);
393
+ return;
394
+ }
395
+ span.setAttributes({
396
+ 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
397
+ 'http.request.body.type': 'multipart',
398
+ 'http.request.body.size': totalSize,
399
+ 'http.request.body.fields_count': Object.keys(parsed.fields).length,
400
+ 'http.request.body.files_count': parsed.files.length,
401
+ });
402
+ } catch (e) {
403
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
404
+ span.setAttribute('http.request.body.type', 'multipart');
405
+ span.setAttribute('http.request.body.parse_error', true);
406
+ }
407
+ })
408
+ : null;
409
+
410
+ if (isMultipartBody && captureMultipart && !multipartCollector) return;
411
+
412
+ function finishStructuredCapture() {
413
+ if (!isStructuredBody || structuredDone) return;
414
+ structuredDone = true;
415
+
416
+ if (size > maxBodySize) {
417
+ span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
418
+ span.setAttribute('http.request.body.size', size);
419
+ chunks = [];
420
+ return;
421
+ }
422
+
423
+ if (chunks.length === 0) return;
424
+
425
+ const body = Buffer.concat(chunks).toString('utf8');
426
+ chunks = [];
427
+
428
+ try {
429
+ if (normalizedContentType.includes('application/graphql')) {
430
+ const redacted = redactGraphQLQuery(body, allSensitiveFields);
431
+ span.setAttributes({
432
+ 'http.request.body': redacted.substring(0, maxBodySize),
433
+ 'http.request.body.type': 'graphql',
434
+ 'http.request.body.size': size,
435
+ });
436
+ } else if (normalizedContentType.includes('application/json')) {
437
+ const parsed = JSON.parse(body);
438
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
439
+ span.setAttributes({
440
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
441
+ 'http.request.body.type': 'json',
442
+ 'http.request.body.size': size,
443
+ });
444
+ } else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
445
+ const parsed = Object.fromEntries(new URLSearchParams(body));
446
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
447
+ span.setAttributes({
448
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
449
+ 'http.request.body.type': 'form',
450
+ 'http.request.body.size': size,
451
+ });
452
+ }
453
+ } catch (e) {
454
+ span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
455
+ span.setAttribute('http.request.body.parse_error', true);
456
+ span.setAttribute('http.request.body.size', size);
457
+ }
458
+ }
459
+
460
+ const originalEmit = request.emit;
461
+ request[BODY_CAPTURE_PATCH] = true;
462
+ request.emit = function securenowObservedEmit(event, ...args) {
463
+ try {
464
+ if (event === 'data' && args.length > 0) {
465
+ const chunk = Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]);
466
+ if (isStructuredBody) {
467
+ size += chunk.length;
468
+ if (size <= maxBodySize) chunks.push(chunk);
469
+ }
470
+ if (multipartCollector) multipartCollector.onData(chunk);
471
+ } else if (event === 'end') {
472
+ finishStructuredCapture();
473
+ if (multipartCollector) multipartCollector.onEnd();
474
+ }
475
+ } catch (_) {
476
+ // Body capture must never interfere with the application request stream.
477
+ }
478
+ return originalEmit.apply(this, [event, ...args]);
479
+ };
480
+ }
481
+
345
482
  // -------- Trusted proxy IP resolution --------
346
483
  const { resolveClientIpWithDetails } = require('./resolve-ip');
347
484
 
@@ -391,100 +528,7 @@ const httpInstrumentation = new HttpInstrumentation({
391
528
 
392
529
  if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
393
530
  const contentType = request.headers['content-type'] || '';
394
-
395
- if (captureBody && (contentType.includes('application/json') ||
396
- contentType.includes('application/graphql') ||
397
- contentType.includes('application/x-www-form-urlencoded'))) {
398
-
399
- let body = '';
400
- const chunks = [];
401
- let size = 0;
402
-
403
- request.on('data', (chunk) => {
404
- size += chunk.length;
405
- if (size <= maxBodySize) {
406
- chunks.push(chunk);
407
- }
408
- });
409
-
410
- request.on('end', () => {
411
- if (size <= maxBodySize && chunks.length > 0) {
412
- body = Buffer.concat(chunks).toString('utf8');
413
-
414
- try {
415
- let redacted;
416
-
417
- if (contentType.includes('application/graphql')) {
418
- // GraphQL: redact query string
419
- redacted = redactGraphQLQuery(body, allSensitiveFields);
420
- span.setAttributes({
421
- 'http.request.body': redacted.substring(0, maxBodySize),
422
- 'http.request.body.type': 'graphql',
423
- 'http.request.body.size': size,
424
- });
425
- } else if (contentType.includes('application/json')) {
426
- // JSON: parse and redact object
427
- const parsed = JSON.parse(body);
428
- redacted = redactSensitiveData(parsed, allSensitiveFields);
429
- span.setAttributes({
430
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
431
- 'http.request.body.type': 'json',
432
- 'http.request.body.size': size,
433
- });
434
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
435
- // Form: parse and redact
436
- const parsed = Object.fromEntries(new URLSearchParams(body));
437
- redacted = redactSensitiveData(parsed, allSensitiveFields);
438
- span.setAttributes({
439
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
440
- 'http.request.body.type': 'form',
441
- 'http.request.body.size': size,
442
- });
443
- }
444
- } catch (e) {
445
- span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
446
- span.setAttribute('http.request.body.parse_error', true);
447
- span.setAttribute('http.request.body.size', size);
448
- }
449
- } else if (size > maxBodySize) {
450
- span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
451
- span.setAttribute('http.request.body.size', size);
452
- }
453
- });
454
- } else if (contentType.includes('multipart/form-data')) {
455
- if (captureMultipart) {
456
- collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
457
- try {
458
- if (error === 'BOUNDARY_NOT_FOUND') {
459
- span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
460
- span.setAttribute('http.request.body.type', 'multipart');
461
- return;
462
- }
463
- if (error) {
464
- span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
465
- span.setAttribute('http.request.body.type', 'multipart');
466
- span.setAttribute('http.request.body.parse_error', true);
467
- return;
468
- }
469
- span.setAttributes({
470
- 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
471
- 'http.request.body.type': 'multipart',
472
- 'http.request.body.size': totalSize,
473
- 'http.request.body.fields_count': Object.keys(parsed.fields).length,
474
- 'http.request.body.files_count': parsed.files.length,
475
- });
476
- } catch (e) {
477
- span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
478
- span.setAttribute('http.request.body.type', 'multipart');
479
- span.setAttribute('http.request.body.parse_error', true);
480
- }
481
- });
482
- } else {
483
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
484
- span.setAttribute('http.request.body.type', 'multipart');
485
- span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
486
- }
487
- }
531
+ installRequestBodyObserver(span, request, contentType);
488
532
  }
489
533
  } catch (error) {
490
534
  // Silently fail