securenow 5.5.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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.5.0",
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",
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
@@ -188,11 +188,74 @@ const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB
188
188
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
189
189
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
190
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
+
191
249
  // Configure HTTP instrumentation with body capture
192
250
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
193
251
  const httpInstrumentation = new HttpInstrumentation({
194
252
  requestHook: (span, request) => {
195
253
  try {
254
+ const clientIp = resolveClientIp(request);
255
+ if (clientIp) {
256
+ span.setAttribute('http.client_ip', clientIp);
257
+ }
258
+
196
259
  if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
197
260
  const contentType = request.headers['content-type'] || '';
198
261
 
@@ -293,6 +356,26 @@ if (loggingEnabled) {
293
356
  resource: sharedResource,
294
357
  });
295
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); };
296
379
  }
297
380
 
298
381
  // -------- SDK --------