securenow 7.6.9 → 7.7.1

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/nextjs.js CHANGED
@@ -27,6 +27,7 @@
27
27
 
28
28
  const { randomUUID } = require('crypto');
29
29
  const appConfig = require('./app-config');
30
+ const { resolveClientIpWithDetails } = require('./resolve-ip');
30
31
  const otelResources = require('@opentelemetry/resources');
31
32
 
32
33
  const env = appConfig.env;
@@ -203,26 +204,11 @@ function registerSecureNow(options = {}) {
203
204
  const headers = request.headers || {};
204
205
 
205
206
  // ======== IP ADDRESS CAPTURE ========
206
- // Try different header sources for IP (priority order)
207
- const forwardedFor = headers['x-forwarded-for'];
208
- const realIp = headers['x-real-ip'];
209
- const cfConnectingIp = headers['cf-connecting-ip']; // Cloudflare
210
- const clientIp = headers['x-client-ip'];
211
- const socketIp = request.socket?.remoteAddress;
212
-
213
- const PRIVATE_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
214
- const isProxied = socketIp && PRIVATE_RE.test(socketIp);
215
- let primaryIp = socketIp || 'unknown';
216
- if (isProxied) {
217
- if (forwardedFor) {
218
- const chain = forwardedFor.split(',').map(s => s.trim()).filter(Boolean);
219
- for (let i = chain.length - 1; i >= 0; i--) {
220
- if (!PRIVATE_RE.test(chain[i])) { primaryIp = chain[i]; break; }
221
- }
222
- } else {
223
- primaryIp = realIp || cfConnectingIp || clientIp || primaryIp;
224
- }
225
- }
207
+ const ipDetails = resolveClientIpWithDetails(request);
208
+ const forwardedFor = ipDetails.forwardedFor;
209
+ const realIp = ipDetails.realIp;
210
+ const socketIp = ipDetails.socketIp;
211
+ const primaryIp = ipDetails.ip || 'unknown';
226
212
 
227
213
  // ======== PROTOCOL & CONNECTION ========
228
214
  const scheme = headers['x-forwarded-proto'] ||
@@ -249,9 +235,16 @@ function registerSecureNow(options = {}) {
249
235
  const attributes = {
250
236
  // IP & Network
251
237
  'http.client_ip': primaryIp,
238
+ 'http.client_ip.source': ipDetails.source,
252
239
  'http.forwarded_for': forwardedFor || '',
253
240
  'http.real_ip': realIp || '',
254
241
  'http.socket_ip': socketIp || '',
242
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
243
+ 'http.request.header.x_forwarded_for': forwardedFor || '',
244
+ 'http.request.header.x_real_ip': realIp || '',
245
+ 'http.request.header.cf_connecting_ip': ipDetails.cfConnectingIp || '',
246
+ 'http.request.header.true_client_ip': ipDetails.trueClientIp || '',
247
+ 'http.request.header.x_client_ip': ipDetails.clientIp || '',
255
248
 
256
249
  // Protocol & Host
257
250
  'http.scheme': scheme,
@@ -327,7 +320,7 @@ function registerSecureNow(options = {}) {
327
320
  if (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
328
321
  console.log('[securenow] 📡 Captured IP: %s (from: %s)',
329
322
  primaryIp,
330
- forwardedFor ? 'x-forwarded-for' : realIp ? 'x-real-ip' : socketIp ? 'socket' : 'unknown'
323
+ ipDetails.source || 'unknown'
331
324
  );
332
325
  }
333
326
 
@@ -520,7 +513,8 @@ function registerSecureNow(options = {}) {
520
513
  const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
521
514
  const duration = Date.now() - start;
522
515
  const status = res.statusCode;
523
- const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
516
+ const ipDetails = resolveClientIpWithDetails(req);
517
+ const ip = ipDetails.ip || '-';
524
518
  const ua = req.headers['user-agent'] || '-';
525
519
  const body = `${method} ${url} ${status} ${duration}ms ip=${ip} ua=${ua}`;
526
520
  const severity = status >= 500 ? SeverityNumber.ERROR : status >= 400 ? SeverityNumber.WARN : SeverityNumber.INFO;
@@ -536,7 +530,12 @@ function registerSecureNow(options = {}) {
536
530
  'http.url': url,
537
531
  'http.status_code': status,
538
532
  'http.duration_ms': duration,
539
- 'http.client_ip': String(ip).split(',')[0].trim(),
533
+ 'http.client_ip': ip,
534
+ 'http.client_ip.source': ipDetails.source || 'unknown',
535
+ 'http.socket_ip': ipDetails.socketIp || '',
536
+ 'http.forwarded_for': ipDetails.forwardedFor || '',
537
+ 'http.real_ip': ipDetails.realIp || '',
538
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
540
539
  'http.user_agent': ua,
541
540
  },
542
541
  ...(reqSpanCtx && { context: reqCtx }),
@@ -22,6 +22,7 @@ import { randomUUID } from 'node:crypto';
22
22
 
23
23
  const nodeRequire = createRequire(import.meta.url);
24
24
  const appConfig = nodeRequire('./app-config');
25
+ const { resolveClientIpWithDetails } = nodeRequire('./resolve-ip');
25
26
 
26
27
  // ── Helpers ──
27
28
 
@@ -137,17 +138,21 @@ export default defineNitroPlugin(async (nitroApp) => {
137
138
  requestHook: (span, request) => {
138
139
  try {
139
140
  const hdrs = request.headers || {};
140
- const fwd = hdrs['x-forwarded-for'];
141
- const clientIp =
142
- (fwd ? String(fwd).split(',')[0].trim() : null) ||
143
- hdrs['x-real-ip'] ||
144
- hdrs['cf-connecting-ip'] ||
145
- hdrs['x-client-ip'] ||
146
- request.socket?.remoteAddress ||
147
- 'unknown';
141
+ const ipDetails = resolveClientIpWithDetails(request);
142
+ const clientIp = ipDetails.ip || 'unknown';
148
143
 
149
144
  span.setAttributes({
150
145
  'http.client_ip': clientIp,
146
+ 'http.client_ip.source': ipDetails.source,
147
+ 'http.socket_ip': ipDetails.socketIp || '',
148
+ 'http.forwarded_for': ipDetails.forwardedFor || '',
149
+ 'http.real_ip': ipDetails.realIp || '',
150
+ 'http.proxy.trusted': String(!!ipDetails.trustedProxy),
151
+ 'http.request.header.x_forwarded_for': ipDetails.forwardedFor || '',
152
+ 'http.request.header.x_real_ip': ipDetails.realIp || '',
153
+ 'http.request.header.cf_connecting_ip': ipDetails.cfConnectingIp || '',
154
+ 'http.request.header.true_client_ip': ipDetails.trueClientIp || '',
155
+ 'http.request.header.x_client_ip': ipDetails.clientIp || '',
151
156
  'http.user_agent': hdrs['user-agent'] || '',
152
157
  'http.host': hdrs['x-forwarded-host'] || hdrs['host'] || '',
153
158
  'http.scheme':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.6.9",
3
+ "version": "7.7.1",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
package/resolve-ip.js CHANGED
@@ -2,75 +2,150 @@
2
2
 
3
3
  const os = require('os');
4
4
  const appConfig = require('./app-config');
5
-
6
- const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
7
- const PRIVATE_IP_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
8
-
5
+
6
+ const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
7
+ const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
8
+
9
9
  const trustedProxies = appConfig.listEnv('SECURENOW_TRUSTED_PROXIES');
10
- const trustedProxySet = trustedProxies.length ? new Set(trustedProxies) : null;
11
-
12
- let _hostIp = null;
13
- function getHostIp() {
14
- if (_hostIp !== null) return _hostIp;
15
- try {
16
- const ifaces = os.networkInterfaces();
17
- for (const name of Object.keys(ifaces)) {
18
- for (const iface of ifaces[name]) {
19
- if (!iface.internal && iface.family === 'IPv4') { _hostIp = iface.address; return _hostIp; }
20
- }
21
- }
22
- } catch (_) {}
23
- _hostIp = '';
24
- return _hostIp;
25
- }
26
-
27
- function isFromTrustedProxy(socketIp) {
28
- if (!socketIp) return false;
29
- const normalized = socketIp.replace(/^::ffff:/, '');
30
- if (trustedProxySet && trustedProxySet.has(normalized)) return true;
31
- return PRIVATE_IP_RE.test(socketIp);
32
- }
10
+ const trustedProxySet = trustedProxies.length ? new Set(trustedProxies.map(normalizeIp).filter(Boolean)) : null;
11
+
12
+ let _hostIps = null;
13
+ function normalizeIp(value) {
14
+ if (Array.isArray(value)) value = value[0];
15
+ if (!value) return '';
16
+ let ip = String(value).trim();
17
+ if (!ip) return '';
18
+ ip = ip.replace(/^::ffff:/i, '');
19
+ if (ip.startsWith('[')) {
20
+ const end = ip.indexOf(']');
21
+ if (end !== -1) ip = ip.slice(1, end);
22
+ } else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(ip)) {
23
+ ip = ip.replace(/:\d+$/, '');
24
+ }
25
+ return ip;
26
+ }
27
+
28
+ function getHostIps() {
29
+ if (_hostIps !== null) return _hostIps;
30
+ const ips = new Set();
31
+ try {
32
+ const ifaces = os.networkInterfaces();
33
+ for (const name of Object.keys(ifaces)) {
34
+ for (const iface of ifaces[name]) {
35
+ if (!iface.internal && iface.address) {
36
+ const normalized = normalizeIp(iface.address);
37
+ if (normalized) ips.add(normalized);
38
+ }
39
+ }
40
+ }
41
+ } catch (_) {}
42
+ _hostIps = ips;
43
+ return _hostIps;
44
+ }
45
+
46
+ function isFromTrustedProxy(socketIp) {
47
+ if (!socketIp) return false;
48
+ const normalized = normalizeIp(socketIp);
49
+ if (!normalized) return false;
50
+ if (trustedProxySet && trustedProxySet.has(normalized)) return true;
51
+ if (getHostIps().has(normalized)) return true;
52
+ return PRIVATE_IP_RE.test(normalized);
53
+ }
54
+
55
+ function firstHeaderValue(value) {
56
+ if (Array.isArray(value)) return value.find(Boolean) || '';
57
+ return value || '';
58
+ }
59
+
60
+ function splitForwardedChain(value) {
61
+ return String(firstHeaderValue(value) || '')
62
+ .split(',')
63
+ .map((s) => normalizeIp(s))
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function getHeader(headers, name) {
68
+ if (!headers) return '';
69
+ return firstHeaderValue(headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()]);
70
+ }
71
+
72
+ function resolveClientIpWithDetails(request) {
73
+ const socketIp = normalizeIp(request.socket?.remoteAddress || '');
74
+ const headers = request.headers || {};
75
+ const trustedProxy = isFromTrustedProxy(socketIp);
76
+
77
+ const details = {
78
+ ip: socketIp,
79
+ source: socketIp ? 'socket' : 'unknown',
80
+ socketIp,
81
+ trustedProxy,
82
+ forwardedFor: String(getHeader(headers, 'x-forwarded-for') || ''),
83
+ realIp: String(getHeader(headers, 'x-real-ip') || ''),
84
+ cfConnectingIp: String(getHeader(headers, 'cf-connecting-ip') || ''),
85
+ trueClientIp: String(getHeader(headers, 'true-client-ip') || ''),
86
+ clientIp: String(getHeader(headers, 'x-client-ip') || ''),
87
+ };
88
+
89
+ if (!trustedProxy) return details;
90
+
91
+ const chain = splitForwardedChain(details.forwardedFor);
92
+ if (chain.length) {
93
+ for (let i = chain.length - 1; i >= 0; i--) {
94
+ if (!isFromTrustedProxy(chain[i])) {
95
+ return { ...details, ip: chain[i], source: 'x-forwarded-for' };
96
+ }
97
+ }
98
+ return details;
99
+ }
100
+
101
+ const headerCandidates = [
102
+ ['cf-connecting-ip', details.cfConnectingIp],
103
+ ['true-client-ip', details.trueClientIp],
104
+ ['x-real-ip', details.realIp],
105
+ ['x-client-ip', details.clientIp],
106
+ ];
107
+ for (const [source, raw] of headerCandidates) {
108
+ const ip = normalizeIp(raw);
109
+ if (ip) return { ...details, ip, source };
110
+ }
111
+
112
+ if (LOOPBACK_RE.test(socketIp)) {
113
+ const hostIp = [...getHostIps()][0] || '';
114
+ if (hostIp) return { ...details, ip: hostIp, source: 'host-network' };
115
+ }
116
+
117
+ if (trustedProxy && getHostIps().has(socketIp)) {
118
+ return { ...details, source: 'trusted-proxy-socket' };
119
+ }
120
+
121
+ return details;
122
+ }
33
123
 
34
124
  /**
35
125
  * Resolve the real client IP from an HTTP request, respecting trusted proxies.
36
126
  * Reads X-Forwarded-For / X-Real-IP only when the direct connection comes
37
127
  * from a private/trusted proxy IP. Prevents client-side IP spoofing.
38
128
  */
39
- function resolveClientIp(request) {
40
- const socketIp = request.socket?.remoteAddress || '';
41
- if (!isFromTrustedProxy(socketIp)) return socketIp;
42
-
43
- const fwd = request.headers['x-forwarded-for'];
44
- if (fwd) {
45
- const chain = String(fwd).split(',').map(s => s.trim()).filter(Boolean);
46
- for (let i = chain.length - 1; i >= 0; i--) {
47
- if (!isFromTrustedProxy(chain[i])) return chain[i];
48
- }
49
- return socketIp;
50
- }
51
- const headerIp = request.headers['x-real-ip'];
52
- if (headerIp) return headerIp;
53
-
54
- if (LOOPBACK_RE.test(socketIp)) {
55
- const hostIp = getHostIp();
56
- if (hostIp) return hostIp;
57
- }
58
- return socketIp;
59
- }
129
+ function resolveClientIp(request) {
130
+ return resolveClientIpWithDetails(request).ip || '';
131
+ }
60
132
 
61
133
  /**
62
134
  * Resolve IP from a raw TCP socket (no HTTP headers available).
63
135
  * Normalizes IPv6-mapped IPv4 addresses.
64
136
  */
65
- function resolveSocketIp(socket) {
66
- const raw = socket?.remoteAddress || '';
67
- return raw.replace(/^::ffff:/, '');
68
- }
69
-
70
- module.exports = {
71
- resolveClientIp,
72
- resolveSocketIp,
73
- isFromTrustedProxy,
74
- LOOPBACK_RE,
75
- PRIVATE_IP_RE,
76
- };
137
+ function resolveSocketIp(socket) {
138
+ const raw = socket?.remoteAddress || '';
139
+ return normalizeIp(raw);
140
+ }
141
+
142
+ module.exports = {
143
+ resolveClientIp,
144
+ resolveClientIpWithDetails,
145
+ resolveSocketIp,
146
+ isFromTrustedProxy,
147
+ normalizeIp,
148
+ getHostIps,
149
+ LOOPBACK_RE,
150
+ PRIVATE_IP_RE,
151
+ };
package/tracing.js CHANGED
@@ -343,16 +343,37 @@ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveField
343
343
  const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
344
344
 
345
345
  // -------- Trusted proxy IP resolution --------
346
- const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
346
+ const { resolveClientIpWithDetails } = require('./resolve-ip');
347
347
 
348
348
  // Configure HTTP instrumentation with body capture
349
349
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
350
350
  const httpInstrumentation = new HttpInstrumentation({
351
351
  requestHook: (span, request) => {
352
352
  try {
353
- const clientIp = resolveClientIp(request);
354
- if (clientIp) {
355
- span.setAttribute('http.client_ip', clientIp);
353
+ const ipDetails = resolveClientIpWithDetails(request);
354
+ if (ipDetails.ip) {
355
+ span.setAttribute('http.client_ip', ipDetails.ip);
356
+ span.setAttribute('http.client_ip.source', ipDetails.source);
357
+ span.setAttribute('http.socket_ip', ipDetails.socketIp || '');
358
+ span.setAttribute('http.proxy.trusted', String(!!ipDetails.trustedProxy));
359
+ if (ipDetails.forwardedFor) {
360
+ span.setAttribute('http.forwarded_for', ipDetails.forwardedFor);
361
+ span.setAttribute('http.request.header.x_forwarded_for', ipDetails.forwardedFor);
362
+ }
363
+ if (ipDetails.realIp) {
364
+ span.setAttribute('http.real_ip', ipDetails.realIp);
365
+ span.setAttribute('http.request.header.x_real_ip', ipDetails.realIp);
366
+ }
367
+ if (ipDetails.cfConnectingIp) {
368
+ span.setAttribute('http.cf.connecting_ip', ipDetails.cfConnectingIp);
369
+ span.setAttribute('http.request.header.cf_connecting_ip', ipDetails.cfConnectingIp);
370
+ }
371
+ if (ipDetails.trueClientIp) {
372
+ span.setAttribute('http.request.header.true_client_ip', ipDetails.trueClientIp);
373
+ }
374
+ if (ipDetails.clientIp) {
375
+ span.setAttribute('http.request.header.x_client_ip', ipDetails.clientIp);
376
+ }
356
377
  }
357
378
 
358
379
  if (request.headers) {