securenow 6.0.1 → 6.1.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/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/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -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 +466 -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 +199 -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 +224 -198
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +67 -45
- package/postinstall.js +6 -6
- 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 +333 -31
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
package/nextjs.js
CHANGED
|
@@ -5,21 +5,39 @@
|
|
|
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
|
-
* SECURENOW_INSTANCE=http://your-
|
|
18
|
-
*
|
|
19
|
-
* That's it! 🎉 No webpack warnings!
|
|
37
|
+
* SECURENOW_INSTANCE=http://your-otlp-backend:4318
|
|
20
38
|
*/
|
|
21
39
|
|
|
22
|
-
const {
|
|
40
|
+
const { randomUUID } = require('crypto');
|
|
23
41
|
|
|
24
42
|
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
25
43
|
|
|
@@ -50,10 +68,9 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
50
68
|
|
|
51
69
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
52
70
|
|
|
53
|
-
for (const key
|
|
71
|
+
for (const key of Object.keys(redacted)) {
|
|
54
72
|
const lowerKey = key.toLowerCase();
|
|
55
73
|
|
|
56
|
-
// Check if field is sensitive
|
|
57
74
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
58
75
|
redacted[key] = '[REDACTED]';
|
|
59
76
|
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
@@ -65,6 +82,10 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
65
82
|
return redacted;
|
|
66
83
|
}
|
|
67
84
|
|
|
85
|
+
function escapeRegex(str) {
|
|
86
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
/**
|
|
69
90
|
* Redact sensitive data from GraphQL query strings
|
|
70
91
|
*/
|
|
@@ -76,10 +97,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
76
97
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
98
|
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
78
99
|
sensitiveFields.forEach(field => {
|
|
79
|
-
|
|
100
|
+
const escaped = escapeRegex(field);
|
|
80
101
|
const patterns = [
|
|
81
|
-
new RegExp(`(${
|
|
82
|
-
new RegExp(`(${
|
|
102
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
103
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
83
104
|
];
|
|
84
105
|
|
|
85
106
|
patterns.forEach(pattern => {
|
|
@@ -96,115 +117,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
96
117
|
return redacted;
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
/**
|
|
100
|
-
* Parse and capture request body safely
|
|
101
|
-
*/
|
|
102
|
-
async function captureRequestBody(request, maxSize = 10240) {
|
|
103
|
-
try {
|
|
104
|
-
const contentType = request.headers['content-type'] || '';
|
|
105
|
-
let body = '';
|
|
106
|
-
|
|
107
|
-
// Collect body chunks
|
|
108
|
-
const chunks = [];
|
|
109
|
-
let size = 0;
|
|
110
|
-
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
request.on('data', (chunk) => {
|
|
113
|
-
size += chunk.length;
|
|
114
|
-
if (size <= maxSize) {
|
|
115
|
-
chunks.push(chunk);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
request.on('end', () => {
|
|
120
|
-
if (size > maxSize) {
|
|
121
|
-
resolve({
|
|
122
|
-
captured: false,
|
|
123
|
-
reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
|
|
124
|
-
size
|
|
125
|
-
});
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
body = Buffer.concat(chunks).toString('utf8');
|
|
130
|
-
|
|
131
|
-
// Parse based on content type
|
|
132
|
-
if (contentType.includes('application/json')) {
|
|
133
|
-
try {
|
|
134
|
-
const parsed = JSON.parse(body);
|
|
135
|
-
resolve({
|
|
136
|
-
captured: true,
|
|
137
|
-
type: 'json',
|
|
138
|
-
body: parsed,
|
|
139
|
-
size
|
|
140
|
-
});
|
|
141
|
-
} catch (e) {
|
|
142
|
-
resolve({
|
|
143
|
-
captured: true,
|
|
144
|
-
type: 'json',
|
|
145
|
-
body: body.substring(0, 1000),
|
|
146
|
-
parseError: true,
|
|
147
|
-
size
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
} else if (contentType.includes('application/graphql')) {
|
|
151
|
-
// GraphQL queries need redaction too!
|
|
152
|
-
resolve({
|
|
153
|
-
captured: true,
|
|
154
|
-
type: 'graphql',
|
|
155
|
-
body: body, // Will be redacted later
|
|
156
|
-
size
|
|
157
|
-
});
|
|
158
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
159
|
-
// Multipart is NOT captured (files can be huge)
|
|
160
|
-
resolve({
|
|
161
|
-
captured: false,
|
|
162
|
-
type: 'multipart',
|
|
163
|
-
reason: 'Multipart data not captured (file uploads)',
|
|
164
|
-
size
|
|
165
|
-
});
|
|
166
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
167
|
-
try {
|
|
168
|
-
const params = new URLSearchParams(body);
|
|
169
|
-
const parsed = Object.fromEntries(params);
|
|
170
|
-
resolve({
|
|
171
|
-
captured: true,
|
|
172
|
-
type: 'form',
|
|
173
|
-
body: parsed,
|
|
174
|
-
size
|
|
175
|
-
});
|
|
176
|
-
} catch (e) {
|
|
177
|
-
resolve({
|
|
178
|
-
captured: true,
|
|
179
|
-
type: 'form',
|
|
180
|
-
body: body.substring(0, 1000),
|
|
181
|
-
size
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
resolve({
|
|
186
|
-
captured: true,
|
|
187
|
-
type: 'text',
|
|
188
|
-
body: body.substring(0, 1000),
|
|
189
|
-
size
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
request.on('error', () => {
|
|
195
|
-
resolve({ captured: false, reason: 'Stream error' });
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Timeout after 100ms
|
|
199
|
-
setTimeout(() => {
|
|
200
|
-
resolve({ captured: false, reason: 'Timeout' });
|
|
201
|
-
}, 100);
|
|
202
|
-
});
|
|
203
|
-
} catch (error) {
|
|
204
|
-
return { captured: false, reason: error.message };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
120
|
/**
|
|
209
121
|
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
210
122
|
* @param {Object} options - Optional configuration
|
|
@@ -245,9 +157,9 @@ function registerSecureNow(options = {}) {
|
|
|
245
157
|
// service.name
|
|
246
158
|
let serviceName;
|
|
247
159
|
if (baseName) {
|
|
248
|
-
serviceName = noUuid ? baseName : `${baseName}-${
|
|
160
|
+
serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
|
|
249
161
|
} else {
|
|
250
|
-
serviceName = `nextjs-app-${
|
|
162
|
+
serviceName = `nextjs-app-${randomUUID()}`;
|
|
251
163
|
console.warn('[securenow] ⚠️ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
|
|
252
164
|
console.warn('[securenow] 💡 Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
|
|
253
165
|
}
|
|
@@ -261,17 +173,11 @@ function registerSecureNow(options = {}) {
|
|
|
261
173
|
).replace(/\/$/, '');
|
|
262
174
|
|
|
263
175
|
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
264
|
-
const logsUrl
|
|
265
|
-
|
|
266
|
-
// Set environment variables for @vercel/otel to pick up
|
|
267
|
-
process.env.OTEL_SERVICE_NAME = serviceName;
|
|
268
|
-
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
269
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
176
|
+
const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
270
177
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
|
|
178
|
+
if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
|
|
179
|
+
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
180
|
+
if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
275
181
|
|
|
276
182
|
console.log('[securenow] 🚀 Next.js App → service.name=%s', serviceName);
|
|
277
183
|
|
|
@@ -279,7 +185,7 @@ function registerSecureNow(options = {}) {
|
|
|
279
185
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
280
186
|
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
|
|
281
187
|
options.captureBody === true;
|
|
282
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') ||
|
|
188
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
283
189
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
284
190
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
285
191
|
|
|
@@ -313,14 +219,19 @@ function registerSecureNow(options = {}) {
|
|
|
313
219
|
const clientIp = headers['x-client-ip'];
|
|
314
220
|
const socketIp = request.socket?.remoteAddress;
|
|
315
221
|
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
222
|
+
const PRIVATE_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
|
|
223
|
+
const isProxied = socketIp && PRIVATE_RE.test(socketIp);
|
|
224
|
+
let primaryIp = socketIp || 'unknown';
|
|
225
|
+
if (isProxied) {
|
|
226
|
+
if (forwardedFor) {
|
|
227
|
+
const chain = forwardedFor.split(',').map(s => s.trim()).filter(Boolean);
|
|
228
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
229
|
+
if (!PRIVATE_RE.test(chain[i])) { primaryIp = chain[i]; break; }
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
primaryIp = realIp || cfConnectingIp || clientIp || primaryIp;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
324
235
|
|
|
325
236
|
// ======== PROTOCOL & CONNECTION ========
|
|
326
237
|
const scheme = headers['x-forwarded-proto'] ||
|
|
@@ -456,6 +367,44 @@ function registerSecureNow(options = {}) {
|
|
|
456
367
|
},
|
|
457
368
|
});
|
|
458
369
|
|
|
370
|
+
// -------- Guard against OTLP exporter socket errors --------
|
|
371
|
+
// The OTLP HTTP exporter uses keep-alive connections that can be reset by
|
|
372
|
+
// the remote end (ECONNRESET / "socket hang up"). These transient errors
|
|
373
|
+
// sometimes escape as unhandled exceptions or rejections. We catch them
|
|
374
|
+
// here and log at debug level instead of crashing the host app.
|
|
375
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
376
|
+
function _isOtlpTransientError(err) {
|
|
377
|
+
if (!err) return false;
|
|
378
|
+
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
379
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
function _looksLikeOtlpStack(err) {
|
|
383
|
+
const s = err && err.stack;
|
|
384
|
+
if (!s) return false;
|
|
385
|
+
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
386
|
+
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
387
|
+
}
|
|
388
|
+
const _diagDebug = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
|
|
389
|
+
process.on('uncaughtException', (err, origin) => {
|
|
390
|
+
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
391
|
+
if (_diagDebug) {
|
|
392
|
+
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
throw err;
|
|
397
|
+
});
|
|
398
|
+
process.on('unhandledRejection', (reason) => {
|
|
399
|
+
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
400
|
+
if (_diagDebug) {
|
|
401
|
+
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
throw reason;
|
|
406
|
+
});
|
|
407
|
+
|
|
459
408
|
if (isVercel) {
|
|
460
409
|
// -------- Vercel Environment: Use @vercel/otel --------
|
|
461
410
|
const { registerOTel } = require('@vercel/otel');
|
|
@@ -510,70 +459,126 @@ function registerSecureNow(options = {}) {
|
|
|
510
459
|
|
|
511
460
|
sdk.start();
|
|
512
461
|
console.log('[securenow] 🎯 Vanilla SDK initialized for self-hosted environment');
|
|
513
|
-
}
|
|
514
462
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (loggingEnabled) {
|
|
522
|
-
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
523
|
-
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
524
|
-
const { logs } = require('@opentelemetry/api-logs');
|
|
525
|
-
const { Resource } = require('@opentelemetry/resources');
|
|
526
|
-
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
463
|
+
// -------- Logging (self-hosted only) --------
|
|
464
|
+
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' || String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
|
|
465
|
+
if (loggingEnabled) {
|
|
466
|
+
try {
|
|
467
|
+
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
468
|
+
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
527
469
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
470
|
+
const logExporter = new OTLPLogExporter({
|
|
471
|
+
url: logsUrl,
|
|
472
|
+
headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
|
|
473
|
+
});
|
|
533
474
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
|
|
540
|
-
logs.setGlobalLoggerProvider(loggerProvider);
|
|
541
|
-
|
|
542
|
-
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
543
|
-
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
544
|
-
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
545
|
-
const _emit = (sn, st, args) => {
|
|
546
|
-
try {
|
|
547
|
-
_logger.emit({
|
|
548
|
-
severityNumber: sn,
|
|
549
|
-
severityText: st,
|
|
550
|
-
body: args.map(a => (typeof a === 'object' && a !== null)
|
|
551
|
-
? (() => { try { return JSON.stringify(a); } catch { return String(a); } })()
|
|
552
|
-
: String(a)).join(' '),
|
|
553
|
-
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
475
|
+
const loggerProvider = new LoggerProvider({
|
|
476
|
+
resource: new Resource({
|
|
477
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
478
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || 'production',
|
|
479
|
+
}),
|
|
554
480
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
481
|
+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
|
|
482
|
+
|
|
483
|
+
// Patch console to forward logs as OTLP log records
|
|
484
|
+
const logger = loggerProvider.getLogger('console', '1.0.0');
|
|
485
|
+
const SeverityNumber = { INFO: 9, WARN: 13, ERROR: 17 };
|
|
486
|
+
const origLog = console.log;
|
|
487
|
+
const origWarn = console.warn;
|
|
488
|
+
const origError = console.error;
|
|
489
|
+
|
|
490
|
+
const { context: otelContext, trace: otelTrace } = require('@opentelemetry/api');
|
|
491
|
+
function _emitLog(sn, st, args) {
|
|
492
|
+
try {
|
|
493
|
+
const activeCtx = otelContext.active();
|
|
494
|
+
const spanCtx = otelTrace.getSpanContext(activeCtx);
|
|
495
|
+
logger.emit({
|
|
496
|
+
severityNumber: sn,
|
|
497
|
+
severityText: st,
|
|
498
|
+
body: args.map(String).join(' '),
|
|
499
|
+
...(spanCtx && { context: activeCtx }),
|
|
500
|
+
});
|
|
501
|
+
} catch (_) {}
|
|
502
|
+
}
|
|
503
|
+
console.log = (...args) => {
|
|
504
|
+
origLog.apply(console, args);
|
|
505
|
+
_emitLog(SeverityNumber.INFO, 'INFO', args);
|
|
506
|
+
};
|
|
507
|
+
console.warn = (...args) => {
|
|
508
|
+
origWarn.apply(console, args);
|
|
509
|
+
_emitLog(SeverityNumber.WARN, 'WARN', args);
|
|
510
|
+
};
|
|
511
|
+
console.error = (...args) => {
|
|
512
|
+
origError.apply(console, args);
|
|
513
|
+
_emitLog(SeverityNumber.ERROR, 'ERROR', args);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
517
|
+
|
|
518
|
+
// Auto-log every incoming HTTP request/response
|
|
519
|
+
try {
|
|
520
|
+
const http = require('http');
|
|
521
|
+
const originalEmit = http.Server.prototype.emit;
|
|
522
|
+
http.Server.prototype.emit = function (event, req, res) {
|
|
523
|
+
if (event === 'request' && req && res) {
|
|
524
|
+
const start = Date.now();
|
|
525
|
+
const method = req.method;
|
|
526
|
+
const url = req.url;
|
|
527
|
+
res.on('finish', () => {
|
|
528
|
+
const reqCtx = otelContext.active();
|
|
529
|
+
const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
|
|
530
|
+
const duration = Date.now() - start;
|
|
531
|
+
const status = res.statusCode;
|
|
532
|
+
const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
|
|
533
|
+
const ua = req.headers['user-agent'] || '-';
|
|
534
|
+
const body = `${method} ${url} ${status} ${duration}ms ip=${ip} ua=${ua}`;
|
|
535
|
+
const severity = status >= 500 ? SeverityNumber.ERROR : status >= 400 ? SeverityNumber.WARN : SeverityNumber.INFO;
|
|
536
|
+
const severityText = status >= 500 ? 'ERROR' : status >= 400 ? 'WARN' : 'INFO';
|
|
537
|
+
origLog.call(console, '[securenow] %s %s %d %dms', method, url, status, duration);
|
|
538
|
+
try {
|
|
539
|
+
logger.emit({
|
|
540
|
+
severityNumber: severity,
|
|
541
|
+
severityText,
|
|
542
|
+
body,
|
|
543
|
+
attributes: {
|
|
544
|
+
'http.method': method,
|
|
545
|
+
'http.url': url,
|
|
546
|
+
'http.status_code': status,
|
|
547
|
+
'http.duration_ms': duration,
|
|
548
|
+
'http.client_ip': String(ip).split(',')[0].trim(),
|
|
549
|
+
'http.user_agent': ua,
|
|
550
|
+
},
|
|
551
|
+
...(reqSpanCtx && { context: reqCtx }),
|
|
552
|
+
});
|
|
553
|
+
} catch (_) {}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return originalEmit.apply(this, arguments);
|
|
557
|
+
};
|
|
558
|
+
console.log('[securenow] 📋 HTTP request logging: ENABLED');
|
|
559
|
+
} catch (_) {}
|
|
560
|
+
|
|
561
|
+
// Graceful shutdown for logs
|
|
562
|
+
process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
|
|
563
|
+
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
|
|
564
|
+
} catch (e) {
|
|
565
|
+
console.warn('[securenow] ⚠️ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
console.log('[securenow] 📋 Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
|
|
569
|
+
}
|
|
574
570
|
}
|
|
575
571
|
|
|
576
572
|
isRegistered = true;
|
|
573
|
+
|
|
574
|
+
// Free trial banner (optional — may not be bundled in standalone builds)
|
|
575
|
+
try {
|
|
576
|
+
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
577
|
+
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
578
|
+
patchHttpForBanner();
|
|
579
|
+
}
|
|
580
|
+
} catch (_) {}
|
|
581
|
+
|
|
577
582
|
console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
|
|
578
583
|
console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
|
|
579
584
|
console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
|
|
@@ -605,6 +610,27 @@ function registerSecureNow(options = {}) {
|
|
|
605
610
|
console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
|
|
606
611
|
}
|
|
607
612
|
}
|
|
613
|
+
|
|
614
|
+
// Firewall — runs independently from OTel so it works even if tracing fails
|
|
615
|
+
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
616
|
+
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
617
|
+
try {
|
|
618
|
+
require('./firewall').init({
|
|
619
|
+
apiKey: firewallApiKey,
|
|
620
|
+
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
621
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
622
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
623
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
624
|
+
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
625
|
+
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
626
|
+
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
627
|
+
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
628
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
629
|
+
});
|
|
630
|
+
} catch (e) {
|
|
631
|
+
console.warn('[securenow] Firewall init failed:', e.message);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
608
634
|
}
|
|
609
635
|
|
|
610
636
|
module.exports = {
|