securenow 6.0.1 → 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.
- package/CONSUMING-APPS-GUIDE.md +455 -0
- package/NPM_README.md +2029 -0
- package/README.md +297 -40
- package/SKILL-API.md +634 -0
- package/SKILL-CLI.md +454 -0
- package/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -0
- package/cli/diagnostics.js +387 -0
- package/cli/firewall.js +100 -0
- package/cli/fp.js +638 -0
- package/cli/init.js +201 -0
- package/cli/monitor.js +440 -0
- package/cli/run.js +148 -0
- package/cli/security.js +980 -0
- package/cli/ui.js +386 -0
- package/cli/utils.js +127 -0
- package/cli.js +466 -455
- package/console-instrumentation.js +147 -136
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
- package/docs/API-KEYS-GUIDE.md +233 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-FIX.md +261 -0
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -35
- package/docs/COMPLETION-REPORT.md +408 -0
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +880 -652
- package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
- package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
- package/docs/FINAL-SOLUTION.md +335 -0
- package/docs/FIREWALL-GUIDE.md +426 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +22 -4
- package/docs/LOGGING-GUIDE.md +701 -708
- package/docs/LOGGING-QUICKSTART.md +234 -255
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
- package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
- package/docs/NEXTJS-GUIDE.md +14 -14
- package/docs/NEXTJS-QUICKSTART.md +1 -1
- package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
- package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
- package/docs/NUXT-GUIDE.md +166 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +19 -10
- package/docs/SOLUTION-SUMMARY.md +312 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
- package/examples/README.md +6 -6
- package/examples/instrumentation-with-auto-capture.ts +1 -1
- package/examples/nextjs-env-example.txt +2 -2
- package/examples/nextjs-instrumentation.js +1 -1
- package/examples/nextjs-instrumentation.ts +1 -1
- package/examples/nextjs-with-logging-example.md +6 -6
- package/examples/nextjs-with-options.ts +1 -1
- package/examples/test-nextjs-setup.js +1 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +74 -0
- package/firewall.js +720 -0
- package/free-trial-banner.js +174 -0
- package/nextjs-auto-capture.js +199 -207
- package/nextjs-middleware.js +186 -181
- package/nextjs-webpack-config.js +88 -53
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +224 -198
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +67 -45
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +39 -4
- package/resolve-ip.js +77 -0
- package/tracing.d.ts +2 -1
- package/tracing.js +333 -31
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
package/tracing.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Preload with:
|
|
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
|
|
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
|
-
|
|
91
|
+
const escaped = escapeRegex(field);
|
|
82
92
|
const patterns = [
|
|
83
|
-
new RegExp(`(${
|
|
84
|
-
new RegExp(`(${
|
|
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,13 +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 --------
|
|
261
|
+
const diagLevel = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
102
262
|
(() => {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
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;
|
|
108
267
|
diag.setLogger(new DiagConsoleLogger(), level);
|
|
109
268
|
console.log('[securenow] preload loaded pid=%d', process.pid);
|
|
110
269
|
})();
|
|
@@ -161,21 +320,44 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
161
320
|
|
|
162
321
|
// -------- Body Capture Configuration --------
|
|
163
322
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
164
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') ||
|
|
323
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
165
324
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
166
325
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
167
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
|
+
|
|
168
332
|
// Configure HTTP instrumentation with body capture
|
|
169
333
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
170
334
|
const httpInstrumentation = new HttpInstrumentation({
|
|
171
335
|
requestHook: (span, request) => {
|
|
172
336
|
try {
|
|
173
|
-
|
|
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)) {
|
|
174
356
|
const contentType = request.headers['content-type'] || '';
|
|
175
357
|
|
|
176
|
-
if (contentType.includes('application/json') ||
|
|
358
|
+
if (captureBody && (contentType.includes('application/json') ||
|
|
177
359
|
contentType.includes('application/graphql') ||
|
|
178
|
-
contentType.includes('application/x-www-form-urlencoded')) {
|
|
360
|
+
contentType.includes('application/x-www-form-urlencoded'))) {
|
|
179
361
|
|
|
180
362
|
let body = '';
|
|
181
363
|
const chunks = [];
|
|
@@ -223,9 +405,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
223
405
|
});
|
|
224
406
|
}
|
|
225
407
|
} catch (e) {
|
|
226
|
-
|
|
227
|
-
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
408
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
228
409
|
span.setAttribute('http.request.body.parse_error', true);
|
|
410
|
+
span.setAttribute('http.request.body.size', size);
|
|
229
411
|
}
|
|
230
412
|
} else if (size > maxBodySize) {
|
|
231
413
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
@@ -233,10 +415,38 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
233
415
|
}
|
|
234
416
|
});
|
|
235
417
|
} else if (contentType.includes('multipart/form-data')) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
450
|
}
|
|
241
451
|
}
|
|
242
452
|
} catch (error) {
|
|
@@ -246,7 +456,7 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
246
456
|
});
|
|
247
457
|
|
|
248
458
|
// -------- Logging Configuration --------
|
|
249
|
-
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED'))
|
|
459
|
+
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' || String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
|
|
250
460
|
|
|
251
461
|
// Create shared resource for both traces and logs
|
|
252
462
|
const sharedResource = new Resource({
|
|
@@ -258,7 +468,6 @@ const sharedResource = new Resource({
|
|
|
258
468
|
|
|
259
469
|
// Initialize LoggerProvider if logging is enabled
|
|
260
470
|
let loggerProvider = null;
|
|
261
|
-
let globalLogger = null;
|
|
262
471
|
|
|
263
472
|
if (loggingEnabled) {
|
|
264
473
|
const logExporter = new OTLPLogExporter({
|
|
@@ -266,27 +475,90 @@ if (loggingEnabled) {
|
|
|
266
475
|
headers
|
|
267
476
|
});
|
|
268
477
|
|
|
478
|
+
const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
|
|
269
479
|
loggerProvider = new LoggerProvider({
|
|
270
480
|
resource: sharedResource,
|
|
271
481
|
});
|
|
272
|
-
|
|
273
|
-
// so the provider would silently keep a NoopLogRecordProcessor and drop every
|
|
274
|
-
// emit(). Register the processor explicitly instead.
|
|
275
|
-
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
|
|
276
|
-
apiLogs.setGlobalLoggerProvider(loggerProvider);
|
|
482
|
+
loggerProvider.addLogRecordProcessor(batchLogProcessor);
|
|
277
483
|
|
|
278
|
-
|
|
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;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// -------- Guard against OTLP exporter socket errors --------
|
|
510
|
+
// The OTLP HTTP exporter uses keep-alive connections that can be reset by the
|
|
511
|
+
// remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
|
|
512
|
+
// escape as unhandled exceptions or rejections because the underlying HTTP
|
|
513
|
+
// request's error path isn't fully covered by the OTel library. We install
|
|
514
|
+
// targeted process-level handlers to catch them and log at debug level instead
|
|
515
|
+
// of crashing the host app.
|
|
516
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
517
|
+
function _isOtlpTransientError(err) {
|
|
518
|
+
if (!err) return false;
|
|
519
|
+
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
520
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
function _looksLikeOtlpStack(err) {
|
|
524
|
+
const s = err && err.stack;
|
|
525
|
+
if (!s) return false;
|
|
526
|
+
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
527
|
+
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
279
528
|
}
|
|
280
529
|
|
|
530
|
+
process.on('uncaughtException', (err, origin) => {
|
|
531
|
+
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
532
|
+
if (diagLevel === 'debug') {
|
|
533
|
+
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
534
|
+
}
|
|
535
|
+
return; // swallow — do not crash
|
|
536
|
+
}
|
|
537
|
+
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
538
|
+
throw err;
|
|
539
|
+
});
|
|
540
|
+
process.on('unhandledRejection', (reason) => {
|
|
541
|
+
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
542
|
+
if (diagLevel === 'debug') {
|
|
543
|
+
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
544
|
+
}
|
|
545
|
+
return; // swallow
|
|
546
|
+
}
|
|
547
|
+
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
548
|
+
throw reason;
|
|
549
|
+
});
|
|
550
|
+
|
|
281
551
|
// -------- SDK --------
|
|
282
552
|
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
283
553
|
const sdk = new NodeSDK({
|
|
284
554
|
traceExporter,
|
|
285
555
|
instrumentations: [
|
|
286
556
|
httpInstrumentation,
|
|
557
|
+
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
287
558
|
...getNodeAutoInstrumentations({
|
|
288
559
|
...disabledMap,
|
|
289
|
-
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
560
|
+
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
561
|
+
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
290
562
|
}),
|
|
291
563
|
],
|
|
292
564
|
resource: sharedResource,
|
|
@@ -305,22 +577,52 @@ const sdk = new NodeSDK({
|
|
|
305
577
|
if (captureBody) {
|
|
306
578
|
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
307
579
|
}
|
|
580
|
+
if (captureMultipart) {
|
|
581
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
582
|
+
}
|
|
308
583
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
309
584
|
const api = require('@opentelemetry/api');
|
|
310
585
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
311
586
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
312
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
|
+
}
|
|
313
611
|
} catch (e) {
|
|
314
612
|
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
315
613
|
}
|
|
316
614
|
})();
|
|
317
615
|
|
|
616
|
+
let shuttingDown = false;
|
|
318
617
|
async function safeShutdown(sig) {
|
|
618
|
+
if (shuttingDown) return;
|
|
619
|
+
shuttingDown = true;
|
|
319
620
|
try {
|
|
320
621
|
await Promise.resolve(sdk.shutdown?.());
|
|
321
622
|
if (loggerProvider) {
|
|
322
623
|
await Promise.resolve(loggerProvider.shutdown?.());
|
|
323
624
|
}
|
|
625
|
+
try { require('./firewall').shutdown(); } catch (_) {}
|
|
324
626
|
console.log(`[securenow] Tracing and logging terminated on ${sig}`);
|
|
325
627
|
}
|
|
326
628
|
catch (e) { console.error('[securenow] Shutdown error:', e); }
|