securenow 4.0.6 → 4.0.9
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/README.md +4 -3
- package/cli.js +4 -1
- package/docs/ARCHITECTURE.md +408 -0
- package/{AUTO-BODY-CAPTURE.md → docs/AUTO-BODY-CAPTURE.md} +3 -0
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/{AUTO-SETUP.md → docs/AUTO-SETUP.md} +3 -0
- package/{AUTOMATIC-IP-CAPTURE.md → docs/AUTOMATIC-IP-CAPTURE.md} +3 -0
- package/{BODY-CAPTURE-FIX.md → docs/BODY-CAPTURE-FIX.md} +3 -0
- package/{BODY-CAPTURE-QUICKSTART.md → docs/BODY-CAPTURE-QUICKSTART.md} +147 -147
- package/docs/CHANGELOG-NEXTJS.md +235 -0
- package/docs/COMPLETION-REPORT.md +408 -0
- package/{EASIEST-SETUP.md → docs/EASIEST-SETUP.md} +3 -0
- package/docs/EXPRESS-BODY-CAPTURE.md +1027 -0
- package/{FINAL-SOLUTION.md → docs/FINAL-SOLUTION.md} +3 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +129 -0
- package/{NEXTJS-BODY-CAPTURE-COMPARISON.md → docs/NEXTJS-BODY-CAPTURE-COMPARISON.md} +3 -0
- package/docs/NEXTJS-WEBPACK-WARNINGS.md +267 -0
- package/{NEXTJS-WRAPPER-APPROACH.md → docs/NEXTJS-WRAPPER-APPROACH.md} +3 -0
- package/{QUICKSTART-BODY-CAPTURE.md → docs/QUICKSTART-BODY-CAPTURE.md} +3 -0
- package/{REDACTION-EXAMPLES.md → docs/REDACTION-EXAMPLES.md} +3 -0
- package/{REQUEST-BODY-CAPTURE.md → docs/REQUEST-BODY-CAPTURE.md} +575 -575
- package/{SOLUTION-SUMMARY.md → docs/SOLUTION-SUMMARY.md} +3 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +255 -0
- package/examples/README.md +3 -0
- package/examples/instrumentation-with-auto-capture.ts +3 -0
- package/examples/next.config.js +3 -0
- package/examples/nextjs-api-route-with-body-capture.ts +3 -0
- package/examples/nextjs-env-example.txt +3 -0
- package/examples/nextjs-instrumentation.js +3 -0
- package/examples/nextjs-instrumentation.ts +3 -0
- package/examples/nextjs-middleware.js +3 -0
- package/examples/nextjs-middleware.ts +3 -0
- package/examples/nextjs-with-options.ts +3 -0
- package/examples/test-nextjs-setup.js +3 -0
- package/nextjs-auto-capture.js +3 -0
- package/nextjs-middleware.js +3 -0
- package/nextjs-wrapper.js +3 -0
- package/nextjs.js +174 -72
- package/package.json +3 -19
- package/postinstall.js +310 -310
- package/tracing.js +287 -287
- /package/{CUSTOMER-GUIDE.md → docs/CUSTOMER-GUIDE.md} +0 -0
- /package/{NEXTJS-BODY-CAPTURE.md → docs/NEXTJS-BODY-CAPTURE.md} +0 -0
- /package/{NEXTJS-GUIDE.md → docs/NEXTJS-GUIDE.md} +0 -0
- /package/{NEXTJS-QUICKSTART.md → docs/NEXTJS-QUICKSTART.md} +0 -0
package/tracing.js
CHANGED
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Preload with: NODE_OPTIONS="-r securenow/register"
|
|
5
|
-
*
|
|
6
|
-
* Env:
|
|
7
|
-
* SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
|
|
8
|
-
* SECURENOW_NO_UUID=1 # one service.name across all workers
|
|
9
|
-
* SECURENOW_INSTANCE=http://host:4318 # OTLP/HTTP base (default http://46.62.173.237:4318)
|
|
10
|
-
* OTEL_EXPORTER_OTLP_ENDPOINT=... # alternative base
|
|
11
|
-
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
12
|
-
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
13
|
-
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
14
|
-
* OTEL_LOG_LEVEL=info|debug
|
|
15
|
-
* SECURENOW_TEST_SPAN=1
|
|
16
|
-
*
|
|
17
|
-
* Safety:
|
|
18
|
-
* SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
|
22
|
-
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
23
|
-
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
24
|
-
const { Resource } = require('@opentelemetry/resources');
|
|
25
|
-
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
26
|
-
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
27
|
-
const { v4: uuidv4 } = require('uuid');
|
|
28
|
-
|
|
29
|
-
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
30
|
-
const parseHeaders = str => {
|
|
31
|
-
const out = {}; if (!str) return out;
|
|
32
|
-
for (const raw of String(str).split(',')) {
|
|
33
|
-
const s = raw.trim(); if (!s) continue;
|
|
34
|
-
const i = s.indexOf('='); if (i === -1) continue;
|
|
35
|
-
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Default sensitive fields to redact from request bodies
|
|
41
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
42
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
43
|
-
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
44
|
-
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Redact sensitive fields from an object
|
|
49
|
-
*/
|
|
50
|
-
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
51
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
52
|
-
|
|
53
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
54
|
-
|
|
55
|
-
for (const key in redacted) {
|
|
56
|
-
const lowerKey = key.toLowerCase();
|
|
57
|
-
|
|
58
|
-
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
59
|
-
redacted[key] = '[REDACTED]';
|
|
60
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
61
|
-
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return redacted;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Redact sensitive data from GraphQL query strings
|
|
70
|
-
*/
|
|
71
|
-
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
72
|
-
if (!query || typeof query !== 'string') return query;
|
|
73
|
-
|
|
74
|
-
let redacted = query;
|
|
75
|
-
|
|
76
|
-
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
|
-
sensitiveFields.forEach(field => {
|
|
78
|
-
// Match patterns: field: "value" or field: 'value' or field:"value"
|
|
79
|
-
const patterns = [
|
|
80
|
-
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
81
|
-
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
patterns.forEach(pattern => {
|
|
85
|
-
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
86
|
-
if (suffix) {
|
|
87
|
-
return `${prefix}[REDACTED]${suffix}`;
|
|
88
|
-
} else {
|
|
89
|
-
return `${prefix}[REDACTED]`;
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return redacted;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// -------- diagnostics --------
|
|
99
|
-
(() => {
|
|
100
|
-
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
101
|
-
const level = L === 'debug' ? DiagLogLevel.DEBUG :
|
|
102
|
-
L === 'info' ? DiagLogLevel.INFO :
|
|
103
|
-
L === 'warn' ? DiagLogLevel.WARN :
|
|
104
|
-
L === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
|
|
105
|
-
diag.setLogger(new DiagConsoleLogger(), level);
|
|
106
|
-
console.log('[securenow] preload loaded pid=%d', process.pid);
|
|
107
|
-
})();
|
|
108
|
-
|
|
109
|
-
// -------- endpoints --------
|
|
110
|
-
const endpointBase = (env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'http://46.62.173.237:4318').replace(/\/$/, '');
|
|
111
|
-
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
112
|
-
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
113
|
-
|
|
114
|
-
// -------- naming rules --------
|
|
115
|
-
const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
|
|
116
|
-
const baseName = rawBase || null;
|
|
117
|
-
const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
|
|
118
|
-
const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
|
|
119
|
-
const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
|
|
120
|
-
|
|
121
|
-
// Fail fast in cluster if base is missing (no more "free" names)
|
|
122
|
-
if (!baseName && inPm2Cluster && strict) {
|
|
123
|
-
console.error('[securenow] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing in cluster (pid=%d). Exiting due to SECURENOW_STRICT=1.', process.pid);
|
|
124
|
-
// small delay so the log flushes
|
|
125
|
-
setTimeout(() => process.exit(1), 10);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// service.name
|
|
129
|
-
let serviceName;
|
|
130
|
-
if (baseName) {
|
|
131
|
-
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
132
|
-
} else {
|
|
133
|
-
// last-resort fallback (only if STRlCT is off). You can rename this to make it obvious in monitoring.
|
|
134
|
-
serviceName = `securenow-free-${uuidv4()}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// service.instance.id = <appid-or-fallback>-<uuid> (unique per worker)
|
|
138
|
-
const instancePrefix = baseName || 'securenow';
|
|
139
|
-
const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
|
|
140
|
-
|
|
141
|
-
// Loud line per worker to prove what was used
|
|
142
|
-
console.log('[securenow] pid=%d SECURENOW_APPID=%s OTEL_SERVICE_NAME=%s SECURENOW_NO_UUID=%s SECURENOW_STRICT=%s → service.name=%s instance.id=%s',
|
|
143
|
-
process.pid,
|
|
144
|
-
JSON.stringify(env('SECURENOW_APPID')),
|
|
145
|
-
JSON.stringify(env('OTEL_SERVICE_NAME')),
|
|
146
|
-
JSON.stringify(env('SECURENOW_NO_UUID')),
|
|
147
|
-
JSON.stringify(env('SECURENOW_STRICT')),
|
|
148
|
-
serviceName,
|
|
149
|
-
serviceInstanceId
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// -------- instrumentations --------
|
|
153
|
-
const disabledMap = {};
|
|
154
|
-
for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map(s => s.trim()).filter(Boolean)) {
|
|
155
|
-
disabledMap[n] = { enabled: false };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// -------- Body Capture Configuration --------
|
|
159
|
-
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
160
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
161
|
-
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
162
|
-
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
163
|
-
|
|
164
|
-
// Configure HTTP instrumentation with body capture
|
|
165
|
-
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
166
|
-
const httpInstrumentation = new HttpInstrumentation({
|
|
167
|
-
requestHook: (span, request) => {
|
|
168
|
-
try {
|
|
169
|
-
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
170
|
-
const contentType = request.headers['content-type'] || '';
|
|
171
|
-
|
|
172
|
-
if (contentType.includes('application/json') ||
|
|
173
|
-
contentType.includes('application/graphql') ||
|
|
174
|
-
contentType.includes('application/x-www-form-urlencoded')) {
|
|
175
|
-
|
|
176
|
-
let body = '';
|
|
177
|
-
const chunks = [];
|
|
178
|
-
let size = 0;
|
|
179
|
-
|
|
180
|
-
request.on('data', (chunk) => {
|
|
181
|
-
size += chunk.length;
|
|
182
|
-
if (size <= maxBodySize) {
|
|
183
|
-
chunks.push(chunk);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
request.on('end', () => {
|
|
188
|
-
if (size <= maxBodySize && chunks.length > 0) {
|
|
189
|
-
body = Buffer.concat(chunks).toString('utf8');
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
let redacted;
|
|
193
|
-
|
|
194
|
-
if (contentType.includes('application/graphql')) {
|
|
195
|
-
// GraphQL: redact query string
|
|
196
|
-
redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
197
|
-
span.setAttributes({
|
|
198
|
-
'http.request.body': redacted.substring(0, maxBodySize),
|
|
199
|
-
'http.request.body.type': 'graphql',
|
|
200
|
-
'http.request.body.size': size,
|
|
201
|
-
});
|
|
202
|
-
} else if (contentType.includes('application/json')) {
|
|
203
|
-
// JSON: parse and redact object
|
|
204
|
-
const parsed = JSON.parse(body);
|
|
205
|
-
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
206
|
-
span.setAttributes({
|
|
207
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
208
|
-
'http.request.body.type': 'json',
|
|
209
|
-
'http.request.body.size': size,
|
|
210
|
-
});
|
|
211
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
212
|
-
// Form: parse and redact
|
|
213
|
-
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
214
|
-
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
215
|
-
span.setAttributes({
|
|
216
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
217
|
-
'http.request.body.type': 'form',
|
|
218
|
-
'http.request.body.size': size,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
} catch (e) {
|
|
222
|
-
// Parse error: capture as-is (truncated)
|
|
223
|
-
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
224
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
225
|
-
}
|
|
226
|
-
} else if (size > maxBodySize) {
|
|
227
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
228
|
-
span.setAttribute('http.request.body.size', size);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
232
|
-
// Multipart is NOT captured
|
|
233
|
-
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
234
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
235
|
-
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
} catch (error) {
|
|
239
|
-
// Silently fail
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// -------- SDK --------
|
|
245
|
-
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
246
|
-
const sdk = new NodeSDK({
|
|
247
|
-
traceExporter,
|
|
248
|
-
instrumentations: [
|
|
249
|
-
httpInstrumentation,
|
|
250
|
-
...getNodeAutoInstrumentations({
|
|
251
|
-
...disabledMap,
|
|
252
|
-
'@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
|
|
253
|
-
}),
|
|
254
|
-
],
|
|
255
|
-
resource: new Resource({
|
|
256
|
-
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
257
|
-
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
258
|
-
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || 'production',
|
|
259
|
-
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
|
|
260
|
-
}),
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// -------- start / shutdown (sync/async safe) --------
|
|
264
|
-
(async () => {
|
|
265
|
-
try {
|
|
266
|
-
await Promise.resolve(sdk.start?.());
|
|
267
|
-
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
268
|
-
if (captureBody) {
|
|
269
|
-
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
270
|
-
}
|
|
271
|
-
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
272
|
-
const api = require('@opentelemetry/api');
|
|
273
|
-
const tracer = api.trace.getTracer('securenow-smoke');
|
|
274
|
-
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
275
|
-
}
|
|
276
|
-
} catch (e) {
|
|
277
|
-
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
278
|
-
}
|
|
279
|
-
})();
|
|
280
|
-
|
|
281
|
-
async function safeShutdown(sig) {
|
|
282
|
-
try { await Promise.resolve(sdk.shutdown?.()); console.log(`[securenow] Tracing terminated on ${sig}`); }
|
|
283
|
-
catch (e) { console.error('[securenow] Tracing shutdown error:', e); }
|
|
284
|
-
finally { process.exit(0); }
|
|
285
|
-
}
|
|
286
|
-
process.on('SIGINT', () => safeShutdown('SIGINT'));
|
|
287
|
-
process.on('SIGTERM', () => safeShutdown('SIGTERM'));
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preload with: NODE_OPTIONS="-r securenow/register"
|
|
5
|
+
*
|
|
6
|
+
* Env:
|
|
7
|
+
* SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
|
|
8
|
+
* SECURENOW_NO_UUID=1 # one service.name across all workers
|
|
9
|
+
* SECURENOW_INSTANCE=http://host:4318 # OTLP/HTTP base (default http://46.62.173.237:4318)
|
|
10
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT=... # alternative base
|
|
11
|
+
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
12
|
+
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
13
|
+
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
14
|
+
* OTEL_LOG_LEVEL=info|debug
|
|
15
|
+
* SECURENOW_TEST_SPAN=1
|
|
16
|
+
*
|
|
17
|
+
* Safety:
|
|
18
|
+
* SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
|
22
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
23
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
24
|
+
const { Resource } = require('@opentelemetry/resources');
|
|
25
|
+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
26
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
27
|
+
const { v4: uuidv4 } = require('uuid');
|
|
28
|
+
|
|
29
|
+
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
30
|
+
const parseHeaders = str => {
|
|
31
|
+
const out = {}; if (!str) return out;
|
|
32
|
+
for (const raw of String(str).split(',')) {
|
|
33
|
+
const s = raw.trim(); if (!s) continue;
|
|
34
|
+
const i = s.indexOf('='); if (i === -1) continue;
|
|
35
|
+
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Default sensitive fields to redact from request bodies
|
|
41
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
42
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
43
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
44
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Redact sensitive fields from an object
|
|
49
|
+
*/
|
|
50
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
51
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
52
|
+
|
|
53
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
54
|
+
|
|
55
|
+
for (const key in redacted) {
|
|
56
|
+
const lowerKey = key.toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
59
|
+
redacted[key] = '[REDACTED]';
|
|
60
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
61
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return redacted;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Redact sensitive data from GraphQL query strings
|
|
70
|
+
*/
|
|
71
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
72
|
+
if (!query || typeof query !== 'string') return query;
|
|
73
|
+
|
|
74
|
+
let redacted = query;
|
|
75
|
+
|
|
76
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
|
+
sensitiveFields.forEach(field => {
|
|
78
|
+
// Match patterns: field: "value" or field: 'value' or field:"value"
|
|
79
|
+
const patterns = [
|
|
80
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
81
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
patterns.forEach(pattern => {
|
|
85
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
86
|
+
if (suffix) {
|
|
87
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
88
|
+
} else {
|
|
89
|
+
return `${prefix}[REDACTED]`;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return redacted;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -------- diagnostics --------
|
|
99
|
+
(() => {
|
|
100
|
+
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
101
|
+
const level = L === 'debug' ? DiagLogLevel.DEBUG :
|
|
102
|
+
L === 'info' ? DiagLogLevel.INFO :
|
|
103
|
+
L === 'warn' ? DiagLogLevel.WARN :
|
|
104
|
+
L === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
|
|
105
|
+
diag.setLogger(new DiagConsoleLogger(), level);
|
|
106
|
+
console.log('[securenow] preload loaded pid=%d', process.pid);
|
|
107
|
+
})();
|
|
108
|
+
|
|
109
|
+
// -------- endpoints --------
|
|
110
|
+
const endpointBase = (env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'http://46.62.173.237:4318').replace(/\/$/, '');
|
|
111
|
+
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
112
|
+
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
113
|
+
|
|
114
|
+
// -------- naming rules --------
|
|
115
|
+
const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
|
|
116
|
+
const baseName = rawBase || null;
|
|
117
|
+
const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
|
|
118
|
+
const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
|
|
119
|
+
const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
|
|
120
|
+
|
|
121
|
+
// Fail fast in cluster if base is missing (no more "free" names)
|
|
122
|
+
if (!baseName && inPm2Cluster && strict) {
|
|
123
|
+
console.error('[securenow] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing in cluster (pid=%d). Exiting due to SECURENOW_STRICT=1.', process.pid);
|
|
124
|
+
// small delay so the log flushes
|
|
125
|
+
setTimeout(() => process.exit(1), 10);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// service.name
|
|
129
|
+
let serviceName;
|
|
130
|
+
if (baseName) {
|
|
131
|
+
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
132
|
+
} else {
|
|
133
|
+
// last-resort fallback (only if STRlCT is off). You can rename this to make it obvious in monitoring.
|
|
134
|
+
serviceName = `securenow-free-${uuidv4()}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// service.instance.id = <appid-or-fallback>-<uuid> (unique per worker)
|
|
138
|
+
const instancePrefix = baseName || 'securenow';
|
|
139
|
+
const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
|
|
140
|
+
|
|
141
|
+
// Loud line per worker to prove what was used
|
|
142
|
+
console.log('[securenow] pid=%d SECURENOW_APPID=%s OTEL_SERVICE_NAME=%s SECURENOW_NO_UUID=%s SECURENOW_STRICT=%s → service.name=%s instance.id=%s',
|
|
143
|
+
process.pid,
|
|
144
|
+
JSON.stringify(env('SECURENOW_APPID')),
|
|
145
|
+
JSON.stringify(env('OTEL_SERVICE_NAME')),
|
|
146
|
+
JSON.stringify(env('SECURENOW_NO_UUID')),
|
|
147
|
+
JSON.stringify(env('SECURENOW_STRICT')),
|
|
148
|
+
serviceName,
|
|
149
|
+
serviceInstanceId
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// -------- instrumentations --------
|
|
153
|
+
const disabledMap = {};
|
|
154
|
+
for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map(s => s.trim()).filter(Boolean)) {
|
|
155
|
+
disabledMap[n] = { enabled: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// -------- Body Capture Configuration --------
|
|
159
|
+
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
160
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
161
|
+
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
162
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
163
|
+
|
|
164
|
+
// Configure HTTP instrumentation with body capture
|
|
165
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
166
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
167
|
+
requestHook: (span, request) => {
|
|
168
|
+
try {
|
|
169
|
+
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
170
|
+
const contentType = request.headers['content-type'] || '';
|
|
171
|
+
|
|
172
|
+
if (contentType.includes('application/json') ||
|
|
173
|
+
contentType.includes('application/graphql') ||
|
|
174
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
175
|
+
|
|
176
|
+
let body = '';
|
|
177
|
+
const chunks = [];
|
|
178
|
+
let size = 0;
|
|
179
|
+
|
|
180
|
+
request.on('data', (chunk) => {
|
|
181
|
+
size += chunk.length;
|
|
182
|
+
if (size <= maxBodySize) {
|
|
183
|
+
chunks.push(chunk);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
request.on('end', () => {
|
|
188
|
+
if (size <= maxBodySize && chunks.length > 0) {
|
|
189
|
+
body = Buffer.concat(chunks).toString('utf8');
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let redacted;
|
|
193
|
+
|
|
194
|
+
if (contentType.includes('application/graphql')) {
|
|
195
|
+
// GraphQL: redact query string
|
|
196
|
+
redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
197
|
+
span.setAttributes({
|
|
198
|
+
'http.request.body': redacted.substring(0, maxBodySize),
|
|
199
|
+
'http.request.body.type': 'graphql',
|
|
200
|
+
'http.request.body.size': size,
|
|
201
|
+
});
|
|
202
|
+
} else if (contentType.includes('application/json')) {
|
|
203
|
+
// JSON: parse and redact object
|
|
204
|
+
const parsed = JSON.parse(body);
|
|
205
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
206
|
+
span.setAttributes({
|
|
207
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
208
|
+
'http.request.body.type': 'json',
|
|
209
|
+
'http.request.body.size': size,
|
|
210
|
+
});
|
|
211
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
212
|
+
// Form: parse and redact
|
|
213
|
+
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
214
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
215
|
+
span.setAttributes({
|
|
216
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
217
|
+
'http.request.body.type': 'form',
|
|
218
|
+
'http.request.body.size': size,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Parse error: capture as-is (truncated)
|
|
223
|
+
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
224
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
225
|
+
}
|
|
226
|
+
} else if (size > maxBodySize) {
|
|
227
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
228
|
+
span.setAttribute('http.request.body.size', size);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
232
|
+
// Multipart is NOT captured
|
|
233
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
234
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
235
|
+
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Silently fail
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// -------- SDK --------
|
|
245
|
+
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
246
|
+
const sdk = new NodeSDK({
|
|
247
|
+
traceExporter,
|
|
248
|
+
instrumentations: [
|
|
249
|
+
httpInstrumentation,
|
|
250
|
+
...getNodeAutoInstrumentations({
|
|
251
|
+
...disabledMap,
|
|
252
|
+
'@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
|
|
253
|
+
}),
|
|
254
|
+
],
|
|
255
|
+
resource: new Resource({
|
|
256
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
257
|
+
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
258
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || 'production',
|
|
259
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// -------- start / shutdown (sync/async safe) --------
|
|
264
|
+
(async () => {
|
|
265
|
+
try {
|
|
266
|
+
await Promise.resolve(sdk.start?.());
|
|
267
|
+
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
268
|
+
if (captureBody) {
|
|
269
|
+
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
270
|
+
}
|
|
271
|
+
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
272
|
+
const api = require('@opentelemetry/api');
|
|
273
|
+
const tracer = api.trace.getTracer('securenow-smoke');
|
|
274
|
+
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
280
|
+
|
|
281
|
+
async function safeShutdown(sig) {
|
|
282
|
+
try { await Promise.resolve(sdk.shutdown?.()); console.log(`[securenow] Tracing terminated on ${sig}`); }
|
|
283
|
+
catch (e) { console.error('[securenow] Tracing shutdown error:', e); }
|
|
284
|
+
finally { process.exit(0); }
|
|
285
|
+
}
|
|
286
|
+
process.on('SIGINT', () => safeShutdown('SIGINT'));
|
|
287
|
+
process.on('SIGTERM', () => safeShutdown('SIGTERM'));
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|