securenow 6.0.2 → 6.1.0

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.
Files changed (87) hide show
  1. package/CONSUMING-APPS-GUIDE.md +455 -0
  2. package/NPM_README.md +2029 -0
  3. package/README.md +297 -40
  4. package/SKILL-API.md +634 -0
  5. package/SKILL-CLI.md +454 -0
  6. package/cidr.js +83 -0
  7. package/cli/apps.js +585 -0
  8. package/cli/auth.js +280 -0
  9. package/cli/client.js +115 -0
  10. package/cli/config.js +173 -0
  11. package/cli/diagnostics.js +387 -0
  12. package/cli/firewall.js +100 -0
  13. package/cli/fp.js +638 -0
  14. package/cli/init.js +201 -0
  15. package/cli/monitor.js +440 -0
  16. package/cli/run.js +148 -0
  17. package/cli/security.js +980 -0
  18. package/cli/ui.js +386 -0
  19. package/cli/utils.js +127 -0
  20. package/cli.js +466 -455
  21. package/console-instrumentation.js +147 -136
  22. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
  23. package/docs/API-KEYS-GUIDE.md +233 -0
  24. package/docs/ARCHITECTURE.md +3 -3
  25. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  26. package/docs/AUTO-SETUP-SUMMARY.md +331 -0
  27. package/docs/AUTO-SETUP.md +4 -4
  28. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  29. package/docs/BODY-CAPTURE-FIX.md +261 -0
  30. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  31. package/docs/CHANGELOG-NEXTJS.md +1 -35
  32. package/docs/COMPLETION-REPORT.md +408 -0
  33. package/docs/CUSTOMER-GUIDE.md +16 -16
  34. package/docs/EASIEST-SETUP.md +5 -5
  35. package/docs/ENVIRONMENT-VARIABLES.md +880 -652
  36. package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
  37. package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
  38. package/docs/FINAL-SOLUTION.md +335 -0
  39. package/docs/FIREWALL-GUIDE.md +426 -0
  40. package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
  41. package/docs/INDEX.md +22 -4
  42. package/docs/LOGGING-GUIDE.md +701 -708
  43. package/docs/LOGGING-QUICKSTART.md +234 -255
  44. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
  45. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  46. package/docs/NEXTJS-GUIDE.md +14 -14
  47. package/docs/NEXTJS-QUICKSTART.md +1 -1
  48. package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
  49. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  50. package/docs/NUXT-GUIDE.md +166 -0
  51. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  52. package/docs/REDACTION-EXAMPLES.md +1 -1
  53. package/docs/REQUEST-BODY-CAPTURE.md +19 -10
  54. package/docs/SOLUTION-SUMMARY.md +312 -0
  55. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  56. package/examples/README.md +6 -6
  57. package/examples/instrumentation-with-auto-capture.ts +1 -1
  58. package/examples/nextjs-env-example.txt +2 -2
  59. package/examples/nextjs-instrumentation.js +1 -1
  60. package/examples/nextjs-instrumentation.ts +1 -1
  61. package/examples/nextjs-with-logging-example.md +6 -6
  62. package/examples/nextjs-with-options.ts +1 -1
  63. package/examples/test-nextjs-setup.js +1 -1
  64. package/firewall-cloud.js +212 -0
  65. package/firewall-iptables.js +139 -0
  66. package/firewall-only.js +38 -0
  67. package/firewall-tcp.js +74 -0
  68. package/firewall.js +720 -0
  69. package/free-trial-banner.js +174 -0
  70. package/nextjs-auto-capture.js +199 -207
  71. package/nextjs-middleware.js +186 -181
  72. package/nextjs-webpack-config.js +88 -53
  73. package/nextjs-wrapper.js +158 -158
  74. package/nextjs.d.ts +1 -1
  75. package/nextjs.js +639 -647
  76. package/nuxt-server-plugin.mjs +423 -0
  77. package/nuxt.d.ts +60 -0
  78. package/nuxt.mjs +75 -0
  79. package/package.json +186 -164
  80. package/postinstall.js +6 -6
  81. package/register.d.ts +1 -1
  82. package/register.js +39 -4
  83. package/resolve-ip.js +77 -0
  84. package/tracing.d.ts +2 -1
  85. package/tracing.js +295 -34
  86. package/web-vite.mjs +239 -156
  87. package/LICENSE +0 -15
package/tracing.js CHANGED
@@ -1,7 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Preload with: NODE_OPTIONS="-r securenow/register"
4
+ * Preload with: node --require securenow/register app.js
5
+ *
6
+ * Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
7
+ * auto-registered via module.register() — no --import flag needed.
8
+ * On Node 18 with "type": "module", add the hook manually:
9
+ * node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
5
10
  *
6
11
  * Env:
7
12
  * SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
@@ -11,6 +16,7 @@
11
16
  * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
12
17
  * OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
13
18
  * SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
19
+ * SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
14
20
  * OTEL_LOG_LEVEL=info|debug
15
21
  * SECURENOW_TEST_SPAN=1
16
22
  *
@@ -18,15 +24,15 @@
18
24
  * SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
19
25
  */
20
26
 
21
- const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
27
+ const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
22
28
  const { NodeSDK } = require('@opentelemetry/sdk-node');
23
29
  const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
24
30
  const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
25
31
  const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
26
- const { logs: apiLogs } = require('@opentelemetry/api-logs');
27
32
  const { Resource } = require('@opentelemetry/resources');
28
33
  const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
29
34
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
35
+ const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
30
36
  const { v4: uuidv4 } = require('uuid');
31
37
 
32
38
  const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
@@ -47,6 +53,10 @@ const DEFAULT_SENSITIVE_FIELDS = [
47
53
  'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
48
54
  ];
49
55
 
56
+ function escapeRegex(str) {
57
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ }
59
+
50
60
  /**
51
61
  * Redact sensitive fields from an object
52
62
  */
@@ -55,7 +65,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
55
65
 
56
66
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
57
67
 
58
- for (const key in redacted) {
68
+ for (const key of Object.keys(redacted)) {
59
69
  const lowerKey = key.toLowerCase();
60
70
 
61
71
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
@@ -78,10 +88,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
78
88
 
79
89
  // Redact sensitive fields in GraphQL arguments and variables
80
90
  sensitiveFields.forEach(field => {
81
- // Match patterns: field: "value" or field: 'value' or field:"value"
91
+ const escaped = escapeRegex(field);
82
92
  const patterns = [
83
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
84
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
93
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
94
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
85
95
  ];
86
96
 
87
97
  patterns.forEach(pattern => {
@@ -98,14 +108,162 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
98
108
  return redacted;
99
109
  }
100
110
 
111
+ // -------- Multipart streaming parser --------
112
+ // Streams through the request without buffering file content.
113
+ // Only part headers and text-field values are kept in memory,
114
+ // so memory stays bounded (~few KB) regardless of upload size.
115
+
116
+ function extractBoundary(contentType) {
117
+ const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
118
+ return match ? (match[1] || match[2]) : null;
119
+ }
120
+
121
+ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
122
+ const boundary = extractBoundary(contentType);
123
+ if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
124
+
125
+ const result = { fields: Object.create(null), files: [] };
126
+ let totalSize = 0;
127
+ let buf = Buffer.alloc(0);
128
+
129
+ const MAX_PARTS = 100;
130
+ let partCount = 0;
131
+
132
+ const FIRST_DELIM = Buffer.from('--' + boundary);
133
+ const DELIM = Buffer.from('\r\n--' + boundary);
134
+ const HDR_END = Buffer.from('\r\n\r\n');
135
+
136
+ let initialized = false;
137
+ let inHeaders = true;
138
+ let isFile = false;
139
+ let fldName = '';
140
+ let fName = '';
141
+ let pCT = '';
142
+ let bodyBytes = 0;
143
+ let textVal = '';
144
+
145
+ function flushPart() {
146
+ if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
147
+ if (isFile) {
148
+ result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
149
+ } else {
150
+ const lower = fldName.toLowerCase();
151
+ const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
152
+ result.fields[fldName] = redact ? '[REDACTED]' : textVal.substring(0, maxTextFieldSize);
153
+ }
154
+ fldName = ''; bodyBytes = 0; textVal = ''; partCount++;
155
+ }
156
+
157
+ function drain() {
158
+ if (!initialized) {
159
+ const i = buf.indexOf(FIRST_DELIM);
160
+ if (i === -1) {
161
+ if (buf.length > FIRST_DELIM.length + 4) buf = buf.slice(buf.length - FIRST_DELIM.length - 4);
162
+ return;
163
+ }
164
+ buf = buf.slice(i + FIRST_DELIM.length);
165
+ initialized = true;
166
+ inHeaders = true;
167
+ }
168
+
169
+ let guard = 200;
170
+ while (buf.length > 0 && guard-- > 0 && partCount < MAX_PARTS) {
171
+ if (inHeaders) {
172
+ if (buf.length >= 2 && buf[0] === 0x2D && buf[1] === 0x2D) { buf = Buffer.alloc(0); return; }
173
+ if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) { buf = buf.slice(2); continue; }
174
+
175
+ const hi = buf.indexOf(HDR_END);
176
+ if (hi === -1) return;
177
+
178
+ const hdr = buf.slice(0, hi).toString('latin1');
179
+ buf = buf.slice(hi + 4);
180
+
181
+ const nm = hdr.match(/name="([^"]+)"/);
182
+ const fn = hdr.match(/filename="([^"]*)"/);
183
+ const ct = hdr.match(/Content-Type:\s*(.+)/i);
184
+ fldName = nm ? nm[1] : '';
185
+ fName = fn ? fn[1] : '';
186
+ pCT = ct ? ct[1].trim() : '';
187
+ isFile = !!fn;
188
+ bodyBytes = 0;
189
+ textVal = '';
190
+ inHeaders = false;
191
+ }
192
+
193
+ const di = buf.indexOf(DELIM);
194
+ if (di === -1) {
195
+ const safe = Math.max(0, buf.length - DELIM.length - 2);
196
+ if (safe > 0) {
197
+ bodyBytes += safe;
198
+ if (!isFile && textVal.length < maxTextFieldSize) {
199
+ textVal += buf.slice(0, safe).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
200
+ }
201
+ buf = buf.slice(safe);
202
+ }
203
+ return;
204
+ }
205
+
206
+ bodyBytes += di;
207
+ if (!isFile && textVal.length < maxTextFieldSize) {
208
+ textVal += buf.slice(0, di).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
209
+ }
210
+ flushPart();
211
+ buf = buf.slice(di + DELIM.length);
212
+ inHeaders = true;
213
+ }
214
+ }
215
+
216
+ request.on('data', (chunk) => {
217
+ totalSize += chunk.length;
218
+ buf = Buffer.concat([buf, chunk]);
219
+ drain();
220
+ });
221
+
222
+ request.on('end', () => {
223
+ try {
224
+ if (!inHeaders && fldName) {
225
+ bodyBytes += buf.length;
226
+ if (!isFile) textVal += buf.toString('utf8').substring(0, maxTextFieldSize - textVal.length);
227
+ flushPart();
228
+ }
229
+ onComplete({ parsed: result, totalSize });
230
+ } catch (e) {
231
+ onComplete({ error: 'PARSE_ERROR' });
232
+ }
233
+ });
234
+ }
235
+
236
+ // -------- ESM detection --------
237
+ // register.js auto-registers the hook via module.register() on Node >=20.6.
238
+ // This warning only fires if BOTH --import AND module.register() were skipped
239
+ // (e.g. Node 18, or require('securenow/tracing') called directly without register.js).
240
+ (() => {
241
+ try {
242
+ const fs = require('fs');
243
+ const path = require('path');
244
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
245
+ if (fs.existsSync(pkgPath)) {
246
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
247
+ if (pkg.type === 'module') {
248
+ const execArgv = process.execArgv.join(' ');
249
+ const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
250
+ const hasModuleRegister = typeof require('node:module').register === 'function';
251
+ if (!hasCliHook && !hasModuleRegister) {
252
+ console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
253
+ console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
254
+ }
255
+ }
256
+ }
257
+ } catch (_) {}
258
+ })();
259
+
101
260
  // -------- diagnostics --------
102
261
  const diagLevel = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
103
262
  (() => {
104
- const L = diagLevel;
105
- const level = L === 'debug' ? DiagLogLevel.DEBUG :
106
- L === 'info' ? DiagLogLevel.INFO :
107
- L === 'warn' ? DiagLogLevel.WARN :
108
- L === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
263
+ const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
264
+ diagLevel === 'info' ? DiagLogLevel.INFO :
265
+ diagLevel === 'warn' ? DiagLogLevel.WARN :
266
+ diagLevel === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
109
267
  diag.setLogger(new DiagConsoleLogger(), level);
110
268
  console.log('[securenow] preload loaded pid=%d', process.pid);
111
269
  })();
@@ -162,21 +320,44 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
162
320
 
163
321
  // -------- Body Capture Configuration --------
164
322
  const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
165
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
323
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
166
324
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
167
325
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
168
326
 
327
+ const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || String(env('SECURENOW_CAPTURE_MULTIPART')).toLowerCase() === 'true';
328
+
329
+ // -------- Trusted proxy IP resolution --------
330
+ const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
331
+
169
332
  // Configure HTTP instrumentation with body capture
170
333
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
171
334
  const httpInstrumentation = new HttpInstrumentation({
172
335
  requestHook: (span, request) => {
173
336
  try {
174
- if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
337
+ const clientIp = resolveClientIp(request);
338
+ if (clientIp) {
339
+ span.setAttribute('http.client_ip', clientIp);
340
+ }
341
+
342
+ if (request.headers) {
343
+ const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
344
+ const safe = {};
345
+ for (const [k, v] of Object.entries(request.headers)) {
346
+ if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
347
+ safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
348
+ }
349
+ const serialized = JSON.stringify(safe);
350
+ if (serialized.length <= 8192) {
351
+ span.setAttribute('http.request.headers', serialized);
352
+ }
353
+ }
354
+
355
+ if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
175
356
  const contentType = request.headers['content-type'] || '';
176
357
 
177
- if (contentType.includes('application/json') ||
358
+ if (captureBody && (contentType.includes('application/json') ||
178
359
  contentType.includes('application/graphql') ||
179
- contentType.includes('application/x-www-form-urlencoded')) {
360
+ contentType.includes('application/x-www-form-urlencoded'))) {
180
361
 
181
362
  let body = '';
182
363
  const chunks = [];
@@ -224,9 +405,9 @@ const httpInstrumentation = new HttpInstrumentation({
224
405
  });
225
406
  }
226
407
  } catch (e) {
227
- // Parse error: capture as-is (truncated)
228
- span.setAttribute('http.request.body', body.substring(0, 1000));
408
+ span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
229
409
  span.setAttribute('http.request.body.parse_error', true);
410
+ span.setAttribute('http.request.body.size', size);
230
411
  }
231
412
  } else if (size > maxBodySize) {
232
413
  span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
@@ -234,10 +415,38 @@ const httpInstrumentation = new HttpInstrumentation({
234
415
  }
235
416
  });
236
417
  } else if (contentType.includes('multipart/form-data')) {
237
- // Multipart is NOT captured
238
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
239
- span.setAttribute('http.request.body.type', 'multipart');
240
- span.setAttribute('http.request.body.note', 'File uploads not captured by design');
418
+ if (captureMultipart) {
419
+ collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
420
+ try {
421
+ if (error === 'BOUNDARY_NOT_FOUND') {
422
+ span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
423
+ span.setAttribute('http.request.body.type', 'multipart');
424
+ return;
425
+ }
426
+ if (error) {
427
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
428
+ span.setAttribute('http.request.body.type', 'multipart');
429
+ span.setAttribute('http.request.body.parse_error', true);
430
+ return;
431
+ }
432
+ span.setAttributes({
433
+ 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
434
+ 'http.request.body.type': 'multipart',
435
+ 'http.request.body.size': totalSize,
436
+ 'http.request.body.fields_count': Object.keys(parsed.fields).length,
437
+ 'http.request.body.files_count': parsed.files.length,
438
+ });
439
+ } catch (e) {
440
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
441
+ span.setAttribute('http.request.body.type', 'multipart');
442
+ span.setAttribute('http.request.body.parse_error', true);
443
+ }
444
+ });
445
+ } else {
446
+ span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
447
+ span.setAttribute('http.request.body.type', 'multipart');
448
+ span.setAttribute('http.request.body.note', 'Set SECURENOW_CAPTURE_MULTIPART=1 to enable');
449
+ }
241
450
  }
242
451
  }
243
452
  } catch (error) {
@@ -247,7 +456,7 @@ const httpInstrumentation = new HttpInstrumentation({
247
456
  });
248
457
 
249
458
  // -------- Logging Configuration --------
250
- const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) !== '0' && String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() !== 'false';
459
+ const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' || String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
251
460
 
252
461
  // Create shared resource for both traces and logs
253
462
  const sharedResource = new Resource({
@@ -259,7 +468,6 @@ const sharedResource = new Resource({
259
468
 
260
469
  // Initialize LoggerProvider if logging is enabled
261
470
  let loggerProvider = null;
262
- let globalLogger = null;
263
471
 
264
472
  if (loggingEnabled) {
265
473
  const logExporter = new OTLPLogExporter({
@@ -267,23 +475,42 @@ if (loggingEnabled) {
267
475
  headers
268
476
  });
269
477
 
478
+ const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
270
479
  loggerProvider = new LoggerProvider({
271
480
  resource: sharedResource,
272
481
  });
273
- // sdk-logs 0.47.x ignores the `processors` constructor option (added in 0.52),
274
- // so the provider would silently keep a NoopLogRecordProcessor and drop every
275
- // emit(). Register the processor explicitly instead.
276
- loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
277
- apiLogs.setGlobalLoggerProvider(loggerProvider);
482
+ loggerProvider.addLogRecordProcessor(batchLogProcessor);
278
483
 
279
- globalLogger = loggerProvider.getLogger('securenow', '1.0.0');
484
+ // Auto-patch console.* so every log/warn/error becomes an OTel log record
485
+ const _logger = loggerProvider.getLogger('console', '1.0.0');
486
+ const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
487
+ const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
488
+ function _emit(sn, st, args) {
489
+ try {
490
+ const activeCtx = context.active();
491
+ const spanCtx = trace.getSpanContext(activeCtx);
492
+ _logger.emit({
493
+ severityNumber: sn,
494
+ severityText: st,
495
+ body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
496
+ attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
497
+ ...(spanCtx && { context: activeCtx }),
498
+ });
499
+ } catch (_) {}
500
+ }
501
+ console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
502
+ console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
503
+ console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
504
+ console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
505
+ console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
506
+ console.__securenow_patched = true;
280
507
  }
281
508
 
282
509
  // -------- Guard against OTLP exporter socket errors --------
283
510
  // The OTLP HTTP exporter uses keep-alive connections that can be reset by the
284
511
  // remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
285
512
  // escape as unhandled exceptions or rejections because the underlying HTTP
286
- // request's error path is not fully covered by the OTel library. We install
513
+ // request's error path isn't fully covered by the OTel library. We install
287
514
  // targeted process-level handlers to catch them and log at debug level instead
288
515
  // of crashing the host app.
289
516
  const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
@@ -305,8 +532,9 @@ process.on('uncaughtException', (err, origin) => {
305
532
  if (diagLevel === 'debug') {
306
533
  console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
307
534
  }
308
- return;
535
+ return; // swallow — do not crash
309
536
  }
537
+ // Not ours — re-throw so the default handler (or the app's own handler) fires
310
538
  throw err;
311
539
  });
312
540
  process.on('unhandledRejection', (reason) => {
@@ -314,8 +542,9 @@ process.on('unhandledRejection', (reason) => {
314
542
  if (diagLevel === 'debug') {
315
543
  console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
316
544
  }
317
- return;
545
+ return; // swallow
318
546
  }
547
+ // Not ours — re-throw as unhandled so Node's default behaviour applies
319
548
  throw reason;
320
549
  });
321
550
 
@@ -325,9 +554,11 @@ const sdk = new NodeSDK({
325
554
  traceExporter,
326
555
  instrumentations: [
327
556
  httpInstrumentation,
557
+ ...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
328
558
  ...getNodeAutoInstrumentations({
329
559
  ...disabledMap,
330
- '@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
560
+ '@opentelemetry/instrumentation-http': { enabled: false },
561
+ '@opentelemetry/instrumentation-mongodb': { enabled: false },
331
562
  }),
332
563
  ],
333
564
  resource: sharedResource,
@@ -346,22 +577,52 @@ const sdk = new NodeSDK({
346
577
  if (captureBody) {
347
578
  console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
348
579
  }
580
+ if (captureMultipart) {
581
+ console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
582
+ }
349
583
  if (String(env('SECURENOW_TEST_SPAN')) === '1') {
350
584
  const api = require('@opentelemetry/api');
351
585
  const tracer = api.trace.getTracer('securenow-smoke');
352
586
  const span = tracer.startSpan('securenow.startup.smoke'); span.end();
353
587
  }
588
+
589
+ // Free trial banner
590
+ const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
591
+ if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
592
+ patchHttpForBanner();
593
+ }
594
+
595
+ // Firewall — auto-activates when SECURENOW_API_KEY is set
596
+ const firewallApiKey = env('SECURENOW_API_KEY');
597
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
598
+ require('./firewall').init({
599
+ apiKey: firewallApiKey,
600
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
601
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
602
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
603
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
604
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
605
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
606
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
607
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
608
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
609
+ });
610
+ }
354
611
  } catch (e) {
355
612
  console.error('[securenow] OTel start failed:', e && e.stack || e);
356
613
  }
357
614
  })();
358
615
 
616
+ let shuttingDown = false;
359
617
  async function safeShutdown(sig) {
618
+ if (shuttingDown) return;
619
+ shuttingDown = true;
360
620
  try {
361
621
  await Promise.resolve(sdk.shutdown?.());
362
622
  if (loggerProvider) {
363
623
  await Promise.resolve(loggerProvider.shutdown?.());
364
624
  }
625
+ try { require('./firewall').shutdown(); } catch (_) {}
365
626
  console.log(`[securenow] Tracing and logging terminated on ${sig}`);
366
627
  }
367
628
  catch (e) { console.error('[securenow] Shutdown error:', e); }