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/NPM_README.md +17 -3
- package/README.md +9 -4
- package/SKILL-API.md +2 -2
- package/SKILL-CLI.md +36 -22
- package/app-config.js +81 -3
- package/cli/automation.js +275 -0
- package/cli/config.js +8 -8
- package/cli/credentials.js +2 -1
- package/cli/firewall.js +29 -12
- package/cli/human.js +96 -2
- package/cli/security.js +171 -42
- package/cli.js +71 -28
- package/mcp/catalog.js +327 -15
- package/nextjs.js +22 -23
- package/nuxt-server-plugin.mjs +13 -8
- package/package.json +1 -1
- package/resolve-ip.js +135 -60
- package/tracing.js +25 -4
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
|
-
|
|
207
|
-
const forwardedFor =
|
|
208
|
-
const realIp =
|
|
209
|
-
const
|
|
210
|
-
const
|
|
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
|
-
|
|
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
|
|
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':
|
|
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 }),
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -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
|
|
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
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
|
|
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
|
|
13
|
-
function
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
module.exports = {
|
|
71
|
-
resolveClientIp,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 {
|
|
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
|
|
354
|
-
if (
|
|
355
|
-
span.setAttribute('http.client_ip',
|
|
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) {
|