securenow 5.4.0 → 5.6.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/cli.js +1 -1
- package/nextjs.js +22 -4
- package/package.json +3 -2
- package/tracing.d.ts +1 -0
- package/tracing.js +108 -1
package/cli.js
CHANGED
package/nextjs.js
CHANGED
|
@@ -5,18 +5,36 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage in Next.js app:
|
|
7
7
|
*
|
|
8
|
-
* 1.
|
|
8
|
+
* 1. Add serverExternalPackages to next.config.js (REQUIRED to avoid webpack bundling issues):
|
|
9
|
+
*
|
|
10
|
+
* const nextConfig = {
|
|
11
|
+
* serverExternalPackages: [
|
|
12
|
+
* "securenow",
|
|
13
|
+
* "@opentelemetry/sdk-node",
|
|
14
|
+
* "@opentelemetry/auto-instrumentations-node",
|
|
15
|
+
* "@opentelemetry/instrumentation-http",
|
|
16
|
+
* "@opentelemetry/exporter-trace-otlp-http",
|
|
17
|
+
* "@opentelemetry/exporter-logs-otlp-http",
|
|
18
|
+
* "@opentelemetry/sdk-logs",
|
|
19
|
+
* "@opentelemetry/instrumentation",
|
|
20
|
+
* "@opentelemetry/resources",
|
|
21
|
+
* "@opentelemetry/semantic-conventions",
|
|
22
|
+
* "@opentelemetry/api",
|
|
23
|
+
* "@opentelemetry/api-logs",
|
|
24
|
+
* "@vercel/otel",
|
|
25
|
+
* ],
|
|
26
|
+
* };
|
|
27
|
+
*
|
|
28
|
+
* 2. Create instrumentation.ts (or .js) in your project root:
|
|
9
29
|
*
|
|
10
30
|
* import { registerSecureNow } from 'securenow/nextjs';
|
|
11
31
|
* export function register() {
|
|
12
32
|
* registerSecureNow();
|
|
13
33
|
* }
|
|
14
34
|
*
|
|
15
|
-
*
|
|
35
|
+
* 3. Set environment variables:
|
|
16
36
|
* SECURENOW_APPID=my-nextjs-app
|
|
17
37
|
* SECURENOW_INSTANCE=http://your-otlp-backend:4318
|
|
18
|
-
*
|
|
19
|
-
* That's it! 🎉 No webpack warnings!
|
|
20
38
|
*/
|
|
21
39
|
|
|
22
40
|
const { v4: uuidv4 } = require('uuid');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js and Next.js - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"default": "./nextjs-wrapper.js"
|
|
65
65
|
},
|
|
66
66
|
"./nextjs-webpack-config": "./nextjs-webpack-config.js",
|
|
67
|
+
"./package.json": "./package.json",
|
|
67
68
|
"./register-vite": "./register-vite.js",
|
|
68
69
|
"./web-vite": {
|
|
69
70
|
"import": "./web-vite.mjs",
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
"@opentelemetry/instrumentation": "0.47.0",
|
|
106
107
|
"@opentelemetry/instrumentation-document-load": "0.47.0",
|
|
107
108
|
"@opentelemetry/instrumentation-fetch": "0.47.0",
|
|
108
|
-
"@opentelemetry/instrumentation-http": "
|
|
109
|
+
"@opentelemetry/instrumentation-http": "0.47.0",
|
|
109
110
|
"@opentelemetry/instrumentation-user-interaction": "0.47.0",
|
|
110
111
|
"@opentelemetry/instrumentation-xml-http-request": "0.47.0",
|
|
111
112
|
"@opentelemetry/resources": "1.20.0",
|
package/tracing.d.ts
CHANGED
|
@@ -178,5 +178,6 @@ export const loggerProvider: LoggerProvider | null;
|
|
|
178
178
|
* - SECURENOW_DISABLE_INSTRUMENTATIONS=pkg1,pkg2
|
|
179
179
|
* - OTEL_LOG_LEVEL=info|debug
|
|
180
180
|
* - SECURENOW_TEST_SPAN=1
|
|
181
|
+
* - SECURENOW_TRUSTED_PROXIES=ip1,ip2 # Additional trusted proxy IPs for X-Forwarded-For
|
|
181
182
|
* - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=... # Override logs endpoint
|
|
182
183
|
*/
|
package/tracing.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Preload with:
|
|
4
|
+
* Preload with: node --require securenow/register app.js
|
|
5
|
+
*
|
|
6
|
+
* For ESM apps ("type": "module" in package.json), you MUST also add the ESM loader hook:
|
|
7
|
+
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
5
8
|
*
|
|
6
9
|
* Env:
|
|
7
10
|
* SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
|
|
@@ -97,6 +100,27 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
97
100
|
return redacted;
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// -------- ESM detection --------
|
|
104
|
+
(() => {
|
|
105
|
+
try {
|
|
106
|
+
const fs = require('fs');
|
|
107
|
+
const path = require('path');
|
|
108
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
109
|
+
if (fs.existsSync(pkgPath)) {
|
|
110
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
111
|
+
if (pkg.type === 'module') {
|
|
112
|
+
const execArgv = process.execArgv.join(' ');
|
|
113
|
+
const hasEsmHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
114
|
+
if (!hasEsmHook) {
|
|
115
|
+
console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook found.');
|
|
116
|
+
console.warn('[securenow] Instrumentations will NOT work without the ESM hook.');
|
|
117
|
+
console.warn('[securenow] Fix: node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
})();
|
|
123
|
+
|
|
100
124
|
// -------- diagnostics --------
|
|
101
125
|
(() => {
|
|
102
126
|
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
@@ -164,11 +188,74 @@ const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB
|
|
|
164
188
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
165
189
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
166
190
|
|
|
191
|
+
// -------- Trusted proxy IP resolution --------
|
|
192
|
+
// Only trust X-Forwarded-For / X-Real-IP when the direct connection comes from
|
|
193
|
+
// a known proxy (loopback, private RFC-1918/RFC-4193, or an explicit allowlist).
|
|
194
|
+
// This prevents end-users from spoofing their IP via custom headers.
|
|
195
|
+
const os = require('os');
|
|
196
|
+
const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
|
|
197
|
+
const PRIVATE_IP_RE = /^(127\.|::1|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|fc|fd)/;
|
|
198
|
+
const trustedProxyCsv = (env('SECURENOW_TRUSTED_PROXIES') || '').trim();
|
|
199
|
+
const trustedProxySet = trustedProxyCsv ? new Set(trustedProxyCsv.split(',').map(s => s.trim()).filter(Boolean)) : null;
|
|
200
|
+
|
|
201
|
+
// Resolve the host's actual network IP once at startup (used when socket is loopback)
|
|
202
|
+
let _hostIp = null;
|
|
203
|
+
function getHostIp() {
|
|
204
|
+
if (_hostIp !== null) return _hostIp;
|
|
205
|
+
try {
|
|
206
|
+
const ifaces = os.networkInterfaces();
|
|
207
|
+
for (const name of Object.keys(ifaces)) {
|
|
208
|
+
for (const iface of ifaces[name]) {
|
|
209
|
+
if (!iface.internal && iface.family === 'IPv4') { _hostIp = iface.address; return _hostIp; }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (_) {}
|
|
213
|
+
_hostIp = '';
|
|
214
|
+
return _hostIp;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isFromTrustedProxy(socketIp) {
|
|
218
|
+
if (!socketIp) return false;
|
|
219
|
+
const normalized = socketIp.replace(/^::ffff:/, '');
|
|
220
|
+
if (trustedProxySet && trustedProxySet.has(normalized)) return true;
|
|
221
|
+
return PRIVATE_IP_RE.test(socketIp);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveClientIp(request) {
|
|
225
|
+
const socketIp = request.socket?.remoteAddress || '';
|
|
226
|
+
if (!isFromTrustedProxy(socketIp)) return socketIp;
|
|
227
|
+
|
|
228
|
+
// Connection is from a trusted proxy — read the leftmost untrusted IP
|
|
229
|
+
const fwd = request.headers['x-forwarded-for'];
|
|
230
|
+
if (fwd) {
|
|
231
|
+
const chain = String(fwd).split(',').map(s => s.trim()).filter(Boolean);
|
|
232
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
233
|
+
if (!isFromTrustedProxy(chain[i])) return chain[i];
|
|
234
|
+
}
|
|
235
|
+
return chain[0] || socketIp;
|
|
236
|
+
}
|
|
237
|
+
const headerIp = request.headers['x-real-ip'];
|
|
238
|
+
if (headerIp) return headerIp;
|
|
239
|
+
|
|
240
|
+
// Loopback means the client is on this machine — use the host's network IP
|
|
241
|
+
// so traces are attributed to the actual machine, not a useless ::1 / 127.0.0.1
|
|
242
|
+
if (LOOPBACK_RE.test(socketIp)) {
|
|
243
|
+
const hostIp = getHostIp();
|
|
244
|
+
if (hostIp) return hostIp;
|
|
245
|
+
}
|
|
246
|
+
return socketIp;
|
|
247
|
+
}
|
|
248
|
+
|
|
167
249
|
// Configure HTTP instrumentation with body capture
|
|
168
250
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
169
251
|
const httpInstrumentation = new HttpInstrumentation({
|
|
170
252
|
requestHook: (span, request) => {
|
|
171
253
|
try {
|
|
254
|
+
const clientIp = resolveClientIp(request);
|
|
255
|
+
if (clientIp) {
|
|
256
|
+
span.setAttribute('http.client_ip', clientIp);
|
|
257
|
+
}
|
|
258
|
+
|
|
172
259
|
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
173
260
|
const contentType = request.headers['content-type'] || '';
|
|
174
261
|
|
|
@@ -269,6 +356,26 @@ if (loggingEnabled) {
|
|
|
269
356
|
resource: sharedResource,
|
|
270
357
|
});
|
|
271
358
|
loggerProvider.addLogRecordProcessor(batchLogProcessor);
|
|
359
|
+
|
|
360
|
+
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
361
|
+
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
362
|
+
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
363
|
+
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
364
|
+
function _emit(sn, st, args) {
|
|
365
|
+
try {
|
|
366
|
+
_logger.emit({
|
|
367
|
+
severityNumber: sn,
|
|
368
|
+
severityText: st,
|
|
369
|
+
body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
|
|
370
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
371
|
+
});
|
|
372
|
+
} catch (_) {}
|
|
373
|
+
}
|
|
374
|
+
console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
|
|
375
|
+
console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
|
|
376
|
+
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
377
|
+
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
378
|
+
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
272
379
|
}
|
|
273
380
|
|
|
274
381
|
// -------- SDK --------
|