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.
- package/free-trial-banner.js +2 -1
- package/nextjs.js +14 -5
- package/package.json +1 -1
- package/tracing.js +146 -102
package/free-trial-banner.js
CHANGED
|
@@ -97,7 +97,8 @@ var BANNER_SCRIPT =
|
|
|
97
97
|
*/
|
|
98
98
|
function patchHttpForBanner() {
|
|
99
99
|
try {
|
|
100
|
-
var
|
|
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 =
|
|
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 {
|
|
553
|
-
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try {
|
|
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 } =
|
|
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
|
-
|
|
619
|
+
requireRuntimeModule('./firewall').init({
|
|
611
620
|
apiKey: firewallOptions.apiKey,
|
|
612
621
|
appKey: firewallOptions.appKey,
|
|
613
622
|
environment: deploymentEnvironment || firewallOptions.environment,
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|