securenow 5.10.2 → 5.11.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/CONSUMING-APPS-GUIDE.md +30 -0
- package/NPM_README.md +65 -0
- package/README.md +13 -0
- package/cidr.js +83 -0
- package/cli/auth.js +208 -208
- package/cli/config.js +117 -117
- package/cli/firewall.js +81 -0
- package/cli/fp.js +638 -0
- package/cli/security.js +4 -8
- package/cli.js +28 -1
- package/console-instrumentation.js +147 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +40 -1
- package/docs/API-KEYS-GUIDE.md +215 -0
- package/docs/ENVIRONMENT-VARIABLES.md +880 -697
- package/docs/FIREWALL-GUIDE.md +388 -0
- package/docs/INDEX.md +8 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-tcp.js +58 -0
- package/firewall.js +235 -0
- package/free-trial-banner.js +174 -174
- package/nextjs-auto-capture.js +199 -199
- package/nextjs-middleware.js +186 -186
- package/nextjs-wrapper.js +158 -158
- package/nextjs.js +22 -2
- package/nuxt-server-plugin.mjs +400 -400
- package/package.json +30 -3
- package/resolve-ip.js +77 -0
- package/tracing.js +31 -56
- package/web-vite.mjs +239 -239
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -1,400 +1,400 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Nitro Server Plugin
|
|
3
|
-
*
|
|
4
|
-
* Initialises the OpenTelemetry SDK and hooks into Nitro's request lifecycle
|
|
5
|
-
* to create spans, capture metadata, and forward logs.
|
|
6
|
-
*
|
|
7
|
-
* This file is registered by the Nuxt module (nuxt.mjs) via addServerPlugin.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
11
|
-
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
12
|
-
import { Resource } from '@opentelemetry/resources';
|
|
13
|
-
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
14
|
-
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
15
|
-
import {
|
|
16
|
-
context as otelContext,
|
|
17
|
-
trace as otelTrace,
|
|
18
|
-
SpanStatusCode,
|
|
19
|
-
} from '@opentelemetry/api';
|
|
20
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
21
|
-
|
|
22
|
-
// ── Helpers ──
|
|
23
|
-
|
|
24
|
-
const env = (k) =>
|
|
25
|
-
process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
26
|
-
|
|
27
|
-
const parseHeaders = (str) => {
|
|
28
|
-
const out = {};
|
|
29
|
-
if (!str) return out;
|
|
30
|
-
for (const raw of String(str).split(',')) {
|
|
31
|
-
const s = raw.trim();
|
|
32
|
-
if (!s) continue;
|
|
33
|
-
const i = s.indexOf('=');
|
|
34
|
-
if (i === -1) continue;
|
|
35
|
-
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
41
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
42
|
-
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
43
|
-
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
function redactSensitiveData(obj, fields = DEFAULT_SENSITIVE_FIELDS) {
|
|
47
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
48
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
49
|
-
for (const key of Object.keys(redacted)) {
|
|
50
|
-
const lower = key.toLowerCase();
|
|
51
|
-
if (fields.some((f) => lower.includes(f.toLowerCase()))) {
|
|
52
|
-
redacted[key] = '[REDACTED]';
|
|
53
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
54
|
-
redacted[key] = redactSensitiveData(redacted[key], fields);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return redacted;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ── Runtime config helpers ──
|
|
61
|
-
|
|
62
|
-
function getRuntimeOptions() {
|
|
63
|
-
try {
|
|
64
|
-
const cfg = useRuntimeConfig();
|
|
65
|
-
return cfg.securenow || {};
|
|
66
|
-
} catch {
|
|
67
|
-
return {};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── Plugin ──
|
|
72
|
-
|
|
73
|
-
export default defineNitroPlugin((nitroApp) => {
|
|
74
|
-
const opts = getRuntimeOptions();
|
|
75
|
-
|
|
76
|
-
// ── Naming ──
|
|
77
|
-
const rawBase = (
|
|
78
|
-
opts.serviceName ||
|
|
79
|
-
env('OTEL_SERVICE_NAME') ||
|
|
80
|
-
env('SECURENOW_APPID') ||
|
|
81
|
-
''
|
|
82
|
-
).trim().replace(/^['"]|['"]$/g, '');
|
|
83
|
-
|
|
84
|
-
const baseName = rawBase || null;
|
|
85
|
-
const noUuid =
|
|
86
|
-
opts.noUuid ??
|
|
87
|
-
(String(env('SECURENOW_NO_UUID')) === '1' ||
|
|
88
|
-
String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
|
|
89
|
-
|
|
90
|
-
let serviceName;
|
|
91
|
-
if (baseName) {
|
|
92
|
-
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
93
|
-
} else {
|
|
94
|
-
serviceName = `nuxt-app-${uuidv4()}`;
|
|
95
|
-
console.warn(
|
|
96
|
-
'[securenow] ⚠️ No SECURENOW_APPID set. Using fallback: %s',
|
|
97
|
-
serviceName,
|
|
98
|
-
);
|
|
99
|
-
console.warn(
|
|
100
|
-
'[securenow] 💡 Set SECURENOW_APPID=your-app-name in .env for better tracking',
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const serviceInstanceId = `${baseName || 'securenow'}-${uuidv4()}`;
|
|
105
|
-
|
|
106
|
-
// ── Endpoints ──
|
|
107
|
-
const endpointBase = (
|
|
108
|
-
opts.endpoint ||
|
|
109
|
-
env('SECURENOW_INSTANCE') ||
|
|
110
|
-
env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
|
|
111
|
-
'https://freetrial.securenow.ai:4318'
|
|
112
|
-
).replace(/\/$/, '');
|
|
113
|
-
|
|
114
|
-
const tracesUrl =
|
|
115
|
-
env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
116
|
-
const logsUrl =
|
|
117
|
-
env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
118
|
-
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
119
|
-
|
|
120
|
-
// ── Resource ──
|
|
121
|
-
const resource = new Resource({
|
|
122
|
-
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
123
|
-
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
124
|
-
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
|
|
125
|
-
env('NODE_ENV') || 'production',
|
|
126
|
-
[SemanticResourceAttributes.SERVICE_VERSION]:
|
|
127
|
-
process.env.npm_package_version || undefined,
|
|
128
|
-
'framework': 'nuxt',
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ── Body capture config ──
|
|
132
|
-
const captureBody =
|
|
133
|
-
opts.captureBody ??
|
|
134
|
-
(String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
135
|
-
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true');
|
|
136
|
-
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
137
|
-
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '')
|
|
138
|
-
.split(',')
|
|
139
|
-
.map((s) => s.trim())
|
|
140
|
-
.filter(Boolean);
|
|
141
|
-
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
142
|
-
|
|
143
|
-
// ── HTTP instrumentation ──
|
|
144
|
-
const httpInstrumentation = new HttpInstrumentation({
|
|
145
|
-
requestHook: (span, request) => {
|
|
146
|
-
try {
|
|
147
|
-
const hdrs = request.headers || {};
|
|
148
|
-
const fwd = hdrs['x-forwarded-for'];
|
|
149
|
-
const clientIp =
|
|
150
|
-
(fwd ? String(fwd).split(',')[0].trim() : null) ||
|
|
151
|
-
hdrs['x-real-ip'] ||
|
|
152
|
-
hdrs['cf-connecting-ip'] ||
|
|
153
|
-
hdrs['x-client-ip'] ||
|
|
154
|
-
request.socket?.remoteAddress ||
|
|
155
|
-
'unknown';
|
|
156
|
-
|
|
157
|
-
span.setAttributes({
|
|
158
|
-
'http.client_ip': clientIp,
|
|
159
|
-
'http.user_agent': hdrs['user-agent'] || '',
|
|
160
|
-
'http.host': hdrs['x-forwarded-host'] || hdrs['host'] || '',
|
|
161
|
-
'http.scheme':
|
|
162
|
-
hdrs['x-forwarded-proto'] ||
|
|
163
|
-
(request.socket?.encrypted ? 'https' : 'http'),
|
|
164
|
-
'http.referer': hdrs['referer'] || '',
|
|
165
|
-
'http.origin': hdrs['origin'] || '',
|
|
166
|
-
'http.request_id':
|
|
167
|
-
hdrs['x-request-id'] || hdrs['x-trace-id'] || '',
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (hdrs['authorization']) {
|
|
171
|
-
span.setAttribute('http.security.auth_present', 'true');
|
|
172
|
-
}
|
|
173
|
-
if (hdrs['cookie']) {
|
|
174
|
-
span.setAttribute('http.security.cookies_present', 'true');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Body capture via stream listener (same approach as tracing.js)
|
|
178
|
-
if (
|
|
179
|
-
captureBody &&
|
|
180
|
-
request.method &&
|
|
181
|
-
['POST', 'PUT', 'PATCH'].includes(request.method)
|
|
182
|
-
) {
|
|
183
|
-
const ct = hdrs['content-type'] || '';
|
|
184
|
-
if (
|
|
185
|
-
ct.includes('application/json') ||
|
|
186
|
-
ct.includes('application/graphql') ||
|
|
187
|
-
ct.includes('application/x-www-form-urlencoded')
|
|
188
|
-
) {
|
|
189
|
-
const chunks = [];
|
|
190
|
-
let size = 0;
|
|
191
|
-
request.on('data', (chunk) => {
|
|
192
|
-
size += chunk.length;
|
|
193
|
-
if (size <= maxBodySize) chunks.push(chunk);
|
|
194
|
-
});
|
|
195
|
-
request.on('end', () => {
|
|
196
|
-
if (size > maxBodySize) {
|
|
197
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (chunks.length === 0) return;
|
|
201
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
202
|
-
try {
|
|
203
|
-
const parsed = JSON.parse(raw);
|
|
204
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
205
|
-
span.setAttributes({
|
|
206
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
207
|
-
'http.request.body.type': ct.includes('graphql') ? 'graphql' : 'json',
|
|
208
|
-
'http.request.body.size': size,
|
|
209
|
-
});
|
|
210
|
-
} catch {
|
|
211
|
-
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
212
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
// never break the request
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ── Trace exporter + SDK ──
|
|
224
|
-
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
225
|
-
|
|
226
|
-
const sdk = new NodeSDK({
|
|
227
|
-
traceExporter,
|
|
228
|
-
resource,
|
|
229
|
-
instrumentations: [httpInstrumentation],
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
sdk.start();
|
|
233
|
-
console.log('[securenow] 🚀 Nuxt OTel SDK started → %s', tracesUrl);
|
|
234
|
-
console.log(
|
|
235
|
-
'[securenow] service.name=%s instance.id=%s',
|
|
236
|
-
serviceName,
|
|
237
|
-
serviceInstanceId,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// ── Logging ──
|
|
241
|
-
const loggingEnabled =
|
|
242
|
-
opts.logging ??
|
|
243
|
-
(String(env('SECURENOW_LOGGING_ENABLED')) === '1' ||
|
|
244
|
-
String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true');
|
|
245
|
-
|
|
246
|
-
let loggerProvider = null;
|
|
247
|
-
|
|
248
|
-
if (loggingEnabled) {
|
|
249
|
-
try {
|
|
250
|
-
const { OTLPLogExporter } = await import(
|
|
251
|
-
'@opentelemetry/exporter-logs-otlp-http'
|
|
252
|
-
);
|
|
253
|
-
const { LoggerProvider, BatchLogRecordProcessor } = await import(
|
|
254
|
-
'@opentelemetry/sdk-logs'
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const logExporter = new OTLPLogExporter({ url: logsUrl, headers });
|
|
258
|
-
loggerProvider = new LoggerProvider({ resource });
|
|
259
|
-
loggerProvider.addLogRecordProcessor(
|
|
260
|
-
new BatchLogRecordProcessor(logExporter),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const logger = loggerProvider.getLogger('console', '1.0.0');
|
|
264
|
-
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
265
|
-
const orig = {
|
|
266
|
-
log: console.log,
|
|
267
|
-
info: console.info,
|
|
268
|
-
warn: console.warn,
|
|
269
|
-
error: console.error,
|
|
270
|
-
debug: console.debug,
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
function emitLog(sn, st, args) {
|
|
274
|
-
try {
|
|
275
|
-
const ctx = otelContext.active();
|
|
276
|
-
const spanCtx = otelTrace.getSpanContext(ctx);
|
|
277
|
-
logger.emit({
|
|
278
|
-
severityNumber: sn,
|
|
279
|
-
severityText: st,
|
|
280
|
-
body: args
|
|
281
|
-
.map((a) =>
|
|
282
|
-
typeof a === 'object' && a !== null ? JSON.stringify(a) : String(a),
|
|
283
|
-
)
|
|
284
|
-
.join(' '),
|
|
285
|
-
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
286
|
-
...(spanCtx && { context: ctx }),
|
|
287
|
-
});
|
|
288
|
-
} catch {
|
|
289
|
-
// swallow
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
console.log = (...a) => { emitLog(SEV.INFO, 'INFO', a); orig.log.apply(console, a); };
|
|
294
|
-
console.info = (...a) => { emitLog(SEV.INFO, 'INFO', a); orig.info.apply(console, a); };
|
|
295
|
-
console.warn = (...a) => { emitLog(SEV.WARN, 'WARN', a); orig.warn.apply(console, a); };
|
|
296
|
-
console.error = (...a) => { emitLog(SEV.ERROR, 'ERROR', a); orig.error.apply(console, a); };
|
|
297
|
-
console.debug = (...a) => { emitLog(SEV.DEBUG, 'DEBUG', a); orig.debug.apply(console, a); };
|
|
298
|
-
|
|
299
|
-
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
300
|
-
} catch (e) {
|
|
301
|
-
console.warn('[securenow] ⚠️ Logging setup failed:', e.message);
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
console.log(
|
|
305
|
-
'[securenow] 📋 Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)',
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (captureBody) {
|
|
310
|
-
console.log(
|
|
311
|
-
'[securenow] 📝 Body capture: ENABLED (max %d bytes, redacting %d fields)',
|
|
312
|
-
maxBodySize,
|
|
313
|
-
allSensitiveFields.length,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ── Free trial banner ──
|
|
318
|
-
try {
|
|
319
|
-
const { isFreeTrial, patchHttpForBanner } = await import('./free-trial-banner.js');
|
|
320
|
-
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
321
|
-
patchHttpForBanner();
|
|
322
|
-
}
|
|
323
|
-
} catch {
|
|
324
|
-
// not critical
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ── Graceful shutdown ──
|
|
328
|
-
const shutdown = async (sig) => {
|
|
329
|
-
try {
|
|
330
|
-
await sdk.shutdown?.();
|
|
331
|
-
if (loggerProvider) await loggerProvider.shutdown?.();
|
|
332
|
-
console.log(`[securenow] Shut down on ${sig}`);
|
|
333
|
-
} catch {
|
|
334
|
-
// swallow
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
338
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
339
|
-
|
|
340
|
-
// ── Nitro request hooks for span enrichment ──
|
|
341
|
-
const tracer = otelTrace.getTracer('securenow-nuxt', '1.0.0');
|
|
342
|
-
const spanMap = new WeakMap();
|
|
343
|
-
|
|
344
|
-
nitroApp.hooks.hook('request', (event) => {
|
|
345
|
-
try {
|
|
346
|
-
const req = event.node.req;
|
|
347
|
-
const method = event.method || req.method || 'GET';
|
|
348
|
-
const path = event.path || req.url || '/';
|
|
349
|
-
|
|
350
|
-
const span = tracer.startSpan(`${method} ${path}`, {
|
|
351
|
-
attributes: {
|
|
352
|
-
'http.method': method,
|
|
353
|
-
'http.target': path,
|
|
354
|
-
'http.url': `${req.headers?.['x-forwarded-proto'] || 'http'}://${req.headers?.host || 'localhost'}${path}`,
|
|
355
|
-
'component': 'nuxt-nitro',
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
spanMap.set(event, span);
|
|
360
|
-
} catch {
|
|
361
|
-
// never break the request
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
nitroApp.hooks.hook('afterResponse', (event) => {
|
|
366
|
-
try {
|
|
367
|
-
const span = spanMap.get(event);
|
|
368
|
-
if (!span) return;
|
|
369
|
-
|
|
370
|
-
const status = event.node.res.statusCode || 200;
|
|
371
|
-
span.setAttribute('http.status_code', status);
|
|
372
|
-
|
|
373
|
-
if (status >= 500) {
|
|
374
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
span.end();
|
|
378
|
-
spanMap.delete(event);
|
|
379
|
-
} catch {
|
|
380
|
-
// swallow
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
nitroApp.hooks.hook('error', (error, { event }) => {
|
|
385
|
-
try {
|
|
386
|
-
const span = event ? spanMap.get(event) : null;
|
|
387
|
-
if (span) {
|
|
388
|
-
span.recordException(error);
|
|
389
|
-
span.setStatus({
|
|
390
|
-
code: SpanStatusCode.ERROR,
|
|
391
|
-
message: error.message || 'Internal Server Error',
|
|
392
|
-
});
|
|
393
|
-
span.end();
|
|
394
|
-
spanMap.delete(event);
|
|
395
|
-
}
|
|
396
|
-
} catch {
|
|
397
|
-
// swallow
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Nitro Server Plugin
|
|
3
|
+
*
|
|
4
|
+
* Initialises the OpenTelemetry SDK and hooks into Nitro's request lifecycle
|
|
5
|
+
* to create spans, capture metadata, and forward logs.
|
|
6
|
+
*
|
|
7
|
+
* This file is registered by the Nuxt module (nuxt.mjs) via addServerPlugin.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
11
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
12
|
+
import { Resource } from '@opentelemetry/resources';
|
|
13
|
+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
14
|
+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
15
|
+
import {
|
|
16
|
+
context as otelContext,
|
|
17
|
+
trace as otelTrace,
|
|
18
|
+
SpanStatusCode,
|
|
19
|
+
} from '@opentelemetry/api';
|
|
20
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
21
|
+
|
|
22
|
+
// ── Helpers ──
|
|
23
|
+
|
|
24
|
+
const env = (k) =>
|
|
25
|
+
process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
26
|
+
|
|
27
|
+
const parseHeaders = (str) => {
|
|
28
|
+
const out = {};
|
|
29
|
+
if (!str) return out;
|
|
30
|
+
for (const raw of String(str).split(',')) {
|
|
31
|
+
const s = raw.trim();
|
|
32
|
+
if (!s) continue;
|
|
33
|
+
const i = s.indexOf('=');
|
|
34
|
+
if (i === -1) continue;
|
|
35
|
+
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
41
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
42
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
43
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function redactSensitiveData(obj, fields = DEFAULT_SENSITIVE_FIELDS) {
|
|
47
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
48
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
49
|
+
for (const key of Object.keys(redacted)) {
|
|
50
|
+
const lower = key.toLowerCase();
|
|
51
|
+
if (fields.some((f) => lower.includes(f.toLowerCase()))) {
|
|
52
|
+
redacted[key] = '[REDACTED]';
|
|
53
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
54
|
+
redacted[key] = redactSensitiveData(redacted[key], fields);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return redacted;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Runtime config helpers ──
|
|
61
|
+
|
|
62
|
+
function getRuntimeOptions() {
|
|
63
|
+
try {
|
|
64
|
+
const cfg = useRuntimeConfig();
|
|
65
|
+
return cfg.securenow || {};
|
|
66
|
+
} catch {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Plugin ──
|
|
72
|
+
|
|
73
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
74
|
+
const opts = getRuntimeOptions();
|
|
75
|
+
|
|
76
|
+
// ── Naming ──
|
|
77
|
+
const rawBase = (
|
|
78
|
+
opts.serviceName ||
|
|
79
|
+
env('OTEL_SERVICE_NAME') ||
|
|
80
|
+
env('SECURENOW_APPID') ||
|
|
81
|
+
''
|
|
82
|
+
).trim().replace(/^['"]|['"]$/g, '');
|
|
83
|
+
|
|
84
|
+
const baseName = rawBase || null;
|
|
85
|
+
const noUuid =
|
|
86
|
+
opts.noUuid ??
|
|
87
|
+
(String(env('SECURENOW_NO_UUID')) === '1' ||
|
|
88
|
+
String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
|
|
89
|
+
|
|
90
|
+
let serviceName;
|
|
91
|
+
if (baseName) {
|
|
92
|
+
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
93
|
+
} else {
|
|
94
|
+
serviceName = `nuxt-app-${uuidv4()}`;
|
|
95
|
+
console.warn(
|
|
96
|
+
'[securenow] ⚠️ No SECURENOW_APPID set. Using fallback: %s',
|
|
97
|
+
serviceName,
|
|
98
|
+
);
|
|
99
|
+
console.warn(
|
|
100
|
+
'[securenow] 💡 Set SECURENOW_APPID=your-app-name in .env for better tracking',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const serviceInstanceId = `${baseName || 'securenow'}-${uuidv4()}`;
|
|
105
|
+
|
|
106
|
+
// ── Endpoints ──
|
|
107
|
+
const endpointBase = (
|
|
108
|
+
opts.endpoint ||
|
|
109
|
+
env('SECURENOW_INSTANCE') ||
|
|
110
|
+
env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
|
|
111
|
+
'https://freetrial.securenow.ai:4318'
|
|
112
|
+
).replace(/\/$/, '');
|
|
113
|
+
|
|
114
|
+
const tracesUrl =
|
|
115
|
+
env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
116
|
+
const logsUrl =
|
|
117
|
+
env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
118
|
+
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
119
|
+
|
|
120
|
+
// ── Resource ──
|
|
121
|
+
const resource = new Resource({
|
|
122
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
123
|
+
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
124
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
|
|
125
|
+
env('NODE_ENV') || 'production',
|
|
126
|
+
[SemanticResourceAttributes.SERVICE_VERSION]:
|
|
127
|
+
process.env.npm_package_version || undefined,
|
|
128
|
+
'framework': 'nuxt',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Body capture config ──
|
|
132
|
+
const captureBody =
|
|
133
|
+
opts.captureBody ??
|
|
134
|
+
(String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
135
|
+
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true');
|
|
136
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
137
|
+
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '')
|
|
138
|
+
.split(',')
|
|
139
|
+
.map((s) => s.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
142
|
+
|
|
143
|
+
// ── HTTP instrumentation ──
|
|
144
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
145
|
+
requestHook: (span, request) => {
|
|
146
|
+
try {
|
|
147
|
+
const hdrs = request.headers || {};
|
|
148
|
+
const fwd = hdrs['x-forwarded-for'];
|
|
149
|
+
const clientIp =
|
|
150
|
+
(fwd ? String(fwd).split(',')[0].trim() : null) ||
|
|
151
|
+
hdrs['x-real-ip'] ||
|
|
152
|
+
hdrs['cf-connecting-ip'] ||
|
|
153
|
+
hdrs['x-client-ip'] ||
|
|
154
|
+
request.socket?.remoteAddress ||
|
|
155
|
+
'unknown';
|
|
156
|
+
|
|
157
|
+
span.setAttributes({
|
|
158
|
+
'http.client_ip': clientIp,
|
|
159
|
+
'http.user_agent': hdrs['user-agent'] || '',
|
|
160
|
+
'http.host': hdrs['x-forwarded-host'] || hdrs['host'] || '',
|
|
161
|
+
'http.scheme':
|
|
162
|
+
hdrs['x-forwarded-proto'] ||
|
|
163
|
+
(request.socket?.encrypted ? 'https' : 'http'),
|
|
164
|
+
'http.referer': hdrs['referer'] || '',
|
|
165
|
+
'http.origin': hdrs['origin'] || '',
|
|
166
|
+
'http.request_id':
|
|
167
|
+
hdrs['x-request-id'] || hdrs['x-trace-id'] || '',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (hdrs['authorization']) {
|
|
171
|
+
span.setAttribute('http.security.auth_present', 'true');
|
|
172
|
+
}
|
|
173
|
+
if (hdrs['cookie']) {
|
|
174
|
+
span.setAttribute('http.security.cookies_present', 'true');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Body capture via stream listener (same approach as tracing.js)
|
|
178
|
+
if (
|
|
179
|
+
captureBody &&
|
|
180
|
+
request.method &&
|
|
181
|
+
['POST', 'PUT', 'PATCH'].includes(request.method)
|
|
182
|
+
) {
|
|
183
|
+
const ct = hdrs['content-type'] || '';
|
|
184
|
+
if (
|
|
185
|
+
ct.includes('application/json') ||
|
|
186
|
+
ct.includes('application/graphql') ||
|
|
187
|
+
ct.includes('application/x-www-form-urlencoded')
|
|
188
|
+
) {
|
|
189
|
+
const chunks = [];
|
|
190
|
+
let size = 0;
|
|
191
|
+
request.on('data', (chunk) => {
|
|
192
|
+
size += chunk.length;
|
|
193
|
+
if (size <= maxBodySize) chunks.push(chunk);
|
|
194
|
+
});
|
|
195
|
+
request.on('end', () => {
|
|
196
|
+
if (size > maxBodySize) {
|
|
197
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (chunks.length === 0) return;
|
|
201
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(raw);
|
|
204
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
205
|
+
span.setAttributes({
|
|
206
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
207
|
+
'http.request.body.type': ct.includes('graphql') ? 'graphql' : 'json',
|
|
208
|
+
'http.request.body.size': size,
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
212
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// never break the request
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── Trace exporter + SDK ──
|
|
224
|
+
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
225
|
+
|
|
226
|
+
const sdk = new NodeSDK({
|
|
227
|
+
traceExporter,
|
|
228
|
+
resource,
|
|
229
|
+
instrumentations: [httpInstrumentation],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
sdk.start();
|
|
233
|
+
console.log('[securenow] 🚀 Nuxt OTel SDK started → %s', tracesUrl);
|
|
234
|
+
console.log(
|
|
235
|
+
'[securenow] service.name=%s instance.id=%s',
|
|
236
|
+
serviceName,
|
|
237
|
+
serviceInstanceId,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// ── Logging ──
|
|
241
|
+
const loggingEnabled =
|
|
242
|
+
opts.logging ??
|
|
243
|
+
(String(env('SECURENOW_LOGGING_ENABLED')) === '1' ||
|
|
244
|
+
String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true');
|
|
245
|
+
|
|
246
|
+
let loggerProvider = null;
|
|
247
|
+
|
|
248
|
+
if (loggingEnabled) {
|
|
249
|
+
try {
|
|
250
|
+
const { OTLPLogExporter } = await import(
|
|
251
|
+
'@opentelemetry/exporter-logs-otlp-http'
|
|
252
|
+
);
|
|
253
|
+
const { LoggerProvider, BatchLogRecordProcessor } = await import(
|
|
254
|
+
'@opentelemetry/sdk-logs'
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const logExporter = new OTLPLogExporter({ url: logsUrl, headers });
|
|
258
|
+
loggerProvider = new LoggerProvider({ resource });
|
|
259
|
+
loggerProvider.addLogRecordProcessor(
|
|
260
|
+
new BatchLogRecordProcessor(logExporter),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const logger = loggerProvider.getLogger('console', '1.0.0');
|
|
264
|
+
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
265
|
+
const orig = {
|
|
266
|
+
log: console.log,
|
|
267
|
+
info: console.info,
|
|
268
|
+
warn: console.warn,
|
|
269
|
+
error: console.error,
|
|
270
|
+
debug: console.debug,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
function emitLog(sn, st, args) {
|
|
274
|
+
try {
|
|
275
|
+
const ctx = otelContext.active();
|
|
276
|
+
const spanCtx = otelTrace.getSpanContext(ctx);
|
|
277
|
+
logger.emit({
|
|
278
|
+
severityNumber: sn,
|
|
279
|
+
severityText: st,
|
|
280
|
+
body: args
|
|
281
|
+
.map((a) =>
|
|
282
|
+
typeof a === 'object' && a !== null ? JSON.stringify(a) : String(a),
|
|
283
|
+
)
|
|
284
|
+
.join(' '),
|
|
285
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
286
|
+
...(spanCtx && { context: ctx }),
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
// swallow
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log = (...a) => { emitLog(SEV.INFO, 'INFO', a); orig.log.apply(console, a); };
|
|
294
|
+
console.info = (...a) => { emitLog(SEV.INFO, 'INFO', a); orig.info.apply(console, a); };
|
|
295
|
+
console.warn = (...a) => { emitLog(SEV.WARN, 'WARN', a); orig.warn.apply(console, a); };
|
|
296
|
+
console.error = (...a) => { emitLog(SEV.ERROR, 'ERROR', a); orig.error.apply(console, a); };
|
|
297
|
+
console.debug = (...a) => { emitLog(SEV.DEBUG, 'DEBUG', a); orig.debug.apply(console, a); };
|
|
298
|
+
|
|
299
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.warn('[securenow] ⚠️ Logging setup failed:', e.message);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
console.log(
|
|
305
|
+
'[securenow] 📋 Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (captureBody) {
|
|
310
|
+
console.log(
|
|
311
|
+
'[securenow] 📝 Body capture: ENABLED (max %d bytes, redacting %d fields)',
|
|
312
|
+
maxBodySize,
|
|
313
|
+
allSensitiveFields.length,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Free trial banner ──
|
|
318
|
+
try {
|
|
319
|
+
const { isFreeTrial, patchHttpForBanner } = await import('./free-trial-banner.js');
|
|
320
|
+
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
321
|
+
patchHttpForBanner();
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// not critical
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Graceful shutdown ──
|
|
328
|
+
const shutdown = async (sig) => {
|
|
329
|
+
try {
|
|
330
|
+
await sdk.shutdown?.();
|
|
331
|
+
if (loggerProvider) await loggerProvider.shutdown?.();
|
|
332
|
+
console.log(`[securenow] Shut down on ${sig}`);
|
|
333
|
+
} catch {
|
|
334
|
+
// swallow
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
338
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
339
|
+
|
|
340
|
+
// ── Nitro request hooks for span enrichment ──
|
|
341
|
+
const tracer = otelTrace.getTracer('securenow-nuxt', '1.0.0');
|
|
342
|
+
const spanMap = new WeakMap();
|
|
343
|
+
|
|
344
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
345
|
+
try {
|
|
346
|
+
const req = event.node.req;
|
|
347
|
+
const method = event.method || req.method || 'GET';
|
|
348
|
+
const path = event.path || req.url || '/';
|
|
349
|
+
|
|
350
|
+
const span = tracer.startSpan(`${method} ${path}`, {
|
|
351
|
+
attributes: {
|
|
352
|
+
'http.method': method,
|
|
353
|
+
'http.target': path,
|
|
354
|
+
'http.url': `${req.headers?.['x-forwarded-proto'] || 'http'}://${req.headers?.host || 'localhost'}${path}`,
|
|
355
|
+
'component': 'nuxt-nitro',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
spanMap.set(event, span);
|
|
360
|
+
} catch {
|
|
361
|
+
// never break the request
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
nitroApp.hooks.hook('afterResponse', (event) => {
|
|
366
|
+
try {
|
|
367
|
+
const span = spanMap.get(event);
|
|
368
|
+
if (!span) return;
|
|
369
|
+
|
|
370
|
+
const status = event.node.res.statusCode || 200;
|
|
371
|
+
span.setAttribute('http.status_code', status);
|
|
372
|
+
|
|
373
|
+
if (status >= 500) {
|
|
374
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
span.end();
|
|
378
|
+
spanMap.delete(event);
|
|
379
|
+
} catch {
|
|
380
|
+
// swallow
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
nitroApp.hooks.hook('error', (error, { event }) => {
|
|
385
|
+
try {
|
|
386
|
+
const span = event ? spanMap.get(event) : null;
|
|
387
|
+
if (span) {
|
|
388
|
+
span.recordException(error);
|
|
389
|
+
span.setStatus({
|
|
390
|
+
code: SpanStatusCode.ERROR,
|
|
391
|
+
message: error.message || 'Internal Server Error',
|
|
392
|
+
});
|
|
393
|
+
span.end();
|
|
394
|
+
spanMap.delete(event);
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// swallow
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|