securenow 6.0.2 → 7.0.0-anas
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/app-config.js +130 -0
- package/cidr.js +83 -0
- package/cli/apps.js +608 -0
- package/cli/auth.js +298 -0
- package/cli/client.js +115 -0
- package/cli/config.js +202 -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 +469 -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 +198 -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 +638 -647
- package/nuxt-server-plugin.mjs +425 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +172 -164
- package/postinstall.js +42 -14
- 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 +318 -45
- 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,26 +108,185 @@ 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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
})();
|
|
112
270
|
|
|
113
|
-
// -------- endpoints --------
|
|
114
|
-
|
|
271
|
+
// -------- endpoints & app resolution --------
|
|
272
|
+
// Resolution order for endpoint/appId/apiKey: env → .securenow/credentials.json → package.json#name → defaults.
|
|
273
|
+
const appConfig = require('./app-config');
|
|
274
|
+
const resolvedApp = appConfig.resolveAll();
|
|
275
|
+
|
|
276
|
+
const endpointBase = resolvedApp.instance.replace(/\/$/, '');
|
|
115
277
|
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
116
278
|
const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
279
|
+
|
|
280
|
+
// If the credentials file provided an app key and no OTLP headers are set,
|
|
281
|
+
// surface it as x-api-key so the collector can route telemetry to the right app bucket.
|
|
282
|
+
if (resolvedApp.appKey && !env('OTEL_EXPORTER_OTLP_HEADERS') && !env('SECURENOW_API_KEY')) {
|
|
283
|
+
process.env.SECURENOW_API_KEY = resolvedApp.appKey;
|
|
284
|
+
process.env.OTEL_EXPORTER_OTLP_HEADERS = `x-api-key=${resolvedApp.appKey}`;
|
|
285
|
+
}
|
|
117
286
|
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
118
287
|
|
|
119
288
|
// -------- naming rules --------
|
|
120
|
-
const rawBase = (
|
|
289
|
+
const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
121
290
|
const baseName = rawBase || null;
|
|
122
291
|
const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
|
|
123
292
|
const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
|
|
@@ -144,12 +313,11 @@ const instancePrefix = baseName || 'securenow';
|
|
|
144
313
|
const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
|
|
145
314
|
|
|
146
315
|
// Loud line per worker to prove what was used
|
|
147
|
-
console.log('[securenow] pid=%d
|
|
316
|
+
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
|
|
148
317
|
process.pid,
|
|
149
|
-
JSON.stringify(
|
|
150
|
-
JSON.stringify(
|
|
151
|
-
|
|
152
|
-
JSON.stringify(env('SECURENOW_STRICT')),
|
|
318
|
+
JSON.stringify(baseName),
|
|
319
|
+
JSON.stringify(endpointBase),
|
|
320
|
+
resolvedApp.appKey ? 'set' : 'none',
|
|
153
321
|
serviceName,
|
|
154
322
|
serviceInstanceId
|
|
155
323
|
);
|
|
@@ -161,22 +329,46 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
161
329
|
}
|
|
162
330
|
|
|
163
331
|
// -------- Body Capture Configuration --------
|
|
164
|
-
|
|
165
|
-
const
|
|
332
|
+
// Opt-out defaults: set =0 or =false to disable.
|
|
333
|
+
const captureBody = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
|
|
334
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
166
335
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
167
336
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
168
337
|
|
|
338
|
+
const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
|
|
339
|
+
|
|
340
|
+
// -------- Trusted proxy IP resolution --------
|
|
341
|
+
const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
|
|
342
|
+
|
|
169
343
|
// Configure HTTP instrumentation with body capture
|
|
170
344
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
171
345
|
const httpInstrumentation = new HttpInstrumentation({
|
|
172
346
|
requestHook: (span, request) => {
|
|
173
347
|
try {
|
|
174
|
-
|
|
348
|
+
const clientIp = resolveClientIp(request);
|
|
349
|
+
if (clientIp) {
|
|
350
|
+
span.setAttribute('http.client_ip', clientIp);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (request.headers) {
|
|
354
|
+
const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
|
|
355
|
+
const safe = {};
|
|
356
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
357
|
+
if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
|
|
358
|
+
safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
|
|
359
|
+
}
|
|
360
|
+
const serialized = JSON.stringify(safe);
|
|
361
|
+
if (serialized.length <= 8192) {
|
|
362
|
+
span.setAttribute('http.request.headers', serialized);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
175
367
|
const contentType = request.headers['content-type'] || '';
|
|
176
368
|
|
|
177
|
-
if (contentType.includes('application/json') ||
|
|
369
|
+
if (captureBody && (contentType.includes('application/json') ||
|
|
178
370
|
contentType.includes('application/graphql') ||
|
|
179
|
-
contentType.includes('application/x-www-form-urlencoded')) {
|
|
371
|
+
contentType.includes('application/x-www-form-urlencoded'))) {
|
|
180
372
|
|
|
181
373
|
let body = '';
|
|
182
374
|
const chunks = [];
|
|
@@ -224,9 +416,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
224
416
|
});
|
|
225
417
|
}
|
|
226
418
|
} catch (e) {
|
|
227
|
-
|
|
228
|
-
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
419
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
229
420
|
span.setAttribute('http.request.body.parse_error', true);
|
|
421
|
+
span.setAttribute('http.request.body.size', size);
|
|
230
422
|
}
|
|
231
423
|
} else if (size > maxBodySize) {
|
|
232
424
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
@@ -234,10 +426,38 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
234
426
|
}
|
|
235
427
|
});
|
|
236
428
|
} else if (contentType.includes('multipart/form-data')) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
429
|
+
if (captureMultipart) {
|
|
430
|
+
collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
|
|
431
|
+
try {
|
|
432
|
+
if (error === 'BOUNDARY_NOT_FOUND') {
|
|
433
|
+
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
434
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (error) {
|
|
438
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
439
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
440
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
span.setAttributes({
|
|
444
|
+
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
445
|
+
'http.request.body.type': 'multipart',
|
|
446
|
+
'http.request.body.size': totalSize,
|
|
447
|
+
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
448
|
+
'http.request.body.files_count': parsed.files.length,
|
|
449
|
+
});
|
|
450
|
+
} catch (e) {
|
|
451
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
452
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
453
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
458
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
459
|
+
span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
|
|
460
|
+
}
|
|
241
461
|
}
|
|
242
462
|
}
|
|
243
463
|
} catch (error) {
|
|
@@ -247,7 +467,8 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
247
467
|
});
|
|
248
468
|
|
|
249
469
|
// -------- Logging Configuration --------
|
|
250
|
-
|
|
470
|
+
// Opt-out default: set =0 or =false to disable.
|
|
471
|
+
const loggingEnabled = !/^(0|false)$/i.test(String(env('SECURENOW_LOGGING_ENABLED') ?? ''));
|
|
251
472
|
|
|
252
473
|
// Create shared resource for both traces and logs
|
|
253
474
|
const sharedResource = new Resource({
|
|
@@ -259,7 +480,6 @@ const sharedResource = new Resource({
|
|
|
259
480
|
|
|
260
481
|
// Initialize LoggerProvider if logging is enabled
|
|
261
482
|
let loggerProvider = null;
|
|
262
|
-
let globalLogger = null;
|
|
263
483
|
|
|
264
484
|
if (loggingEnabled) {
|
|
265
485
|
const logExporter = new OTLPLogExporter({
|
|
@@ -267,23 +487,42 @@ if (loggingEnabled) {
|
|
|
267
487
|
headers
|
|
268
488
|
});
|
|
269
489
|
|
|
490
|
+
const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
|
|
270
491
|
loggerProvider = new LoggerProvider({
|
|
271
492
|
resource: sharedResource,
|
|
272
493
|
});
|
|
273
|
-
|
|
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);
|
|
494
|
+
loggerProvider.addLogRecordProcessor(batchLogProcessor);
|
|
278
495
|
|
|
279
|
-
|
|
496
|
+
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
497
|
+
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
498
|
+
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
499
|
+
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
500
|
+
function _emit(sn, st, args) {
|
|
501
|
+
try {
|
|
502
|
+
const activeCtx = context.active();
|
|
503
|
+
const spanCtx = trace.getSpanContext(activeCtx);
|
|
504
|
+
_logger.emit({
|
|
505
|
+
severityNumber: sn,
|
|
506
|
+
severityText: st,
|
|
507
|
+
body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
|
|
508
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
509
|
+
...(spanCtx && { context: activeCtx }),
|
|
510
|
+
});
|
|
511
|
+
} catch (_) {}
|
|
512
|
+
}
|
|
513
|
+
console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
|
|
514
|
+
console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
|
|
515
|
+
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
516
|
+
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
517
|
+
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
518
|
+
console.__securenow_patched = true;
|
|
280
519
|
}
|
|
281
520
|
|
|
282
521
|
// -------- Guard against OTLP exporter socket errors --------
|
|
283
522
|
// The OTLP HTTP exporter uses keep-alive connections that can be reset by the
|
|
284
523
|
// remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
|
|
285
524
|
// escape as unhandled exceptions or rejections because the underlying HTTP
|
|
286
|
-
// request's error path
|
|
525
|
+
// request's error path isn't fully covered by the OTel library. We install
|
|
287
526
|
// targeted process-level handlers to catch them and log at debug level instead
|
|
288
527
|
// of crashing the host app.
|
|
289
528
|
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
@@ -305,8 +544,9 @@ process.on('uncaughtException', (err, origin) => {
|
|
|
305
544
|
if (diagLevel === 'debug') {
|
|
306
545
|
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
307
546
|
}
|
|
308
|
-
return;
|
|
547
|
+
return; // swallow — do not crash
|
|
309
548
|
}
|
|
549
|
+
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
310
550
|
throw err;
|
|
311
551
|
});
|
|
312
552
|
process.on('unhandledRejection', (reason) => {
|
|
@@ -314,8 +554,9 @@ process.on('unhandledRejection', (reason) => {
|
|
|
314
554
|
if (diagLevel === 'debug') {
|
|
315
555
|
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
316
556
|
}
|
|
317
|
-
return;
|
|
557
|
+
return; // swallow
|
|
318
558
|
}
|
|
559
|
+
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
319
560
|
throw reason;
|
|
320
561
|
});
|
|
321
562
|
|
|
@@ -325,9 +566,11 @@ const sdk = new NodeSDK({
|
|
|
325
566
|
traceExporter,
|
|
326
567
|
instrumentations: [
|
|
327
568
|
httpInstrumentation,
|
|
569
|
+
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
328
570
|
...getNodeAutoInstrumentations({
|
|
329
571
|
...disabledMap,
|
|
330
|
-
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
572
|
+
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
573
|
+
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
331
574
|
}),
|
|
332
575
|
],
|
|
333
576
|
resource: sharedResource,
|
|
@@ -341,27 +584,57 @@ const sdk = new NodeSDK({
|
|
|
341
584
|
if (loggingEnabled) {
|
|
342
585
|
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
343
586
|
} else {
|
|
344
|
-
console.log('[securenow] 📋 Logging: DISABLED (
|
|
587
|
+
console.log('[securenow] 📋 Logging: DISABLED (SECURENOW_LOGGING_ENABLED=0)');
|
|
345
588
|
}
|
|
346
589
|
if (captureBody) {
|
|
347
590
|
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
348
591
|
}
|
|
592
|
+
if (captureMultipart) {
|
|
593
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
594
|
+
}
|
|
349
595
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
350
596
|
const api = require('@opentelemetry/api');
|
|
351
597
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
352
598
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
353
599
|
}
|
|
600
|
+
|
|
601
|
+
// Free trial banner
|
|
602
|
+
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
603
|
+
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
604
|
+
patchHttpForBanner();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Firewall — auto-activates when SECURENOW_API_KEY is set
|
|
608
|
+
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
609
|
+
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
610
|
+
require('./firewall').init({
|
|
611
|
+
apiKey: firewallApiKey,
|
|
612
|
+
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
613
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
614
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
615
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
616
|
+
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
617
|
+
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
618
|
+
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
619
|
+
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
620
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
354
623
|
} catch (e) {
|
|
355
624
|
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
356
625
|
}
|
|
357
626
|
})();
|
|
358
627
|
|
|
628
|
+
let shuttingDown = false;
|
|
359
629
|
async function safeShutdown(sig) {
|
|
630
|
+
if (shuttingDown) return;
|
|
631
|
+
shuttingDown = true;
|
|
360
632
|
try {
|
|
361
633
|
await Promise.resolve(sdk.shutdown?.());
|
|
362
634
|
if (loggerProvider) {
|
|
363
635
|
await Promise.resolve(loggerProvider.shutdown?.());
|
|
364
636
|
}
|
|
637
|
+
try { require('./firewall').shutdown(); } catch (_) {}
|
|
365
638
|
console.log(`[securenow] Tracing and logging terminated on ${sig}`);
|
|
366
639
|
}
|
|
367
640
|
catch (e) { console.error('[securenow] Shutdown error:', e); }
|
|
@@ -375,7 +648,7 @@ module.exports = {
|
|
|
375
648
|
loggerProvider,
|
|
376
649
|
getLogger: (name = 'default', version = '1.0.0') => {
|
|
377
650
|
if (!loggerProvider) {
|
|
378
|
-
console.warn('[securenow] Logging is
|
|
651
|
+
console.warn('[securenow] Logging is disabled (SECURENOW_LOGGING_ENABLED=0). Remove the override to enable.');
|
|
379
652
|
return null;
|
|
380
653
|
}
|
|
381
654
|
return loggerProvider.getLogger(name, version);
|